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