@dimah-s3/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/adapters/next.d.ts +18 -0
- package/dist/adapters/next.d.ts.map +1 -0
- package/dist/adapters/next.js +12 -0
- package/dist/adapters/next.js.map +1 -0
- package/dist/adapters/node.d.ts +16 -0
- package/dist/adapters/node.d.ts.map +1 -0
- package/dist/adapters/node.js +116 -0
- package/dist/adapters/node.js.map +1 -0
- package/dist/api.d.ts +5 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/dimah-s3.d.ts +36 -0
- package/dist/dimah-s3.d.ts.map +1 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/handler.d.ts +5 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.d.ts.map +1 -0
- package/dist/helpers/is-aws-not-found.d.ts +3 -0
- package/dist/helpers/is-aws-not-found.d.ts.map +1 -0
- package/dist/helpers/is-aws-not-found.test.d.ts +2 -0
- package/dist/helpers/is-aws-not-found.test.d.ts.map +1 -0
- package/dist/helpers/resolve-object-acl.d.ts +10 -0
- package/dist/helpers/resolve-object-acl.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1047 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-helpers.d.ts +23 -0
- package/dist/internal-helpers.d.ts.map +1 -0
- package/dist/internal-helpers.test.d.ts +2 -0
- package/dist/internal-helpers.test.d.ts.map +1 -0
- package/dist/plugin/apply-plugins.d.ts +11 -0
- package/dist/plugin/apply-plugins.d.ts.map +1 -0
- package/dist/plugin/apply-plugins.test.d.ts +2 -0
- package/dist/plugin/apply-plugins.test.d.ts.map +1 -0
- package/dist/plugin/chain-hooks.d.ts +18 -0
- package/dist/plugin/chain-hooks.d.ts.map +1 -0
- package/dist/plugin/create-endpoint.d.ts +20 -0
- package/dist/plugin/create-endpoint.d.ts.map +1 -0
- package/dist/plugin/define-plugin.d.ts +17 -0
- package/dist/plugin/define-plugin.d.ts.map +1 -0
- package/dist/plugin/hook-registry.d.ts +12 -0
- package/dist/plugin/hook-registry.d.ts.map +1 -0
- package/dist/plugin/index.d.ts +8 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/types.d.ts +101 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/procedures/confirm.d.ts +8 -0
- package/dist/procedures/confirm.d.ts.map +1 -0
- package/dist/procedures/delete.d.ts +8 -0
- package/dist/procedures/delete.d.ts.map +1 -0
- package/dist/procedures/download.d.ts +10 -0
- package/dist/procedures/download.d.ts.map +1 -0
- package/dist/procedures/multipart/abort.d.ts +13 -0
- package/dist/procedures/multipart/abort.d.ts.map +1 -0
- package/dist/procedures/multipart/complete.d.ts +12 -0
- package/dist/procedures/multipart/complete.d.ts.map +1 -0
- package/dist/procedures/multipart/init.d.ts +13 -0
- package/dist/procedures/multipart/init.d.ts.map +1 -0
- package/dist/procedures/multipart/list-parts.d.ts +9 -0
- package/dist/procedures/multipart/list-parts.d.ts.map +1 -0
- package/dist/procedures/multipart/part.d.ts +12 -0
- package/dist/procedures/multipart/part.d.ts.map +1 -0
- package/dist/procedures/registry.d.ts +78 -0
- package/dist/procedures/registry.d.ts.map +1 -0
- package/dist/procedures/upload.d.ts +14 -0
- package/dist/procedures/upload.d.ts.map +1 -0
- package/dist/types/config.d.ts +87 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/hook-contexts.d.ts +147 -0
- package/dist/types/hook-contexts.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +72 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1047 @@
|
|
|
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';
|
|
4
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
5
|
+
import { createPresignedPost } from '@aws-sdk/s3-presigned-post';
|
|
6
|
+
|
|
7
|
+
// src/dimah-s3.ts
|
|
8
|
+
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 }
|
|
28
|
+
}),
|
|
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
|
|
32
|
+
}),
|
|
33
|
+
uploadIdRequired: () => new DimahS3Error("uploadId is required", 400, {
|
|
34
|
+
code: S3_ERROR_CODES.UPLOAD_ID_REQUIRED
|
|
35
|
+
}),
|
|
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 }
|
|
46
|
+
),
|
|
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
|
+
)
|
|
52
|
+
};
|
|
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
|
+
}
|
|
117
|
+
async function runHook(hook, context) {
|
|
118
|
+
if (!hook) return;
|
|
119
|
+
try {
|
|
120
|
+
await hook(context);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof DimahS3Error) throw err;
|
|
123
|
+
if (err instanceof Error && err.message.trim()) {
|
|
124
|
+
throw toDimahS3Error(err, errors.forbidden().message, 403);
|
|
125
|
+
}
|
|
126
|
+
throw errors.forbidden();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async function runLifecycleHook(hook, context) {
|
|
130
|
+
if (!hook) return;
|
|
131
|
+
try {
|
|
132
|
+
await hook(context);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err instanceof DimahS3Error) throw err;
|
|
135
|
+
console.error("[S3 API] lifecycle hook failed", err);
|
|
136
|
+
throw errors.internalError();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function normalizeExpiresIn(value) {
|
|
140
|
+
const n = Number(value);
|
|
141
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 600;
|
|
142
|
+
}
|
|
143
|
+
function requestFromHeaders(headers) {
|
|
144
|
+
return new Request("http://dimah-s3.local", { headers });
|
|
145
|
+
}
|
|
146
|
+
|
|
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;
|
|
151
|
+
}
|
|
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")
|
|
163
|
+
);
|
|
164
|
+
return isPublic ? "public-read" : "private";
|
|
165
|
+
} catch {
|
|
166
|
+
return void 0;
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
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;
|
|
176
|
+
await runHook(config.upload?.confirmGuard, {
|
|
177
|
+
request,
|
|
178
|
+
key,
|
|
179
|
+
bucket
|
|
180
|
+
});
|
|
181
|
+
const head = await config.s3.send(
|
|
182
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
183
|
+
);
|
|
184
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
|
|
185
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
186
|
+
const context = {
|
|
187
|
+
request,
|
|
188
|
+
key,
|
|
189
|
+
bucket,
|
|
190
|
+
contentType: head.ContentType,
|
|
191
|
+
contentLength: head.ContentLength ?? 0,
|
|
192
|
+
eTag: head.ETag?.replace(/"/g, ""),
|
|
193
|
+
metadata: head.Metadata,
|
|
194
|
+
acl,
|
|
195
|
+
fileName,
|
|
196
|
+
versionId: head.VersionId,
|
|
197
|
+
lastModified: head.LastModified?.toISOString()
|
|
198
|
+
};
|
|
199
|
+
await runLifecycleHook(config.upload?.onConfirmed, context);
|
|
200
|
+
return {
|
|
201
|
+
key,
|
|
202
|
+
bucket,
|
|
203
|
+
contentType: context.contentType,
|
|
204
|
+
contentLength: context.contentLength,
|
|
205
|
+
eTag: context.eTag,
|
|
206
|
+
metadata: context.metadata ?? {},
|
|
207
|
+
acl,
|
|
208
|
+
fileName,
|
|
209
|
+
versionId: context.versionId,
|
|
210
|
+
lastModified: context.lastModified
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function deleteObject(config, input, request) {
|
|
214
|
+
const key = input.key?.trim();
|
|
215
|
+
if (!key) {
|
|
216
|
+
throw errors.keyRequired();
|
|
217
|
+
}
|
|
218
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
219
|
+
await runHook(config.delete?.guard, {
|
|
220
|
+
request,
|
|
221
|
+
key,
|
|
222
|
+
bucket
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (isAwsNotFound(err)) {
|
|
228
|
+
throw errors.objectNotFound();
|
|
229
|
+
}
|
|
230
|
+
throw err;
|
|
231
|
+
}
|
|
232
|
+
await config.s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: key }));
|
|
233
|
+
await runLifecycleHook(config.delete?.onDeleted, { request, key, bucket });
|
|
234
|
+
return { success: true, bucket, key };
|
|
235
|
+
}
|
|
236
|
+
async function download(config, input, request) {
|
|
237
|
+
const key = input.key?.trim();
|
|
238
|
+
if (!key) {
|
|
239
|
+
throw errors.keyRequired();
|
|
240
|
+
}
|
|
241
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
242
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
243
|
+
const fileName = input.fileName?.trim();
|
|
244
|
+
await runHook(config.download?.presignGuard, {
|
|
245
|
+
request,
|
|
246
|
+
key,
|
|
247
|
+
bucket,
|
|
248
|
+
fileName: fileName || void 0
|
|
249
|
+
});
|
|
250
|
+
try {
|
|
251
|
+
await config.s3.send(new HeadObjectCommand({ Bucket: bucket, Key: key }));
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (isAwsNotFound(err)) {
|
|
254
|
+
throw errors.objectNotFound();
|
|
255
|
+
}
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
const url = await getSignedUrl(
|
|
259
|
+
config.s3,
|
|
260
|
+
new GetObjectCommand({
|
|
261
|
+
Bucket: bucket,
|
|
262
|
+
Key: key,
|
|
263
|
+
ResponseContentDisposition: fileName ? buildContentDisposition(fileName) : "attachment"
|
|
264
|
+
}),
|
|
265
|
+
{ expiresIn }
|
|
266
|
+
);
|
|
267
|
+
await runLifecycleHook(config.download?.onPresigned, {
|
|
268
|
+
request,
|
|
269
|
+
key,
|
|
270
|
+
bucket,
|
|
271
|
+
fileName: fileName || void 0,
|
|
272
|
+
url,
|
|
273
|
+
expiresIn
|
|
274
|
+
});
|
|
275
|
+
return { bucket, key, url, expiresIn };
|
|
276
|
+
}
|
|
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;
|
|
281
|
+
await runHook(config.multipart?.abortGuard, {
|
|
282
|
+
request,
|
|
283
|
+
key,
|
|
284
|
+
bucket,
|
|
285
|
+
uploadId
|
|
286
|
+
});
|
|
287
|
+
await config.s3.send(
|
|
288
|
+
new AbortMultipartUploadCommand({
|
|
289
|
+
Bucket: bucket,
|
|
290
|
+
Key: key,
|
|
291
|
+
UploadId: uploadId
|
|
292
|
+
})
|
|
293
|
+
);
|
|
294
|
+
await runLifecycleHook(config.multipart?.onAbort, {
|
|
295
|
+
request,
|
|
296
|
+
key,
|
|
297
|
+
bucket,
|
|
298
|
+
uploadId
|
|
299
|
+
});
|
|
300
|
+
return { bucket, key, uploadId, aborted: true };
|
|
301
|
+
}
|
|
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();
|
|
308
|
+
}
|
|
309
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
310
|
+
const partRefs = parts.map((partNumber) => ({ partNumber }));
|
|
311
|
+
await runHook(config.multipart?.completeGuard, {
|
|
312
|
+
request,
|
|
313
|
+
key,
|
|
314
|
+
bucket,
|
|
315
|
+
uploadId,
|
|
316
|
+
parts: partRefs
|
|
317
|
+
});
|
|
318
|
+
const listed = await config.s3.send(
|
|
319
|
+
new ListPartsCommand({ Bucket: bucket, Key: key, UploadId: uploadId })
|
|
320
|
+
);
|
|
321
|
+
const listedParts = listed.Parts ?? [];
|
|
322
|
+
const completeParts = parts.map((partNumber) => {
|
|
323
|
+
const found = listedParts.find((p) => p.PartNumber === partNumber);
|
|
324
|
+
return { PartNumber: partNumber, ETag: found?.ETag ?? "" };
|
|
325
|
+
});
|
|
326
|
+
const completeResult = await config.s3.send(
|
|
327
|
+
new CompleteMultipartUploadCommand({
|
|
328
|
+
Bucket: bucket,
|
|
329
|
+
Key: key,
|
|
330
|
+
UploadId: uploadId,
|
|
331
|
+
MultipartUpload: { Parts: completeParts }
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
let head = await config.s3.send(
|
|
335
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
336
|
+
);
|
|
337
|
+
for (let attempt = 0; attempt < 4 && !head.ContentLength; attempt++) {
|
|
338
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
339
|
+
head = await config.s3.send(
|
|
340
|
+
new HeadObjectCommand({ Bucket: bucket, Key: key })
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
const contentLength = head.ContentLength ?? 0;
|
|
344
|
+
const contentType = head.ContentType;
|
|
345
|
+
const eTag = (head.ETag ?? completeResult.ETag ?? "").replace(/"/g, "");
|
|
346
|
+
const metadata = head.Metadata ?? {};
|
|
347
|
+
const versionId = head.VersionId;
|
|
348
|
+
const lastModified = head.LastModified?.toISOString();
|
|
349
|
+
const acl = config.resolveObjectAcl ? await resolveObjectAcl(config.s3, bucket, key) : void 0;
|
|
350
|
+
const fileName = parseFileName(head.ContentDisposition);
|
|
351
|
+
await runLifecycleHook(config.multipart?.onComplete, {
|
|
352
|
+
request,
|
|
353
|
+
key,
|
|
354
|
+
bucket,
|
|
355
|
+
uploadId,
|
|
356
|
+
contentLength,
|
|
357
|
+
contentType,
|
|
358
|
+
eTag,
|
|
359
|
+
metadata,
|
|
360
|
+
acl,
|
|
361
|
+
fileName,
|
|
362
|
+
versionId,
|
|
363
|
+
lastModified
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
bucket,
|
|
367
|
+
key,
|
|
368
|
+
uploadId,
|
|
369
|
+
contentLength,
|
|
370
|
+
contentType,
|
|
371
|
+
eTag,
|
|
372
|
+
metadata,
|
|
373
|
+
acl,
|
|
374
|
+
fileName,
|
|
375
|
+
versionId,
|
|
376
|
+
lastModified
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
async function multipartInit(config, input, request) {
|
|
380
|
+
const key = requireString(input.key, "key");
|
|
381
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
382
|
+
const acl = input.acl === "public-read" ? "public-read" : "private";
|
|
383
|
+
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : void 0;
|
|
384
|
+
if (config.multipart?.requireFileSize && fileSize === void 0) {
|
|
385
|
+
throw errors.fileSizeRequiredMultipart();
|
|
386
|
+
}
|
|
387
|
+
await runHook(config.multipart?.initGuard, {
|
|
388
|
+
request,
|
|
389
|
+
key,
|
|
390
|
+
bucket,
|
|
391
|
+
fileSize
|
|
392
|
+
});
|
|
393
|
+
const { UploadId } = await config.s3.send(
|
|
394
|
+
new CreateMultipartUploadCommand({
|
|
395
|
+
Bucket: bucket,
|
|
396
|
+
Key: key,
|
|
397
|
+
ContentType: input.contentType,
|
|
398
|
+
ContentDisposition: input.fileName ? buildContentDisposition(input.fileName) : void 0,
|
|
399
|
+
Metadata: input.metadata,
|
|
400
|
+
ACL: acl
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
await runLifecycleHook(config.multipart?.onInit, {
|
|
404
|
+
request,
|
|
405
|
+
key,
|
|
406
|
+
bucket,
|
|
407
|
+
uploadId: UploadId,
|
|
408
|
+
contentType: input.contentType,
|
|
409
|
+
fileSize,
|
|
410
|
+
metadata: input.metadata,
|
|
411
|
+
acl,
|
|
412
|
+
fileName: input.fileName
|
|
413
|
+
});
|
|
414
|
+
return { bucket, key, uploadId: UploadId };
|
|
415
|
+
}
|
|
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();
|
|
425
|
+
}
|
|
426
|
+
const bucket = bucketParam || config.defaultBucket;
|
|
427
|
+
await runHook(config.multipart?.listGuard, {
|
|
428
|
+
request,
|
|
429
|
+
key,
|
|
430
|
+
bucket,
|
|
431
|
+
uploadId
|
|
432
|
+
});
|
|
433
|
+
const response = await config.s3.send(
|
|
434
|
+
new ListPartsCommand({
|
|
435
|
+
Bucket: bucket,
|
|
436
|
+
Key: key,
|
|
437
|
+
UploadId: uploadId
|
|
438
|
+
})
|
|
439
|
+
);
|
|
440
|
+
const parts = (response.Parts ?? []).map((p) => ({
|
|
441
|
+
partNumber: p.PartNumber ?? 0,
|
|
442
|
+
size: p.Size ?? 0,
|
|
443
|
+
eTag: (p.ETag ?? "").replace(/"/g, "")
|
|
444
|
+
}));
|
|
445
|
+
await runLifecycleHook(config.multipart?.onList, {
|
|
446
|
+
request,
|
|
447
|
+
key,
|
|
448
|
+
bucket,
|
|
449
|
+
uploadId,
|
|
450
|
+
parts
|
|
451
|
+
});
|
|
452
|
+
return { parts };
|
|
453
|
+
}
|
|
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();
|
|
460
|
+
}
|
|
461
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
462
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
463
|
+
const partSize = typeof input.partSize === "number" && input.partSize > 0 ? Math.floor(input.partSize) : null;
|
|
464
|
+
await runHook(config.multipart?.partGuard, {
|
|
465
|
+
request,
|
|
466
|
+
key,
|
|
467
|
+
bucket,
|
|
468
|
+
uploadId,
|
|
469
|
+
partNumber,
|
|
470
|
+
partSize: partSize ?? void 0
|
|
471
|
+
});
|
|
472
|
+
const presignedUrl = await getSignedUrl(
|
|
473
|
+
config.s3,
|
|
474
|
+
new UploadPartCommand({
|
|
475
|
+
Bucket: bucket,
|
|
476
|
+
Key: key,
|
|
477
|
+
UploadId: uploadId,
|
|
478
|
+
PartNumber: partNumber,
|
|
479
|
+
...partSize !== null ? { ContentLength: partSize } : {}
|
|
480
|
+
}),
|
|
481
|
+
{
|
|
482
|
+
expiresIn,
|
|
483
|
+
...partSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
return {
|
|
487
|
+
presignedUrl,
|
|
488
|
+
partNumber,
|
|
489
|
+
uploadId,
|
|
490
|
+
bucket,
|
|
491
|
+
expiresIn,
|
|
492
|
+
...partSize !== null ? { partSize } : {}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function upload(config, input, request) {
|
|
496
|
+
const key = requireString(input.key, "key");
|
|
497
|
+
const bucket = input.bucket?.trim() || config.defaultBucket;
|
|
498
|
+
const expiresIn = normalizeExpiresIn(input.expiresIn);
|
|
499
|
+
const acl = input.acl === "public-read" ? "public-read" : "private";
|
|
500
|
+
const contentType = input.contentType?.trim() || "application/octet-stream";
|
|
501
|
+
const fileSize = typeof input.fileSize === "number" && input.fileSize > 0 ? Math.floor(input.fileSize) : null;
|
|
502
|
+
await runHook(config.upload?.presignGuard, {
|
|
503
|
+
request,
|
|
504
|
+
key,
|
|
505
|
+
bucket,
|
|
506
|
+
contentType: input.contentType,
|
|
507
|
+
fileSize: fileSize ?? void 0,
|
|
508
|
+
metadata: input.metadata,
|
|
509
|
+
acl,
|
|
510
|
+
fileName: input.fileName
|
|
511
|
+
});
|
|
512
|
+
const method = config.upload?.method ?? "POST";
|
|
513
|
+
if (method === "PUT" && config.upload?.requireFileSize && fileSize === null) {
|
|
514
|
+
throw errors.fileSizeRequiredUpload();
|
|
515
|
+
}
|
|
516
|
+
if (method === "PUT") {
|
|
517
|
+
const putHeaders = {
|
|
518
|
+
"Content-Type": contentType
|
|
519
|
+
};
|
|
520
|
+
if (input.fileName) {
|
|
521
|
+
putHeaders["Content-Disposition"] = buildContentDisposition(
|
|
522
|
+
input.fileName
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const url2 = await getSignedUrl(
|
|
526
|
+
config.s3,
|
|
527
|
+
new PutObjectCommand({
|
|
528
|
+
Bucket: bucket,
|
|
529
|
+
Key: key,
|
|
530
|
+
ContentType: contentType,
|
|
531
|
+
ACL: acl,
|
|
532
|
+
...input.fileName ? { ContentDisposition: buildContentDisposition(input.fileName) } : {},
|
|
533
|
+
...fileSize !== null ? { ContentLength: fileSize } : {}
|
|
534
|
+
}),
|
|
535
|
+
{
|
|
536
|
+
expiresIn,
|
|
537
|
+
...fileSize !== null ? { signableHeaders: /* @__PURE__ */ new Set(["content-length"]) } : {}
|
|
538
|
+
}
|
|
539
|
+
);
|
|
540
|
+
await runLifecycleHook(config.upload?.onPresigned, {
|
|
541
|
+
request,
|
|
542
|
+
key,
|
|
543
|
+
bucket,
|
|
544
|
+
contentType: input.contentType,
|
|
545
|
+
fileSize: fileSize ?? void 0,
|
|
546
|
+
metadata: input.metadata,
|
|
547
|
+
acl,
|
|
548
|
+
fileName: input.fileName,
|
|
549
|
+
url: url2,
|
|
550
|
+
expiresIn
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
bucket,
|
|
554
|
+
key,
|
|
555
|
+
url: url2,
|
|
556
|
+
headers: putHeaders,
|
|
557
|
+
expiresIn,
|
|
558
|
+
method: "PUT"
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const fields = { acl, "Content-Type": contentType };
|
|
562
|
+
if (input.fileName) {
|
|
563
|
+
fields["Content-Disposition"] = buildContentDisposition(input.fileName);
|
|
564
|
+
}
|
|
565
|
+
if (input.metadata) {
|
|
566
|
+
for (const [k, v] of Object.entries(input.metadata)) {
|
|
567
|
+
fields[`x-amz-meta-${k}`] = v;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const rangeMin = fileSize ?? 1;
|
|
571
|
+
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
|
+
});
|
|
579
|
+
await runLifecycleHook(config.upload?.onPresigned, {
|
|
580
|
+
request,
|
|
581
|
+
key,
|
|
582
|
+
bucket,
|
|
583
|
+
contentType: input.contentType,
|
|
584
|
+
fileSize: fileSize ?? void 0,
|
|
585
|
+
metadata: input.metadata,
|
|
586
|
+
acl,
|
|
587
|
+
fileName: input.fileName,
|
|
588
|
+
url,
|
|
589
|
+
expiresIn
|
|
590
|
+
});
|
|
591
|
+
return {
|
|
592
|
+
bucket,
|
|
593
|
+
key,
|
|
594
|
+
url,
|
|
595
|
+
fields: signedFields,
|
|
596
|
+
expiresIn,
|
|
597
|
+
method: "POST"
|
|
598
|
+
};
|
|
599
|
+
}
|
|
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
|
+
)
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
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
|
+
}
|
|
849
|
+
|
|
850
|
+
// src/plugin/chain-hooks.ts
|
|
851
|
+
function chainHooks(...hooks) {
|
|
852
|
+
return async (context) => {
|
|
853
|
+
for (const hook of hooks) {
|
|
854
|
+
if (hook) await hook(context);
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/plugin/hook-registry.ts
|
|
860
|
+
var FEATURE_HOOK_KEYS = {
|
|
861
|
+
upload: ["presignGuard", "onPresigned", "confirmGuard", "onConfirmed"],
|
|
862
|
+
download: ["presignGuard", "onPresigned"],
|
|
863
|
+
delete: ["guard", "onDeleted"],
|
|
864
|
+
multipart: [
|
|
865
|
+
"initGuard",
|
|
866
|
+
"partGuard",
|
|
867
|
+
"completeGuard",
|
|
868
|
+
"abortGuard",
|
|
869
|
+
"listGuard",
|
|
870
|
+
"onInit",
|
|
871
|
+
"onComplete",
|
|
872
|
+
"onAbort",
|
|
873
|
+
"onList"
|
|
874
|
+
]
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// src/plugin/types.ts
|
|
878
|
+
var RESERVED_PLUGIN_IDS = [
|
|
879
|
+
"handler",
|
|
880
|
+
"api",
|
|
881
|
+
"context",
|
|
882
|
+
"getPlugin"
|
|
883
|
+
];
|
|
884
|
+
|
|
885
|
+
// src/plugin/apply-plugins.ts
|
|
886
|
+
function mergeHookField(pluginHooks, userHook) {
|
|
887
|
+
const present = [...pluginHooks, userHook].filter(
|
|
888
|
+
(h) => h != null
|
|
889
|
+
);
|
|
890
|
+
if (present.length === 0) return void 0;
|
|
891
|
+
if (present.length === 1) return present[0];
|
|
892
|
+
return chainHooks(...present);
|
|
893
|
+
}
|
|
894
|
+
function mergeFeatureHooks(feature, keys, plugins, userFeature) {
|
|
895
|
+
if (!userFeature && plugins.every((p) => !p.hooks?.[feature])) {
|
|
896
|
+
return userFeature;
|
|
897
|
+
}
|
|
898
|
+
const merged = { ...userFeature };
|
|
899
|
+
for (const key of keys) {
|
|
900
|
+
const fromPlugins = plugins.map(
|
|
901
|
+
(p) => p.hooks?.[feature]?.[key]
|
|
902
|
+
);
|
|
903
|
+
const userHook = userFeature?.[key];
|
|
904
|
+
const next = mergeHookField(fromPlugins, userHook);
|
|
905
|
+
if (next !== void 0) {
|
|
906
|
+
merged[key] = next;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return merged;
|
|
910
|
+
}
|
|
911
|
+
function applyPlugins(config) {
|
|
912
|
+
const plugins = config.plugins ?? [];
|
|
913
|
+
const seen = /* @__PURE__ */ new Set();
|
|
914
|
+
const byId = /* @__PURE__ */ new Map();
|
|
915
|
+
const context = {};
|
|
916
|
+
const getPlugin = (id) => byId.get(id);
|
|
917
|
+
for (const plugin of plugins) {
|
|
918
|
+
if (RESERVED_PLUGIN_IDS.includes(plugin.id)) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`Plugin id "${plugin.id}" is reserved on the dimahS3 instance. Choose a different id.`
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (seen.has(plugin.id)) {
|
|
924
|
+
throw new Error(
|
|
925
|
+
`Duplicate dimah-s3 plugin id "${plugin.id}". Each plugin id must be unique.`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
seen.add(plugin.id);
|
|
929
|
+
byId.set(plugin.id, plugin);
|
|
930
|
+
}
|
|
931
|
+
for (const plugin of plugins) {
|
|
932
|
+
for (const dep of plugin.dependsOn ?? []) {
|
|
933
|
+
if (!seen.has(dep)) {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`Plugin "${plugin.id}" depends on "${dep}", which is not registered.`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
941
|
+
const visited = /* @__PURE__ */ new Set();
|
|
942
|
+
function visit(id, stack) {
|
|
943
|
+
if (visited.has(id)) return;
|
|
944
|
+
if (visiting.has(id)) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
`Circular plugin dependency detected: ${[...stack, id].join(" -> ")}.`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
visiting.add(id);
|
|
950
|
+
const plugin = byId.get(id);
|
|
951
|
+
for (const dep of plugin?.dependsOn ?? []) {
|
|
952
|
+
visit(dep, [...stack, id]);
|
|
953
|
+
}
|
|
954
|
+
visiting.delete(id);
|
|
955
|
+
visited.add(id);
|
|
956
|
+
}
|
|
957
|
+
for (const plugin of plugins) {
|
|
958
|
+
visit(plugin.id, []);
|
|
959
|
+
}
|
|
960
|
+
for (const plugin of plugins) {
|
|
961
|
+
if (plugin.context !== void 0) {
|
|
962
|
+
context[plugin.id] = plugin.context;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
const reservedRoutePaths = new Set(Object.values(S3_API_ROUTES));
|
|
966
|
+
const seenEndpointKeys = /* @__PURE__ */ new Set();
|
|
967
|
+
const endpoints = [];
|
|
968
|
+
for (const plugin of plugins) {
|
|
969
|
+
for (const [name, endpoint] of Object.entries(plugin.endpoints ?? {})) {
|
|
970
|
+
const mounted = pluginEndpointPath(plugin.id, endpoint.path);
|
|
971
|
+
if (reservedRoutePaths.has(mounted)) {
|
|
972
|
+
throw new Error(
|
|
973
|
+
`Plugin "${plugin.id}" endpoint "${name}" collides with a core route.`
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
const routeKey = `${endpoint.method} ${mounted}`;
|
|
977
|
+
if (seenEndpointKeys.has(routeKey)) {
|
|
978
|
+
throw new Error(`Duplicate plugin endpoint ${routeKey}.`);
|
|
979
|
+
}
|
|
980
|
+
seenEndpointKeys.add(routeKey);
|
|
981
|
+
endpoints.push({ ...endpoint, pluginId: plugin.id });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const { plugins: _omit, ...rest } = config;
|
|
985
|
+
for (const plugin of plugins) {
|
|
986
|
+
plugin.init?.({ config: rest, getPlugin });
|
|
987
|
+
}
|
|
988
|
+
const guard = mergeHookField(
|
|
989
|
+
plugins.map((p) => p.hooks?.guard),
|
|
990
|
+
rest.guard
|
|
991
|
+
);
|
|
992
|
+
const merged = {
|
|
993
|
+
...rest,
|
|
994
|
+
...guard !== void 0 ? { guard } : {}
|
|
995
|
+
};
|
|
996
|
+
for (const feature of Object.keys(FEATURE_HOOK_KEYS)) {
|
|
997
|
+
merged[feature] = mergeFeatureHooks(
|
|
998
|
+
feature,
|
|
999
|
+
FEATURE_HOOK_KEYS[feature],
|
|
1000
|
+
plugins,
|
|
1001
|
+
rest[feature]
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return {
|
|
1005
|
+
config: merged,
|
|
1006
|
+
context,
|
|
1007
|
+
getPlugin,
|
|
1008
|
+
endpoints
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
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
|
+
// src/plugin/define-plugin.ts
|
|
1023
|
+
function definePlugin(plugin) {
|
|
1024
|
+
return plugin;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/dimah-s3.ts
|
|
1028
|
+
function dimahS3(config) {
|
|
1029
|
+
const {
|
|
1030
|
+
config: resolved,
|
|
1031
|
+
context,
|
|
1032
|
+
getPlugin,
|
|
1033
|
+
endpoints
|
|
1034
|
+
} = applyPlugins(config);
|
|
1035
|
+
const basePath = resolved.basePath ?? S3_API_BASE_PATH;
|
|
1036
|
+
return {
|
|
1037
|
+
handler: createHandler(resolved, basePath, endpoints),
|
|
1038
|
+
api: createServerApi(resolved),
|
|
1039
|
+
context,
|
|
1040
|
+
getPlugin,
|
|
1041
|
+
...context
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
export { FEATURE_HOOK_KEYS, RESERVED_PLUGIN_IDS, applyPlugins, chainHooks, createEndpoint, definePlugin, dimahS3, errors, resolveObjectAcl };
|
|
1046
|
+
//# sourceMappingURL=index.js.map
|
|
1047
|
+
//# sourceMappingURL=index.js.map
|