@dimah-s3/server 0.7.4 → 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,984 @@
1
+ import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition } from '@dimah-s3/core';
2
+ import { HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
3
+ import { createMiddleware, createEndpoint } from 'better-call';
4
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
+ import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
6
+
7
+ // src/plugin/apply-plugins.ts
8
+
9
+ // src/helpers/is-aws-not-found.ts
10
+ function isAwsNotFound(err) {
11
+ const e = err;
12
+ return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
13
+ }
14
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
15
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
18
+ try {
19
+ const result = await s3.send(
20
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
21
+ { abortSignal: controller.signal }
22
+ );
23
+ const isPublic = result.Grants?.some(
24
+ (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
25
+ );
26
+ return isPublic ? "public-read" : "private";
27
+ } catch {
28
+ return void 0;
29
+ } finally {
30
+ clearTimeout(timeout);
31
+ }
32
+ }
33
+ var errors = {
34
+ notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
35
+ unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
36
+ forbidden: () => DimahS3Error.from("FORBIDDEN", S3_ERROR_CODES.FORBIDDEN),
37
+ conflict: () => DimahS3Error.from("CONFLICT", S3_ERROR_CODES.CONFLICT),
38
+ internalError: () => DimahS3Error.from("INTERNAL_SERVER_ERROR", S3_ERROR_CODES.INTERNAL_ERROR),
39
+ objectNotFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.OBJECT_NOT_FOUND),
40
+ featureDisabled: (feature) => DimahS3Error.from("NOT_FOUND", {
41
+ code: S3_ERROR_CODES.FEATURE_DISABLED.code,
42
+ message: S3_ERROR_CODES.FEATURE_DISABLED.message.replaceAll(
43
+ "{feature}",
44
+ feature
45
+ ),
46
+ params: { feature }
47
+ }),
48
+ invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
49
+ invalidBucket: (bucket) => DimahS3Error.from("FORBIDDEN", {
50
+ code: S3_ERROR_CODES.INVALID_BUCKET.code,
51
+ message: S3_ERROR_CODES.INVALID_BUCKET.message,
52
+ params: { bucket }
53
+ }),
54
+ s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
55
+ code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
56
+ message: S3_ERROR_CODES.S3_NETWORK_ERROR.message.replaceAll(
57
+ "{code}",
58
+ code
59
+ ),
60
+ params: { code }
61
+ }),
62
+ fileSizeRequiredUpload: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD),
63
+ fileSizeRequiredMultipart: () => DimahS3Error.from(
64
+ "BAD_REQUEST",
65
+ S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
66
+ ),
67
+ validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
68
+ code: S3_ERROR_CODES.VALIDATION_ERROR.code,
69
+ message
70
+ })
71
+ };
72
+
73
+ // src/internal-helpers.ts
74
+ async function runHook(hook, context) {
75
+ if (!hook) return;
76
+ try {
77
+ await hook(context);
78
+ } catch (err) {
79
+ if (isAPIError(err)) throw err;
80
+ if (err instanceof Error && err.message.trim()) {
81
+ throw new DimahS3Error("FORBIDDEN", {
82
+ message: err.message,
83
+ cause: err
84
+ });
85
+ }
86
+ throw errors.forbidden();
87
+ }
88
+ }
89
+ async function runLifecycleHook(hook, context) {
90
+ if (!hook) return;
91
+ try {
92
+ await hook(context);
93
+ } catch (err) {
94
+ if (isAPIError(err)) throw err;
95
+ console.error("[S3 API] lifecycle hook failed", err);
96
+ throw errors.internalError();
97
+ }
98
+ }
99
+ function normalizeExpiresIn(value) {
100
+ const n = Number(value);
101
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
102
+ }
103
+ function requestFromHeaders(headers) {
104
+ return new Request("http://dimah-s3.local", { headers });
105
+ }
106
+
107
+ // src/helpers/resolve-target.ts
108
+ function assertSafeObjectKey(key) {
109
+ const trimmed = key.replace(/^\/+/u, "").replace(/\/+$/u, "");
110
+ if (!trimmed) {
111
+ throw errors.invalidKey();
112
+ }
113
+ if (trimmed.includes("\\") || trimmed.includes("\0")) {
114
+ throw errors.invalidKey();
115
+ }
116
+ const parts = trimmed.split("/");
117
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
118
+ throw errors.invalidKey();
119
+ }
120
+ return parts.join("/");
121
+ }
122
+ function applyPrefix(prefix, key) {
123
+ const p = assertSafeObjectKey(prefix);
124
+ const k = assertSafeObjectKey(key);
125
+ if (k === p || k.startsWith(`${p}/`)) return k;
126
+ return `${p}/${k}`;
127
+ }
128
+ async function resolveObjectKey(policy, context) {
129
+ if (policy?.resolveKey) {
130
+ return assertSafeObjectKey(await policy.resolveKey(context));
131
+ }
132
+ const prefix = typeof policy?.prefix === "function" ? await policy.prefix(context) : policy?.prefix;
133
+ if (prefix) {
134
+ return applyPrefix(prefix, context.proposedKey);
135
+ }
136
+ return assertSafeObjectKey(context.proposedKey);
137
+ }
138
+ function resolveBucket(config, requested) {
139
+ if (!requested) return config.bucket;
140
+ if (config.allowClientBucket) return requested;
141
+ if (config.buckets?.length) {
142
+ if (config.buckets.includes(requested)) return requested;
143
+ throw errors.invalidBucket(requested);
144
+ }
145
+ return config.bucket;
146
+ }
147
+ async function resolveRequestTarget(config, policy, input) {
148
+ const bucket = resolveBucket(config, input.bucket);
149
+ const key = await resolveObjectKey(policy, {
150
+ request: input.request,
151
+ proposedKey: input.key,
152
+ fileName: input.fileName,
153
+ contentType: input.contentType,
154
+ bucket
155
+ });
156
+ return { key, bucket };
157
+ }
158
+ function normalizeFeature(value) {
159
+ if (value === void 0) return void 0;
160
+ if (value === false) return { enabled: false };
161
+ if (value === true) return { enabled: true };
162
+ return { ...value, enabled: value.enabled ?? true };
163
+ }
164
+ function isFeatureEnabled(config, feature) {
165
+ return config[feature]?.enabled === true;
166
+ }
167
+ function applyMultipartDefault(config, multipartInput) {
168
+ let next = config;
169
+ if (multipartInput === void 0 && config.upload?.enabled) {
170
+ next = {
171
+ ...config,
172
+ multipart: { ...config.multipart, enabled: true }
173
+ };
174
+ }
175
+ if (next.multipart && next.upload) {
176
+ next = {
177
+ ...next,
178
+ multipart: {
179
+ ...next.multipart,
180
+ prefix: next.multipart.prefix ?? next.upload.prefix,
181
+ resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
182
+ }
183
+ };
184
+ }
185
+ return next;
186
+ }
187
+ function normalizeFeatures(config) {
188
+ return {
189
+ upload: normalizeFeature(config.upload),
190
+ download: normalizeFeature(config.download),
191
+ delete: normalizeFeature(config.delete),
192
+ multipart: normalizeFeature(config.multipart)
193
+ };
194
+ }
195
+
196
+ // src/api/assert-feature-enabled.ts
197
+ function assertFeatureEnabled(config, feature) {
198
+ if (!isFeatureEnabled(config, feature)) {
199
+ throw errors.featureDisabled(feature);
200
+ }
201
+ }
202
+ var s3ContextMiddleware = createMiddleware(async (ctx) => {
203
+ const injected = ctx.context;
204
+ if (!injected?.config) {
205
+ throw new Error(
206
+ "createS3Endpoint requires dimahS3 router context. Call endpoints through dimahS3().api or the HTTP handler."
207
+ );
208
+ }
209
+ const request = ctx.request ?? injected.request ?? requestFromHeaders(ctx.headers);
210
+ await runHook(injected.config.guard, { request });
211
+ return {
212
+ config: injected.config,
213
+ request
214
+ };
215
+ });
216
+ var createS3Middleware = createMiddleware.create({
217
+ use: [s3ContextMiddleware]
218
+ });
219
+
220
+ // src/api/create-s3-endpoint.ts
221
+ var createEndpointWithContext = createEndpoint.create({
222
+ use: [s3ContextMiddleware]
223
+ });
224
+ function withS3Validation(options) {
225
+ return {
226
+ ...options,
227
+ onValidationError: options.onValidationError ?? (({ message }) => {
228
+ throw errors.validationError(message);
229
+ })
230
+ };
231
+ }
232
+ var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
233
+ if (typeof pathOrOptions === "string") {
234
+ return createEndpointWithContext(
235
+ pathOrOptions,
236
+ withS3Validation(optionsOrHandler),
237
+ maybeHandler
238
+ );
239
+ }
240
+ return createEndpointWithContext(
241
+ withS3Validation(pathOrOptions),
242
+ optionsOrHandler
243
+ );
244
+ });
245
+
246
+ // src/api/routes/confirm.ts
247
+ async function handleConfirm(config, input, request) {
248
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
249
+ request,
250
+ key: input.key,
251
+ bucket: input.bucket
252
+ });
253
+ await runHook(config.upload?.confirmGuard, {
254
+ request,
255
+ key,
256
+ bucket
257
+ });
258
+ const head = await config.client.send(
259
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
260
+ );
261
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
262
+ const fileName = parseFileName(head.ContentDisposition);
263
+ const context = {
264
+ request,
265
+ key,
266
+ bucket,
267
+ contentType: head.ContentType,
268
+ contentLength: head.ContentLength ?? 0,
269
+ eTag: head.ETag?.replace(/"/g, ""),
270
+ metadata: head.Metadata,
271
+ acl,
272
+ fileName,
273
+ versionId: head.VersionId,
274
+ lastModified: head.LastModified?.toISOString()
275
+ };
276
+ await runLifecycleHook(config.upload?.onConfirmed, context);
277
+ return {
278
+ key,
279
+ bucket,
280
+ contentType: context.contentType,
281
+ contentLength: context.contentLength,
282
+ eTag: context.eTag,
283
+ metadata: context.metadata ?? {},
284
+ acl,
285
+ fileName,
286
+ versionId: context.versionId,
287
+ lastModified: context.lastModified
288
+ };
289
+ }
290
+ var confirm = createS3Endpoint(
291
+ S3_API_ROUTES.uploadConfirm,
292
+ { method: "POST", body: confirmBodySchema },
293
+ async (ctx) => {
294
+ assertFeatureEnabled(ctx.context.config, "upload");
295
+ return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
296
+ }
297
+ );
298
+ async function handleDelete(config, input, request) {
299
+ const { key, bucket } = await resolveRequestTarget(config, config.delete, {
300
+ request,
301
+ key: input.key,
302
+ bucket: input.bucket
303
+ });
304
+ await runHook(config.delete?.guard, {
305
+ request,
306
+ key,
307
+ bucket
308
+ });
309
+ try {
310
+ await config.client.send(
311
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
312
+ );
313
+ } catch (err) {
314
+ if (isAwsNotFound(err)) {
315
+ throw errors.objectNotFound();
316
+ }
317
+ throw err;
318
+ }
319
+ await config.client.send(
320
+ new DeleteObjectCommand({ Bucket: bucket, Key: key })
321
+ );
322
+ await runLifecycleHook(config.delete?.onDeleted, { request, key, bucket });
323
+ return { success: true, bucket, key };
324
+ }
325
+ var deleteObject = createS3Endpoint(
326
+ S3_API_ROUTES.delete,
327
+ { method: "DELETE", query: deleteQuerySchema },
328
+ async (ctx) => {
329
+ assertFeatureEnabled(ctx.context.config, "delete");
330
+ return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
331
+ }
332
+ );
333
+ async function handleDownload(config, input, request) {
334
+ const { key, bucket } = await resolveRequestTarget(config, config.download, {
335
+ request,
336
+ key: input.key,
337
+ bucket: input.bucket,
338
+ fileName: input.fileName
339
+ });
340
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
341
+ const fileName = input.fileName;
342
+ await runHook(config.download?.guard, {
343
+ request,
344
+ key,
345
+ bucket,
346
+ fileName
347
+ });
348
+ try {
349
+ await config.client.send(
350
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
351
+ );
352
+ } catch (err) {
353
+ if (isAwsNotFound(err)) {
354
+ throw errors.objectNotFound();
355
+ }
356
+ throw err;
357
+ }
358
+ const url = await getSignedUrl(
359
+ config.client,
360
+ new GetObjectCommand({
361
+ Bucket: bucket,
362
+ Key: key,
363
+ ResponseContentDisposition: fileName ? buildContentDisposition(fileName) : "attachment"
364
+ }),
365
+ { expiresIn }
366
+ );
367
+ await runLifecycleHook(config.download?.onPresigned, {
368
+ request,
369
+ key,
370
+ bucket,
371
+ fileName,
372
+ url,
373
+ expiresIn
374
+ });
375
+ return { bucket, key, url, expiresIn };
376
+ }
377
+ var download = createS3Endpoint(
378
+ S3_API_ROUTES.download,
379
+ { method: "GET", query: downloadQuerySchema },
380
+ async (ctx) => {
381
+ assertFeatureEnabled(ctx.context.config, "download");
382
+ return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
383
+ }
384
+ );
385
+ async function handleAbort(config, input, request) {
386
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
387
+ request,
388
+ key: input.key,
389
+ bucket: input.bucket
390
+ });
391
+ const uploadId = input.uploadId;
392
+ await runHook(config.multipart?.abortGuard, {
393
+ request,
394
+ key,
395
+ bucket,
396
+ uploadId
397
+ });
398
+ await config.client.send(
399
+ new AbortMultipartUploadCommand({
400
+ Bucket: bucket,
401
+ Key: key,
402
+ UploadId: uploadId
403
+ })
404
+ );
405
+ await runLifecycleHook(config.multipart?.onAbort, {
406
+ request,
407
+ key,
408
+ bucket,
409
+ uploadId
410
+ });
411
+ return { bucket, key, uploadId, aborted: true };
412
+ }
413
+ var multipartAbort = createS3Endpoint(
414
+ S3_API_ROUTES.multipartAbort,
415
+ { method: "POST", body: multipartAbortBodySchema },
416
+ async (ctx) => {
417
+ assertFeatureEnabled(ctx.context.config, "multipart");
418
+ return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
419
+ }
420
+ );
421
+ async function handleComplete(config, input, request) {
422
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
423
+ request,
424
+ key: input.key,
425
+ bucket: input.bucket
426
+ });
427
+ const uploadId = input.uploadId;
428
+ const parts = input.parts.map(({ partNumber }) => partNumber).sort((a, b) => a - b);
429
+ const partRefs = parts.map((partNumber) => ({ partNumber }));
430
+ await runHook(config.multipart?.completeGuard, {
431
+ request,
432
+ key,
433
+ bucket,
434
+ uploadId,
435
+ parts: partRefs
436
+ });
437
+ const listed = await config.client.send(
438
+ new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
439
+ );
440
+ const listedParts = listed.Parts ?? [];
441
+ const completeParts = parts.map((partNumber) => {
442
+ const found = listedParts.find((p) => p.PartNumber === partNumber);
443
+ return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
444
+ });
445
+ const completeResult = await config.client.send(
446
+ new CompleteMultipartUploadCommand({
447
+ Bucket: bucket,
448
+ Key: key,
449
+ UploadId: uploadId,
450
+ MultipartUpload: { Parts: completeParts }
451
+ })
452
+ );
453
+ let head = await config.client.send(
454
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
455
+ );
456
+ for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
457
+ await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
458
+ head = await config.client.send(
459
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
460
+ );
461
+ }
462
+ const contentLength = head.ContentLength ?? 0;
463
+ const contentType = head.ContentType;
464
+ const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
465
+ const metadata = head.Metadata ?? {};
466
+ const versionId = head.VersionId;
467
+ const lastModified = head.LastModified?.toISOString();
468
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
469
+ const fileName = parseFileName(head.ContentDisposition);
470
+ await runLifecycleHook(config.multipart?.onComplete, {
471
+ request,
472
+ key,
473
+ bucket,
474
+ uploadId,
475
+ contentLength,
476
+ contentType,
477
+ eTag,
478
+ metadata,
479
+ acl,
480
+ fileName,
481
+ versionId,
482
+ lastModified
483
+ });
484
+ return {
485
+ bucket,
486
+ key,
487
+ uploadId,
488
+ contentLength,
489
+ contentType,
490
+ eTag,
491
+ metadata,
492
+ acl,
493
+ fileName,
494
+ versionId,
495
+ lastModified
496
+ };
497
+ }
498
+ var multipartComplete = createS3Endpoint(
499
+ S3_API_ROUTES.multipartComplete,
500
+ { method: "POST", body: multipartCompleteBodySchema },
501
+ async (ctx) => {
502
+ assertFeatureEnabled(ctx.context.config, "multipart");
503
+ return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
504
+ }
505
+ );
506
+ async function handleMultipartInit(config, input, request) {
507
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
508
+ request,
509
+ key: input.key,
510
+ bucket: input.bucket,
511
+ fileName: input.fileName,
512
+ contentType: input.contentType
513
+ });
514
+ const acl = input.acl === "public-read" ? "public-read" : "private";
515
+ const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
516
+ if (config.multipart?.requireFileSize && fileSize === void 0) {
517
+ throw errors.fileSizeRequiredMultipart();
518
+ }
519
+ await runHook(config.multipart?.initGuard, {
520
+ request,
521
+ key,
522
+ bucket,
523
+ fileSize
524
+ });
525
+ const { UploadId } = await config.client.send(
526
+ new CreateMultipartUploadCommand({
527
+ Bucket: bucket,
528
+ Key: key,
529
+ ContentType: input.contentType,
530
+ ContentDisposition: input.fileName ? buildContentDisposition(input.fileName) : void 0,
531
+ Metadata: input.metadata,
532
+ ACL: acl
533
+ })
534
+ );
535
+ await runLifecycleHook(config.multipart?.onInit, {
536
+ request,
537
+ key,
538
+ bucket,
539
+ uploadId: UploadId,
540
+ contentType: input.contentType,
541
+ fileSize,
542
+ metadata: input.metadata,
543
+ acl,
544
+ fileName: input.fileName
545
+ });
546
+ return { bucket, key, uploadId: UploadId };
547
+ }
548
+ var multipartInit = createS3Endpoint(
549
+ S3_API_ROUTES.multipartInit,
550
+ { method: "POST", body: multipartInitBodySchema },
551
+ async (ctx) => {
552
+ assertFeatureEnabled(ctx.context.config, "multipart");
553
+ ctx.setStatus(201);
554
+ return handleMultipartInit(
555
+ ctx.context.config,
556
+ ctx.body,
557
+ ctx.context.request
558
+ );
559
+ }
560
+ );
561
+ async function handleListParts(config, input, request) {
562
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
563
+ request,
564
+ key: input.key,
565
+ bucket: input.bucket
566
+ });
567
+ const uploadId = input.uploadId;
568
+ await runHook(config.multipart?.listGuard, {
569
+ request,
570
+ key,
571
+ bucket,
572
+ uploadId
573
+ });
574
+ const response = await config.client.send(
575
+ new ListPartsCommand({
576
+ Bucket: bucket,
577
+ Key: key,
578
+ UploadId: uploadId
579
+ })
580
+ );
581
+ const parts = (response.Parts ?? []).map((p) => ({
582
+ partNumber: p.PartNumber ?? 0,
583
+ size: p.Size ?? 0,
584
+ eTag: (p.ETag ?? "").replace(/"/g, "")
585
+ }));
586
+ await runLifecycleHook(config.multipart?.onList, {
587
+ request,
588
+ key,
589
+ bucket,
590
+ uploadId,
591
+ parts
592
+ });
593
+ return { parts };
594
+ }
595
+ var multipartListParts = createS3Endpoint(
596
+ S3_API_ROUTES.multipartListParts,
597
+ { method: "GET", query: multipartListPartsQuerySchema },
598
+ async (ctx) => {
599
+ assertFeatureEnabled(ctx.context.config, "multipart");
600
+ return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
601
+ }
602
+ );
603
+ async function handleSignPart(config, input, request) {
604
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
605
+ request,
606
+ key: input.key,
607
+ bucket: input.bucket
608
+ });
609
+ const uploadId = input.uploadId;
610
+ const partNumber = input.partNumber;
611
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
612
+ const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
613
+ await runHook(config.multipart?.partGuard, {
614
+ request,
615
+ key,
616
+ bucket,
617
+ uploadId,
618
+ partNumber,
619
+ partSize: partSize ?? void 0
620
+ });
621
+ const presignedUrl = await getSignedUrl(
622
+ config.client,
623
+ new UploadPartCommand({
624
+ Bucket: bucket,
625
+ Key: key,
626
+ UploadId: uploadId,
627
+ PartNumber: partNumber,
628
+ ...partSize !== null ? { ContentLength: partSize } : {}
629
+ }),
630
+ {
631
+ expiresIn,
632
+ ...partSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
633
+ }
634
+ );
635
+ return {
636
+ presignedUrl,
637
+ partNumber,
638
+ uploadId,
639
+ bucket,
640
+ expiresIn,
641
+ ...partSize !== null ? { partSize } : {}
642
+ };
643
+ }
644
+ var multipartPart = createS3Endpoint(
645
+ S3_API_ROUTES.multipartPart,
646
+ { method: "POST", body: multipartSignPartBodySchema },
647
+ async (ctx) => {
648
+ assertFeatureEnabled(ctx.context.config, "multipart");
649
+ return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
650
+ }
651
+ );
652
+ async function handleUpload(config, input, request) {
653
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
654
+ request,
655
+ key: input.key,
656
+ bucket: input.bucket,
657
+ fileName: input.fileName,
658
+ contentType: input.contentType
659
+ });
660
+ const expiresIn = normalizeExpiresIn(input.expiresIn);
661
+ const acl = input.acl === "public-read" ? "public-read" : "private";
662
+ const contentType = input.contentType ?? "application/octet-stream";
663
+ const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
664
+ await runHook(config.upload?.guard, {
665
+ request,
666
+ key,
667
+ bucket,
668
+ contentType: input.contentType,
669
+ fileSize: fileSize ?? void 0,
670
+ metadata: input.metadata,
671
+ acl,
672
+ fileName: input.fileName
673
+ });
674
+ const method = config.upload?.method ?? "POST";
675
+ if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
676
+ throw errors.fileSizeRequiredUpload();
677
+ }
678
+ if (method === "PUT") {
679
+ const putHeaders = {
680
+ "Content-Type": contentType
681
+ };
682
+ if (input.fileName) {
683
+ putHeaders["Content-Disposition"] = buildContentDisposition(
684
+ input.fileName
685
+ );
686
+ }
687
+ const url2 = await getSignedUrl(
688
+ config.client,
689
+ new PutObjectCommand({
690
+ Bucket: bucket,
691
+ Key: key,
692
+ ContentType: contentType,
693
+ ACL: acl,
694
+ ...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
695
+ ...fileSize !== null ? { ContentLength: fileSize } : {}
696
+ }),
697
+ {
698
+ expiresIn,
699
+ ...fileSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
700
+ }
701
+ );
702
+ await runLifecycleHook(config.upload?.onPresigned, {
703
+ request,
704
+ key,
705
+ bucket,
706
+ contentType: input.contentType,
707
+ fileSize: fileSize ?? void 0,
708
+ metadata: input.metadata,
709
+ acl,
710
+ fileName: input.fileName,
711
+ url: url2,
712
+ expiresIn
713
+ });
714
+ return {
715
+ bucket,
716
+ key,
717
+ url: url2,
718
+ headers: putHeaders,
719
+ expiresIn,
720
+ method: "PUT"
721
+ };
722
+ }
723
+ const fields = { acl, "Content-Type": contentType };
724
+ if (input.fileName) {
725
+ fields["Content-Disposition"] = buildContentDisposition(input.fileName);
726
+ }
727
+ if (input.metadata) {
728
+ for (const [k, v] of Object.entries(input.metadata)) {
729
+ fields[`x-amz-meta-${k}`] = v;
730
+ }
731
+ }
732
+ const rangeMin = fileSize ?? 1;
733
+ const rangeMax = fileSize ?? void 0;
734
+ const { url, fields: signedFields } = await createPresignedPost(
735
+ config.client,
736
+ {
737
+ Bucket: bucket,
738
+ Key: key,
739
+ Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
740
+ Fields: fields,
741
+ Expires: expiresIn
742
+ }
743
+ );
744
+ await runLifecycleHook(config.upload?.onPresigned, {
745
+ request,
746
+ key,
747
+ bucket,
748
+ contentType: input.contentType,
749
+ fileSize: fileSize ?? void 0,
750
+ metadata: input.metadata,
751
+ acl,
752
+ fileName: input.fileName,
753
+ url,
754
+ expiresIn
755
+ });
756
+ return {
757
+ bucket,
758
+ key,
759
+ url,
760
+ fields: signedFields,
761
+ expiresIn,
762
+ method: "POST"
763
+ };
764
+ }
765
+ var upload = createS3Endpoint(
766
+ S3_API_ROUTES.upload,
767
+ { method: "POST", body: uploadBodySchema },
768
+ async (ctx) => {
769
+ assertFeatureEnabled(ctx.context.config, "upload");
770
+ return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
771
+ }
772
+ );
773
+
774
+ // src/api/routes/index.ts
775
+ var coreEndpoints = {
776
+ upload,
777
+ confirm,
778
+ download,
779
+ delete: deleteObject,
780
+ multipartInit,
781
+ multipartPart,
782
+ multipartListParts,
783
+ multipartComplete,
784
+ multipartAbort
785
+ };
786
+ var CORE_ENDPOINT_NAMES = Object.keys(
787
+ coreEndpoints
788
+ );
789
+
790
+ // src/plugin/chain-hooks.ts
791
+ function chainHooks(...hooks) {
792
+ return async (context) => {
793
+ for (const hook of hooks) {
794
+ if (hook) await hook(context);
795
+ }
796
+ };
797
+ }
798
+
799
+ // src/plugin/hook-registry.ts
800
+ var FEATURE_HOOK_KEYS = {
801
+ upload: ["guard", "onPresigned", "confirmGuard", "onConfirmed"],
802
+ download: ["guard", "onPresigned"],
803
+ delete: ["guard", "onDeleted"],
804
+ multipart: [
805
+ "initGuard",
806
+ "partGuard",
807
+ "completeGuard",
808
+ "abortGuard",
809
+ "listGuard",
810
+ "onInit",
811
+ "onComplete",
812
+ "onAbort",
813
+ "onList"
814
+ ]
815
+ };
816
+
817
+ // src/plugin/types.ts
818
+ var RESERVED_PLUGIN_IDS = [
819
+ "handler",
820
+ "api",
821
+ "context",
822
+ "getPlugin",
823
+ "$ERROR_CODES",
824
+ "$Infer"
825
+ ];
826
+
827
+ // src/plugin/apply-plugins.ts
828
+ function mergeHookField(pluginHooks, userHook) {
829
+ const present = [...pluginHooks, userHook].filter(
830
+ (h) => h != null
831
+ );
832
+ if (present.length === 0) return void 0;
833
+ if (present.length === 1) return present[0];
834
+ return chainHooks(...present);
835
+ }
836
+ function mergeFeatureHooks(feature, keys, plugins, userFeature) {
837
+ if (!userFeature && plugins.every((p) => !p.hooks?.[feature])) {
838
+ return userFeature;
839
+ }
840
+ const merged = { ...userFeature ?? {} };
841
+ for (const key of keys) {
842
+ const fromPlugins = plugins.map(
843
+ (p) => p.hooks?.[feature]?.[key]
844
+ );
845
+ const userHook = userFeature?.[key];
846
+ const next = mergeHookField(fromPlugins, userHook);
847
+ if (next !== void 0) {
848
+ merged[key] = next;
849
+ }
850
+ }
851
+ return merged;
852
+ }
853
+ function applyPlugins(config) {
854
+ const plugins = config.plugins ?? [];
855
+ const seen = /* @__PURE__ */ new Set();
856
+ const byId = /* @__PURE__ */ new Map();
857
+ const context = {};
858
+ const getPlugin = (id) => byId.get(id);
859
+ for (const plugin of plugins) {
860
+ if (RESERVED_PLUGIN_IDS.includes(plugin.id)) {
861
+ throw new Error(
862
+ `Plugin id "${plugin.id}" is reserved on the dimahS3 instance. Choose a different id.`
863
+ );
864
+ }
865
+ if (seen.has(plugin.id)) {
866
+ throw new Error(
867
+ `Duplicate dimah-s3 plugin id "${plugin.id}". Each plugin id must be unique.`
868
+ );
869
+ }
870
+ seen.add(plugin.id);
871
+ byId.set(plugin.id, plugin);
872
+ }
873
+ for (const plugin of plugins) {
874
+ for (const dep of plugin.dependsOn ?? []) {
875
+ if (!seen.has(dep)) {
876
+ throw new Error(
877
+ `Plugin "${plugin.id}" depends on "${dep}", which is not registered.`
878
+ );
879
+ }
880
+ }
881
+ }
882
+ const visiting = /* @__PURE__ */ new Set();
883
+ const visited = /* @__PURE__ */ new Set();
884
+ function visit(id, stack) {
885
+ if (visited.has(id)) return;
886
+ if (visiting.has(id)) {
887
+ throw new Error(
888
+ `Circular plugin dependency detected: ${[...stack, id].join(" -> ")}.`
889
+ );
890
+ }
891
+ visiting.add(id);
892
+ const plugin = byId.get(id);
893
+ for (const dep of plugin?.dependsOn ?? []) {
894
+ visit(dep, [...stack, id]);
895
+ }
896
+ visiting.delete(id);
897
+ visited.add(id);
898
+ }
899
+ for (const plugin of plugins) {
900
+ visit(plugin.id, []);
901
+ }
902
+ for (const plugin of plugins) {
903
+ if (plugin.context !== void 0) {
904
+ context[plugin.id] = plugin.context;
905
+ }
906
+ }
907
+ const reservedRoutePaths = new Set(Object.values(S3_API_ROUTES));
908
+ const reservedNames = /* @__PURE__ */ new Set([...CORE_ENDPOINT_NAMES, "multipart"]);
909
+ const seenEndpointKeys = /* @__PURE__ */ new Set();
910
+ const endpoints = {};
911
+ for (const plugin of plugins) {
912
+ for (const [name, endpoint] of Object.entries(plugin.endpoints ?? {})) {
913
+ if (reservedNames.has(name) || name in endpoints) {
914
+ throw new Error(
915
+ `Plugin "${plugin.id}" endpoint "${name}" collides with an existing endpoint.`
916
+ );
917
+ }
918
+ const path = endpoint.path;
919
+ if (typeof path !== "string" || !path.startsWith("/")) {
920
+ throw new Error(
921
+ `Plugin "${plugin.id}" endpoint "${name}" path must start with "/".`
922
+ );
923
+ }
924
+ if (reservedRoutePaths.has(path)) {
925
+ throw new Error(
926
+ `Plugin "${plugin.id}" endpoint "${name}" collides with a core route.`
927
+ );
928
+ }
929
+ const methods = Array.isArray(endpoint.options.method) ? endpoint.options.method : [endpoint.options.method];
930
+ for (const method of methods) {
931
+ const routeKey = `${method} ${path}`;
932
+ if (seenEndpointKeys.has(routeKey)) {
933
+ throw new Error(`Duplicate plugin endpoint ${routeKey}.`);
934
+ }
935
+ seenEndpointKeys.add(routeKey);
936
+ }
937
+ endpoints[name] = endpoint;
938
+ }
939
+ }
940
+ const { plugins: _omit, ...rest } = config;
941
+ for (const plugin of plugins) {
942
+ plugin.init?.({ config: rest, getPlugin });
943
+ }
944
+ const features = normalizeFeatures(rest);
945
+ const {
946
+ upload: _upload,
947
+ download: _download,
948
+ delete: _delete,
949
+ multipart: _multipart,
950
+ ...base
951
+ } = rest;
952
+ const guard = mergeHookField(
953
+ plugins.map((p) => p.hooks?.guard),
954
+ rest.guard
955
+ );
956
+ const merged = {
957
+ ...base,
958
+ ...features,
959
+ ...guard !== void 0 ? { guard } : {}
960
+ };
961
+ for (const feature of Object.keys(FEATURE_HOOK_KEYS)) {
962
+ merged[feature] = mergeFeatureHooks(
963
+ feature,
964
+ FEATURE_HOOK_KEYS[feature],
965
+ plugins,
966
+ merged[feature]
967
+ );
968
+ }
969
+ return {
970
+ config: applyMultipartDefault(merged, rest.multipart),
971
+ context,
972
+ getPlugin,
973
+ endpoints
974
+ };
975
+ }
976
+
977
+ // src/plugin/define-plugin.ts
978
+ function definePlugin(plugin) {
979
+ return plugin;
980
+ }
981
+
982
+ export { CORE_ENDPOINT_NAMES, FEATURE_HOOK_KEYS, RESERVED_PLUGIN_IDS, applyPlugins, chainHooks, createS3Endpoint, createS3Middleware, definePlugin };
983
+ //# sourceMappingURL=plugins.js.map
984
+ //# sourceMappingURL=plugins.js.map