@lunora/storage 1.0.0-alpha.47 → 1.0.0-alpha.49

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
@@ -75,13 +75,15 @@ export default defineApp<Env>()
75
75
  import { action, v } from "@/lunora/_generated/server";
76
76
 
77
77
  // Minting an upload URL is a write capability, so this is an `action`.
78
- // Queries and mutations get a read-only `ctx.storage`; the full surface
79
- // (generateUploadUrl/store/delete/multipart) is action-only.
78
+ // Queries and mutations are TYPED with a read-only `ctx.storage`; the full
79
+ // surface (generateUploadUrl/store/delete/getPresignedUrl) is action-only.
80
+ // Multipart and `list` are on a `createStorage` instance, not on any ctx.
80
81
  export const uploadAvatar = action.input({ contentType: v.string() }).action(async ({ args, ctx }) => {
81
82
  const key = `avatars/${ctx.auth.userId ?? "anonymous"}/profile`;
82
83
 
83
- // Short-lived signed PUT URL — the client uploads straight to R2 through
84
- // your Worker route, with the Content-Type pinned into the signature.
84
+ // Short-lived signed PUT URL — the client uploads to your Worker route,
85
+ // which verifies the signature and writes to R2, with the Content-Type
86
+ // pinned into that signature.
85
87
  const url = await ctx.storage.generateUploadUrl(key, { contentType: args.contentType, expiresInSeconds: 60 });
86
88
 
87
89
  return { key, url };
@@ -115,7 +117,7 @@ const url = await storage.getSignedUrl("uploads/avatar.png", { expiresInSeconds:
115
117
  ## Related
116
118
 
117
119
  - [`@lunora/server`](https://www.npmjs.com/package/@lunora/server) — call storage from queries, mutations, and actions.
118
- - [`@lunora/runtime`](https://www.npmjs.com/package/@lunora/runtime) — the Worker runtime that serves gated `GET /storage/:key` routes.
120
+ - [`@lunora/runtime`](https://www.npmjs.com/package/@lunora/runtime) — the Worker runtime your signed-URL route sits in front of. It ships no public storage route of its own: the route that verifies a signed URL and moves the bytes is yours to add (see the `storage` registry item).
119
121
  - [`@lunora/d1`](https://www.npmjs.com/package/@lunora/d1) — store object metadata alongside your data.
120
122
 
121
123
  ## Supported Node.js Versions
package/dist/index.d.mts CHANGED
@@ -114,6 +114,17 @@ interface ObjectMetadata {
114
114
  uploaded?: number;
115
115
  }
116
116
  interface Storage {
117
+ /**
118
+ * The bucket name this accessor operates under — the same value
119
+ * {@link Storage.getSignedUrl} puts into the HMAC canonical.
120
+ *
121
+ * Exposed so everything downstream agrees on one name: `asBucketStorage`
122
+ * tags a single-bucket storage with it, and `storageRules(...)` matches
123
+ * `(bucket, operation)` rules against that tag. Without it a
124
+ * `createStorage({ bucketName: "avatars" })` signed URLs as `avatars` while
125
+ * the rules engine only ever saw `"default"`.
126
+ */
127
+ readonly bucketName: string;
117
128
  /**
118
129
  * Begin a native R2 **multipart upload** for very large objects — upload
119
130
  * parts (each uniform in size except the last), then `complete` with the
package/dist/index.d.ts CHANGED
@@ -114,6 +114,17 @@ interface ObjectMetadata {
114
114
  uploaded?: number;
115
115
  }
116
116
  interface Storage {
117
+ /**
118
+ * The bucket name this accessor operates under — the same value
119
+ * {@link Storage.getSignedUrl} puts into the HMAC canonical.
120
+ *
121
+ * Exposed so everything downstream agrees on one name: `asBucketStorage`
122
+ * tags a single-bucket storage with it, and `storageRules(...)` matches
123
+ * `(bucket, operation)` rules against that tag. Without it a
124
+ * `createStorage({ bucketName: "avatars" })` signed URLs as `avatars` while
125
+ * the rules engine only ever saw `"default"`.
126
+ */
127
+ readonly bucketName: string;
117
128
  /**
118
129
  * Begin a native R2 **multipart upload** for very large objects — upload
119
130
  * parts (each uniform in size except the last), then `complete` with the
package/dist/index.mjs CHANGED
@@ -1 +1 @@
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};
1
+ import{createBucketStorage as o}from"./packem_shared/createBucketStorage-D9fBoaEi.mjs";import{createStorage as i,scopeKey as d}from"./packem_shared/createStorage-B-E6dH3Q.mjs";import{buildPresignedUrl as g}from"./packem_shared/buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as p,verifySignedUrl as a}from"./packem_shared/buildSignedUrl-czz1xjLU.mjs";export{g as buildPresignedUrl,p as buildSignedUrl,o as createBucketStorage,i as createStorage,d as scopeKey,a as verifySignedUrl};
@@ -1 +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};
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)})!==!0)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,6 @@
1
+ import{LunoraError as f}from"@lunora/errors";import{i as y,c as u,e as p,s as v,t as U,f as N,d as w,v as I,M as $}from"./internal-B5yyxF10.mjs";const k=/^\//,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=I(r,$);if(t!==void 0)throw new f("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!==""&&!y(a))throw new f("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(u(e.key),u(e.bucketName),e.bucketName==="")throw new f("VALIDATION_ERROR","@lunora/storage: bucketName must not be empty");if(e.key==="")throw new f("VALIDATION_ERROR","@lunora/storage: key must not be empty");const n=Math.floor(Date.now()/1e3)+r,c=p(e.baseUrl),i=await v(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(k,"").split("/").map(R=>decodeURIComponent(R)).join("/"),u(l),u(i),d=N(n)}catch{return{reason:"malformed",valid:!1}}const h=r?.expectedHost===void 0?t.host:p(r.expectedHost);return await w(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";import{t as U,h as M,a as I,b as T}from"./internal-B5yyxF10.mjs";import{buildPresignedUrl as p}from"./buildPresignedUrl-U55ktL8x.mjs";import{buildSignedUrl as k}from"./buildSignedUrl-czz1xjLU.mjs";const w=1024,x=1e3,_=100,v=e=>{const a=e.checksums?.sha256;if(a===void 0)return e;const h=T(a),l=I(new Uint8Array(a));return new Proxy(e,{get(i,s){if(s==="sha256")return h;if(s==="sha256Base64")return l;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)}})},B=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}},D=(e,a)=>{let h=0;const l=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,i=new TransformStream({transform(s,u){const m=l(s);if(m===void 0){u.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(h+=m,h>a){u.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(a)} bytes)`));return}u.enqueue(s)}});return e.pipeThrough(i)},z=new TextEncoder,A=e=>z.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(M(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 l=`${e.endsWith("/")?e.slice(0,-1):e}/${a}`;if(A(l)>w)throw new n("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(w)}-byte limit`);return l},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 g;if(t instanceof ArrayBuffer?g=t.byteLength:t instanceof Blob&&(g=t.size),g!==void 0&&g>c.maxSize)throw new n("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(g)} > ${String(c.maxSize)})`);t instanceof ReadableStream&&(f=D(t,c.maxSize))}const d=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:d.etag,httpEtag:d.httpEtag??`"${d.etag}"`,key:d.key}},h=async(r,t={})=>{o(r);const c=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return c&&v(c)},l=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&&B(t)},u=async(r,t={})=>{if(r?.includes("\0"))throw new n("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const c=t.limit??_,f=Math.min(Math.max(1,Math.floor(c)),x),d=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:f,prefix:r}),{delimitedPrefixes:g}=d;return{cursor:d.cursor,delimitedPrefixes:g,objects:d.objects.map(S=>R(S)),truncated:d.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),k({baseUrl:e.publicBaseUrl,bucketName:e.bucketName,contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method,secret:e.signingSecret})},b=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})},N=(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)},L=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),p({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},E=async(r,t={})=>y(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"});return{bucketName:e.bucketName,createMultipartUpload:b,delete:l,download:h,generateUploadUrl:E,getMetadata:s,getPresignedUrl:L,getSignedUrl:y,getUrl:m,head:i,list:u,resumeMultipartUpload:N,store:a,upload:a}};export{K as createStorage,O as scopeKey};
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-ByeD2zWk.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-Dhj02auN.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.47",
3
+ "version": "1.0.0-alpha.49",
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.29",
54
- "@lunora/platform": "1.0.0-alpha.24",
53
+ "@lunora/errors": "1.0.0-alpha.30",
54
+ "@lunora/platform": "1.0.0-alpha.25",
55
55
  "@visulima/storage": "2.0.0",
56
56
  "aws4fetch": "1.0.20"
57
57
  },
@@ -1,6 +0,0 @@
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};
@@ -1 +0,0 @@
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};