1import type { R2Bucket } from "@cloudflare/workers-types";
2import type * as Cloudflare from "@/Cloudflare/index.ts";
3import { AwsClient } from "aws4fetch";
4
5interface Env {
6 BUCKET: R2Bucket;
7 /** `Cloudflare.R2.S3Credentials(bucket, { access: "read-write" })` — a JSON string. */
8 BUCKET_S3: string;
9}
10
11/**
12 * Async (non-Effect) Worker that presigns with `aws4fetch` over
13 * `Cloudflare.R2.S3Credentials`. Same routes as `routes.ts`, so the shared
14 * `presignRoundTrip` drives it; the same code runs in dev and deployed.
15 */
16export default {
17 async fetch(request: Request, env: Env): Promise<Response> {
18 const url = new URL(request.url);
19 const key = url.searchParams.get("key") ?? "";
20 const contentType = url.searchParams.get("contentType") ?? undefined;
21 switch (url.pathname) {
22 case "/presign-put":
23 return Response.json({
24 url: await presign(env, key, "PUT", {
25 headers: contentType ? { "content-type": contentType } : undefined,
26 }),
27 });
28 case "/presign-get":
29 return Response.json({
30 url: await presign(env, key, "GET", {
31 query: contentType
32 ? { "response-content-type": contentType }
33 : undefined,
34 }),
35 });
36 case "/read": {
37 const object = await env.BUCKET.get(key);
38 return Response.json({
39 value: object === null ? null : await object.text(),
40 contentType: object?.httpMetadata?.contentType ?? null,
41 });
42 }
43 case "/write":
44 await env.BUCKET.put(key, await request.text());
45 return new Response("ok");
46 default:
47 return new Response("ok");
48 }
49 },
50};
51
52const presign = async (
53 env: Env,
54 key: string,
55 method: "GET" | "PUT",
56 options: {
57 headers?: Record<string, string>;
58 query?: Record<string, string>;
59 },
60) => {
61 const s3: Cloudflare.R2.S3CredentialsValue = JSON.parse(env.BUCKET_S3);
62 const client = new AwsClient({ ...s3, service: "s3" });
63 const url = new URL(
64 `${s3.endpoint}/${encodeURIComponent(s3.bucketName)}/${key
65 .split("/")
66 .map(encodeURIComponent)
67 .join("/")}`,
68 );
69 for (const [name, value] of Object.entries(options.query ?? {})) {
70 url.searchParams.set(name, value);
71 }
72 url.searchParams.set("X-Amz-Expires", "900");
73 const signed = await client.sign(url.toString(), {
74 method,
75 headers: options.headers,
76 aws: { signQuery: true, allHeaders: options.headers !== undefined },
77 });
78 return signed.url;
79};