From ed8e50fbaa836a2de2c75f3fe7d243c48988f64f Mon Sep 17 00:00:00 2001 From: Pengfei Date: Sat, 11 Apr 2026 09:30:12 +0800 Subject: [PATCH 1/2] fix(cloudflare): lazy-init S3 client to support dev mode without R2 creds createS3Client(c.env) was called unconditionally at the top of the /uploads/presign handler, which throws "accessKeyId is a required option" when R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY are not set. The dev-mode proxy-put branch is supposed to avoid the S3 path entirely, but the upfront construction made dev-mode upload impossible without S3 creds. Move the createS3Client call into the else branch so it only runs in non-dev mode (where presigned direct-to-R2 upload is actually used). This unblocks staging deployments that intentionally run with ENVIRONMENT=development to avoid creating R2 S3 API tokens. Note: multipart upload routes (/uploads/multipart/part-url and /uploads/multipart/status) still construct the S3 client unconditionally. Those paths trigger only for files >= 100MB, so this fix is sufficient for the common single-file upload flow on staging. --- cloudflare/src/routes/upload.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cloudflare/src/routes/upload.ts b/cloudflare/src/routes/upload.ts index 1e23876..23fedd7 100644 --- a/cloudflare/src/routes/upload.ts +++ b/cloudflare/src/routes/upload.ts @@ -66,7 +66,9 @@ uploadRoutes.post('/uploads/presign', async (c) => { const db = c.get('db'); const user = c.get('user'); - const s3 = createS3Client(c.env); + // S3 client is only needed in non-dev mode (presigned direct-to-R2 upload). + // Lazy-construct inside the else branch below so dev mode (proxy-put) works + // without R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY secrets. const cleanExt = ext.replace(/^\./, ''); const sessionId = crypto.randomUUID(); const tempKey = `tmp/${crypto.randomUUID()}.${cleanExt}`; @@ -128,6 +130,7 @@ uploadRoutes.post('/uploads/presign', async (c) => { if (isDev) { presignedUrl = `/api/uploads/proxy-put/${sessionId}`; } else { + const s3 = createS3Client(c.env); presignedUrl = await generatePresignedPutUrl(s3, c.env, tempKey, { contentType: mimetype, }); From 34ae6e5fba8a4b3c14f4e4bffd19800db84ebd2e Mon Sep 17 00:00:00 2001 From: Pengfei Date: Sat, 11 Apr 2026 09:31:00 +0800 Subject: [PATCH 2/2] chore(cloudflare): gitignore wrangler.staging.toml and wrangler.local.toml These per-environment wrangler config files are local: they contain environment-specific D1/R2 ids, KV namespace ids and service binding targets that differ per Cloudflare account, and should never be committed to the upstream repo. The tracked wrangler.toml stays as the template; operators copy it to wrangler.staging.toml / wrangler.local.toml and fill in real values for their account. --- cloudflare/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloudflare/.gitignore b/cloudflare/.gitignore index b12db96..f5f80a8 100644 --- a/cloudflare/.gitignore +++ b/cloudflare/.gitignore @@ -7,6 +7,8 @@ public/ # Wrangler .wrangler/ .dev.vars +wrangler.staging.toml +wrangler.local.toml # Frontend frontend/node_modules/