@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
package/dist/index.js CHANGED
@@ -1,127 +1,88 @@
1
- import { S3_API_ROUTES, DimahS3Error, parseFileName, buildContentDisposition, S3_ERROR_CODES, pluginEndpointPath, S3_API_BASE_PATH, normalizeS3ApiBasePath } from '@dimah-s3/core';
2
- export { DimahS3Error } from '@dimah-s3/core';
3
- import { ListPartsCommand, AbortMultipartUploadCommand, CompleteMultipartUploadCommand, HeadObjectCommand, UploadPartCommand, CreateMultipartUploadCommand, DeleteObjectCommand, GetObjectCommand, PutObjectCommand, GetObjectAclCommand } from '@aws-sdk/client-s3';
1
+ import { S3_API_ROUTES, isAPIError, DimahS3Error, S3_ERROR_CODES, confirmBodySchema, deleteQuerySchema, downloadQuerySchema, multipartAbortBodySchema, multipartCompleteBodySchema, multipartInitBodySchema, multipartListPartsQuerySchema, multipartSignPartBodySchema, uploadBodySchema, parseFileName, buildContentDisposition, normalizeS3ApiBasePath, S3_API_BASE_PATH } from '@dimah-s3/core';
2
+ export { DimahS3Error, isAPIError, isDimahS3Error, isS3ErrorCode } from '@dimah-s3/core';
3
+ import { GetObjectAclCommand, HeadObjectCommand, DeleteObjectCommand, GetObjectCommand, AbortMultipartUploadCommand, ListPartsCommand, CompleteMultipartUploadCommand, CreateMultipartUploadCommand, UploadPartCommand, PutObjectCommand } from '@aws-sdk/client-s3';
4
+ import { createMiddleware, createEndpoint, createRouter, toResponse } from 'better-call';
4
5
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
6
  import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
6
7
 
7
8
  // src/dimah-s3.ts
9
+
10
+ // src/helpers/is-aws-not-found.ts
11
+ function isAwsNotFound(err) {
12
+ const e = err;
13
+ return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
14
+ }
15
+ var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
16
+ async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
17
+ const controller = new AbortController();
18
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
19
+ try {
20
+ const result = await s3.send(
21
+ new GetObjectAclCommand({ Bucket: bucket, Key: key }),
22
+ { abortSignal: controller.signal }
23
+ );
24
+ const isPublic = result.Grants?.some(
25
+ (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
26
+ );
27
+ return isPublic ? "public-read" : "private";
28
+ } catch {
29
+ return void 0;
30
+ } finally {
31
+ clearTimeout(timeout);
32
+ }
33
+ }
8
34
  var errors = {
9
- notFound: () => new DimahS3Error("Not Found", 404, { code: S3_ERROR_CODES.NOT_FOUND }),
10
- invalidJson: () => new DimahS3Error("Invalid JSON payload", 400, {
11
- code: S3_ERROR_CODES.INVALID_JSON
12
- }),
13
- forbidden: () => new DimahS3Error("Forbidden", 403, { code: S3_ERROR_CODES.FORBIDDEN }),
14
- internalError: () => new DimahS3Error("Internal server error", 500, {
15
- code: S3_ERROR_CODES.INTERNAL_ERROR
16
- }),
17
- objectNotFound: () => new DimahS3Error("Object not found", 404, {
18
- code: S3_ERROR_CODES.OBJECT_NOT_FOUND
19
- }),
20
- s3NetworkError: (code) => new DimahS3Error(
21
- `S3 endpoint unreachable (${code}): check your endpoint URL and network connectivity`,
22
- 502,
23
- { code: S3_ERROR_CODES.S3_NETWORK_ERROR, params: { code } }
24
- ),
25
- fieldRequired: (name) => new DimahS3Error(`${name} is required`, 400, {
26
- code: S3_ERROR_CODES.FIELD_REQUIRED,
27
- params: { name }
35
+ notFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.NOT_FOUND),
36
+ unauthorized: () => DimahS3Error.from("UNAUTHORIZED", S3_ERROR_CODES.UNAUTHORIZED),
37
+ forbidden: () => DimahS3Error.from("FORBIDDEN", S3_ERROR_CODES.FORBIDDEN),
38
+ conflict: () => DimahS3Error.from("CONFLICT", S3_ERROR_CODES.CONFLICT),
39
+ internalError: () => DimahS3Error.from("INTERNAL_SERVER_ERROR", S3_ERROR_CODES.INTERNAL_ERROR),
40
+ objectNotFound: () => DimahS3Error.from("NOT_FOUND", S3_ERROR_CODES.OBJECT_NOT_FOUND),
41
+ featureDisabled: (feature) => DimahS3Error.from("NOT_FOUND", {
42
+ code: S3_ERROR_CODES.FEATURE_DISABLED.code,
43
+ message: S3_ERROR_CODES.FEATURE_DISABLED.message.replaceAll(
44
+ "{feature}",
45
+ feature
46
+ ),
47
+ params: { feature }
28
48
  }),
29
- /** Missing `key` on query-string routes (download / delete / list-parts). */
30
- keyRequired: () => new DimahS3Error("key query parameter is required", 400, {
31
- code: S3_ERROR_CODES.KEY_REQUIRED
49
+ invalidKey: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.INVALID_KEY),
50
+ invalidBucket: (bucket) => DimahS3Error.from("FORBIDDEN", {
51
+ code: S3_ERROR_CODES.INVALID_BUCKET.code,
52
+ message: S3_ERROR_CODES.INVALID_BUCKET.message,
53
+ params: { bucket }
32
54
  }),
33
- uploadIdRequired: () => new DimahS3Error("uploadId is required", 400, {
34
- code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED
55
+ s3NetworkError: (code) => DimahS3Error.from("BAD_GATEWAY", {
56
+ code: S3_ERROR_CODES.S3_NETWORK_ERROR.code,
57
+ message: S3_ERROR_CODES.S3_NETWORK_ERROR.message.replaceAll(
58
+ "{code}",
59
+ code
60
+ ),
61
+ params: { code }
35
62
  }),
36
- partNumberInvalid: () => new DimahS3Error("partNumber must be a positive integer", 400, {
37
- code: S3_ERROR_CODES.PART_NUMBER_INVALID
38
- }),
39
- partsRequired: () => new DimahS3Error("At least one valid part is required", 400, {
40
- code: S3_ERROR_CODES.PARTS_REQUIRED
41
- }),
42
- fileSizeRequiredUpload: () => new DimahS3Error(
43
- "fileSize is required when upload.requireFileSize is enabled",
44
- 400,
45
- { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD }
63
+ fileSizeRequiredUpload: () => DimahS3Error.from("BAD_REQUEST", S3_ERROR_CODES.FILE_SIZE_REQUIRED_UPLOAD),
64
+ fileSizeRequiredMultipart: () => DimahS3Error.from(
65
+ "BAD_REQUEST",
66
+ S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART
46
67
  ),
47
- fileSizeRequiredMultipart: () => new DimahS3Error(
48
- "fileSize is required when multipart.requireFileSize is enabled",
49
- 400,
50
- { code: S3_ERROR_CODES.FILE_SIZE_REQUIRED_MULTIPART }
51
- )
68
+ validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
69
+ code: S3_ERROR_CODES.VALIDATION_ERROR.code,
70
+ message
71
+ })
52
72
  };
53
- function requireString(value, name) {
54
- const trimmed = typeof value === "string" ? value.trim() : "";
55
- if (!trimmed) throw errors.fieldRequired(name);
56
- return trimmed;
57
- }
58
- function statusOf(err, fallback) {
59
- return typeof err?.status === "number" ? err.status : fallback;
60
- }
61
- function toDimahS3Error(err, fallbackMessage, fallbackStatus = 403, options) {
62
- if (err instanceof DimahS3Error) return err;
63
- const message = err instanceof Error && err.message.trim() ? err.message : fallbackMessage;
64
- const status = statusOf(err, fallbackStatus);
65
- return new DimahS3Error(message, status, {
66
- cause: err instanceof Error ? err : void 0,
67
- ...{}
68
- });
69
- }
70
- async function parseBody(request) {
71
- try {
72
- const body = await request.json();
73
- return body && typeof body === "object" ? body : null;
74
- } catch {
75
- return null;
76
- }
77
- }
78
- function errorJson(err) {
79
- return Response.json(
80
- {
81
- message: err.message,
82
- ...err.code !== void 0 ? { code: err.code } : {},
83
- ...err.params !== void 0 ? { params: err.params } : {}
84
- },
85
- { status: err.status }
86
- );
87
- }
88
- var NETWORK_CODES = /* @__PURE__ */ new Set([
89
- "EAI_AGAIN",
90
- "ECONNREFUSED",
91
- "ECONNRESET",
92
- "ETIMEDOUT",
93
- "ENOTFOUND"
94
- ]);
95
- function toErrorResponse(err) {
96
- if (err instanceof DimahS3Error) {
97
- return errorJson(err);
98
- }
99
- const code = err?.code;
100
- if (typeof code === "string" && NETWORK_CODES.has(code)) {
101
- const networkErr = errors.s3NetworkError(code);
102
- console.error("[S3 API]", networkErr.message, err);
103
- return errorJson(networkErr);
104
- }
105
- console.error("[S3 API]", err);
106
- return errorJson(errors.internalError());
107
- }
108
- function wrapHandler(handler) {
109
- return async (request) => {
110
- try {
111
- return await handler(request);
112
- } catch (err) {
113
- return toErrorResponse(err);
114
- }
115
- };
116
- }
73
+
74
+ // src/internal-helpers.ts
117
75
  async function runHook(hook, context) {
118
76
  if (!hook) return;
119
77
  try {
120
78
  await hook(context);
121
79
  } catch (err) {
122
- if (err instanceof DimahS3Error) throw err;
80
+ if (isAPIError(err)) throw err;
123
81
  if (err instanceof Error && err.message.trim()) {
124
- throw toDimahS3Error(err, errors.forbidden().message, 403);
82
+ throw new DimahS3Error("FORBIDDEN", {
83
+ message: err.message,
84
+ cause: err
85
+ });
125
86
  }
126
87
  throw errors.forbidden();
127
88
  }
@@ -131,7 +92,7 @@ async function runLifecycleHook(hook, context) {
131
92
  try {
132
93
  await hook(context);
133
94
  } catch (err) {
134
- if (err instanceof DimahS3Error) throw err;
95
+ if (isAPIError(err)) throw err;
135
96
  console.error("[S3 API] lifecycle hook failed", err);
136
97
  throw errors.internalError();
137
98
  }
@@ -144,44 +105,161 @@ function requestFromHeaders(headers) {
144
105
  return new Request("http://dimah-s3.local", { headers });
145
106
  }
146
107
 
147
- // src/helpers/is-aws-not-found.ts
148
- function isAwsNotFound(err) {
149
- const e = err;
150
- return e?.name === "NoSuchKey" || e?.name === "NotFound" || e?.Code === "NoSuchKey" || e?.Code === "NotFound" || e?.$metadata?.httpStatusCode === 404;
108
+ // src/helpers/resolve-target.ts
109
+ function assertSafeObjectKey(key) {
110
+ const trimmed = key.replace(/^\/+/u, "").replace(/\/+$/u, "");
111
+ if (!trimmed) {
112
+ throw errors.invalidKey();
113
+ }
114
+ if (trimmed.includes("\\") || trimmed.includes("\0")) {
115
+ throw errors.invalidKey();
116
+ }
117
+ const parts = trimmed.split("/");
118
+ if (parts.some((part) => part === "" || part === "." || part === "..")) {
119
+ throw errors.invalidKey();
120
+ }
121
+ return parts.join("/");
151
122
  }
152
- var DEFAULT_ACL_LOOKUP_TIMEOUT_MS = 1500;
153
- async function resolveObjectAcl(s3, bucket, key, timeoutMs = DEFAULT_ACL_LOOKUP_TIMEOUT_MS) {
154
- const controller = new AbortController();
155
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
156
- try {
157
- const result = await s3.send(
158
- new GetObjectAclCommand({ Bucket: bucket, Key: key }),
159
- { abortSignal: controller.signal }
160
- );
161
- const isPublic = result.Grants?.some(
162
- (g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL")
123
+ function applyPrefix(prefix, key) {
124
+ const p = assertSafeObjectKey(prefix);
125
+ const k = assertSafeObjectKey(key);
126
+ if (k === p || k.startsWith(`${p}/`)) return k;
127
+ return `${p}/${k}`;
128
+ }
129
+ async function resolveObjectKey(policy, context) {
130
+ if (policy?.resolveKey) {
131
+ return assertSafeObjectKey(await policy.resolveKey(context));
132
+ }
133
+ const prefix = typeof policy?.prefix === "function" ? await policy.prefix(context) : policy?.prefix;
134
+ if (prefix) {
135
+ return applyPrefix(prefix, context.proposedKey);
136
+ }
137
+ return assertSafeObjectKey(context.proposedKey);
138
+ }
139
+ function resolveBucket(config, requested) {
140
+ if (!requested) return config.bucket;
141
+ if (config.allowClientBucket) return requested;
142
+ if (config.buckets?.length) {
143
+ if (config.buckets.includes(requested)) return requested;
144
+ throw errors.invalidBucket(requested);
145
+ }
146
+ return config.bucket;
147
+ }
148
+ async function resolveRequestTarget(config, policy, input) {
149
+ const bucket = resolveBucket(config, input.bucket);
150
+ const key = await resolveObjectKey(policy, {
151
+ request: input.request,
152
+ proposedKey: input.key,
153
+ fileName: input.fileName,
154
+ contentType: input.contentType,
155
+ bucket
156
+ });
157
+ return { key, bucket };
158
+ }
159
+ function normalizeFeature(value) {
160
+ if (value === void 0) return void 0;
161
+ if (value === false) return { enabled: false };
162
+ if (value === true) return { enabled: true };
163
+ return { ...value, enabled: value.enabled ?? true };
164
+ }
165
+ function isFeatureEnabled(config, feature) {
166
+ return config[feature]?.enabled === true;
167
+ }
168
+ function applyMultipartDefault(config, multipartInput) {
169
+ let next = config;
170
+ if (multipartInput === void 0 && config.upload?.enabled) {
171
+ next = {
172
+ ...config,
173
+ multipart: { ...config.multipart, enabled: true }
174
+ };
175
+ }
176
+ if (next.multipart && next.upload) {
177
+ next = {
178
+ ...next,
179
+ multipart: {
180
+ ...next.multipart,
181
+ prefix: next.multipart.prefix ?? next.upload.prefix,
182
+ resolveKey: next.multipart.resolveKey ?? next.upload.resolveKey
183
+ }
184
+ };
185
+ }
186
+ return next;
187
+ }
188
+ function normalizeFeatures(config) {
189
+ return {
190
+ upload: normalizeFeature(config.upload),
191
+ download: normalizeFeature(config.download),
192
+ delete: normalizeFeature(config.delete),
193
+ multipart: normalizeFeature(config.multipart)
194
+ };
195
+ }
196
+
197
+ // src/api/assert-feature-enabled.ts
198
+ function assertFeatureEnabled(config, feature) {
199
+ if (!isFeatureEnabled(config, feature)) {
200
+ throw errors.featureDisabled(feature);
201
+ }
202
+ }
203
+ var s3ContextMiddleware = createMiddleware(async (ctx) => {
204
+ const injected = ctx.context;
205
+ if (!injected?.config) {
206
+ throw new Error(
207
+ "createS3Endpoint requires dimahS3 router context. Call endpoints through dimahS3().api or the HTTP handler."
163
208
  );
164
- return isPublic ? "public-read" : "private";
165
- } catch {
166
- return void 0;
167
- } finally {
168
- clearTimeout(timeout);
169
209
  }
210
+ const request = ctx.request ?? injected.request ?? requestFromHeaders(ctx.headers);
211
+ await runHook(injected.config.guard, { request });
212
+ return {
213
+ config: injected.config,
214
+ request
215
+ };
216
+ });
217
+ var createS3Middleware = createMiddleware.create({
218
+ use: [s3ContextMiddleware]
219
+ });
220
+
221
+ // src/api/create-s3-endpoint.ts
222
+ var createEndpointWithContext = createEndpoint.create({
223
+ use: [s3ContextMiddleware]
224
+ });
225
+ function withS3Validation(options) {
226
+ return {
227
+ ...options,
228
+ onValidationError: options.onValidationError ?? (({ message }) => {
229
+ throw errors.validationError(message);
230
+ })
231
+ };
170
232
  }
233
+ var createS3Endpoint = ((pathOrOptions, optionsOrHandler, maybeHandler) => {
234
+ if (typeof pathOrOptions === "string") {
235
+ return createEndpointWithContext(
236
+ pathOrOptions,
237
+ withS3Validation(optionsOrHandler),
238
+ maybeHandler
239
+ );
240
+ }
241
+ return createEndpointWithContext(
242
+ withS3Validation(pathOrOptions),
243
+ optionsOrHandler
244
+ );
245
+ });
171
246
 
172
- // src/procedures/confirm.ts
173
- async function confirm(config, input, request) {
174
- const key = requireString(input.key, "key");
175
- const bucket = input.bucket?.trim() || config.defaultBucket;
247
+ // src/api/routes/confirm.ts
248
+ async function handleConfirm(config, input, request) {
249
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
250
+ request,
251
+ key: input.key,
252
+ bucket: input.bucket
253
+ });
176
254
  await runHook(config.upload?.confirmGuard, {
177
255
  request,
178
256
  key,
179
257
  bucket
180
258
  });
181
- const head = await config.s3.send(
259
+ const head = await config.client.send(
182
260
  new HeadObjectCommand({ Bucket: bucket, Key: key })
183
261
  );
184
- const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
262
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
185
263
  const fileName = parseFileName(head.ContentDisposition);
186
264
  const context = {
187
265
  request,
@@ -210,45 +288,68 @@ async function confirm(config, input, request) {
210
288
  lastModified: context.lastModified
211
289
  };
212
290
  }
213
- async function deleteObject(config, input, request) {
214
- const key = input.key?.trim();
215
- if (!key) {
216
- throw errors.keyRequired();
291
+ var confirm = createS3Endpoint(
292
+ S3_API_ROUTES.uploadConfirm,
293
+ { method: "POST", body: confirmBodySchema },
294
+ async (ctx) => {
295
+ assertFeatureEnabled(ctx.context.config, "upload");
296
+ return handleConfirm(ctx.context.config, ctx.body, ctx.context.request);
217
297
  }
218
- const bucket = input.bucket?.trim() || config.defaultBucket;
298
+ );
299
+ async function handleDelete(config, input, request) {
300
+ const { key, bucket } = await resolveRequestTarget(config, config.delete, {
301
+ request,
302
+ key: input.key,
303
+ bucket: input.bucket
304
+ });
219
305
  await runHook(config.delete?.guard, {
220
306
  request,
221
307
  key,
222
308
  bucket
223
309
  });
224
310
  try {
225
- await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
311
+ await config.client.send(
312
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
313
+ );
226
314
  } catch (err) {
227
315
  if (isAwsNotFound(err)) {
228
316
  throw errors.objectNotFound();
229
317
  }
230
318
  throw err;
231
319
  }
232
- await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
320
+ await config.client.send(
321
+ new DeleteObjectCommand({ Bucket: bucket, Key: key })
322
+ );
233
323
  await runLifecycleHook(config.delete?.onDeleted, { request, key, bucket });
234
324
  return { success: true, bucket, key };
235
325
  }
236
- async function download(config, input, request) {
237
- const key = input.key?.trim();
238
- if (!key) {
239
- throw errors.keyRequired();
326
+ var deleteObject = createS3Endpoint(
327
+ S3_API_ROUTES.delete,
328
+ { method: "DELETE", query: deleteQuerySchema },
329
+ async (ctx) => {
330
+ assertFeatureEnabled(ctx.context.config, "delete");
331
+ return handleDelete(ctx.context.config, ctx.query, ctx.context.request);
240
332
  }
241
- const bucket = input.bucket?.trim() || config.defaultBucket;
333
+ );
334
+ async function handleDownload(config, input, request) {
335
+ const { key, bucket } = await resolveRequestTarget(config, config.download, {
336
+ request,
337
+ key: input.key,
338
+ bucket: input.bucket,
339
+ fileName: input.fileName
340
+ });
242
341
  const expiresIn = normalizeExpiresIn(input.expiresIn);
243
- const fileName = input.fileName?.trim();
244
- await runHook(config.download?.presignGuard, {
342
+ const fileName = input.fileName;
343
+ await runHook(config.download?.guard, {
245
344
  request,
246
345
  key,
247
346
  bucket,
248
- fileName: fileName || void 0
347
+ fileName
249
348
  });
250
349
  try {
251
- await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
350
+ await config.client.send(
351
+ new HeadObjectCommand({ Bucket: bucket, Key: key })
352
+ );
252
353
  } catch (err) {
253
354
  if (isAwsNotFound(err)) {
254
355
  throw errors.objectNotFound();
@@ -256,7 +357,7 @@ async function download(config, input, request) {
256
357
  throw err;
257
358
  }
258
359
  const url = await getSignedUrl(
259
- config.s3,
360
+ config.client,
260
361
  new GetObjectCommand({
261
362
  Bucket: bucket,
262
363
  Key: key,
@@ -268,23 +369,34 @@ async function download(config, input, request) {
268
369
  request,
269
370
  key,
270
371
  bucket,
271
- fileName: fileName || void 0,
372
+ fileName,
272
373
  url,
273
374
  expiresIn
274
375
  });
275
376
  return { bucket, key, url, expiresIn };
276
377
  }
277
- async function multipartAbort(config, input, request) {
278
- const key = requireString(input.key, "key");
279
- const uploadId = requireString(input.uploadId, "uploadId");
280
- const bucket = input.bucket?.trim() || config.defaultBucket;
378
+ var download = createS3Endpoint(
379
+ S3_API_ROUTES.download,
380
+ { method: "GET", query: downloadQuerySchema },
381
+ async (ctx) => {
382
+ assertFeatureEnabled(ctx.context.config, "download");
383
+ return handleDownload(ctx.context.config, ctx.query, ctx.context.request);
384
+ }
385
+ );
386
+ async function handleAbort(config, input, request) {
387
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
388
+ request,
389
+ key: input.key,
390
+ bucket: input.bucket
391
+ });
392
+ const uploadId = input.uploadId;
281
393
  await runHook(config.multipart?.abortGuard, {
282
394
  request,
283
395
  key,
284
396
  bucket,
285
397
  uploadId
286
398
  });
287
- await config.s3.send(
399
+ await config.client.send(
288
400
  new AbortMultipartUploadCommand({
289
401
  Bucket: bucket,
290
402
  Key: key,
@@ -299,14 +411,22 @@ async function multipartAbort(config, input, request) {
299
411
  });
300
412
  return { bucket, key, uploadId, aborted: true };
301
413
  }
302
- async function multipartComplete(config, input, request) {
303
- const key = requireString(input.key, "key");
304
- const uploadId = requireString(input.uploadId, "uploadId");
305
- const parts = (Array.isArray(input.parts) ? input.parts : []).map(({ partNumber }) => Number(partNumber)).filter((n) => Number.isInteger(n) && n > 0).sort((a, b) => a - b);
306
- if (!parts.length) {
307
- throw errors.partsRequired();
414
+ var multipartAbort = createS3Endpoint(
415
+ S3_API_ROUTES.multipartAbort,
416
+ { method: "POST", body: multipartAbortBodySchema },
417
+ async (ctx) => {
418
+ assertFeatureEnabled(ctx.context.config, "multipart");
419
+ return handleAbort(ctx.context.config, ctx.body, ctx.context.request);
308
420
  }
309
- const bucket = input.bucket?.trim() || config.defaultBucket;
421
+ );
422
+ async function handleComplete(config, input, request) {
423
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
424
+ request,
425
+ key: input.key,
426
+ bucket: input.bucket
427
+ });
428
+ const uploadId = input.uploadId;
429
+ const parts = input.parts.map(({ partNumber }) => partNumber).sort((a, b) => a - b);
310
430
  const partRefs = parts.map((partNumber) => ({ partNumber }));
311
431
  await runHook(config.multipart?.completeGuard, {
312
432
  request,
@@ -315,7 +435,7 @@ async function multipartComplete(config, input, request) {
315
435
  uploadId,
316
436
  parts: partRefs
317
437
  });
318
- const listed = await config.s3.send(
438
+ const listed = await config.client.send(
319
439
  new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
320
440
  );
321
441
  const listedParts = listed.Parts ?? [];
@@ -323,7 +443,7 @@ async function multipartComplete(config, input, request) {
323
443
  const found = listedParts.find((p) => p.PartNumber === partNumber);
324
444
  return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
325
445
  });
326
- const completeResult = await config.s3.send(
446
+ const completeResult = await config.client.send(
327
447
  new CompleteMultipartUploadCommand({
328
448
  Bucket: bucket,
329
449
  Key: key,
@@ -331,12 +451,12 @@ async function multipartComplete(config, input, request) {
331
451
  MultipartUpload: { Parts: completeParts }
332
452
  })
333
453
  );
334
- let head = await config.s3.send(
454
+ let head = await config.client.send(
335
455
  new HeadObjectCommand({ Bucket: bucket, Key: key })
336
456
  );
337
457
  for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
338
458
  await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
339
- head = await config.s3.send(
459
+ head = await config.client.send(
340
460
  new HeadObjectCommand({ Bucket: bucket, Key: key })
341
461
  );
342
462
  }
@@ -346,7 +466,7 @@ async function multipartComplete(config, input, request) {
346
466
  const metadata = head.Metadata ?? {};
347
467
  const versionId = head.VersionId;
348
468
  const lastModified = head.LastModified?.toISOString();
349
- const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
469
+ const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.client, bucket, key) : void 0;
350
470
  const fileName = parseFileName(head.ContentDisposition);
351
471
  await runLifecycleHook(config.multipart?.onComplete, {
352
472
  request,
@@ -376,9 +496,22 @@ async function multipartComplete(config, input, request) {
376
496
  lastModified
377
497
  };
378
498
  }
379
- async function multipartInit(config, input, request) {
380
- const key = requireString(input.key, "key");
381
- const bucket = input.bucket?.trim() || config.defaultBucket;
499
+ var multipartComplete = createS3Endpoint(
500
+ S3_API_ROUTES.multipartComplete,
501
+ { method: "POST", body: multipartCompleteBodySchema },
502
+ async (ctx) => {
503
+ assertFeatureEnabled(ctx.context.config, "multipart");
504
+ return handleComplete(ctx.context.config, ctx.body, ctx.context.request);
505
+ }
506
+ );
507
+ async function handleMultipartInit(config, input, request) {
508
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
509
+ request,
510
+ key: input.key,
511
+ bucket: input.bucket,
512
+ fileName: input.fileName,
513
+ contentType: input.contentType
514
+ });
382
515
  const acl = input.acl === "public-read" ? "public-read" : "private";
383
516
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
384
517
  if (config.multipart?.requireFileSize && fileSize === void 0) {
@@ -390,7 +523,7 @@ async function multipartInit(config, input, request) {
390
523
  bucket,
391
524
  fileSize
392
525
  });
393
- const { UploadId } = await config.s3.send(
526
+ const { UploadId } = await config.client.send(
394
527
  new CreateMultipartUploadCommand({
395
528
  Bucket: bucket,
396
529
  Key: key,
@@ -413,24 +546,33 @@ async function multipartInit(config, input, request) {
413
546
  });
414
547
  return { bucket, key, uploadId: UploadId };
415
548
  }
416
- async function multipartListParts(config, input, request) {
417
- const key = input.key?.trim() ?? "";
418
- const uploadId = input.uploadId?.trim() ?? "";
419
- const bucketParam = input.bucket?.trim();
420
- if (!key) {
421
- throw errors.keyRequired();
422
- }
423
- if (!uploadId) {
424
- throw errors.uploadIdRequired();
549
+ var multipartInit = createS3Endpoint(
550
+ S3_API_ROUTES.multipartInit,
551
+ { method: "POST", body: multipartInitBodySchema },
552
+ async (ctx) => {
553
+ assertFeatureEnabled(ctx.context.config, "multipart");
554
+ ctx.setStatus(201);
555
+ return handleMultipartInit(
556
+ ctx.context.config,
557
+ ctx.body,
558
+ ctx.context.request
559
+ );
425
560
  }
426
- const bucket = bucketParam || config.defaultBucket;
561
+ );
562
+ async function handleListParts(config, input, request) {
563
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
564
+ request,
565
+ key: input.key,
566
+ bucket: input.bucket
567
+ });
568
+ const uploadId = input.uploadId;
427
569
  await runHook(config.multipart?.listGuard, {
428
570
  request,
429
571
  key,
430
572
  bucket,
431
573
  uploadId
432
574
  });
433
- const response = await config.s3.send(
575
+ const response = await config.client.send(
434
576
  new ListPartsCommand({
435
577
  Bucket: bucket,
436
578
  Key: key,
@@ -451,14 +593,22 @@ async function multipartListParts(config, input, request) {
451
593
  });
452
594
  return { parts };
453
595
  }
454
- async function multipartSignPart(config, input, request) {
455
- const key = requireString(input.key, "key");
456
- const uploadId = requireString(input.uploadId, "uploadId");
457
- const partNumber = Number(input.partNumber);
458
- if (!Number.isInteger(partNumber) || partNumber <= 0) {
459
- throw errors.partNumberInvalid();
596
+ var multipartListParts = createS3Endpoint(
597
+ S3_API_ROUTES.multipartListParts,
598
+ { method: "GET", query: multipartListPartsQuerySchema },
599
+ async (ctx) => {
600
+ assertFeatureEnabled(ctx.context.config, "multipart");
601
+ return handleListParts(ctx.context.config, ctx.query, ctx.context.request);
460
602
  }
461
- const bucket = input.bucket?.trim() || config.defaultBucket;
603
+ );
604
+ async function handleSignPart(config, input, request) {
605
+ const { key, bucket } = await resolveRequestTarget(config, config.multipart, {
606
+ request,
607
+ key: input.key,
608
+ bucket: input.bucket
609
+ });
610
+ const uploadId = input.uploadId;
611
+ const partNumber = input.partNumber;
462
612
  const expiresIn = normalizeExpiresIn(input.expiresIn);
463
613
  const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
464
614
  await runHook(config.multipart?.partGuard, {
@@ -470,7 +620,7 @@ async function multipartSignPart(config, input, request) {
470
620
  partSize: partSize ?? void 0
471
621
  });
472
622
  const presignedUrl = await getSignedUrl(
473
- config.s3,
623
+ config.client,
474
624
  new UploadPartCommand({
475
625
  Bucket: bucket,
476
626
  Key: key,
@@ -492,14 +642,27 @@ async function multipartSignPart(config, input, request) {
492
642
  ...partSize !== null ? { partSize } : {}
493
643
  };
494
644
  }
495
- async function upload(config, input, request) {
496
- const key = requireString(input.key, "key");
497
- const bucket = input.bucket?.trim() || config.defaultBucket;
645
+ var multipartPart = createS3Endpoint(
646
+ S3_API_ROUTES.multipartPart,
647
+ { method: "POST", body: multipartSignPartBodySchema },
648
+ async (ctx) => {
649
+ assertFeatureEnabled(ctx.context.config, "multipart");
650
+ return handleSignPart(ctx.context.config, ctx.body, ctx.context.request);
651
+ }
652
+ );
653
+ async function handleUpload(config, input, request) {
654
+ const { key, bucket } = await resolveRequestTarget(config, config.upload, {
655
+ request,
656
+ key: input.key,
657
+ bucket: input.bucket,
658
+ fileName: input.fileName,
659
+ contentType: input.contentType
660
+ });
498
661
  const expiresIn = normalizeExpiresIn(input.expiresIn);
499
662
  const acl = input.acl === "public-read" ? "public-read" : "private";
500
- const contentType = input.contentType?.trim() || "application/octet-stream";
663
+ const contentType = input.contentType ?? "application/octet-stream";
501
664
  const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
502
- await runHook(config.upload?.presignGuard, {
665
+ await runHook(config.upload?.guard, {
503
666
  request,
504
667
  key,
505
668
  bucket,
@@ -523,7 +686,7 @@ async function upload(config, input, request) {
523
686
  );
524
687
  }
525
688
  const url2 = await getSignedUrl(
526
- config.s3,
689
+ config.client,
527
690
  new PutObjectCommand({
528
691
  Bucket: bucket,
529
692
  Key: key,
@@ -569,13 +732,16 @@ async function upload(config, input, request) {
569
732
  }
570
733
  const rangeMin = fileSize ?? 1;
571
734
  const rangeMax = fileSize ?? void 0;
572
- const { url, fields: signedFields } = await createPresignedPost(config.s3, {
573
- Bucket: bucket,
574
- Key: key,
575
- Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
576
- Fields: fields,
577
- Expires: expiresIn
578
- });
735
+ const { url, fields: signedFields } = await createPresignedPost(
736
+ config.client,
737
+ {
738
+ Bucket: bucket,
739
+ Key: key,
740
+ Conditions: rangeMax !== void 0 ? [["content-length-range", rangeMin, rangeMax]] : [["content-length-range", rangeMin, Number.MAX_SAFE_INTEGER]],
741
+ Fields: fields,
742
+ Expires: expiresIn
743
+ }
744
+ );
579
745
  await runLifecycleHook(config.upload?.onPresigned, {
580
746
  request,
581
747
  key,
@@ -597,255 +763,30 @@ async function upload(config, input, request) {
597
763
  method: "POST"
598
764
  };
599
765
  }
600
-
601
- // src/procedures/registry.ts
602
- var PROCEDURE_REGISTRY = {
603
- upload: {
604
- path: S3_API_ROUTES.upload,
605
- method: "POST",
606
- isEnabled: (c) => c.upload?.enabled,
607
- run: (config, input, request) => upload(config, input, request)
608
- },
609
- uploadConfirm: {
610
- path: S3_API_ROUTES.uploadConfirm,
611
- method: "POST",
612
- isEnabled: (c) => c.upload?.enabled,
613
- run: (config, input, request) => confirm(config, input, request)
614
- },
615
- download: {
616
- path: S3_API_ROUTES.download,
617
- method: "GET",
618
- isEnabled: (c) => c.download?.enabled,
619
- run: (config, input, request) => download(config, input, request)
620
- },
621
- delete: {
622
- path: S3_API_ROUTES.delete,
623
- method: "DELETE",
624
- isEnabled: (c) => c.delete?.enabled,
625
- run: (config, input, request) => deleteObject(
626
- config,
627
- input,
628
- request
629
- )
630
- },
631
- multipartInit: {
632
- path: S3_API_ROUTES.multipartInit,
633
- method: "POST",
634
- status: 201,
635
- isEnabled: (c) => c.multipart?.enabled,
636
- run: (config, input, request) => multipartInit(
637
- config,
638
- input,
639
- request
640
- )
641
- },
642
- multipartPart: {
643
- path: S3_API_ROUTES.multipartPart,
644
- method: "POST",
645
- isEnabled: (c) => c.multipart?.enabled,
646
- run: (config, input, request) => multipartSignPart(
647
- config,
648
- input,
649
- request
650
- )
651
- },
652
- multipartComplete: {
653
- path: S3_API_ROUTES.multipartComplete,
654
- method: "POST",
655
- isEnabled: (c) => c.multipart?.enabled,
656
- run: (config, input, request) => multipartComplete(
657
- config,
658
- input,
659
- request
660
- )
661
- },
662
- multipartAbort: {
663
- path: S3_API_ROUTES.multipartAbort,
664
- method: "POST",
665
- isEnabled: (c) => c.multipart?.enabled,
666
- run: (config, input, request) => multipartAbort(
667
- config,
668
- input,
669
- request
670
- )
671
- },
672
- multipartListParts: {
673
- path: S3_API_ROUTES.multipartListParts,
674
- method: "GET",
675
- isEnabled: (c) => c.multipart?.enabled,
676
- run: (config, input, request) => multipartListParts(
677
- config,
678
- input,
679
- request
680
- )
766
+ var upload = createS3Endpoint(
767
+ S3_API_ROUTES.upload,
768
+ { method: "POST", body: uploadBodySchema },
769
+ async (ctx) => {
770
+ assertFeatureEnabled(ctx.context.config, "upload");
771
+ return handleUpload(ctx.context.config, ctx.body, ctx.context.request);
681
772
  }
682
- };
773
+ );
683
774
 
684
- // src/api.ts
685
- async function runProcedure(config, entry, headers, input) {
686
- const request = requestFromHeaders(headers);
687
- await runHook(config.guard, { request });
688
- if (!entry.isEnabled(config)) {
689
- throw errors.notFound();
690
- }
691
- return entry.run(config, input, request);
692
- }
693
- function createServerApi(config) {
694
- return {
695
- upload: (payload) => {
696
- const { headers, ...input } = payload;
697
- return runProcedure(config, PROCEDURE_REGISTRY.upload, headers, input);
698
- },
699
- confirm: (payload) => {
700
- const { headers, ...input } = payload;
701
- return runProcedure(
702
- config,
703
- PROCEDURE_REGISTRY.uploadConfirm,
704
- headers,
705
- input
706
- );
707
- },
708
- download: (key, options) => {
709
- const { headers, ...rest } = options ?? {};
710
- return runProcedure(config, PROCEDURE_REGISTRY.download, headers, {
711
- key,
712
- ...rest
713
- });
714
- },
715
- delete: (key, options) => {
716
- const { headers, ...rest } = options ?? {};
717
- return runProcedure(config, PROCEDURE_REGISTRY.delete, headers, {
718
- key,
719
- ...rest
720
- });
721
- },
722
- multipart: {
723
- init: (payload) => {
724
- const { headers, ...input } = payload;
725
- return runProcedure(
726
- config,
727
- PROCEDURE_REGISTRY.multipartInit,
728
- headers,
729
- input
730
- );
731
- },
732
- signPart: (payload) => {
733
- const { headers, ...input } = payload;
734
- return runProcedure(
735
- config,
736
- PROCEDURE_REGISTRY.multipartPart,
737
- headers,
738
- input
739
- );
740
- },
741
- listParts: (payload) => {
742
- const { headers, ...input } = payload;
743
- return runProcedure(
744
- config,
745
- PROCEDURE_REGISTRY.multipartListParts,
746
- headers,
747
- input
748
- );
749
- },
750
- complete: (payload) => {
751
- const { headers, ...input } = payload;
752
- return runProcedure(
753
- config,
754
- PROCEDURE_REGISTRY.multipartComplete,
755
- headers,
756
- input
757
- );
758
- },
759
- abort: (payload) => {
760
- const { headers, ...input } = payload;
761
- return runProcedure(
762
- config,
763
- PROCEDURE_REGISTRY.multipartAbort,
764
- headers,
765
- input
766
- );
767
- }
768
- }
769
- };
770
- }
771
- async function requireJson(request) {
772
- const body = await parseBody(request);
773
- if (!body) {
774
- throw errors.invalidJson();
775
- }
776
- return body;
777
- }
778
- function decodeInput(entry, ctx) {
779
- const { request, url } = ctx;
780
- const { searchParams } = url;
781
- switch (entry.path) {
782
- case PROCEDURE_REGISTRY.download.path: {
783
- const expiresRaw = searchParams.get("expiresIn");
784
- return Promise.resolve({
785
- key: searchParams.get("key")?.trim() ?? "",
786
- bucket: searchParams.get("bucket")?.trim() || void 0,
787
- fileName: searchParams.get("fileName")?.trim() || void 0,
788
- expiresIn: expiresRaw != null ? Number(expiresRaw) : void 0
789
- });
790
- }
791
- case PROCEDURE_REGISTRY.delete.path:
792
- return Promise.resolve({
793
- key: searchParams.get("key")?.trim() ?? "",
794
- bucket: searchParams.get("bucket")?.trim() || void 0
795
- });
796
- case PROCEDURE_REGISTRY.multipartListParts.path:
797
- return Promise.resolve({
798
- key: searchParams.get("key")?.trim() ?? "",
799
- uploadId: searchParams.get("uploadId")?.trim() ?? "",
800
- bucket: searchParams.get("bucket")?.trim() || void 0
801
- });
802
- default:
803
- return requireJson(request);
804
- }
805
- }
806
- var routes = Object.values(PROCEDURE_REGISTRY).map((entry) => ({
807
- method: entry.method,
808
- path: entry.path,
809
- enabled: entry.isEnabled,
810
- status: "status" in entry ? entry.status : void 0,
811
- run: async (ctx) => {
812
- const input = await decodeInput(entry, ctx);
813
- return entry.run(ctx.config, input, ctx.request);
814
- }
815
- }));
816
- function createHandler(config, basePath = S3_API_BASE_PATH, pluginEndpoints = []) {
817
- const base = normalizeS3ApiBasePath(basePath);
818
- const pluginRoutes = pluginEndpoints.map((e) => ({
819
- method: e.method,
820
- path: pluginEndpointPath(e.pluginId, e.path),
821
- status: e.status,
822
- enabled: () => true,
823
- run: ({ config: cfg, request, url }) => e.handler({
824
- request,
825
- url,
826
- config: cfg,
827
- errors,
828
- json: () => parseBody(request)
829
- })
830
- }));
831
- const allRoutes = [...routes, ...pluginRoutes];
832
- return wrapHandler(async (request) => {
833
- await runHook(config.guard, { request });
834
- const url = new URL(request.url);
835
- const subpath = url.pathname.slice(base.length).replace(/^\//, "");
836
- const route = allRoutes.find(
837
- (r) => r.method === request.method && r.path === subpath
838
- );
839
- if (!route || !route.enabled(config)) {
840
- throw errors.notFound();
841
- }
842
- const data = await route.run({ config, request, url, errors });
843
- return Response.json(
844
- data,
845
- route.status ? { status: route.status } : void 0
846
- );
847
- });
848
- }
775
+ // src/api/routes/index.ts
776
+ var coreEndpoints = {
777
+ upload,
778
+ confirm,
779
+ download,
780
+ delete: deleteObject,
781
+ multipartInit,
782
+ multipartPart,
783
+ multipartListParts,
784
+ multipartComplete,
785
+ multipartAbort
786
+ };
787
+ var CORE_ENDPOINT_NAMES = Object.keys(
788
+ coreEndpoints
789
+ );
849
790
 
850
791
  // src/plugin/chain-hooks.ts
851
792
  function chainHooks(...hooks) {
@@ -858,8 +799,8 @@ function chainHooks(...hooks) {
858
799
 
859
800
  // src/plugin/hook-registry.ts
860
801
  var FEATURE_HOOK_KEYS = {
861
- upload: ["presignGuard", "onPresigned", "confirmGuard", "onConfirmed"],
862
- download: ["presignGuard", "onPresigned"],
802
+ upload: ["guard", "onPresigned", "confirmGuard", "onConfirmed"],
803
+ download: ["guard", "onPresigned"],
863
804
  delete: ["guard", "onDeleted"],
864
805
  multipart: [
865
806
  "initGuard",
@@ -879,7 +820,9 @@ var RESERVED_PLUGIN_IDS = [
879
820
  "handler",
880
821
  "api",
881
822
  "context",
882
- "getPlugin"
823
+ "getPlugin",
824
+ "$ERROR_CODES",
825
+ "$Infer"
883
826
  ];
884
827
 
885
828
  // src/plugin/apply-plugins.ts
@@ -895,7 +838,7 @@ function mergeFeatureHooks(feature, keys, plugins, userFeature) {
895
838
  if (!userFeature && plugins.every((p) => !p.hooks?.[feature])) {
896
839
  return userFeature;
897
840
  }
898
- const merged = { ...userFeature };
841
+ const merged = { ...userFeature ?? {} };
899
842
  for (const key of keys) {
900
843
  const fromPlugins = plugins.map(
901
844
  (p) => p.hooks?.[feature]?.[key]
@@ -963,34 +906,57 @@ function applyPlugins(config) {
963
906
  }
964
907
  }
965
908
  const reservedRoutePaths = new Set(Object.values(S3_API_ROUTES));
909
+ const reservedNames = /* @__PURE__ */ new Set([...CORE_ENDPOINT_NAMES, "multipart"]);
966
910
  const seenEndpointKeys = /* @__PURE__ */ new Set();
967
- const endpoints = [];
911
+ const endpoints = {};
968
912
  for (const plugin of plugins) {
969
913
  for (const [name, endpoint] of Object.entries(plugin.endpoints ?? {})) {
970
- const mounted = pluginEndpointPath(plugin.id, endpoint.path);
971
- if (reservedRoutePaths.has(mounted)) {
914
+ if (reservedNames.has(name) || name in endpoints) {
915
+ throw new Error(
916
+ `Plugin "${plugin.id}" endpoint "${name}" collides with an existing endpoint.`
917
+ );
918
+ }
919
+ const path = endpoint.path;
920
+ if (typeof path !== "string" || !path.startsWith("/")) {
921
+ throw new Error(
922
+ `Plugin "${plugin.id}" endpoint "${name}" path must start with "/".`
923
+ );
924
+ }
925
+ if (reservedRoutePaths.has(path)) {
972
926
  throw new Error(
973
927
  `Plugin "${plugin.id}" endpoint "${name}" collides with a core route.`
974
928
  );
975
929
  }
976
- const routeKey = `${endpoint.method} ${mounted}`;
977
- if (seenEndpointKeys.has(routeKey)) {
978
- throw new Error(`Duplicate plugin endpoint ${routeKey}.`);
930
+ const methods = Array.isArray(endpoint.options.method) ? endpoint.options.method : [endpoint.options.method];
931
+ for (const method of methods) {
932
+ const routeKey = `${method} ${path}`;
933
+ if (seenEndpointKeys.has(routeKey)) {
934
+ throw new Error(`Duplicate plugin endpoint ${routeKey}.`);
935
+ }
936
+ seenEndpointKeys.add(routeKey);
979
937
  }
980
- seenEndpointKeys.add(routeKey);
981
- endpoints.push({ ...endpoint, pluginId: plugin.id });
938
+ endpoints[name] = endpoint;
982
939
  }
983
940
  }
984
941
  const { plugins: _omit, ...rest } = config;
985
942
  for (const plugin of plugins) {
986
943
  plugin.init?.({ config: rest, getPlugin });
987
944
  }
945
+ const features = normalizeFeatures(rest);
946
+ const {
947
+ upload: _upload,
948
+ download: _download,
949
+ delete: _delete,
950
+ multipart: _multipart,
951
+ ...base
952
+ } = rest;
988
953
  const guard = mergeHookField(
989
954
  plugins.map((p) => p.hooks?.guard),
990
955
  rest.guard
991
956
  );
992
957
  const merged = {
993
- ...rest,
958
+ ...base,
959
+ ...features,
994
960
  ...guard !== void 0 ? { guard } : {}
995
961
  };
996
962
  for (const feature of Object.keys(FEATURE_HOOK_KEYS)) {
@@ -998,50 +964,122 @@ function applyPlugins(config) {
998
964
  feature,
999
965
  FEATURE_HOOK_KEYS[feature],
1000
966
  plugins,
1001
- rest[feature]
967
+ merged[feature]
1002
968
  );
1003
969
  }
1004
970
  return {
1005
- config: merged,
971
+ config: applyMultipartDefault(merged, rest.multipart),
1006
972
  context,
1007
973
  getPlugin,
1008
974
  endpoints
1009
975
  };
1010
976
  }
1011
977
 
1012
- // src/plugin/create-endpoint.ts
1013
- function createEndpoint(path, options, handler) {
1014
- return {
1015
- path,
1016
- method: options.method,
1017
- status: options.status,
1018
- handler
1019
- };
1020
- }
1021
-
1022
978
  // src/plugin/define-plugin.ts
1023
979
  function definePlugin(plugin) {
1024
980
  return plugin;
1025
981
  }
1026
982
 
983
+ // src/api/bind-endpoints.ts
984
+ function bindEndpoints(endpoints, context) {
985
+ const api = {};
986
+ for (const [key, endpoint] of Object.entries(endpoints)) {
987
+ const bound = (async (input = {}) => {
988
+ const headers = input.headers;
989
+ const request = input.request ?? requestFromHeaders(headers);
990
+ return endpoint({
991
+ ...input,
992
+ headers: headers ?? request.headers,
993
+ request,
994
+ context: {
995
+ ...context,
996
+ ...typeof input.context === "object" && input.context !== null ? input.context : {}
997
+ }
998
+ });
999
+ });
1000
+ bound.path = endpoint.path;
1001
+ bound.options = endpoint.options;
1002
+ api[key] = bound;
1003
+ }
1004
+ return api;
1005
+ }
1006
+
1007
+ // src/router.ts
1008
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
1009
+ "EAI_AGAIN",
1010
+ "ECONNREFUSED",
1011
+ "ECONNRESET",
1012
+ "ETIMEDOUT",
1013
+ "ENOTFOUND"
1014
+ ]);
1015
+ function onS3RouterError(error) {
1016
+ if (isAPIError(error)) return;
1017
+ const code = error?.code;
1018
+ if (typeof code === "string" && NETWORK_CODES.has(code)) {
1019
+ const networkErr = errors.s3NetworkError(code);
1020
+ console.error("[S3 API]", networkErr.message, error);
1021
+ throw networkErr;
1022
+ }
1023
+ console.error("[S3 API]", error);
1024
+ throw errors.internalError();
1025
+ }
1026
+ function withUnmatchedRouteJson(handler) {
1027
+ return async (request) => {
1028
+ const response = await handler(request);
1029
+ if (response.status !== 404) return response;
1030
+ const contentType = response.headers.get("content-type") ?? "";
1031
+ if (contentType.includes("application/json")) return response;
1032
+ return toResponse(errors.notFound());
1033
+ };
1034
+ }
1035
+ function createS3Router(endpoints, env) {
1036
+ const basePath = normalizeS3ApiBasePath(
1037
+ env.config.basePath ?? S3_API_BASE_PATH
1038
+ );
1039
+ const router = createRouter(endpoints, {
1040
+ basePath,
1041
+ routerContext: { config: env.config },
1042
+ openapi: { disabled: true },
1043
+ onError: onS3RouterError
1044
+ });
1045
+ return {
1046
+ endpoints: bindEndpoints(router.endpoints, { config: env.config }),
1047
+ handler: withUnmatchedRouteJson(router.handler)
1048
+ };
1049
+ }
1050
+
1027
1051
  // src/dimah-s3.ts
1028
1052
  function dimahS3(config) {
1029
1053
  const {
1030
1054
  config: resolved,
1031
1055
  context,
1032
1056
  getPlugin,
1033
- endpoints
1057
+ endpoints: pluginEndpoints
1034
1058
  } = applyPlugins(config);
1035
- const basePath = resolved.basePath ?? S3_API_BASE_PATH;
1059
+ const endpoints = { ...coreEndpoints, ...pluginEndpoints };
1060
+ const router = createS3Router(endpoints, { config: resolved });
1061
+ const bound = router.endpoints;
1062
+ const api = {
1063
+ ...bound,
1064
+ multipart: {
1065
+ init: bound.multipartInit,
1066
+ signPart: bound.multipartPart,
1067
+ listParts: bound.multipartListParts,
1068
+ complete: bound.multipartComplete,
1069
+ abort: bound.multipartAbort
1070
+ }
1071
+ };
1036
1072
  return {
1037
- handler: createHandler(resolved, basePath, endpoints),
1038
- api: createServerApi(resolved),
1073
+ handler: router.handler,
1074
+ api,
1039
1075
  context,
1040
1076
  getPlugin,
1077
+ $ERROR_CODES: S3_ERROR_CODES,
1078
+ $Infer: {},
1041
1079
  ...context
1042
1080
  };
1043
1081
  }
1044
1082
 
1045
- export { FEATURE_HOOK_KEYS, RESERVED_PLUGIN_IDS, applyPlugins, chainHooks, createEndpoint, definePlugin, dimahS3, errors, resolveObjectAcl };
1083
+ export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors, resolveObjectAcl };
1046
1084
  //# sourceMappingURL=index.js.map
1047
1085
  //# sourceMappingURL=index.js.map