@lunora/storage 1.0.0-alpha.28 → 1.0.0-alpha.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createBucketStorage as o}from"./packem_shared/createBucketStorage-DAip6cEw.mjs";import{createStorage as a,scopeKey as i}from"./packem_shared/createStorage-DBrCSt6G.mjs";import{buildPresignedUrl as f}from"./packem_shared/buildPresignedUrl-TCv-5TAT.mjs";import{b as l,v as p}from"./packem_shared/signed-url-Dm2EFS18.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 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};
@@ -0,0 +1 @@
1
+ import{Rest as u,Multipart as l,Tus as h}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as p}from"@visulima/storage/provider/aws-light";const m=100*1024*1024,g="1.0.0",i=(e,t,r)=>{const n={"content-type":"application/json"};return e==="tus"&&(n["Tus-Resumable"]=g),Response.json({error:r},{headers:n,status:t})},s=e=>i(e,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),f=e=>i(e,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),S=(e,t)=>{if(t==="multipart")return;const r=e.headers.get("Upload-Length")??e.headers.get("Content-Length");if(r===null)return;const n=Number(r);return Number.isFinite(n)?n:void 0},R=(e,t)=>e==="chunked-rest"?new u(t):e==="multipart"?new l(t):new h(t),E=e=>{const t=e.protocol??"tus",r=e.maxFileSize??m,n={maxFileSize:r,storage:e.storage},d=R(t,n),{authorize:o}=e;return o===void 0&&!e.silent&&!e.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open."),{fetch:async a=>{const c=S(a,t);if(c!==void 0&&c>r)return f(t);if(o!==void 0)try{if(!await o({method:a.method,protocol:t,request:a,url:new URL(a.url)}))return s(t)}catch{return s(t)}return d.fetch(a)},protocol:t}},b=e=>new p({accessKeyId:e.accessKeyId,bucket:e.bucket,endpoint:e.endpoint??`https://${e.accountId}.r2.cloudflarestorage.com`,path:e.path??"/",region:"auto",secretAccessKey:e.secretAccessKey,...e.partSize===void 0?{}:{partSize:e.partSize}});export{m as DEFAULT_MAX_UPLOAD_BYTES,b as createR2UploadStorage,E as createUploadHandler};
@@ -0,0 +1,4 @@
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};
@@ -0,0 +1 @@
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};
@@ -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??"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};
@@ -0,0 +1 @@
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 +1 @@
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},a=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)};export{o as a,a as t};
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};
@@ -0,0 +1,5 @@
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};
package/dist/upload.d.mts CHANGED
@@ -115,4 +115,58 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
115
115
  declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
116
116
  secretAccessKey: string;
117
117
  }) => AwsLightStorage;
118
- export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
118
+ export {
119
+ /**
120
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
121
+ *
122
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
123
+ * `@visulima/storage` handler dependency only loads for apps that mount an
124
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
125
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
126
+ */
127
+ type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
128
+ /**
129
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
130
+ *
131
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
132
+ * `@visulima/storage` handler dependency only loads for apps that mount an
133
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
134
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
135
+ */
136
+ type R2UploadStorageOptions,
137
+ /**
138
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
139
+ *
140
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
141
+ * `@visulima/storage` handler dependency only loads for apps that mount an
142
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
143
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
144
+ */
145
+ type UploadAuthzContext,
146
+ /**
147
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
148
+ *
149
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
150
+ * `@visulima/storage` handler dependency only loads for apps that mount an
151
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
152
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
153
+ */
154
+ type UploadHandler,
155
+ /**
156
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
157
+ *
158
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
159
+ * `@visulima/storage` handler dependency only loads for apps that mount an
160
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
161
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
162
+ */
163
+ type UploadProtocol,
164
+ /**
165
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
166
+ *
167
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
168
+ * `@visulima/storage` handler dependency only loads for apps that mount an
169
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
170
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
171
+ */
172
+ type UploadStorage, createR2UploadStorage, createUploadHandler };
package/dist/upload.d.ts CHANGED
@@ -115,4 +115,58 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
115
115
  declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
116
116
  secretAccessKey: string;
117
117
  }) => AwsLightStorage;
118
- export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
118
+ export {
119
+ /**
120
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
121
+ *
122
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
123
+ * `@visulima/storage` handler dependency only loads for apps that mount an
124
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
125
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
126
+ */
127
+ type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
128
+ /**
129
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
130
+ *
131
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
132
+ * `@visulima/storage` handler dependency only loads for apps that mount an
133
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
134
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
135
+ */
136
+ type R2UploadStorageOptions,
137
+ /**
138
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
139
+ *
140
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
141
+ * `@visulima/storage` handler dependency only loads for apps that mount an
142
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
143
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
144
+ */
145
+ type UploadAuthzContext,
146
+ /**
147
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
148
+ *
149
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
150
+ * `@visulima/storage` handler dependency only loads for apps that mount an
151
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
152
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
153
+ */
154
+ type UploadHandler,
155
+ /**
156
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
157
+ *
158
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
159
+ * `@visulima/storage` handler dependency only loads for apps that mount an
160
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
161
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
162
+ */
163
+ type UploadProtocol,
164
+ /**
165
+ * `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
166
+ *
167
+ * A separate subpath so the base `@lunora/storage` entry stays lean: the
168
+ * `@visulima/storage` handler dependency only loads for apps that mount an
169
+ * end-user upload endpoint. Pair with `@visulima/storage-client` (or the
170
+ * `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
171
+ */
172
+ type UploadStorage, createR2UploadStorage, createUploadHandler };
package/dist/upload.mjs CHANGED
@@ -1 +1 @@
1
- import{DEFAULT_MAX_UPLOAD_BYTES as r,createR2UploadStorage as o,createUploadHandler as t}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-CeVvqDXZ.mjs";export{r as DEFAULT_MAX_UPLOAD_BYTES,o as createR2UploadStorage,t as createUploadHandler};
1
+ import{DEFAULT_MAX_UPLOAD_BYTES as r,createR2UploadStorage as o,createUploadHandler as t}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-ByeD2zWk.mjs";export{r as DEFAULT_MAX_UPLOAD_BYTES,o as createR2UploadStorage,t as createUploadHandler};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/storage",
3
- "version": "1.0.0-alpha.28",
3
+ "version": "1.0.0-alpha.29",
4
4
  "description": "R2-backed storage for Lunora: typed buckets and signed URLs",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -52,7 +52,7 @@
52
52
  "dependencies": {
53
53
  "@lunora/errors": "1.0.0-alpha.21",
54
54
  "@lunora/platform": "1.0.0-alpha.10",
55
- "@visulima/storage": "1.0.5",
55
+ "@visulima/storage": "2.0.0",
56
56
  "aws4fetch": "1.0.20"
57
57
  },
58
58
  "engines": {
@@ -1 +0,0 @@
1
- import{Rest as d,Multipart as l,Tus as p}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as h}from"@visulima/storage/provider/aws-light";const m=100*1024*1024,g="1.0.0",c=(e,t,r)=>{const o={"content-type":"application/json"};return e==="tus"&&(o["Tus-Resumable"]=g),Response.json({error:r},{headers:o,status:t})},i=e=>c(e,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),f=e=>c(e,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),b=(e,t)=>{if(t==="multipart")return;const r=e.headers.get("Upload-Length")??e.headers.get("Content-Length");if(r===null)return;const o=Number(r);return Number.isFinite(o)?o:void 0},S=(e,t)=>e==="chunked-rest"?new d(t):e==="multipart"?new l(t):new p(t),z=e=>{const t=e.protocol??"tus",r=e.maxFileSize??m,o={maxFileSize:r,storage:e.storage},u=S(t,o),{authorize:n}=e;return n===void 0&&!e.silent&&!e.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open."),{fetch:async a=>{const s=b(a,t);if(s!==void 0&&s>r)return f(t);if(n!==void 0)try{if(!await n({method:a.method,protocol:t,request:a,url:new URL(a.url)}))return i(t)}catch{return i(t)}return u.fetch(a)},protocol:t}},E=e=>new h({accessKeyId:e.accessKeyId,bucket:e.bucket,endpoint:e.endpoint??`https://${e.accountId}.r2.cloudflarestorage.com`,path:e.path??"/",region:"auto",secretAccessKey:e.secretAccessKey,...e.partSize===void 0?{}:{partSize:e.partSize}});export{m as DEFAULT_MAX_UPLOAD_BYTES,E as createR2UploadStorage,z as createUploadHandler};
@@ -1,4 +0,0 @@
1
- import{a as g}from"./internal-DpjlEJPE.mjs";const h="auto",y="s3",S="AWS4-HMAC-SHA256",M=1,X=10080*60,w=900,d=new TextEncoder,j=(e,t)=>e[0]<t[0]?-1:e[0]>t[0]?1:0,s=e=>encodeURIComponent(e).replaceAll(/[!'()*]/gu,t=>`%${t.codePointAt(0)?.toString(16).toUpperCase()??""}`),f=e=>e.split("/").map(t=>s(t)).join("/"),U=async e=>g(await crypto.subtle.digest("SHA-256",d.encode(e))),n=async(e,t)=>{const a=await crypto.subtle.importKey("raw",e,{hash:"SHA-256",name:"HMAC"},!1,["sign"]);return crypto.subtle.sign("HMAC",a,d.encode(t))},E=async(e,t)=>{const a=await n(d.encode(`AWS4${e}`),t),r=await n(a,h),o=await n(r,y);return n(o,"aws4_request")},K=e=>{const t=e.jurisdiction===void 0?"":`${e.jurisdiction}.`;return`${e.accountId}.${t}r2.cloudflarestorage.com`},P=e=>{const t=`${e.toISOString().replaceAll(/[:-]/gu,"").slice(0,15)}Z`;return{amzDate:t,dateStamp:t.slice(0,8)}},k=async e=>{const{credentials:t,key:a}=e,r=e.method??"GET",o=e.expiresInSeconds??w,z=Number.isFinite(o)?o:w,H=Math.min(Math.max(M,Math.floor(z)),X),m=K(t),D=new Date(e.now?.()??Date.now()),{amzDate:l,dateStamp:u}=P(D),A=`${u}/${h}/${y}/aws4_request`,p=`/${s(t.bucket)}/${f(a)}`,$=[["X-Amz-Algorithm",S],["X-Amz-Credential",`${t.accessKeyId}/${A}`],["X-Amz-Date",l],["X-Amz-Expires",H.toString()],["X-Amz-SignedHeaders","host"]].map(([i,c])=>[s(i),s(c)]).toSorted(j).map(([i,c])=>`${i}=${c}`).join("&"),b=[r,p,$,`host:${m}
2
- `,"host","UNSIGNED-PAYLOAD"].join(`
3
- `),x=[S,l,A,await U(b)].join(`
4
- `),C=await E(t.secretAccessKey,u),I=g(await n(C,x));return`https://${m}${p}?${$}&X-Amz-Signature=${I}`};export{k as buildPresignedUrl};
@@ -1 +0,0 @@
1
- import"@lunora/errors";import{b as d,v as l}from"./signed-url-Dm2EFS18.mjs";import"./internal-DpjlEJPE.mjs";export{d as buildSignedUrl,l as verifySignedUrl};
@@ -1 +0,0 @@
1
- import{LunoraError as u}from"@lunora/errors";const d=(t,o={})=>{const r=Object.keys(t),[n]=r;if(n===void 0)throw new u("INTERNAL","@lunora/storage: createBucketStorage requires at least one bucket");if(o.default!==void 0&&!t[o.default])throw new u("INTERNAL",`@lunora/storage: default bucket "${o.default}" is not in the bucket map (have: ${r.join(", ")})`);const e=o.default??"default",c=t[e]??t[n];if(c===void 0)throw new u("INTERNAL",`@lunora/storage: default bucket "${e}" is not in the bucket map (have: ${r.join(", ")})`);const f=[...new Set([e,...r])],i=a=>{const s=a===e?c:t[a];if(!s)throw new u("INTERNAL",`@lunora/storage: no bucket registered for "${a}". Known buckets: ${f.join(", ")}`);return{...s,bucket:k=>i(k),bucketName:a}};return i(e)};export{d as createBucketStorage};
@@ -1 +0,0 @@
1
- import{LunoraError as n}from"@lunora/errors";import{b as R,d as T}from"./signed-url-Dm2EFS18.mjs";import{t as k,a as f}from"./internal-DpjlEJPE.mjs";import{buildPresignedUrl as I}from"./buildPresignedUrl-TCv-5TAT.mjs";const m=1024,A=1e3,U=100,b=e=>{const o=new Uint8Array(e);let i="";for(const p of o)i+=String.fromCodePoint(p);return btoa(i)},S=e=>{const o=e.checksums?.sha256;if(o===void 0)return e;const i=f(o),p=b(o);return new Proxy(e,{get(u,s){if(s==="sha256")return i;if(s==="sha256Base64")return p;const d=Reflect.get(u,s,u);return typeof d=="function"?d.bind(u):d},has(u,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(u,s)}})},w=e=>{const o=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:o===void 0?void 0:f(o),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},E=e=>{const o=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:o===void 0?void 0:f(o),sha256Base64:o===void 0?void 0:b(o),size:e.size,uploaded:e.uploaded}},M=(e,o)=>{let i=0;const p=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,u=new TransformStream({transform(s,d){const h=p(s);if(h===void 0){d.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(i+=h,i>o){d.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(o)} bytes)`));return}d.enqueue(s)}});return e.pipeThrough(u)},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>m)throw new n("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(m)}-byte limit`);if(e.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(T(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")},N=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`)}},B=(e,o)=>{c(e),c(o);const i=`${e.endsWith("/")?e.slice(0,-1):e}/${o}`;if(i.length>m)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(m)}-byte limit`);return i},_=e=>{if(!e.bucket)throw new n("INTERNAL","@lunora/storage: `bucket` is required");const o=async(r,t,a={})=>{c(r),N(a);let y=t;if(typeof a.maxSize=="number"){let g;if(t instanceof ArrayBuffer?g=t.byteLength:t instanceof Blob&&(g=t.size),g!==void 0&&g>a.maxSize)throw new n("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(g)} > ${String(a.maxSize)})`);t instanceof ReadableStream&&(y=M(t,a.maxSize))}const l=await e.bucket.put(r,y,{customMetadata:a.customMetadata,httpMetadata:a.contentType?{contentType:a.contentType}:void 0,...a.sha256===void 0?{}:{sha256:a.sha256}});return{etag:l.etag,httpEtag:l.httpEtag??`"${l.etag}"`,key:l.key}},i=async(r,t={})=>{c(r);const a=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return a&&S(a)},p=async r=>{c(r),await e.bucket.delete(r)},u=async r=>{if(c(r),e.bucket.head){const a=await e.bucket.head(r);return a&&w(a)}const t=await e.bucket.get(r,{range:{length:0}});return t&&w(t)},s=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const a=t.limit??U,y=Math.min(Math.max(1,Math.floor(a)),A),l=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:y,prefix:r});return{cursor:l.cursor,objects:l.objects.map(g=>E(g)),truncated:l.truncated}},d=r=>{if(!e.publicBaseUrl)throw new n("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(a=>encodeURIComponent(a)).join("/");return`${k(e.publicBaseUrl)}/${t}`},h=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),R({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:p,download:i,generateUploadUrl:async(r,t={})=>h(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"}),getMetadata:u,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),I({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},getSignedUrl:h,getUrl:d,list:s,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:o,upload:o}};export{_ as createStorage,B as scopeKey};
@@ -1,5 +0,0 @@
1
- import{LunoraError as u}from"@lunora/errors";import{t as R}from"./internal-DpjlEJPE.mjs";const U=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},f=new TextEncoder,h=10080*60,$=/^[a-z][a-z0-9+\-.]*:\/\//i,x=Array.from({length:32},(e,r)=>r),y=new RegExp(`[${x.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),I=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},L=e=>{const r=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),t=atob(r),n=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)n[a]=t.codePointAt(a)??0;return n},T=64,p=new Map,g=async e=>{const r=p.get(e);if(r)return r;U(p,T);const t=crypto.subtle.importKey("raw",f.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return p.set(e,t),t},w=e=>{try{return new URL(e).host}catch{return e.replace($,"").split("/")[0]??""}},v=e=>{if(y.test(e))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},H=e=>y.test(e),S=async(e,r)=>{const t=await g(e),n=await crypto.subtle.sign("HMAC",t,f.encode(r));return I(new Uint8Array(n))},C=async(e,r,t)=>{const n=await g(e);return crypto.subtle.verify("HMAC",n,t,f.encode(r))},E=/^\//,N=/^\/+$/u,b=(e,r,t,n,a)=>{const o=`${e}
2
- ${r.toLowerCase()}
3
- ${t}
4
- ${String(n)}`;return a===void 0?o:`${o}
5
- ${a}`},M=async e=>{const r=e.method??"GET",t=e.expiresInSeconds??3600;if(!Number.isFinite(t)||t<=0)throw new u("VALIDATION_ERROR","@lunora/storage: expiresInSeconds must be a positive finite number");if(t>h)throw new u("VALIDATION_ERROR",`@lunora/storage: expiresInSeconds must not exceed ${String(h)} (7 days)`);const n=r==="PUT"?e.contentType:void 0;let a="";try{a=new URL(e.baseUrl).pathname}catch{}if(a!==""&&!N.test(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`);v(e.key);const o=Math.floor(Date.now()/1e3)+t,l=w(e.baseUrl),s=await S(e.secret,b(r,l,e.key,o,n)),c=R(e.baseUrl),i=e.key.split("/").map(m=>encodeURIComponent(m)).join("/"),d=n===void 0?"":`&ct=${encodeURIComponent(n)}`;return`${c}/${i}?exp=${String(o)}&method=${r}&sig=${s}${d}`},O=async(e,r,t)=>{let n;try{n=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const a=n.searchParams.get("exp"),o=a===null?Number.NaN:Number(a),l=n.searchParams.get("sig"),s=n.searchParams.get("method")??"GET",c=n.searchParams.get("ct")??void 0;if(!l||!Number.isInteger(o))return{reason:"malformed",valid:!1};if(o<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(s!=="GET"&&s!=="PUT")return{reason:"malformed",valid:!1};let i,d;try{i=n.pathname.replace(E,"").split("/").map(A=>decodeURIComponent(A)).join("/"),v(i),d=L(l)}catch{return{reason:"malformed",valid:!1}}const m=t?.expectedHost===void 0?n.host:w(t.expectedHost);return await C(r,b(s,m,i,o,c),d)?{contentType:c,key:i,method:s,valid:!0}:{reason:"bad_signature",valid:!1}};export{M as b,H as d,O as v};