@lunora/storage 1.0.0-alpha.34 → 1.0.0-alpha.36

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 CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  ---
36
36
 
37
- R2-backed file storage for Lunora. Wraps a Cloudflare `R2Bucket` binding with a typed API (`upload`/`store`, `download`, `delete`, `list`, `getMetadata`, multipart), worker-signed URLs for app-gated access, and native S3 presigned URLs for direct-to-R2 transfer.
37
+ R2-backed file storage for Lunora. Wraps a Cloudflare `R2Bucket` binding with a typed API (`upload`/`store`, `download`, `head`, `delete`, `list`, `getMetadata`, multipart), worker-signed URLs for app-gated access, and native S3 presigned URLs for direct-to-R2 transfer.
38
38
 
39
39
  Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
40
40
 
package/dist/index.d.mts CHANGED
@@ -20,7 +20,7 @@ interface R2S3Credentials {
20
20
  }
21
21
  /** Options for {@link Storage.getPresignedUrl}. */
22
22
  interface PresignedUrlOptions {
23
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
23
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
24
24
  expiresInSeconds?: number;
25
25
  /** HTTP method the URL authorizes. Default `GET`. */
26
26
  method?: "GET" | "PUT";
@@ -131,10 +131,10 @@ interface Storage {
131
131
  }) => Promise<string>;
132
132
  /**
133
133
  * Read a stored object's metadata (size, content-type, sha256, upload time,
134
- * custom metadata) without fetching its body. Returns `null` when the object
135
- * is absent. Backed by an R2 HEAD (`bucket.head`) when available, falling
136
- * back to a 0-length ranged `get()` otherwise. Mirrors Convex's
137
- * `ctx.storage.getMetadata`.
134
+ * custom metadata) without fetching its body, as a flat serializable shape.
135
+ * Returns `null` when the object is absent. A projection of
136
+ * {@link Storage.head}, so it makes the same single body-free read. Mirrors
137
+ * Convex's `ctx.storage.getMetadata`.
138
138
  */
139
139
  getMetadata: (key: string) => Promise<ObjectMetadata | null>;
140
140
  /**
@@ -147,6 +147,19 @@ interface Storage {
147
147
  getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
148
148
  getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
149
149
  getUrl: (key: string) => string;
150
+ /**
151
+ * Read an object's R2 metadata WITHOUT its body — `size` (the full object
152
+ * size), `etag`, `httpMetadata`, `checksums`, plus the `sha256`/`sha256Base64`
153
+ * projection `download()` adds. Returns `null` when the object is absent.
154
+ *
155
+ * Backed by an R2 HEAD when the binding exposes one, falling back to a
156
+ * 0-length ranged `get()` otherwise. Prefer this over `download()` whenever
157
+ * only the metadata is wanted — notably to resolve a `Range` header, where a
158
+ * plain `download()` starts a full-object body transfer that is then thrown
159
+ * away. {@link Storage.getMetadata} is the flat, serializable projection of
160
+ * the same read.
161
+ */
162
+ head: (key: string) => Promise<R2ObjectLike | null>;
150
163
  list: (prefix?: string, options?: ListOptions) => Promise<{
151
164
  cursor?: string;
152
165
  objects: R2ObjectLike[];
@@ -217,7 +230,7 @@ declare const createStorage: (options: LunoraStorageOptions) => Storage;
217
230
  interface PresignedUrlParams {
218
231
  /** R2 S3 API credentials + bucket/account. */
219
232
  credentials: R2S3Credentials;
220
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
233
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
221
234
  expiresInSeconds?: number;
222
235
  /** Object key (path-style; not URL-encoded by the caller). */
223
236
  key: string;
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ interface R2S3Credentials {
20
20
  }
21
21
  /** Options for {@link Storage.getPresignedUrl}. */
22
22
  interface PresignedUrlOptions {
23
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
23
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
24
24
  expiresInSeconds?: number;
25
25
  /** HTTP method the URL authorizes. Default `GET`. */
26
26
  method?: "GET" | "PUT";
@@ -131,10 +131,10 @@ interface Storage {
131
131
  }) => Promise<string>;
132
132
  /**
133
133
  * Read a stored object's metadata (size, content-type, sha256, upload time,
134
- * custom metadata) without fetching its body. Returns `null` when the object
135
- * is absent. Backed by an R2 HEAD (`bucket.head`) when available, falling
136
- * back to a 0-length ranged `get()` otherwise. Mirrors Convex's
137
- * `ctx.storage.getMetadata`.
134
+ * custom metadata) without fetching its body, as a flat serializable shape.
135
+ * Returns `null` when the object is absent. A projection of
136
+ * {@link Storage.head}, so it makes the same single body-free read. Mirrors
137
+ * Convex's `ctx.storage.getMetadata`.
138
138
  */
139
139
  getMetadata: (key: string) => Promise<ObjectMetadata | null>;
140
140
  /**
@@ -147,6 +147,19 @@ interface Storage {
147
147
  getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
148
148
  getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
149
149
  getUrl: (key: string) => string;
150
+ /**
151
+ * Read an object's R2 metadata WITHOUT its body — `size` (the full object
152
+ * size), `etag`, `httpMetadata`, `checksums`, plus the `sha256`/`sha256Base64`
153
+ * projection `download()` adds. Returns `null` when the object is absent.
154
+ *
155
+ * Backed by an R2 HEAD when the binding exposes one, falling back to a
156
+ * 0-length ranged `get()` otherwise. Prefer this over `download()` whenever
157
+ * only the metadata is wanted — notably to resolve a `Range` header, where a
158
+ * plain `download()` starts a full-object body transfer that is then thrown
159
+ * away. {@link Storage.getMetadata} is the flat, serializable projection of
160
+ * the same read.
161
+ */
162
+ head: (key: string) => Promise<R2ObjectLike | null>;
150
163
  list: (prefix?: string, options?: ListOptions) => Promise<{
151
164
  cursor?: string;
152
165
  objects: R2ObjectLike[];
@@ -217,7 +230,7 @@ declare const createStorage: (options: LunoraStorageOptions) => Storage;
217
230
  interface PresignedUrlParams {
218
231
  /** R2 S3 API credentials + bucket/account. */
219
232
  credentials: R2S3Credentials;
220
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
233
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
221
234
  expiresInSeconds?: number;
222
235
  /** Object key (path-style; not URL-encoded by the caller). */
223
236
  key: string;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createBucketStorage as o}from"./packem_shared/createBucketStorage-BE3H6x-j.mjs";import{createStorage as a,scopeKey as i}from"./packem_shared/createStorage-BGxUJCQr.mjs";import{buildPresignedUrl as f}from"./packem_shared/buildPresignedUrl-pN4iJAXf.mjs";import{b as l,v as p}from"./packem_shared/signed-url-C_cjNfJG.mjs";export{f as buildPresignedUrl,l as buildSignedUrl,o as createBucketStorage,a as createStorage,i as scopeKey,p as verifySignedUrl};
1
+ import{createBucketStorage as o}from"./packem_shared/createBucketStorage-BE3H6x-j.mjs";import{createStorage as i,scopeKey as d}from"./packem_shared/createStorage-CucDv4aP.mjs";import{buildPresignedUrl as g}from"./packem_shared/buildPresignedUrl-BpGOczQ6.mjs";import{buildSignedUrl as p,verifySignedUrl as a}from"./packem_shared/buildSignedUrl-Cpw9-kvk.mjs";export{g as buildPresignedUrl,p as buildSignedUrl,o as createBucketStorage,i as createStorage,d as scopeKey,a as verifySignedUrl};
@@ -0,0 +1,4 @@
1
+ import{LunoraError as H}from"@lunora/errors";import{a as y,v as R}from"./internal-FPCXOi7X.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),a=await c(n,$),s=await c(a,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)}},N=async t=>{const{credentials:e,key:n}=t,a=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,r])=>[i(o),i(r)]).toSorted(z).map(([o,r])=>`${o}=${r}`).join("&"),h=[a,m,p,`host:${l}
2
+ `,"host","UNSIGNED-PAYLOAD"].join(`
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{N as buildPresignedUrl};
@@ -0,0 +1,5 @@
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-FPCXOi7X.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};
@@ -0,0 +1 @@
1
+ import{LunoraError as n}from"@lunora/errors";import{t as A,h as U,a as T}from"./internal-FPCXOi7X.mjs";import{buildPresignedUrl as p}from"./buildPresignedUrl-BpGOczQ6.mjs";import{buildSignedUrl as b}from"./buildSignedUrl-Cpw9-kvk.mjs";const w=1024,L=1e3,M=100,I=e=>{const a=new Uint8Array(e);let i="";for(const u of a)i+=String.fromCodePoint(u);return btoa(i)},S=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)}})},E=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}},N=(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)},c=e=>{if(typeof e!="string"||e.length===0)throw new n("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(e.length>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(U(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")},x=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`)}},C=(e,a)=>{c(e),c(a);const u=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(u.length>w)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return u},P=e=>{if(!e.bucket)throw new n("INTERNAL","@lunora/storage: `bucket` is required");const a=async(r,t,o={})=>{c(r),x(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=N(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&&S(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&&E(t)},d=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const o=t.limit??M,f=Math.min(Math.max(1,Math.floor(o)),L),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`${A(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),b({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{P as createStorage,C as scopeKey};
@@ -0,0 +1 @@
1
+ const y=(t,e)=>{if(t.size<e)return;const n=t.keys().next().value;n!==void 0&&t.delete(n)},d=(t,e,n,s)=>{const o=t.get(e);if(o!==void 0)return o;y(t,s);const r=n().catch(l=>{throw t.get(e)===r&&t.delete(e),l});return t.set(e,r),r},c=new TextEncoder,A=10080*60,h=/^[a-z][a-z0-9+\-.]*:\/\//i,u=Array.from({length:32},(t,e)=>e),a=new RegExp(`[${u.map(t=>String.fromCodePoint(t)).join("")}]`,"u"),f=t=>{const e=String.fromCodePoint(...t);return btoa(e).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},p=t=>{const e=t.replaceAll("-","+").replaceAll("_","/")+"===".slice((t.length+3)%4),n=atob(e),s=new Uint8Array(n.length);for(let o=0;o<n.length;o+=1)s[o]=n.codePointAt(o)??0;return s},C=64,S=new Map,i=async t=>d(S,t,async()=>crypto.subtle.importKey("raw",c.encode(t),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]),C),E=t=>{try{return new URL(t).host}catch{return t.replace(h,"").split("/")[0]??""}},_=t=>{if(a.test(t))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},w=t=>a.test(t),g=/^\/+$/u,b=t=>g.test(t),m=(t,e)=>{if(!Number.isFinite(t)||t<=0)return"expiresInSeconds must be a positive finite number";if(t>e){const n=` (${String(e/86400)} days)`;return`expiresInSeconds must not exceed ${String(e)} seconds${n}`}},H=async(t,e)=>{const n=await i(t),s=await crypto.subtle.sign("HMAC",n,c.encode(e));return f(new Uint8Array(s))},R=async(t,e,n)=>{const s=await i(t);return crypto.subtle.verify("HMAC",s,n,c.encode(e))},x=t=>{const e=new Uint8Array(t);let n="";for(const s of e)n+=s.toString(16).padStart(2,"0");return n},O=t=>{let e=t.length;for(;e>0&&t[e-1]==="/";)e-=1;return t.slice(0,e)};export{A as M,x as a,_ as b,R as c,E as e,p as f,w as h,b as i,H as s,O as t,m as v};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/storage",
3
- "version": "1.0.0-alpha.34",
3
+ "version": "1.0.0-alpha.36",
4
4
  "description": "R2-backed storage for Lunora: typed buckets and signed URLs",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@lunora/errors": "1.0.0-alpha.22",
54
- "@lunora/platform": "1.0.0-alpha.15",
54
+ "@lunora/platform": "1.0.0-alpha.17",
55
55
  "@visulima/storage": "2.0.0",
56
56
  "aws4fetch": "1.0.20"
57
57
  },
@@ -1,4 +0,0 @@
1
- import{a as $}from"./internal-B_cLiXkR.mjs";const E="auto",h="s3",y="AWS4-HMAC-SHA256",X=1,z=10080*60,g=900,d=new TextEncoder,M=(t,e)=>t[0]<e[0]?-1:t[0]>e[0]?1:0,c=t=>encodeURIComponent(t).replaceAll(/[!'()*]/gu,e=>`%${e.codePointAt(0)?.toString(16).toUpperCase()??""}`),R=t=>t.split("/").map(e=>c(e)).join("/"),f=async t=>$(await crypto.subtle.digest("SHA-256",d.encode(t))),o=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))},N=async(t,e)=>{const n=await o(d.encode(`AWS4${t}`),e),a=await o(n,E),s=await o(a,h);return o(s,"aws4_request")},_=t=>{const e=t.jurisdiction===void 0?"":`${t.jurisdiction}.`;return`${t.accountId}.${e}r2.cloudflarestorage.com`},O=t=>{const e=`${t.toISOString().replaceAll(/[:-]/gu,"").slice(0,15)}Z`;return{amzDate:e,dateStamp:e.slice(0,8)}},P=async t=>{const{credentials:e,key:n}=t,a=t.method??"GET",s=t.expiresInSeconds??g,w=Number.isFinite(s)?s:g,I=Math.min(Math.max(X,Math.floor(w)),z),u=_(e),D=new Date(t.now?.()??Date.now()),{amzDate:S,dateStamp:m}=O(D),A=`${m}/${E}/${h}/aws4_request`,l=`/${c(e.bucket)}/${R(n)}`,p=[["X-Amz-Algorithm",y],["X-Amz-Credential",`${e.accessKeyId}/${A}`],["X-Amz-Date",S],["X-Amz-Expires",I.toString()],["X-Amz-SignedHeaders","host"]].map(([i,r])=>[c(i),c(r)]).toSorted(M).map(([i,r])=>`${i}=${r}`).join("&"),H=[a,l,p,`host:${u}
2
- `,"host","UNSIGNED-PAYLOAD"].join(`
3
- `),x=[y,S,A,await f(H)].join(`
4
- `),C=await N(e.secretAccessKey,m),K=$(await o(C,x));return`https://${u}${l}?${p}&X-Amz-Signature=${K}`};export{P as buildPresignedUrl};
@@ -1 +0,0 @@
1
- import{b as d,v as l}from"./signed-url-C_cjNfJG.mjs";import"./internal-B_cLiXkR.mjs";export{d as buildSignedUrl,l as verifySignedUrl};
@@ -1 +0,0 @@
1
- import{LunoraError as s}from"@lunora/errors";import{b as I,h as A}from"./signed-url-C_cjNfJG.mjs";import{t as U,a as T}from"./internal-B_cLiXkR.mjs";import{buildPresignedUrl as p}from"./buildPresignedUrl-pN4iJAXf.mjs";const w=1024,b=1e3,L=100,R=e=>{const a=new Uint8Array(e);let i="";for(const u of a)i+=String.fromCodePoint(u);return btoa(i)},M=e=>{const a=e.checksums?.sha256;if(a===void 0)return e;const i=T(a),u=R(a);return new Proxy(e,{get(l,o){if(o==="sha256")return i;if(o==="sha256Base64")return u;const d=Reflect.get(l,o,l);return typeof d=="function"?d.bind(l):d},has(l,o){return o==="sha256"||o==="sha256Base64"||Reflect.has(l,o)}})},y=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()}},S=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:R(a),size:e.size,uploaded:e.uploaded}},E=(e,a)=>{let i=0;const u=o=>o instanceof ArrayBuffer||ArrayBuffer.isView(o)?o.byteLength:void 0,l=new TransformStream({transform(o,d){const m=u(o);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(o)}});return e.pipeThrough(l)},c=e=>{if(typeof e!="string"||e.length===0)throw new s("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(e.length>w)throw new s("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(w)}-byte limit`);if(e.includes("\0"))throw new s("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(A(e))throw new s("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new s("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");if(e.split("/").includes(".."))throw new s("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},N=e=>{if(e.allowedContentTypes!==void 0){if(e.contentType===void 0)throw new s("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!e.allowedContentTypes.includes(e.contentType))throw new s("VALIDATION_ERROR",`@lunora/storage: contentType "${e.contentType}" not in allowedContentTypes`)}},$=(e,a)=>{c(e),c(a);const u=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(u.length>w)throw new s("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return u},C=e=>{if(!e.bucket)throw new s("INTERNAL","@lunora/storage: `bucket` is required");const a=async(r,t,n={})=>{c(r),N(n);let f=t;if(typeof n.maxSize=="number"){let h;if(t instanceof ArrayBuffer?h=t.byteLength:t instanceof Blob&&(h=t.size),h!==void 0&&h>n.maxSize)throw new s("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(h)} > ${String(n.maxSize)})`);t instanceof ReadableStream&&(f=E(t,n.maxSize))}const g=await e.bucket.put(r,f,{customMetadata:n.customMetadata,httpMetadata:n.contentType?{contentType:n.contentType}:void 0,...n.sha256===void 0?{}:{sha256:n.sha256}});return{etag:g.etag,httpEtag:g.httpEtag??`"${g.etag}"`,key:g.key}},i=async(r,t={})=>{c(r);const n=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return n&&M(n)},u=async r=>{c(r),await e.bucket.delete(r)},l=async r=>{if(c(r),e.bucket.head){const n=await e.bucket.head(r);return n&&y(n)}const t=await e.bucket.get(r,{range:{length:0}});return t&&y(t)},o=async(r,t={})=>{if(r?.includes("\0"))throw new s("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const n=t.limit??L,f=Math.min(Math.max(1,Math.floor(n)),b),g=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:f,prefix:r});return{cursor:g.cursor,objects:g.objects.map(h=>S(h)),truncated:g.truncated}},d=r=>{if(!e.publicBaseUrl)throw new s("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(n=>encodeURIComponent(n)).join("/");return`${U(e.publicBaseUrl)}/${t}`},m=async(r,t={})=>{if(!e.publicBaseUrl)throw new s("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new s("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return c(r),I({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 s("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={})=>m(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"}),getMetadata:l,getPresignedUrl:async(r,t={})=>{if(!e.s3)throw new s("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:m,getUrl:d,list:o,resumeMultipartUpload:(r,t)=>{if(c(r),typeof t!="string"||t.length===0)throw new s("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new s("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:a,upload:a}};export{C as createStorage,$ as scopeKey};
@@ -1 +0,0 @@
1
- const o=e=>{const t=new Uint8Array(e);let n="";for(const r of t)n+=r.toString(16).padStart(2,"0");return n},s=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)};export{o as a,s as t};
@@ -1,5 +0,0 @@
1
- import{LunoraError as h}from"@lunora/errors";import{t as E}from"./internal-B_cLiXkR.mjs";const S=(e,t)=>{if(e.size<t)return;const n=e.keys().next().value;n!==void 0&&e.delete(n)},y=new TextEncoder,f=10080*60,g=/^[a-z][a-z0-9+\-.]*:\/\//i,C=Array.from({length:32},(e,t)=>t),p=new RegExp(`[${C.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),_=e=>{const t=String.fromCodePoint(...e);return btoa(t).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},U=e=>{const t=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),n=atob(t),o=new Uint8Array(n.length);for(let r=0;r<n.length;r+=1)o[r]=n.codePointAt(r)??0;return o},x=64,u=new Map,R=async e=>{const t=u.get(e);if(t)return t;S(u,x);const n=crypto.subtle.importKey("raw",y.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return u.set(e,n),n},v=e=>{try{return new URL(e).host}catch{return e.replace(g,"").split("/")[0]??""}},w=e=>{if(p.test(e))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},P=e=>p.test(e),L=async(e,t)=>{const n=await R(e),o=await crypto.subtle.sign("HMAC",n,y.encode(t));return _(new Uint8Array(o))},I=async(e,t,n)=>{const o=await R(e);return crypto.subtle.verify("HMAC",o,n,y.encode(t))},T=/^\//,$=/^\/+$/u,A=(e,t,n,o,r)=>{const a=`${e}
2
- ${t.toLowerCase()}
3
- ${n}
4
- ${String(o)}`;return r===void 0?a:`${a}
5
- ${r}`},k=async e=>{const t=e.method??"GET",n=e.expiresInSeconds??3600;if(!Number.isFinite(n)||n<=0)throw new h("VALIDATION_ERROR","@lunora/storage: expiresInSeconds must be a positive finite number");if(n>f)throw new h("VALIDATION_ERROR",`@lunora/storage: expiresInSeconds must not exceed ${String(f)} (7 days)`);const o=t==="PUT"?e.contentType:void 0;let r="";try{r=new URL(e.baseUrl).pathname}catch{}if(r!==""&&!$.test(r))throw new h("VALIDATION_ERROR",`@lunora/storage: baseUrl must not carry a path ("${r}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);w(e.key);const a=Math.floor(Date.now()/1e3)+n,i=v(e.baseUrl),s=await L(e.secret,A(t,i,e.key,a,o)),l=E(e.baseUrl),c=e.key.split("/").map(m=>encodeURIComponent(m)).join("/"),d=o===void 0?"":`&ct=${encodeURIComponent(o)}`;return`${l}/${c}?exp=${String(a)}&method=${t}&sig=${s}${d}`},D=async(e,t,n)=>{let o;try{o=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const r=o.searchParams.get("exp"),a=r===null?Number.NaN:Number(r),i=o.searchParams.get("sig"),s=o.searchParams.get("method")??"GET",l=o.searchParams.get("ct")??void 0;if(!i||!Number.isInteger(a))return{reason:"malformed",valid:!1};if(a<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(s!=="GET"&&s!=="PUT")return{reason:"malformed",valid:!1};let c,d;try{c=o.pathname.replace(T,"").split("/").map(b=>decodeURIComponent(b)).join("/"),w(c),d=U(i)}catch{return{reason:"malformed",valid:!1}}const m=n?.expectedHost===void 0?o.host:v(n.expectedHost);return await I(t,A(s,m,c,a,l),d)?{contentType:l,key:c,method:s,valid:!0}:{reason:"bad_signature",valid:!1}};export{k as b,P as h,D as v};