@better-s3/server 2.3.1 → 3.0.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @better-s3/server
1
+ # @better-s3/server
2
2
 
3
3
  Framework-agnostic S3 server handlers — presigned uploads, downloads, deletes, and multipart operations. Works with any `Request`/`Response` runtime (Next.js, Hono, Bun, Deno, Cloudflare Workers, etc).
4
4
 
@@ -10,10 +10,8 @@ pnpm add @better-s3/server @aws-sdk/client-s3
10
10
 
11
11
  ## Quick Start
12
12
 
13
- ### Next.js App Router
14
-
15
13
  ```ts
16
- // app/api/s3/[...s3]/route.ts
14
+ // app/api/s3/[...s3]/route.ts (Next.js App Router)
17
15
  import { S3Client } from "@aws-sdk/client-s3";
18
16
  import { createRouteHandler } from "@better-s3/server/next";
19
17
 
@@ -21,104 +19,169 @@ const handler = createRouteHandler({
21
19
  s3: new S3Client({ region: "us-east-1" }),
22
20
  defaultBucket: "my-bucket",
23
21
  basePath: "/api/s3",
22
+ maxFileSize: 100 * 1024 * 1024, // (100 MB) global server limit for all uploads
24
23
  });
25
24
 
26
25
  export { handler as GET, handler as POST, handler as DELETE };
27
26
  ```
28
27
 
29
- ### Other Frameworks
28
+ Other frameworks via `createRouter` from `@better-s3/server`:
30
29
 
31
30
  ```ts
32
- import { S3Client } from "@aws-sdk/client-s3";
33
- import { createRouter } from "@better-s3/server";
34
-
35
31
  const router = createRouter({
36
- s3: new S3Client({ region: "us-east-1" }),
32
+ s3,
37
33
  defaultBucket: "my-bucket",
38
34
  basePath: "/api/s3",
39
35
  });
40
36
 
41
- // Hono
42
- app.all("/api/s3/*", (c) => router(c.req.raw));
37
+ app.all("/api/s3/*", (c) => router(c.req.raw)); // Hono example
43
38
  ```
44
39
 
40
+
41
+ | Upload mode | Enforcement |
42
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | Simple | S3 enforces exact size via `content-length-range` in the signed POST policy — tamperproof |
44
+ | Multipart | 1. Init rejected if `fileSize` exceeds limit · 2. Part requests capped at `ceil(maxFileSize / 5 MiB)` · 3. `HeadObject` after complete → delete + 422 if exceeded |
45
+
46
+ > Simple upload is inherently more secure — enforcement happens at the S3 storage layer. Multipart presigned `UploadPart` URLs cannot enforce per-part size; see [Multipart cost attacks](#multipart-cost-attacks).
47
+
45
48
  ## Server Hooks
46
49
 
47
- Run server-side logic at key points auth, logging, database writes. Every hook has access to the `Request` object.
50
+ Run server-side logic at key points. Every hook receives the `Request` object. **Throw to reject** returned as `{ message }` with status 403, or any status you set on the thrown error.
51
+
52
+ ```
53
+ Simple: upload.guard → upload.onSuccess → [S3] → upload.onComplete
54
+ Multipart: multipart.guard(init) → multipart.onInit
55
+ multipart.guard(part) → [S3]
56
+ multipart.guard(complete) → HeadObject → multipart.onComplete
57
+ multipart.guard(abort) → multipart.onAbort
58
+ ```
59
+
60
+ ### Authentication
48
61
 
49
62
  ```ts
50
- createRouteHandler({
51
- s3,
52
- defaultBucket: "my-bucket",
53
- basePath: "/api/s3",
54
- hooks: {
55
- guard: async ({ request }) => {
56
- const session = await getSession(request);
57
- if (!session) throw new Error("Unauthorized");
58
- },
59
- upload: {
60
- onSuccess: async ({ request, key, contentType }) => {
61
- /* ... */
62
- },
63
- onComplete: async ({ key, contentType, contentLength, eTag }) => {
64
- /* ... */
65
- },
66
- },
67
- delete: {
68
- guard: async ({ request, key }) => {
69
- /* ownership check */
70
- },
71
- onSuccess: async ({ key }) => {
72
- /* remove DB record */
73
- },
74
- },
75
- multipart: {
76
- onInit: async ({ key, uploadId }) => {
77
- /* ... */
78
- },
79
- onComplete: async ({ key, uploadId }) => {
80
- /* ... */
81
- },
82
- onAbort: async ({ key, uploadId }) => {
83
- /* ... */
84
- },
85
- },
63
+ hooks: {
64
+ guard: async ({ request }) => {
65
+ const session = await getSession(request);
66
+ if (!session) throw Object.assign(new Error("Unauthorized"), { status: 401 });
86
67
  },
87
- });
68
+ }
88
69
  ```
89
70
 
90
- **Available hooks:** `guard`, `upload.guard`, `upload.onSuccess`, `upload.onComplete`, `download.guard`, `download.onSuccess`, `delete.guard`, `delete.onSuccess`, `multipart.guard`, `multipart.onInit`, `multipart.onComplete`, `multipart.onAbort`
71
+ ### Quota check
91
72
 
92
- ## API Routes
73
+ `fileSize` in `upload.guard` and `multipart.guard` (init only) is **declared by the client** — use it for pre-checks. `contentLength` in `onComplete` is verified by S3.
93
74
 
94
- | Method | Path | Description |
95
- | -------- | ----------------------------- | ---------------------- |
96
- | `POST` | `/presign/upload` | Presigned upload URL |
97
- | `POST` | `/presign/upload/confirm` | Confirm upload |
98
- | `GET` | `/presign/download` | Presigned download URL |
99
- | `DELETE` | `/delete` | Delete object |
100
- | `POST` | `/presign/multipart/init` | Init multipart |
101
- | `POST` | `/presign/multipart/part` | Sign part |
102
- | `POST` | `/presign/multipart/complete` | Complete multipart |
103
- | `POST` | `/presign/multipart/abort` | Abort multipart |
75
+ ```ts
76
+ hooks: {
77
+ upload: {
78
+ guard: async ({ request, fileSize }) => {
79
+ const { userId } = await getSession(request);
80
+ const used = await db.storage.getUsed(userId);
81
+ if (fileSize && used + fileSize > QUOTA)
82
+ throw Object.assign(new Error("Quota exceeded"), { status: 403 });
83
+ },
84
+ },
85
+ }
86
+ ```
104
87
 
105
- ## S3 API Client
88
+ ## Multipart cost attacks
106
89
 
107
- Client-side helper for calling the server endpoints (used internally by `@better-s3/react`):
90
+ S3 presigned `UploadPart` URLs cannot enforce per-part size. A malicious client can upload oversized parts — the post-complete `HeadObject` check will catch and delete the final object, but temporary part storage still accumulates cost. Without completion, orphaned parts stay on AWS S3 indefinitely (R2 cleans up after 7 days).
91
+
92
+ **Mitigation: track uploads in a database + cron cleanup**
108
93
 
109
94
  ```ts
110
- import { createS3Api } from "@better-s3/server";
95
+ hooks: {
96
+ multipart: {
97
+ // Guard runs on init, part, complete, and abort.
98
+ // fileSize is only present during init — use it to limit open sessions.
99
+ guard: async ({ request, fileSize }) => {
100
+ if (!fileSize) return;
101
+ const { userId } = await getSession(request);
102
+ const pending = await db.upload.count({ where: { userId, status: "pending" } });
103
+ if (pending >= 3)
104
+ throw Object.assign(new Error("Too many pending uploads"), { status: 429 });
105
+ },
111
106
 
112
- const api = createS3Api("/api/s3");
107
+ onInit: async ({ request, key, uploadId, contentType, fileSize }) => {
108
+ const { userId } = await getSession(request);
109
+ await db.upload.create({
110
+ data: { key, userId, uploadId, contentType, declaredSize: fileSize, status: "pending" },
111
+ });
112
+ },
113
113
 
114
- const { url } = await api.upload({
115
- key: "photo.jpg",
116
- contentType: "image/jpeg",
114
+ onComplete: async ({ key, contentLength, contentType, eTag }) => {
115
+ await db.upload.update({
116
+ where: { key },
117
+ data: { status: "complete", verifiedSize: contentLength, contentType, eTag },
118
+ });
119
+ },
120
+
121
+ onAbort: async ({ key }) => {
122
+ await db.upload.update({ where: { key }, data: { status: "aborted" } });
123
+ },
124
+ },
125
+ }
126
+ ```
127
+
128
+ **Cron job** — abort stale uploads and release S3 part storage:
129
+
130
+ ```ts
131
+ // Runs every hour
132
+ const stale = await db.upload.findMany({
133
+ where: {
134
+ status: "pending",
135
+ createdAt: { lt: new Date(Date.now() - 3_600_000) },
136
+ },
117
137
  });
118
- const { url: downloadUrl } = await api.download("photo.jpg");
119
- await api.delete("photo.jpg");
138
+ for (const upload of stale) {
139
+ await s3
140
+ .send(
141
+ new AbortMultipartUploadCommand({
142
+ Bucket: defaultBucket,
143
+ Key: upload.key,
144
+ UploadId: upload.uploadId,
145
+ }),
146
+ )
147
+ .catch(() => {});
148
+ await db.upload.update({
149
+ where: { key: upload.key },
150
+ data: { status: "expired" },
151
+ });
152
+ }
153
+ ```
154
+
155
+ **S3 lifecycle rule** — safety net for anything the cron misses (e.g. crash before `onInit`):
156
+
157
+ ```json
158
+ {
159
+ "Rules": [
160
+ {
161
+ "ID": "abort-incomplete-multipart",
162
+ "Status": "Enabled",
163
+ "Filter": { "Prefix": "" },
164
+ "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 1 }
165
+ }
166
+ ]
167
+ }
120
168
  ```
121
169
 
170
+ > Also rate-limit `/presign/multipart/part` per user/IP at the application layer.
171
+
172
+ ## API Routes
173
+
174
+ | Method | Path | Description |
175
+ | -------- | ----------------------------- | -------------------------------- |
176
+ | `POST` | `/presign/upload` | Presigned POST for direct upload |
177
+ | `POST` | `/presign/upload/confirm` | Confirm upload via HeadObject |
178
+ | `GET` | `/presign/download` | Presigned download URL |
179
+ | `DELETE` | `/delete` | Delete object |
180
+ | `POST` | `/presign/multipart/init` | Init multipart upload |
181
+ | `POST` | `/presign/multipart/part` | Sign a part URL |
182
+ | `POST` | `/presign/multipart/complete` | Complete + verify |
183
+ | `POST` | `/presign/multipart/abort` | Abort multipart |
184
+
122
185
  ## License
123
186
 
124
187
  MIT
@@ -1,3 +1,3 @@
1
- import {PutObjectCommand,HeadObjectCommand,GetObjectCommand,DeleteObjectCommand,CreateMultipartUploadCommand,UploadPartCommand,CompleteMultipartUploadCommand,AbortMultipartUploadCommand}from'@aws-sdk/client-s3';import {getSignedUrl}from'@aws-sdk/s3-request-presigner';async function l(t){try{let n=await t.json();return n&&typeof n=="object"?n:null}catch{return null}}function d(t,n){let r=typeof t=="string"?t.trim():"";return r||Response.json({message:`${n} is required`},{status:400})}function y(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):600}function p(t){return async n=>{try{return await t(n)}catch(r){let e=r instanceof Error?r.message:"Internal server error";return console.error("[S3 API]",e),Response.json({message:e},{status:500})}}}async function u(t,n){if(!t)return null;try{return await t(n),null}catch(r){let e=r instanceof Error?r.message:"Forbidden",a=typeof r?.status=="number"?r.status:403;return Response.json({message:e},{status:a})}}function k(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=y(r.expiresIn),o=r.acl==="public-read"?"public-read":"private",i=await u(t.hooks?.upload?.guard,{request:n,key:e,bucket:a,contentType:r.contentType,metadata:r.metadata,acl:o});if(i)return i;let m=await getSignedUrl(t.s3,new PutObjectCommand({Bucket:a,Key:e,ContentType:r.contentType,Metadata:r.metadata,ACL:o}),{expiresIn:s});return await t.hooks?.upload?.onSuccess?.({request:n,key:e,bucket:a,contentType:r.contentType,metadata:r.metadata,acl:o,url:m,expiresIn:s}),Response.json({bucket:a,key:e,url:m,expiresIn:s})})}function b(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=await u(t.hooks?.upload?.guard,{request:n,key:e,bucket:a});if(s)return s;let o=await t.s3.send(new HeadObjectCommand({Bucket:a,Key:e})),i={request:n,key:e,bucket:a,contentType:o.ContentType,contentLength:o.ContentLength??0,eTag:o.ETag?.replace(/"/g,""),metadata:o.Metadata};return await t.hooks?.upload?.onComplete?.(i),Response.json({key:e,bucket:a,contentType:i.contentType,contentLength:i.contentLength,eTag:i.eTag})})}function R(t){return p(async n=>{let{searchParams:r}=new URL(n.url),e=r.get("key")?.trim();if(!e)return Response.json({message:"key query parameter is required"},{status:400});let a=r.get("bucket")?.trim()||t.defaultBucket,s=y(r.get("expiresIn")),o=r.get("fileName")?.trim(),i=await u(t.hooks?.download?.guard,{request:n,key:e,bucket:a,fileName:o||void 0});if(i)return i;let m=await getSignedUrl(t.s3,new GetObjectCommand({Bucket:a,Key:e,ResponseContentDisposition:`attachment${o?`; filename="${o}"`:""}`}),{expiresIn:s});return await t.hooks?.download?.onSuccess?.({request:n,key:e,bucket:a,fileName:o||void 0,url:m,expiresIn:s}),Response.json({bucket:a,key:e,url:m,expiresIn:s})})}function w(t){return p(async n=>{let{searchParams:r}=new URL(n.url),e=r.get("key")?.trim();if(!e)return Response.json({message:"key query parameter is required"},{status:400});let a=r.get("bucket")?.trim()||t.defaultBucket,s=await u(t.hooks?.delete?.guard,{request:n,key:e,bucket:a});if(s)return s;try{await t.s3.send(new HeadObjectCommand({Bucket:a,Key:e}));}catch{return Response.json({message:`Object "${e}" not found`},{status:404})}return await t.s3.send(new DeleteObjectCommand({Bucket:a,Key:e})),await t.hooks?.delete?.onSuccess?.({request:n,key:e,bucket:a}),Response.json({success:!0,bucket:a,key:e})})}function H(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=r.acl==="public-read"?"public-read":"private",o=await u(t.hooks?.multipart?.guard,{request:n,key:e,bucket:a});if(o)return o;let{UploadId:i}=await t.s3.send(new CreateMultipartUploadCommand({Bucket:a,Key:e,ContentType:r.contentType,Metadata:r.metadata,ACL:s}));return await t.hooks?.multipart?.onInit?.({request:n,key:e,bucket:a,uploadId:i,contentType:r.contentType,metadata:r.metadata,acl:s}),Response.json({bucket:a,key:e,uploadId:i},{status:201})})}function S(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=d(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=Number(r.partNumber);if(!Number.isInteger(s)||s<=0)return Response.json({message:"partNumber must be a positive integer"},{status:400});let o=r.bucket?.trim()||t.defaultBucket,i=y(r.expiresIn),m=await u(t.hooks?.multipart?.guard,{request:n,key:e,bucket:o});if(m)return m;let f=await getSignedUrl(t.s3,new UploadPartCommand({Bucket:o,Key:e,UploadId:a,PartNumber:s}),{expiresIn:i});return Response.json({presignedUrl:f,partNumber:s,uploadId:a,bucket:o,expiresIn:i})})}function C(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=d(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=(Array.isArray(r.parts)?r.parts:[]).map(({partNumber:c,eTag:g})=>({PartNumber:Number(c),ETag:String(g)})).filter(c=>Number.isInteger(c.PartNumber)&&c.ETag).sort((c,g)=>c.PartNumber-g.PartNumber);if(!s.length)return Response.json({message:"At least one valid part is required"},{status:400});let o=r.bucket?.trim()||t.defaultBucket,i=await u(t.hooks?.multipart?.guard,{request:n,key:e,bucket:o});if(i)return i;let{Location:m,ETag:f}=await t.s3.send(new CompleteMultipartUploadCommand({Bucket:o,Key:e,UploadId:a,MultipartUpload:{Parts:s}}));return await t.hooks?.multipart?.onComplete?.({request:n,key:e,bucket:o,uploadId:a}),Response.json({bucket:o,key:e,uploadId:a,location:m,eTag:f})})}function h(t){return p(async n=>{let r=await l(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=d(r.key,"key");if(e instanceof Response)return e;let a=d(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=r.bucket?.trim()||t.defaultBucket,o=await u(t.hooks?.multipart?.guard,{request:n,key:e,bucket:s});return o||(await t.s3.send(new AbortMultipartUploadCommand({Bucket:s,Key:e,UploadId:a})),await t.hooks?.multipart?.onAbort?.({request:n,key:e,bucket:s,uploadId:a}),Response.json({bucket:s,key:e,uploadId:a,aborted:!0}))})}function I(t){return {presign:{upload:k(t),confirm:b(t),download:R(t)},multipart:{init:H(t),part:S(t),complete:C(t),abort:h(t)},delete:w(t)}}function P(t){let n=I(t),r=t.basePath.replace(/\/$/,"");return async e=>{let a=await u(t.hooks?.guard,{request:e});if(a)return a;let o=new URL(e.url).pathname.slice(r.length).replace(/^\//,""),i=e.method;return i==="POST"&&o==="presign/upload"?n.presign.upload(e):i==="POST"&&o==="presign/upload/confirm"?n.presign.confirm(e):i==="GET"&&o==="presign/download"?n.presign.download(e):i==="DELETE"&&o==="delete"?n.delete(e):i==="POST"&&o==="presign/multipart/init"?n.multipart.init(e):i==="POST"&&o==="presign/multipart/part"?n.multipart.part(e):i==="POST"&&o==="presign/multipart/complete"?n.multipart.complete(e):i==="POST"&&o==="presign/multipart/abort"?n.multipart.abort(e):Response.json({message:"Not Found"},{status:404})}}function Pe(t){return P(t)}
2
- export{Pe as createRouteHandler};//# sourceMappingURL=next.js.map
1
+ import {createPresignedPost}from'@aws-sdk/s3-presigned-post';import {HeadObjectCommand,GetObjectCommand,DeleteObjectCommand,CreateMultipartUploadCommand,UploadPartCommand,CompleteMultipartUploadCommand,AbortMultipartUploadCommand}from'@aws-sdk/client-s3';import {getSignedUrl}from'@aws-sdk/s3-request-presigner';function S(e){let n=e.replace(/[^\x20-\x7E]/g,"_").replace(/["\\]/g,"_"),t=encodeURIComponent(e);return `attachment; filename="${n}"; filename*=UTF-8''${t}`}async function m(e){try{let n=await e.json();return n&&typeof n=="object"?n:null}catch{return null}}function d(e,n){let t=typeof e=="string"?e.trim():"";return t||Response.json({message:`${n} is required`},{status:400})}function g(e){let n=Number(e);return Number.isFinite(n)&&n>0?Math.floor(n):600}function l(e){return async n=>{try{return await e(n)}catch(t){let r=t instanceof Error?t.message:"Internal server error";return console.error("[S3 API]",r),Response.json({message:r},{status:500})}}}async function p(e,n){if(!e)return null;try{return await e(n),null}catch(t){let r=t instanceof Error?t.message:"Forbidden",a=typeof t?.status=="number"?t.status:403;return Response.json({message:r},{status:a})}}var O=5*1024*1024*1024;function w(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=g(t.expiresIn),o=t.acl==="public-read"?"public-read":"private",i=t.contentType?.trim()||"application/octet-stream",u=typeof t.fileSize=="number"&&t.fileSize>0?Math.floor(t.fileSize):null;if(u!==null&&e.maxFileSize&&u>e.maxFileSize)return Response.json({message:`File size (${u} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:413});let c=await p(e.hooks?.upload?.guard,{request:n,key:r,bucket:a,contentType:t.contentType,fileSize:u??void 0,metadata:t.metadata,acl:o});if(c)return c;let f={acl:o,"Content-Type":i};if(t.fileName&&(f["Content-Disposition"]=S(t.fileName)),t.metadata)for(let[E,B]of Object.entries(t.metadata))f[`x-amz-meta-${E}`]=B;let b=e.maxFileSize??O,y=u??1,k=u??b,{url:R,fields:N}=await createPresignedPost(e.s3,{Bucket:a,Key:r,Conditions:[["content-length-range",y,k]],Fields:f,Expires:s});return await e.hooks?.upload?.onSuccess?.({request:n,key:r,bucket:a,contentType:t.contentType,metadata:t.metadata,acl:o,url:R,expiresIn:s}),Response.json({bucket:a,key:r,url:R,fields:N,expiresIn:s})})}function H(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=await p(e.hooks?.upload?.guard,{request:n,key:r,bucket:a});if(s)return s;let o=await e.s3.send(new HeadObjectCommand({Bucket:a,Key:r})),i={request:n,key:r,bucket:a,contentType:o.ContentType,contentLength:o.ContentLength??0,eTag:o.ETag?.replace(/"/g,""),metadata:o.Metadata};return await e.hooks?.upload?.onComplete?.(i),Response.json({key:r,bucket:a,contentType:i.contentType,contentLength:i.contentLength,eTag:i.eTag})})}function h(e){return l(async n=>{let{searchParams:t}=new URL(n.url),r=t.get("key")?.trim();if(!r)return Response.json({message:"key query parameter is required"},{status:400});let a=t.get("bucket")?.trim()||e.defaultBucket,s=g(t.get("expiresIn")),o=t.get("fileName")?.trim(),i=await p(e.hooks?.download?.guard,{request:n,key:r,bucket:a,fileName:o||void 0});if(i)return i;let u=await getSignedUrl(e.s3,new GetObjectCommand({Bucket:a,Key:r,...o?{ResponseContentDisposition:`attachment; filename="${o}"`}:{}}),{expiresIn:s});return await e.hooks?.download?.onSuccess?.({request:n,key:r,bucket:a,fileName:o||void 0,url:u,expiresIn:s}),Response.json({bucket:a,key:r,url:u,expiresIn:s})})}function x(e){return l(async n=>{let{searchParams:t}=new URL(n.url),r=t.get("key")?.trim();if(!r)return Response.json({message:"key query parameter is required"},{status:400});let a=t.get("bucket")?.trim()||e.defaultBucket,s=await p(e.hooks?.delete?.guard,{request:n,key:r,bucket:a});if(s)return s;try{await e.s3.send(new HeadObjectCommand({Bucket:a,Key:r}));}catch{return Response.json({message:`Object "${r}" not found`},{status:404})}return await e.s3.send(new DeleteObjectCommand({Bucket:a,Key:r})),await e.hooks?.delete?.onSuccess?.({request:n,key:r,bucket:a}),Response.json({success:!0,bucket:a,key:r})})}function C(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=t.bucket?.trim()||e.defaultBucket,s=t.acl==="public-read"?"public-read":"private",o=typeof t.fileSize=="number"&&t.fileSize>0?Math.floor(t.fileSize):void 0;if(e.maxFileSize&&o===void 0)return Response.json({message:"fileSize is required when maxFileSize is configured"},{status:400});if(o!==void 0&&e.maxFileSize&&o>e.maxFileSize)return Response.json({message:`File size (${o} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:413});let i=await p(e.hooks?.multipart?.guard,{request:n,key:r,bucket:a,fileSize:o});if(i)return i;let{UploadId:u}=await e.s3.send(new CreateMultipartUploadCommand({Bucket:a,Key:r,ContentType:t.contentType,ContentDisposition:t.fileName?S(t.fileName):void 0,Metadata:t.metadata,ACL:s}));return await e.hooks?.multipart?.onInit?.({request:n,key:r,bucket:a,uploadId:u,contentType:t.contentType,fileSize:o,metadata:t.metadata,acl:s}),Response.json({bucket:a,key:r,uploadId:u},{status:201})})}function I(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=d(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=Number(t.partNumber);if(!Number.isInteger(s)||s<=0)return Response.json({message:"partNumber must be a positive integer"},{status:400});let o=t.bucket?.trim()||e.defaultBucket,i=g(t.expiresIn),u=await p(e.hooks?.multipart?.guard,{request:n,key:r,bucket:o});if(u)return u;let c=await getSignedUrl(e.s3,new UploadPartCommand({Bucket:o,Key:r,UploadId:a,PartNumber:s}),{expiresIn:i});return Response.json({presignedUrl:c,partNumber:s,uploadId:a,bucket:o,expiresIn:i})})}function T(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=d(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=(Array.isArray(t.parts)?t.parts:[]).map(({partNumber:y,eTag:k})=>({PartNumber:Number(y),ETag:String(k)})).filter(y=>Number.isInteger(y.PartNumber)&&y.ETag).sort((y,k)=>y.PartNumber-k.PartNumber);if(!s.length)return Response.json({message:"At least one valid part is required"},{status:400});let o=t.bucket?.trim()||e.defaultBucket,i=await p(e.hooks?.multipart?.guard,{request:n,key:r,bucket:o});if(i)return i;await e.s3.send(new CompleteMultipartUploadCommand({Bucket:o,Key:r,UploadId:a,MultipartUpload:{Parts:s}}));let u=await e.s3.send(new HeadObjectCommand({Bucket:o,Key:r})),c=u.ContentLength??0,f=u.ContentType,b=u.ETag?.replace(/"/g,"");return e.maxFileSize&&c>e.maxFileSize?(await e.s3.send(new DeleteObjectCommand({Bucket:o,Key:r})).catch(()=>{}),Response.json({message:`File size (${c} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:422})):(await e.hooks?.multipart?.onComplete?.({request:n,key:r,bucket:o,uploadId:a,contentLength:c,contentType:f,eTag:b}),Response.json({bucket:o,key:r,uploadId:a,contentLength:c,contentType:f,eTag:b}))})}function P(e){return l(async n=>{let t=await m(n);if(!t)return Response.json({message:"Invalid JSON payload"},{status:400});let r=d(t.key,"key");if(r instanceof Response)return r;let a=d(t.uploadId,"uploadId");if(a instanceof Response)return a;let s=t.bucket?.trim()||e.defaultBucket,o=await p(e.hooks?.multipart?.guard,{request:n,key:r,bucket:s});return o||(await e.s3.send(new AbortMultipartUploadCommand({Bucket:s,Key:r,UploadId:a})),await e.hooks?.multipart?.onAbort?.({request:n,key:r,bucket:s,uploadId:a}),Response.json({bucket:s,key:r,uploadId:a,aborted:!0}))})}function j(e){return {presign:{upload:w(e),confirm:H(e),download:h(e)},multipart:{init:C(e),part:I(e),complete:T(e),abort:P(e)},delete:x(e)}}function z(e){let n=j(e),t=e.basePath.replace(/\/$/,"");return async r=>{let a=await p(e.hooks?.guard,{request:r});if(a)return a;let o=new URL(r.url).pathname.slice(t.length).replace(/^\//,""),i=r.method;return i==="POST"&&o==="presign/upload"?n.presign.upload(r):i==="POST"&&o==="presign/upload/confirm"?n.presign.confirm(r):i==="GET"&&o==="presign/download"?n.presign.download(r):i==="DELETE"&&o==="delete"?n.delete(r):i==="POST"&&o==="presign/multipart/init"?n.multipart.init(r):i==="POST"&&o==="presign/multipart/part"?n.multipart.part(r):i==="POST"&&o==="presign/multipart/complete"?n.multipart.complete(r):i==="POST"&&o==="presign/multipart/abort"?n.multipart.abort(r):Response.json({message:"Not Found"},{status:404})}}function Fe(e){return z(e)}
2
+ export{Fe as createRouteHandler};//# sourceMappingURL=next.js.map
3
3
  //# sourceMappingURL=next.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/helpers.ts","../../src/handlers/presign/upload.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/adapters/next.ts"],"names":["parseBody","request","body","requireString","value","name","trimmed","normalizeExpiresIn","withS3ErrorHandler","handler","err","message","runHook","hook","context","status","createUploadHandler","config","key","bucket","expiresIn","acl","guardResult","url","getSignedUrl","PutObjectCommand","createConfirmHandler","head","HeadObjectCommand","createDownloadHandler","searchParams","fileName","GetObjectCommand","createDeleteHandler","DeleteObjectCommand","createMultipartInitHandler","UploadId","CreateMultipartUploadCommand","createMultipartPartHandler","uploadId","partNumber","presignedUrl","UploadPartCommand","createMultipartCompleteHandler","parts","eTag","p","a","b","Location","ETag","CompleteMultipartUploadCommand","createMultipartAbortHandler","AbortMultipartUploadCommand","createHandlers","createRouter","handlers","base","subpath","method","createRouteHandler"],"mappings":"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","file":"next.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 message =\n err instanceof Error ? err.message : \"Internal server error\";\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 500 });\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","import { PutObjectCommand } 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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n contentType?: string;\n metadata?: Record<string, string>;\n bucket?: string;\n expiresIn?: number;\n acl?: \"private\" | \"public-read\";\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\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n\n const guardResult = await runHook(config.hooks?.upload?.guard, {\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: body.contentType,\n Metadata: body.metadata,\n ACL: acl,\n }),\n { expiresIn },\n );\n\n await config.hooks?.upload?.onSuccess?.({\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({ bucket, key, url, expiresIn });\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 \"../../helpers\";\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.hooks?.upload?.guard, {\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 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 };\n\n await config.hooks?.upload?.onComplete?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n });\n });\n}\n","import { GetObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport { normalizeExpiresIn, runHook, withS3ErrorHandler } 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.hooks?.download?.guard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ResponseContentDisposition: `attachment${fileName ? `; filename=\"${fileName}\"` : \"\"}`,\n }),\n { expiresIn },\n );\n\n await config.hooks?.download?.onSuccess?.({\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 \"../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.hooks?.delete?.guard, {\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(\n { message: `Object \"${key}\" not found` },\n { status: 404 },\n );\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.hooks?.delete?.onSuccess?.({ 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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n contentType?: string;\n metadata?: Record<string, string>;\n acl?: \"private\" | \"public-read\";\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\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\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 Metadata: body.metadata,\n ACL: acl,\n }),\n );\n\n await config.hooks?.multipart?.onInit?.({\n request,\n key,\n bucket,\n uploadId: UploadId!,\n contentType: body.contentType,\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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: 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\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n }),\n { expiresIn },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n });\n });\n}\n","import { CompleteMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\ntype PartEntry = {\n partNumber: number;\n eTag: string;\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, eTag }) => ({\n PartNumber: Number(partNumber),\n ETag: String(eTag),\n }))\n .filter((p) => Number.isInteger(p.PartNumber) && p.ETag)\n .sort((a, b) => a.PartNumber - b.PartNumber);\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.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const { Location, ETag } = await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: parts },\n }),\n );\n\n await config.hooks?.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n });\n\n return Response.json({\n bucket,\n key,\n uploadId,\n location: Location,\n eTag: ETag,\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 \"../../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.hooks?.multipart?.guard, {\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.hooks?.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 \"./helpers\";\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.hooks?.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 handlers.presign.upload(request);\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return handlers.presign.confirm(request);\n if (method === \"GET\" && subpath === \"presign/download\")\n return handlers.presign.download(request);\n if (method === \"DELETE\" && subpath === \"delete\")\n return handlers.delete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return handlers.multipart.init(request);\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return handlers.multipart.part(request);\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return handlers.multipart.complete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return handlers.multipart.abort(request);\n\n return Response.json({ message: \"Not Found\" }, { status: 404 });\n };\n}\n","import { createRouter } from \"../router\";\nimport type { S3RouteHandlerConfig } from \"../types\";\n\n/**\n * Creates a Next.js App Router catch-all route handler.\n *\n * Usage in `app/api/s3/[...s3]/route.ts`:\n * ```ts\n * import { createRouteHandler } from \"@better-s3/server/next\";\n *\n * const handler = createRouteHandler({ s3, defaultBucket: \"my-bucket\", basePath: \"/api/s3\" });\n * export { handler as GET, handler as POST, handler as DELETE };\n * ```\n */\nexport function createRouteHandler(config: S3RouteHandlerConfig) {\n return createRouter(config);\n}\n"]}
1
+ {"version":3,"sources":["../../src/helpers.ts","../../src/handlers/presign/upload.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/adapters/next.ts"],"names":["buildContentDisposition","fileName","ascii","encoded","parseBody","request","body","requireString","value","name","trimmed","normalizeExpiresIn","withS3ErrorHandler","handler","err","message","runHook","hook","context","status","MAX_UPLOAD_SIZE","createUploadHandler","config","key","bucket","expiresIn","acl","contentType","fileSize","guardResult","fields","k","v","maxBytes","rangeMin","rangeMax","url","signedFields","createPresignedPost","createConfirmHandler","head","HeadObjectCommand","createDownloadHandler","searchParams","getSignedUrl","GetObjectCommand","createDeleteHandler","DeleteObjectCommand","createMultipartInitHandler","UploadId","CreateMultipartUploadCommand","createMultipartPartHandler","uploadId","partNumber","presignedUrl","UploadPartCommand","createMultipartCompleteHandler","parts","eTag","p","a","b","CompleteMultipartUploadCommand","contentLength","createMultipartAbortHandler","AbortMultipartUploadCommand","createHandlers","createRouter","handlers","base","subpath","method","createRouteHandler"],"mappings":"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","file":"next.js","sourcesContent":["/**\n * Builds a RFC 6266 `Content-Disposition` value for a file attachment.\n * Includes both the ASCII-safe `filename` fallback and the RFC 5987\n * `filename*` parameter for full Unicode support.\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\nexport 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 message =\n err instanceof Error ? err.message : \"Internal server error\";\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 500 });\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","import { createPresignedPost } from \"@aws-sdk/s3-presigned-post\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n buildContentDisposition,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\n// 5 GiB — S3 single-object size limit\nconst MAX_UPLOAD_SIZE = 5 * 1024 * 1024 * 1024;\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 // Reject immediately if the declared size already exceeds the server limit,\n // before generating any presigned URL.\n if (\n fileSize !== null &&\n config.maxFileSize &&\n fileSize > config.maxFileSize\n ) {\n return Response.json(\n {\n message: `File size (${fileSize} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 413 },\n );\n }\n\n const guardResult = await runHook(config.hooks?.upload?.guard, {\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 // Build presigned POST fields. Fields are embedded in the signed policy\n // so the client cannot change them without invalidating the signature.\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 // `content-length-range` is enforced by S3 at the storage layer —\n // uploads outside this range are rejected before the object is stored.\n //\n // When the client declares fileSize: S3 locks the range to exactly that\n // number of bytes, so uploading a different-sized file is impossible.\n // When fileSize is not declared: falls back to [1, maxFileSize ?? 5 GiB].\n const maxBytes = config.maxFileSize ?? MAX_UPLOAD_SIZE;\n const rangeMin = fileSize ?? 1;\n const rangeMax = fileSize ?? maxBytes;\n\n const { url, fields: signedFields } = await createPresignedPost(config.s3, {\n Bucket: bucket,\n Key: key,\n Conditions: [[\"content-length-range\", rangeMin, rangeMax]],\n Fields: fields,\n Expires: expiresIn,\n });\n\n await config.hooks?.upload?.onSuccess?.({\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({ bucket, key, url, fields: signedFields, expiresIn });\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 \"../../helpers\";\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.hooks?.upload?.guard, {\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 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 };\n\n await config.hooks?.upload?.onComplete?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n });\n });\n}\n","import { GetObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport { normalizeExpiresIn, runHook, withS3ErrorHandler } 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.hooks?.download?.guard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ...(fileName\n ? { ResponseContentDisposition: `attachment; filename=\"${fileName}\"` }\n : {}),\n }),\n { expiresIn },\n );\n\n await config.hooks?.download?.onSuccess?.({\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 \"../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.hooks?.delete?.guard, {\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(\n { message: `Object \"${key}\" not found` },\n { status: 404 },\n );\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.hooks?.delete?.onSuccess?.({ 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 buildContentDisposition,\n runHook,\n withS3ErrorHandler,\n} 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 // enforce the limit at init time (before the multipart upload is created).\n if (config.maxFileSize && fileSize === undefined) {\n return Response.json(\n { message: \"fileSize is required when maxFileSize is configured\" },\n { status: 400 },\n );\n }\n\n // Reject immediately if the declared size already exceeds the server limit.\n if (\n fileSize !== undefined &&\n config.maxFileSize &&\n fileSize > config.maxFileSize\n ) {\n return Response.json(\n {\n message: `File size (${fileSize} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 413 },\n );\n }\n\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\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.hooks?.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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: 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\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n }),\n { expiresIn },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n });\n });\n}\n","import { CompleteMultipartUploadCommand, HeadObjectCommand, DeleteObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\ntype PartEntry = {\n partNumber: number;\n eTag: string;\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, eTag }) => ({\n PartNumber: Number(partNumber),\n ETag: String(eTag),\n }))\n .filter((p) => Number.isInteger(p.PartNumber) && p.ETag)\n .sort((a, b) => a.PartNumber - b.PartNumber);\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.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: parts },\n }),\n );\n\n // Verify the actual uploaded object metadata via HeadObject.\n // This is the server's only opportunity to enforce maxFileSize for\n // multipart uploads, since individual parts bypass size checks.\n const head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n const contentLength = head.ContentLength ?? 0;\n const contentType = head.ContentType;\n const eTag = head.ETag?.replace(/\"/g, \"\");\n\n if (config.maxFileSize && contentLength > config.maxFileSize) {\n // Delete the object and reject the request. The client has uploaded\n // more data than the server policy allows.\n await config.s3\n .send(new DeleteObjectCommand({ Bucket: bucket, Key: key }))\n .catch(() => {});\n return Response.json(\n {\n message: `File size (${contentLength} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 422 },\n );\n }\n\n await config.hooks?.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n contentLength,\n contentType,\n eTag,\n });\n\n return Response.json({ bucket, key, uploadId, contentLength, contentType, eTag });\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 \"../../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.hooks?.multipart?.guard, {\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.hooks?.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 \"./helpers\";\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.hooks?.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 handlers.presign.upload(request);\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return handlers.presign.confirm(request);\n if (method === \"GET\" && subpath === \"presign/download\")\n return handlers.presign.download(request);\n if (method === \"DELETE\" && subpath === \"delete\")\n return handlers.delete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return handlers.multipart.init(request);\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return handlers.multipart.part(request);\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return handlers.multipart.complete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return handlers.multipart.abort(request);\n\n return Response.json({ message: \"Not Found\" }, { status: 404 });\n };\n}\n","import { createRouter } from \"../router\";\nimport type { S3RouteHandlerConfig } from \"../types\";\n\n/**\n * Creates a Next.js App Router catch-all route handler.\n *\n * Usage in `app/api/s3/[...s3]/route.ts`:\n * ```ts\n * import { createRouteHandler } from \"@better-s3/server/next\";\n *\n * const handler = createRouteHandler({ s3, defaultBucket: \"my-bucket\", basePath: \"/api/s3\" });\n * export { handler as GET, handler as POST, handler as DELETE };\n * ```\n */\nexport function createRouteHandler(config: S3RouteHandlerConfig) {\n return createRouter(config);\n}\n"]}
package/dist/api.d.ts CHANGED
@@ -4,6 +4,13 @@ export type PresignResponse = {
4
4
  url: string;
5
5
  expiresIn: number;
6
6
  };
7
+ export type PresignedPostResponse = {
8
+ key: string;
9
+ bucket: string;
10
+ url: string;
11
+ fields: Record<string, string>;
12
+ expiresIn: number;
13
+ };
7
14
  export type MultipartInitResponse = {
8
15
  key: string;
9
16
  bucket: string;
@@ -27,10 +34,18 @@ export type S3Api = {
27
34
  upload: (payload: {
28
35
  key: string;
29
36
  contentType?: string;
37
+ /**
38
+ * Exact byte size of the file. When provided the presigned POST policy
39
+ * locks `content-length-range` to `[fileSize, fileSize]` so S3 rejects
40
+ * uploads of any other size at the storage layer.
41
+ */
42
+ fileSize?: number;
30
43
  metadata?: Record<string, string>;
31
44
  bucket?: string;
32
45
  acl?: "private" | "public-read";
33
- }) => Promise<PresignResponse>;
46
+ /** Original file name. Stored as `Content-Disposition` on the S3 object. */
47
+ fileName?: string;
48
+ }) => Promise<PresignedPostResponse>;
34
49
  confirm: (payload: {
35
50
  key: string;
36
51
  bucket?: string;
@@ -50,9 +65,13 @@ export type S3Api = {
50
65
  init: (payload: {
51
66
  key: string;
52
67
  contentType?: string;
68
+ /** Declared total byte size of the file. Used for quota/guard checks and stored in `onInit` context. */
69
+ fileSize?: number;
53
70
  metadata?: Record<string, string>;
54
71
  bucket?: string;
55
72
  acl?: "private" | "public-read";
73
+ /** Original file name. Stored as `Content-Disposition` on the S3 object. */
74
+ fileName?: string;
56
75
  }) => Promise<MultipartInitResponse>;
57
76
  signPart: (payload: {
58
77
  key: string;
@@ -72,6 +91,9 @@ export type S3Api = {
72
91
  key: string;
73
92
  bucket: string;
74
93
  uploadId: string;
94
+ contentLength: number;
95
+ contentType?: string;
96
+ eTag?: string;
75
97
  }>;
76
98
  abort: (payload: {
77
99
  key: string;
package/dist/helpers.d.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Builds a RFC 6266 `Content-Disposition` value for a file attachment.
3
+ * Includes both the ASCII-safe `filename` fallback and the RFC 5987
4
+ * `filename*` parameter for full Unicode support.
5
+ *
6
+ * @example
7
+ * buildContentDisposition("résumé finale.pdf")
8
+ * // → 'attachment; filename="resume finale.pdf"; filename*=UTF-8\'\'r%C3%A9sum%C3%A9%20finale.pdf'
9
+ */
10
+ export declare function buildContentDisposition(fileName: string): string;
1
11
  export declare function parseBody<T extends Record<string, unknown>>(request: Request): Promise<T | null>;
2
12
  export declare function requireString(value: unknown, name: string): string | Response;
3
13
  export declare function normalizeExpiresIn(value: unknown): number;
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export { createMultipartInitHandler } from "./handlers/multipart/init";
8
8
  export { createMultipartPartHandler } from "./handlers/multipart/part";
9
9
  export { createMultipartCompleteHandler } from "./handlers/multipart/complete";
10
10
  export { createMultipartAbortHandler } from "./handlers/multipart/abort";
11
- export { createS3Api, type S3Api, type PresignResponse, type MultipartInitResponse, type MultipartPartResponse, type UploadConfirmResponse, } from "./api";
11
+ export { createS3Api, type S3Api, type PresignResponse, type PresignedPostResponse, type MultipartInitResponse, type MultipartPartResponse, type UploadConfirmResponse, } from "./api";
12
12
  export { createS3Api as createPresignApi, type S3Api as PresignApi, } from "./api";
13
13
  export { validateFile } from "./validate";
14
14
  export type { S3HandlerConfig, S3RouteHandlerConfig, S3Handler, S3Handlers, S3ServerHooks, HookContext, UploadHookContext, UploadSuccessContext, UploadCompleteContext, DownloadHookContext, DownloadSuccessContext, DeleteHookContext, MultipartHookContext, MultipartInitSuccessContext, MultipartCompleteSuccessContext, } from "./types";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import {PutObjectCommand,HeadObjectCommand,GetObjectCommand,DeleteObjectCommand,CreateMultipartUploadCommand,UploadPartCommand,CompleteMultipartUploadCommand,AbortMultipartUploadCommand}from'@aws-sdk/client-s3';import {getSignedUrl}from'@aws-sdk/s3-request-presigner';async function d(t){try{let n=await t.json();return n&&typeof n=="object"?n:null}catch{return null}}function l(t,n){let r=typeof t=="string"?t.trim():"";return r||Response.json({message:`${n} is required`},{status:400})}function c(t){let n=Number(t);return Number.isFinite(n)&&n>0?Math.floor(n):600}function u(t){return async n=>{try{return await t(n)}catch(r){let e=r instanceof Error?r.message:"Internal server error";return console.error("[S3 API]",e),Response.json({message:e},{status:500})}}}async function p(t,n){if(!t)return null;try{return await t(n),null}catch(r){let e=r instanceof Error?r.message:"Forbidden",a=typeof r?.status=="number"?r.status:403;return Response.json({message:e},{status:a})}}function k(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=c(r.expiresIn),o=r.acl==="public-read"?"public-read":"private",i=await p(t.hooks?.upload?.guard,{request:n,key:e,bucket:a,contentType:r.contentType,metadata:r.metadata,acl:o});if(i)return i;let m=await getSignedUrl(t.s3,new PutObjectCommand({Bucket:a,Key:e,ContentType:r.contentType,Metadata:r.metadata,ACL:o}),{expiresIn:s});return await t.hooks?.upload?.onSuccess?.({request:n,key:e,bucket:a,contentType:r.contentType,metadata:r.metadata,acl:o,url:m,expiresIn:s}),Response.json({bucket:a,key:e,url:m,expiresIn:s})})}function b(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=await p(t.hooks?.upload?.guard,{request:n,key:e,bucket:a});if(s)return s;let o=await t.s3.send(new HeadObjectCommand({Bucket:a,Key:e})),i={request:n,key:e,bucket:a,contentType:o.ContentType,contentLength:o.ContentLength??0,eTag:o.ETag?.replace(/"/g,""),metadata:o.Metadata};return await t.hooks?.upload?.onComplete?.(i),Response.json({key:e,bucket:a,contentType:i.contentType,contentLength:i.contentLength,eTag:i.eTag})})}function R(t){return u(async n=>{let{searchParams:r}=new URL(n.url),e=r.get("key")?.trim();if(!e)return Response.json({message:"key query parameter is required"},{status:400});let a=r.get("bucket")?.trim()||t.defaultBucket,s=c(r.get("expiresIn")),o=r.get("fileName")?.trim(),i=await p(t.hooks?.download?.guard,{request:n,key:e,bucket:a,fileName:o||void 0});if(i)return i;let m=await getSignedUrl(t.s3,new GetObjectCommand({Bucket:a,Key:e,ResponseContentDisposition:`attachment${o?`; filename="${o}"`:""}`}),{expiresIn:s});return await t.hooks?.download?.onSuccess?.({request:n,key:e,bucket:a,fileName:o||void 0,url:m,expiresIn:s}),Response.json({bucket:a,key:e,url:m,expiresIn:s})})}function w(t){return u(async n=>{let{searchParams:r}=new URL(n.url),e=r.get("key")?.trim();if(!e)return Response.json({message:"key query parameter is required"},{status:400});let a=r.get("bucket")?.trim()||t.defaultBucket,s=await p(t.hooks?.delete?.guard,{request:n,key:e,bucket:a});if(s)return s;try{await t.s3.send(new HeadObjectCommand({Bucket:a,Key:e}));}catch{return Response.json({message:`Object "${e}" not found`},{status:404})}return await t.s3.send(new DeleteObjectCommand({Bucket:a,Key:e})),await t.hooks?.delete?.onSuccess?.({request:n,key:e,bucket:a}),Response.json({success:!0,bucket:a,key:e})})}function S(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=r.bucket?.trim()||t.defaultBucket,s=r.acl==="public-read"?"public-read":"private",o=await p(t.hooks?.multipart?.guard,{request:n,key:e,bucket:a});if(o)return o;let{UploadId:i}=await t.s3.send(new CreateMultipartUploadCommand({Bucket:a,Key:e,ContentType:r.contentType,Metadata:r.metadata,ACL:s}));return await t.hooks?.multipart?.onInit?.({request:n,key:e,bucket:a,uploadId:i,contentType:r.contentType,metadata:r.metadata,acl:s}),Response.json({bucket:a,key:e,uploadId:i},{status:201})})}function H(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=l(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=Number(r.partNumber);if(!Number.isInteger(s)||s<=0)return Response.json({message:"partNumber must be a positive integer"},{status:400});let o=r.bucket?.trim()||t.defaultBucket,i=c(r.expiresIn),m=await p(t.hooks?.multipart?.guard,{request:n,key:e,bucket:o});if(m)return m;let y=await getSignedUrl(t.s3,new UploadPartCommand({Bucket:o,Key:e,UploadId:a,PartNumber:s}),{expiresIn:i});return Response.json({presignedUrl:y,partNumber:s,uploadId:a,bucket:o,expiresIn:i})})}function x(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=l(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=(Array.isArray(r.parts)?r.parts:[]).map(({partNumber:g,eTag:f})=>({PartNumber:Number(g),ETag:String(f)})).filter(g=>Number.isInteger(g.PartNumber)&&g.ETag).sort((g,f)=>g.PartNumber-f.PartNumber);if(!s.length)return Response.json({message:"At least one valid part is required"},{status:400});let o=r.bucket?.trim()||t.defaultBucket,i=await p(t.hooks?.multipart?.guard,{request:n,key:e,bucket:o});if(i)return i;let{Location:m,ETag:y}=await t.s3.send(new CompleteMultipartUploadCommand({Bucket:o,Key:e,UploadId:a,MultipartUpload:{Parts:s}}));return await t.hooks?.multipart?.onComplete?.({request:n,key:e,bucket:o,uploadId:a}),Response.json({bucket:o,key:e,uploadId:a,location:m,eTag:y})})}function P(t){return u(async n=>{let r=await d(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let e=l(r.key,"key");if(e instanceof Response)return e;let a=l(r.uploadId,"uploadId");if(a instanceof Response)return a;let s=r.bucket?.trim()||t.defaultBucket,o=await p(t.hooks?.multipart?.guard,{request:n,key:e,bucket:s});return o||(await t.s3.send(new AbortMultipartUploadCommand({Bucket:s,Key:e,UploadId:a})),await t.hooks?.multipart?.onAbort?.({request:n,key:e,bucket:s,uploadId:a}),Response.json({bucket:s,key:e,uploadId:a,aborted:!0}))})}function C(t){return {presign:{upload:k(t),confirm:b(t),download:R(t)},multipart:{init:S(t),part:H(t),complete:x(t),abort:P(t)},delete:w(t)}}function L(t){let n=C(t),r=t.basePath.replace(/\/$/,"");return async e=>{let a=await p(t.hooks?.guard,{request:e});if(a)return a;let o=new URL(e.url).pathname.slice(r.length).replace(/^\//,""),i=e.method;return i==="POST"&&o==="presign/upload"?n.presign.upload(e):i==="POST"&&o==="presign/upload/confirm"?n.presign.confirm(e):i==="GET"&&o==="presign/download"?n.presign.download(e):i==="DELETE"&&o==="delete"?n.delete(e):i==="POST"&&o==="presign/multipart/init"?n.multipart.init(e):i==="POST"&&o==="presign/multipart/part"?n.multipart.part(e):i==="POST"&&o==="presign/multipart/complete"?n.multipart.complete(e):i==="POST"&&o==="presign/multipart/abort"?n.multipart.abort(e):Response.json({message:"Not Found"},{status:404})}}function h(t="/api/s3"){let n=t.replace(/\/$/,""),r=async(a,s)=>{let o=await fetch(a,s);if(!o.ok){let i=await o.json().catch(()=>({}));throw new Error(i.message??o.statusText)}return o.json()},e=(a,s)=>r(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});return {upload(a){return e(`${n}/presign/upload`,a)},confirm(a){return e(`${n}/presign/upload/confirm`,a)},download(a,s){let o=new URLSearchParams({key:a});if(s?.fileName){let i=s.fileName.replace(/["\\\r\n]/g,"_");o.set("fileName",i);}return s?.bucket&&o.set("bucket",s.bucket),r(`${n}/presign/download?${o}`)},delete(a,s){let o=new URLSearchParams({key:a});return s?.bucket&&o.set("bucket",s.bucket),r(`${n}/delete?${o}`,{method:"DELETE"})},multipart:{init(a){return e(`${n}/presign/multipart/init`,a)},signPart(a){return e(`${n}/presign/multipart/part`,a)},complete(a){return e(`${n}/presign/multipart/complete`,a)},abort(a){return e(`${n}/presign/multipart/abort`,a)}}}}function F(t,n){if(n.accept?.length&&!n.accept.some(e=>e.startsWith(".")?t.name.toLowerCase().endsWith(e.toLowerCase()):e.endsWith("/*")?t.type.startsWith(e.replace("/*","/")):t.type===e)){let e=t.name.includes(".")?t.name.split(".").pop():null;return `File type "${e?`.${e}`:t.type||"unknown"}" is not allowed`}return t.size===0?"File is empty":n.maxFileSize&&t.size>n.maxFileSize?`File size exceeds ${(n.maxFileSize/1048576).toFixed(1)} MB limit`:null}
2
- export{b as createConfirmHandler,w as createDeleteHandler,R as createDownloadHandler,C as createHandlers,P as createMultipartAbortHandler,x as createMultipartCompleteHandler,S as createMultipartInitHandler,H as createMultipartPartHandler,h as createPresignApi,L as createRouter,h as createS3Api,k as createUploadHandler,c as normalizeExpiresIn,d as parseBody,l as requireString,p as runHook,F as validateFile,u as withS3ErrorHandler};//# sourceMappingURL=index.js.map
1
+ import {createPresignedPost}from'@aws-sdk/s3-presigned-post';import {HeadObjectCommand,GetObjectCommand,DeleteObjectCommand,CreateMultipartUploadCommand,UploadPartCommand,CompleteMultipartUploadCommand,AbortMultipartUploadCommand}from'@aws-sdk/client-s3';import {getSignedUrl}from'@aws-sdk/s3-request-presigner';function S(e){let n=e.replace(/[^\x20-\x7E]/g,"_").replace(/["\\]/g,"_"),r=encodeURIComponent(e);return `attachment; filename="${n}"; filename*=UTF-8''${r}`}async function m(e){try{let n=await e.json();return n&&typeof n=="object"?n:null}catch{return null}}function d(e,n){let r=typeof e=="string"?e.trim():"";return r||Response.json({message:`${n} is required`},{status:400})}function y(e){let n=Number(e);return Number.isFinite(n)&&n>0?Math.floor(n):600}function l(e){return async n=>{try{return await e(n)}catch(r){let t=r instanceof Error?r.message:"Internal server error";return console.error("[S3 API]",t),Response.json({message:t},{status:500})}}}async function u(e,n){if(!e)return null;try{return await e(n),null}catch(r){let t=r instanceof Error?r.message:"Forbidden",o=typeof r?.status=="number"?r.status:403;return Response.json({message:t},{status:o})}}var B=5*1024*1024*1024;function R(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=r.bucket?.trim()||e.defaultBucket,s=y(r.expiresIn),a=r.acl==="public-read"?"public-read":"private",i=r.contentType?.trim()||"application/octet-stream",p=typeof r.fileSize=="number"&&r.fileSize>0?Math.floor(r.fileSize):null;if(p!==null&&e.maxFileSize&&p>e.maxFileSize)return Response.json({message:`File size (${p} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:413});let c=await u(e.hooks?.upload?.guard,{request:n,key:t,bucket:o,contentType:r.contentType,fileSize:p??void 0,metadata:r.metadata,acl:a});if(c)return c;let f={acl:a,"Content-Type":i};if(r.fileName&&(f["Content-Disposition"]=S(r.fileName)),r.metadata)for(let[M,E]of Object.entries(r.metadata))f[`x-amz-meta-${M}`]=E;let b=e.maxFileSize??B,g=p??1,k=p??b,{url:z,fields:j}=await createPresignedPost(e.s3,{Bucket:o,Key:t,Conditions:[["content-length-range",g,k]],Fields:f,Expires:s});return await e.hooks?.upload?.onSuccess?.({request:n,key:t,bucket:o,contentType:r.contentType,metadata:r.metadata,acl:a,url:z,expiresIn:s}),Response.json({bucket:o,key:t,url:z,fields:j,expiresIn:s})})}function x(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=r.bucket?.trim()||e.defaultBucket,s=await u(e.hooks?.upload?.guard,{request:n,key:t,bucket:o});if(s)return s;let a=await e.s3.send(new HeadObjectCommand({Bucket:o,Key:t})),i={request:n,key:t,bucket:o,contentType:a.ContentType,contentLength:a.ContentLength??0,eTag:a.ETag?.replace(/"/g,""),metadata:a.Metadata};return await e.hooks?.upload?.onComplete?.(i),Response.json({key:t,bucket:o,contentType:i.contentType,contentLength:i.contentLength,eTag:i.eTag})})}function w(e){return l(async n=>{let{searchParams:r}=new URL(n.url),t=r.get("key")?.trim();if(!t)return Response.json({message:"key query parameter is required"},{status:400});let o=r.get("bucket")?.trim()||e.defaultBucket,s=y(r.get("expiresIn")),a=r.get("fileName")?.trim(),i=await u(e.hooks?.download?.guard,{request:n,key:t,bucket:o,fileName:a||void 0});if(i)return i;let p=await getSignedUrl(e.s3,new GetObjectCommand({Bucket:o,Key:t,...a?{ResponseContentDisposition:`attachment; filename="${a}"`}:{}}),{expiresIn:s});return await e.hooks?.download?.onSuccess?.({request:n,key:t,bucket:o,fileName:a||void 0,url:p,expiresIn:s}),Response.json({bucket:o,key:t,url:p,expiresIn:s})})}function H(e){return l(async n=>{let{searchParams:r}=new URL(n.url),t=r.get("key")?.trim();if(!t)return Response.json({message:"key query parameter is required"},{status:400});let o=r.get("bucket")?.trim()||e.defaultBucket,s=await u(e.hooks?.delete?.guard,{request:n,key:t,bucket:o});if(s)return s;try{await e.s3.send(new HeadObjectCommand({Bucket:o,Key:t}));}catch{return Response.json({message:`Object "${t}" not found`},{status:404})}return await e.s3.send(new DeleteObjectCommand({Bucket:o,Key:t})),await e.hooks?.delete?.onSuccess?.({request:n,key:t,bucket:o}),Response.json({success:!0,bucket:o,key:t})})}function C(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=r.bucket?.trim()||e.defaultBucket,s=r.acl==="public-read"?"public-read":"private",a=typeof r.fileSize=="number"&&r.fileSize>0?Math.floor(r.fileSize):void 0;if(e.maxFileSize&&a===void 0)return Response.json({message:"fileSize is required when maxFileSize is configured"},{status:400});if(a!==void 0&&e.maxFileSize&&a>e.maxFileSize)return Response.json({message:`File size (${a} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:413});let i=await u(e.hooks?.multipart?.guard,{request:n,key:t,bucket:o,fileSize:a});if(i)return i;let{UploadId:p}=await e.s3.send(new CreateMultipartUploadCommand({Bucket:o,Key:t,ContentType:r.contentType,ContentDisposition:r.fileName?S(r.fileName):void 0,Metadata:r.metadata,ACL:s}));return await e.hooks?.multipart?.onInit?.({request:n,key:t,bucket:o,uploadId:p,contentType:r.contentType,fileSize:a,metadata:r.metadata,acl:s}),Response.json({bucket:o,key:t,uploadId:p},{status:201})})}function P(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=d(r.uploadId,"uploadId");if(o instanceof Response)return o;let s=Number(r.partNumber);if(!Number.isInteger(s)||s<=0)return Response.json({message:"partNumber must be a positive integer"},{status:400});let a=r.bucket?.trim()||e.defaultBucket,i=y(r.expiresIn),p=await u(e.hooks?.multipart?.guard,{request:n,key:t,bucket:a});if(p)return p;let c=await getSignedUrl(e.s3,new UploadPartCommand({Bucket:a,Key:t,UploadId:o,PartNumber:s}),{expiresIn:i});return Response.json({presignedUrl:c,partNumber:s,uploadId:o,bucket:a,expiresIn:i})})}function h(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=d(r.uploadId,"uploadId");if(o instanceof Response)return o;let s=(Array.isArray(r.parts)?r.parts:[]).map(({partNumber:g,eTag:k})=>({PartNumber:Number(g),ETag:String(k)})).filter(g=>Number.isInteger(g.PartNumber)&&g.ETag).sort((g,k)=>g.PartNumber-k.PartNumber);if(!s.length)return Response.json({message:"At least one valid part is required"},{status:400});let a=r.bucket?.trim()||e.defaultBucket,i=await u(e.hooks?.multipart?.guard,{request:n,key:t,bucket:a});if(i)return i;await e.s3.send(new CompleteMultipartUploadCommand({Bucket:a,Key:t,UploadId:o,MultipartUpload:{Parts:s}}));let p=await e.s3.send(new HeadObjectCommand({Bucket:a,Key:t})),c=p.ContentLength??0,f=p.ContentType,b=p.ETag?.replace(/"/g,"");return e.maxFileSize&&c>e.maxFileSize?(await e.s3.send(new DeleteObjectCommand({Bucket:a,Key:t})).catch(()=>{}),Response.json({message:`File size (${c} bytes) exceeds the maximum allowed size of ${e.maxFileSize} bytes`},{status:422})):(await e.hooks?.multipart?.onComplete?.({request:n,key:t,bucket:a,uploadId:o,contentLength:c,contentType:f,eTag:b}),Response.json({bucket:a,key:t,uploadId:o,contentLength:c,contentType:f,eTag:b}))})}function I(e){return l(async n=>{let r=await m(n);if(!r)return Response.json({message:"Invalid JSON payload"},{status:400});let t=d(r.key,"key");if(t instanceof Response)return t;let o=d(r.uploadId,"uploadId");if(o instanceof Response)return o;let s=r.bucket?.trim()||e.defaultBucket,a=await u(e.hooks?.multipart?.guard,{request:n,key:t,bucket:s});return a||(await e.s3.send(new AbortMultipartUploadCommand({Bucket:s,Key:t,UploadId:o})),await e.hooks?.multipart?.onAbort?.({request:n,key:t,bucket:s,uploadId:o}),Response.json({bucket:s,key:t,uploadId:o,aborted:!0}))})}function T(e){return {presign:{upload:R(e),confirm:x(e),download:w(e)},multipart:{init:C(e),part:P(e),complete:h(e),abort:I(e)},delete:H(e)}}function G(e){let n=T(e),r=e.basePath.replace(/\/$/,"");return async t=>{let o=await u(e.hooks?.guard,{request:t});if(o)return o;let a=new URL(t.url).pathname.slice(r.length).replace(/^\//,""),i=t.method;return i==="POST"&&a==="presign/upload"?n.presign.upload(t):i==="POST"&&a==="presign/upload/confirm"?n.presign.confirm(t):i==="GET"&&a==="presign/download"?n.presign.download(t):i==="DELETE"&&a==="delete"?n.delete(t):i==="POST"&&a==="presign/multipart/init"?n.multipart.init(t):i==="POST"&&a==="presign/multipart/part"?n.multipart.part(t):i==="POST"&&a==="presign/multipart/complete"?n.multipart.complete(t):i==="POST"&&a==="presign/multipart/abort"?n.multipart.abort(t):Response.json({message:"Not Found"},{status:404})}}function N(e="/api/s3"){let n=e.replace(/\/$/,""),r=async(o,s)=>{let a=await fetch(o,s);if(!a.ok){let i=await a.json().catch(()=>({}));throw new Error(i.message??a.statusText)}return a.json()},t=(o,s)=>r(o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});return {upload(o){return t(`${n}/presign/upload`,o)},confirm(o){return t(`${n}/presign/upload/confirm`,o)},download(o,s){let a=new URLSearchParams({key:o});if(s?.fileName){let i=s.fileName.replace(/["\\\r\n]/g,"_");a.set("fileName",i);}return s?.bucket&&a.set("bucket",s.bucket),r(`${n}/presign/download?${a}`)},delete(o,s){let a=new URLSearchParams({key:o});return s?.bucket&&a.set("bucket",s.bucket),r(`${n}/delete?${a}`,{method:"DELETE"})},multipart:{init(o){return t(`${n}/presign/multipart/init`,o)},signPart(o){return t(`${n}/presign/multipart/part`,o)},complete(o){return t(`${n}/presign/multipart/complete`,o)},abort(o){return t(`${n}/presign/multipart/abort`,o)}}}}function X(e,n){if(n.accept?.length&&!n.accept.some(t=>t.startsWith(".")?e.name.toLowerCase().endsWith(t.toLowerCase()):t.endsWith("/*")?e.type.startsWith(t.replace("/*","/")):e.type===t)){let t=e.name.includes(".")?e.name.split(".").pop():null;return `File type "${t?`.${t}`:e.type||"unknown"}" is not allowed`}return e.size===0?"File is empty":n.maxFileSize&&e.size>n.maxFileSize?`File size exceeds ${(n.maxFileSize/1048576).toFixed(1)} MB limit`:null}
2
+ export{x as createConfirmHandler,H as createDeleteHandler,w as createDownloadHandler,T as createHandlers,I as createMultipartAbortHandler,h as createMultipartCompleteHandler,C as createMultipartInitHandler,P as createMultipartPartHandler,N as createPresignApi,G as createRouter,N as createS3Api,R as createUploadHandler,y as normalizeExpiresIn,m as parseBody,d as requireString,u as runHook,X as validateFile,l as withS3ErrorHandler};//# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/helpers.ts","../src/handlers/presign/upload.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/validate.ts"],"names":["parseBody","request","body","requireString","value","name","trimmed","normalizeExpiresIn","withS3ErrorHandler","handler","err","message","runHook","hook","context","status","createUploadHandler","config","key","bucket","expiresIn","acl","guardResult","url","getSignedUrl","PutObjectCommand","createConfirmHandler","head","HeadObjectCommand","createDownloadHandler","searchParams","fileName","GetObjectCommand","createDeleteHandler","DeleteObjectCommand","createMultipartInitHandler","UploadId","CreateMultipartUploadCommand","createMultipartPartHandler","uploadId","partNumber","presignedUrl","UploadPartCommand","createMultipartCompleteHandler","parts","eTag","p","a","b","Location","ETag","CompleteMultipartUploadCommand","createMultipartAbortHandler","AbortMultipartUploadCommand","createHandlers","createRouter","handlers","base","subpath","method","createS3Api","basePath","json","init","res","post","payload","options","params","safe","validateFile","file","type","ext"],"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 message =\n err instanceof Error ? err.message : \"Internal server error\";\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 500 });\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","import { PutObjectCommand } 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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n contentType?: string;\n metadata?: Record<string, string>;\n bucket?: string;\n expiresIn?: number;\n acl?: \"private\" | \"public-read\";\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\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n\n const guardResult = await runHook(config.hooks?.upload?.guard, {\n request,\n key,\n bucket,\n contentType: body.contentType,\n metadata: body.metadata,\n acl,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new PutObjectCommand({\n Bucket: bucket,\n Key: key,\n ContentType: body.contentType,\n Metadata: body.metadata,\n ACL: acl,\n }),\n { expiresIn },\n );\n\n await config.hooks?.upload?.onSuccess?.({\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({ bucket, key, url, expiresIn });\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 \"../../helpers\";\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.hooks?.upload?.guard, {\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 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 };\n\n await config.hooks?.upload?.onComplete?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n });\n });\n}\n","import { GetObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport { normalizeExpiresIn, runHook, withS3ErrorHandler } 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.hooks?.download?.guard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ResponseContentDisposition: `attachment${fileName ? `; filename=\"${fileName}\"` : \"\"}`,\n }),\n { expiresIn },\n );\n\n await config.hooks?.download?.onSuccess?.({\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 \"../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.hooks?.delete?.guard, {\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(\n { message: `Object \"${key}\" not found` },\n { status: 404 },\n );\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.hooks?.delete?.onSuccess?.({ 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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n bucket?: string;\n contentType?: string;\n metadata?: Record<string, string>;\n acl?: \"private\" | \"public-read\";\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\n const acl = body.acl === \"public-read\" ? \"public-read\" : \"private\";\n\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\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 Metadata: body.metadata,\n ACL: acl,\n }),\n );\n\n await config.hooks?.multipart?.onInit?.({\n request,\n key,\n bucket,\n uploadId: UploadId!,\n contentType: body.contentType,\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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: 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\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n }),\n { expiresIn },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n });\n });\n}\n","import { CompleteMultipartUploadCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\ntype PartEntry = {\n partNumber: number;\n eTag: string;\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, eTag }) => ({\n PartNumber: Number(partNumber),\n ETag: String(eTag),\n }))\n .filter((p) => Number.isInteger(p.PartNumber) && p.ETag)\n .sort((a, b) => a.PartNumber - b.PartNumber);\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.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const { Location, ETag } = await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: parts },\n }),\n );\n\n await config.hooks?.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n });\n\n return Response.json({\n bucket,\n key,\n uploadId,\n location: Location,\n eTag: ETag,\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 \"../../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.hooks?.multipart?.guard, {\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.hooks?.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 \"./helpers\";\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.hooks?.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 handlers.presign.upload(request);\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return handlers.presign.confirm(request);\n if (method === \"GET\" && subpath === \"presign/download\")\n return handlers.presign.download(request);\n if (method === \"DELETE\" && subpath === \"delete\")\n return handlers.delete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return handlers.multipart.init(request);\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return handlers.multipart.part(request);\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return handlers.multipart.complete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return handlers.multipart.abort(request);\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 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};\n\nexport type UploadConfirmResponse = {\n key: string;\n bucket: string;\n contentType?: string;\n contentLength: number;\n eTag?: string;\n};\n\nexport type S3Api = {\n upload: (payload: {\n key: string;\n contentType?: string;\n metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n }) => Promise<PresignResponse>;\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 metadata?: Record<string, string>;\n bucket?: string;\n acl?: \"private\" | \"public-read\";\n }) => Promise<MultipartInitResponse>;\n signPart: (payload: {\n key: string;\n uploadId: string;\n partNumber: number;\n bucket?: string;\n }) => Promise<MultipartPartResponse>;\n complete: (payload: {\n key: string;\n uploadId: string;\n parts: Array<{ partNumber: number; eTag: string }>;\n bucket?: string;\n }) => Promise<{ key: string; bucket: string; uploadId: string }>;\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<PresignResponse>(`${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<{ key: string; bucket: string; uploadId: string }>(\n `${base}/presign/multipart/complete`,\n payload,\n );\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/helpers.ts","../src/handlers/presign/upload.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/validate.ts"],"names":["buildContentDisposition","fileName","ascii","encoded","parseBody","request","body","requireString","value","name","trimmed","normalizeExpiresIn","withS3ErrorHandler","handler","err","message","runHook","hook","context","status","MAX_UPLOAD_SIZE","createUploadHandler","config","key","bucket","expiresIn","acl","contentType","fileSize","guardResult","fields","k","v","maxBytes","rangeMin","rangeMax","url","signedFields","createPresignedPost","createConfirmHandler","head","HeadObjectCommand","createDownloadHandler","searchParams","getSignedUrl","GetObjectCommand","createDeleteHandler","DeleteObjectCommand","createMultipartInitHandler","UploadId","CreateMultipartUploadCommand","createMultipartPartHandler","uploadId","partNumber","presignedUrl","UploadPartCommand","createMultipartCompleteHandler","parts","eTag","p","a","b","CompleteMultipartUploadCommand","contentLength","createMultipartAbortHandler","AbortMultipartUploadCommand","createHandlers","createRouter","handlers","base","subpath","method","createS3Api","basePath","json","init","res","post","payload","options","params","safe","validateFile","file","type","ext"],"mappings":"wTASO,SAASA,CAAAA,CAAwBC,CAAAA,CAA0B,CAChE,IAAMC,CAAAA,CAAQD,CAAAA,CAAS,OAAA,CAAQ,eAAA,CAAiB,GAAG,CAAA,CAAE,OAAA,CAAQ,QAAA,CAAU,GAAG,CAAA,CACpEE,CAAAA,CAAU,kBAAA,CAAmBF,CAAQ,CAAA,CAC3C,OAAO,CAAA,sBAAA,EAAyBC,CAAK,CAAA,oBAAA,EAAuBC,CAAO,CAAA,CACrE,CAEA,eAAsBC,CAAAA,CACpBC,CAAAA,CACmB,CACnB,GAAI,CACF,IAAMC,CAAAA,CAAO,MAAMD,CAAAA,CAAQ,IAAA,EAAK,CAChC,OAAOC,CAAAA,EAAQ,OAAOA,CAAAA,EAAS,QAAA,CAAYA,EAAa,IAC1D,CAAA,KAAQ,CACN,OAAO,IACT,CACF,CAEO,SAASC,EAAcC,CAAAA,CAAgBC,CAAAA,CAAiC,CAC7E,IAAMC,CAAAA,CAAU,OAAOF,CAAAA,EAAU,QAAA,CAAWA,EAAM,IAAA,EAAK,CAAI,EAAA,CAC3D,OAAKE,CAAAA,EACI,QAAA,CAAS,IAAA,CAAK,CAAE,OAAA,CAAS,CAAA,EAAGD,CAAI,CAAA,YAAA,CAAe,CAAA,CAAG,CAAE,MAAA,CAAQ,GAAI,CAAC,CAG5E,CAEO,SAASE,CAAAA,CAAmBH,CAAAA,CAAwB,CACzD,IAAM,CAAA,CAAI,OAAOA,CAAK,CAAA,CACtB,OAAO,MAAA,CAAO,QAAA,CAAS,CAAC,CAAA,EAAK,CAAA,CAAI,EAAI,IAAA,CAAK,KAAA,CAAM,CAAC,CAAA,CAAI,GACvD,CAEO,SAASI,CAAAA,CACdC,CAAAA,CACA,CACA,OAAO,MAAOR,CAAAA,EAAqB,CACjC,GAAI,CACF,OAAO,MAAMQ,CAAAA,CAAQR,CAAO,CAC9B,CAAA,MAASS,CAAAA,CAAK,CACZ,IAAMC,EACJD,CAAAA,YAAe,KAAA,CAAQA,CAAAA,CAAI,OAAA,CAAU,uBAAA,CACvC,OAAA,OAAA,CAAQ,KAAA,CAAM,UAAA,CAAYC,CAAO,CAAA,CAC1B,QAAA,CAAS,IAAA,CAAK,CAAE,OAAA,CAAAA,CAAQ,CAAA,CAAG,CAAE,MAAA,CAAQ,GAAI,CAAC,CACnD,CACF,CACF,CAMA,eAAsBC,EACpBC,CAAAA,CACAC,CAAAA,CAC0B,CAC1B,GAAI,CAACD,CAAAA,CAAM,OAAO,IAAA,CAClB,GAAI,CACF,OAAA,MAAMA,CAAAA,CAAKC,CAAO,CAAA,CACX,IACT,CAAA,MAASJ,CAAAA,CAAK,CACZ,IAAMC,CAAAA,CAAUD,CAAAA,YAAe,KAAA,CAAQA,CAAAA,CAAI,OAAA,CAAU,WAAA,CAC/CK,CAAAA,CACJ,OAAQL,CAAAA,EAAiC,MAAA,EAAW,QAAA,CAC9CA,CAAAA,CAAgC,MAAA,CAClC,GAAA,CACN,OAAO,SAAS,IAAA,CAAK,CAAE,OAAA,CAAAC,CAAQ,CAAA,CAAG,CAAE,MAAA,CAAAI,CAAO,CAAC,CAC9C,CACF,CC9DA,IAAMC,CAAAA,CAAkB,CAAA,CAAI,IAAA,CAAO,IAAA,CAAO,KAqBnC,SAASC,CAAAA,CAAoBC,CAAAA,CAAyB,CAC3D,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,CAAA,CAClC,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,CAAAA,CAAK,IAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,QAAA,CAAU,OAAOA,CAAAA,CAEpC,IAAMC,CAAAA,CAASlB,CAAAA,CAAK,MAAA,EAAQ,IAAA,EAAK,EAAKgB,CAAAA,CAAO,aAAA,CACvCG,CAAAA,CAAYd,EAAmBL,CAAAA,CAAK,SAAS,CAAA,CAC7CoB,CAAAA,CAAMpB,CAAAA,CAAK,GAAA,GAAQ,aAAA,CAAgB,aAAA,CAAgB,UACnDqB,CAAAA,CAAcrB,CAAAA,CAAK,WAAA,EAAa,IAAA,EAAK,EAAK,0BAAA,CAC1CsB,CAAAA,CACJ,OAAOtB,EAAK,QAAA,EAAa,QAAA,EAAYA,CAAAA,CAAK,QAAA,CAAW,CAAA,CACjD,IAAA,CAAK,KAAA,CAAMA,CAAAA,CAAK,QAAQ,CAAA,CACxB,IAAA,CAIN,GACEsB,CAAAA,GAAa,IAAA,EACbN,CAAAA,CAAO,WAAA,EACPM,EAAWN,CAAAA,CAAO,WAAA,CAElB,OAAO,QAAA,CAAS,IAAA,CACd,CACE,OAAA,CAAS,CAAA,WAAA,EAAcM,CAAQ,CAAA,4CAAA,EAA+CN,CAAAA,CAAO,WAAW,CAAA,MAAA,CAClG,CAAA,CACA,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,KAAA,CAAO,CAC7D,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,EACA,WAAA,CAAalB,CAAAA,CAAK,WAAA,CAClB,QAAA,CAAUsB,CAAAA,EAAY,KAAA,CAAA,CACtB,QAAA,CAAUtB,CAAAA,CAAK,SACf,GAAA,CAAAoB,CACF,CAAC,CAAA,CACD,GAAIG,CAAAA,CAAa,OAAOA,CAAAA,CAIxB,IAAMC,CAAAA,CAAiC,CAAE,GAAA,CAAAJ,CAAAA,CAAK,cAAA,CAAgBC,CAAY,CAAA,CAM1E,GAJIrB,CAAAA,CAAK,QAAA,GACPwB,CAAAA,CAAO,qBAAqB,CAAA,CAAI9B,CAAAA,CAAwBM,CAAAA,CAAK,QAAQ,GAGnEA,CAAAA,CAAK,QAAA,CACP,IAAA,GAAW,CAACyB,CAAAA,CAAGC,CAAC,CAAA,GAAK,MAAA,CAAO,QAAQ1B,CAAAA,CAAK,QAAQ,CAAA,CAC/CwB,CAAAA,CAAO,CAAA,WAAA,EAAcC,CAAC,CAAA,CAAE,CAAA,CAAIC,EAUhC,IAAMC,CAAAA,CAAWX,CAAAA,CAAO,WAAA,EAAeF,CAAAA,CACjCc,CAAAA,CAAWN,CAAAA,EAAY,CAAA,CACvBO,CAAAA,CAAWP,CAAAA,EAAYK,CAAAA,CAEvB,CAAE,GAAA,CAAAG,CAAAA,CAAK,MAAA,CAAQC,CAAa,EAAI,MAAMC,mBAAAA,CAAoBhB,CAAAA,CAAO,EAAA,CAAI,CACzE,MAAA,CAAQE,CAAAA,CACR,GAAA,CAAKD,EACL,UAAA,CAAY,CAAC,CAAC,sBAAA,CAAwBW,CAAAA,CAAUC,CAAQ,CAAC,CAAA,CACzD,OAAQL,CAAAA,CACR,OAAA,CAASL,CACX,CAAC,CAAA,CAED,OAAA,MAAMH,CAAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,SAAA,GAAY,CACtC,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,EACA,WAAA,CAAalB,CAAAA,CAAK,WAAA,CAClB,QAAA,CAAUA,CAAAA,CAAK,QAAA,CACf,GAAA,CAAAoB,CAAAA,CACA,IAAAU,CAAAA,CACA,SAAA,CAAAX,CACF,CAAC,CAAA,CAEM,QAAA,CAAS,IAAA,CAAK,CAAE,OAAAD,CAAAA,CAAQ,GAAA,CAAAD,CAAAA,CAAK,GAAA,CAAAa,CAAAA,CAAK,MAAA,CAAQC,CAAAA,CAAc,SAAA,CAAAZ,CAAU,CAAC,CAC5E,CAAC,CACH,CChHO,SAASc,CAAAA,CAAqBjB,CAAAA,CAAyB,CAC5D,OAAOV,CAAAA,CAAmB,MAAOP,GAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,CAAA,CAClC,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,EAAK,GAAA,CAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,QAAA,CAAU,OAAOA,CAAAA,CAEpC,IAAMC,CAAAA,CAASlB,CAAAA,CAAK,MAAA,EAAQ,IAAA,EAAK,EAAKgB,CAAAA,CAAO,aAAA,CAEvCO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,KAAA,CAAO,CAC7D,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CACF,CAAC,CAAA,CACD,GAAIK,CAAAA,CAAa,OAAOA,CAAAA,CAExB,IAAMW,CAAAA,CAAO,MAAMlB,CAAAA,CAAO,EAAA,CAAG,IAAA,CAC3B,IAAImB,kBAAkB,CAAE,MAAA,CAAQjB,CAAAA,CAAQ,GAAA,CAAKD,CAAI,CAAC,CACpD,CAAA,CAEML,EAAU,CACd,OAAA,CAAAb,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,WAAA,CAAagB,CAAAA,CAAK,WAAA,CAClB,aAAA,CAAeA,CAAAA,CAAK,aAAA,EAAiB,CAAA,CACrC,IAAA,CAAMA,CAAAA,CAAK,MAAM,OAAA,CAAQ,IAAA,CAAM,EAAE,CAAA,CACjC,QAAA,CAAUA,CAAAA,CAAK,QACjB,CAAA,CAEA,OAAA,MAAMlB,CAAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,UAAA,GAAaJ,CAAO,CAAA,CAEzC,QAAA,CAAS,KAAK,CACnB,GAAA,CAAAK,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,WAAA,CAAaN,CAAAA,CAAQ,WAAA,CACrB,aAAA,CAAeA,CAAAA,CAAQ,aAAA,CACvB,IAAA,CAAMA,CAAAA,CAAQ,IAChB,CAAC,CACH,CAAC,CACH,CCvDO,SAASwB,CAAAA,CAAsBpB,CAAAA,CAAyB,CAC7D,OAAOV,CAAAA,CAAmB,MAAOP,GAAqB,CACpD,GAAM,CAAE,YAAA,CAAAsC,CAAa,CAAA,CAAI,IAAI,GAAA,CAAItC,CAAAA,CAAQ,GAAG,CAAA,CACtCkB,CAAAA,CAAMoB,CAAAA,CAAa,GAAA,CAAI,KAAK,CAAA,EAAG,MAAK,CAC1C,GAAI,CAACpB,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,QAAS,iCAAkC,CAAA,CAC7C,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMC,EAASmB,CAAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,EAAG,IAAA,EAAK,EAAKrB,CAAAA,CAAO,aAAA,CACtDG,CAAAA,CAAYd,CAAAA,CAAmBgC,CAAAA,CAAa,GAAA,CAAI,WAAW,CAAC,CAAA,CAC5D1C,CAAAA,CAAW0C,EAAa,GAAA,CAAI,UAAU,CAAA,EAAG,IAAA,EAAK,CAE9Cd,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,EAAO,KAAA,EAAO,QAAA,EAAU,KAAA,CAAO,CAC/D,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,OAAAC,CAAAA,CACA,QAAA,CAAUvB,CAAAA,EAAY,KAAA,CACxB,CAAC,CAAA,CACD,GAAI4B,CAAAA,CAAa,OAAOA,CAAAA,CAExB,IAAMO,CAAAA,CAAM,MAAMQ,YAAAA,CAChBtB,CAAAA,CAAO,EAAA,CACP,IAAIuB,gBAAAA,CAAiB,CACnB,MAAA,CAAQrB,CAAAA,CACR,GAAA,CAAKD,CAAAA,CACL,GAAItB,CAAAA,CACA,CAAE,0BAAA,CAA4B,CAAA,sBAAA,EAAyBA,CAAQ,CAAA,CAAA,CAAI,CAAA,CACnE,EACN,CAAC,EACD,CAAE,SAAA,CAAAwB,CAAU,CACd,CAAA,CAEA,OAAA,MAAMH,CAAAA,CAAO,KAAA,EAAO,QAAA,EAAU,SAAA,GAAY,CACxC,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,EACA,QAAA,CAAUvB,CAAAA,EAAY,KAAA,CAAA,CACtB,GAAA,CAAAmC,CAAAA,CACA,SAAA,CAAAX,CACF,CAAC,EAEM,QAAA,CAAS,IAAA,CAAK,CAAE,MAAA,CAAAD,CAAAA,CAAQ,GAAA,CAAAD,CAAAA,CAAK,GAAA,CAAAa,EAAK,SAAA,CAAAX,CAAU,CAAC,CACtD,CAAC,CACH,CC/CO,SAASqB,CAAAA,CAAoBxB,CAAAA,CAAyB,CAC3D,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,GAAM,CAAE,YAAA,CAAAsC,CAAa,CAAA,CAAI,IAAI,GAAA,CAAItC,CAAAA,CAAQ,GAAG,CAAA,CACtCkB,CAAAA,CAAMoB,CAAAA,CAAa,IAAI,KAAK,CAAA,EAAG,IAAA,EAAK,CAC1C,GAAI,CAACpB,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,iCAAkC,CAAA,CAC7C,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMC,CAAAA,CAASmB,CAAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,EAAG,MAAK,EAAKrB,CAAAA,CAAO,aAAA,CAEtDO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,KAAA,EAAO,QAAQ,KAAA,CAAO,CAC7D,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CACF,CAAC,CAAA,CACD,GAAIK,CAAAA,CAAa,OAAOA,CAAAA,CAExB,GAAI,CACF,MAAMP,CAAAA,CAAO,EAAA,CAAG,IAAA,CAAK,IAAImB,iBAAAA,CAAkB,CAAE,MAAA,CAAQjB,CAAAA,CAAQ,IAAKD,CAAI,CAAC,CAAC,EAC1E,CAAA,KAAQ,CACN,OAAO,QAAA,CAAS,KACd,CAAE,OAAA,CAAS,CAAA,QAAA,EAAWA,CAAG,CAAA,WAAA,CAAc,CAAA,CACvC,CAAE,MAAA,CAAQ,GAAI,CAChB,CACF,CAEA,OAAA,MAAMD,CAAAA,CAAO,EAAA,CAAG,IAAA,CAAK,IAAIyB,mBAAAA,CAAoB,CAAE,MAAA,CAAQvB,CAAAA,CAAQ,GAAA,CAAKD,CAAI,CAAC,CAAC,EAE1E,MAAMD,CAAAA,CAAO,KAAA,EAAO,MAAA,EAAQ,SAAA,GAAY,CAAE,OAAA,CAAAjB,CAAAA,CAAS,IAAAkB,CAAAA,CAAK,MAAA,CAAAC,CAAO,CAAC,CAAA,CAEzD,QAAA,CAAS,IAAA,CAAK,CAAE,OAAA,CAAS,CAAA,CAAA,CAAM,MAAA,CAAAA,CAAAA,CAAQ,GAAA,CAAAD,CAAI,CAAC,CACrD,CAAC,CACH,CCjBO,SAASyB,CAAAA,CAA2B1B,EAAyB,CAClE,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,CAAA,CAClC,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,CAAAA,CAAK,GAAA,CAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,QAAA,CAAU,OAAOA,CAAAA,CAEpC,IAAMC,CAAAA,CAASlB,CAAAA,CAAK,MAAA,EAAQ,MAAK,EAAKgB,CAAAA,CAAO,aAAA,CACvCI,CAAAA,CAAMpB,CAAAA,CAAK,GAAA,GAAQ,aAAA,CAAgB,aAAA,CAAgB,SAAA,CACnDsB,CAAAA,CACJ,OAAOtB,CAAAA,CAAK,QAAA,EAAa,QAAA,EAAYA,CAAAA,CAAK,QAAA,CAAW,EACjD,IAAA,CAAK,KAAA,CAAMA,CAAAA,CAAK,QAAQ,CAAA,CACxB,KAAA,CAAA,CAGN,GAAIgB,CAAAA,CAAO,aAAeM,CAAAA,GAAa,KAAA,CAAA,CACrC,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,qDAAsD,EACjE,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAIF,GACEA,CAAAA,GAAa,KAAA,CAAA,EACbN,CAAAA,CAAO,WAAA,EACPM,CAAAA,CAAWN,CAAAA,CAAO,WAAA,CAElB,OAAO,QAAA,CAAS,IAAA,CACd,CACE,OAAA,CAAS,CAAA,WAAA,EAAcM,CAAQ,CAAA,4CAAA,EAA+CN,CAAAA,CAAO,WAAW,CAAA,MAAA,CAClG,CAAA,CACA,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,EAAO,KAAA,EAAO,SAAA,EAAW,KAAA,CAAO,CAChE,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,QAAA,CAAAI,CACF,CAAC,CAAA,CACD,GAAIC,CAAAA,CAAa,OAAOA,CAAAA,CAExB,GAAM,CAAE,QAAA,CAAAoB,CAAS,CAAA,CAAI,MAAM3B,CAAAA,CAAO,GAAG,IAAA,CACnC,IAAI4B,4BAAAA,CAA6B,CAC/B,MAAA,CAAQ1B,CAAAA,CACR,GAAA,CAAKD,CAAAA,CACL,YAAajB,CAAAA,CAAK,WAAA,CAClB,kBAAA,CAAoBA,CAAAA,CAAK,QAAA,CACrBN,CAAAA,CAAwBM,CAAAA,CAAK,QAAQ,CAAA,CACrC,KAAA,CAAA,CACJ,QAAA,CAAUA,CAAAA,CAAK,QAAA,CACf,GAAA,CAAKoB,CACP,CAAC,CACH,CAAA,CAEA,OAAA,MAAMJ,CAAAA,CAAO,KAAA,EAAO,SAAA,EAAW,MAAA,GAAS,CACtC,OAAA,CAAAjB,EACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,QAAA,CAAUyB,CAAAA,CACV,WAAA,CAAa3C,CAAAA,CAAK,YAClB,QAAA,CAAAsB,CAAAA,CACA,QAAA,CAAUtB,CAAAA,CAAK,QAAA,CACf,GAAA,CAAAoB,CACF,CAAC,CAAA,CAEM,QAAA,CAAS,IAAA,CAAK,CAAE,MAAA,CAAAF,CAAAA,CAAQ,GAAA,CAAAD,CAAAA,CAAK,SAAU0B,CAAS,CAAA,CAAG,CAAE,MAAA,CAAQ,GAAI,CAAC,CAC3E,CAAC,CACH,CC/EO,SAASE,CAAAA,CAA2B7B,CAAAA,CAAyB,CAClE,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,EAClC,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,EAAK,GAAA,CAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,QAAA,CAAU,OAAOA,CAAAA,CAEpC,IAAM6B,CAAAA,CAAW7C,CAAAA,CAAcD,CAAAA,CAAK,QAAA,CAAU,UAAU,CAAA,CACxD,GAAI8C,aAAoB,QAAA,CAAU,OAAOA,CAAAA,CAEzC,IAAMC,CAAAA,CAAa,MAAA,CAAO/C,CAAAA,CAAK,UAAU,EACzC,GAAI,CAAC,MAAA,CAAO,SAAA,CAAU+C,CAAU,CAAA,EAAKA,CAAAA,EAAc,CAAA,CACjD,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,uCAAwC,CAAA,CACnD,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAM7B,CAAAA,CAASlB,CAAAA,CAAK,MAAA,EAAQ,IAAA,IAAUgB,CAAAA,CAAO,aAAA,CACvCG,CAAAA,CAAYd,CAAAA,CAAmBL,CAAAA,CAAK,SAAS,CAAA,CAE7CuB,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,KAAA,EAAO,SAAA,EAAW,KAAA,CAAO,CAChE,OAAA,CAAAjB,CAAAA,CACA,IAAAkB,CAAAA,CACA,MAAA,CAAAC,CACF,CAAC,CAAA,CACD,GAAIK,CAAAA,CAAa,OAAOA,CAAAA,CAExB,IAAMyB,CAAAA,CAAe,MAAMV,YAAAA,CACzBtB,CAAAA,CAAO,EAAA,CACP,IAAIiC,kBAAkB,CACpB,MAAA,CAAQ/B,CAAAA,CACR,GAAA,CAAKD,CAAAA,CACL,QAAA,CAAU6B,CAAAA,CACV,UAAA,CAAYC,CACd,CAAC,CAAA,CACD,CAAE,SAAA,CAAA5B,CAAU,CACd,CAAA,CAEA,OAAO,SAAS,IAAA,CAAK,CACnB,YAAA,CAAA6B,CAAAA,CACA,UAAA,CAAAD,CAAAA,CACA,QAAA,CAAAD,CAAAA,CACA,MAAA,CAAA5B,CAAAA,CACA,SAAA,CAAAC,CACF,CAAC,CACH,CAAC,CACH,CCnDO,SAAS+B,CAAAA,CAA+BlC,CAAAA,CAAyB,CACtE,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,CAAA,CAClC,CAAE,OAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,CAAAA,CAAK,GAAA,CAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,QAAA,CAAU,OAAOA,CAAAA,CAEpC,IAAM6B,CAAAA,CAAW7C,EAAcD,CAAAA,CAAK,QAAA,CAAU,UAAU,CAAA,CACxD,GAAI8C,CAAAA,YAAoB,QAAA,CAAU,OAAOA,CAAAA,CAEzC,IAAMK,CAAAA,CAAAA,CAAS,KAAA,CAAM,OAAA,CAAQnD,CAAAA,CAAK,KAAK,CAAA,CAAIA,EAAK,KAAA,CAAQ,EAAC,EACtD,GAAA,CAAI,CAAC,CAAE,UAAA,CAAA+C,CAAAA,CAAY,KAAAK,CAAK,CAAA,IAAO,CAC9B,UAAA,CAAY,MAAA,CAAOL,CAAU,CAAA,CAC7B,IAAA,CAAM,OAAOK,CAAI,CACnB,CAAA,CAAE,CAAA,CACD,MAAA,CAAQC,CAAAA,EAAM,MAAA,CAAO,SAAA,CAAUA,CAAAA,CAAE,UAAU,CAAA,EAAKA,CAAAA,CAAE,IAAI,CAAA,CACtD,IAAA,CAAK,CAACC,EAAGC,CAAAA,GAAMD,CAAAA,CAAE,UAAA,CAAaC,CAAAA,CAAE,UAAU,CAAA,CAE7C,GAAI,CAACJ,EAAM,MAAA,CACT,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,qCAAsC,CAAA,CACjD,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMjC,CAAAA,CAASlB,CAAAA,CAAK,MAAA,EAAQ,IAAA,EAAK,EAAKgB,CAAAA,CAAO,aAAA,CAEvCO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,OAAO,SAAA,EAAW,KAAA,CAAO,CAChE,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CACF,CAAC,CAAA,CACD,GAAIK,CAAAA,CAAa,OAAOA,CAAAA,CAExB,MAAMP,CAAAA,CAAO,GAAG,IAAA,CACd,IAAIwC,8BAAAA,CAA+B,CACjC,MAAA,CAAQtC,CAAAA,CACR,GAAA,CAAKD,CAAAA,CACL,QAAA,CAAU6B,CAAAA,CACV,eAAA,CAAiB,CAAE,KAAA,CAAOK,CAAM,CAClC,CAAC,CACH,CAAA,CAKA,IAAMjB,CAAAA,CAAO,MAAMlB,CAAAA,CAAO,EAAA,CAAG,IAAA,CAC3B,IAAImB,kBAAkB,CAAE,MAAA,CAAQjB,CAAAA,CAAQ,GAAA,CAAKD,CAAI,CAAC,CACpD,CAAA,CACMwC,EAAgBvB,CAAAA,CAAK,aAAA,EAAiB,CAAA,CACtCb,CAAAA,CAAca,CAAAA,CAAK,WAAA,CACnBkB,CAAAA,CAAOlB,CAAAA,CAAK,IAAA,EAAM,OAAA,CAAQ,IAAA,CAAM,EAAE,CAAA,CAExC,OAAIlB,CAAAA,CAAO,WAAA,EAAeyC,EAAgBzC,CAAAA,CAAO,WAAA,EAG/C,MAAMA,CAAAA,CAAO,EAAA,CACV,IAAA,CAAK,IAAIyB,mBAAAA,CAAoB,CAAE,MAAA,CAAQvB,CAAAA,CAAQ,GAAA,CAAKD,CAAI,CAAC,CAAC,CAAA,CAC1D,KAAA,CAAM,IAAM,CAAC,CAAC,CAAA,CACV,QAAA,CAAS,IAAA,CACd,CACE,OAAA,CAAS,CAAA,WAAA,EAAcwC,CAAa,CAAA,4CAAA,EAA+CzC,CAAAA,CAAO,WAAW,CAAA,MAAA,CACvG,CAAA,CACA,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,GAGF,MAAMA,CAAAA,CAAO,KAAA,EAAO,SAAA,EAAW,UAAA,GAAa,CAC1C,QAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,QAAA,CAAA4B,CAAAA,CACA,aAAA,CAAAW,EACA,WAAA,CAAApC,CAAAA,CACA,IAAA,CAAA+B,CACF,CAAC,CAAA,CAEM,QAAA,CAAS,IAAA,CAAK,CAAE,MAAA,CAAAlC,CAAAA,CAAQ,GAAA,CAAAD,CAAAA,CAAK,QAAA,CAAA6B,CAAAA,CAAU,aAAA,CAAAW,EAAe,WAAA,CAAApC,CAAAA,CAAa,IAAA,CAAA+B,CAAK,CAAC,CAAA,CAClF,CAAC,CACH,CC3FO,SAASM,CAAAA,CAA4B1C,CAAAA,CAAyB,CACnE,OAAOV,CAAAA,CAAmB,MAAOP,CAAAA,EAAqB,CACpD,IAAMC,CAAAA,CAAO,MAAMF,CAAAA,CAAmBC,CAAO,CAAA,CAC7C,GAAI,CAACC,CAAAA,CACH,OAAO,QAAA,CAAS,IAAA,CACd,CAAE,OAAA,CAAS,sBAAuB,CAAA,CAClC,CAAE,MAAA,CAAQ,GAAI,CAChB,CAAA,CAGF,IAAMiB,CAAAA,CAAMhB,CAAAA,CAAcD,CAAAA,CAAK,GAAA,CAAK,KAAK,CAAA,CACzC,GAAIiB,CAAAA,YAAe,SAAU,OAAOA,CAAAA,CAEpC,IAAM6B,CAAAA,CAAW7C,CAAAA,CAAcD,CAAAA,CAAK,QAAA,CAAU,UAAU,CAAA,CACxD,GAAI8C,CAAAA,YAAoB,QAAA,CAAU,OAAOA,CAAAA,CAEzC,IAAM5B,CAAAA,CAASlB,EAAK,MAAA,EAAQ,IAAA,EAAK,EAAKgB,CAAAA,CAAO,aAAA,CAEvCO,CAAAA,CAAc,MAAMb,CAAAA,CAAQM,EAAO,KAAA,EAAO,SAAA,EAAW,KAAA,CAAO,CAChE,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,OAAAC,CACF,CAAC,CAAA,CACD,OAAIK,CAAAA,GAEJ,MAAMP,CAAAA,CAAO,EAAA,CAAG,IAAA,CACd,IAAI2C,2BAAAA,CAA4B,CAC9B,MAAA,CAAQzC,CAAAA,CACR,GAAA,CAAKD,CAAAA,CACL,SAAU6B,CACZ,CAAC,CACH,CAAA,CAEA,MAAM9B,CAAAA,CAAO,KAAA,EAAO,SAAA,EAAW,OAAA,GAAU,CACvC,OAAA,CAAAjB,CAAAA,CACA,GAAA,CAAAkB,CAAAA,CACA,MAAA,CAAAC,CAAAA,CACA,SAAA4B,CACF,CAAC,CAAA,CAEM,QAAA,CAAS,IAAA,CAAK,CAAE,MAAA,CAAA5B,CAAAA,CAAQ,GAAA,CAAAD,CAAAA,CAAK,QAAA,CAAA6B,CAAAA,CAAU,OAAA,CAAS,CAAA,CAAK,CAAC,CAAA,CAC/D,CAAC,CACH,CC/CO,SAASc,CAAAA,CAAe5C,CAAAA,CAAqC,CAClE,OAAO,CACL,QAAS,CACP,MAAA,CAAQD,CAAAA,CAAoBC,CAAM,CAAA,CAClC,OAAA,CAASiB,CAAAA,CAAqBjB,CAAM,EACpC,QAAA,CAAUoB,CAAAA,CAAsBpB,CAAM,CACxC,CAAA,CACA,SAAA,CAAW,CACT,IAAA,CAAM0B,CAAAA,CAA2B1B,CAAM,CAAA,CACvC,IAAA,CAAM6B,CAAAA,CAA2B7B,CAAM,CAAA,CACvC,QAAA,CAAUkC,EAA+BlC,CAAM,CAAA,CAC/C,KAAA,CAAO0C,CAAAA,CAA4B1C,CAAM,CAC3C,CAAA,CACA,MAAA,CAAQwB,EAAoBxB,CAAM,CACpC,CACF,CCrBO,SAAS6C,CAAAA,CAAa7C,CAAAA,CAA8B,CACzD,IAAM8C,CAAAA,CAAWF,CAAAA,CAAe5C,CAAM,CAAA,CAChC+C,CAAAA,CAAO/C,CAAAA,CAAO,QAAA,CAAS,OAAA,CAAQ,KAAA,CAAO,EAAE,CAAA,CAE9C,OAAO,MAAOjB,CAAAA,EAAwC,CAEpD,IAAMwB,EAAc,MAAMb,CAAAA,CAAQM,CAAAA,CAAO,KAAA,EAAO,KAAA,CAAO,CAAE,OAAA,CAAAjB,CAAQ,CAAC,CAAA,CAClE,GAAIwB,CAAAA,CAAa,OAAOA,CAAAA,CAGxB,IAAMyC,CAAAA,CADM,IAAI,IAAIjE,CAAAA,CAAQ,GAAG,CAAA,CACX,QAAA,CAAS,KAAA,CAAMgE,CAAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,CAAO,EAAE,CAAA,CAC3DE,CAAAA,CAASlE,CAAAA,CAAQ,MAAA,CAEvB,OAAIkE,IAAW,MAAA,EAAUD,CAAAA,GAAY,gBAAA,CAC5BF,CAAAA,CAAS,OAAA,CAAQ,MAAA,CAAO/D,CAAO,CAAA,CACpCkE,IAAW,MAAA,EAAUD,CAAAA,GAAY,wBAAA,CAC5BF,CAAAA,CAAS,OAAA,CAAQ,OAAA,CAAQ/D,CAAO,CAAA,CACrCkE,IAAW,KAAA,EAASD,CAAAA,GAAY,kBAAA,CAC3BF,CAAAA,CAAS,OAAA,CAAQ,QAAA,CAAS/D,CAAO,CAAA,CACtCkE,CAAAA,GAAW,QAAA,EAAYD,CAAAA,GAAY,QAAA,CAC9BF,CAAAA,CAAS,MAAA,CAAO/D,CAAO,CAAA,CAC5BkE,IAAW,MAAA,EAAUD,CAAAA,GAAY,wBAAA,CAC5BF,CAAAA,CAAS,SAAA,CAAU,IAAA,CAAK/D,CAAO,CAAA,CACpCkE,IAAW,MAAA,EAAUD,CAAAA,GAAY,wBAAA,CAC5BF,CAAAA,CAAS,SAAA,CAAU,IAAA,CAAK/D,CAAO,CAAA,CACpCkE,IAAW,MAAA,EAAUD,CAAAA,GAAY,4BAAA,CAC5BF,CAAAA,CAAS,SAAA,CAAU,QAAA,CAAS/D,CAAO,CAAA,CACxCkE,CAAAA,GAAW,MAAA,EAAUD,CAAAA,GAAY,yBAAA,CAC5BF,CAAAA,CAAS,SAAA,CAAU,KAAA,CAAM/D,CAAO,EAElC,QAAA,CAAS,IAAA,CAAK,CAAE,OAAA,CAAS,WAAY,CAAA,CAAG,CAAE,MAAA,CAAQ,GAAI,CAAC,CAChE,CACF,CCoEO,SAASmE,CAAAA,CAAYC,CAAAA,CAAW,SAAA,CAAkB,CACvD,IAAMJ,CAAAA,CAAOI,CAAAA,CAAS,OAAA,CAAQ,KAAA,CAAO,EAAE,CAAA,CAEjCC,CAAAA,CAAO,MAAUtC,CAAAA,CAAauC,CAAAA,GAAmC,CACrE,IAAMC,CAAAA,CAAM,MAAM,KAAA,CAAMxC,EAAKuC,CAAI,CAAA,CACjC,GAAI,CAACC,CAAAA,CAAI,EAAA,CAAI,CACX,IAAMtE,EAAO,MAAMsE,CAAAA,CAAI,IAAA,EAAK,CAAE,KAAA,CAAM,KAAO,EAAC,CAAE,EAC9C,MAAM,IAAI,KAAA,CAAOtE,CAAAA,CAA8B,OAAA,EAAWsE,CAAAA,CAAI,UAAU,CAC1E,CACA,OAAOA,CAAAA,CAAI,IAAA,EACb,CAAA,CAEMC,CAAAA,CAAO,CAAIzC,EAAa9B,CAAAA,GAC5BoE,CAAAA,CAAQtC,CAAAA,CAAK,CACX,MAAA,CAAQ,MAAA,CACR,OAAA,CAAS,CAAE,eAAgB,kBAAmB,CAAA,CAC9C,IAAA,CAAM,IAAA,CAAK,SAAA,CAAU9B,CAAI,CAC3B,CAAC,EAEH,OAAO,CACL,MAAA,CAAOwE,CAAAA,CAAS,CACd,OAAOD,CAAAA,CAA4B,CAAA,EAAGR,CAAI,CAAA,eAAA,CAAA,CAAmBS,CAAO,CACtE,CAAA,CAEA,OAAA,CAAQA,CAAAA,CAAS,CACf,OAAOD,CAAAA,CACL,CAAA,EAAGR,CAAI,CAAA,uBAAA,CAAA,CACPS,CACF,CACF,CAAA,CAEA,QAAA,CAASvD,EAAKwD,CAAAA,CAAU,CACtB,IAAMC,CAAAA,CAAS,IAAI,eAAA,CAAgB,CAAE,GAAA,CAAAzD,CAAI,CAAC,CAAA,CAC1C,GAAIwD,CAAAA,EAAS,QAAA,CAAU,CACrB,IAAME,CAAAA,CAAOF,CAAAA,CAAQ,QAAA,CAAS,OAAA,CAAQ,YAAA,CAAc,GAAG,CAAA,CACvDC,CAAAA,CAAO,GAAA,CAAI,WAAYC,CAAI,EAC7B,CACA,OAAIF,CAAAA,EAAS,MAAA,EAAQC,CAAAA,CAAO,GAAA,CAAI,SAAUD,CAAAA,CAAQ,MAAM,CAAA,CACjDL,CAAAA,CAAsB,CAAA,EAAGL,CAAI,CAAA,kBAAA,EAAqBW,CAAM,EAAE,CACnE,CAAA,CAEA,MAAA,CAAOzD,CAAAA,CAAKwD,CAAAA,CAAU,CACpB,IAAMC,CAAAA,CAAS,IAAI,eAAA,CAAgB,CAAE,GAAA,CAAAzD,CAAI,CAAC,CAAA,CAC1C,OAAIwD,GAAS,MAAA,EAAQC,CAAAA,CAAO,GAAA,CAAI,QAAA,CAAUD,CAAAA,CAAQ,MAAM,CAAA,CACjDL,CAAAA,CACL,GAAGL,CAAI,CAAA,QAAA,EAAWW,CAAM,CAAA,CAAA,CACxB,CAAE,MAAA,CAAQ,QAAS,CACrB,CACF,CAAA,CAEA,SAAA,CAAW,CACT,IAAA,CAAKF,CAAAA,CAAS,CACZ,OAAOD,CAAAA,CACL,CAAA,EAAGR,CAAI,CAAA,uBAAA,CAAA,CACPS,CACF,CACF,CAAA,CAEA,QAAA,CAASA,CAAAA,CAAS,CAChB,OAAOD,CAAAA,CACL,CAAA,EAAGR,CAAI,CAAA,uBAAA,CAAA,CACPS,CACF,CACF,CAAA,CAEA,SAASA,CAAAA,CAAS,CAChB,OAAOD,CAAAA,CAOJ,CAAA,EAAGR,CAAI,CAAA,2BAAA,CAAA,CAA+BS,CAAO,CAClD,CAAA,CAEA,KAAA,CAAMA,CAAAA,CAAS,CACb,OAAOD,CAAAA,CACL,CAAA,EAAGR,CAAI,CAAA,wBAAA,CAAA,CACPS,CACF,CACF,CACF,CACF,CACF,CC5LO,SAASI,EACdC,CAAAA,CACAJ,CAAAA,CACe,CACf,GAAIA,CAAAA,CAAQ,MAAA,EAAQ,MAAA,EAad,CAZYA,EAAQ,MAAA,CAAO,IAAA,CAAMK,CAAAA,EAE/BA,CAAAA,CAAK,UAAA,CAAW,GAAG,CAAA,CACdD,CAAAA,CAAK,KAAK,WAAA,EAAY,CAAE,QAAA,CAASC,CAAAA,CAAK,WAAA,EAAa,CAAA,CAGxDA,CAAAA,CAAK,QAAA,CAAS,IAAI,CAAA,CACbD,CAAAA,CAAK,IAAA,CAAK,UAAA,CAAWC,CAAAA,CAAK,OAAA,CAAQ,KAAM,GAAG,CAAC,CAAA,CAG9CD,CAAAA,CAAK,IAAA,GAASC,CACtB,CAAA,CACa,CACZ,IAAMC,CAAAA,CAAMF,CAAAA,CAAK,IAAA,CAAK,QAAA,CAAS,GAAG,CAAA,CAAIA,CAAAA,CAAK,IAAA,CAAK,MAAM,GAAG,CAAA,CAAE,GAAA,EAAI,CAAI,IAAA,CACnE,OAAO,CAAA,WAAA,EAAcE,CAAAA,CAAM,CAAA,CAAA,EAAIA,CAAG,CAAA,CAAA,CAAKF,CAAAA,CAAK,IAAA,EAAQ,SAAS,CAAA,gBAAA,CAC/D,CAGF,OAAIA,CAAAA,CAAK,IAAA,GAAS,CAAA,CACT,eAAA,CAGLJ,CAAAA,CAAQ,WAAA,EAAeI,CAAAA,CAAK,IAAA,CAAOJ,EAAQ,WAAA,CAEtC,CAAA,kBAAA,EAAA,CADQA,CAAAA,CAAQ,WAAA,CAAe,OAAA,EAAc,OAAA,CAAQ,CAAC,CAC5B,YAG5B,IACT","file":"index.js","sourcesContent":["/**\n * Builds a RFC 6266 `Content-Disposition` value for a file attachment.\n * Includes both the ASCII-safe `filename` fallback and the RFC 5987\n * `filename*` parameter for full Unicode support.\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\nexport 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 message =\n err instanceof Error ? err.message : \"Internal server error\";\n console.error(\"[S3 API]\", message);\n return Response.json({ message }, { status: 500 });\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","import { createPresignedPost } from \"@aws-sdk/s3-presigned-post\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n normalizeExpiresIn,\n buildContentDisposition,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\n// 5 GiB — S3 single-object size limit\nconst MAX_UPLOAD_SIZE = 5 * 1024 * 1024 * 1024;\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 // Reject immediately if the declared size already exceeds the server limit,\n // before generating any presigned URL.\n if (\n fileSize !== null &&\n config.maxFileSize &&\n fileSize > config.maxFileSize\n ) {\n return Response.json(\n {\n message: `File size (${fileSize} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 413 },\n );\n }\n\n const guardResult = await runHook(config.hooks?.upload?.guard, {\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 // Build presigned POST fields. Fields are embedded in the signed policy\n // so the client cannot change them without invalidating the signature.\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 // `content-length-range` is enforced by S3 at the storage layer —\n // uploads outside this range are rejected before the object is stored.\n //\n // When the client declares fileSize: S3 locks the range to exactly that\n // number of bytes, so uploading a different-sized file is impossible.\n // When fileSize is not declared: falls back to [1, maxFileSize ?? 5 GiB].\n const maxBytes = config.maxFileSize ?? MAX_UPLOAD_SIZE;\n const rangeMin = fileSize ?? 1;\n const rangeMax = fileSize ?? maxBytes;\n\n const { url, fields: signedFields } = await createPresignedPost(config.s3, {\n Bucket: bucket,\n Key: key,\n Conditions: [[\"content-length-range\", rangeMin, rangeMax]],\n Fields: fields,\n Expires: expiresIn,\n });\n\n await config.hooks?.upload?.onSuccess?.({\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({ bucket, key, url, fields: signedFields, expiresIn });\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 \"../../helpers\";\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.hooks?.upload?.guard, {\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 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 };\n\n await config.hooks?.upload?.onComplete?.(context);\n\n return Response.json({\n key,\n bucket,\n contentType: context.contentType,\n contentLength: context.contentLength,\n eTag: context.eTag,\n });\n });\n}\n","import { GetObjectCommand } from \"@aws-sdk/client-s3\";\nimport { getSignedUrl } from \"@aws-sdk/s3-request-presigner\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport { normalizeExpiresIn, runHook, withS3ErrorHandler } 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.hooks?.download?.guard, {\n request,\n key,\n bucket,\n fileName: fileName || undefined,\n });\n if (guardResult) return guardResult;\n\n const url = await getSignedUrl(\n config.s3,\n new GetObjectCommand({\n Bucket: bucket,\n Key: key,\n ...(fileName\n ? { ResponseContentDisposition: `attachment; filename=\"${fileName}\"` }\n : {}),\n }),\n { expiresIn },\n );\n\n await config.hooks?.download?.onSuccess?.({\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 \"../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.hooks?.delete?.guard, {\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(\n { message: `Object \"${key}\" not found` },\n { status: 404 },\n );\n }\n\n await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));\n\n await config.hooks?.delete?.onSuccess?.({ 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 buildContentDisposition,\n runHook,\n withS3ErrorHandler,\n} 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 // enforce the limit at init time (before the multipart upload is created).\n if (config.maxFileSize && fileSize === undefined) {\n return Response.json(\n { message: \"fileSize is required when maxFileSize is configured\" },\n { status: 400 },\n );\n }\n\n // Reject immediately if the declared size already exceeds the server limit.\n if (\n fileSize !== undefined &&\n config.maxFileSize &&\n fileSize > config.maxFileSize\n ) {\n return Response.json(\n {\n message: `File size (${fileSize} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 413 },\n );\n }\n\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\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.hooks?.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 \"../../helpers\";\n\ntype Payload = {\n key: string;\n uploadId: string;\n partNumber: 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\n const guardResult = await runHook(config.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n const presignedUrl = await getSignedUrl(\n config.s3,\n new UploadPartCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n PartNumber: partNumber,\n }),\n { expiresIn },\n );\n\n return Response.json({\n presignedUrl,\n partNumber,\n uploadId,\n bucket,\n expiresIn,\n });\n });\n}\n","import { CompleteMultipartUploadCommand, HeadObjectCommand, DeleteObjectCommand } from \"@aws-sdk/client-s3\";\nimport type { S3HandlerConfig } from \"../../types\";\nimport {\n parseBody,\n requireString,\n runHook,\n withS3ErrorHandler,\n} from \"../../helpers\";\n\ntype PartEntry = {\n partNumber: number;\n eTag: string;\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, eTag }) => ({\n PartNumber: Number(partNumber),\n ETag: String(eTag),\n }))\n .filter((p) => Number.isInteger(p.PartNumber) && p.ETag)\n .sort((a, b) => a.PartNumber - b.PartNumber);\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.hooks?.multipart?.guard, {\n request,\n key,\n bucket,\n });\n if (guardResult) return guardResult;\n\n await config.s3.send(\n new CompleteMultipartUploadCommand({\n Bucket: bucket,\n Key: key,\n UploadId: uploadId,\n MultipartUpload: { Parts: parts },\n }),\n );\n\n // Verify the actual uploaded object metadata via HeadObject.\n // This is the server's only opportunity to enforce maxFileSize for\n // multipart uploads, since individual parts bypass size checks.\n const head = await config.s3.send(\n new HeadObjectCommand({ Bucket: bucket, Key: key }),\n );\n const contentLength = head.ContentLength ?? 0;\n const contentType = head.ContentType;\n const eTag = head.ETag?.replace(/\"/g, \"\");\n\n if (config.maxFileSize && contentLength > config.maxFileSize) {\n // Delete the object and reject the request. The client has uploaded\n // more data than the server policy allows.\n await config.s3\n .send(new DeleteObjectCommand({ Bucket: bucket, Key: key }))\n .catch(() => {});\n return Response.json(\n {\n message: `File size (${contentLength} bytes) exceeds the maximum allowed size of ${config.maxFileSize} bytes`,\n },\n { status: 422 },\n );\n }\n\n await config.hooks?.multipart?.onComplete?.({\n request,\n key,\n bucket,\n uploadId,\n contentLength,\n contentType,\n eTag,\n });\n\n return Response.json({ bucket, key, uploadId, contentLength, contentType, eTag });\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 \"../../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.hooks?.multipart?.guard, {\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.hooks?.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 \"./helpers\";\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.hooks?.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 handlers.presign.upload(request);\n if (method === \"POST\" && subpath === \"presign/upload/confirm\")\n return handlers.presign.confirm(request);\n if (method === \"GET\" && subpath === \"presign/download\")\n return handlers.presign.download(request);\n if (method === \"DELETE\" && subpath === \"delete\")\n return handlers.delete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/init\")\n return handlers.multipart.init(request);\n if (method === \"POST\" && subpath === \"presign/multipart/part\")\n return handlers.multipart.part(request);\n if (method === \"POST\" && subpath === \"presign/multipart/complete\")\n return handlers.multipart.complete(request);\n if (method === \"POST\" && subpath === \"presign/multipart/abort\")\n return handlers.multipart.abort(request);\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 PresignedPostResponse = {\n key: string;\n bucket: string;\n url: string;\n fields: Record<string, string>;\n expiresIn: number;\n};\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};\n\nexport type UploadConfirmResponse = {\n key: string;\n bucket: string;\n contentType?: string;\n contentLength: number;\n eTag?: 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<PresignedPostResponse>;\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 bucket?: string;\n }) => Promise<MultipartPartResponse>;\n complete: (payload: {\n key: string;\n uploadId: string;\n parts: Array<{ partNumber: number; eTag: string }>;\n bucket?: string;\n }) => Promise<{\n key: string;\n bucket: string;\n uploadId: string;\n contentLength: number;\n contentType?: string;\n eTag?: 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<PresignedPostResponse>(`${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 }>(`${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"]}
package/dist/types.d.ts CHANGED
@@ -6,6 +6,8 @@ export type UploadHookContext = HookContext & {
6
6
  key: string;
7
7
  bucket: string;
8
8
  contentType?: string;
9
+ /** Declared byte size of the file the client intends to upload. */
10
+ fileSize?: number;
9
11
  metadata?: Record<string, string>;
10
12
  acl?: "private" | "public-read";
11
13
  };
@@ -37,15 +39,25 @@ export type DeleteHookContext = HookContext & {
37
39
  export type MultipartHookContext = HookContext & {
38
40
  key: string;
39
41
  bucket: string;
42
+ /**
43
+ * Declared byte size of the file — available during `init` only.
44
+ * Undefined during `part`, `complete`, and `abort` operations.
45
+ */
46
+ fileSize?: number;
40
47
  };
41
48
  export type MultipartInitSuccessContext = MultipartHookContext & {
42
49
  uploadId: string;
43
50
  contentType?: string;
51
+ /** Declared byte size of the file (as provided by the client). */
52
+ fileSize?: number;
44
53
  metadata?: Record<string, string>;
45
54
  acl?: "private" | "public-read";
46
55
  };
47
56
  export type MultipartCompleteSuccessContext = MultipartHookContext & {
48
57
  uploadId: string;
58
+ contentLength: number;
59
+ contentType?: string;
60
+ eTag?: string;
49
61
  };
50
62
  export type S3ServerHooks = {
51
63
  /** Runs before every request. Throw to reject. */
@@ -86,6 +98,22 @@ export type S3ServerHooks = {
86
98
  export type S3HandlerConfig = {
87
99
  s3: S3Client;
88
100
  defaultBucket: string;
101
+ /**
102
+ * Maximum file size in bytes enforced server-side.
103
+ * - Simple uploads: enforced via `content-length-range` in the presigned POST policy (S3 rejects oversized files).
104
+ * - Multipart uploads: enforced at three points:
105
+ * 1. Init-time rejection if the client declares a `fileSize` that exceeds this limit.
106
+ * 2. Part-request rejection if the requested `partNumber` exceeds the maximum number of
107
+ * parts derivable from this limit (caps potential over-upload to `maxFileSize + ~5 MB`).
108
+ * 3. HeadObject verification after CompleteMultipartUpload; the object is deleted and a 422
109
+ * is returned if the actual size still exceeds this limit.
110
+ *
111
+ * Note: S3 presigned UploadPart URLs do not enforce per-part size at the S3 level.
112
+ * For strict enforcement, combine this setting with `requireFileSizeForMultipart`,
113
+ * infrastructure-level S3 lifecycle rules for incomplete multipart uploads, and
114
+ * rate limiting on your API endpoints.
115
+ */
116
+ maxFileSize?: number;
89
117
  hooks?: S3ServerHooks;
90
118
  };
91
119
  export type S3RouteHandlerConfig = S3HandlerConfig & {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-s3/server",
3
- "version": "2.3.1",
3
+ "version": "3.0.0",
4
4
  "description": "Framework-agnostic S3 server handlers — presigned uploads, downloads, deletes, and multipart operations",
5
5
  "keywords": [
6
6
  "s3",
@@ -38,6 +38,7 @@
38
38
  "dist"
39
39
  ],
40
40
  "dependencies": {
41
+ "@aws-sdk/s3-presigned-post": "^3.1032.0",
41
42
  "@aws-sdk/s3-request-presigner": "^3.1032.0"
42
43
  },
43
44
  "peerDependencies": {
@@ -45,6 +46,7 @@
45
46
  },
46
47
  "devDependencies": {
47
48
  "@aws-sdk/client-s3": "^3.1032.0",
49
+ "@aws-sdk/s3-presigned-post": "^3.1032.0",
48
50
  "@aws-sdk/s3-request-presigner": "^3.1029.0",
49
51
  "tsup": "^8.5.1",
50
52
  "typescript": "^6.0.3"