@lunora/storage 1.0.0-alpha.2 → 1.0.0-alpha.21

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.
@@ -0,0 +1,118 @@
1
+ import { Tus } from '@visulima/storage/handler/http/fetch';
2
+ import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
3
+ /** Resumable upload wire protocols the handler can speak. */
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;
12
+ type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
13
+ /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
14
+ type UploadStorage = UploadHandlerOptions["storage"];
15
+ /**
16
+ * The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
17
+ * needed to make an RLS decision: the raw `request` (headers/cookies/auth), the
18
+ * `method`, the parsed `url`, and which `protocol` the handler speaks.
19
+ */
20
+ interface UploadAuthzContext {
21
+ /** The upload method being invoked (`POST` create, `PATCH` chunk, `HEAD` resume, `DELETE`). */
22
+ method: string;
23
+ /** The protocol this handler is mounted for. */
24
+ protocol: UploadProtocol;
25
+ /** The inbound request — inspect headers/cookies to resolve the caller's identity. */
26
+ request: Request;
27
+ /** The parsed request URL (query params, upload-id path segment). */
28
+ url: URL;
29
+ }
30
+ /** Options for {@link createUploadHandler}. */
31
+ interface CreateUploadHandlerOptions {
32
+ /**
33
+ * The RLS gate. Runs before every upload request and denies fail-closed:
34
+ * returning `false` **or throwing** yields a `403`. Omit only for a fully
35
+ * public bucket — the whole point of this handler over the admin path is
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.
41
+ */
42
+ authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
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
+ */
51
+ maxFileSize?: number;
52
+ /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
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;
63
+ /** The storage provider the bytes land in (R2 in prod, memory in tests). */
64
+ storage: UploadStorage;
65
+ }
66
+ /** The object returned by {@link createUploadHandler}. */
67
+ interface UploadHandler {
68
+ /**
69
+ * Handle one upload request. Runs the RLS gate, then delegates to the
70
+ * `@visulima/storage` protocol handler. Wire this into your Worker's routing
71
+ * for the path the client uploads to.
72
+ */
73
+ fetch: (request: Request) => Promise<Response>;
74
+ /** The protocol this handler speaks. */
75
+ protocol: UploadProtocol;
76
+ }
77
+ /** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
78
+ interface R2UploadStorageOptions {
79
+ /** R2 S3 API Access Key ID (from an R2 API token). */
80
+ accessKeyId: string;
81
+ /** Cloudflare account id — used to derive the R2 S3 endpoint host. */
82
+ accountId: string;
83
+ /** Target R2 bucket name. */
84
+ bucket: string;
85
+ /**
86
+ * Explicit R2 S3 endpoint. Defaults to
87
+ * `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
88
+ * jurisdiction (e.g. `<accountId>.eu.r2.cloudflarestorage.com`).
89
+ */
90
+ endpoint?: string;
91
+ /** Client-side multipart part size (bytes or a size string like `"16MB"`). */
92
+ partSize?: number | string;
93
+ /**
94
+ * Path prefix the handler is mounted on (must match the client endpoint's
95
+ * path). Default `"/"`.
96
+ */
97
+ path?: string;
98
+ }
99
+ /**
100
+ * Build an RLS-gated resumable upload handler over a `@visulima/storage`
101
+ * provider. Mount its {@link UploadHandler.fetch} on the route your client
102
+ * uploads to and drive it with `@visulima/storage-client`.
103
+ */
104
+ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
105
+ /**
106
+ * Build an R2-backed storage provider for {@link createUploadHandler} using
107
+ * `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
108
+ * AWS SDK). R2's S3 region alias is always `auto`.
109
+ *
110
+ * Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
111
+ * same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
112
+ * Worker the `aws-light` provider needs `nodejs_compat` (it imports
113
+ * `node:stream`).
114
+ */
115
+ declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
116
+ secretAccessKey: string;
117
+ }) => AwsLightStorage;
118
+ export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
@@ -0,0 +1,118 @@
1
+ import { Tus } from '@visulima/storage/handler/http/fetch';
2
+ import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
3
+ /** Resumable upload wire protocols the handler can speak. */
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;
12
+ type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
13
+ /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
14
+ type UploadStorage = UploadHandlerOptions["storage"];
15
+ /**
16
+ * The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
17
+ * needed to make an RLS decision: the raw `request` (headers/cookies/auth), the
18
+ * `method`, the parsed `url`, and which `protocol` the handler speaks.
19
+ */
20
+ interface UploadAuthzContext {
21
+ /** The upload method being invoked (`POST` create, `PATCH` chunk, `HEAD` resume, `DELETE`). */
22
+ method: string;
23
+ /** The protocol this handler is mounted for. */
24
+ protocol: UploadProtocol;
25
+ /** The inbound request — inspect headers/cookies to resolve the caller's identity. */
26
+ request: Request;
27
+ /** The parsed request URL (query params, upload-id path segment). */
28
+ url: URL;
29
+ }
30
+ /** Options for {@link createUploadHandler}. */
31
+ interface CreateUploadHandlerOptions {
32
+ /**
33
+ * The RLS gate. Runs before every upload request and denies fail-closed:
34
+ * returning `false` **or throwing** yields a `403`. Omit only for a fully
35
+ * public bucket — the whole point of this handler over the admin path is
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.
41
+ */
42
+ authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
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
+ */
51
+ maxFileSize?: number;
52
+ /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
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;
63
+ /** The storage provider the bytes land in (R2 in prod, memory in tests). */
64
+ storage: UploadStorage;
65
+ }
66
+ /** The object returned by {@link createUploadHandler}. */
67
+ interface UploadHandler {
68
+ /**
69
+ * Handle one upload request. Runs the RLS gate, then delegates to the
70
+ * `@visulima/storage` protocol handler. Wire this into your Worker's routing
71
+ * for the path the client uploads to.
72
+ */
73
+ fetch: (request: Request) => Promise<Response>;
74
+ /** The protocol this handler speaks. */
75
+ protocol: UploadProtocol;
76
+ }
77
+ /** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
78
+ interface R2UploadStorageOptions {
79
+ /** R2 S3 API Access Key ID (from an R2 API token). */
80
+ accessKeyId: string;
81
+ /** Cloudflare account id — used to derive the R2 S3 endpoint host. */
82
+ accountId: string;
83
+ /** Target R2 bucket name. */
84
+ bucket: string;
85
+ /**
86
+ * Explicit R2 S3 endpoint. Defaults to
87
+ * `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
88
+ * jurisdiction (e.g. `<accountId>.eu.r2.cloudflarestorage.com`).
89
+ */
90
+ endpoint?: string;
91
+ /** Client-side multipart part size (bytes or a size string like `"16MB"`). */
92
+ partSize?: number | string;
93
+ /**
94
+ * Path prefix the handler is mounted on (must match the client endpoint's
95
+ * path). Default `"/"`.
96
+ */
97
+ path?: string;
98
+ }
99
+ /**
100
+ * Build an RLS-gated resumable upload handler over a `@visulima/storage`
101
+ * provider. Mount its {@link UploadHandler.fetch} on the route your client
102
+ * uploads to and drive it with `@visulima/storage-client`.
103
+ */
104
+ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
105
+ /**
106
+ * Build an R2-backed storage provider for {@link createUploadHandler} using
107
+ * `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
108
+ * AWS SDK). R2's S3 region alias is always `auto`.
109
+ *
110
+ * Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
111
+ * same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
112
+ * Worker the `aws-light` provider needs `nodejs_compat` (it imports
113
+ * `node:stream`).
114
+ */
115
+ declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
116
+ secretAccessKey: string;
117
+ }) => AwsLightStorage;
118
+ export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
@@ -0,0 +1 @@
1
+ import{DEFAULT_MAX_UPLOAD_BYTES as r,createR2UploadStorage as o,createUploadHandler as t}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-CeVvqDXZ.mjs";export{r as DEFAULT_MAX_UPLOAD_BYTES,o as createR2UploadStorage,t as createUploadHandler};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/storage",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.21",
4
4
  "description": "R2-backed storage for Lunora: typed buckets and signed URLs",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/storage"
26
26
  },
27
27
  "files": [
28
- "dist",
28
+ "./dist",
29
29
  "__assets__",
30
30
  "README.md",
31
31
  "LICENSE.md"
@@ -40,11 +40,21 @@
40
40
  "types": "./dist/index.d.ts",
41
41
  "import": "./dist/index.mjs"
42
42
  },
43
+ "./upload": {
44
+ "types": "./dist/upload.d.ts",
45
+ "import": "./dist/upload.mjs"
46
+ },
43
47
  "./package.json": "./package.json"
44
48
  },
45
49
  "publishConfig": {
46
50
  "access": "public"
47
51
  },
52
+ "dependencies": {
53
+ "@lunora/errors": "1.0.0-alpha.15",
54
+ "@lunora/platform": "1.0.0-alpha.6",
55
+ "@visulima/storage": "1.0.5",
56
+ "aws4fetch": "1.0.20"
57
+ },
48
58
  "engines": {
49
59
  "node": "^22.15.0 || >=24.11.0"
50
60
  }
@@ -1,70 +0,0 @@
1
- const REGION = "auto";
2
- const SERVICE = "s3";
3
- const ALGORITHM = "AWS4-HMAC-SHA256";
4
- const MIN_EXPIRES_SECONDS = 1;
5
- const MAX_EXPIRES_SECONDS = 7 * 24 * 60 * 60;
6
- const DEFAULT_EXPIRES_SECONDS = 900;
7
- const textEncoder = new TextEncoder();
8
- const compareEntries = (a, b) => {
9
- if (a[0] < b[0]) {
10
- return -1;
11
- }
12
- return a[0] > b[0] ? 1 : 0;
13
- };
14
- const toHex = (buffer) => {
15
- const bytes = new Uint8Array(buffer);
16
- let out = "";
17
- for (const byte of bytes) {
18
- out += byte.toString(16).padStart(2, "0");
19
- }
20
- return out;
21
- };
22
- const encodeRfc3986 = (value) => encodeURIComponent(value).replaceAll(/[!'()*]/gu, (char) => `%${char.codePointAt(0)?.toString(16).toUpperCase() ?? ""}`);
23
- const encodeKey = (key) => key.split("/").map((segment) => encodeRfc3986(segment)).join("/");
24
- const sha256Hex = async (input) => toHex(await crypto.subtle.digest("SHA-256", textEncoder.encode(input)));
25
- const hmac = async (key, message) => {
26
- const cryptoKey = await crypto.subtle.importKey("raw", key, { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
27
- return crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(message));
28
- };
29
- const deriveSigningKey = async (secretAccessKey, dateStamp) => {
30
- const dateKey = await hmac(textEncoder.encode(`AWS4${secretAccessKey}`), dateStamp);
31
- const regionKey = await hmac(dateKey, REGION);
32
- const serviceKey = await hmac(regionKey, SERVICE);
33
- return hmac(serviceKey, "aws4_request");
34
- };
35
- const endpointHost = (credentials) => {
36
- const infix = credentials.jurisdiction === void 0 ? "" : `${credentials.jurisdiction}.`;
37
- return `${credentials.accountId}.${infix}r2.cloudflarestorage.com`;
38
- };
39
- const formatAmzDate = (date) => {
40
- const amzDate = `${date.toISOString().replaceAll(/[:-]/gu, "").slice(0, 15)}Z`;
41
- return { amzDate, dateStamp: amzDate.slice(0, 8) };
42
- };
43
- const buildPresignedUrl = async (parameters) => {
44
- const { credentials, key } = parameters;
45
- const method = parameters.method ?? "GET";
46
- const requested = parameters.expiresInSeconds ?? DEFAULT_EXPIRES_SECONDS;
47
- const normalised = Number.isFinite(requested) ? requested : DEFAULT_EXPIRES_SECONDS;
48
- const expires = Math.min(Math.max(MIN_EXPIRES_SECONDS, Math.floor(normalised)), MAX_EXPIRES_SECONDS);
49
- const host = endpointHost(credentials);
50
- const date = new Date(parameters.now?.() ?? Date.now());
51
- const { amzDate, dateStamp } = formatAmzDate(date);
52
- const credentialScope = `${dateStamp}/${REGION}/${SERVICE}/aws4_request`;
53
- const canonicalUri = `/${encodeRfc3986(credentials.bucket)}/${encodeKey(key)}`;
54
- const query = [
55
- ["X-Amz-Algorithm", ALGORITHM],
56
- ["X-Amz-Credential", `${credentials.accessKeyId}/${credentialScope}`],
57
- ["X-Amz-Date", amzDate],
58
- ["X-Amz-Expires", expires.toString()],
59
- ["X-Amz-SignedHeaders", "host"]
60
- ];
61
- const canonicalQuery = query.map(([name, value]) => [encodeRfc3986(name), encodeRfc3986(value)]).toSorted(compareEntries).map(([name, value]) => `${name}=${value}`).join("&");
62
- const canonicalRequest = [method, canonicalUri, canonicalQuery, `host:${host}
63
- `, "host", "UNSIGNED-PAYLOAD"].join("\n");
64
- const stringToSign = [ALGORITHM, amzDate, credentialScope, await sha256Hex(canonicalRequest)].join("\n");
65
- const signingKey = await deriveSigningKey(credentials.secretAccessKey, dateStamp);
66
- const signature = toHex(await hmac(signingKey, stringToSign));
67
- return `https://${host}${canonicalUri}?${canonicalQuery}&X-Amz-Signature=${signature}`;
68
- };
69
-
70
- export { buildPresignedUrl };
@@ -1,107 +0,0 @@
1
- const textEncoder = new TextEncoder();
2
- const MAX_EXPIRES_IN_SECONDS = 7 * 24 * 60 * 60;
3
- const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+\-.]*:\/\//i;
4
- const LEADING_SLASH_RE = /^\//;
5
- const toBase64Url = (bytes) => {
6
- const binary = String.fromCodePoint(...bytes);
7
- return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
8
- };
9
- const fromBase64Url = (input) => {
10
- const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
11
- const binary = atob(padded);
12
- const bytes = new Uint8Array(binary.length);
13
- for (let index = 0; index < binary.length; index += 1) {
14
- bytes[index] = binary.codePointAt(index) ?? 0;
15
- }
16
- return bytes;
17
- };
18
- const keyCache = /* @__PURE__ */ new Map();
19
- const importHmacKey = async (secret) => {
20
- const cached = keyCache.get(secret);
21
- if (cached) {
22
- return cached;
23
- }
24
- const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
25
- keyCache.set(secret, keyPromise);
26
- return keyPromise;
27
- };
28
- const canonicalize = (method, host, key, exp, contentType) => {
29
- const base = `${method}
30
- ${host.toLowerCase()}
31
- ${key}
32
- ${String(exp)}`;
33
- return contentType === void 0 ? base : `${base}
34
- ${contentType}`;
35
- };
36
- const extractHost = (input) => {
37
- try {
38
- return new URL(input).host;
39
- } catch {
40
- const noScheme = input.replace(SCHEME_PREFIX_RE, "");
41
- return noScheme.split("/")[0] ?? "";
42
- }
43
- };
44
- const buildSignedUrl = async (args) => {
45
- const method = args.method ?? "GET";
46
- const expiresInSeconds = args.expiresInSeconds ?? 60 * 60;
47
- if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
48
- throw new Error("@lunora/storage: expiresInSeconds must be a positive finite number");
49
- }
50
- if (expiresInSeconds > MAX_EXPIRES_IN_SECONDS) {
51
- throw new Error(`@lunora/storage: expiresInSeconds must not exceed ${String(MAX_EXPIRES_IN_SECONDS)} (7 days)`);
52
- }
53
- const contentType = method === "PUT" ? args.contentType : void 0;
54
- const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
55
- const host = extractHost(args.baseUrl);
56
- const cryptoKey = await importHmacKey(args.secret);
57
- const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonicalize(method, host, args.key, exp, contentType)));
58
- const sig = toBase64Url(new Uint8Array(signature));
59
- const base = args.baseUrl.endsWith("/") ? args.baseUrl.slice(0, -1) : args.baseUrl;
60
- const safeKey = args.key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
61
- const ctParameter = contentType === void 0 ? "" : `&ct=${encodeURIComponent(contentType)}`;
62
- return `${base}/${safeKey}?exp=${String(exp)}&method=${method}&sig=${sig}${ctParameter}`;
63
- };
64
- const verifySignedUrl = async (input, secret, options) => {
65
- let url;
66
- try {
67
- url = input instanceof URL ? input : new URL(input);
68
- } catch {
69
- return { reason: "malformed", valid: false };
70
- }
71
- const expRaw = url.searchParams.get("exp");
72
- const exp = expRaw === null ? Number.NaN : Number(expRaw);
73
- const sig = url.searchParams.get("sig");
74
- const method = url.searchParams.get("method") ?? "GET";
75
- const contentType = url.searchParams.get("ct") ?? void 0;
76
- if (!sig || !Number.isInteger(exp)) {
77
- return { reason: "malformed", valid: false };
78
- }
79
- if (exp < Math.floor(Date.now() / 1e3)) {
80
- return { reason: "expired", valid: false };
81
- }
82
- if (method !== "GET" && method !== "PUT") {
83
- return { reason: "malformed", valid: false };
84
- }
85
- let key;
86
- let sigBytes;
87
- try {
88
- key = url.pathname.replace(LEADING_SLASH_RE, "").split("/").map((segment) => decodeURIComponent(segment)).join("/");
89
- sigBytes = fromBase64Url(sig);
90
- } catch {
91
- return { reason: "malformed", valid: false };
92
- }
93
- const host = options?.expectedHost === void 0 ? url.host : extractHost(options.expectedHost);
94
- const cryptoKey = await importHmacKey(secret);
95
- const valid = await crypto.subtle.verify(
96
- "HMAC",
97
- cryptoKey,
98
- sigBytes,
99
- textEncoder.encode(canonicalize(method, host, key, exp, contentType))
100
- );
101
- if (!valid) {
102
- return { reason: "bad_signature", valid: false };
103
- }
104
- return { contentType, key, method, valid: true };
105
- };
106
-
107
- export { buildSignedUrl, verifySignedUrl };
@@ -1,26 +0,0 @@
1
- const createBucketStorage = (buckets, options = {}) => {
2
- const names = Object.keys(buckets);
3
- const [firstName] = names;
4
- if (firstName === void 0) {
5
- throw new Error("@lunora/storage: createBucketStorage requires at least one bucket");
6
- }
7
- if (options.default !== void 0 && !buckets[options.default]) {
8
- throw new Error(`@lunora/storage: default bucket "${options.default}" is not in the bucket map (have: ${names.join(", ")})`);
9
- }
10
- const defaultTag = options.default ?? "default";
11
- const defaultBinding = buckets[defaultTag] ?? buckets[firstName];
12
- if (defaultBinding === void 0) {
13
- throw new Error(`@lunora/storage: default bucket "${defaultTag}" is not in the bucket map (have: ${names.join(", ")})`);
14
- }
15
- const addressable = [.../* @__PURE__ */ new Set([defaultTag, ...names])];
16
- const make = (name) => {
17
- const target = name === defaultTag ? defaultBinding : buckets[name];
18
- if (!target) {
19
- throw new Error(`@lunora/storage: no bucket registered for "${name}". Known buckets: ${addressable.join(", ")}`);
20
- }
21
- return { ...target, bucket: (next) => make(next), bucketName: name };
22
- };
23
- return make(defaultTag);
24
- };
25
-
26
- export { createBucketStorage };