@lunora/storage 1.0.0-alpha.4 → 1.0.0-alpha.5
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/dist/index.mjs +3 -3
- package/dist/packem_shared/{buildSignedUrl-O-vsHfyW.mjs → buildSignedUrl-DX535yKv.mjs} +4 -2
- package/dist/packem_shared/{createBucketStorage-4Xk7-5CN.mjs → createBucketStorage-JYdrS4e5.mjs} +6 -4
- package/dist/packem_shared/{createStorage-DoE7gOqo.mjs → createStorage-fwSr3UdU.mjs} +23 -19
- package/package.json +4 -1
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createBucketStorage } from './packem_shared/createBucketStorage-
|
|
2
|
-
export { createStorage, scopeKey } from './packem_shared/createStorage-
|
|
1
|
+
export { createBucketStorage } from './packem_shared/createBucketStorage-JYdrS4e5.mjs';
|
|
2
|
+
export { createStorage, scopeKey } from './packem_shared/createStorage-fwSr3UdU.mjs';
|
|
3
3
|
export { buildPresignedUrl } from './packem_shared/buildPresignedUrl-DzPwi1bY.mjs';
|
|
4
|
-
export { buildSignedUrl, verifySignedUrl } from './packem_shared/buildSignedUrl-
|
|
4
|
+
export { buildSignedUrl, verifySignedUrl } from './packem_shared/buildSignedUrl-DX535yKv.mjs';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const textEncoder = new TextEncoder();
|
|
2
4
|
const MAX_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
|
|
3
5
|
const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+\-.]*:\/\//i;
|
|
@@ -52,10 +54,10 @@ const buildSignedUrl = async (args) => {
|
|
|
52
54
|
const method = args.method ?? "GET";
|
|
53
55
|
const expiresInSeconds = args.expiresInSeconds ?? 60 * 60;
|
|
54
56
|
if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
55
|
-
throw new
|
|
57
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: expiresInSeconds must be a positive finite number");
|
|
56
58
|
}
|
|
57
59
|
if (expiresInSeconds > MAX_EXPIRES_IN_SECONDS) {
|
|
58
|
-
throw new
|
|
60
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: expiresInSeconds must not exceed ${String(MAX_EXPIRES_IN_SECONDS)} (7 days)`);
|
|
59
61
|
}
|
|
60
62
|
const contentType = method === "PUT" ? args.contentType : void 0;
|
|
61
63
|
const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
|
package/dist/packem_shared/{createBucketStorage-4Xk7-5CN.mjs → createBucketStorage-JYdrS4e5.mjs}
RENAMED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const createBucketStorage = (buckets, options = {}) => {
|
|
2
4
|
const names = Object.keys(buckets);
|
|
3
5
|
const [firstName] = names;
|
|
4
6
|
if (firstName === void 0) {
|
|
5
|
-
throw new
|
|
7
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: createBucketStorage requires at least one bucket");
|
|
6
8
|
}
|
|
7
9
|
if (options.default !== void 0 && !buckets[options.default]) {
|
|
8
|
-
throw new
|
|
10
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: default bucket "${options.default}" is not in the bucket map (have: ${names.join(", ")})`);
|
|
9
11
|
}
|
|
10
12
|
const defaultTag = options.default ?? "default";
|
|
11
13
|
const defaultBinding = buckets[defaultTag] ?? buckets[firstName];
|
|
12
14
|
if (defaultBinding === void 0) {
|
|
13
|
-
throw new
|
|
15
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: default bucket "${defaultTag}" is not in the bucket map (have: ${names.join(", ")})`);
|
|
14
16
|
}
|
|
15
17
|
const addressable = [.../* @__PURE__ */ new Set([defaultTag, ...names])];
|
|
16
18
|
const make = (name) => {
|
|
17
19
|
const target = name === defaultTag ? defaultBinding : buckets[name];
|
|
18
20
|
if (!target) {
|
|
19
|
-
throw new
|
|
21
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: no bucket registered for "${name}". Known buckets: ${addressable.join(", ")}`);
|
|
20
22
|
}
|
|
21
23
|
return { ...target, bucket: (next) => make(next), bucketName: name };
|
|
22
24
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
1
2
|
import { buildPresignedUrl } from './buildPresignedUrl-DzPwi1bY.mjs';
|
|
2
|
-
import { buildSignedUrl } from './buildSignedUrl-
|
|
3
|
+
import { buildSignedUrl } from './buildSignedUrl-DX535yKv.mjs';
|
|
3
4
|
|
|
4
5
|
const MAX_KEY_LENGTH = 1024;
|
|
5
6
|
const MAX_LIST_LIMIT = 1e3;
|
|
@@ -83,21 +84,21 @@ const trimTrailingSlashes = (value) => {
|
|
|
83
84
|
};
|
|
84
85
|
const validateKey = (key) => {
|
|
85
86
|
if (typeof key !== "string" || key.length === 0) {
|
|
86
|
-
throw new
|
|
87
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: key must be a non-empty string");
|
|
87
88
|
}
|
|
88
89
|
if (key.length > MAX_KEY_LENGTH) {
|
|
89
|
-
throw new
|
|
90
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
90
91
|
}
|
|
91
92
|
if (key.includes("\0")) {
|
|
92
|
-
throw new
|
|
93
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: key contains NUL byte");
|
|
93
94
|
}
|
|
94
95
|
if (key.startsWith("/")) {
|
|
95
|
-
throw new
|
|
96
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: key must not start with `/`");
|
|
96
97
|
}
|
|
97
98
|
const segments = key.split("/");
|
|
98
99
|
for (const segment of segments) {
|
|
99
100
|
if (segment === "..") {
|
|
100
|
-
throw new
|
|
101
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: key contains a `..` path component");
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
};
|
|
@@ -107,22 +108,22 @@ const scopeKey = (prefix, key) => {
|
|
|
107
108
|
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
108
109
|
const composed = `${trimmedPrefix}/${key}`;
|
|
109
110
|
if (composed.length > MAX_KEY_LENGTH) {
|
|
110
|
-
throw new
|
|
111
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
111
112
|
}
|
|
112
113
|
return composed;
|
|
113
114
|
};
|
|
114
115
|
const createStorage = (options) => {
|
|
115
116
|
if (!options.bucket) {
|
|
116
|
-
throw new
|
|
117
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `bucket` is required");
|
|
117
118
|
}
|
|
118
119
|
const upload = async (key, body, uploadOptions = {}) => {
|
|
119
120
|
validateKey(key);
|
|
120
121
|
if (uploadOptions.allowedContentTypes !== void 0) {
|
|
121
122
|
if (uploadOptions.contentType === void 0) {
|
|
122
|
-
throw new
|
|
123
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: contentType is required when allowedContentTypes is set");
|
|
123
124
|
}
|
|
124
125
|
if (!uploadOptions.allowedContentTypes.includes(uploadOptions.contentType)) {
|
|
125
|
-
throw new
|
|
126
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: contentType "${uploadOptions.contentType}" not in allowedContentTypes`);
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
let putBody = body;
|
|
@@ -134,7 +135,7 @@ const createStorage = (options) => {
|
|
|
134
135
|
size = body.size;
|
|
135
136
|
}
|
|
136
137
|
if (size !== void 0 && size > uploadOptions.maxSize) {
|
|
137
|
-
throw new
|
|
138
|
+
throw new LunoraError("INTERNAL", `@lunora/storage: body exceeds maxSize (${String(size)} > ${String(uploadOptions.maxSize)})`);
|
|
138
139
|
}
|
|
139
140
|
if (body instanceof ReadableStream) {
|
|
140
141
|
putBody = enforceStreamMaxSize(body, uploadOptions.maxSize);
|
|
@@ -166,7 +167,7 @@ const createStorage = (options) => {
|
|
|
166
167
|
};
|
|
167
168
|
const list = async (prefix, listOptions = {}) => {
|
|
168
169
|
if (prefix?.includes("\0")) {
|
|
169
|
-
throw new
|
|
170
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: prefix contains NUL byte");
|
|
170
171
|
}
|
|
171
172
|
const requested = listOptions.limit ?? DEFAULT_LIST_LIMIT;
|
|
172
173
|
const limit = Math.min(Math.max(1, Math.floor(requested)), MAX_LIST_LIMIT);
|
|
@@ -175,7 +176,7 @@ const createStorage = (options) => {
|
|
|
175
176
|
};
|
|
176
177
|
const getUrl = (key) => {
|
|
177
178
|
if (!options.publicBaseUrl) {
|
|
178
|
-
throw new
|
|
179
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getUrl()");
|
|
179
180
|
}
|
|
180
181
|
validateKey(key);
|
|
181
182
|
const safeKey = key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
@@ -183,10 +184,10 @@ const createStorage = (options) => {
|
|
|
183
184
|
};
|
|
184
185
|
const getSignedUrl = async (key, signedOptions = {}) => {
|
|
185
186
|
if (!options.publicBaseUrl) {
|
|
186
|
-
throw new
|
|
187
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");
|
|
187
188
|
}
|
|
188
189
|
if (!options.signingSecret) {
|
|
189
|
-
throw new
|
|
190
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `signingSecret` is required for getSignedUrl()");
|
|
190
191
|
}
|
|
191
192
|
validateKey(key);
|
|
192
193
|
return buildSignedUrl({
|
|
@@ -201,7 +202,7 @@ const createStorage = (options) => {
|
|
|
201
202
|
const createMultipartUpload = async (key, multipartOptions = {}) => {
|
|
202
203
|
validateKey(key);
|
|
203
204
|
if (!options.bucket.createMultipartUpload) {
|
|
204
|
-
throw new
|
|
205
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");
|
|
205
206
|
}
|
|
206
207
|
return options.bucket.createMultipartUpload(key, {
|
|
207
208
|
customMetadata: multipartOptions.customMetadata,
|
|
@@ -211,16 +212,19 @@ const createStorage = (options) => {
|
|
|
211
212
|
const resumeMultipartUpload = (key, uploadId) => {
|
|
212
213
|
validateKey(key);
|
|
213
214
|
if (typeof uploadId !== "string" || uploadId.length === 0) {
|
|
214
|
-
throw new
|
|
215
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");
|
|
215
216
|
}
|
|
216
217
|
if (!options.bucket.resumeMultipartUpload) {
|
|
217
|
-
throw new
|
|
218
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");
|
|
218
219
|
}
|
|
219
220
|
return options.bucket.resumeMultipartUpload(key, uploadId);
|
|
220
221
|
};
|
|
221
222
|
const getPresignedUrl = async (key, presignedOptions = {}) => {
|
|
222
223
|
if (!options.s3) {
|
|
223
|
-
throw new
|
|
224
|
+
throw new LunoraError(
|
|
225
|
+
"INTERNAL",
|
|
226
|
+
"@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }"
|
|
227
|
+
);
|
|
224
228
|
}
|
|
225
229
|
validateKey(key);
|
|
226
230
|
return buildPresignedUrl({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/storage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.5",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.1"
|
|
50
|
+
},
|
|
48
51
|
"engines": {
|
|
49
52
|
"node": "^22.15.0 || >=24.11.0"
|
|
50
53
|
}
|