@lunora/storage 1.0.0-alpha.43 → 1.0.0-alpha.45
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 +6 -0
- package/dist/index.d.mts +47 -14
- package/dist/index.d.ts +47 -14
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/{buildPresignedUrl-CnSO8AMP.mjs → buildPresignedUrl-U55ktL8x.mjs} +2 -2
- package/dist/packem_shared/buildSignedUrl-BnUO-9Fl.mjs +6 -0
- package/dist/packem_shared/createBucketStorage-D9fBoaEi.mjs +1 -0
- package/dist/packem_shared/createStorage-Bq-jf3qA.mjs +1 -0
- package/dist/packem_shared/internal-B5yyxF10.mjs +1 -0
- package/package.json +3 -3
- package/dist/packem_shared/buildSignedUrl-mAx0Keqt.mjs +0 -5
- package/dist/packem_shared/createBucketStorage-BE3H6x-j.mjs +0 -1
- package/dist/packem_shared/createStorage-Fzu--nFr.mjs +0 -1
- package/dist/packem_shared/internal-B2FsuR4l.mjs +0 -1
package/README.md
CHANGED
|
@@ -95,6 +95,12 @@ import { createStorage } from "@lunora/storage";
|
|
|
95
95
|
|
|
96
96
|
const storage = createStorage({
|
|
97
97
|
bucket: env.FILES,
|
|
98
|
+
// Required. The name this bucket is registered under, bound into every signed
|
|
99
|
+
// URL's HMAC (and mirrored as `&bucket=`), so a URL minted for one bucket
|
|
100
|
+
// cannot be replayed against another sharing the signing secret. `"default"`
|
|
101
|
+
// for a single-bucket app; the registered name for anything reached through
|
|
102
|
+
// `createBucketStorage`.
|
|
103
|
+
bucketName: "default",
|
|
98
104
|
publicBaseUrl: "https://cdn.acme.test",
|
|
99
105
|
signingSecret: env.STORAGE_SECRET,
|
|
100
106
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -27,6 +27,20 @@ interface PresignedUrlOptions {
|
|
|
27
27
|
}
|
|
28
28
|
interface LunoraStorageOptions {
|
|
29
29
|
bucket: R2BucketLike;
|
|
30
|
+
/**
|
|
31
|
+
* The name this bucket is registered under — the same string a
|
|
32
|
+
* `defineStorageRule({ bucket })` rule and the generated `StorageBucketName`
|
|
33
|
+
* union use. Bound into every signed URL's HMAC and mirrored on it as
|
|
34
|
+
* `&bucket=`, so a URL minted for one bucket can't be replayed against
|
|
35
|
+
* another sharing the signing secret, and the serving route can resolve
|
|
36
|
+
* which bucket to read.
|
|
37
|
+
*
|
|
38
|
+
* Required, and deliberately without a default: a defaulted name is how
|
|
39
|
+
* every bucket ended up signing as `"default"` and cross-verifying against
|
|
40
|
+
* each other. Pass `"default"` for a single-bucket app's `ctx.storage`, and
|
|
41
|
+
* the registered name for any bucket reached through `createBucketStorage`.
|
|
42
|
+
*/
|
|
43
|
+
bucketName: string;
|
|
30
44
|
/** Public base URL used by `getSignedUrl()`. Required for signed URLs. */
|
|
31
45
|
publicBaseUrl?: string;
|
|
32
46
|
/**
|
|
@@ -160,8 +174,15 @@ interface Storage {
|
|
|
160
174
|
* the same read.
|
|
161
175
|
*/
|
|
162
176
|
head: (key: string) => Promise<R2ObjectLike | null>;
|
|
177
|
+
/**
|
|
178
|
+
* List objects under `prefix`. With `options.delimiter` set, keys sharing a
|
|
179
|
+
* segment are rolled up into `delimitedPrefixes` (the "folders") and are NOT
|
|
180
|
+
* in `objects` — a folder browser needs both, so a listing whose `objects` is
|
|
181
|
+
* empty is not an empty directory.
|
|
182
|
+
*/
|
|
163
183
|
list: (prefix?: string, options?: ListOptions) => Promise<{
|
|
164
184
|
cursor?: string;
|
|
185
|
+
delimitedPrefixes?: string[];
|
|
165
186
|
objects: R2ObjectLike[];
|
|
166
187
|
truncated?: boolean;
|
|
167
188
|
}>;
|
|
@@ -207,19 +228,21 @@ interface BucketStorage extends Storage {
|
|
|
207
228
|
*
|
|
208
229
|
* ```ts
|
|
209
230
|
* storage: (env) => createBucketStorage({
|
|
210
|
-
* default: createStorage({ bucket: env.FILES }),
|
|
211
|
-
* avatars: createStorage({ bucket: env.AVATARS }),
|
|
231
|
+
* default: createStorage({ bucket: env.FILES, bucketName: "default" }),
|
|
232
|
+
* avatars: createStorage({ bucket: env.AVATARS, bucketName: "avatars" }),
|
|
212
233
|
* }),
|
|
213
234
|
* // → ctx.storage.download(key) // default bucket
|
|
214
235
|
* // → ctx.storage.bucket("avatars").store() // the avatars bucket
|
|
215
236
|
* ```
|
|
216
237
|
*
|
|
217
|
-
* The bare accessor is tagged
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
238
|
+
* The bare accessor is tagged with the name of the binding it actually
|
|
239
|
+
* delegates to: `options.default` when given, else `"default"` when that key
|
|
240
|
+
* exists (the canonical name a `defineStorageRule({ bucket: "default" })` rule
|
|
241
|
+
* and the generated `StorageBucketName` union use), else the first registered
|
|
242
|
+
* bucket. Tag and binding must agree or a `{ bucket: "avatars" }` rule would
|
|
243
|
+
* gate `bucket("avatars").download()` and not the identical bare
|
|
244
|
+
* `ctx.storage.download()` reaching the same R2 bucket. Named buckets are
|
|
245
|
+
* reached with `bucket(name)`.
|
|
223
246
|
*/
|
|
224
247
|
declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
|
|
225
248
|
default?: string;
|
|
@@ -251,10 +274,12 @@ declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<str
|
|
|
251
274
|
* string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
|
|
252
275
|
* (a base64url HMAC).
|
|
253
276
|
*
|
|
254
|
-
* The HMAC canonical includes the URL host so a signature
|
|
255
|
-
* cannot be replayed against another
|
|
256
|
-
*
|
|
257
|
-
*
|
|
277
|
+
* The HMAC canonical includes the URL host AND the `bucketName`, so a signature
|
|
278
|
+
* minted for one bucket cannot be replayed against another bucket (all buckets
|
|
279
|
+
* of one `.storage()` declaration share a base URL and signing secret) or
|
|
280
|
+
* another host. Even so, the signing secret MUST NOT be shared across
|
|
281
|
+
* tenants — this binding narrows replay surface but is not a substitute for
|
|
282
|
+
* per-tenant key isolation.
|
|
258
283
|
*
|
|
259
284
|
* The Worker route handling the signed download/upload (mounted at
|
|
260
285
|
* `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
|
|
@@ -262,12 +287,20 @@ declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<str
|
|
|
262
287
|
* the R2 body. `baseUrl` must be a bare origin (no path) — see the module
|
|
263
288
|
* docstring.
|
|
264
289
|
*/
|
|
265
|
-
declare const buildSignedUrl: (args:
|
|
290
|
+
declare const buildSignedUrl: (args: {
|
|
266
291
|
baseUrl: string;
|
|
292
|
+
/** The bucket the URL addresses — bound into the HMAC and mirrored as `&bucket=`. */
|
|
293
|
+
bucketName: string;
|
|
267
294
|
key: string;
|
|
268
295
|
secret: string;
|
|
269
|
-
}) => Promise<string>;
|
|
296
|
+
} & SignedUrlOptions) => Promise<string>;
|
|
270
297
|
interface VerifyResult {
|
|
298
|
+
/**
|
|
299
|
+
* The bucket the URL was minted for. The serving route MUST resolve its R2
|
|
300
|
+
* binding from this (never from a caller-supplied bucket), since one signing
|
|
301
|
+
* secret covers every bucket of a `.storage()` declaration.
|
|
302
|
+
*/
|
|
303
|
+
bucketName?: string;
|
|
271
304
|
/** The pinned upload `Content-Type` carried by a PUT URL, when present. */
|
|
272
305
|
contentType?: string;
|
|
273
306
|
key?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,20 @@ interface PresignedUrlOptions {
|
|
|
27
27
|
}
|
|
28
28
|
interface LunoraStorageOptions {
|
|
29
29
|
bucket: R2BucketLike;
|
|
30
|
+
/**
|
|
31
|
+
* The name this bucket is registered under — the same string a
|
|
32
|
+
* `defineStorageRule({ bucket })` rule and the generated `StorageBucketName`
|
|
33
|
+
* union use. Bound into every signed URL's HMAC and mirrored on it as
|
|
34
|
+
* `&bucket=`, so a URL minted for one bucket can't be replayed against
|
|
35
|
+
* another sharing the signing secret, and the serving route can resolve
|
|
36
|
+
* which bucket to read.
|
|
37
|
+
*
|
|
38
|
+
* Required, and deliberately without a default: a defaulted name is how
|
|
39
|
+
* every bucket ended up signing as `"default"` and cross-verifying against
|
|
40
|
+
* each other. Pass `"default"` for a single-bucket app's `ctx.storage`, and
|
|
41
|
+
* the registered name for any bucket reached through `createBucketStorage`.
|
|
42
|
+
*/
|
|
43
|
+
bucketName: string;
|
|
30
44
|
/** Public base URL used by `getSignedUrl()`. Required for signed URLs. */
|
|
31
45
|
publicBaseUrl?: string;
|
|
32
46
|
/**
|
|
@@ -160,8 +174,15 @@ interface Storage {
|
|
|
160
174
|
* the same read.
|
|
161
175
|
*/
|
|
162
176
|
head: (key: string) => Promise<R2ObjectLike | null>;
|
|
177
|
+
/**
|
|
178
|
+
* List objects under `prefix`. With `options.delimiter` set, keys sharing a
|
|
179
|
+
* segment are rolled up into `delimitedPrefixes` (the "folders") and are NOT
|
|
180
|
+
* in `objects` — a folder browser needs both, so a listing whose `objects` is
|
|
181
|
+
* empty is not an empty directory.
|
|
182
|
+
*/
|
|
163
183
|
list: (prefix?: string, options?: ListOptions) => Promise<{
|
|
164
184
|
cursor?: string;
|
|
185
|
+
delimitedPrefixes?: string[];
|
|
165
186
|
objects: R2ObjectLike[];
|
|
166
187
|
truncated?: boolean;
|
|
167
188
|
}>;
|
|
@@ -207,19 +228,21 @@ interface BucketStorage extends Storage {
|
|
|
207
228
|
*
|
|
208
229
|
* ```ts
|
|
209
230
|
* storage: (env) => createBucketStorage({
|
|
210
|
-
* default: createStorage({ bucket: env.FILES }),
|
|
211
|
-
* avatars: createStorage({ bucket: env.AVATARS }),
|
|
231
|
+
* default: createStorage({ bucket: env.FILES, bucketName: "default" }),
|
|
232
|
+
* avatars: createStorage({ bucket: env.AVATARS, bucketName: "avatars" }),
|
|
212
233
|
* }),
|
|
213
234
|
* // → ctx.storage.download(key) // default bucket
|
|
214
235
|
* // → ctx.storage.bucket("avatars").store() // the avatars bucket
|
|
215
236
|
* ```
|
|
216
237
|
*
|
|
217
|
-
* The bare accessor is tagged
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
238
|
+
* The bare accessor is tagged with the name of the binding it actually
|
|
239
|
+
* delegates to: `options.default` when given, else `"default"` when that key
|
|
240
|
+
* exists (the canonical name a `defineStorageRule({ bucket: "default" })` rule
|
|
241
|
+
* and the generated `StorageBucketName` union use), else the first registered
|
|
242
|
+
* bucket. Tag and binding must agree or a `{ bucket: "avatars" }` rule would
|
|
243
|
+
* gate `bucket("avatars").download()` and not the identical bare
|
|
244
|
+
* `ctx.storage.download()` reaching the same R2 bucket. Named buckets are
|
|
245
|
+
* reached with `bucket(name)`.
|
|
223
246
|
*/
|
|
224
247
|
declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
|
|
225
248
|
default?: string;
|
|
@@ -251,10 +274,12 @@ declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<str
|
|
|
251
274
|
* string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
|
|
252
275
|
* (a base64url HMAC).
|
|
253
276
|
*
|
|
254
|
-
* The HMAC canonical includes the URL host so a signature
|
|
255
|
-
* cannot be replayed against another
|
|
256
|
-
*
|
|
257
|
-
*
|
|
277
|
+
* The HMAC canonical includes the URL host AND the `bucketName`, so a signature
|
|
278
|
+
* minted for one bucket cannot be replayed against another bucket (all buckets
|
|
279
|
+
* of one `.storage()` declaration share a base URL and signing secret) or
|
|
280
|
+
* another host. Even so, the signing secret MUST NOT be shared across
|
|
281
|
+
* tenants — this binding narrows replay surface but is not a substitute for
|
|
282
|
+
* per-tenant key isolation.
|
|
258
283
|
*
|
|
259
284
|
* The Worker route handling the signed download/upload (mounted at
|
|
260
285
|
* `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
|
|
@@ -262,12 +287,20 @@ declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<str
|
|
|
262
287
|
* the R2 body. `baseUrl` must be a bare origin (no path) — see the module
|
|
263
288
|
* docstring.
|
|
264
289
|
*/
|
|
265
|
-
declare const buildSignedUrl: (args:
|
|
290
|
+
declare const buildSignedUrl: (args: {
|
|
266
291
|
baseUrl: string;
|
|
292
|
+
/** The bucket the URL addresses — bound into the HMAC and mirrored as `&bucket=`. */
|
|
293
|
+
bucketName: string;
|
|
267
294
|
key: string;
|
|
268
295
|
secret: string;
|
|
269
|
-
}) => Promise<string>;
|
|
296
|
+
} & SignedUrlOptions) => Promise<string>;
|
|
270
297
|
interface VerifyResult {
|
|
298
|
+
/**
|
|
299
|
+
* The bucket the URL was minted for. The serving route MUST resolve its R2
|
|
300
|
+
* binding from this (never from a caller-supplied bucket), since one signing
|
|
301
|
+
* secret covers every bucket of a `.storage()` declaration.
|
|
302
|
+
*/
|
|
303
|
+
bucketName?: string;
|
|
271
304
|
/** The pinned upload `Content-Type` carried by a PUT URL, when present. */
|
|
272
305
|
contentType?: string;
|
|
273
306
|
key?: string;
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createBucketStorage as o}from"./packem_shared/createBucketStorage-
|
|
1
|
+
import{createBucketStorage as o}from"./packem_shared/createBucketStorage-D9fBoaEi.mjs";import{createStorage as i,scopeKey as d}from"./packem_shared/createStorage-Bq-jf3qA.mjs";import{buildPresignedUrl as g}from"./packem_shared/buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as p,verifySignedUrl as a}from"./packem_shared/buildSignedUrl-BnUO-9Fl.mjs";export{g as buildPresignedUrl,p as buildSignedUrl,o as createBucketStorage,i as createStorage,d as scopeKey,a as verifySignedUrl};
|
package/dist/packem_shared/{buildPresignedUrl-CnSO8AMP.mjs → buildPresignedUrl-U55ktL8x.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{LunoraError as H}from"@lunora/errors";import{
|
|
1
|
+
import{LunoraError as H}from"@lunora/errors";import{b as y,v as R}from"./internal-B5yyxF10.mjs";const $="auto",E="s3",g="AWS4-HMAC-SHA256",x=10080*60,K=900,d=new TextEncoder,z=(t,e)=>t[0]<e[0]?-1:t[0]>e[0]?1:0,i=t=>encodeURIComponent(t).replaceAll(/[!'()*]/gu,e=>`%${e.codePointAt(0)?.toString(16).toUpperCase()??""}`),C=t=>t.split("/").map(e=>i(e)).join("/"),X=async t=>y(await crypto.subtle.digest("SHA-256",d.encode(t))),c=async(t,e)=>{const n=await crypto.subtle.importKey("raw",t,{hash:"SHA-256",name:"HMAC"},!1,["sign"]);return crypto.subtle.sign("HMAC",n,d.encode(e))},O=async(t,e)=>{const n=await c(d.encode(`AWS4${t}`),e),r=await c(n,$),s=await c(r,E);return c(s,"aws4_request")},v=t=>{const e=t.jurisdiction===void 0?"":`${t.jurisdiction}.`;return`${t.accountId}.${e}r2.cloudflarestorage.com`},T=t=>{const e=`${t.toISOString().replaceAll(/[:-]/gu,"").slice(0,15)}Z`;return{amzDate:e,dateStamp:e.slice(0,8)}},M=async t=>{const{credentials:e,key:n}=t,r=t.method??"GET";let s=K;if(t.expiresInSeconds!==void 0){s=Math.floor(t.expiresInSeconds);const o=R(s,x);if(o!==void 0)throw new H("VALIDATION_ERROR",`@lunora/storage: ${o}`)}const l=v(e),w=new Date(t.now?.()??Date.now()),{amzDate:u,dateStamp:S}=T(w),A=`${S}/${$}/${E}/aws4_request`,m=`/${i(e.bucket)}/${C(n)}`,p=[["X-Amz-Algorithm",g],["X-Amz-Credential",`${e.accessKeyId}/${A}`],["X-Amz-Date",u],["X-Amz-Expires",s.toString()],["X-Amz-SignedHeaders","host"]].map(([o,a])=>[i(o),i(a)]).toSorted(z).map(([o,a])=>`${o}=${a}`).join("&"),h=[r,m,p,`host:${l}
|
|
2
2
|
`,"host","UNSIGNED-PAYLOAD"].join(`
|
|
3
3
|
`),I=[g,u,A,await X(h)].join(`
|
|
4
|
-
`),D=await O(e.secretAccessKey,S),f=y(await c(D,I));return`https://${l}${m}?${p}&X-Amz-Signature=${f}`};export{
|
|
4
|
+
`),D=await O(e.secretAccessKey,S),f=y(await c(D,I));return`https://${l}${m}?${p}&X-Amz-Signature=${f}`};export{M as buildPresignedUrl};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{i as R,c as f,e as p,s as y,t as U,f as N,d as $,v as w,M as I}from"./internal-B5yyxF10.mjs";const S=/^\//,b=(e,s,r,t,o,a)=>{const n=`${e}
|
|
2
|
+
${s.toLowerCase()}
|
|
3
|
+
${r}
|
|
4
|
+
${t}
|
|
5
|
+
${String(o)}`;return a===void 0?n:`${n}
|
|
6
|
+
${a}`},x=async e=>{const s=e.method??"GET",r=e.expiresInSeconds??3600,t=w(r,I);if(t!==void 0)throw new u("VALIDATION_ERROR",`@lunora/storage: ${t}`);const o=s==="PUT"?e.contentType:void 0;let a="";try{a=new URL(e.baseUrl).pathname}catch{}if(a!==""&&!R(a))throw new u("VALIDATION_ERROR",`@lunora/storage: baseUrl must not carry a path ("${a}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);if(f(e.key),f(e.bucketName),e.bucketName==="")throw new u("VALIDATION_ERROR","@lunora/storage: bucketName must not be empty");const n=Math.floor(Date.now()/1e3)+r,c=p(e.baseUrl),i=await y(e.secret,b(s,c,e.bucketName,e.key,n,o)),m=U(e.baseUrl),l=e.key.split("/").map(h=>encodeURIComponent(h)).join("/"),d=o===void 0?"":`&ct=${encodeURIComponent(o)}`;return`${m}/${l}?exp=${String(n)}&method=${s}&bucket=${encodeURIComponent(e.bucketName)}&sig=${i}${d}`},E=async(e,s,r)=>{let t;try{t=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const o=t.searchParams.get("exp"),a=o===null?Number.NaN:Number(o),n=t.searchParams.get("sig"),c=t.searchParams.get("method")??"GET",i=t.searchParams.get("bucket"),m=t.searchParams.get("ct")??void 0;if(!n||!i||!Number.isInteger(a))return{reason:"malformed",valid:!1};if(a<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(c!=="GET"&&c!=="PUT")return{reason:"malformed",valid:!1};let l,d;try{l=t.pathname.replace(S,"").split("/").map(v=>decodeURIComponent(v)).join("/"),f(l),f(i),d=N(n)}catch{return{reason:"malformed",valid:!1}}const h=r?.expectedHost===void 0?t.host:p(r.expectedHost);return await $(s,b(c,h,i,l,a,m),d)?{bucketName:i,contentType:m,key:l,method:c,valid:!0}:{reason:"bad_signature",valid:!1}};export{x as buildSignedUrl,E as verifySignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";const g=(e,r={})=>{const a=Object.keys(e),[u]=a;if(u===void 0)throw new n("INTERNAL","@lunora/storage: createBucketStorage requires at least one bucket");if(r.default!==void 0&&!e[r.default])throw new n("INTERNAL",`@lunora/storage: default bucket "${r.default}" is not in the bucket map (have: ${a.join(", ")})`);const t=r.default??(e.default?"default":u),c=e[t];if(c===void 0)throw new n("INTERNAL",`@lunora/storage: default bucket "${t}" is not in the bucket map (have: ${a.join(", ")})`);const i=[...new Set([t,...a])],d=o=>{const f=o===t?c:e[o];if(!f)throw new n("INTERNAL",`@lunora/storage: no bucket registered for "${o}". Known buckets: ${i.join(", ")}`);return{...f,bucket:l=>d(l),bucketName:o}};return d(t)};export{g as createBucketStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as n}from"@lunora/errors";import{t as U,h as p,a as I,b as T}from"./internal-B5yyxF10.mjs";import{buildPresignedUrl as L}from"./buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as N}from"./buildSignedUrl-BnUO-9Fl.mjs";const w=1024,E=1e3,M=100,S=e=>{const a=e.checksums?.sha256;if(a===void 0)return e;const g=T(a),d=I(new Uint8Array(a));return new Proxy(e,{get(i,s){if(s==="sha256")return g;if(s==="sha256Base64")return d;const u=Reflect.get(i,s,i);return typeof u=="function"?u.bind(i):u},has(i,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(i,s)}})},k=e=>{const a=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:a===void 0?void 0:T(a),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},R=e=>{const a=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:a===void 0?void 0:T(a),sha256Base64:a===void 0?void 0:I(new Uint8Array(a)),size:e.size,uploaded:e.uploaded}},x=(e,a)=>{let g=0;const d=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,i=new TransformStream({transform(s,u){const m=d(s);if(m===void 0){u.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(g+=m,g>a){u.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(a)} bytes)`));return}u.enqueue(s)}});return e.pipeThrough(i)},_=new TextEncoder,A=e=>_.encode(e).length,o=e=>{if(typeof e!="string"||e.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(A(e)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(w)}-byte limit`);if(e.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(p(e))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new n("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");if(e.split("/").includes(".."))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},v=e=>{if(e.allowedContentTypes!==void 0){if(e.contentType===void 0)throw new n("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!e.allowedContentTypes.includes(e.contentType))throw new n("VALIDATION_ERROR",`@lunora/storage: contentType "${e.contentType}" not in allowedContentTypes`)}},O=(e,a)=>{o(e),o(a);const d=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(A(d)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return d},K=e=>{if(!e.bucket)throw new n("INTERNAL","@lunora/storage: `bucket` is required");if(typeof e.bucketName!="string"||e.bucketName==="")throw new n("INTERNAL",'@lunora/storage: `bucketName` is required — pass the name this bucket is registered under (`"default"` for a single-bucket app)');const a=async(r,t,c={})=>{o(r),v(c);let f=t;if(typeof c.maxSize=="number"){let h;if(t instanceof ArrayBuffer?h=t.byteLength:t instanceof Blob&&(h=t.size),h!==void 0&&h>c.maxSize)throw new n("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(h)} > ${String(c.maxSize)})`);t instanceof ReadableStream&&(f=x(t,c.maxSize))}const l=await e.bucket.put(r,f,{customMetadata:c.customMetadata,httpMetadata:c.contentType?{contentType:c.contentType}:void 0,...c.sha256===void 0?{}:{sha256:c.sha256}});return{etag:l.etag,httpEtag:l.httpEtag??`"${l.etag}"`,key:l.key}},g=async(r,t={})=>{o(r);const c=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return c&&S(c)},d=async r=>{o(r),await e.bucket.delete(r)},i=async r=>{o(r);const t=e.bucket.head?await e.bucket.head(r):await e.bucket.get(r,{range:{length:0}});return t&&R(t)},s=async r=>{const t=await i(r);return t&&k(t)},u=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const c=t.limit??M,f=Math.min(Math.max(1,Math.floor(c)),E),l=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:f,prefix:r}),{delimitedPrefixes:h}=l;return{cursor:l.cursor,delimitedPrefixes:h,objects:l.objects.map(b=>R(b)),truncated:l.truncated}},m=r=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");o(r);const t=r.split("/").map(c=>encodeURIComponent(c)).join("/");return`${U(e.publicBaseUrl)}/${t}`},y=async(r,t={})=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new n("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return o(r),N({baseUrl:e.publicBaseUrl,bucketName:e.bucketName,contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method,secret:e.signingSecret})};return{createMultipartUpload:async(r,t={})=>{if(o(r),!e.bucket.createMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");return e.bucket.createMultipartUpload(r,{customMetadata:t.customMetadata,httpMetadata:t.contentType?{contentType:t.contentType}:void 0})},delete:d,download:g,generateUploadUrl:async(r,t={})=>y(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"}),getMetadata:s,getPresignedUrl:async(r,t={})=>{if(!e.s3)throw new n("INTERNAL","@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");return o(r),L({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},getSignedUrl:y,getUrl:m,head:i,list:u,resumeMultipartUpload:(r,t)=>{if(o(r),typeof t!="string"||t.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:a,upload:a}};export{K as createStorage,O as scopeKey};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const p=t=>{let n="";for(let r=0;r<t.length;r+=32768)n+=String.fromCharCode(...t.subarray(r,r+32768));return btoa(n)},d=t=>{const n=atob(t),e=new Uint8Array(n.length);for(let r=0;r<n.length;r+=1)e[r]=n.codePointAt(r)??0;return e},s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",u=t=>{let n="",e=0;const r=t.length-2;for(;e<r;e+=3){const o=t[e]<<16|t[e+1]<<8|t[e+2];n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)+s.charAt(o&63)}const c=t.length-e;if(c===1){const o=t[e]<<16;n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)}else if(c===2){const o=t[e]<<16|t[e+1]<<8;n+=s.charAt(o>>18&63)+s.charAt(o>>12&63)+s.charAt(o>>6&63)}return n},_=t=>{const n=t.replaceAll("-","+").replaceAll("_","/"),e=n+"=".repeat((4-n.length%4)%4);return d(e)},A=(t,n)=>{if(t.size<n)return;const e=t.keys().next().value;e!==void 0&&t.delete(e)},f=(t,n,e,r)=>{const c=t.get(n);if(c!==void 0)return c;A(t,r);const o=e().catch(h=>{throw t.get(n)===o&&t.delete(n),h});return t.set(n,o),o},a=new TextEncoder,w=10080*60,g=/^[a-z][a-z0-9+\-.]*:\/\//i,y=Array.from({length:32},(t,n)=>n),i=new RegExp(`[${y.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),C=64,S=new Map,l=async t=>f(S,t,async()=>crypto.subtle.importKey("raw",a.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),C),m=t=>{try{return new URL(t).host}catch{return t.replace(g,"").split("/")[0]??""}},H=t=>{if(i.test(t))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},R=t=>i.test(t),E=/^\/+$/u,x=t=>E.test(t),L=(t,n)=>{if(!Number.isFinite(t)||t<=0)return"expiresInSeconds must be a positive finite number";if(t>n){const e=` (${String(n/86400)} days)`;return`expiresInSeconds must not exceed ${String(n)} seconds${e}`}},T=async(t,n)=>{const e=await l(t),r=await crypto.subtle.sign("HMAC",e,a.encode(n));return u(new Uint8Array(r))},O=async(t,n,e)=>{const r=await l(t);return crypto.subtle.verify("HMAC",r,e,a.encode(n))},v=t=>{const n=new Uint8Array(t);let e="";for(const r of n)e+=r.toString(16).padStart(2,"0");return e},M=t=>{let n=t.length;for(;n>0&&t[n-1]==="/";)n-=1;return t.slice(0,n)};export{w as M,p as a,v as b,H as c,O as d,m as e,_ as f,R as h,x as i,T as s,M as t,L as v};
|
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.45",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
54
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
53
|
+
"@lunora/errors": "1.0.0-alpha.27",
|
|
54
|
+
"@lunora/platform": "1.0.0-alpha.23",
|
|
55
55
|
"@visulima/storage": "2.0.0",
|
|
56
56
|
"aws4fetch": "1.0.20"
|
|
57
57
|
},
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import{LunoraError as h}from"@lunora/errors";import{i as U,b as p,e as u,s as R,t as b,f as S,c as $,v as w,M as L}from"./internal-B2FsuR4l.mjs";const T=/^\//,v=(e,s,n,t,o)=>{const a=`${e}
|
|
2
|
-
${s.toLowerCase()}
|
|
3
|
-
${n}
|
|
4
|
-
${String(t)}`;return o===void 0?a:`${a}
|
|
5
|
-
${o}`},g=async e=>{const s=e.method??"GET",n=e.expiresInSeconds??3600,t=w(n,L);if(t!==void 0)throw new h("VALIDATION_ERROR",`@lunora/storage: ${t}`);const o=s==="PUT"?e.contentType:void 0;let a="";try{a=new URL(e.baseUrl).pathname}catch{}if(a!==""&&!U(a))throw new h("VALIDATION_ERROR",`@lunora/storage: baseUrl must not carry a path ("${a}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);p(e.key);const c=Math.floor(Date.now()/1e3)+n,r=u(e.baseUrl),l=await R(e.secret,v(s,r,e.key,c,o)),i=b(e.baseUrl),d=e.key.split("/").map(f=>encodeURIComponent(f)).join("/"),m=o===void 0?"":`&ct=${encodeURIComponent(o)}`;return`${i}/${d}?exp=${String(c)}&method=${s}&sig=${l}${m}`},E=async(e,s,n)=>{let t;try{t=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const o=t.searchParams.get("exp"),a=o===null?Number.NaN:Number(o),c=t.searchParams.get("sig"),r=t.searchParams.get("method")??"GET",l=t.searchParams.get("ct")??void 0;if(!c||!Number.isInteger(a))return{reason:"malformed",valid:!1};if(a<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(r!=="GET"&&r!=="PUT")return{reason:"malformed",valid:!1};let i,d;try{i=t.pathname.replace(T,"").split("/").map(y=>decodeURIComponent(y)).join("/"),p(i),d=S(c)}catch{return{reason:"malformed",valid:!1}}const m=n?.expectedHost===void 0?t.host:u(n.expectedHost);return await $(s,v(r,m,i,a,l),d)?{contentType:l,key:i,method:r,valid:!0}:{reason:"bad_signature",valid:!1}};export{g as buildSignedUrl,E as verifySignedUrl};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as n}from"@lunora/errors";const g=(e,r={})=>{const a=Object.keys(e),[u]=a;if(u===void 0)throw new n("INTERNAL","@lunora/storage: createBucketStorage requires at least one bucket");if(r.default!==void 0&&!e[r.default])throw new n("INTERNAL",`@lunora/storage: default bucket "${r.default}" is not in the bucket map (have: ${a.join(", ")})`);const t=r.default??"default",c=e[t]??e[u];if(c===void 0)throw new n("INTERNAL",`@lunora/storage: default bucket "${t}" is not in the bucket map (have: ${a.join(", ")})`);const f=[...new Set([t,...a])],i=o=>{const d=o===t?c:e[o];if(!d)throw new n("INTERNAL",`@lunora/storage: no bucket registered for "${o}". Known buckets: ${f.join(", ")}`);return{...d,bucket:s=>i(s),bucketName:o}};return i(t)};export{g as createBucketStorage};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{LunoraError as n}from"@lunora/errors";import{t as U,h as b,a as T}from"./internal-B2FsuR4l.mjs";import{buildPresignedUrl as p}from"./buildPresignedUrl-CnSO8AMP.mjs";import{buildSignedUrl as L}from"./buildSignedUrl-mAx0Keqt.mjs";const w=1024,M=1e3,S=100,I=e=>{const a=new Uint8Array(e);let i="";for(const u of a)i+=String.fromCodePoint(u);return btoa(i)},E=e=>{const a=e.checksums?.sha256;if(a===void 0)return e;const i=T(a),u=I(a);return new Proxy(e,{get(l,s){if(s==="sha256")return i;if(s==="sha256Base64")return u;const d=Reflect.get(l,s,l);return typeof d=="function"?d.bind(l):d},has(l,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(l,s)}})},N=e=>{const a=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:a===void 0?void 0:T(a),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},R=e=>{const a=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:a===void 0?void 0:T(a),sha256Base64:a===void 0?void 0:I(a),size:e.size,uploaded:e.uploaded}},x=(e,a)=>{let i=0;const u=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,l=new TransformStream({transform(s,d){const m=u(s);if(m===void 0){d.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(i+=m,i>a){d.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(a)} bytes)`));return}d.enqueue(s)}});return e.pipeThrough(l)},_=new TextEncoder,A=e=>_.encode(e).length,c=e=>{if(typeof e!="string"||e.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(A(e)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(w)}-byte limit`);if(e.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(b(e))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new n("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");if(e.split("/").includes(".."))throw new n("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},v=e=>{if(e.allowedContentTypes!==void 0){if(e.contentType===void 0)throw new n("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!e.allowedContentTypes.includes(e.contentType))throw new n("VALIDATION_ERROR",`@lunora/storage: contentType "${e.contentType}" not in allowedContentTypes`)}},q=(e,a)=>{c(e),c(a);const u=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(A(u)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return u},O=e=>{if(!e.bucket)throw new n("INTERNAL","@lunora/storage: `bucket` is required");const a=async(r,t,o={})=>{c(r),v(o);let f=t;if(typeof o.maxSize=="number"){let h;if(t instanceof ArrayBuffer?h=t.byteLength:t instanceof Blob&&(h=t.size),h!==void 0&&h>o.maxSize)throw new n("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(h)} > ${String(o.maxSize)})`);t instanceof ReadableStream&&(f=x(t,o.maxSize))}const g=await e.bucket.put(r,f,{customMetadata:o.customMetadata,httpMetadata:o.contentType?{contentType:o.contentType}:void 0,...o.sha256===void 0?{}:{sha256:o.sha256}});return{etag:g.etag,httpEtag:g.httpEtag??`"${g.etag}"`,key:g.key}},i=async(r,t={})=>{c(r);const o=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return o&&E(o)},u=async r=>{c(r),await e.bucket.delete(r)},l=async r=>{c(r);const t=e.bucket.head?await e.bucket.head(r):await e.bucket.get(r,{range:{length:0}});return t&&R(t)},s=async r=>{const t=await l(r);return t&&N(t)},d=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const o=t.limit??S,f=Math.min(Math.max(1,Math.floor(o)),M),g=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:f,prefix:r});return{cursor:g.cursor,objects:g.objects.map(h=>R(h)),truncated:g.truncated}},m=r=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(o=>encodeURIComponent(o)).join("/");return`${U(e.publicBaseUrl)}/${t}`},y=async(r,t={})=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new n("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return c(r),L({baseUrl:e.publicBaseUrl,contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method,secret:e.signingSecret})};return{createMultipartUpload:async(r,t={})=>{if(c(r),!e.bucket.createMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");return e.bucket.createMultipartUpload(r,{customMetadata:t.customMetadata,httpMetadata:t.contentType?{contentType:t.contentType}:void 0})},delete:u,download:i,generateUploadUrl:async(r,t={})=>y(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"}),getMetadata:s,getPresignedUrl:async(r,t={})=>{if(!e.s3)throw new n("INTERNAL","@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");return c(r),p({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},getSignedUrl:y,getUrl:m,head:l,list:d,resumeMultipartUpload:(r,t)=>{if(c(r),typeof t!="string"||t.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new n("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:a,upload:a}};export{O as createStorage,q as scopeKey};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const d=t=>{const n=atob(t),e=new Uint8Array(n.length);for(let o=0;o<n.length;o+=1)e[o]=n.codePointAt(o)??0;return e},s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",A=t=>{let n="",e=0;const o=t.length-2;for(;e<o;e+=3){const r=t[e]<<16|t[e+1]<<8|t[e+2];n+=s.charAt(r>>18&63)+s.charAt(r>>12&63)+s.charAt(r>>6&63)+s.charAt(r&63)}const c=t.length-e;if(c===1){const r=t[e]<<16;n+=s.charAt(r>>18&63)+s.charAt(r>>12&63)}else if(c===2){const r=t[e]<<16|t[e+1]<<8;n+=s.charAt(r>>18&63)+s.charAt(r>>12&63)+s.charAt(r>>6&63)}return n},p=t=>{const n=t.replaceAll("-","+").replaceAll("_","/"),e=n+"=".repeat((4-n.length%4)%4);return d(e)},u=(t,n)=>{if(t.size<n)return;const e=t.keys().next().value;e!==void 0&&t.delete(e)},f=(t,n,e,o)=>{const c=t.get(n);if(c!==void 0)return c;u(t,o);const r=e().catch(h=>{throw t.get(n)===r&&t.delete(n),h});return t.set(n,r),r},a=new TextEncoder,_=10080*60,g=/^[a-z][a-z0-9+\-.]*:\/\//i,y=Array.from({length:32},(t,n)=>n),i=new RegExp(`[${y.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),S=64,C=new Map,l=async t=>f(C,t,async()=>crypto.subtle.importKey("raw",a.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),S),w=t=>{try{return new URL(t).host}catch{return t.replace(g,"").split("/")[0]??""}},m=t=>{if(i.test(t))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},H=t=>i.test(t),E=/^\/+$/u,R=t=>E.test(t),x=(t,n)=>{if(!Number.isFinite(t)||t<=0)return"expiresInSeconds must be a positive finite number";if(t>n){const e=` (${String(n/86400)} days)`;return`expiresInSeconds must not exceed ${String(n)} seconds${e}`}},L=async(t,n)=>{const e=await l(t),o=await crypto.subtle.sign("HMAC",e,a.encode(n));return A(new Uint8Array(o))},T=async(t,n,e)=>{const o=await l(t);return crypto.subtle.verify("HMAC",o,e,a.encode(n))},O=t=>{const n=new Uint8Array(t);let e="";for(const o of n)e+=o.toString(16).padStart(2,"0");return e},v=t=>{let n=t.length;for(;n>0&&t[n-1]==="/";)n-=1;return t.slice(0,n)};export{_ as M,O as a,m as b,T as c,w as e,p as f,H as h,R as i,L as s,v as t,x as v};
|