@lunora/storage 1.0.0-alpha.15 → 1.0.0-alpha.16

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 i,scopeKey as d}from"./packem_shared/createStorage-CWKguFEG.mjs";import{buildPresignedUrl as g}from"./packem_shared/buildPresignedUrl-TCv-5TAT.mjs";import{buildSignedUrl as p,verifySignedUrl as a}from"./packem_shared/buildSignedUrl-DocLLDGy.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-DAip6cEw.mjs";import{createStorage as a,scopeKey as i}from"./packem_shared/createStorage-BDKKnFcV.mjs";import{buildPresignedUrl as f}from"./packem_shared/buildPresignedUrl-TCv-5TAT.mjs";import{b as l,v as p}from"./packem_shared/signed-url-4m6WhCR1.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 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,u="1.0.0",i=e=>{const t={"content-type":"application/json"};return e==="tus"&&(t["Tus-Resumable"]=u),Response.json({error:{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}},{headers:t,status:403})},g=e=>{const t={"content-type":"application/json"};return e==="tus"&&(t["Tus-Resumable"]=u),Response.json({error:{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}},{headers:t,status:413})},f=(e,t)=>{if(t==="multipart")return;const o=e.headers.get("Upload-Length")??e.headers.get("Content-Length");if(o===null)return;const n=Number(o);return Number.isFinite(n)?n:void 0},R=(e,t)=>e==="chunked-rest"?new d(t):e==="multipart"?new l(t):new p(t),S=e=>{const t=e.protocol??"tus",o=e.maxFileSize??m,n={maxFileSize:o,storage:e.storage},c=R(t,n),{authorize:a}=e;return a===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 r=>{const s=f(r,t);if(s!==void 0&&s>o)return g(t);if(a!==void 0)try{if(!await a({method:r.method,protocol:t,request:r,url:new URL(r.url)}))return i(t)}catch{return i(t)}return c.fetch(r)},protocol:t}},z=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,z as createR2UploadStorage,S as createUploadHandler};
@@ -0,0 +1 @@
1
+ import"@lunora/errors";import{b as d,v as l}from"./signed-url-4m6WhCR1.mjs";import"./internal-DpjlEJPE.mjs";export{d as buildSignedUrl,l as verifySignedUrl};
@@ -0,0 +1 @@
1
+ import{LunoraError as a}from"@lunora/errors";import{b as R,d as T}from"./signed-url-4m6WhCR1.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 n=new Uint8Array(e);let i="";for(const p of n)i+=String.fromCodePoint(p);return btoa(i)},S=e=>{const n=e.checksums?.sha256;if(n===void 0)return e;const i=f(n),p=b(n);return new Proxy(e,{get(u,s){if(s==="sha256")return i;if(s==="sha256Base64")return p;const l=Reflect.get(u,s,u);return typeof l=="function"?l.bind(u):l},has(u,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(u,s)}})},w=e=>{const n=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:n===void 0?void 0:f(n),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},M=e=>{const n=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:n===void 0?void 0:f(n),sha256Base64:n===void 0?void 0:b(n),size:e.size,uploaded:e.uploaded}},N=(e,n)=>{let i=0;const p=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,u=new TransformStream({transform(s,l){const h=p(s);if(h===void 0){l.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(i+=h,i>n){l.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(n)} bytes)`));return}l.enqueue(s)}});return e.pipeThrough(u)},c=e=>{if(typeof e!="string"||e.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(e.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(m)}-byte limit`);if(e.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(T(e))throw new a("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new a("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");const n=e.split("/");for(const i of n)if(i==="..")throw new a("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},v=(e,n)=>{c(e),c(n);const i=`${e.endsWith("/")?e.slice(0,-1):e}/${n}`;if(i.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(m)}-byte limit`);return i},B=e=>{if(!e.bucket)throw new a("INTERNAL","@lunora/storage: `bucket` is required");const n=async(r,t,o={})=>{if(c(r),o.allowedContentTypes!==void 0){if(o.contentType===void 0)throw new a("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!o.allowedContentTypes.includes(o.contentType))throw new a("VALIDATION_ERROR",`@lunora/storage: contentType "${o.contentType}" not in allowedContentTypes`)}let y=t;if(typeof o.maxSize=="number"){let g;if(t instanceof ArrayBuffer?g=t.byteLength:t instanceof Blob&&(g=t.size),g!==void 0&&g>o.maxSize)throw new a("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(g)} > ${String(o.maxSize)})`);t instanceof ReadableStream&&(y=N(t,o.maxSize))}const d=await e.bucket.put(r,y,{customMetadata:o.customMetadata,httpMetadata:o.contentType?{contentType:o.contentType}:void 0});return{etag:d.etag,httpEtag:d.httpEtag??`"${d.etag}"`,key:d.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)},p=async r=>{c(r),await e.bucket.delete(r)},u=async r=>{if(c(r),e.bucket.head){const o=await e.bucket.head(r);return o&&w(o)}const t=await e.bucket.get(r,{range:{length:0}});return t&&w(t)},s=async(r,t={})=>{if(r?.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const o=t.limit??U,y=Math.min(Math.max(1,Math.floor(o)),A),d=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:y,prefix:r});return{cursor:d.cursor,objects:d.objects.map(g=>M(g)),truncated:d.truncated}},l=r=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(o=>encodeURIComponent(o)).join("/");return`${k(e.publicBaseUrl)}/${t}`},h=async(r,t={})=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new a("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 a("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 a("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:l,list:s,resumeMultipartUpload:(r,t)=>{if(c(r),typeof t!="string"||t.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new a("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:async(r,t,o={})=>n(r,t,o),upload:n}};export{B as createStorage,v as scopeKey};
@@ -0,0 +1,5 @@
1
+ import{LunoraError as p}from"@lunora/errors";import{t as b}from"./internal-DpjlEJPE.mjs";const R=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},u=new TextEncoder,f=10080*60,U=/^[a-z][a-z0-9+\-.]*:\/\//i,x=Array.from({length:32},(e,r)=>r),g=new RegExp(`[${x.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),T=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},S=e=>{const r=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),t=atob(r),n=new Uint8Array(t.length);for(let o=0;o<t.length;o+=1)n[o]=t.codePointAt(o)??0;return n},C=64,m=new Map,h=async e=>{const r=m.get(e);if(r)return r;R(m,C);const t=crypto.subtle.importKey("raw",u.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return m.set(e,t),t},y=e=>{try{return new URL(e).host}catch{return e.replace(U,"").split("/")[0]??""}},w=e=>{if(g.test(e))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},H=e=>g.test(e),I=async(e,r)=>{const t=await h(e),n=await crypto.subtle.sign("HMAC",t,u.encode(r));return T(new Uint8Array(n))},E=async(e,r,t)=>{const n=await h(e);return crypto.subtle.verify("HMAC",n,t,u.encode(r))},P=/^\//,v=(e,r,t,n,o)=>{const a=`${e}
2
+ ${r.toLowerCase()}
3
+ ${t}
4
+ ${String(n)}`;return o===void 0?a:`${a}
5
+ ${o}`},M=async e=>{const r=e.method??"GET",t=e.expiresInSeconds??3600;if(!Number.isFinite(t)||t<=0)throw new p("VALIDATION_ERROR","@lunora/storage: expiresInSeconds must be a positive finite number");if(t>f)throw new p("VALIDATION_ERROR",`@lunora/storage: expiresInSeconds must not exceed ${String(f)} (7 days)`);const n=r==="PUT"?e.contentType:void 0;w(e.key);const o=Math.floor(Date.now()/1e3)+t,a=y(e.baseUrl),c=await I(e.secret,v(r,a,e.key,o,n)),s=b(e.baseUrl),l=e.key.split("/").map(d=>encodeURIComponent(d)).join("/"),i=n===void 0?"":`&ct=${encodeURIComponent(n)}`;return`${s}/${l}?exp=${String(o)}&method=${r}&sig=${c}${i}`},k=async(e,r,t)=>{let n;try{n=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const o=n.searchParams.get("exp"),a=o===null?Number.NaN:Number(o),c=n.searchParams.get("sig"),s=n.searchParams.get("method")??"GET",l=n.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(s!=="GET"&&s!=="PUT")return{reason:"malformed",valid:!1};let i,d;try{i=n.pathname.replace(P,"").split("/").map($=>decodeURIComponent($)).join("/"),w(i),d=S(c)}catch{return{reason:"malformed",valid:!1}}const A=t?.expectedHost===void 0?n.host:y(t.expectedHost);return await E(r,v(s,A,i,a,l),d)?{contentType:l,key:i,method:s,valid:!0}:{reason:"bad_signature",valid:!1}};export{M as b,H as d,k as v};
package/dist/upload.d.mts CHANGED
@@ -2,6 +2,13 @@ import { Tus } from '@visulima/storage/handler/http/fetch';
2
2
  import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
3
3
  /** Resumable upload wire protocols the handler can speak. */
4
4
  type UploadProtocol = "chunked-rest" | "multipart" | "tus";
5
+ /**
6
+ * Default ceiling applied to `maxFileSize` when the caller doesn't supply one
7
+ * (100 MiB). Without SOME cap, an unauthenticated or loosely-authorized
8
+ * handler accepts an unbounded request body — pass an explicit `maxFileSize`
9
+ * to raise or lower this for your app.
10
+ */
11
+ declare const DEFAULT_MAX_UPLOAD_BYTES: number;
5
12
  type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
6
13
  /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
7
14
  type UploadStorage = UploadHandlerOptions["storage"];
@@ -27,12 +34,32 @@ interface CreateUploadHandlerOptions {
27
34
  * returning `false` **or throwing** yields a `403`. Omit only for a fully
28
35
  * public bucket — the whole point of this handler over the admin path is
29
36
  * that uploads are gated by *your* per-user policy, not an admin token.
37
+ *
38
+ * Omitting it mounts an unauthenticated, unbounded-write endpoint, so
39
+ * doing so logs a one-time warning (per handler) unless `silent`/`public`
40
+ * says the omission is intentional.
30
41
  */
31
42
  authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
32
- /** Maximum accepted file size in bytes (forwarded to the multipart parser). */
43
+ /**
44
+ * Maximum accepted file size in bytes. Forwarded to the multipart parser
45
+ * (protocol `"multipart"`) and, for `"tus"`/`"chunked-rest"`, enforced by
46
+ * this handler itself against the request's declared size (`Upload-Length`
47
+ * / `Content-Length`) — see {@link declaredUploadSize}. Defaults to
48
+ * {@link DEFAULT_MAX_UPLOAD_BYTES} (100 MiB) — pass this to raise or lower
49
+ * the ceiling; there is no unbounded option.
50
+ */
33
51
  maxFileSize?: number;
34
52
  /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
35
53
  protocol?: UploadProtocol;
54
+ /**
55
+ * Set when omitting `authorize` is intentional (a fully public upload
56
+ * bucket) — suppresses the one-time "no authorize gate" warning that would
57
+ * otherwise print when the handler is constructed. Has no effect when
58
+ * `authorize` is provided.
59
+ */
60
+ public?: boolean;
61
+ /** Suppress the one-time default-open-authorize warning. Alias of `public`. */
62
+ silent?: boolean;
36
63
  /** The storage provider the bytes land in (R2 in prod, memory in tests). */
37
64
  storage: UploadStorage;
38
65
  }
@@ -88,4 +115,4 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
88
115
  declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
89
116
  secretAccessKey: string;
90
117
  }) => AwsLightStorage;
91
- export { type CreateUploadHandlerOptions, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
118
+ export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
package/dist/upload.d.ts CHANGED
@@ -2,6 +2,13 @@ import { Tus } from '@visulima/storage/handler/http/fetch';
2
2
  import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
3
3
  /** Resumable upload wire protocols the handler can speak. */
4
4
  type UploadProtocol = "chunked-rest" | "multipart" | "tus";
5
+ /**
6
+ * Default ceiling applied to `maxFileSize` when the caller doesn't supply one
7
+ * (100 MiB). Without SOME cap, an unauthenticated or loosely-authorized
8
+ * handler accepts an unbounded request body — pass an explicit `maxFileSize`
9
+ * to raise or lower this for your app.
10
+ */
11
+ declare const DEFAULT_MAX_UPLOAD_BYTES: number;
5
12
  type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
6
13
  /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
7
14
  type UploadStorage = UploadHandlerOptions["storage"];
@@ -27,12 +34,32 @@ interface CreateUploadHandlerOptions {
27
34
  * returning `false` **or throwing** yields a `403`. Omit only for a fully
28
35
  * public bucket — the whole point of this handler over the admin path is
29
36
  * that uploads are gated by *your* per-user policy, not an admin token.
37
+ *
38
+ * Omitting it mounts an unauthenticated, unbounded-write endpoint, so
39
+ * doing so logs a one-time warning (per handler) unless `silent`/`public`
40
+ * says the omission is intentional.
30
41
  */
31
42
  authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
32
- /** Maximum accepted file size in bytes (forwarded to the multipart parser). */
43
+ /**
44
+ * Maximum accepted file size in bytes. Forwarded to the multipart parser
45
+ * (protocol `"multipart"`) and, for `"tus"`/`"chunked-rest"`, enforced by
46
+ * this handler itself against the request's declared size (`Upload-Length`
47
+ * / `Content-Length`) — see {@link declaredUploadSize}. Defaults to
48
+ * {@link DEFAULT_MAX_UPLOAD_BYTES} (100 MiB) — pass this to raise or lower
49
+ * the ceiling; there is no unbounded option.
50
+ */
33
51
  maxFileSize?: number;
34
52
  /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
35
53
  protocol?: UploadProtocol;
54
+ /**
55
+ * Set when omitting `authorize` is intentional (a fully public upload
56
+ * bucket) — suppresses the one-time "no authorize gate" warning that would
57
+ * otherwise print when the handler is constructed. Has no effect when
58
+ * `authorize` is provided.
59
+ */
60
+ public?: boolean;
61
+ /** Suppress the one-time default-open-authorize warning. Alias of `public`. */
62
+ silent?: boolean;
36
63
  /** The storage provider the bytes land in (R2 in prod, memory in tests). */
37
64
  storage: UploadStorage;
38
65
  }
@@ -88,4 +115,4 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
88
115
  declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
89
116
  secretAccessKey: string;
90
117
  }) => AwsLightStorage;
91
- export { type CreateUploadHandlerOptions, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
118
+ export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
package/dist/upload.mjs CHANGED
@@ -1 +1 @@
1
- import{createR2UploadStorage as r,createUploadHandler as o}from"./packem_shared/createR2UploadStorage-BSgOjRvv.mjs";export{r as createR2UploadStorage,o as createUploadHandler};
1
+ import{DEFAULT_MAX_UPLOAD_BYTES as r,createR2UploadStorage as o,createUploadHandler as t}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-CeL_tQBR.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.15",
3
+ "version": "1.0.0-alpha.16",
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.10",
54
- "@lunora/platform": "1.0.0-alpha.1",
53
+ "@lunora/errors": "1.0.0-alpha.11",
54
+ "@lunora/platform": "1.0.0-alpha.2",
55
55
  "@visulima/storage": "1.0.5",
56
56
  "aws4fetch": "1.0.20"
57
57
  },
@@ -1,5 +0,0 @@
1
- import{LunoraError as p}from"@lunora/errors";import{t as A}from"./internal-DpjlEJPE.mjs";const $=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},u=new TextEncoder,f=10080*60,b=/^[a-z][a-z0-9+\-.]*:\/\//i,U=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},x=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},R=64,m=new Map,g=async e=>{const r=m.get(e);if(r)return r;$(m,R);const t=crypto.subtle.importKey("raw",u.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return m.set(e,t),t},y=e=>{try{return new URL(e).host}catch{return e.replace(b,"").split("/")[0]??""}},S=async(e,r)=>{const t=await g(e),n=await crypto.subtle.sign("HMAC",t,u.encode(r));return U(new Uint8Array(n))},I=async(e,r,t)=>{const n=await g(e);return crypto.subtle.verify("HMAC",n,t,u.encode(r))},T=/^\//,h=(e,r,t,n,a)=>{const o=`${e}
2
- ${r.toLowerCase()}
3
- ${t}
4
- ${String(n)}`;return a===void 0?o:`${o}
5
- ${a}`},P=async e=>{const r=e.method??"GET",t=e.expiresInSeconds??3600;if(!Number.isFinite(t)||t<=0)throw new p("VALIDATION_ERROR","@lunora/storage: expiresInSeconds must be a positive finite number");if(t>f)throw new p("VALIDATION_ERROR",`@lunora/storage: expiresInSeconds must not exceed ${String(f)} (7 days)`);const n=r==="PUT"?e.contentType:void 0,a=Math.floor(Date.now()/1e3)+t,o=y(e.baseUrl),c=await S(e.secret,h(r,o,e.key,a,n)),s=A(e.baseUrl),l=e.key.split("/").map(d=>encodeURIComponent(d)).join("/"),i=n===void 0?"":`&ct=${encodeURIComponent(n)}`;return`${s}/${l}?exp=${String(a)}&method=${r}&sig=${c}${i}`},E=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),c=n.searchParams.get("sig"),s=n.searchParams.get("method")??"GET",l=n.searchParams.get("ct")??void 0;if(!c||!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(T,"").split("/").map(v=>decodeURIComponent(v)).join("/"),d=x(c)}catch{return{reason:"malformed",valid:!1}}const w=t?.expectedHost===void 0?n.host:y(t.expectedHost);return await I(r,h(s,w,i,o,l),d)?{contentType:l,key:i,method:s,valid:!0}:{reason:"bad_signature",valid:!1}};export{P as buildSignedUrl,E as verifySignedUrl};
@@ -1 +0,0 @@
1
- import{Rest as n,Multipart as i,Tus as u}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as p}from"@visulima/storage/provider/aws-light";const d="1.0.0",a=e=>{const t={"content-type":"application/json"};return e==="tus"&&(t["Tus-Resumable"]=d),Response.json({error:{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}},{headers:t,status:403})},l=(e,t)=>e==="chunked-rest"?new n(t):e==="multipart"?new i(t):new u(t),g=e=>{const t=e.protocol??"tus",s={storage:e.storage,...e.maxFileSize===void 0?{}:{maxFileSize:e.maxFileSize}},c=l(t,s),{authorize:o}=e;return{fetch:async r=>{if(o!==void 0)try{if(!await o({method:r.method,protocol:t,request:r,url:new URL(r.url)}))return a(t)}catch{return a(t)}return c.fetch(r)},protocol:t}},y=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{y as createR2UploadStorage,g as createUploadHandler};
@@ -1 +0,0 @@
1
- import{LunoraError as a}from"@lunora/errors";import{t as T,a as f}from"./internal-DpjlEJPE.mjs";import{buildPresignedUrl as R}from"./buildPresignedUrl-TCv-5TAT.mjs";import{buildSignedUrl as k}from"./buildSignedUrl-DocLLDGy.mjs";const m=1024,I=1e3,A=100,b=e=>{const o=new Uint8Array(e);let i="";for(const p of o)i+=String.fromCodePoint(p);return btoa(i)},U=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 l=Reflect.get(u,s,u);return typeof l=="function"?l.bind(u):l},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()}},S=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,l){const h=p(s);if(h===void 0){l.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(i+=h,i>o){l.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(o)} bytes)`));return}l.enqueue(s)}});return e.pipeThrough(u)},c=e=>{if(typeof e!="string"||e.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(e.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(m)}-byte limit`);if(e.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(e.startsWith("/"))throw new a("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");const o=e.split("/");for(const i of o)if(i==="..")throw new a("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},O=(e,o)=>{c(e),c(o);const i=`${e.endsWith("/")?e.slice(0,-1):e}/${o}`;if(i.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(m)}-byte limit`);return i},v=e=>{if(!e.bucket)throw new a("INTERNAL","@lunora/storage: `bucket` is required");const o=async(r,t,n={})=>{if(c(r),n.allowedContentTypes!==void 0){if(n.contentType===void 0)throw new a("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!n.allowedContentTypes.includes(n.contentType))throw new a("VALIDATION_ERROR",`@lunora/storage: contentType "${n.contentType}" not in allowedContentTypes`)}let y=t;if(typeof n.maxSize=="number"){let g;if(t instanceof ArrayBuffer?g=t.byteLength:t instanceof Blob&&(g=t.size),g!==void 0&&g>n.maxSize)throw new a("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(g)} > ${String(n.maxSize)})`);t instanceof ReadableStream&&(y=M(t,n.maxSize))}const d=await e.bucket.put(r,y,{customMetadata:n.customMetadata,httpMetadata:n.contentType?{contentType:n.contentType}:void 0});return{etag:d.etag,httpEtag:d.httpEtag??`"${d.etag}"`,key:d.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&&U(n)},p=async r=>{c(r),await e.bucket.delete(r)},u=async r=>{if(c(r),e.bucket.head){const n=await e.bucket.head(r);return n&&w(n)}const t=await e.bucket.get(r,{range:{length:0}});return t&&w(t)},s=async(r,t={})=>{if(r?.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const n=t.limit??A,y=Math.min(Math.max(1,Math.floor(n)),I),d=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:y,prefix:r});return{cursor:d.cursor,objects:d.objects.map(g=>S(g)),truncated:d.truncated}},l=r=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(n=>encodeURIComponent(n)).join("/");return`${T(e.publicBaseUrl)}/${t}`},h=async(r,t={})=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new a("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return c(r),k({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 a("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 a("INTERNAL","@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");return c(r),R({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},getSignedUrl:h,getUrl:l,list:s,resumeMultipartUpload:(r,t)=>{if(c(r),typeof t!="string"||t.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new a("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:async(r,t,n={})=>o(r,t,n),upload:o}};export{v as createStorage,O as scopeKey};