@lunora/storage 1.0.0-alpha.10 → 1.0.0-alpha.11

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,54 @@
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 };
@@ -0,0 +1,91 @@
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
+ type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
6
+ /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
7
+ type UploadStorage = UploadHandlerOptions["storage"];
8
+ /**
9
+ * The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
10
+ * needed to make an RLS decision: the raw `request` (headers/cookies/auth), the
11
+ * `method`, the parsed `url`, and which `protocol` the handler speaks.
12
+ */
13
+ interface UploadAuthzContext {
14
+ /** The upload method being invoked (`POST` create, `PATCH` chunk, `HEAD` resume, `DELETE`). */
15
+ method: string;
16
+ /** The protocol this handler is mounted for. */
17
+ protocol: UploadProtocol;
18
+ /** The inbound request — inspect headers/cookies to resolve the caller's identity. */
19
+ request: Request;
20
+ /** The parsed request URL (query params, upload-id path segment). */
21
+ url: URL;
22
+ }
23
+ /** Options for {@link createUploadHandler}. */
24
+ interface CreateUploadHandlerOptions {
25
+ /**
26
+ * The RLS gate. Runs before every upload request and denies fail-closed:
27
+ * returning `false` **or throwing** yields a `403`. Omit only for a fully
28
+ * public bucket — the whole point of this handler over the admin path is
29
+ * that uploads are gated by *your* per-user policy, not an admin token.
30
+ */
31
+ authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
32
+ /** Maximum accepted file size in bytes (forwarded to the multipart parser). */
33
+ maxFileSize?: number;
34
+ /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
35
+ protocol?: UploadProtocol;
36
+ /** The storage provider the bytes land in (R2 in prod, memory in tests). */
37
+ storage: UploadStorage;
38
+ }
39
+ /** The object returned by {@link createUploadHandler}. */
40
+ interface UploadHandler {
41
+ /**
42
+ * Handle one upload request. Runs the RLS gate, then delegates to the
43
+ * `@visulima/storage` protocol handler. Wire this into your Worker's routing
44
+ * for the path the client uploads to.
45
+ */
46
+ fetch: (request: Request) => Promise<Response>;
47
+ /** The protocol this handler speaks. */
48
+ protocol: UploadProtocol;
49
+ }
50
+ /** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
51
+ interface R2UploadStorageOptions {
52
+ /** R2 S3 API Access Key ID (from an R2 API token). */
53
+ accessKeyId: string;
54
+ /** Cloudflare account id — used to derive the R2 S3 endpoint host. */
55
+ accountId: string;
56
+ /** Target R2 bucket name. */
57
+ bucket: string;
58
+ /**
59
+ * Explicit R2 S3 endpoint. Defaults to
60
+ * `https://&lt;accountId>.r2.cloudflarestorage.com`. Pass this to pin a
61
+ * jurisdiction (e.g. `&lt;accountId>.eu.r2.cloudflarestorage.com`).
62
+ */
63
+ endpoint?: string;
64
+ /** Client-side multipart part size (bytes or a size string like `"16MB"`). */
65
+ partSize?: number | string;
66
+ /**
67
+ * Path prefix the handler is mounted on (must match the client endpoint's
68
+ * path). Default `"/"`.
69
+ */
70
+ path?: string;
71
+ }
72
+ /**
73
+ * Build an RLS-gated resumable upload handler over a `@visulima/storage`
74
+ * provider. Mount its {@link UploadHandler.fetch} on the route your client
75
+ * uploads to and drive it with `@visulima/storage-client`.
76
+ */
77
+ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
78
+ /**
79
+ * Build an R2-backed storage provider for {@link createUploadHandler} using
80
+ * `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
81
+ * AWS SDK). R2's S3 region alias is always `auto`.
82
+ *
83
+ * Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
84
+ * same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
85
+ * Worker the `aws-light` provider needs `nodejs_compat` (it imports
86
+ * `node:stream`).
87
+ */
88
+ declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
89
+ secretAccessKey: string;
90
+ }) => AwsLightStorage;
91
+ export { type CreateUploadHandlerOptions, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
@@ -0,0 +1,91 @@
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
+ type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
6
+ /** A `@visulima/storage` storage provider (e.g. {@link createR2UploadStorage} or a memory provider in tests). */
7
+ type UploadStorage = UploadHandlerOptions["storage"];
8
+ /**
9
+ * The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
10
+ * needed to make an RLS decision: the raw `request` (headers/cookies/auth), the
11
+ * `method`, the parsed `url`, and which `protocol` the handler speaks.
12
+ */
13
+ interface UploadAuthzContext {
14
+ /** The upload method being invoked (`POST` create, `PATCH` chunk, `HEAD` resume, `DELETE`). */
15
+ method: string;
16
+ /** The protocol this handler is mounted for. */
17
+ protocol: UploadProtocol;
18
+ /** The inbound request — inspect headers/cookies to resolve the caller's identity. */
19
+ request: Request;
20
+ /** The parsed request URL (query params, upload-id path segment). */
21
+ url: URL;
22
+ }
23
+ /** Options for {@link createUploadHandler}. */
24
+ interface CreateUploadHandlerOptions {
25
+ /**
26
+ * The RLS gate. Runs before every upload request and denies fail-closed:
27
+ * returning `false` **or throwing** yields a `403`. Omit only for a fully
28
+ * public bucket — the whole point of this handler over the admin path is
29
+ * that uploads are gated by *your* per-user policy, not an admin token.
30
+ */
31
+ authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
32
+ /** Maximum accepted file size in bytes (forwarded to the multipart parser). */
33
+ maxFileSize?: number;
34
+ /** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
35
+ protocol?: UploadProtocol;
36
+ /** The storage provider the bytes land in (R2 in prod, memory in tests). */
37
+ storage: UploadStorage;
38
+ }
39
+ /** The object returned by {@link createUploadHandler}. */
40
+ interface UploadHandler {
41
+ /**
42
+ * Handle one upload request. Runs the RLS gate, then delegates to the
43
+ * `@visulima/storage` protocol handler. Wire this into your Worker's routing
44
+ * for the path the client uploads to.
45
+ */
46
+ fetch: (request: Request) => Promise<Response>;
47
+ /** The protocol this handler speaks. */
48
+ protocol: UploadProtocol;
49
+ }
50
+ /** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
51
+ interface R2UploadStorageOptions {
52
+ /** R2 S3 API Access Key ID (from an R2 API token). */
53
+ accessKeyId: string;
54
+ /** Cloudflare account id — used to derive the R2 S3 endpoint host. */
55
+ accountId: string;
56
+ /** Target R2 bucket name. */
57
+ bucket: string;
58
+ /**
59
+ * Explicit R2 S3 endpoint. Defaults to
60
+ * `https://&lt;accountId>.r2.cloudflarestorage.com`. Pass this to pin a
61
+ * jurisdiction (e.g. `&lt;accountId>.eu.r2.cloudflarestorage.com`).
62
+ */
63
+ endpoint?: string;
64
+ /** Client-side multipart part size (bytes or a size string like `"16MB"`). */
65
+ partSize?: number | string;
66
+ /**
67
+ * Path prefix the handler is mounted on (must match the client endpoint's
68
+ * path). Default `"/"`.
69
+ */
70
+ path?: string;
71
+ }
72
+ /**
73
+ * Build an RLS-gated resumable upload handler over a `@visulima/storage`
74
+ * provider. Mount its {@link UploadHandler.fetch} on the route your client
75
+ * uploads to and drive it with `@visulima/storage-client`.
76
+ */
77
+ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
78
+ /**
79
+ * Build an R2-backed storage provider for {@link createUploadHandler} using
80
+ * `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
81
+ * AWS SDK). R2's S3 region alias is always `auto`.
82
+ *
83
+ * Requires an R2 **S3 API** token's Access Key ID / Secret Access Key — the
84
+ * same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
85
+ * Worker the `aws-light` provider needs `nodejs_compat` (it imports
86
+ * `node:stream`).
87
+ */
88
+ declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
89
+ secretAccessKey: string;
90
+ }) => AwsLightStorage;
91
+ export { type CreateUploadHandlerOptions, type R2UploadStorageOptions, type UploadAuthzContext, type UploadHandler, type UploadProtocol, type UploadStorage, createR2UploadStorage, createUploadHandler };
@@ -0,0 +1 @@
1
+ export { createR2UploadStorage, createUploadHandler } from './packem_shared/createR2UploadStorage-BfLIpWdf.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/storage",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.11",
4
4
  "description": "R2-backed storage for Lunora: typed buckets and signed URLs",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -40,13 +40,19 @@
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
  },
48
52
  "dependencies": {
49
- "@lunora/errors": "1.0.0-alpha.6"
53
+ "@lunora/errors": "1.0.0-alpha.7",
54
+ "@visulima/storage": "1.0.5",
55
+ "aws4fetch": "1.0.20"
50
56
  },
51
57
  "engines": {
52
58
  "node": "^22.15.0 || >=24.11.0"