@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.
@@ -1,17 +1,11 @@
1
- import { buildPresignedUrl } from './buildPresignedUrl-DzPwi1bY.mjs';
2
- import { buildSignedUrl } from './buildSignedUrl-ZzB16yPl.mjs';
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
- seen += byteLengthOf(chunk);
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 Error("@lunora/storage: key must be a non-empty string");
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 Error(`@lunora/storage: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
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 Error("@lunora/storage: key contains NUL byte");
99
+ throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key contains NUL byte");
93
100
  }
94
101
  if (key.startsWith("/")) {
95
- throw new Error("@lunora/storage: key must not start with `/`");
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 Error("@lunora/storage: key contains a `..` path component");
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 Error(`@lunora/storage: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
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 Error("@lunora/storage: `bucket` is required");
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 Error("@lunora/storage: contentType is required when allowedContentTypes is set");
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 Error(`@lunora/storage: contentType "${uploadOptions.contentType}" not in allowedContentTypes`);
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 Error(`@lunora/storage: body exceeds maxSize (${String(size)} > ${String(uploadOptions.maxSize)})`);
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 Error("@lunora/storage: prefix contains NUL byte");
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) => withSha256(object)), truncated: result.truncated };
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 Error("@lunora/storage: `publicBaseUrl` is required for getUrl()");
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 Error("@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");
193
+ throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");
187
194
  }
188
195
  if (!options.signingSecret) {
189
- throw new Error("@lunora/storage: `signingSecret` is required for getSignedUrl()");
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 Error("@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");
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 Error("@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");
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 Error("@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");
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 Error("@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");
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.1",
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
  }