1import type * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import type { RuntimeContext } from "../../RuntimeContext.ts";
4import type { Bucket } from "./Bucket.ts";
5import type { PresignError } from "./BucketTypes.ts";
6
7export interface PresignPutObjectRequest {
8 /**
9 * Key of the object to mint a presigned upload URL for.
10 */
11 key: string;
12 /**
13 * Number of seconds the URL remains valid (at most 7 days).
14 * @default 900
15 */
16 expiresIn?: number;
17 /**
18 * `Content-Type` signed into the URL. The uploader must send exactly this
19 * `Content-Type` header or R2 rejects the upload with a signature
20 * mismatch. Omit to leave the Content-Type unconstrained.
21 */
22 contentType?: string;
23}
24
25/**
26 * Mint presigned upload (PUT) URLs for objects in an R2 {@link Bucket}, so a
27 * browser can upload directly to R2 without credentials and without routing
28 * the body through a Worker.
29 *
30 * Presigning is a pure SigV4 computation against R2's S3-compatible API — no
31 * request is made. When deployed, the binding mints a scoped account API
32 * token (`Workers R2 Storage Write`) and derives R2 S3 credentials from it;
33 * the URLs point at `https://{accountId}.r2.cloudflarestorage.com`.
34 *
35 * Under `alchemy dev`, a locally-emulated bucket is served on the Worker's
36 * local S3 endpoint (`{worker url}/cdn-cgi/local/r2/s3`) and the minted URLs
37 * point there — the same code runs unchanged in both modes, and objects
38 * uploaded locally are visible to every local binding of the bucket.
39 *
40 * Browser uploads to a deployed bucket also need a matching CORS rule on the
41 * {@link Bucket} (`cors` prop). The local endpoint always allows
42 * cross-origin requests.
43 *
44 * ### Presigning Upload URLs
45 * **Example:** Mint a presigned PUT URL
46 * ```typescript
47 * const presignPut = yield* Cloudflare.R2.PresignPutObject(bucket);
48 * const url = yield* presignPut({ key: "uploads/avatar.png" });
49 * // the browser uploads with: fetch(url, { method: "PUT", body: file })
50 * ```
51 *
52 * **Example:** Pin the uploaded Content-Type
53 * ```typescript
54 * const url = yield* presignPut({
55 * key: "uploads/avatar.png",
56 * expiresIn: 300, // valid for 5 minutes
57 * contentType: "image/png", // uploader must send Content-Type: image/png
58 * });
59 * ```
60 *
61 * **Example:** Allow browser uploads from a web app
62 * ```typescript
63 * const Uploads = Cloudflare.R2.Bucket("Uploads", {
64 * cors: [
65 * {
66 * allowedMethods: ["PUT"],
67 * allowedOrigins: ["https://app.example.com"],
68 * allowedHeaders: ["content-type"],
69 * },
70 * ],
71 * });
72 *
73 * export default class Api extends Cloudflare.Worker<Api>()(
74 * "Api",
75 * { main: import.meta.url },
76 * Effect.gen(function* () {
77 * const presignPut = yield* Cloudflare.R2.PresignPutObject(Uploads);
78 * return {
79 * fetch: Effect.gen(function* () {
80 * const url = yield* presignPut({ key: crypto.randomUUID() });
81 * return yield* HttpServerResponse.json({ url });
82 * }),
83 * };
84 * }).pipe(Effect.provide(Cloudflare.R2.PresignPutObjectToken)),
85 * ) {}
86 * ```
87 *
88 * @binding
89 * @product R2
90 * @category Storage & Databases
91 */
92export interface PresignPutObject extends Binding.Service<
93 PresignPutObject,
94 "Cloudflare.R2.PresignPutObject",
95 (
96 bucket: Bucket,
97 ) => Effect.Effect<
98 (
99 request: PresignPutObjectRequest,
100 ) => Effect.Effect<string, PresignError, RuntimeContext>
101 >
102> {}
103
104export const PresignPutObject = Binding.Service<PresignPutObject>(
105 "Cloudflare.R2.PresignPutObject",
106);