@dimah-s3/server 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +8 -9
  2. package/dist/adapters/express.js +36 -49
  3. package/dist/adapters/express.js.map +1 -1
  4. package/dist/adapters/fastify.js +36 -49
  5. package/dist/adapters/fastify.js.map +1 -1
  6. package/dist/adapters/next.d.ts +3 -1
  7. package/dist/adapters/next.d.ts.map +1 -1
  8. package/dist/adapters/next.js +2 -0
  9. package/dist/adapters/next.js.map +1 -1
  10. package/dist/adapters/node.d.ts +1 -1
  11. package/dist/adapters/node.d.ts.map +1 -1
  12. package/dist/adapters/node.js +36 -49
  13. package/dist/adapters/node.js.map +1 -1
  14. package/dist/adapters/types.d.ts +1 -1
  15. package/dist/adapters/types.d.ts.map +1 -1
  16. package/dist/api/assert-feature-enabled.d.ts +5 -0
  17. package/dist/api/assert-feature-enabled.d.ts.map +1 -0
  18. package/dist/api/bind-endpoints.d.ts +8 -0
  19. package/dist/api/bind-endpoints.d.ts.map +1 -0
  20. package/dist/api/context.d.ts +10 -0
  21. package/dist/api/context.d.ts.map +1 -0
  22. package/dist/api/create-s3-endpoint.d.ts +141 -0
  23. package/dist/api/create-s3-endpoint.d.ts.map +1 -0
  24. package/dist/api/create-s3-middleware.d.ts +23 -0
  25. package/dist/api/create-s3-middleware.d.ts.map +1 -0
  26. package/dist/api/index.d.ts +6 -0
  27. package/dist/api/index.d.ts.map +1 -0
  28. package/dist/api/index.js +758 -0
  29. package/dist/api/index.js.map +1 -0
  30. package/dist/api/routes/confirm.d.ts +14 -0
  31. package/dist/api/routes/confirm.d.ts.map +1 -0
  32. package/dist/api/routes/delete.d.ts +16 -0
  33. package/dist/api/routes/delete.d.ts.map +1 -0
  34. package/dist/api/routes/download.d.ts +19 -0
  35. package/dist/api/routes/download.d.ts.map +1 -0
  36. package/dist/api/routes/index.d.ts +170 -0
  37. package/dist/api/routes/index.d.ts.map +1 -0
  38. package/dist/api/routes/multipart/abort.d.ts +20 -0
  39. package/dist/api/routes/multipart/abort.d.ts.map +1 -0
  40. package/dist/api/routes/multipart/complete.d.ts +22 -0
  41. package/dist/api/routes/multipart/complete.d.ts.map +1 -0
  42. package/dist/api/routes/multipart/init.d.ts +26 -0
  43. package/dist/api/routes/multipart/init.d.ts.map +1 -0
  44. package/dist/api/routes/multipart/list-parts.d.ts +17 -0
  45. package/dist/api/routes/multipart/list-parts.d.ts.map +1 -0
  46. package/dist/api/routes/multipart/part.d.ts +22 -0
  47. package/dist/api/routes/multipart/part.d.ts.map +1 -0
  48. package/dist/api/routes/upload.d.ts +26 -0
  49. package/dist/api/routes/upload.d.ts.map +1 -0
  50. package/dist/dimah-s3.d.ts +41 -12
  51. package/dist/dimah-s3.d.ts.map +1 -1
  52. package/dist/errors.d.ts +8 -22
  53. package/dist/errors.d.ts.map +1 -1
  54. package/dist/helpers/resolve-target.d.ts +51 -0
  55. package/dist/helpers/resolve-target.d.ts.map +1 -0
  56. package/dist/index.d.ts +7 -5
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +521 -483
  59. package/dist/index.js.map +1 -1
  60. package/dist/internal-helpers.d.ts +4 -8
  61. package/dist/internal-helpers.d.ts.map +1 -1
  62. package/dist/plugin/apply-plugins.d.ts.map +1 -1
  63. package/dist/plugin/chain-hooks.d.ts +1 -1
  64. package/dist/plugin/define-plugin.d.ts +4 -13
  65. package/dist/plugin/define-plugin.d.ts.map +1 -1
  66. package/dist/plugin/hook-registry.d.ts +2 -2
  67. package/dist/plugin/hook-registry.d.ts.map +1 -1
  68. package/dist/plugin/index.d.ts +1 -2
  69. package/dist/plugin/index.d.ts.map +1 -1
  70. package/dist/plugin/types.d.ts +21 -41
  71. package/dist/plugin/types.d.ts.map +1 -1
  72. package/dist/plugins.d.ts +7 -0
  73. package/dist/plugins.d.ts.map +1 -0
  74. package/dist/plugins.js +984 -0
  75. package/dist/plugins.js.map +1 -0
  76. package/dist/router.d.ts +12 -0
  77. package/dist/router.d.ts.map +1 -0
  78. package/dist/types/config.d.ts +61 -22
  79. package/dist/types/config.d.ts.map +1 -1
  80. package/dist/types/hook-contexts.d.ts +11 -6
  81. package/dist/types/hook-contexts.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +2 -2
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/package.json +16 -6
  85. package/dist/adapters/adapters.test.d.ts +0 -2
  86. package/dist/adapters/adapters.test.d.ts.map +0 -1
  87. package/dist/api.d.ts +0 -5
  88. package/dist/api.d.ts.map +0 -1
  89. package/dist/handler.d.ts +0 -5
  90. package/dist/handler.d.ts.map +0 -1
  91. package/dist/helpers/is-aws-not-found.test.d.ts +0 -2
  92. package/dist/helpers/is-aws-not-found.test.d.ts.map +0 -1
  93. package/dist/internal-helpers.test.d.ts +0 -2
  94. package/dist/internal-helpers.test.d.ts.map +0 -1
  95. package/dist/plugin/apply-plugins.test.d.ts +0 -2
  96. package/dist/plugin/apply-plugins.test.d.ts.map +0 -1
  97. package/dist/plugin/create-endpoint.d.ts +0 -20
  98. package/dist/plugin/create-endpoint.d.ts.map +0 -1
  99. package/dist/procedures/confirm.d.ts +0 -8
  100. package/dist/procedures/confirm.d.ts.map +0 -1
  101. package/dist/procedures/delete.d.ts +0 -8
  102. package/dist/procedures/delete.d.ts.map +0 -1
  103. package/dist/procedures/download.d.ts +0 -10
  104. package/dist/procedures/download.d.ts.map +0 -1
  105. package/dist/procedures/multipart/abort.d.ts +0 -13
  106. package/dist/procedures/multipart/abort.d.ts.map +0 -1
  107. package/dist/procedures/multipart/complete.d.ts +0 -12
  108. package/dist/procedures/multipart/complete.d.ts.map +0 -1
  109. package/dist/procedures/multipart/init.d.ts +0 -13
  110. package/dist/procedures/multipart/init.d.ts.map +0 -1
  111. package/dist/procedures/multipart/list-parts.d.ts +0 -9
  112. package/dist/procedures/multipart/list-parts.d.ts.map +0 -1
  113. package/dist/procedures/multipart/part.d.ts +0 -12
  114. package/dist/procedures/multipart/part.d.ts.map +0 -1
  115. package/dist/procedures/registry.d.ts +0 -78
  116. package/dist/procedures/registry.d.ts.map +0 -1
  117. package/dist/procedures/upload.d.ts +0 -14
  118. package/dist/procedures/upload.d.ts.map +0 -1
@@ -0,0 +1,758 @@
1
+ import { createMiddleware, createEndpoint } from 'better-call';
2
+ import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition } from '@dimah-s3/core';
3
+ import { HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
4
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
+ import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
6
+
7
+ // src/api/create-s3-endpoint.ts
8
+ var errors = {
9
+ notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
10
+ unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
11
+ forbidden: () => DimahS3Error.from("FORBIDDEN", S3_ERROR_CODES.FORBIDDEN),
12
+ conflict: () => DimahS3Error.from("CONFLICT", S3_ERROR_CODES.CONFLICT),
13
+ internalError: () => DimahS3Error.from("INTERNAL_SERVER_ERROR", S3_ERROR_CODES.INTERNAL_ERROR),
14
+ objectNotFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.OBJECT_NOT_FOUND),
15
+ featureDisabled: (feature) => DimahS3Error.from("NOT_FOUND", {
16
+ code: S3_ERROR_CODES.FEATURE_DISABLED.code,
17
+ message: S3_ERROR_CODES.FEATURE_DISABLED.message.replaceAll(
18
+ "{feature}",
19
+ feature
20
+ ),
21
+ params: { feature }
22
+ }),
23
+ invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
24
+ invalidBucket: (bucket) => DimahS3Error.from("FORBIDDEN", {
25
+ code: S3_ERROR_CODES.INVALID_BUCKET.code,
26
+ message: S3_ERROR_CODES.INVALID_BUCKET.message,
27
+ params: { bucket }
28
+ }),
29
+ s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
30
+ code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
31
+ message: S3_ERROR_CODES.S3_NETWORK_ERROR.message.replaceAll(
32
+ "{code}",
33
+ code
34
+ ),
35
+ params: { code }
36
+ }),
37
+ fileSizeRequiredUpload: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD),
38
+ fileSizeRequiredMultipart: () => DimahS3Error.from(
39
+ "BAD_REQUEST",
40
+ S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
41
+ ),
42
+ validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
43
+ code: S3_ERROR_CODES.VALIDATION_ERROR.code,
44
+ message
45
+ })
46
+ };
47
+ async function runHook(hook, context) {
48
+ if (!hook) return;
49
+ try {
50
+ await hook(context);
51
+ } catch (err) {
52
+ if (isAPIError(err)) throw err;
53
+ if (err instanceof Error && err.message.trim()) {
54
+ throw new DimahS3Error("FORBIDDEN", {
55
+ message: err.message,
56
+ cause: err
57
+ });
58
+ }
59
+ throw errors.forbidden();
60
+ }
61
+ }
62
+ async function runLifecycleHook(hook, context) {
63
+ if (!hook) return;
64
+ try {
65
+ await hook(context);
66
+ } catch (err) {
67
+ if (isAPIError(err)) throw err;
68
+ console.error("[S3 API] lifecycle hook failed", err);
69
+ throw errors.internalError();
70
+ }
71
+ }
72
+ function normalizeExpiresIn(value) {
73
+ const n = Number(value);
74
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
75
+ }
76
+ function requestFromHeaders(headers) {
77
+ return new Request("http://dimah-s3.local", { headers });
78
+ }
79
+
80
+ // src/api/create-s3-middleware.ts
81
+ var s3ContextMiddleware = createMiddleware(async (ctx) => {
82
+ const injected = ctx.context;
83
+ if (!injected?.config) {
84
+ throw new Error(
85
+ "createS3Endpoint requires dimahS3 router context. Call endpoints through dimahS3().api or the HTTP handler."
86
+ );
87
+ }
88
+ const request = ctx.request ?? injected.request ?? requestFromHeaders(ctx.headers);
89
+ await runHook(injected.config.guard, { request });
90
+ return {
91
+ config: injected.config,
92
+ request
93
+ };
94
+ });
95
+ var createS3Middleware = createMiddleware.create({
96
+ use: [s3ContextMiddleware]
97
+ });
98
+
99
+ // src/api/create-s3-endpoint.ts
100
+ var createEndpointWithContext = createEndpoint.create({
101
+ use: [s3ContextMiddleware]
102
+ });
103
+ function withS3Validation(options) {
104
+ return {
105
+ ...options,
106
+ onValidationError: options.onValidationError ?? (({ message }) => {
107
+ throw errors.validationError(message);
108
+ })
109
+ };
110
+ }
111
+ var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
112
+ if (typeof pathOrOptions === "string") {
113
+ return createEndpointWithContext(
114
+ pathOrOptions,
115
+ withS3Validation(optionsOrHandler),
116
+ maybeHandler
117
+ );
118
+ }
119
+ return createEndpointWithContext(
120
+ withS3Validation(pathOrOptions),
121
+ optionsOrHandler
122
+ );
123
+ });
124
+
125
+ // src/helpers/is-aws-not-found.ts
126
+ function isAwsNotFound(err) {
127
+ const e = err;
128
+ return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
129
+ }
130
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
131
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
132
+ const controller = new AbortController();
133
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
134
+ try {
135
+ const result = await s3.send(
136
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
137
+ { abortSignal: controller.signal }
138
+ );
139
+ const isPublic = result.Grants?.some(
140
+ (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
141
+ );
142
+ return isPublic ? "public-read" : "private";
143
+ } catch {
144
+ return void 0;
145
+ } finally {
146
+ clearTimeout(timeout);
147
+ }
148
+ }
149
+
150
+ // src/helpers/resolve-target.ts
151
+ function assertSafeObjectKey(key) {
152
+ const trimmed = key.replace(/^\/+/u, "").replace(/\/+$/u, "");
153
+ if (!trimmed) {
154
+ throw errors.invalidKey();
155
+ }
156
+ if (trimmed.includes("\\") || trimmed.includes("\0")) {
157
+ throw errors.invalidKey();
158
+ }
159
+ const parts = trimmed.split("/");
160
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
161
+ throw errors.invalidKey();
162
+ }
163
+ return parts.join("/");
164
+ }
165
+ function applyPrefix(prefix, key) {
166
+ const p = assertSafeObjectKey(prefix);
167
+ const k = assertSafeObjectKey(key);
168
+ if (k === p || k.startsWith(`${p}/`)) return k;
169
+ return `${p}/${k}`;
170
+ }
171
+ async function resolveObjectKey(policy, context) {
172
+ if (policy?.resolveKey) {
173
+ return assertSafeObjectKey(await policy.resolveKey(context));
174
+ }
175
+ const prefix = typeof policy?.prefix === "function" ? await policy.prefix(context) : policy?.prefix;
176
+ if (prefix) {
177
+ return applyPrefix(prefix, context.proposedKey);
178
+ }
179
+ return assertSafeObjectKey(context.proposedKey);
180
+ }
181
+ function resolveBucket(config, requested) {
182
+ if (!requested) return config.bucket;
183
+ if (config.allowClientBucket) return requested;
184
+ if (config.buckets?.length) {
185
+ if (config.buckets.includes(requested)) return requested;
186
+ throw errors.invalidBucket(requested);
187
+ }
188
+ return config.bucket;
189
+ }
190
+ async function resolveRequestTarget(config, policy, input) {
191
+ const bucket = resolveBucket(config, input.bucket);
192
+ const key = await resolveObjectKey(policy, {
193
+ request: input.request,
194
+ proposedKey: input.key,
195
+ fileName: input.fileName,
196
+ contentType: input.contentType,
197
+ bucket
198
+ });
199
+ return { key, bucket };
200
+ }
201
+ function isFeatureEnabled(config, feature) {
202
+ return config[feature]?.enabled === true;
203
+ }
204
+
205
+ // src/api/assert-feature-enabled.ts
206
+ function assertFeatureEnabled(config, feature) {
207
+ if (!isFeatureEnabled(config, feature)) {
208
+ throw errors.featureDisabled(feature);
209
+ }
210
+ }
211
+
212
+ // src/api/routes/confirm.ts
213
+ async function handleConfirm(config, input, request) {
214
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
215
+ request,
216
+ key: input.key,
217
+ bucket: input.bucket
218
+ });
219
+ await runHook(config.upload?.confirmGuard, {
220
+ request,
221
+ key,
222
+ bucket
223
+ });
224
+ const head = await config.client.send(
225
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
226
+ );
227
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
228
+ const fileName = parseFileName(head.ContentDisposition);
229
+ const context = {
230
+ request,
231
+ key,
232
+ bucket,
233
+ contentType: head.ContentType,
234
+ contentLength: head.ContentLength ?? 0,
235
+ eTag: head.ETag?.replace(/"/g, ""),
236
+ metadata: head.Metadata,
237
+ acl,
238
+ fileName,
239
+ versionId: head.VersionId,
240
+ lastModified: head.LastModified?.toISOString()
241
+ };
242
+ await runLifecycleHook(config.upload?.onConfirmed, context);
243
+ return {
244
+ key,
245
+ bucket,
246
+ contentType: context.contentType,
247
+ contentLength: context.contentLength,
248
+ eTag: context.eTag,
249
+ metadata: context.metadata ?? {},
250
+ acl,
251
+ fileName,
252
+ versionId: context.versionId,
253
+ lastModified: context.lastModified
254
+ };
255
+ }
256
+ var confirm = createS3Endpoint(
257
+ S3_API_ROUTES.uploadConfirm,
258
+ { method: "POST", body: confirmBodySchema },
259
+ async (ctx) => {
260
+ assertFeatureEnabled(ctx.context.config, "upload");
261
+ return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
262
+ }
263
+ );
264
+ async function handleDelete(config, input, request) {
265
+ const { key, bucket } = await resolveRequestTarget(config, config.delete, {
266
+ request,
267
+ key: input.key,
268
+ bucket: input.bucket
269
+ });
270
+ await runHook(config.delete?.guard, {
271
+ request,
272
+ key,
273
+ bucket
274
+ });
275
+ try {
276
+ await config.client.send(
277
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
278
+ );
279
+ } catch (err) {
280
+ if (isAwsNotFound(err)) {
281
+ throw errors.objectNotFound();
282
+ }
283
+ throw err;
284
+ }
285
+ await config.client.send(
286
+ new DeleteObjectCommand({ Bucket: bucket, Key: key })
287
+ );
288
+ await runLifecycleHook(config.delete?.onDeleted, { request, key, bucket });
289
+ return { success: true, bucket, key };
290
+ }
291
+ var deleteObject = createS3Endpoint(
292
+ S3_API_ROUTES.delete,
293
+ { method: "DELETE", query: deleteQuerySchema },
294
+ async (ctx) => {
295
+ assertFeatureEnabled(ctx.context.config, "delete");
296
+ return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
297
+ }
298
+ );
299
+ async function handleDownload(config, input, request) {
300
+ const { key, bucket } = await resolveRequestTarget(config, config.download, {
301
+ request,
302
+ key: input.key,
303
+ bucket: input.bucket,
304
+ fileName: input.fileName
305
+ });
306
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
307
+ const fileName = input.fileName;
308
+ await runHook(config.download?.guard, {
309
+ request,
310
+ key,
311
+ bucket,
312
+ fileName
313
+ });
314
+ try {
315
+ await config.client.send(
316
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
317
+ );
318
+ } catch (err) {
319
+ if (isAwsNotFound(err)) {
320
+ throw errors.objectNotFound();
321
+ }
322
+ throw err;
323
+ }
324
+ const url = await getSignedUrl(
325
+ config.client,
326
+ new GetObjectCommand({
327
+ Bucket: bucket,
328
+ Key: key,
329
+ ResponseContentDisposition: fileName ? buildContentDisposition(fileName) : "attachment"
330
+ }),
331
+ { expiresIn }
332
+ );
333
+ await runLifecycleHook(config.download?.onPresigned, {
334
+ request,
335
+ key,
336
+ bucket,
337
+ fileName,
338
+ url,
339
+ expiresIn
340
+ });
341
+ return { bucket, key, url, expiresIn };
342
+ }
343
+ var download = createS3Endpoint(
344
+ S3_API_ROUTES.download,
345
+ { method: "GET", query: downloadQuerySchema },
346
+ async (ctx) => {
347
+ assertFeatureEnabled(ctx.context.config, "download");
348
+ return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
349
+ }
350
+ );
351
+ async function handleAbort(config, input, request) {
352
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
353
+ request,
354
+ key: input.key,
355
+ bucket: input.bucket
356
+ });
357
+ const uploadId = input.uploadId;
358
+ await runHook(config.multipart?.abortGuard, {
359
+ request,
360
+ key,
361
+ bucket,
362
+ uploadId
363
+ });
364
+ await config.client.send(
365
+ new AbortMultipartUploadCommand({
366
+ Bucket: bucket,
367
+ Key: key,
368
+ UploadId: uploadId
369
+ })
370
+ );
371
+ await runLifecycleHook(config.multipart?.onAbort, {
372
+ request,
373
+ key,
374
+ bucket,
375
+ uploadId
376
+ });
377
+ return { bucket, key, uploadId, aborted: true };
378
+ }
379
+ var multipartAbort = createS3Endpoint(
380
+ S3_API_ROUTES.multipartAbort,
381
+ { method: "POST", body: multipartAbortBodySchema },
382
+ async (ctx) => {
383
+ assertFeatureEnabled(ctx.context.config, "multipart");
384
+ return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
385
+ }
386
+ );
387
+ async function handleComplete(config, input, request) {
388
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
389
+ request,
390
+ key: input.key,
391
+ bucket: input.bucket
392
+ });
393
+ const uploadId = input.uploadId;
394
+ const parts = input.parts.map(({ partNumber }) => partNumber).sort((a, b) => a - b);
395
+ const partRefs = parts.map((partNumber) => ({ partNumber }));
396
+ await runHook(config.multipart?.completeGuard, {
397
+ request,
398
+ key,
399
+ bucket,
400
+ uploadId,
401
+ parts: partRefs
402
+ });
403
+ const listed = await config.client.send(
404
+ new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
405
+ );
406
+ const listedParts = listed.Parts ?? [];
407
+ const completeParts = parts.map((partNumber) => {
408
+ const found = listedParts.find((p) => p.PartNumber === partNumber);
409
+ return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
410
+ });
411
+ const completeResult = await config.client.send(
412
+ new CompleteMultipartUploadCommand({
413
+ Bucket: bucket,
414
+ Key: key,
415
+ UploadId: uploadId,
416
+ MultipartUpload: { Parts: completeParts }
417
+ })
418
+ );
419
+ let head = await config.client.send(
420
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
421
+ );
422
+ for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
423
+ await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
424
+ head = await config.client.send(
425
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
426
+ );
427
+ }
428
+ const contentLength = head.ContentLength ?? 0;
429
+ const contentType = head.ContentType;
430
+ const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
431
+ const metadata = head.Metadata ?? {};
432
+ const versionId = head.VersionId;
433
+ const lastModified = head.LastModified?.toISOString();
434
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
435
+ const fileName = parseFileName(head.ContentDisposition);
436
+ await runLifecycleHook(config.multipart?.onComplete, {
437
+ request,
438
+ key,
439
+ bucket,
440
+ uploadId,
441
+ contentLength,
442
+ contentType,
443
+ eTag,
444
+ metadata,
445
+ acl,
446
+ fileName,
447
+ versionId,
448
+ lastModified
449
+ });
450
+ return {
451
+ bucket,
452
+ key,
453
+ uploadId,
454
+ contentLength,
455
+ contentType,
456
+ eTag,
457
+ metadata,
458
+ acl,
459
+ fileName,
460
+ versionId,
461
+ lastModified
462
+ };
463
+ }
464
+ var multipartComplete = createS3Endpoint(
465
+ S3_API_ROUTES.multipartComplete,
466
+ { method: "POST", body: multipartCompleteBodySchema },
467
+ async (ctx) => {
468
+ assertFeatureEnabled(ctx.context.config, "multipart");
469
+ return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
470
+ }
471
+ );
472
+ async function handleMultipartInit(config, input, request) {
473
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
474
+ request,
475
+ key: input.key,
476
+ bucket: input.bucket,
477
+ fileName: input.fileName,
478
+ contentType: input.contentType
479
+ });
480
+ const acl = input.acl === "public-read" ? "public-read" : "private";
481
+ const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
482
+ if (config.multipart?.requireFileSize && fileSize === void 0) {
483
+ throw errors.fileSizeRequiredMultipart();
484
+ }
485
+ await runHook(config.multipart?.initGuard, {
486
+ request,
487
+ key,
488
+ bucket,
489
+ fileSize
490
+ });
491
+ const { UploadId } = await config.client.send(
492
+ new CreateMultipartUploadCommand({
493
+ Bucket: bucket,
494
+ Key: key,
495
+ ContentType: input.contentType,
496
+ ContentDisposition: input.fileName ? buildContentDisposition(input.fileName) : void 0,
497
+ Metadata: input.metadata,
498
+ ACL: acl
499
+ })
500
+ );
501
+ await runLifecycleHook(config.multipart?.onInit, {
502
+ request,
503
+ key,
504
+ bucket,
505
+ uploadId: UploadId,
506
+ contentType: input.contentType,
507
+ fileSize,
508
+ metadata: input.metadata,
509
+ acl,
510
+ fileName: input.fileName
511
+ });
512
+ return { bucket, key, uploadId: UploadId };
513
+ }
514
+ var multipartInit = createS3Endpoint(
515
+ S3_API_ROUTES.multipartInit,
516
+ { method: "POST", body: multipartInitBodySchema },
517
+ async (ctx) => {
518
+ assertFeatureEnabled(ctx.context.config, "multipart");
519
+ ctx.setStatus(201);
520
+ return handleMultipartInit(
521
+ ctx.context.config,
522
+ ctx.body,
523
+ ctx.context.request
524
+ );
525
+ }
526
+ );
527
+ async function handleListParts(config, input, request) {
528
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
529
+ request,
530
+ key: input.key,
531
+ bucket: input.bucket
532
+ });
533
+ const uploadId = input.uploadId;
534
+ await runHook(config.multipart?.listGuard, {
535
+ request,
536
+ key,
537
+ bucket,
538
+ uploadId
539
+ });
540
+ const response = await config.client.send(
541
+ new ListPartsCommand({
542
+ Bucket: bucket,
543
+ Key: key,
544
+ UploadId: uploadId
545
+ })
546
+ );
547
+ const parts = (response.Parts ?? []).map((p) => ({
548
+ partNumber: p.PartNumber ?? 0,
549
+ size: p.Size ?? 0,
550
+ eTag: (p.ETag ?? "").replace(/"/g, "")
551
+ }));
552
+ await runLifecycleHook(config.multipart?.onList, {
553
+ request,
554
+ key,
555
+ bucket,
556
+ uploadId,
557
+ parts
558
+ });
559
+ return { parts };
560
+ }
561
+ var multipartListParts = createS3Endpoint(
562
+ S3_API_ROUTES.multipartListParts,
563
+ { method: "GET", query: multipartListPartsQuerySchema },
564
+ async (ctx) => {
565
+ assertFeatureEnabled(ctx.context.config, "multipart");
566
+ return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
567
+ }
568
+ );
569
+ async function handleSignPart(config, input, request) {
570
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
571
+ request,
572
+ key: input.key,
573
+ bucket: input.bucket
574
+ });
575
+ const uploadId = input.uploadId;
576
+ const partNumber = input.partNumber;
577
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
578
+ const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
579
+ await runHook(config.multipart?.partGuard, {
580
+ request,
581
+ key,
582
+ bucket,
583
+ uploadId,
584
+ partNumber,
585
+ partSize: partSize ?? void 0
586
+ });
587
+ const presignedUrl = await getSignedUrl(
588
+ config.client,
589
+ new UploadPartCommand({
590
+ Bucket: bucket,
591
+ Key: key,
592
+ UploadId: uploadId,
593
+ PartNumber: partNumber,
594
+ ...partSize !== null ? { ContentLength: partSize } : {}
595
+ }),
596
+ {
597
+ expiresIn,
598
+ ...partSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
599
+ }
600
+ );
601
+ return {
602
+ presignedUrl,
603
+ partNumber,
604
+ uploadId,
605
+ bucket,
606
+ expiresIn,
607
+ ...partSize !== null ? { partSize } : {}
608
+ };
609
+ }
610
+ var multipartPart = createS3Endpoint(
611
+ S3_API_ROUTES.multipartPart,
612
+ { method: "POST", body: multipartSignPartBodySchema },
613
+ async (ctx) => {
614
+ assertFeatureEnabled(ctx.context.config, "multipart");
615
+ return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
616
+ }
617
+ );
618
+ async function handleUpload(config, input, request) {
619
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
620
+ request,
621
+ key: input.key,
622
+ bucket: input.bucket,
623
+ fileName: input.fileName,
624
+ contentType: input.contentType
625
+ });
626
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
627
+ const acl = input.acl === "public-read" ? "public-read" : "private";
628
+ const contentType = input.contentType ?? "application/octet-stream";
629
+ const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
630
+ await runHook(config.upload?.guard, {
631
+ request,
632
+ key,
633
+ bucket,
634
+ contentType: input.contentType,
635
+ fileSize: fileSize ?? void 0,
636
+ metadata: input.metadata,
637
+ acl,
638
+ fileName: input.fileName
639
+ });
640
+ const method = config.upload?.method ?? "POST";
641
+ if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
642
+ throw errors.fileSizeRequiredUpload();
643
+ }
644
+ if (method === "PUT") {
645
+ const putHeaders = {
646
+ "Content-Type": contentType
647
+ };
648
+ if (input.fileName) {
649
+ putHeaders["Content-Disposition"] = buildContentDisposition(
650
+ input.fileName
651
+ );
652
+ }
653
+ const url2 = await getSignedUrl(
654
+ config.client,
655
+ new PutObjectCommand({
656
+ Bucket: bucket,
657
+ Key: key,
658
+ ContentType: contentType,
659
+ ACL: acl,
660
+ ...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
661
+ ...fileSize !== null ? { ContentLength: fileSize } : {}
662
+ }),
663
+ {
664
+ expiresIn,
665
+ ...fileSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
666
+ }
667
+ );
668
+ await runLifecycleHook(config.upload?.onPresigned, {
669
+ request,
670
+ key,
671
+ bucket,
672
+ contentType: input.contentType,
673
+ fileSize: fileSize ?? void 0,
674
+ metadata: input.metadata,
675
+ acl,
676
+ fileName: input.fileName,
677
+ url: url2,
678
+ expiresIn
679
+ });
680
+ return {
681
+ bucket,
682
+ key,
683
+ url: url2,
684
+ headers: putHeaders,
685
+ expiresIn,
686
+ method: "PUT"
687
+ };
688
+ }
689
+ const fields = { acl, "Content-Type": contentType };
690
+ if (input.fileName) {
691
+ fields["Content-Disposition"] = buildContentDisposition(input.fileName);
692
+ }
693
+ if (input.metadata) {
694
+ for (const [k, v] of Object.entries(input.metadata)) {
695
+ fields[`x-amz-meta-${k}`] = v;
696
+ }
697
+ }
698
+ const rangeMin = fileSize ?? 1;
699
+ const rangeMax = fileSize ?? void 0;
700
+ const { url, fields: signedFields } = await createPresignedPost(
701
+ config.client,
702
+ {
703
+ Bucket: bucket,
704
+ Key: key,
705
+ Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
706
+ Fields: fields,
707
+ Expires: expiresIn
708
+ }
709
+ );
710
+ await runLifecycleHook(config.upload?.onPresigned, {
711
+ request,
712
+ key,
713
+ bucket,
714
+ contentType: input.contentType,
715
+ fileSize: fileSize ?? void 0,
716
+ metadata: input.metadata,
717
+ acl,
718
+ fileName: input.fileName,
719
+ url,
720
+ expiresIn
721
+ });
722
+ return {
723
+ bucket,
724
+ key,
725
+ url,
726
+ fields: signedFields,
727
+ expiresIn,
728
+ method: "POST"
729
+ };
730
+ }
731
+ var upload = createS3Endpoint(
732
+ S3_API_ROUTES.upload,
733
+ { method: "POST", body: uploadBodySchema },
734
+ async (ctx) => {
735
+ assertFeatureEnabled(ctx.context.config, "upload");
736
+ return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
737
+ }
738
+ );
739
+
740
+ // src/api/routes/index.ts
741
+ var coreEndpoints = {
742
+ upload,
743
+ confirm,
744
+ download,
745
+ delete: deleteObject,
746
+ multipartInit,
747
+ multipartPart,
748
+ multipartListParts,
749
+ multipartComplete,
750
+ multipartAbort
751
+ };
752
+ var CORE_ENDPOINT_NAMES = Object.keys(
753
+ coreEndpoints
754
+ );
755
+
756
+ export { CORE_ENDPOINT_NAMES, coreEndpoints, createS3Endpoint, createS3Middleware, s3ContextMiddleware };
757
+ //# sourceMappingURL=index.js.map
758
+ //# sourceMappingURL=index.js.map