@lunora/storage 1.0.0-alpha.1 → 1.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +163 -163
- package/dist/index.d.ts +163 -163
- package/dist/index.mjs +4 -4
- package/dist/packem_shared/{buildPresignedUrl-DzPwi1bY.mjs → buildPresignedUrl-B5f1wvz6.mjs} +2 -8
- package/dist/packem_shared/{buildSignedUrl-ZzB16yPl.mjs → buildSignedUrl-rt-6azia.mjs} +41 -24
- package/dist/packem_shared/{createBucketStorage-4Xk7-5CN.mjs → createBucketStorage-JYdrS4e5.mjs} +6 -4
- package/dist/packem_shared/{scopeKey-Bs_iJ1Mx.mjs → createStorage-Bu6GJB25.mjs} +48 -38
- package/dist/packem_shared/internal-Dqk0MrAj.mjs +17 -0
- package/package.json +5 -2
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { t as trimTrailingSlashes, a as toHex } from './internal-Dqk0MrAj.mjs';
|
|
3
|
+
import { buildPresignedUrl } from './buildPresignedUrl-B5f1wvz6.mjs';
|
|
4
|
+
import { buildSignedUrl } from './buildSignedUrl-rt-6azia.mjs';
|
|
3
5
|
|
|
4
6
|
const MAX_KEY_LENGTH = 1024;
|
|
5
7
|
const MAX_LIST_LIMIT = 1e3;
|
|
6
8
|
const DEFAULT_LIST_LIMIT = 100;
|
|
7
|
-
const toHex = (buffer) => {
|
|
8
|
-
const bytes = new Uint8Array(buffer);
|
|
9
|
-
let out = "";
|
|
10
|
-
for (const byte of bytes) {
|
|
11
|
-
out += byte.toString(16).padStart(2, "0");
|
|
12
|
-
}
|
|
13
|
-
return out;
|
|
14
|
-
};
|
|
15
9
|
const toBase64 = (buffer) => {
|
|
16
10
|
const bytes = new Uint8Array(buffer);
|
|
17
11
|
let binary = "";
|
|
@@ -54,17 +48,37 @@ const toMetadata = (object) => {
|
|
|
54
48
|
uploaded: object.uploaded === void 0 ? void 0 : object.uploaded.getTime()
|
|
55
49
|
};
|
|
56
50
|
};
|
|
51
|
+
const toListObject = (object) => {
|
|
52
|
+
const raw = object.checksums?.sha256;
|
|
53
|
+
return {
|
|
54
|
+
checksums: object.checksums,
|
|
55
|
+
customMetadata: object.customMetadata,
|
|
56
|
+
etag: object.etag,
|
|
57
|
+
httpEtag: object.httpEtag,
|
|
58
|
+
httpMetadata: object.httpMetadata,
|
|
59
|
+
key: object.key,
|
|
60
|
+
sha256: raw === void 0 ? void 0 : toHex(raw),
|
|
61
|
+
sha256Base64: raw === void 0 ? void 0 : toBase64(raw),
|
|
62
|
+
size: object.size,
|
|
63
|
+
uploaded: object.uploaded
|
|
64
|
+
};
|
|
65
|
+
};
|
|
57
66
|
const enforceStreamMaxSize = (stream, maxSize) => {
|
|
58
67
|
let seen = 0;
|
|
59
68
|
const byteLengthOf = (chunk) => {
|
|
60
69
|
if (chunk instanceof ArrayBuffer) {
|
|
61
70
|
return chunk.byteLength;
|
|
62
71
|
}
|
|
63
|
-
return ArrayBuffer.isView(chunk) ? chunk.byteLength : 0;
|
|
72
|
+
return ArrayBuffer.isView(chunk) ? chunk.byteLength : void 0;
|
|
64
73
|
};
|
|
65
74
|
const counter = new TransformStream({
|
|
66
75
|
transform(chunk, controller) {
|
|
67
|
-
|
|
76
|
+
const length = byteLengthOf(chunk);
|
|
77
|
+
if (length === void 0) {
|
|
78
|
+
controller.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
seen += length;
|
|
68
82
|
if (seen > maxSize) {
|
|
69
83
|
controller.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(maxSize)} bytes)`));
|
|
70
84
|
return;
|
|
@@ -74,30 +88,23 @@ const enforceStreamMaxSize = (stream, maxSize) => {
|
|
|
74
88
|
});
|
|
75
89
|
return stream.pipeThrough(counter);
|
|
76
90
|
};
|
|
77
|
-
const trimTrailingSlashes = (value) => {
|
|
78
|
-
let end = value.length;
|
|
79
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
80
|
-
end -= 1;
|
|
81
|
-
}
|
|
82
|
-
return value.slice(0, end);
|
|
83
|
-
};
|
|
84
91
|
const validateKey = (key) => {
|
|
85
92
|
if (typeof key !== "string" || key.length === 0) {
|
|
86
|
-
throw new
|
|
93
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key must be a non-empty string");
|
|
87
94
|
}
|
|
88
95
|
if (key.length > MAX_KEY_LENGTH) {
|
|
89
|
-
throw new
|
|
96
|
+
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
90
97
|
}
|
|
91
98
|
if (key.includes("\0")) {
|
|
92
|
-
throw new
|
|
99
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key contains NUL byte");
|
|
93
100
|
}
|
|
94
101
|
if (key.startsWith("/")) {
|
|
95
|
-
throw new
|
|
102
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key must not start with `/`");
|
|
96
103
|
}
|
|
97
104
|
const segments = key.split("/");
|
|
98
105
|
for (const segment of segments) {
|
|
99
106
|
if (segment === "..") {
|
|
100
|
-
throw new
|
|
107
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key contains a `..` path component");
|
|
101
108
|
}
|
|
102
109
|
}
|
|
103
110
|
};
|
|
@@ -107,22 +114,22 @@ const scopeKey = (prefix, key) => {
|
|
|
107
114
|
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
108
115
|
const composed = `${trimmedPrefix}/${key}`;
|
|
109
116
|
if (composed.length > MAX_KEY_LENGTH) {
|
|
110
|
-
throw new
|
|
117
|
+
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
111
118
|
}
|
|
112
119
|
return composed;
|
|
113
120
|
};
|
|
114
121
|
const createStorage = (options) => {
|
|
115
122
|
if (!options.bucket) {
|
|
116
|
-
throw new
|
|
123
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `bucket` is required");
|
|
117
124
|
}
|
|
118
125
|
const upload = async (key, body, uploadOptions = {}) => {
|
|
119
126
|
validateKey(key);
|
|
120
127
|
if (uploadOptions.allowedContentTypes !== void 0) {
|
|
121
128
|
if (uploadOptions.contentType === void 0) {
|
|
122
|
-
throw new
|
|
129
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: contentType is required when allowedContentTypes is set");
|
|
123
130
|
}
|
|
124
131
|
if (!uploadOptions.allowedContentTypes.includes(uploadOptions.contentType)) {
|
|
125
|
-
throw new
|
|
132
|
+
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: contentType "${uploadOptions.contentType}" not in allowedContentTypes`);
|
|
126
133
|
}
|
|
127
134
|
}
|
|
128
135
|
let putBody = body;
|
|
@@ -134,7 +141,7 @@ const createStorage = (options) => {
|
|
|
134
141
|
size = body.size;
|
|
135
142
|
}
|
|
136
143
|
if (size !== void 0 && size > uploadOptions.maxSize) {
|
|
137
|
-
throw new
|
|
144
|
+
throw new LunoraError("PAYLOAD_TOO_LARGE", `@lunora/storage: body exceeds maxSize (${String(size)} > ${String(uploadOptions.maxSize)})`);
|
|
138
145
|
}
|
|
139
146
|
if (body instanceof ReadableStream) {
|
|
140
147
|
putBody = enforceStreamMaxSize(body, uploadOptions.maxSize);
|
|
@@ -166,16 +173,16 @@ const createStorage = (options) => {
|
|
|
166
173
|
};
|
|
167
174
|
const list = async (prefix, listOptions = {}) => {
|
|
168
175
|
if (prefix?.includes("\0")) {
|
|
169
|
-
throw new
|
|
176
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: prefix contains NUL byte");
|
|
170
177
|
}
|
|
171
178
|
const requested = listOptions.limit ?? DEFAULT_LIST_LIMIT;
|
|
172
179
|
const limit = Math.min(Math.max(1, Math.floor(requested)), MAX_LIST_LIMIT);
|
|
173
180
|
const result = await options.bucket.list({ cursor: listOptions.cursor, delimiter: listOptions.delimiter, limit, prefix });
|
|
174
|
-
return { cursor: result.cursor, objects: result.objects.map((object) =>
|
|
181
|
+
return { cursor: result.cursor, objects: result.objects.map((object) => toListObject(object)), truncated: result.truncated };
|
|
175
182
|
};
|
|
176
183
|
const getUrl = (key) => {
|
|
177
184
|
if (!options.publicBaseUrl) {
|
|
178
|
-
throw new
|
|
185
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getUrl()");
|
|
179
186
|
}
|
|
180
187
|
validateKey(key);
|
|
181
188
|
const safeKey = key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
@@ -183,10 +190,10 @@ const createStorage = (options) => {
|
|
|
183
190
|
};
|
|
184
191
|
const getSignedUrl = async (key, signedOptions = {}) => {
|
|
185
192
|
if (!options.publicBaseUrl) {
|
|
186
|
-
throw new
|
|
193
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");
|
|
187
194
|
}
|
|
188
195
|
if (!options.signingSecret) {
|
|
189
|
-
throw new
|
|
196
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: `signingSecret` is required for getSignedUrl()");
|
|
190
197
|
}
|
|
191
198
|
validateKey(key);
|
|
192
199
|
return buildSignedUrl({
|
|
@@ -201,7 +208,7 @@ const createStorage = (options) => {
|
|
|
201
208
|
const createMultipartUpload = async (key, multipartOptions = {}) => {
|
|
202
209
|
validateKey(key);
|
|
203
210
|
if (!options.bucket.createMultipartUpload) {
|
|
204
|
-
throw new
|
|
211
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");
|
|
205
212
|
}
|
|
206
213
|
return options.bucket.createMultipartUpload(key, {
|
|
207
214
|
customMetadata: multipartOptions.customMetadata,
|
|
@@ -211,16 +218,19 @@ const createStorage = (options) => {
|
|
|
211
218
|
const resumeMultipartUpload = (key, uploadId) => {
|
|
212
219
|
validateKey(key);
|
|
213
220
|
if (typeof uploadId !== "string" || uploadId.length === 0) {
|
|
214
|
-
throw new
|
|
221
|
+
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");
|
|
215
222
|
}
|
|
216
223
|
if (!options.bucket.resumeMultipartUpload) {
|
|
217
|
-
throw new
|
|
224
|
+
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");
|
|
218
225
|
}
|
|
219
226
|
return options.bucket.resumeMultipartUpload(key, uploadId);
|
|
220
227
|
};
|
|
221
228
|
const getPresignedUrl = async (key, presignedOptions = {}) => {
|
|
222
229
|
if (!options.s3) {
|
|
223
|
-
throw new
|
|
230
|
+
throw new LunoraError(
|
|
231
|
+
"INTERNAL",
|
|
232
|
+
"@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }"
|
|
233
|
+
);
|
|
224
234
|
}
|
|
225
235
|
validateKey(key);
|
|
226
236
|
return buildPresignedUrl({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const toHex = (buffer) => {
|
|
2
|
+
const bytes = new Uint8Array(buffer);
|
|
3
|
+
let out = "";
|
|
4
|
+
for (const byte of bytes) {
|
|
5
|
+
out += byte.toString(16).padStart(2, "0");
|
|
6
|
+
}
|
|
7
|
+
return out;
|
|
8
|
+
};
|
|
9
|
+
const trimTrailingSlashes = (value) => {
|
|
10
|
+
let end = value.length;
|
|
11
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
12
|
+
end -= 1;
|
|
13
|
+
}
|
|
14
|
+
return value.slice(0, end);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export { toHex as a, trimTrailingSlashes as t };
|
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.10",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"directory": "packages/storage"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
-
"dist",
|
|
28
|
+
"./dist",
|
|
29
29
|
"__assets__",
|
|
30
30
|
"README.md",
|
|
31
31
|
"LICENSE.md"
|
|
@@ -45,6 +45,9 @@
|
|
|
45
45
|
"publishConfig": {
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.6"
|
|
50
|
+
},
|
|
48
51
|
"engines": {
|
|
49
52
|
"node": "^22.15.0 || >=24.11.0"
|
|
50
53
|
}
|