@lunora/storage 1.0.0-alpha.11 → 1.0.0-alpha.13
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 +1 -4
- package/dist/packem_shared/buildPresignedUrl-TCv-5TAT.mjs +4 -0
- package/dist/packem_shared/buildSignedUrl-DocLLDGy.mjs +5 -0
- package/dist/packem_shared/createBucketStorage-DAip6cEw.mjs +1 -0
- package/dist/packem_shared/createR2UploadStorage-BSgOjRvv.mjs +1 -0
- package/dist/packem_shared/createStorage-CWKguFEG.mjs +1 -0
- package/dist/packem_shared/internal-DpjlEJPE.mjs +1 -0
- package/dist/upload.mjs +1 -1
- package/package.json +2 -2
- package/dist/packem_shared/buildPresignedUrl-B5f1wvz6.mjs +0 -64
- package/dist/packem_shared/buildSignedUrl-rt-6azia.mjs +0 -124
- package/dist/packem_shared/createBucketStorage-JYdrS4e5.mjs +0 -28
- package/dist/packem_shared/createR2UploadStorage-BfLIpWdf.mjs +0 -54
- package/dist/packem_shared/createStorage-Bu6GJB25.mjs +0 -261
- package/dist/packem_shared/internal-Dqk0MrAj.mjs +0 -17
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createStorage, scopeKey } from './packem_shared/createStorage-Bu6GJB25.mjs';
|
|
3
|
-
export { buildPresignedUrl } from './packem_shared/buildPresignedUrl-B5f1wvz6.mjs';
|
|
4
|
-
export { buildSignedUrl, verifySignedUrl } from './packem_shared/buildSignedUrl-rt-6azia.mjs';
|
|
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};
|
|
@@ -0,0 +1,4 @@
|
|
|
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};
|
|
@@ -0,0 +1,5 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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};
|
|
@@ -0,0 +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};
|
package/dist/upload.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{createR2UploadStorage as r,createUploadHandler as o}from"./packem_shared/createR2UploadStorage-BSgOjRvv.mjs";export{r as createR2UploadStorage,o as createUploadHandler};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/storage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
53
|
+
"@lunora/errors": "1.0.0-alpha.9",
|
|
54
54
|
"@visulima/storage": "1.0.5",
|
|
55
55
|
"aws4fetch": "1.0.20"
|
|
56
56
|
},
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { a as toHex } from './internal-Dqk0MrAj.mjs';
|
|
2
|
-
|
|
3
|
-
const REGION = "auto";
|
|
4
|
-
const SERVICE = "s3";
|
|
5
|
-
const ALGORITHM = "AWS4-HMAC-SHA256";
|
|
6
|
-
const MIN_EXPIRES_SECONDS = 1;
|
|
7
|
-
const MAX_EXPIRES_SECONDS = 7 * 24 * 60 * 60;
|
|
8
|
-
const DEFAULT_EXPIRES_SECONDS = 900;
|
|
9
|
-
const textEncoder = new TextEncoder();
|
|
10
|
-
const compareEntries = (a, b) => {
|
|
11
|
-
if (a[0] < b[0]) {
|
|
12
|
-
return -1;
|
|
13
|
-
}
|
|
14
|
-
return a[0] > b[0] ? 1 : 0;
|
|
15
|
-
};
|
|
16
|
-
const encodeRfc3986 = (value) => encodeURIComponent(value).replaceAll(/[!'()*]/gu, (char) => `%${char.codePointAt(0)?.toString(16).toUpperCase() ?? ""}`);
|
|
17
|
-
const encodeKey = (key) => key.split("/").map((segment) => encodeRfc3986(segment)).join("/");
|
|
18
|
-
const sha256Hex = async (input) => toHex(await crypto.subtle.digest("SHA-256", textEncoder.encode(input)));
|
|
19
|
-
const hmac = async (key, message) => {
|
|
20
|
-
const cryptoKey = await crypto.subtle.importKey("raw", key, { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
|
|
21
|
-
return crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(message));
|
|
22
|
-
};
|
|
23
|
-
const deriveSigningKey = async (secretAccessKey, dateStamp) => {
|
|
24
|
-
const dateKey = await hmac(textEncoder.encode(`AWS4${secretAccessKey}`), dateStamp);
|
|
25
|
-
const regionKey = await hmac(dateKey, REGION);
|
|
26
|
-
const serviceKey = await hmac(regionKey, SERVICE);
|
|
27
|
-
return hmac(serviceKey, "aws4_request");
|
|
28
|
-
};
|
|
29
|
-
const endpointHost = (credentials) => {
|
|
30
|
-
const infix = credentials.jurisdiction === void 0 ? "" : `${credentials.jurisdiction}.`;
|
|
31
|
-
return `${credentials.accountId}.${infix}r2.cloudflarestorage.com`;
|
|
32
|
-
};
|
|
33
|
-
const formatAmzDate = (date) => {
|
|
34
|
-
const amzDate = `${date.toISOString().replaceAll(/[:-]/gu, "").slice(0, 15)}Z`;
|
|
35
|
-
return { amzDate, dateStamp: amzDate.slice(0, 8) };
|
|
36
|
-
};
|
|
37
|
-
const buildPresignedUrl = async (parameters) => {
|
|
38
|
-
const { credentials, key } = parameters;
|
|
39
|
-
const method = parameters.method ?? "GET";
|
|
40
|
-
const requested = parameters.expiresInSeconds ?? DEFAULT_EXPIRES_SECONDS;
|
|
41
|
-
const normalised = Number.isFinite(requested) ? requested : DEFAULT_EXPIRES_SECONDS;
|
|
42
|
-
const expires = Math.min(Math.max(MIN_EXPIRES_SECONDS, Math.floor(normalised)), MAX_EXPIRES_SECONDS);
|
|
43
|
-
const host = endpointHost(credentials);
|
|
44
|
-
const date = new Date(parameters.now?.() ?? Date.now());
|
|
45
|
-
const { amzDate, dateStamp } = formatAmzDate(date);
|
|
46
|
-
const credentialScope = `${dateStamp}/${REGION}/${SERVICE}/aws4_request`;
|
|
47
|
-
const canonicalUri = `/${encodeRfc3986(credentials.bucket)}/${encodeKey(key)}`;
|
|
48
|
-
const query = [
|
|
49
|
-
["X-Amz-Algorithm", ALGORITHM],
|
|
50
|
-
["X-Amz-Credential", `${credentials.accessKeyId}/${credentialScope}`],
|
|
51
|
-
["X-Amz-Date", amzDate],
|
|
52
|
-
["X-Amz-Expires", expires.toString()],
|
|
53
|
-
["X-Amz-SignedHeaders", "host"]
|
|
54
|
-
];
|
|
55
|
-
const canonicalQuery = query.map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)]).toSorted(compareEntries).map(([name, value]) => `${name}=${value}`).join("&");
|
|
56
|
-
const canonicalRequest = [method, canonicalUri, canonicalQuery, `host:${host}
|
|
57
|
-
`, "host", "UNSIGNED-PAYLOAD"].join("\n");
|
|
58
|
-
const stringToSign = [ALGORITHM, amzDate, credentialScope, await sha256Hex(canonicalRequest)].join("\n");
|
|
59
|
-
const signingKey = await deriveSigningKey(credentials.secretAccessKey, dateStamp);
|
|
60
|
-
const signature = toHex(await hmac(signingKey, stringToSign));
|
|
61
|
-
return `https://${host}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
export { buildPresignedUrl };
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { t as trimTrailingSlashes } from './internal-Dqk0MrAj.mjs';
|
|
3
|
-
|
|
4
|
-
const evictOldestEntry = (map, capacity) => {
|
|
5
|
-
if (map.size < capacity) {
|
|
6
|
-
return;
|
|
7
|
-
}
|
|
8
|
-
const oldest = map.keys().next().value;
|
|
9
|
-
if (oldest !== void 0) {
|
|
10
|
-
map.delete(oldest);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const textEncoder = new TextEncoder();
|
|
15
|
-
const MAX_SIGNED_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
16
|
-
const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+\-.]*:\/\//i;
|
|
17
|
-
const toBase64Url = (bytes) => {
|
|
18
|
-
const binary = String.fromCodePoint(...bytes);
|
|
19
|
-
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
20
|
-
};
|
|
21
|
-
const fromBase64Url = (input) => {
|
|
22
|
-
const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
|
|
23
|
-
const binary = atob(padded);
|
|
24
|
-
const bytes = new Uint8Array(binary.length);
|
|
25
|
-
for (let index = 0; index < binary.length; index += 1) {
|
|
26
|
-
bytes[index] = binary.codePointAt(index) ?? 0;
|
|
27
|
-
}
|
|
28
|
-
return bytes;
|
|
29
|
-
};
|
|
30
|
-
const KEY_CACHE_MAX = 64;
|
|
31
|
-
const keyCache = /* @__PURE__ */ new Map();
|
|
32
|
-
const importHmacKey = async (secret) => {
|
|
33
|
-
const cached = keyCache.get(secret);
|
|
34
|
-
if (cached) {
|
|
35
|
-
return cached;
|
|
36
|
-
}
|
|
37
|
-
evictOldestEntry(keyCache, KEY_CACHE_MAX);
|
|
38
|
-
const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
|
|
39
|
-
keyCache.set(secret, keyPromise);
|
|
40
|
-
return keyPromise;
|
|
41
|
-
};
|
|
42
|
-
const extractHost = (input) => {
|
|
43
|
-
try {
|
|
44
|
-
return new URL(input).host;
|
|
45
|
-
} catch {
|
|
46
|
-
const noScheme = input.replace(SCHEME_PREFIX_RE, "");
|
|
47
|
-
return noScheme.split("/")[0] ?? "";
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
const signCanonical = async (secret, canonical) => {
|
|
51
|
-
const cryptoKey = await importHmacKey(secret);
|
|
52
|
-
const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonical));
|
|
53
|
-
return toBase64Url(new Uint8Array(signature));
|
|
54
|
-
};
|
|
55
|
-
const verifyCanonical = async (secret, canonical, sigBytes) => {
|
|
56
|
-
const cryptoKey = await importHmacKey(secret);
|
|
57
|
-
return crypto.subtle.verify("HMAC", cryptoKey, sigBytes, textEncoder.encode(canonical));
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const LEADING_SLASH_RE = /^\//;
|
|
61
|
-
const canonicalize = (method, host, key, exp, contentType) => {
|
|
62
|
-
const base = `${method}
|
|
63
|
-
${host.toLowerCase()}
|
|
64
|
-
${key}
|
|
65
|
-
${String(exp)}`;
|
|
66
|
-
return contentType === void 0 ? base : `${base}
|
|
67
|
-
${contentType}`;
|
|
68
|
-
};
|
|
69
|
-
const buildSignedUrl = async (args) => {
|
|
70
|
-
const method = args.method ?? "GET";
|
|
71
|
-
const expiresInSeconds = args.expiresInSeconds ?? 60 * 60;
|
|
72
|
-
if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
73
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: expiresInSeconds must be a positive finite number");
|
|
74
|
-
}
|
|
75
|
-
if (expiresInSeconds > MAX_SIGNED_URL_TTL_SECONDS) {
|
|
76
|
-
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: expiresInSeconds must not exceed ${String(MAX_SIGNED_URL_TTL_SECONDS)} (7 days)`);
|
|
77
|
-
}
|
|
78
|
-
const contentType = method === "PUT" ? args.contentType : void 0;
|
|
79
|
-
const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
|
|
80
|
-
const host = extractHost(args.baseUrl);
|
|
81
|
-
const sig = await signCanonical(args.secret, canonicalize(method, host, args.key, exp, contentType));
|
|
82
|
-
const base = trimTrailingSlashes(args.baseUrl);
|
|
83
|
-
const safeKey = args.key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
84
|
-
const ctParameter = contentType === void 0 ? "" : `&ct=${encodeURIComponent(contentType)}`;
|
|
85
|
-
return `${base}/${safeKey}?exp=${String(exp)}&method=${method}&sig=${sig}${ctParameter}`;
|
|
86
|
-
};
|
|
87
|
-
const verifySignedUrl = async (input, secret, options) => {
|
|
88
|
-
let url;
|
|
89
|
-
try {
|
|
90
|
-
url = input instanceof URL ? input : new URL(input);
|
|
91
|
-
} catch {
|
|
92
|
-
return { reason: "malformed", valid: false };
|
|
93
|
-
}
|
|
94
|
-
const expRaw = url.searchParams.get("exp");
|
|
95
|
-
const exp = expRaw === null ? Number.NaN : Number(expRaw);
|
|
96
|
-
const sig = url.searchParams.get("sig");
|
|
97
|
-
const method = url.searchParams.get("method") ?? "GET";
|
|
98
|
-
const contentType = url.searchParams.get("ct") ?? void 0;
|
|
99
|
-
if (!sig || !Number.isInteger(exp)) {
|
|
100
|
-
return { reason: "malformed", valid: false };
|
|
101
|
-
}
|
|
102
|
-
if (exp < Math.floor(Date.now() / 1e3)) {
|
|
103
|
-
return { reason: "expired", valid: false };
|
|
104
|
-
}
|
|
105
|
-
if (method !== "GET" && method !== "PUT") {
|
|
106
|
-
return { reason: "malformed", valid: false };
|
|
107
|
-
}
|
|
108
|
-
let key;
|
|
109
|
-
let sigBytes;
|
|
110
|
-
try {
|
|
111
|
-
key = url.pathname.replace(LEADING_SLASH_RE, "").split("/").map((segment) => decodeURIComponent(segment)).join("/");
|
|
112
|
-
sigBytes = fromBase64Url(sig);
|
|
113
|
-
} catch {
|
|
114
|
-
return { reason: "malformed", valid: false };
|
|
115
|
-
}
|
|
116
|
-
const host = options?.expectedHost === void 0 ? url.host : extractHost(options.expectedHost);
|
|
117
|
-
const valid = await verifyCanonical(secret, canonicalize(method, host, key, exp, contentType), sigBytes);
|
|
118
|
-
if (!valid) {
|
|
119
|
-
return { reason: "bad_signature", valid: false };
|
|
120
|
-
}
|
|
121
|
-
return { contentType, key, method, valid: true };
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export { buildSignedUrl, verifySignedUrl };
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const createBucketStorage = (buckets, options = {}) => {
|
|
4
|
-
const names = Object.keys(buckets);
|
|
5
|
-
const [firstName] = names;
|
|
6
|
-
if (firstName === void 0) {
|
|
7
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: createBucketStorage requires at least one bucket");
|
|
8
|
-
}
|
|
9
|
-
if (options.default !== void 0 && !buckets[options.default]) {
|
|
10
|
-
throw new LunoraError("INTERNAL", `@lunora/storage: default bucket "${options.default}" is not in the bucket map (have: ${names.join(", ")})`);
|
|
11
|
-
}
|
|
12
|
-
const defaultTag = options.default ?? "default";
|
|
13
|
-
const defaultBinding = buckets[defaultTag] ?? buckets[firstName];
|
|
14
|
-
if (defaultBinding === void 0) {
|
|
15
|
-
throw new LunoraError("INTERNAL", `@lunora/storage: default bucket "${defaultTag}" is not in the bucket map (have: ${names.join(", ")})`);
|
|
16
|
-
}
|
|
17
|
-
const addressable = [.../* @__PURE__ */ new Set([defaultTag, ...names])];
|
|
18
|
-
const make = (name) => {
|
|
19
|
-
const target = name === defaultTag ? defaultBinding : buckets[name];
|
|
20
|
-
if (!target) {
|
|
21
|
-
throw new LunoraError("INTERNAL", `@lunora/storage: no bucket registered for "${name}". Known buckets: ${addressable.join(", ")}`);
|
|
22
|
-
}
|
|
23
|
-
return { ...target, bucket: (next) => make(next), bucketName: name };
|
|
24
|
-
};
|
|
25
|
-
return make(defaultTag);
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export { createBucketStorage };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { Rest, Multipart, Tus } from '@visulima/storage/handler/http/fetch';
|
|
2
|
-
import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
|
|
3
|
-
|
|
4
|
-
const TUS_RESUMABLE = "1.0.0";
|
|
5
|
-
const denyResponse = (protocol) => {
|
|
6
|
-
const headers = { "content-type": "application/json" };
|
|
7
|
-
if (protocol === "tus") {
|
|
8
|
-
headers["Tus-Resumable"] = TUS_RESUMABLE;
|
|
9
|
-
}
|
|
10
|
-
return Response.json({ error: { code: "FORBIDDEN", message: "Upload denied by authorization policy", name: "ForbiddenError" } }, { headers, status: 403 });
|
|
11
|
-
};
|
|
12
|
-
const instantiateHandler = (protocol, handlerOptions) => {
|
|
13
|
-
if (protocol === "chunked-rest") {
|
|
14
|
-
return new Rest(handlerOptions);
|
|
15
|
-
}
|
|
16
|
-
if (protocol === "multipart") {
|
|
17
|
-
return new Multipart(handlerOptions);
|
|
18
|
-
}
|
|
19
|
-
return new Tus(handlerOptions);
|
|
20
|
-
};
|
|
21
|
-
const createUploadHandler = (options) => {
|
|
22
|
-
const protocol = options.protocol ?? "tus";
|
|
23
|
-
const handlerOptions = {
|
|
24
|
-
storage: options.storage,
|
|
25
|
-
...options.maxFileSize === void 0 ? {} : { maxFileSize: options.maxFileSize }
|
|
26
|
-
};
|
|
27
|
-
const handler = instantiateHandler(protocol, handlerOptions);
|
|
28
|
-
const { authorize } = options;
|
|
29
|
-
const fetch = async (request) => {
|
|
30
|
-
if (authorize !== void 0) {
|
|
31
|
-
try {
|
|
32
|
-
const allowed = await authorize({ method: request.method, protocol, request, url: new URL(request.url) });
|
|
33
|
-
if (!allowed) {
|
|
34
|
-
return denyResponse(protocol);
|
|
35
|
-
}
|
|
36
|
-
} catch {
|
|
37
|
-
return denyResponse(protocol);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return handler.fetch(request);
|
|
41
|
-
};
|
|
42
|
-
return { fetch, protocol };
|
|
43
|
-
};
|
|
44
|
-
const createR2UploadStorage = (options) => new AwsLightStorage({
|
|
45
|
-
accessKeyId: options.accessKeyId,
|
|
46
|
-
bucket: options.bucket,
|
|
47
|
-
endpoint: options.endpoint ?? `https://${options.accountId}.r2.cloudflarestorage.com`,
|
|
48
|
-
path: options.path ?? "/",
|
|
49
|
-
region: "auto",
|
|
50
|
-
secretAccessKey: options.secretAccessKey,
|
|
51
|
-
...options.partSize === void 0 ? {} : { partSize: options.partSize }
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export { createR2UploadStorage, createUploadHandler };
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { t as trimTrailingSlashes, a as toHex } from './internal-Dqk0MrAj.mjs';
|
|
3
|
-
import { buildPresignedUrl } from './buildPresignedUrl-B5f1wvz6.mjs';
|
|
4
|
-
import { buildSignedUrl } from './buildSignedUrl-rt-6azia.mjs';
|
|
5
|
-
|
|
6
|
-
const MAX_KEY_LENGTH = 1024;
|
|
7
|
-
const MAX_LIST_LIMIT = 1e3;
|
|
8
|
-
const DEFAULT_LIST_LIMIT = 100;
|
|
9
|
-
const toBase64 = (buffer) => {
|
|
10
|
-
const bytes = new Uint8Array(buffer);
|
|
11
|
-
let binary = "";
|
|
12
|
-
for (const byte of bytes) {
|
|
13
|
-
binary += String.fromCodePoint(byte);
|
|
14
|
-
}
|
|
15
|
-
return btoa(binary);
|
|
16
|
-
};
|
|
17
|
-
const withSha256 = (object) => {
|
|
18
|
-
const raw = object.checksums?.sha256;
|
|
19
|
-
if (raw === void 0) {
|
|
20
|
-
return object;
|
|
21
|
-
}
|
|
22
|
-
const sha256 = toHex(raw);
|
|
23
|
-
const sha256Base64 = toBase64(raw);
|
|
24
|
-
return /* @__PURE__ */ new Proxy(object, {
|
|
25
|
-
get(target, property) {
|
|
26
|
-
if (property === "sha256") {
|
|
27
|
-
return sha256;
|
|
28
|
-
}
|
|
29
|
-
if (property === "sha256Base64") {
|
|
30
|
-
return sha256Base64;
|
|
31
|
-
}
|
|
32
|
-
const value = Reflect.get(target, property, target);
|
|
33
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
34
|
-
},
|
|
35
|
-
has(target, property) {
|
|
36
|
-
return property === "sha256" || property === "sha256Base64" || Reflect.has(target, property);
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
};
|
|
40
|
-
const toMetadata = (object) => {
|
|
41
|
-
const raw = object.checksums?.sha256;
|
|
42
|
-
return {
|
|
43
|
-
contentType: object.httpMetadata?.contentType,
|
|
44
|
-
customMetadata: object.customMetadata,
|
|
45
|
-
key: object.key,
|
|
46
|
-
sha256: raw === void 0 ? void 0 : toHex(raw),
|
|
47
|
-
size: object.size,
|
|
48
|
-
uploaded: object.uploaded === void 0 ? void 0 : object.uploaded.getTime()
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
const toListObject = (object) => {
|
|
52
|
-
const raw = object.checksums?.sha256;
|
|
53
|
-
return {
|
|
54
|
-
checksums: object.checksums,
|
|
55
|
-
customMetadata: object.customMetadata,
|
|
56
|
-
etag: object.etag,
|
|
57
|
-
httpEtag: object.httpEtag,
|
|
58
|
-
httpMetadata: object.httpMetadata,
|
|
59
|
-
key: object.key,
|
|
60
|
-
sha256: raw === void 0 ? void 0 : toHex(raw),
|
|
61
|
-
sha256Base64: raw === void 0 ? void 0 : toBase64(raw),
|
|
62
|
-
size: object.size,
|
|
63
|
-
uploaded: object.uploaded
|
|
64
|
-
};
|
|
65
|
-
};
|
|
66
|
-
const enforceStreamMaxSize = (stream, maxSize) => {
|
|
67
|
-
let seen = 0;
|
|
68
|
-
const byteLengthOf = (chunk) => {
|
|
69
|
-
if (chunk instanceof ArrayBuffer) {
|
|
70
|
-
return chunk.byteLength;
|
|
71
|
-
}
|
|
72
|
-
return ArrayBuffer.isView(chunk) ? chunk.byteLength : void 0;
|
|
73
|
-
};
|
|
74
|
-
const counter = new TransformStream({
|
|
75
|
-
transform(chunk, controller) {
|
|
76
|
-
const length = byteLengthOf(chunk);
|
|
77
|
-
if (length === void 0) {
|
|
78
|
-
controller.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
seen += length;
|
|
82
|
-
if (seen > maxSize) {
|
|
83
|
-
controller.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(maxSize)} bytes)`));
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
controller.enqueue(chunk);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
return stream.pipeThrough(counter);
|
|
90
|
-
};
|
|
91
|
-
const validateKey = (key) => {
|
|
92
|
-
if (typeof key !== "string" || key.length === 0) {
|
|
93
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key must be a non-empty string");
|
|
94
|
-
}
|
|
95
|
-
if (key.length > MAX_KEY_LENGTH) {
|
|
96
|
-
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
97
|
-
}
|
|
98
|
-
if (key.includes("\0")) {
|
|
99
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key contains NUL byte");
|
|
100
|
-
}
|
|
101
|
-
if (key.startsWith("/")) {
|
|
102
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key must not start with `/`");
|
|
103
|
-
}
|
|
104
|
-
const segments = key.split("/");
|
|
105
|
-
for (const segment of segments) {
|
|
106
|
-
if (segment === "..") {
|
|
107
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: key contains a `..` path component");
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const scopeKey = (prefix, key) => {
|
|
112
|
-
validateKey(prefix);
|
|
113
|
-
validateKey(key);
|
|
114
|
-
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
115
|
-
const composed = `${trimmedPrefix}/${key}`;
|
|
116
|
-
if (composed.length > MAX_KEY_LENGTH) {
|
|
117
|
-
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
118
|
-
}
|
|
119
|
-
return composed;
|
|
120
|
-
};
|
|
121
|
-
const createStorage = (options) => {
|
|
122
|
-
if (!options.bucket) {
|
|
123
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: `bucket` is required");
|
|
124
|
-
}
|
|
125
|
-
const upload = async (key, body, uploadOptions = {}) => {
|
|
126
|
-
validateKey(key);
|
|
127
|
-
if (uploadOptions.allowedContentTypes !== void 0) {
|
|
128
|
-
if (uploadOptions.contentType === void 0) {
|
|
129
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: contentType is required when allowedContentTypes is set");
|
|
130
|
-
}
|
|
131
|
-
if (!uploadOptions.allowedContentTypes.includes(uploadOptions.contentType)) {
|
|
132
|
-
throw new LunoraError("VALIDATION_ERROR", `@lunora/storage: contentType "${uploadOptions.contentType}" not in allowedContentTypes`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
let putBody = body;
|
|
136
|
-
if (typeof uploadOptions.maxSize === "number") {
|
|
137
|
-
let size;
|
|
138
|
-
if (body instanceof ArrayBuffer) {
|
|
139
|
-
size = body.byteLength;
|
|
140
|
-
} else if (body instanceof Blob) {
|
|
141
|
-
size = body.size;
|
|
142
|
-
}
|
|
143
|
-
if (size !== void 0 && size > uploadOptions.maxSize) {
|
|
144
|
-
throw new LunoraError("PAYLOAD_TOO_LARGE", `@lunora/storage: body exceeds maxSize (${String(size)} > ${String(uploadOptions.maxSize)})`);
|
|
145
|
-
}
|
|
146
|
-
if (body instanceof ReadableStream) {
|
|
147
|
-
putBody = enforceStreamMaxSize(body, uploadOptions.maxSize);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
const object = await options.bucket.put(key, putBody, {
|
|
151
|
-
customMetadata: uploadOptions.customMetadata,
|
|
152
|
-
httpMetadata: uploadOptions.contentType ? { contentType: uploadOptions.contentType } : void 0
|
|
153
|
-
});
|
|
154
|
-
return { etag: object.etag, httpEtag: object.httpEtag ?? `"${object.etag}"`, key: object.key };
|
|
155
|
-
};
|
|
156
|
-
const download = async (key, downloadOptions = {}) => {
|
|
157
|
-
validateKey(key);
|
|
158
|
-
const object = await (downloadOptions.range ? options.bucket.get(key, { range: downloadOptions.range }) : options.bucket.get(key));
|
|
159
|
-
return object && withSha256(object);
|
|
160
|
-
};
|
|
161
|
-
const deleteObject = async (key) => {
|
|
162
|
-
validateKey(key);
|
|
163
|
-
await options.bucket.delete(key);
|
|
164
|
-
};
|
|
165
|
-
const getMetadata = async (key) => {
|
|
166
|
-
validateKey(key);
|
|
167
|
-
if (options.bucket.head) {
|
|
168
|
-
const head = await options.bucket.head(key);
|
|
169
|
-
return head && toMetadata(head);
|
|
170
|
-
}
|
|
171
|
-
const object = await options.bucket.get(key, { range: { length: 0 } });
|
|
172
|
-
return object && toMetadata(object);
|
|
173
|
-
};
|
|
174
|
-
const list = async (prefix, listOptions = {}) => {
|
|
175
|
-
if (prefix?.includes("\0")) {
|
|
176
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: prefix contains NUL byte");
|
|
177
|
-
}
|
|
178
|
-
const requested = listOptions.limit ?? DEFAULT_LIST_LIMIT;
|
|
179
|
-
const limit = Math.min(Math.max(1, Math.floor(requested)), MAX_LIST_LIMIT);
|
|
180
|
-
const result = await options.bucket.list({ cursor: listOptions.cursor, delimiter: listOptions.delimiter, limit, prefix });
|
|
181
|
-
return { cursor: result.cursor, objects: result.objects.map((object) => toListObject(object)), truncated: result.truncated };
|
|
182
|
-
};
|
|
183
|
-
const getUrl = (key) => {
|
|
184
|
-
if (!options.publicBaseUrl) {
|
|
185
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getUrl()");
|
|
186
|
-
}
|
|
187
|
-
validateKey(key);
|
|
188
|
-
const safeKey = key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
189
|
-
return `${trimTrailingSlashes(options.publicBaseUrl)}/${safeKey}`;
|
|
190
|
-
};
|
|
191
|
-
const getSignedUrl = async (key, signedOptions = {}) => {
|
|
192
|
-
if (!options.publicBaseUrl) {
|
|
193
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");
|
|
194
|
-
}
|
|
195
|
-
if (!options.signingSecret) {
|
|
196
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: `signingSecret` is required for getSignedUrl()");
|
|
197
|
-
}
|
|
198
|
-
validateKey(key);
|
|
199
|
-
return buildSignedUrl({
|
|
200
|
-
baseUrl: options.publicBaseUrl,
|
|
201
|
-
contentType: signedOptions.contentType,
|
|
202
|
-
expiresInSeconds: signedOptions.expiresInSeconds,
|
|
203
|
-
key,
|
|
204
|
-
method: signedOptions.method,
|
|
205
|
-
secret: options.signingSecret
|
|
206
|
-
});
|
|
207
|
-
};
|
|
208
|
-
const createMultipartUpload = async (key, multipartOptions = {}) => {
|
|
209
|
-
validateKey(key);
|
|
210
|
-
if (!options.bucket.createMultipartUpload) {
|
|
211
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");
|
|
212
|
-
}
|
|
213
|
-
return options.bucket.createMultipartUpload(key, {
|
|
214
|
-
customMetadata: multipartOptions.customMetadata,
|
|
215
|
-
httpMetadata: multipartOptions.contentType ? { contentType: multipartOptions.contentType } : void 0
|
|
216
|
-
});
|
|
217
|
-
};
|
|
218
|
-
const resumeMultipartUpload = (key, uploadId) => {
|
|
219
|
-
validateKey(key);
|
|
220
|
-
if (typeof uploadId !== "string" || uploadId.length === 0) {
|
|
221
|
-
throw new LunoraError("VALIDATION_ERROR", "@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");
|
|
222
|
-
}
|
|
223
|
-
if (!options.bucket.resumeMultipartUpload) {
|
|
224
|
-
throw new LunoraError("INTERNAL", "@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");
|
|
225
|
-
}
|
|
226
|
-
return options.bucket.resumeMultipartUpload(key, uploadId);
|
|
227
|
-
};
|
|
228
|
-
const getPresignedUrl = async (key, presignedOptions = {}) => {
|
|
229
|
-
if (!options.s3) {
|
|
230
|
-
throw new LunoraError(
|
|
231
|
-
"INTERNAL",
|
|
232
|
-
"@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }"
|
|
233
|
-
);
|
|
234
|
-
}
|
|
235
|
-
validateKey(key);
|
|
236
|
-
return buildPresignedUrl({
|
|
237
|
-
credentials: options.s3,
|
|
238
|
-
expiresInSeconds: presignedOptions.expiresInSeconds,
|
|
239
|
-
key,
|
|
240
|
-
method: presignedOptions.method
|
|
241
|
-
});
|
|
242
|
-
};
|
|
243
|
-
const generateUploadUrl = async (key, uploadUrlOptions = {}) => getSignedUrl(key, { contentType: uploadUrlOptions.contentType, expiresInSeconds: uploadUrlOptions.expiresInSeconds, method: "PUT" });
|
|
244
|
-
const store = async (key, body, storeOptions = {}) => upload(key, body, storeOptions);
|
|
245
|
-
return {
|
|
246
|
-
createMultipartUpload,
|
|
247
|
-
delete: deleteObject,
|
|
248
|
-
download,
|
|
249
|
-
generateUploadUrl,
|
|
250
|
-
getMetadata,
|
|
251
|
-
getPresignedUrl,
|
|
252
|
-
getSignedUrl,
|
|
253
|
-
getUrl,
|
|
254
|
-
list,
|
|
255
|
-
resumeMultipartUpload,
|
|
256
|
-
store,
|
|
257
|
-
upload
|
|
258
|
-
};
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
export { createStorage, scopeKey };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
const toHex = (buffer) => {
|
|
2
|
-
const bytes = new Uint8Array(buffer);
|
|
3
|
-
let out = "";
|
|
4
|
-
for (const byte of bytes) {
|
|
5
|
-
out += byte.toString(16).padStart(2, "0");
|
|
6
|
-
}
|
|
7
|
-
return out;
|
|
8
|
-
};
|
|
9
|
-
const trimTrailingSlashes = (value) => {
|
|
10
|
-
let end = value.length;
|
|
11
|
-
while (end > 0 && value[end - 1] === "/") {
|
|
12
|
-
end -= 1;
|
|
13
|
-
}
|
|
14
|
-
return value.slice(0, end);
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export { toHex as a, trimTrailingSlashes as t };
|