@dimah-s3/server 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/index.js
CHANGED
|
@@ -1,127 +1,88 @@
|
|
|
1
|
-
import { S3_API_ROUTES, DimahS3Error,
|
|
2
|
-
export { DimahS3Error } from '@dimah-s3/core';
|
|
3
|
-
import {
|
|
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: () =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
code: S3_ERROR_CODES.
|
|
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
|
-
|
|
34
|
-
code: S3_ERROR_CODES.
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
68
|
+
validationError: (message) => DimahS3Error.from("BAD_REQUEST", {
|
|
69
|
+
code: S3_ERROR_CODES.VALIDATION_ERROR.code,
|
|
70
|
+
message
|
|
71
|
+
})
|
|
52
72
|
};
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
80
|
+
if (isAPIError(err)) throw err;
|
|
123
81
|
if (err instanceof Error && err.message.trim()) {
|
|
124
|
-
throw
|
|
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
|
|
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/
|
|
148
|
-
function
|
|
149
|
-
const
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
|
|
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/
|
|
173
|
-
async function
|
|
174
|
-
const key =
|
|
175
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
|
244
|
-
await runHook(config.download?.
|
|
342
|
+
const fileName = input.fileName;
|
|
343
|
+
await runHook(config.download?.guard, {
|
|
245
344
|
request,
|
|
246
345
|
key,
|
|
247
346
|
bucket,
|
|
248
|
-
fileName
|
|
347
|
+
fileName
|
|
249
348
|
});
|
|
250
349
|
try {
|
|
251
|
-
await config.
|
|
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.
|
|
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
|
|
372
|
+
fileName,
|
|
272
373
|
url,
|
|
273
374
|
expiresIn
|
|
274
375
|
});
|
|
275
376
|
return { bucket, key, url, expiresIn };
|
|
276
377
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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.
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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.
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
|
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?.
|
|
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.
|
|
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(
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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: ["
|
|
862
|
-
download: ["
|
|
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
|
-
|
|
971
|
-
|
|
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
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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:
|
|
1038
|
-
api
|
|
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 {
|
|
1083
|
+
export { createS3Endpoint, createS3Middleware, definePlugin, dimahS3, errors, resolveObjectAcl };
|
|
1046
1084
|
//# sourceMappingURL=index.js.map
|
|
1047
1085
|
//# sourceMappingURL=index.js.map
|