@better-s3/server 3.1045.2 → 3.1046.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/README.md +6 -2
  2. package/dist/adapters/next.js +54 -20
  3. package/dist/adapters/next.js.map +1 -1
  4. package/dist/api.d.ts +20 -3
  5. package/dist/handlers/multipart/list-parts.d.ts +2 -0
  6. package/dist/helpers/index.d.ts +0 -1
  7. package/dist/helpers/index.js +1 -9
  8. package/dist/helpers/index.js.map +1 -1
  9. package/dist/helpers/server/index.js +9 -3
  10. package/dist/helpers/server/index.js.map +1 -1
  11. package/dist/helpers/server/resolve-object-acl.d.ts +3 -3
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +65 -21
  14. package/dist/index.js.map +1 -1
  15. package/dist/types/delete-hook-context.d.ts +5 -0
  16. package/dist/types/download-hook-context.d.ts +6 -0
  17. package/dist/types/download-success-context.d.ts +5 -0
  18. package/dist/types/hook-context.d.ts +3 -0
  19. package/dist/types/index.d.ts +15 -0
  20. package/dist/types/multipart-complete-success-context.d.ts +13 -0
  21. package/dist/types/multipart-hook-context.d.ts +7 -0
  22. package/dist/types/multipart-init-success-context.d.ts +8 -0
  23. package/dist/types/multipart-list-success-context.d.ts +11 -0
  24. package/dist/types/s3-handler-config.d.ts +66 -0
  25. package/dist/types/s3-handler.d.ts +1 -0
  26. package/dist/types/s3-handlers.d.ts +16 -0
  27. package/dist/types/s3-route-handler-config.d.ts +4 -0
  28. package/dist/types/upload-complete-context.d.ts +15 -0
  29. package/dist/types/upload-hook-context.d.ts +10 -0
  30. package/dist/types/upload-success-context.d.ts +5 -0
  31. package/dist/types.d.ts +1 -214
  32. package/package.json +1 -1
  33. package/dist/helpers/build-public-url.d.ts +0 -13
package/dist/index.js CHANGED
@@ -56,14 +56,6 @@ function buildContentDisposition(fileName) {
56
56
  return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
57
57
  }
58
58
 
59
- // src/helpers/build-public-url.ts
60
- function buildPublicUrl(publicUrlBase, bucket, key, acl) {
61
- if (acl !== "public-read" || !publicUrlBase) return void 0;
62
- const base = publicUrlBase(bucket);
63
- if (!base) return void 0;
64
- return `${base.replace(/\/$/, "")}/${key}`;
65
- }
66
-
67
59
  // src/helpers/parse-file-name.ts
68
60
  function parseFileName(contentDisposition) {
69
61
  if (!contentDisposition) return void 0;
@@ -197,17 +189,23 @@ function createUploadHandler(config) {
197
189
  });
198
190
  });
199
191
  }
200
- async function resolveObjectAcl(s3, bucket, key) {
192
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
193
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
194
+ const controller = new AbortController();
195
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
201
196
  try {
202
197
  const result = await s3.send(
203
- new GetObjectAclCommand({ Bucket: bucket, Key: key })
198
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
199
+ { abortSignal: controller.signal }
204
200
  );
205
201
  const isPublic = result.Grants?.some(
206
202
  (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
207
203
  );
208
204
  return isPublic ? "public-read" : "private";
209
205
  } catch {
210
- return "private";
206
+ return void 0;
207
+ } finally {
208
+ clearTimeout(timeout);
211
209
  }
212
210
  }
213
211
 
@@ -233,8 +231,7 @@ function createConfirmHandler(config) {
233
231
  const head = await config.s3.send(
234
232
  new HeadObjectCommand({ Bucket: bucket, Key: key })
235
233
  );
236
- const acl = await resolveObjectAcl(config.s3, bucket, key);
237
- const publicUrl = buildPublicUrl(config.resolvePublicUrl, bucket, key, acl);
234
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
238
235
  const fileName = parseFileName(head.ContentDisposition);
239
236
  const context = {
240
237
  request,
@@ -246,7 +243,6 @@ function createConfirmHandler(config) {
246
243
  metadata: head.Metadata,
247
244
  acl,
248
245
  fileName,
249
- publicUrl,
250
246
  versionId: head.VersionId,
251
247
  lastModified: head.LastModified?.toISOString()
252
248
  };
@@ -260,7 +256,6 @@ function createConfirmHandler(config) {
260
256
  metadata: context.metadata ?? {},
261
257
  acl,
262
258
  fileName,
263
- publicUrl,
264
259
  versionId: context.versionId,
265
260
  lastModified: context.lastModified
266
261
  });
@@ -505,8 +500,7 @@ function createMultipartCompleteHandler(config) {
505
500
  const metadata = head.Metadata ?? {};
506
501
  const versionId = head.VersionId;
507
502
  const lastModified = head.LastModified?.toISOString();
508
- const acl = await resolveObjectAcl(config.s3, bucket, key);
509
- const publicUrl = buildPublicUrl(config.resolvePublicUrl, bucket, key, acl);
503
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
510
504
  const fileName = parseFileName(head.ContentDisposition);
511
505
  await config.multipart?.onComplete?.({
512
506
  request,
@@ -519,7 +513,6 @@ function createMultipartCompleteHandler(config) {
519
513
  metadata,
520
514
  acl,
521
515
  fileName,
522
- publicUrl,
523
516
  versionId,
524
517
  lastModified
525
518
  });
@@ -533,7 +526,6 @@ function createMultipartCompleteHandler(config) {
533
526
  metadata,
534
527
  acl,
535
528
  fileName,
536
- publicUrl,
537
529
  versionId,
538
530
  lastModified
539
531
  });
@@ -575,6 +567,45 @@ function createMultipartAbortHandler(config) {
575
567
  return Response.json({ bucket, key, uploadId, aborted: true });
576
568
  });
577
569
  }
570
+ function createMultipartListPartsHandler(config) {
571
+ return withS3ErrorHandler(async (request) => {
572
+ const url = new URL(request.url);
573
+ const key = url.searchParams.get("key")?.trim() ?? "";
574
+ const uploadId = url.searchParams.get("uploadId")?.trim() ?? "";
575
+ const bucketParam = url.searchParams.get("bucket")?.trim();
576
+ if (!key) {
577
+ return Response.json({ message: "key is required" }, { status: 400 });
578
+ }
579
+ if (!uploadId) {
580
+ return Response.json(
581
+ { message: "uploadId is required" },
582
+ { status: 400 }
583
+ );
584
+ }
585
+ const bucket = bucketParam || config.defaultBucket;
586
+ const guardResult = await runHook(config.multipart?.listGuard, {
587
+ request,
588
+ key,
589
+ bucket,
590
+ uploadId
591
+ });
592
+ if (guardResult) return guardResult;
593
+ const response = await config.s3.send(
594
+ new ListPartsCommand({
595
+ Bucket: bucket,
596
+ Key: key,
597
+ UploadId: uploadId
598
+ })
599
+ );
600
+ const parts = (response.Parts ?? []).map((p) => ({
601
+ partNumber: p.PartNumber ?? 0,
602
+ size: p.Size ?? 0,
603
+ eTag: (p.ETag ?? "").replace(/"/g, "")
604
+ }));
605
+ await config.multipart?.onList?.({ request, key, bucket, uploadId, parts });
606
+ return Response.json({ parts });
607
+ });
608
+ }
578
609
 
579
610
  // src/create-handlers.ts
580
611
  function createHandlers(config) {
@@ -588,7 +619,8 @@ function createHandlers(config) {
588
619
  init: createMultipartInitHandler(config),
589
620
  part: createMultipartPartHandler(config),
590
621
  complete: createMultipartCompleteHandler(config),
591
- abort: createMultipartAbortHandler(config)
622
+ abort: createMultipartAbortHandler(config),
623
+ listParts: createMultipartListPartsHandler(config)
592
624
  },
593
625
  delete: createDeleteHandler(config)
594
626
  };
@@ -621,6 +653,8 @@ function createRouter(config) {
621
653
  return config.multipart?.enabled ? handlers.multipart.complete(request) : disabled();
622
654
  if (method === "POST" && subpath === "presign/multipart/abort")
623
655
  return config.multipart?.enabled ? handlers.multipart.abort(request) : disabled();
656
+ if (method === "GET" && subpath === "presign/multipart/parts")
657
+ return config.multipart?.enabled ? handlers.multipart.listParts(request) : disabled();
624
658
  return Response.json({ message: "Not Found" }, { status: 404 });
625
659
  };
626
660
  }
@@ -681,6 +715,16 @@ function createS3Api(basePath = "/api/s3") {
681
715
  payload
682
716
  );
683
717
  },
718
+ listParts(payload) {
719
+ const params = new URLSearchParams({
720
+ key: payload.key,
721
+ uploadId: payload.uploadId
722
+ });
723
+ if (payload.bucket) params.set("bucket", payload.bucket);
724
+ return json(
725
+ `${base}/presign/multipart/parts?${params}`
726
+ );
727
+ },
684
728
  complete(payload) {
685
729
  return post(`${base}/presign/multipart/complete`, payload);
686
730
  },
@@ -721,6 +765,6 @@ function validateFile(file, options) {
721
765
  return null;
722
766
  }
723
767
 
724
- export { createConfirmHandler, createDeleteHandler, createDownloadHandler, createHandlers, createMultipartAbortHandler, createMultipartCompleteHandler, createMultipartInitHandler, createMultipartPartHandler, createS3Api as createPresignApi, createRouter, createS3Api, createUploadHandler, validateFile };
768
+ export { createConfirmHandler, createDeleteHandler, createDownloadHandler, createHandlers, createMultipartAbortHandler, createMultipartCompleteHandler, createMultipartInitHandler, createMultipartListPartsHandler, createMultipartPartHandler, createS3Api as createPresignApi, createRouter, createS3Api, createUploadHandler, validateFile };
725
769
  //# sourceMappingURL=index.js.map
726
770
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/internal-helpers.ts","../src/helpers/build-content-disposition.ts","../src/helpers/build-public-url.ts","../src/helpers/parse-file-name.ts","../src/handlers/presign/upload.ts","../src/helpers/server/resolve-object-acl.ts","../src/handlers/presign/confirm.ts","../src/handlers/presign/download.ts","../src/handlers/delete.ts","../src/handlers/multipart/init.ts","../src/handlers/multipart/part.ts","../src/handlers/multipart/complete.ts","../src/handlers/multipart/abort.ts","../src/create-handlers.ts","../src/router.ts","../src/api.ts","../src/helpers/validate-file.ts"],"names":["url","HeadObjectCommand","getSignedUrl"],"mappings":"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","file":"index.js","sourcesContent":["export async function parseBody<T extends Record<string, unknown>>(\n request: Request,\n): Promise<T | null> {\n try {\n const body = await request.json();\n return body && typeof body === \"object\" ? (body as T) : null;\n } catch {\n return null;\n }\n}\n\nexport function requireString(value: unknown, name: string): string | Response {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) {\n return Response.json({ message: `${name} is required` }, { status: 400 });\n }\n return trimmed;\n}\n\nexport function normalizeExpiresIn(value: unknown): number {\n const n = Number(value);\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;\n}\n\nexport function withS3ErrorHandler(\n handler: (request: Request) => Promise<Response>,\n) {\n return async (request: Request) => {\n try {\n return await handler(request);\n } catch (err) {\n const code = (err as { code?: string }).code;\n const isNetworkError =\n code === \"EAI_AGAIN\" ||\n code === \"ECONNREFUSED\" ||\n code === \"ECONNRESET\" ||\n code === \"ETIMEDOUT\" ||\n code === \"ENOTFOUND\";\n\n const message = isNetworkError\n ? `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`\n : err instanceof Error\n ? err.message\n : \"Internal server error\";\n\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 502 });\n }\n };\n}\n\n/**\n * Run a server hook. Returns a Response if the hook rejects (throws),\n * or `null` if the hook passes (or is undefined).\n */\nexport async function runHook<T>(\n hook: ((context: T) => Promise<void> | void) | undefined,\n context: T,\n): Promise<Response | null> {\n if (!hook) return null;\n try {\n await hook(context);\n return null;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Forbidden\";\n const status =\n typeof (err as Record<string, unknown>)?.status === \"number\"\n ? ((err as Record<string, unknown>).status as number)\n : 403;\n return Response.json({ message }, { status });\n }\n}\n","/**\n * Builds a RFC 6266 `Content-Disposition` value for a file attachment.\n *\n * Includes both the ASCII-safe `filename` fallback and the RFC 5987\n * `filename*` parameter for full Unicode support, so the original file\n * name (including non-ASCII characters) is preserved across all browsers\n * and HTTP clients.\n *\n * @example\n * buildContentDisposition(\"résumé finale.pdf\")\n * // → 'attachment; filename=\"resume finale.pdf\"; filename*=UTF-8\\'\\'r%C3%A9sum%C3%A9%20finale.pdf'\n */\nexport function buildContentDisposition(fileName: string): string {\n const ascii = fileName.replace(/[^\\x20-\\x7E]/g, \"_\").replace(/[\"\\\\]/g, \"_\");\n const encoded = encodeURIComponent(fileName);\n return `attachment; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\n}\n","/**\n * Builds the public URL for an S3 object.\n *\n * Returns `undefined` when:\n * - the object is `\"private\"`,\n * - `publicUrlBase` is not provided, or\n * - `publicUrlBase` returns `undefined` for the given bucket.\n *\n * @param publicUrlBase - A function that returns the base URL for a given bucket,\n * or `undefined` when no public URL is available for that bucket.\n * Matches the `resolvePublicUrl` field of `S3HandlerConfig`.\n */\nexport function buildPublicUrl(\n publicUrlBase: ((bucket: string) => string | undefined) | undefined,\n bucket: string,\n key: string,\n acl: \"public-read\" | \"private\",\n): string | undefined {\n if (acl !== \"public-read\" || !publicUrlBase) return undefined;\n const base = publicUrlBase(bucket);\n if (!base) return undefined;\n return `${base.replace(/\\/$/, \"\")}/${key}`;\n}\n","/**\n * Parses the display file name from a `Content-Disposition` header value.\n *\n * Prefers the RFC 5987 `filename*=UTF-8''…` encoding (full Unicode) over\n * the plain ASCII `filename=\"…\"` fallback.\n *\n * Returns `undefined` when no filename can be extracted.\n */\nexport function parseFileName(\n contentDisposition: string | undefined,\n): string | undefined {\n if (!contentDisposition) return undefined;\n\n const utf8Match = contentDisposition.match(/filename\\*=UTF-8''([^;\\s]+)/i);\n if (utf8Match) {\n try {\n return decodeURIComponent(utf8Match[1]);\n } catch {\n /* ignore malformed encoding */\n }\n }\n\n const asciiMatch = contentDisposition.match(/filename=\"([^\"]*)\"/i);\n return asciiMatch?.[1];\n}\n","import { createPresignedPost } from \"@aws-sdk/s3-presigned-post\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { PutObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\ntype Payload = {\n key: string;\n contentType?: string;\n /**\n * Exact byte size of the file the client intends to upload.\n * When provided the presigned POST policy locks `content-length-range` to\n * `[fileSize, fileSize]`, so S3 rejects any upload that is not exactly this\n * size — even if the client tampers with the payload.\n * When omitted the range is `[1, maxFileSize ?? 5 GiB]`.\n */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n expiresIn?: number;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition: attachment; filename=\"...\"` on the S3 object. */\n fileName?: string;\n};\n\nexport function createUploadHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(body.expiresIn);\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n const contentType = body.contentType?.trim() || \"application/octet-stream\";\n const fileSize =\n typeof body.fileSize === \"number\" && body.fileSize > 0\n ? Math.floor(body.fileSize)\n : null;\n\n const guardResult = await runHook(config.upload?.presignGuard, {\n request,\n key,\n bucket,\n contentType: body.contentType,\n fileSize: fileSize ?? undefined,\n metadata: body.metadata,\n acl,\n });\n if (guardResult) return guardResult;\n\n const method = config.upload?.method ?? \"post\";\n\n // Reject the request when size enforcement is mandatory but the client\n // did not declare a file size. Only relevant for presigned PUT — presigned\n // POST already enforces size via content-length-range policy regardless.\n if (\n method === \"put\" &&\n config.upload?.requireFileSize &&\n fileSize === null\n ) {\n return Response.json(\n {\n message:\n \"fileSize is required when upload.requireFileSize is enabled\",\n },\n { status: 400 },\n );\n }\n\n if (method === \"put\") {\n // Presigned PUT: only Content-Type and Content-Disposition are included\n // as signed headers. x-amz-meta-* headers are intentionally excluded.\n //\n // In browser environments, sending x-amz-meta-* on a cross-origin XHR\n // triggers a CORS preflight. Most S3/R2 CORS configs do not include\n // x-amz-meta-* in AllowedHeaders, so the preflight fails and the upload\n // never reaches S3. Content-Disposition is a standard header that is\n // widely allowed in CORS configs.\n //\n // If you need metadata on the object, use method: \"post\" (presigned POST\n // embeds metadata inside the signed form policy, which S3 applies at\n // the storage layer without CORS header constraints).\n // The metadata value from the request body is still forwarded to the\n // onPresigned hook for server-side use (e.g. storing in a database).\n const putHeaders: Record<string, string> = {\n \"Content-Type\": contentType,\n };\n if (body.fileName) {\n putHeaders[\"Content-Disposition\"] = buildContentDisposition(\n body.fileName,\n );\n }\n\n // When fileSize is known, bind the HMAC signature to the exact byte count.\n // S3 enforces Content-Length against the signature value and rejects any\n // PUT whose body size differs with SignatureDoesNotMatch — even when the\n // caller has the URL. Browsers set Content-Length automatically from the\n // body, so no manual header is required on the client side.\n const url = await getSignedUrl(\n config.s3,\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: contentType,\n ACL: acl,\n ...(body.fileName\n ? { ContentDisposition: buildContentDisposition(body.fileName) }\n : {}),\n ...(fileSize !== null ? { ContentLength: fileSize } : {}),\n }),\n {\n expiresIn,\n // Force content-length into X-Amz-SignedHeaders. Without this the\n // SDK omits it from the signed header set and size enforcement is lost.\n ...(fileSize !== null\n ? { signableHeaders: new Set([\"content-length\"]) }\n : {}),\n },\n );\n\n await config.upload?.onPresigned?.({\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n url,\n expiresIn,\n });\n\n return Response.json({\n bucket,\n key,\n url,\n headers: putHeaders,\n expiresIn,\n method: \"put\",\n });\n }\n\n // Presigned POST: policy fields are embedded in the signed form and must\n // be appended to FormData before the file. S3 enforces them at the storage layer.\n const fields: Record<string, string> = { acl, \"Content-Type\": contentType };\n\n if (body.fileName) {\n fields[\"Content-Disposition\"] = buildContentDisposition(body.fileName);\n }\n\n if (body.metadata) {\n for (const [k, v] of Object.entries(body.metadata)) {\n fields[`x-amz-meta-${k}`] = v;\n }\n }\n\n const rangeMin = fileSize ?? 1;\n const rangeMax = fileSize ?? undefined;\n\n const { url, fields: signedFields } = await createPresignedPost(config.s3, {\n Bucket: bucket,\n Key: key,\n Conditions:\n rangeMax !== undefined\n ? [[\"content-length-range\", rangeMin, rangeMax]]\n : [[\"content-length-range\", rangeMin, Number.MAX_SAFE_INTEGER]],\n Fields: fields,\n Expires: expiresIn,\n });\n\n await config.upload?.onPresigned?.({\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n url,\n expiresIn,\n });\n\n return Response.json({\n bucket,\n key,\n url,\n fields: signedFields,\n expiresIn,\n method: \"post\",\n });\n });\n}\n","import { GetObjectAclCommand, type S3Client } from \"@aws-sdk/client-s3\";\n\n/**\n * Determines the ACL of an S3 object by querying `GetObjectAcl`.\n *\n * Returns `\"public-read\"` when the `AllUsers` grantee has `READ` or\n * `FULL_CONTROL` permission; falls back to `\"private\"` when the provider\n * does not support the operation (e.g. Cloudflare R2).\n */\nexport async function resolveObjectAcl(\n s3: S3Client,\n bucket: string,\n key: string,\n): Promise<\"public-read\" | \"private\"> {\n try {\n const result = await s3.send(\n new GetObjectAclCommand({ Bucket: bucket, Key: key }),\n );\n const isPublic = result.Grants?.some(\n (g) =>\n g.Grantee?.URI === \"http://acs.amazonaws.com/groups/global/AllUsers\" &&\n (g.Permission === \"READ\" || g.Permission === \"FULL_CONTROL\"),\n );\n return isPublic ? \"public-read\" : \"private\";\n } catch {\n return \"private\";\n }\n}\n","import { HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildPublicUrl, parseFileName } from \"@/helpers\";\nimport { resolveObjectAcl } from \"@/helpers/server\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n};\n\nexport function createConfirmHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.upload?.confirmGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n\n const acl = await resolveObjectAcl(config.s3, bucket, key);\n const publicUrl = buildPublicUrl(config.resolvePublicUrl, bucket, key, acl);\n const fileName = parseFileName(head.ContentDisposition);\n\n const context = {\n request,\n key,\n bucket,\n contentType: head.ContentType,\n contentLength: head.ContentLength ?? 0,\n eTag: head.ETag?.replace(/\"/g, \"\"),\n metadata: head.Metadata,\n acl,\n fileName,\n publicUrl,\n versionId: head.VersionId,\n lastModified: head.LastModified?.toISOString(),\n };\n\n await config.upload?.onUploadConfirmed?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n metadata: context.metadata ?? {},\n acl,\n fileName,\n publicUrl,\n versionId: context.versionId,\n lastModified: context.lastModified,\n });\n });\n}\n","import { GetObjectCommand, HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\nexport function createDownloadHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const { searchParams } = new URL(request.url);\n const key = searchParams.get(\"key\")?.trim();\n if (!key) {\n return Response.json(\n { message: \"key query parameter is required\" },\n { status: 400 },\n );\n }\n\n const bucket = searchParams.get(\"bucket\")?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(searchParams.get(\"expiresIn\"));\n const fileName = searchParams.get(\"fileName\")?.trim();\n\n const guardResult = await runHook(config.download?.presignGuard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n try {\n await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));\n } catch (err: unknown) {\n const name = (err as { name?: string })?.name;\n if (name === \"NoSuchKey\" || name === \"NotFound\") {\n return Response.json({ message: \"Object not found\" }, { status: 404 });\n }\n throw err;\n }\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ResponseContentDisposition: fileName\n ? buildContentDisposition(fileName)\n : \"attachment\",\n }),\n { expiresIn },\n );\n\n await config.download?.onPresigned?.({\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n url,\n expiresIn,\n });\n\n return Response.json({ bucket, key, url, expiresIn });\n });\n}\n","import { DeleteObjectCommand, HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../types\";\nimport { runHook, withS3ErrorHandler } from \"../internal-helpers\";\n\nexport function createDeleteHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const { searchParams } = new URL(request.url);\n const key = searchParams.get(\"key\")?.trim();\n if (!key) {\n return Response.json(\n { message: \"key query parameter is required\" },\n { status: 400 },\n );\n }\n\n const bucket = searchParams.get(\"bucket\")?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.delete?.deleteGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n try {\n await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));\n } catch {\n return Response.json({ message: \"Object not found\" }, { status: 404 });\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.delete?.onDeleted?.({ request, key, bucket });\n\n return Response.json({ success: true, bucket, key });\n });\n}\n","import { CreateMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n contentType?: string;\n /** Declared total byte size of the file being uploaded. */\n fileSize?: number;\n metadata?: Record<string, string>;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition: attachment; filename=\"...\"` on the S3 object. */\n fileName?: string;\n};\n\nexport function createMultipartInitHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n const fileSize =\n typeof body.fileSize === \"number\" && body.fileSize > 0\n ? Math.floor(body.fileSize)\n : undefined;\n\n // Reject the request when size enforcement is mandatory but the client\n // did not declare a file size. The react client always sends file.size,\n // so this only blocks requests that deliberately omit the field.\n if (config.multipart?.requireFileSize && fileSize === undefined) {\n return Response.json(\n {\n message:\n \"fileSize is required when multipart.requireFileSize is enabled\",\n },\n { status: 400 },\n );\n }\n\n const guardResult = await runHook(config.multipart?.initGuard, {\n request,\n key,\n bucket,\n fileSize,\n });\n if (guardResult) return guardResult;\n\n const { UploadId } = await config.s3.send(\n new CreateMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n ContentType: body.contentType,\n ContentDisposition: body.fileName\n ? buildContentDisposition(body.fileName)\n : undefined,\n Metadata: body.metadata,\n ACL: acl,\n }),\n );\n\n await config.multipart?.onInit?.({\n request,\n key,\n bucket,\n uploadId: UploadId!,\n contentType: body.contentType,\n fileSize,\n metadata: body.metadata,\n acl,\n });\n\n return Response.json({ bucket, key, uploadId: UploadId }, { status: 201 });\n });\n}\n","import { UploadPartCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: number;\n /**\n * Exact byte size of the part being uploaded.\n * When provided, Content-Length is included in the HMAC signature so S3\n * rejects any UploadPart request whose body size does not match — preventing\n * a client from substituting a larger or different blob for a signed URL.\n */\n partSize?: number;\n bucket?: string;\n expiresIn?: number;\n};\n\nexport function createMultipartPartHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const partNumber = Number(body.partNumber);\n if (!Number.isInteger(partNumber) || partNumber <= 0) {\n return Response.json(\n { message: \"partNumber must be a positive integer\" },\n { status: 400 },\n );\n }\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(body.expiresIn);\n const partSize =\n typeof body.partSize === \"number\" && body.partSize > 0\n ? Math.floor(body.partSize)\n : null;\n\n const guardResult = await runHook(config.multipart?.partGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n // Bind the signature to the exact part size when provided. S3 verifies\n // Content-Length against the signed value and rejects mismatches with\n // SignatureDoesNotMatch, so a client cannot upload a larger blob than the\n // one declared at sign time.\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n ...(partSize !== null ? { ContentLength: partSize } : {}),\n }),\n {\n expiresIn,\n ...(partSize !== null\n ? { signableHeaders: new Set([\"content-length\"]) }\n : {}),\n },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n ...(partSize !== null ? { partSize } : {}),\n });\n });\n}\n","import {\n CompleteMultipartUploadCommand,\n HeadObjectCommand,\n ListPartsCommand,\n} from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildPublicUrl, parseFileName } from \"@/helpers\";\nimport { resolveObjectAcl } from \"@/helpers/server\";\n\ntype PartEntry = {\n partNumber: number;\n};\n\ntype Payload = {\n key: string;\n uploadId: string;\n bucket?: string;\n parts: PartEntry[];\n};\n\nexport function createMultipartCompleteHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const parts = (Array.isArray(body.parts) ? body.parts : [])\n .map(({ partNumber }) => Number(partNumber))\n .filter((n) => Number.isInteger(n) && n > 0)\n .sort((a, b) => a - b);\n\n if (!parts.length) {\n return Response.json(\n { message: \"At least one valid part is required\" },\n { status: 400 },\n );\n }\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.multipart?.completeGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n // Fetch authoritative ETags from S3 — this works regardless of whether\n // the client's CORS config exposes the ETag header.\n const listed = await config.s3.send(\n new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId }),\n );\n const listedParts = listed.Parts ?? [];\n\n const completeParts = parts.map((partNumber) => {\n const found = listedParts.find((p) => p.PartNumber === partNumber);\n return { PartNumber: partNumber, ETag: found?.ETag ?? \"\" };\n });\n\n const completeResult = await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: completeParts },\n }),\n );\n\n // Some S3-compatible providers finalize multipart uploads asynchronously,\n // so HeadObject may transiently return stale or empty metadata right after\n // CompleteMultipartUpload. Retry until contentLength is non-zero.\n let head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {\n await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));\n head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n }\n const contentLength = head.ContentLength ?? 0;\n const contentType = head.ContentType;\n const eTag = (head.ETag ?? completeResult.ETag ?? \"\").replace(/\"/g, \"\");\n const metadata = head.Metadata ?? {};\n const versionId = head.VersionId;\n const lastModified = head.LastModified?.toISOString();\n\n const acl = await resolveObjectAcl(config.s3, bucket, key);\n const publicUrl = buildPublicUrl(config.resolvePublicUrl, bucket, key, acl);\n const fileName = parseFileName(head.ContentDisposition);\n\n await config.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n contentLength,\n contentType,\n eTag,\n metadata,\n acl,\n fileName,\n publicUrl,\n versionId,\n lastModified,\n });\n\n return Response.json({\n bucket,\n key,\n uploadId,\n contentLength,\n contentType,\n eTag,\n metadata,\n acl,\n fileName,\n publicUrl,\n versionId,\n lastModified,\n });\n });\n}\n","import { AbortMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n bucket?: string;\n};\n\nexport function createMultipartAbortHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.multipart?.abortGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n await config.s3.send(\n new AbortMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n }),\n );\n\n await config.multipart?.onAbort?.({\n request,\n key,\n bucket,\n uploadId,\n });\n\n return Response.json({ bucket, key, uploadId, aborted: true });\n });\n}\n","import type { S3HandlerConfig, S3Handlers } from \"./types\";\nimport { createUploadHandler } from \"./handlers/presign/upload\";\nimport { createConfirmHandler } from \"./handlers/presign/confirm\";\nimport { createDownloadHandler } from \"./handlers/presign/download\";\nimport { createDeleteHandler } from \"./handlers/delete\";\nimport { createMultipartInitHandler } from \"./handlers/multipart/init\";\nimport { createMultipartPartHandler } from \"./handlers/multipart/part\";\nimport { createMultipartCompleteHandler } from \"./handlers/multipart/complete\";\nimport { createMultipartAbortHandler } from \"./handlers/multipart/abort\";\n\nexport function createHandlers(config: S3HandlerConfig): S3Handlers {\n return {\n presign: {\n upload: createUploadHandler(config),\n confirm: createConfirmHandler(config),\n download: createDownloadHandler(config),\n },\n multipart: {\n init: createMultipartInitHandler(config),\n part: createMultipartPartHandler(config),\n complete: createMultipartCompleteHandler(config),\n abort: createMultipartAbortHandler(config),\n },\n delete: createDeleteHandler(config),\n };\n}\n","import type { S3RouteHandlerConfig } from \"./types\";\nimport { createHandlers } from \"./create-handlers\";\nimport { runHook } from \"./internal-helpers\";\n\nconst disabled = () => Response.json({ message: \"Not Found\" }, { status: 404 });\n\nexport function createRouter(config: S3RouteHandlerConfig) {\n const handlers = createHandlers(config);\n const base = config.basePath.replace(/\\/$/, \"\");\n\n return async (request: Request): Promise<Response> => {\n // Global guard — runs before every request\n const guardResult = await runHook(config.guard, { request });\n if (guardResult) return guardResult;\n\n const url = new URL(request.url);\n const subpath = url.pathname.slice(base.length).replace(/^\\//, \"\");\n const method = request.method;\n\n if (method === \"POST\" && subpath === \"presign/upload\")\n return config.upload?.enabled\n ? handlers.presign.upload(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return config.upload?.enabled\n ? handlers.presign.confirm(request)\n : disabled();\n if (method === \"GET\" && subpath === \"presign/download\")\n return config.download?.enabled\n ? handlers.presign.download(request)\n : disabled();\n if (method === \"DELETE\" && subpath === \"delete\")\n return config.delete?.enabled ? handlers.delete(request) : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return config.multipart?.enabled\n ? handlers.multipart.init(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return config.multipart?.enabled\n ? handlers.multipart.part(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return config.multipart?.enabled\n ? handlers.multipart.complete(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return config.multipart?.enabled\n ? handlers.multipart.abort(request)\n : disabled();\n\n return Response.json({ message: \"Not Found\" }, { status: 404 });\n };\n}\n","export type PresignResponse = {\n key: string;\n bucket: string;\n url: string;\n expiresIn: number;\n};\n\nexport type UploadPresignResponse = {\n key: string;\n bucket: string;\n url: string;\n expiresIn: number;\n method: \"post\" | \"put\";\n /** Present when `method` is `\"post\"`. Must be appended to FormData before the file. */\n fields?: Record<string, string>;\n /** Present when `method` is `\"put\"`. Must be set as request headers on the PUT request. */\n headers?: Record<string, string>;\n};\n\n/** @deprecated Use {@link UploadPresignResponse} instead. */\nexport type PresignedPostResponse = UploadPresignResponse;\n\nexport type MultipartInitResponse = {\n key: string;\n bucket: string;\n uploadId: string;\n};\n\nexport type MultipartPartResponse = {\n presignedUrl: string;\n partNumber: number;\n uploadId: string;\n bucket: string;\n expiresIn: number;\n /** Exact byte size locked into this part's presigned URL signature. Only present when partSize was supplied to signPart. */\n partSize?: number;\n};\n\nexport type UploadConfirmResponse = {\n key: string;\n bucket: string;\n contentType?: string;\n contentLength: number;\n eTag?: string;\n /** S3 object metadata (x-amz-meta-*). Always present; empty object when no metadata was stored. */\n metadata: Record<string, string>;\n acl: \"private\" | \"public-read\";\n /** Display file name parsed from the object's Content-Disposition header. */\n fileName?: string;\n /** Public URL of the object. Only present when `acl` is `public-read` and `publicUrlBase` is configured on the server. */\n publicUrl?: string;\n /** Object version ID. Only present when bucket versioning is enabled. */\n versionId?: string;\n /** Last-modified timestamp from HeadObject (ISO 8601 string). */\n lastModified?: string;\n};\n\nexport type S3Api = {\n upload: (payload: {\n key: string;\n contentType?: string;\n /**\n * Exact byte size of the file. When provided the presigned POST policy\n * locks `content-length-range` to `[fileSize, fileSize]` so S3 rejects\n * uploads of any other size at the storage layer.\n */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition` on the S3 object. */\n fileName?: string;\n }) => Promise<UploadPresignResponse>;\n confirm: (payload: {\n key: string;\n bucket?: string;\n }) => Promise<UploadConfirmResponse>;\n download: (\n key: string,\n options?: { fileName?: string; bucket?: string },\n ) => Promise<PresignResponse>;\n delete: (\n key: string,\n options?: { bucket?: string },\n ) => Promise<{ success: boolean; bucket: string; key: string }>;\n multipart: {\n init: (payload: {\n key: string;\n contentType?: string;\n /** Declared total byte size of the file. Used for quota/guard checks and stored in `onInit` context. */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition` on the S3 object. */\n fileName?: string;\n }) => Promise<MultipartInitResponse>;\n signPart: (payload: {\n key: string;\n uploadId: string;\n partNumber: number;\n /**\n * Exact byte size of the part. When provided, Content-Length is included\n * in the HMAC signature so S3 rejects any part body of a different size.\n */\n partSize?: number;\n bucket?: string;\n }) => Promise<MultipartPartResponse>;\n complete: (payload: {\n key: string;\n uploadId: string;\n parts: Array<{ partNumber: number }>;\n bucket?: string;\n }) => Promise<{\n key: string;\n bucket: string;\n uploadId: string;\n contentLength: number;\n contentType?: string;\n eTag?: string;\n /** S3 object metadata (x-amz-meta-*). Always present; empty object when no metadata was stored. */\n metadata: Record<string, string>;\n /** Object version ID. Only present when bucket versioning is enabled. */\n versionId?: string;\n /** Last-modified timestamp from HeadObject (ISO 8601 string). */\n lastModified?: string;\n }>;\n abort: (payload: {\n key: string;\n uploadId: string;\n bucket?: string;\n }) => Promise<{ aborted: boolean }>;\n };\n};\n\nexport function createS3Api(basePath = \"/api/s3\"): S3Api {\n const base = basePath.replace(/\\/$/, \"\");\n\n const json = async <T>(url: string, init?: RequestInit): Promise<T> => {\n const res = await fetch(url, init);\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n throw new Error((body as { message?: string }).message ?? res.statusText);\n }\n return res.json() as Promise<T>;\n };\n\n const post = <T>(url: string, body: unknown): Promise<T> =>\n json<T>(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n return {\n upload(payload) {\n return post<UploadPresignResponse>(`${base}/presign/upload`, payload);\n },\n\n confirm(payload) {\n return post<UploadConfirmResponse>(\n `${base}/presign/upload/confirm`,\n payload,\n );\n },\n\n download(key, options?) {\n const params = new URLSearchParams({ key });\n if (options?.fileName) {\n const safe = options.fileName.replace(/[\"\\\\\\r\\n]/g, \"_\");\n params.set(\"fileName\", safe);\n }\n if (options?.bucket) params.set(\"bucket\", options.bucket);\n return json<PresignResponse>(`${base}/presign/download?${params}`);\n },\n\n delete(key, options?) {\n const params = new URLSearchParams({ key });\n if (options?.bucket) params.set(\"bucket\", options.bucket);\n return json<{ success: boolean; bucket: string; key: string }>(\n `${base}/delete?${params}`,\n { method: \"DELETE\" },\n );\n },\n\n multipart: {\n init(payload) {\n return post<MultipartInitResponse>(\n `${base}/presign/multipart/init`,\n payload,\n );\n },\n\n signPart(payload) {\n return post<MultipartPartResponse>(\n `${base}/presign/multipart/part`,\n payload,\n );\n },\n\n complete(payload) {\n return post<{\n key: string;\n bucket: string;\n uploadId: string;\n contentLength: number;\n contentType?: string;\n eTag?: string;\n metadata: Record<string, string>;\n }>(`${base}/presign/multipart/complete`, payload);\n },\n\n abort(payload) {\n return post<{ aborted: boolean }>(\n `${base}/presign/multipart/abort`,\n payload,\n );\n },\n },\n };\n}\n","export function validateFile(\n file: File,\n options: { accept?: string[]; maxFileSize?: number },\n): string | null {\n if (options.accept?.length) {\n const allowed = options.accept.some((type) => {\n // Extension check: \".pdf\", \".jpg\", etc.\n if (type.startsWith(\".\")) {\n return file.name.toLowerCase().endsWith(type.toLowerCase());\n }\n // Wildcard MIME: \"image/*\"\n if (type.endsWith(\"/*\")) {\n return file.type.startsWith(type.replace(\"/*\", \"/\"));\n }\n // Exact MIME: \"application/pdf\"\n return file.type === type;\n });\n if (!allowed) {\n const ext = file.name.includes(\".\") ? file.name.split(\".\").pop() : null;\n return `File type \"${ext ? `.${ext}` : file.type || \"unknown\"}\" is not allowed`;\n }\n }\n\n if (file.size === 0) {\n return \"File is empty\";\n }\n\n if (options.maxFileSize && file.size > options.maxFileSize) {\n const maxMB = (options.maxFileSize / (1024 * 1024)).toFixed(1);\n return `File size exceeds ${maxMB} MB limit`;\n }\n\n return null;\n}\n"]}
1
+ {"version":3,"sources":["../src/internal-helpers.ts","../src/helpers/build-content-disposition.ts","../src/helpers/parse-file-name.ts","../src/handlers/presign/upload.ts","../src/helpers/server/resolve-object-acl.ts","../src/handlers/presign/confirm.ts","../src/handlers/presign/download.ts","../src/handlers/delete.ts","../src/handlers/multipart/init.ts","../src/handlers/multipart/part.ts","../src/handlers/multipart/complete.ts","../src/handlers/multipart/abort.ts","../src/handlers/multipart/list-parts.ts","../src/create-handlers.ts","../src/router.ts","../src/api.ts","../src/helpers/validate-file.ts"],"names":["url","HeadObjectCommand","getSignedUrl","ListPartsCommand"],"mappings":"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","file":"index.js","sourcesContent":["export async function parseBody<T extends Record<string, unknown>>(\n request: Request,\n): Promise<T | null> {\n try {\n const body = await request.json();\n return body && typeof body === \"object\" ? (body as T) : null;\n } catch {\n return null;\n }\n}\n\nexport function requireString(value: unknown, name: string): string | Response {\n const trimmed = typeof value === \"string\" ? value.trim() : \"\";\n if (!trimmed) {\n return Response.json({ message: `${name} is required` }, { status: 400 });\n }\n return trimmed;\n}\n\nexport function normalizeExpiresIn(value: unknown): number {\n const n = Number(value);\n return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;\n}\n\nexport function withS3ErrorHandler(\n handler: (request: Request) => Promise<Response>,\n) {\n return async (request: Request) => {\n try {\n return await handler(request);\n } catch (err) {\n const code = (err as { code?: string }).code;\n const isNetworkError =\n code === \"EAI_AGAIN\" ||\n code === \"ECONNREFUSED\" ||\n code === \"ECONNRESET\" ||\n code === \"ETIMEDOUT\" ||\n code === \"ENOTFOUND\";\n\n const message = isNetworkError\n ? `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`\n : err instanceof Error\n ? err.message\n : \"Internal server error\";\n\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 502 });\n }\n };\n}\n\n/**\n * Run a server hook. Returns a Response if the hook rejects (throws),\n * or `null` if the hook passes (or is undefined).\n */\nexport async function runHook<T>(\n hook: ((context: T) => Promise<void> | void) | undefined,\n context: T,\n): Promise<Response | null> {\n if (!hook) return null;\n try {\n await hook(context);\n return null;\n } catch (err) {\n const message = err instanceof Error ? err.message : \"Forbidden\";\n const status =\n typeof (err as Record<string, unknown>)?.status === \"number\"\n ? ((err as Record<string, unknown>).status as number)\n : 403;\n return Response.json({ message }, { status });\n }\n}\n","/**\n * Builds a RFC 6266 `Content-Disposition` value for a file attachment.\n *\n * Includes both the ASCII-safe `filename` fallback and the RFC 5987\n * `filename*` parameter for full Unicode support, so the original file\n * name (including non-ASCII characters) is preserved across all browsers\n * and HTTP clients.\n *\n * @example\n * buildContentDisposition(\"résumé finale.pdf\")\n * // → 'attachment; filename=\"resume finale.pdf\"; filename*=UTF-8\\'\\'r%C3%A9sum%C3%A9%20finale.pdf'\n */\nexport function buildContentDisposition(fileName: string): string {\n const ascii = fileName.replace(/[^\\x20-\\x7E]/g, \"_\").replace(/[\"\\\\]/g, \"_\");\n const encoded = encodeURIComponent(fileName);\n return `attachment; filename=\"${ascii}\"; filename*=UTF-8''${encoded}`;\n}\n","/**\n * Parses the display file name from a `Content-Disposition` header value.\n *\n * Prefers the RFC 5987 `filename*=UTF-8''…` encoding (full Unicode) over\n * the plain ASCII `filename=\"…\"` fallback.\n *\n * Returns `undefined` when no filename can be extracted.\n */\nexport function parseFileName(\n contentDisposition: string | undefined,\n): string | undefined {\n if (!contentDisposition) return undefined;\n\n const utf8Match = contentDisposition.match(/filename\\*=UTF-8''([^;\\s]+)/i);\n if (utf8Match) {\n try {\n return decodeURIComponent(utf8Match[1]);\n } catch {\n /* ignore malformed encoding */\n }\n }\n\n const asciiMatch = contentDisposition.match(/filename=\"([^\"]*)\"/i);\n return asciiMatch?.[1];\n}\n","import { createPresignedPost } from \"@aws-sdk/s3-presigned-post\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport { PutObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\ntype Payload = {\n key: string;\n contentType?: string;\n /**\n * Exact byte size of the file the client intends to upload.\n * When provided the presigned POST policy locks `content-length-range` to\n * `[fileSize, fileSize]`, so S3 rejects any upload that is not exactly this\n * size — even if the client tampers with the payload.\n * When omitted the range is `[1, maxFileSize ?? 5 GiB]`.\n */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n expiresIn?: number;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition: attachment; filename=\"...\"` on the S3 object. */\n fileName?: string;\n};\n\nexport function createUploadHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(body.expiresIn);\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n const contentType = body.contentType?.trim() || \"application/octet-stream\";\n const fileSize =\n typeof body.fileSize === \"number\" && body.fileSize > 0\n ? Math.floor(body.fileSize)\n : null;\n\n const guardResult = await runHook(config.upload?.presignGuard, {\n request,\n key,\n bucket,\n contentType: body.contentType,\n fileSize: fileSize ?? undefined,\n metadata: body.metadata,\n acl,\n });\n if (guardResult) return guardResult;\n\n const method = config.upload?.method ?? \"post\";\n\n // Reject the request when size enforcement is mandatory but the client\n // did not declare a file size. Only relevant for presigned PUT — presigned\n // POST already enforces size via content-length-range policy regardless.\n if (\n method === \"put\" &&\n config.upload?.requireFileSize &&\n fileSize === null\n ) {\n return Response.json(\n {\n message:\n \"fileSize is required when upload.requireFileSize is enabled\",\n },\n { status: 400 },\n );\n }\n\n if (method === \"put\") {\n // Presigned PUT: only Content-Type and Content-Disposition are included\n // as signed headers. x-amz-meta-* headers are intentionally excluded.\n //\n // In browser environments, sending x-amz-meta-* on a cross-origin XHR\n // triggers a CORS preflight. Most S3/R2 CORS configs do not include\n // x-amz-meta-* in AllowedHeaders, so the preflight fails and the upload\n // never reaches S3. Content-Disposition is a standard header that is\n // widely allowed in CORS configs.\n //\n // If you need metadata on the object, use method: \"post\" (presigned POST\n // embeds metadata inside the signed form policy, which S3 applies at\n // the storage layer without CORS header constraints).\n // The metadata value from the request body is still forwarded to the\n // onPresigned hook for server-side use (e.g. storing in a database).\n const putHeaders: Record<string, string> = {\n \"Content-Type\": contentType,\n };\n if (body.fileName) {\n putHeaders[\"Content-Disposition\"] = buildContentDisposition(\n body.fileName,\n );\n }\n\n // When fileSize is known, bind the HMAC signature to the exact byte count.\n // S3 enforces Content-Length against the signature value and rejects any\n // PUT whose body size differs with SignatureDoesNotMatch — even when the\n // caller has the URL. Browsers set Content-Length automatically from the\n // body, so no manual header is required on the client side.\n const url = await getSignedUrl(\n config.s3,\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: contentType,\n ACL: acl,\n ...(body.fileName\n ? { ContentDisposition: buildContentDisposition(body.fileName) }\n : {}),\n ...(fileSize !== null ? { ContentLength: fileSize } : {}),\n }),\n {\n expiresIn,\n // Force content-length into X-Amz-SignedHeaders. Without this the\n // SDK omits it from the signed header set and size enforcement is lost.\n ...(fileSize !== null\n ? { signableHeaders: new Set([\"content-length\"]) }\n : {}),\n },\n );\n\n await config.upload?.onPresigned?.({\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n url,\n expiresIn,\n });\n\n return Response.json({\n bucket,\n key,\n url,\n headers: putHeaders,\n expiresIn,\n method: \"put\",\n });\n }\n\n // Presigned POST: policy fields are embedded in the signed form and must\n // be appended to FormData before the file. S3 enforces them at the storage layer.\n const fields: Record<string, string> = { acl, \"Content-Type\": contentType };\n\n if (body.fileName) {\n fields[\"Content-Disposition\"] = buildContentDisposition(body.fileName);\n }\n\n if (body.metadata) {\n for (const [k, v] of Object.entries(body.metadata)) {\n fields[`x-amz-meta-${k}`] = v;\n }\n }\n\n const rangeMin = fileSize ?? 1;\n const rangeMax = fileSize ?? undefined;\n\n const { url, fields: signedFields } = await createPresignedPost(config.s3, {\n Bucket: bucket,\n Key: key,\n Conditions:\n rangeMax !== undefined\n ? [[\"content-length-range\", rangeMin, rangeMax]]\n : [[\"content-length-range\", rangeMin, Number.MAX_SAFE_INTEGER]],\n Fields: fields,\n Expires: expiresIn,\n });\n\n await config.upload?.onPresigned?.({\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n url,\n expiresIn,\n });\n\n return Response.json({\n bucket,\n key,\n url,\n fields: signedFields,\n expiresIn,\n method: \"post\",\n });\n });\n}\n","import { GetObjectAclCommand, type S3Client } from \"@aws-sdk/client-s3\";\n\nconst DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1_500;\n\n/**\n * Determines the ACL of an S3 object by querying `GetObjectAcl`.\n *\n * Returns `\"public-read\"` when the `AllUsers` grantee has `READ` or\n * `FULL_CONTROL` permission, `\"private\"` when ACL is resolvable but not public,\n * and `undefined` when ACL cannot be resolved (e.g. unsupported providers).\n */\nexport async function resolveObjectAcl(\n s3: S3Client,\n bucket: string,\n key: string,\n timeoutMs: number = DEFAULT_ACL_LOOKUP_TIMEOUT_MS,\n): Promise<\"public-read\" | \"private\" | undefined> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), timeoutMs);\n\n try {\n const result = await s3.send(\n new GetObjectAclCommand({ Bucket: bucket, Key: key }),\n { abortSignal: controller.signal },\n );\n const isPublic = result.Grants?.some(\n (g) =>\n g.Grantee?.URI === \"http://acs.amazonaws.com/groups/global/AllUsers\" &&\n (g.Permission === \"READ\" || g.Permission === \"FULL_CONTROL\"),\n );\n return isPublic ? \"public-read\" : \"private\";\n } catch {\n return undefined;\n } finally {\n clearTimeout(timeout);\n }\n}\n","import { HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { parseFileName } from \"@/helpers\";\nimport { resolveObjectAcl } from \"@/helpers/server\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n};\n\nexport function createConfirmHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.upload?.confirmGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n\n const acl = config.resolveObjectAcl\n ? await resolveObjectAcl(config.s3, bucket, key)\n : undefined;\n const fileName = parseFileName(head.ContentDisposition);\n\n const context = {\n request,\n key,\n bucket,\n contentType: head.ContentType,\n contentLength: head.ContentLength ?? 0,\n eTag: head.ETag?.replace(/\"/g, \"\"),\n metadata: head.Metadata,\n acl,\n fileName,\n versionId: head.VersionId,\n lastModified: head.LastModified?.toISOString(),\n };\n\n await config.upload?.onUploadConfirmed?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n metadata: context.metadata ?? {},\n acl,\n fileName,\n versionId: context.versionId,\n lastModified: context.lastModified,\n });\n });\n}\n","import { GetObjectCommand, HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\nexport function createDownloadHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const { searchParams } = new URL(request.url);\n const key = searchParams.get(\"key\")?.trim();\n if (!key) {\n return Response.json(\n { message: \"key query parameter is required\" },\n { status: 400 },\n );\n }\n\n const bucket = searchParams.get(\"bucket\")?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(searchParams.get(\"expiresIn\"));\n const fileName = searchParams.get(\"fileName\")?.trim();\n\n const guardResult = await runHook(config.download?.presignGuard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n try {\n await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));\n } catch (err: unknown) {\n const name = (err as { name?: string })?.name;\n if (name === \"NoSuchKey\" || name === \"NotFound\") {\n return Response.json({ message: \"Object not found\" }, { status: 404 });\n }\n throw err;\n }\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ResponseContentDisposition: fileName\n ? buildContentDisposition(fileName)\n : \"attachment\",\n }),\n { expiresIn },\n );\n\n await config.download?.onPresigned?.({\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n url,\n expiresIn,\n });\n\n return Response.json({ bucket, key, url, expiresIn });\n });\n}\n","import { DeleteObjectCommand, HeadObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../types\";\nimport { runHook, withS3ErrorHandler } from \"../internal-helpers\";\n\nexport function createDeleteHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const { searchParams } = new URL(request.url);\n const key = searchParams.get(\"key\")?.trim();\n if (!key) {\n return Response.json(\n { message: \"key query parameter is required\" },\n { status: 400 },\n );\n }\n\n const bucket = searchParams.get(\"bucket\")?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.delete?.deleteGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n try {\n await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));\n } catch {\n return Response.json({ message: \"Object not found\" }, { status: 404 });\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.delete?.onDeleted?.({ request, key, bucket });\n\n return Response.json({ success: true, bucket, key });\n });\n}\n","import { CreateMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { buildContentDisposition } from \"@/helpers\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n contentType?: string;\n /** Declared total byte size of the file being uploaded. */\n fileSize?: number;\n metadata?: Record<string, string>;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition: attachment; filename=\"...\"` on the S3 object. */\n fileName?: string;\n};\n\nexport function createMultipartInitHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n const fileSize =\n typeof body.fileSize === \"number\" && body.fileSize > 0\n ? Math.floor(body.fileSize)\n : undefined;\n\n // Reject the request when size enforcement is mandatory but the client\n // did not declare a file size. The react client always sends file.size,\n // so this only blocks requests that deliberately omit the field.\n if (config.multipart?.requireFileSize && fileSize === undefined) {\n return Response.json(\n {\n message:\n \"fileSize is required when multipart.requireFileSize is enabled\",\n },\n { status: 400 },\n );\n }\n\n const guardResult = await runHook(config.multipart?.initGuard, {\n request,\n key,\n bucket,\n fileSize,\n });\n if (guardResult) return guardResult;\n\n const { UploadId } = await config.s3.send(\n new CreateMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n ContentType: body.contentType,\n ContentDisposition: body.fileName\n ? buildContentDisposition(body.fileName)\n : undefined,\n Metadata: body.metadata,\n ACL: acl,\n }),\n );\n\n await config.multipart?.onInit?.({\n request,\n key,\n bucket,\n uploadId: UploadId!,\n contentType: body.contentType,\n fileSize,\n metadata: body.metadata,\n acl,\n });\n\n return Response.json({ bucket, key, uploadId: UploadId }, { status: 201 });\n });\n}\n","import { UploadPartCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: number;\n /**\n * Exact byte size of the part being uploaded.\n * When provided, Content-Length is included in the HMAC signature so S3\n * rejects any UploadPart request whose body size does not match — preventing\n * a client from substituting a larger or different blob for a signed URL.\n */\n partSize?: number;\n bucket?: string;\n expiresIn?: number;\n};\n\nexport function createMultipartPartHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const partNumber = Number(body.partNumber);\n if (!Number.isInteger(partNumber) || partNumber <= 0) {\n return Response.json(\n { message: \"partNumber must be a positive integer\" },\n { status: 400 },\n );\n }\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n const expiresIn = normalizeExpiresIn(body.expiresIn);\n const partSize =\n typeof body.partSize === \"number\" && body.partSize > 0\n ? Math.floor(body.partSize)\n : null;\n\n const guardResult = await runHook(config.multipart?.partGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n // Bind the signature to the exact part size when provided. S3 verifies\n // Content-Length against the signed value and rejects mismatches with\n // SignatureDoesNotMatch, so a client cannot upload a larger blob than the\n // one declared at sign time.\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n ...(partSize !== null ? { ContentLength: partSize } : {}),\n }),\n {\n expiresIn,\n ...(partSize !== null\n ? { signableHeaders: new Set([\"content-length\"]) }\n : {}),\n },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n ...(partSize !== null ? { partSize } : {}),\n });\n });\n}\n","import {\n CompleteMultipartUploadCommand,\n HeadObjectCommand,\n ListPartsCommand,\n} from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\nimport { parseFileName } from \"@/helpers\";\nimport { resolveObjectAcl } from \"@/helpers/server\";\n\ntype PartEntry = {\n partNumber: number;\n};\n\ntype Payload = {\n key: string;\n uploadId: string;\n bucket?: string;\n parts: PartEntry[];\n};\n\nexport function createMultipartCompleteHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const parts = (Array.isArray(body.parts) ? body.parts : [])\n .map(({ partNumber }) => Number(partNumber))\n .filter((n) => Number.isInteger(n) && n > 0)\n .sort((a, b) => a - b);\n\n if (!parts.length) {\n return Response.json(\n { message: \"At least one valid part is required\" },\n { status: 400 },\n );\n }\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.multipart?.completeGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n // Fetch authoritative ETags from S3 — this works regardless of whether\n // the client's CORS config exposes the ETag header.\n const listed = await config.s3.send(\n new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId }),\n );\n const listedParts = listed.Parts ?? [];\n\n const completeParts = parts.map((partNumber) => {\n const found = listedParts.find((p) => p.PartNumber === partNumber);\n return { PartNumber: partNumber, ETag: found?.ETag ?? \"\" };\n });\n\n const completeResult = await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: completeParts },\n }),\n );\n\n // Some S3-compatible providers finalize multipart uploads asynchronously,\n // so HeadObject may transiently return stale or empty metadata right after\n // CompleteMultipartUpload. Retry until contentLength is non-zero.\n let head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {\n await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));\n head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n }\n const contentLength = head.ContentLength ?? 0;\n const contentType = head.ContentType;\n const eTag = (head.ETag ?? completeResult.ETag ?? \"\").replace(/\"/g, \"\");\n const metadata = head.Metadata ?? {};\n const versionId = head.VersionId;\n const lastModified = head.LastModified?.toISOString();\n\n const acl = config.resolveObjectAcl\n ? await resolveObjectAcl(config.s3, bucket, key)\n : undefined;\n const fileName = parseFileName(head.ContentDisposition);\n\n await config.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n contentLength,\n contentType,\n eTag,\n metadata,\n acl,\n fileName,\n versionId,\n lastModified,\n });\n\n return Response.json({\n bucket,\n key,\n uploadId,\n contentLength,\n contentType,\n eTag,\n metadata,\n acl,\n fileName,\n versionId,\n lastModified,\n });\n });\n}\n","import { AbortMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"@/internal-helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n bucket?: string;\n};\n\nexport function createMultipartAbortHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const body = await parseBody<Payload>(request);\n if (!body) {\n return Response.json(\n { message: \"Invalid JSON payload\" },\n { status: 400 },\n );\n }\n\n const key = requireString(body.key, \"key\");\n if (key instanceof Response) return key;\n\n const uploadId = requireString(body.uploadId, \"uploadId\");\n if (uploadId instanceof Response) return uploadId;\n\n const bucket = body.bucket?.trim() || config.defaultBucket;\n\n const guardResult = await runHook(config.multipart?.abortGuard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n await config.s3.send(\n new AbortMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n }),\n );\n\n await config.multipart?.onAbort?.({\n request,\n key,\n bucket,\n uploadId,\n });\n\n return Response.json({ bucket, key, uploadId, aborted: true });\n });\n}\n","import { ListPartsCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"@/types\";\nimport { runHook, withS3ErrorHandler } from \"@/internal-helpers\";\n\nexport function createMultipartListPartsHandler(config: S3HandlerConfig) {\n return withS3ErrorHandler(async (request: Request) => {\n const url = new URL(request.url);\n const key = url.searchParams.get(\"key\")?.trim() ?? \"\";\n const uploadId = url.searchParams.get(\"uploadId\")?.trim() ?? \"\";\n const bucketParam = url.searchParams.get(\"bucket\")?.trim();\n\n if (!key) {\n return Response.json({ message: \"key is required\" }, { status: 400 });\n }\n if (!uploadId) {\n return Response.json(\n { message: \"uploadId is required\" },\n { status: 400 },\n );\n }\n\n const bucket = bucketParam || config.defaultBucket;\n\n const guardResult = await runHook(config.multipart?.listGuard, {\n request,\n key,\n bucket,\n uploadId,\n });\n if (guardResult) return guardResult;\n\n const response = await config.s3.send(\n new ListPartsCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n }),\n );\n\n const parts = (response.Parts ?? []).map((p) => ({\n partNumber: p.PartNumber ?? 0,\n size: p.Size ?? 0,\n eTag: (p.ETag ?? \"\").replace(/\"/g, \"\"),\n }));\n\n await config.multipart?.onList?.({ request, key, bucket, uploadId, parts });\n\n return Response.json({ parts });\n });\n}\n","import type { S3HandlerConfig, S3Handlers } from \"./types\";\nimport { createUploadHandler } from \"./handlers/presign/upload\";\nimport { createConfirmHandler } from \"./handlers/presign/confirm\";\nimport { createDownloadHandler } from \"./handlers/presign/download\";\nimport { createDeleteHandler } from \"./handlers/delete\";\nimport { createMultipartInitHandler } from \"./handlers/multipart/init\";\nimport { createMultipartPartHandler } from \"./handlers/multipart/part\";\nimport { createMultipartCompleteHandler } from \"./handlers/multipart/complete\";\nimport { createMultipartAbortHandler } from \"./handlers/multipart/abort\";\nimport { createMultipartListPartsHandler } from \"./handlers/multipart/list-parts\";\n\nexport function createHandlers(config: S3HandlerConfig): S3Handlers {\n return {\n presign: {\n upload: createUploadHandler(config),\n confirm: createConfirmHandler(config),\n download: createDownloadHandler(config),\n },\n multipart: {\n init: createMultipartInitHandler(config),\n part: createMultipartPartHandler(config),\n complete: createMultipartCompleteHandler(config),\n abort: createMultipartAbortHandler(config),\n listParts: createMultipartListPartsHandler(config),\n },\n delete: createDeleteHandler(config),\n };\n}\n","import type { S3RouteHandlerConfig } from \"./types\";\nimport { createHandlers } from \"./create-handlers\";\nimport { runHook } from \"./internal-helpers\";\n\nconst disabled = () => Response.json({ message: \"Not Found\" }, { status: 404 });\n\nexport function createRouter(config: S3RouteHandlerConfig) {\n const handlers = createHandlers(config);\n const base = config.basePath.replace(/\\/$/, \"\");\n\n return async (request: Request): Promise<Response> => {\n // Global guard — runs before every request\n const guardResult = await runHook(config.guard, { request });\n if (guardResult) return guardResult;\n\n const url = new URL(request.url);\n const subpath = url.pathname.slice(base.length).replace(/^\\//, \"\");\n const method = request.method;\n\n if (method === \"POST\" && subpath === \"presign/upload\")\n return config.upload?.enabled\n ? handlers.presign.upload(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return config.upload?.enabled\n ? handlers.presign.confirm(request)\n : disabled();\n if (method === \"GET\" && subpath === \"presign/download\")\n return config.download?.enabled\n ? handlers.presign.download(request)\n : disabled();\n if (method === \"DELETE\" && subpath === \"delete\")\n return config.delete?.enabled ? handlers.delete(request) : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return config.multipart?.enabled\n ? handlers.multipart.init(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return config.multipart?.enabled\n ? handlers.multipart.part(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return config.multipart?.enabled\n ? handlers.multipart.complete(request)\n : disabled();\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return config.multipart?.enabled\n ? handlers.multipart.abort(request)\n : disabled();\n if (method === \"GET\" && subpath === \"presign/multipart/parts\")\n return config.multipart?.enabled\n ? handlers.multipart.listParts(request)\n : disabled();\n\n return Response.json({ message: \"Not Found\" }, { status: 404 });\n };\n}\n","export type PresignResponse = {\n key: string;\n bucket: string;\n url: string;\n expiresIn: number;\n};\n\nexport type UploadPresignResponse = {\n key: string;\n bucket: string;\n url: string;\n expiresIn: number;\n method: \"post\" | \"put\";\n /** Present when `method` is `\"post\"`. Must be appended to FormData before the file. */\n fields?: Record<string, string>;\n /** Present when `method` is `\"put\"`. Must be set as request headers on the PUT request. */\n headers?: Record<string, string>;\n};\n\n/** @deprecated Use {@link UploadPresignResponse} instead. */\nexport type PresignedPostResponse = UploadPresignResponse;\n\nexport type MultipartInitResponse = {\n key: string;\n bucket: string;\n uploadId: string;\n};\n\nexport type MultipartPartResponse = {\n presignedUrl: string;\n partNumber: number;\n uploadId: string;\n bucket: string;\n expiresIn: number;\n /** Exact byte size locked into this part's presigned URL signature. Only present when partSize was supplied to signPart. */\n partSize?: number;\n};\n\nexport type MultipartListPartsResponse = {\n parts: Array<{\n partNumber: number;\n /** Byte size of the uploaded part. */\n size: number;\n /** ETag of the uploaded part. */\n eTag: string;\n }>;\n};\n\nexport type UploadConfirmResponse = {\n key: string;\n bucket: string;\n contentType?: string;\n contentLength: number;\n eTag?: string;\n /** S3 object metadata (x-amz-meta-*). Always present; empty object when no metadata was stored. */\n metadata: Record<string, string>;\n /** Resolved ACL. Omitted when ACL lookup is disabled or unsupported. */\n acl?: \"private\" | \"public-read\";\n /** Display file name parsed from the object's Content-Disposition header. */\n fileName?: string;\n /** Object version ID. Only present when bucket versioning is enabled. */\n versionId?: string;\n /** Last-modified timestamp from HeadObject (ISO 8601 string). */\n lastModified?: string;\n};\n\nexport type S3Api = {\n upload: (payload: {\n key: string;\n contentType?: string;\n /**\n * Exact byte size of the file. When provided the presigned POST policy\n * locks `content-length-range` to `[fileSize, fileSize]` so S3 rejects\n * uploads of any other size at the storage layer.\n */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition` on the S3 object. */\n fileName?: string;\n }) => Promise<UploadPresignResponse>;\n confirm: (payload: {\n key: string;\n bucket?: string;\n }) => Promise<UploadConfirmResponse>;\n download: (\n key: string,\n options?: { fileName?: string; bucket?: string },\n ) => Promise<PresignResponse>;\n delete: (\n key: string,\n options?: { bucket?: string },\n ) => Promise<{ success: boolean; bucket: string; key: string }>;\n multipart: {\n init: (payload: {\n key: string;\n contentType?: string;\n /** Declared total byte size of the file. Used for quota/guard checks and stored in `onInit` context. */\n fileSize?: number;\n metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n /** Original file name. Stored as `Content-Disposition` on the S3 object. */\n fileName?: string;\n }) => Promise<MultipartInitResponse>;\n signPart: (payload: {\n key: string;\n uploadId: string;\n partNumber: number;\n /**\n * Exact byte size of the part. When provided, Content-Length is included\n * in the HMAC signature so S3 rejects any part body of a different size.\n */\n partSize?: number;\n bucket?: string;\n }) => Promise<MultipartPartResponse>;\n /**\n * List already-uploaded parts for an in-progress multipart upload.\n * Used by the resumable upload flow to skip already-completed parts.\n */\n listParts: (payload: {\n key: string;\n uploadId: string;\n bucket?: string;\n }) => Promise<MultipartListPartsResponse>;\n complete: (payload: {\n key: string;\n uploadId: string;\n parts: Array<{ partNumber: number }>;\n bucket?: string;\n }) => Promise<{\n key: string;\n bucket: string;\n uploadId: string;\n contentLength: number;\n contentType?: string;\n eTag?: string;\n /** S3 object metadata (x-amz-meta-*). Always present; empty object when no metadata was stored. */\n metadata: Record<string, string>;\n /** Object version ID. Only present when bucket versioning is enabled. */\n versionId?: string;\n /** Last-modified timestamp from HeadObject (ISO 8601 string). */\n lastModified?: string;\n }>;\n abort: (payload: {\n key: string;\n uploadId: string;\n bucket?: string;\n }) => Promise<{ aborted: boolean }>;\n };\n};\n\nexport function createS3Api(basePath = \"/api/s3\"): S3Api {\n const base = basePath.replace(/\\/$/, \"\");\n\n const json = async <T>(url: string, init?: RequestInit): Promise<T> => {\n const res = await fetch(url, init);\n if (!res.ok) {\n const body = await res.json().catch(() => ({}));\n throw new Error((body as { message?: string }).message ?? res.statusText);\n }\n return res.json() as Promise<T>;\n };\n\n const post = <T>(url: string, body: unknown): Promise<T> =>\n json<T>(url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n });\n\n return {\n upload(payload) {\n return post<UploadPresignResponse>(`${base}/presign/upload`, payload);\n },\n\n confirm(payload) {\n return post<UploadConfirmResponse>(\n `${base}/presign/upload/confirm`,\n payload,\n );\n },\n\n download(key, options?) {\n const params = new URLSearchParams({ key });\n if (options?.fileName) {\n const safe = options.fileName.replace(/[\"\\\\\\r\\n]/g, \"_\");\n params.set(\"fileName\", safe);\n }\n if (options?.bucket) params.set(\"bucket\", options.bucket);\n return json<PresignResponse>(`${base}/presign/download?${params}`);\n },\n\n delete(key, options?) {\n const params = new URLSearchParams({ key });\n if (options?.bucket) params.set(\"bucket\", options.bucket);\n return json<{ success: boolean; bucket: string; key: string }>(\n `${base}/delete?${params}`,\n { method: \"DELETE\" },\n );\n },\n\n multipart: {\n init(payload) {\n return post<MultipartInitResponse>(\n `${base}/presign/multipart/init`,\n payload,\n );\n },\n\n signPart(payload) {\n return post<MultipartPartResponse>(\n `${base}/presign/multipart/part`,\n payload,\n );\n },\n\n listParts(payload) {\n const params = new URLSearchParams({\n key: payload.key,\n uploadId: payload.uploadId,\n });\n if (payload.bucket) params.set(\"bucket\", payload.bucket);\n return json<MultipartListPartsResponse>(\n `${base}/presign/multipart/parts?${params}`,\n );\n },\n\n complete(payload) {\n return post<{\n key: string;\n bucket: string;\n uploadId: string;\n contentLength: number;\n contentType?: string;\n eTag?: string;\n metadata: Record<string, string>;\n }>(`${base}/presign/multipart/complete`, payload);\n },\n\n abort(payload) {\n return post<{ aborted: boolean }>(\n `${base}/presign/multipart/abort`,\n payload,\n );\n },\n },\n };\n}\n","export function validateFile(\n file: File,\n options: { accept?: string[]; maxFileSize?: number },\n): string | null {\n if (options.accept?.length) {\n const allowed = options.accept.some((type) => {\n // Extension check: \".pdf\", \".jpg\", etc.\n if (type.startsWith(\".\")) {\n return file.name.toLowerCase().endsWith(type.toLowerCase());\n }\n // Wildcard MIME: \"image/*\"\n if (type.endsWith(\"/*\")) {\n return file.type.startsWith(type.replace(\"/*\", \"/\"));\n }\n // Exact MIME: \"application/pdf\"\n return file.type === type;\n });\n if (!allowed) {\n const ext = file.name.includes(\".\") ? file.name.split(\".\").pop() : null;\n return `File type \"${ext ? `.${ext}` : file.type || \"unknown\"}\" is not allowed`;\n }\n }\n\n if (file.size === 0) {\n return \"File is empty\";\n }\n\n if (options.maxFileSize && file.size > options.maxFileSize) {\n const maxMB = (options.maxFileSize / (1024 * 1024)).toFixed(1);\n return `File size exceeds ${maxMB} MB limit`;\n }\n\n return null;\n}\n"]}
@@ -0,0 +1,5 @@
1
+ import type { HookContext } from "./hook-context";
2
+ export type DeleteHookContext = HookContext & {
3
+ key: string;
4
+ bucket: string;
5
+ };
@@ -0,0 +1,6 @@
1
+ import type { HookContext } from "./hook-context";
2
+ export type DownloadHookContext = HookContext & {
3
+ key: string;
4
+ bucket: string;
5
+ fileName?: string;
6
+ };
@@ -0,0 +1,5 @@
1
+ import type { DownloadHookContext } from "./download-hook-context";
2
+ export type DownloadSuccessContext = DownloadHookContext & {
3
+ url: string;
4
+ expiresIn: number;
5
+ };
@@ -0,0 +1,3 @@
1
+ export type HookContext = {
2
+ request: Request;
3
+ };
@@ -0,0 +1,15 @@
1
+ export type { HookContext } from "./hook-context";
2
+ export type { UploadHookContext } from "./upload-hook-context";
3
+ export type { UploadSuccessContext } from "./upload-success-context";
4
+ export type { UploadCompleteContext } from "./upload-complete-context";
5
+ export type { DownloadHookContext } from "./download-hook-context";
6
+ export type { DownloadSuccessContext } from "./download-success-context";
7
+ export type { DeleteHookContext } from "./delete-hook-context";
8
+ export type { MultipartHookContext } from "./multipart-hook-context";
9
+ export type { MultipartInitSuccessContext } from "./multipart-init-success-context";
10
+ export type { MultipartCompleteSuccessContext } from "./multipart-complete-success-context";
11
+ export type { MultipartListSuccessContext } from "./multipart-list-success-context";
12
+ export type { S3HandlerConfig } from "./s3-handler-config";
13
+ export type { S3RouteHandlerConfig } from "./s3-route-handler-config";
14
+ export type { S3Handler } from "./s3-handler";
15
+ export type { S3Handlers } from "./s3-handlers";
@@ -0,0 +1,13 @@
1
+ import type { MultipartHookContext } from "./multipart-hook-context";
2
+ export type MultipartCompleteSuccessContext = MultipartHookContext & {
3
+ uploadId: string;
4
+ contentLength: number;
5
+ contentType?: string;
6
+ eTag?: string;
7
+ metadata: Record<string, string>;
8
+ /** Resolved ACL. Omitted when ACL lookup is disabled or unsupported. */
9
+ acl?: "private" | "public-read";
10
+ fileName?: string;
11
+ versionId?: string;
12
+ lastModified?: string;
13
+ };
@@ -0,0 +1,7 @@
1
+ import type { HookContext } from "./hook-context";
2
+ export type MultipartHookContext = HookContext & {
3
+ key: string;
4
+ bucket: string;
5
+ /** Declared byte size of the file, available during init only. */
6
+ fileSize?: number;
7
+ };
@@ -0,0 +1,8 @@
1
+ import type { MultipartHookContext } from "./multipart-hook-context";
2
+ export type MultipartInitSuccessContext = MultipartHookContext & {
3
+ uploadId: string;
4
+ contentType?: string;
5
+ fileSize?: number;
6
+ metadata?: Record<string, string>;
7
+ acl?: "private" | "public-read";
8
+ };
@@ -0,0 +1,11 @@
1
+ import type { HookContext } from "./hook-context";
2
+ export type MultipartListSuccessContext = HookContext & {
3
+ key: string;
4
+ bucket: string;
5
+ uploadId: string;
6
+ parts: Array<{
7
+ partNumber: number;
8
+ size: number;
9
+ eTag: string;
10
+ }>;
11
+ };