@lunora/storage 1.0.0-alpha.6 → 1.0.0-alpha.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,7 +34,7 @@
34
34
 
35
35
  ---
36
36
 
37
- R2-backed file storage for Lunora. Wraps a Cloudflare `R2Bucket` binding with a typed API (`upload`/`store`, `download`, `delete`, `list`, `getMetadata`, multipart), worker-signed URLs for app-gated access, and native S3 presigned URLs for direct-to-R2 transfer.
37
+ R2-backed file storage for Lunora. Wraps a Cloudflare `R2Bucket` binding with a typed API (`upload`/`store`, `download`, `head`, `delete`, `list`, `getMetadata`, multipart), worker-signed URLs for app-gated access, and native S3 presigned URLs for direct-to-R2 transfer.
38
38
 
39
39
  Part of the [Lunora](https://github.com/anolilab/lunora) framework — a type-safe, real-time backend on Cloudflare Workers + Durable Objects with a Vite-first DX.
40
40
 
@@ -75,13 +75,15 @@ export default defineApp<Env>()
75
75
  import { action, v } from "@/lunora/_generated/server";
76
76
 
77
77
  // Minting an upload URL is a write capability, so this is an `action`.
78
- // Queries and mutations get a read-only `ctx.storage`; the full surface
79
- // (generateUploadUrl/store/delete/multipart) is action-only.
78
+ // Queries and mutations are TYPED with a read-only `ctx.storage`; the full
79
+ // surface (generateUploadUrl/store/delete/getPresignedUrl) is action-only.
80
+ // Multipart and `list` are on a `createStorage` instance, not on any ctx.
80
81
  export const uploadAvatar = action.input({ contentType: v.string() }).action(async ({ args, ctx }) => {
81
82
  const key = `avatars/${ctx.auth.userId ?? "anonymous"}/profile`;
82
83
 
83
- // Short-lived signed PUT URL — the client uploads straight to R2 through
84
- // your Worker route, with the Content-Type pinned into the signature.
84
+ // Short-lived signed PUT URL — the client uploads to your Worker route,
85
+ // which verifies the signature and writes to R2, with the Content-Type
86
+ // pinned into that signature.
85
87
  const url = await ctx.storage.generateUploadUrl(key, { contentType: args.contentType, expiresInSeconds: 60 });
86
88
 
87
89
  return { key, url };
@@ -95,6 +97,12 @@ import { createStorage } from "@lunora/storage";
95
97
 
96
98
  const storage = createStorage({
97
99
  bucket: env.FILES,
100
+ // Required. The name this bucket is registered under, bound into every signed
101
+ // URL's HMAC (and mirrored as `&bucket=`), so a URL minted for one bucket
102
+ // cannot be replayed against another sharing the signing secret. `"default"`
103
+ // for a single-bucket app; the registered name for anything reached through
104
+ // `createBucketStorage`.
105
+ bucketName: "default",
98
106
  publicBaseUrl: "https://cdn.acme.test",
99
107
  signingSecret: env.STORAGE_SECRET,
100
108
  });
@@ -104,12 +112,12 @@ await storage.upload("uploads/avatar.png", bytes, { contentType: "image/png", ma
104
112
  const url = await storage.getSignedUrl("uploads/avatar.png", { expiresInSeconds: 600 });
105
113
  ```
106
114
 
107
- > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/addons/storage)**.
115
+ > This README covers the basics. For the full API, options, and guides, see the **[documentation](https://lunora.sh/docs/packages/storage)**.
108
116
 
109
117
  ## Related
110
118
 
111
119
  - [`@lunora/server`](https://www.npmjs.com/package/@lunora/server) — call storage from queries, mutations, and actions.
112
- - [`@lunora/runtime`](https://www.npmjs.com/package/@lunora/runtime) — the Worker runtime that serves gated `GET /storage/:key` routes.
120
+ - [`@lunora/runtime`](https://www.npmjs.com/package/@lunora/runtime) — the Worker runtime your signed-URL route sits in front of. It ships no public storage route of its own: the route that verifies a signed URL and moves the bytes is yours to add (see the `storage` registry item).
113
121
  - [`@lunora/d1`](https://www.npmjs.com/package/@lunora/d1) — store object metadata alongside your data.
114
122
 
115
123
  ## Supported Node.js Versions
package/dist/index.d.mts CHANGED
@@ -1,140 +1,11 @@
1
+ import { R2MultipartUploadLike, R2RangeLike, R2ObjectBodyLike, R2ObjectLike, R2BucketLike } from '@lunora/platform';
2
+ export type { R2BucketLike, R2MultipartUploadLike, R2ObjectBodyLike, R2ObjectLike, R2RangeLike, R2UploadedPartLike } from '@lunora/platform';
1
3
  /**
2
- * A single-range read against R2: an `{ offset, length }` window (at least one
3
- * bound required, mirroring R2's own `R2Range`) or a `{ suffix }` tail. The
4
- * subset of `R2Range` that {@link Storage.download} forwards so a caller can
5
- * stream just the bytes it needs instead of the whole object.
6
- */
7
- type R2RangeLike = {
8
- length: number;
9
- offset?: number;
10
- } | {
11
- length?: number;
12
- offset: number;
13
- } | {
14
- suffix: number;
15
- };
16
- /**
17
- * Minimal projection of `R2Bucket`. Declared structurally so unit tests can
18
- * pass a plain object double; the real binding satisfies the same shape.
19
- */
20
- interface R2BucketLike {
21
- /**
22
- * Begin a multipart upload (R2 `createMultipartUpload`). Optional so existing
23
- * test doubles still satisfy the type; {@link Storage.createMultipartUpload}
24
- * throws a clear error when the binding lacks it.
25
- */
26
- createMultipartUpload?: (key: string, options?: {
27
- customMetadata?: Record<string, string>;
28
- httpMetadata?: {
29
- contentType?: string;
30
- };
31
- }) => Promise<R2MultipartUploadLike>;
32
- delete: (key: string) => Promise<void>;
33
- get: (key: string, options?: {
34
- range?: R2RangeLike;
35
- }) => Promise<R2ObjectBodyLike | null>;
36
- /**
37
- * Fetch an object's metadata without its body (R2 HEAD). Returns `null` when
38
- * the object is absent. Declared optional so existing test doubles that only
39
- * implement `get`/`put`/`list`/`delete` still satisfy the type; callers that
40
- * need metadata fall back to a 0-length ranged `get()` when `head` is absent.
41
- */
42
- head?: (key: string) => Promise<R2ObjectLike | null>;
43
- list: (options?: {
44
- cursor?: string;
45
- delimiter?: string;
46
- limit?: number;
47
- prefix?: string;
48
- }) => Promise<{
49
- cursor?: string;
50
- objects: R2ObjectLike[];
51
- truncated?: boolean;
52
- }>;
53
- put: (key: string, body: ReadableStream | ArrayBuffer | Blob | string | null, options?: {
54
- customMetadata?: Record<string, string>;
55
- httpMetadata?: {
56
- contentType?: string;
57
- };
58
- }) => Promise<R2ObjectLike>;
59
- /** Resume an in-progress multipart upload by id (R2 `resumeMultipartUpload`). Optional; see {@link Storage.resumeMultipartUpload}. */
60
- resumeMultipartUpload?: (key: string, uploadId: string) => R2MultipartUploadLike;
61
- }
62
- /** One uploaded multipart part — returned by `uploadPart`, required to `complete`. Mirrors R2's `R2UploadedPart`. */
63
- interface R2UploadedPartLike {
64
- etag: string;
65
- partNumber: number;
66
- }
67
- /**
68
- * An in-progress multipart upload, mirroring R2's `R2MultipartUpload`. Each part
69
- * (except the last) must be uniform in size. The object does not guarantee the
70
- * underlying upload still exists — a parallel `complete`/`abort` can invalidate
71
- * it — so wrap each call in error handling.
72
- */
73
- interface R2MultipartUploadLike {
74
- /** Abort the upload, discarding any uploaded parts. */
75
- abort: () => Promise<void>;
76
- /** Finish the upload from the collected parts; resolves to the stored object. */
77
- complete: (uploadedParts: R2UploadedPartLike[]) => Promise<R2ObjectLike>;
78
- /** The object key being assembled. */
79
- readonly key: string;
80
- /** The R2 upload id (persist it to resume across requests). */
81
- readonly uploadId: string;
82
- /** Upload one part (1-indexed); returns the `{ partNumber, etag }` to pass to `complete`. */
83
- uploadPart: (partNumber: number, value: ArrayBuffer | ArrayBufferView | Blob | ReadableStream | string) => Promise<R2UploadedPartLike>;
84
- }
85
- interface R2ObjectLike {
86
- /**
87
- * R2-computed checksums. The real binding exposes `sha256` as an
88
- * `ArrayBuffer` (present only when R2 stored a SHA-256 for the object);
89
- * declared optional so fakes and non-checksummed objects type-check.
90
- */
91
- checksums?: {
92
- sha256?: ArrayBuffer;
93
- };
94
- customMetadata?: Record<string, string>;
95
- etag: string;
96
- /**
97
- * The quoted form of {@link R2ObjectLike.etag} (e.g. `"abc123"`), suitable
98
- * for emitting directly as an HTTP `ETag` header. The real binding always
99
- * provides it; declared optional so existing doubles that only set `etag`
100
- * still type-check (callers fall back to quoting `etag`).
101
- */
102
- httpEtag?: string;
103
- httpMetadata?: {
104
- contentType?: string;
105
- };
106
- key: string;
107
- /**
108
- * Hex-encoded SHA-256 of the object body, surfaced by `download()`/`list()`
109
- * when R2 carries a checksum (derived from {@link R2ObjectLike.checksums}).
110
- */
111
- sha256?: string;
112
- /**
113
- * Base64-encoded SHA-256 of the object body, surfaced alongside
114
- * {@link R2ObjectLike.sha256} from the same checksum. Base64 is the encoding
115
- * RFC 9530 digest headers (`Repr-Digest`/`Content-Digest`) require, so HTTP
116
- * layers can emit a spec-compliant digest without re-deriving it.
117
- */
118
- sha256Base64?: string;
119
- size: number;
120
- /**
121
- * When the object was written. The real binding exposes this as a `Date`;
122
- * declared optional so fakes that omit it still type-check.
123
- * {@link Storage.getMetadata} normalises it to epoch ms.
124
- */
125
- uploaded?: Date;
126
- }
127
- interface R2ObjectBodyLike extends R2ObjectLike {
128
- arrayBuffer: () => Promise<ArrayBuffer>;
129
- body: ReadableStream | null;
130
- text: () => Promise<string>;
131
- }
132
- /**
133
- * R2 S3-API credentials for {@link Storage.getPresignedUrl}. These are an R2 API
134
- * token's Access Key ID / Secret Access Key (NOT a Cloudflare API token), plus
135
- * the account id and bucket name. Required only if you call `getPresignedUrl`;
136
- * the worker-signed URL path (`getSignedUrl`) needs none of this.
137
- */
4
+ * R2 S3-API credentials for {@link Storage.getPresignedUrl}. These are an R2 API
5
+ * token's Access Key ID / Secret Access Key (NOT a Cloudflare API token), plus
6
+ * the account id and bucket name. Required only if you call `getPresignedUrl`;
7
+ * the worker-signed URL path (`getSignedUrl`) needs none of this.
8
+ */
138
9
  interface R2S3Credentials {
139
10
  /** R2 S3 Access Key ID. */
140
11
  accessKeyId: string;
@@ -149,20 +20,34 @@ interface R2S3Credentials {
149
20
  }
150
21
  /** Options for {@link Storage.getPresignedUrl}. */
151
22
  interface PresignedUrlOptions {
152
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
23
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
153
24
  expiresInSeconds?: number;
154
25
  /** HTTP method the URL authorizes. Default `GET`. */
155
26
  method?: "GET" | "PUT";
156
27
  }
157
28
  interface LunoraStorageOptions {
158
29
  bucket: R2BucketLike;
30
+ /**
31
+ * The name this bucket is registered under — the same string a
32
+ * `defineStorageRule({ bucket })` rule and the generated `StorageBucketName`
33
+ * union use. Bound into every signed URL's HMAC and mirrored on it as
34
+ * `&bucket=`, so a URL minted for one bucket can't be replayed against
35
+ * another sharing the signing secret, and the serving route can resolve
36
+ * which bucket to read.
37
+ *
38
+ * Required, and deliberately without a default: a defaulted name is how
39
+ * every bucket ended up signing as `"default"` and cross-verifying against
40
+ * each other. Pass `"default"` for a single-bucket app's `ctx.storage`, and
41
+ * the registered name for any bucket reached through `createBucketStorage`.
42
+ */
43
+ bucketName: string;
159
44
  /** Public base URL used by `getSignedUrl()`. Required for signed URLs. */
160
45
  publicBaseUrl?: string;
161
46
  /**
162
- * R2 S3-API credentials enabling {@link Storage.getPresignedUrl} (native S3
163
- * presigned URLs that hit R2 directly, bypassing the Worker). Omit to use
164
- * only the worker-signed URL path.
165
- */
47
+ * R2 S3-API credentials enabling {@link Storage.getPresignedUrl} (native S3
48
+ * presigned URLs that hit R2 directly, bypassing the Worker). Omit to use
49
+ * only the worker-signed URL path.
50
+ */
166
51
  s3?: R2S3Credentials;
167
52
  /** HMAC secret used by the worker-signed URL helper. Required for signed URLs. */
168
53
  signingSecret?: string;
@@ -173,38 +58,59 @@ interface UploadOptions {
173
58
  contentType?: string;
174
59
  customMetadata?: Record<string, string>;
175
60
  /**
176
- * Maximum body size in bytes. For `ArrayBuffer`/`Blob` sources the length is
177
- * known up front and rejected before the upload starts. For a
178
- * `ReadableStream` the length isn't known synchronously, so the stream is
179
- * piped through a byte counter that aborts the upload once the limit is
180
- * exceeded — this also guards against R2 silently accepting/truncating an
181
- * unbounded stream.
182
- */
61
+ * Maximum body size in bytes. For `ArrayBuffer`/`Blob` sources the length is
62
+ * known up front and rejected before the upload starts.
63
+ *
64
+ * A `ReadableStream` has no length to check, and R2 refuses any stream whose
65
+ * length it cannot read — so a capped stream is READ INTO MEMORY under the
66
+ * cap and uploaded as a sized body. Nothing reaches the bucket if the body
67
+ * crosses the limit.
68
+ *
69
+ * `maxSize` is therefore also the memory ceiling for a streamed upload, and
70
+ * the isolate's ~128 MB is shared by every concurrent request — so on the
71
+ * stream path `maxSize` is itself capped at 16 MiB and rejected above it.
72
+ * For objects larger than that use `createMultipartUpload` /
73
+ * `createUploadHandler`, which upload without ever holding the whole object.
74
+ * Must be a finite, non-negative number; anything else (`Number(undefined)`
75
+ * is the usual source) is rejected as a `VALIDATION_ERROR`.
76
+ */
183
77
  maxSize?: number;
78
+ /**
79
+ * SHA-256 of the body (hex or a 32-byte buffer), recorded with the object.
80
+ *
81
+ * R2 only reports a checksum it was given: without this, `list()`/`head()`
82
+ * return no `sha256` and any later integrity check degrades to comparing
83
+ * sizes. R2 also verifies the digest itself on write, so supplying it turns
84
+ * the upload into a checked one.
85
+ */
86
+ sha256?: ArrayBuffer | string;
184
87
  }
185
88
  interface ListOptions {
186
89
  cursor?: string;
187
90
  /** R2 list delimiter — when set, common prefixes group instead of listing. */
188
91
  delimiter?: string;
189
- /** Defaults to 100, capped at 1000 (R2 limit). */
92
+ /**
93
+ * Defaults to 100, capped at 1000 (R2 limit). A ceiling, not a promise: R2
94
+ * may return fewer per page to fit the entry metadata.
95
+ */
190
96
  limit?: number;
191
97
  }
192
98
  interface SignedUrlOptions {
193
99
  /**
194
- * Pin the `Content-Type` an uploader must send on a `method: "PUT"` URL.
195
- * Baked into the HMAC canonical so the signature only authorizes a PUT with
196
- * exactly this content-type; mirrored on the URL as `&amp;ct=...`. Ignored for
197
- * `GET` URLs (a download has no request body content-type to pin).
198
- */
100
+ * Pin the `Content-Type` an uploader must send on a `method: "PUT"` URL.
101
+ * Baked into the HMAC canonical so the signature only authorizes a PUT with
102
+ * exactly this content-type; mirrored on the URL as `&ct=...`. Ignored for
103
+ * `GET` URLs (a download has no request body content-type to pin).
104
+ */
199
105
  contentType?: string;
200
106
  expiresInSeconds?: number;
201
107
  method?: "GET" | "PUT";
202
108
  }
203
109
  /**
204
- * Per-object metadata returned by {@link Storage.getMetadata} — a flat,
205
- * body-free projection of {@link R2ObjectLike}. Mirrors the shape Convex
206
- * surfaces for `ctx.storage.getMetadata` / the `_storage` system table.
207
- */
110
+ * Per-object metadata returned by {@link Storage.getMetadata} — a flat,
111
+ * body-free projection of {@link R2ObjectLike}. Mirrors the shape Convex
112
+ * surfaces for `ctx.storage.getMetadata` / the `_storage` system table.
113
+ */
208
114
  interface ObjectMetadata {
209
115
  /** The object's `Content-Type` (R2 `httpMetadata.contentType`), if recorded. */
210
116
  contentType?: string;
@@ -221,68 +127,103 @@ interface ObjectMetadata {
221
127
  }
222
128
  interface Storage {
223
129
  /**
224
- * Begin a native R2 **multipart upload** for very large objects — upload
225
- * parts (each uniform in size except the last), then `complete` with the
226
- * returned parts (or `abort`). Wraps R2's `createMultipartUpload`; throws if
227
- * the bound bucket doesn't support it. For ordinary uploads use
228
- * {@link Storage.upload} / {@link Storage.store}.
229
- */
130
+ * The bucket name this accessor operates under — the same value
131
+ * {@link Storage.getSignedUrl} puts into the HMAC canonical.
132
+ *
133
+ * Exposed so everything downstream agrees on one name: `asBucketStorage`
134
+ * tags a single-bucket storage with it, and `storageRules(...)` matches
135
+ * `(bucket, operation)` rules against that tag. Without it a
136
+ * `createStorage({ bucketName: "avatars" })` signed URLs as `avatars` while
137
+ * the rules engine only ever saw `"default"`.
138
+ */
139
+ readonly bucketName: string;
140
+ /**
141
+ * Begin a native R2 **multipart upload** for very large objects — upload
142
+ * parts (each uniform in size except the last), then `complete` with the
143
+ * returned parts (or `abort`). Wraps R2's `createMultipartUpload`; throws if
144
+ * the bound bucket doesn't support it. For ordinary uploads use
145
+ * {@link Storage.upload} / {@link Storage.store}.
146
+ */
230
147
  createMultipartUpload: (key: string, options?: {
231
148
  contentType?: string;
232
149
  customMetadata?: Record<string, string>;
233
150
  }) => Promise<R2MultipartUploadLike>;
234
151
  delete: (key: string) => Promise<void>;
235
152
  /**
236
- * Fetch a stored object's metadata + body. Pass `options.range` to stream
237
- * only a byte window (R2 resolves the range server-side, so the unwanted
238
- * bytes never reach the Worker) — `download(key)` reads the whole object.
239
- */
153
+ * Fetch a stored object's metadata + body. Pass `options.range` to stream
154
+ * only a byte window (R2 resolves the range server-side, so the unwanted
155
+ * bytes never reach the Worker) — `download(key)` reads the whole object.
156
+ */
240
157
  download: (key: string, options?: {
241
158
  range?: R2RangeLike;
242
159
  }) => Promise<R2ObjectBodyLike | null>;
243
160
  /**
244
- * Mint a short-lived signed `PUT` URL a client can upload directly to,
245
- * optionally pinning the request `Content-Type`. Convex-compatible alias
246
- * built on {@link Storage.getSignedUrl} with `method: "PUT"`.
247
- */
161
+ * Mint a short-lived signed `PUT` URL a client can upload directly to,
162
+ * optionally pinning the request `Content-Type`. Convex-compatible alias
163
+ * built on {@link Storage.getSignedUrl} with `method: "PUT"`.
164
+ */
248
165
  generateUploadUrl: (key: string, options?: {
249
166
  contentType?: string;
250
167
  expiresInSeconds?: number;
251
168
  }) => Promise<string>;
252
169
  /**
253
- * Read a stored object's metadata (size, content-type, sha256, upload time,
254
- * custom metadata) without fetching its body. Returns `null` when the object
255
- * is absent. Backed by an R2 HEAD (`bucket.head`) when available, falling
256
- * back to a 0-length ranged `get()` otherwise. Mirrors Convex's
257
- * `ctx.storage.getMetadata`.
258
- */
170
+ * Read a stored object's metadata (size, content-type, sha256, upload time,
171
+ * custom metadata) without fetching its body, as a flat serializable shape.
172
+ * Returns `null` when the object is absent. A projection of
173
+ * {@link Storage.head}, so it makes the same single body-free read. Mirrors
174
+ * Convex's `ctx.storage.getMetadata`.
175
+ */
259
176
  getMetadata: (key: string) => Promise<ObjectMetadata | null>;
260
177
  /**
261
- * Mint a native S3 **presigned URL** (SigV4) that hits R2 directly, bypassing
262
- * the Worker. Use for large downloads/uploads where you don't need per-request
263
- * app gating and want the bytes off the Worker's CPU/bandwidth budget. Requires
264
- * {@link LunoraStorageOptions.s3} credentials; throws if they're absent. For
265
- * app-gated access (auth/policy/rate-limit) prefer {@link Storage.getSignedUrl}.
266
- */
178
+ * Mint a native S3 **presigned URL** (SigV4) that hits R2 directly, bypassing
179
+ * the Worker. Use for large downloads/uploads where you don't need per-request
180
+ * app gating and want the bytes off the Worker's CPU/bandwidth budget. Requires
181
+ * {@link LunoraStorageOptions.s3} credentials; throws if they're absent. For
182
+ * app-gated access (auth/policy/rate-limit) prefer {@link Storage.getSignedUrl}.
183
+ */
267
184
  getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
268
185
  getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
269
186
  getUrl: (key: string) => string;
187
+ /**
188
+ * Read an object's R2 metadata WITHOUT its body — `size` (the full object
189
+ * size), `etag`, `httpMetadata`, `checksums`, plus the `sha256`/`sha256Base64`
190
+ * projection `download()` adds. Returns `null` when the object is absent.
191
+ *
192
+ * Backed by an R2 HEAD when the binding exposes one, falling back to a
193
+ * 0-length ranged `get()` otherwise. Prefer this over `download()` whenever
194
+ * only the metadata is wanted — notably to resolve a `Range` header, where a
195
+ * plain `download()` starts a full-object body transfer that is then thrown
196
+ * away. {@link Storage.getMetadata} is the flat, serializable projection of
197
+ * the same read.
198
+ */
199
+ head: (key: string) => Promise<R2ObjectLike | null>;
200
+ /**
201
+ * List objects under `prefix`. With `options.delimiter` set, keys sharing a
202
+ * segment are rolled up into `delimitedPrefixes` (the "folders") and are NOT
203
+ * in `objects` — a folder browser needs both, so a listing whose `objects` is
204
+ * empty is not an empty directory.
205
+ *
206
+ * A page may hold FEWER objects than `options.limit`: R2 shrinks a page to
207
+ * fit the per-entry metadata this call asks for. Paginate on `truncated` /
208
+ * `cursor`, never on `objects.length === limit`.
209
+ */
270
210
  list: (prefix?: string, options?: ListOptions) => Promise<{
271
211
  cursor?: string;
212
+ delimitedPrefixes?: string[];
272
213
  objects: R2ObjectLike[];
273
214
  truncated?: boolean;
274
215
  }>;
275
216
  /**
276
- * Resume an in-progress multipart upload by its `uploadId` (e.g. across
277
- * requests). Wraps R2's `resumeMultipartUpload`; the id is not validated by
278
- * R2, so a stale id surfaces as an error on the first `uploadPart`/`complete`.
279
- */
217
+ * Resume an in-progress multipart upload by its `uploadId` (e.g. across
218
+ * requests). Wraps R2's `resumeMultipartUpload`; the id is not validated by
219
+ * R2, so a stale id surfaces as an error on the first `uploadPart`/`complete`.
220
+ */
280
221
  resumeMultipartUpload: (key: string, uploadId: string) => R2MultipartUploadLike;
281
222
  /**
282
- * Upload `body` to `key`, returning the stored key + etag. Convex-compatible
283
- * alias for {@link Storage.upload} — it accepts the same {@link UploadOptions}
284
- * so the `maxSize` / `allowedContentTypes` guards aren't lost behind the alias.
285
- */
223
+ * Upload `body` to `key`, returning the stored key + etag. Convex-compatible
224
+ * alias for {@link Storage.upload} — it accepts the same {@link UploadOptions}
225
+ * so the `maxSize` / `allowedContentTypes` guards aren't lost behind the alias.
226
+ */
286
227
  store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: UploadOptions) => Promise<{
287
228
  etag: string;
288
229
  httpEtag: string;
@@ -295,11 +236,11 @@ interface Storage {
295
236
  }>;
296
237
  }
297
238
  /**
298
- * A bucket-aware {@link Storage}: the methods target a default bucket, and
299
- * `bucket(name)` selects a different named bucket (declared via
300
- * `v.storage("name")`). `bucketName` is the bucket the current accessor targets
301
- * — the storage-rules middleware reads it to scope `(bucket, operation)` rules.
302
- */
239
+ * A bucket-aware {@link Storage}: the methods target a default bucket, and
240
+ * `bucket(name)` selects a different named bucket (declared via
241
+ * `v.storage("name")`). `bucketName` is the bucket the current accessor targets
242
+ * — the storage-rules middleware reads it to scope `(bucket, operation)` rules.
243
+ */
303
244
  interface BucketStorage extends Storage {
304
245
  /** Select a named bucket. Unknown names throw with the list of registered buckets. */
305
246
  bucket: (name: string) => BucketStorage;
@@ -307,43 +248,39 @@ interface BucketStorage extends Storage {
307
248
  readonly bucketName: string;
308
249
  }
309
250
  /**
310
- * Compose several per-bucket {@link Storage} instances into one bucket-aware
311
- * accessor. The bare methods (`download` / `store` / …) target the default
312
- * bucket; `bucket(name)` switches to another. Each accessor is tagged with its
313
- * `bucketName` so `storageRules(...)` can enforce per-bucket.
314
- *
315
- * ```ts
316
- * storage: (env) => createBucketStorage({
317
- * default: createStorage({ bucket: env.FILES }),
318
- * avatars: createStorage({ bucket: env.AVATARS }),
319
- * }),
320
- * // → ctx.storage.download(key) // default bucket
321
- * // → ctx.storage.bucket("avatars").store() // the avatars bucket
322
- * ```
323
- *
324
- * The bare accessor is tagged `"default"` — the canonical name a
325
- * `defineStorageRule({ bucket: "default" })` rule and the generated
326
- * `StorageBucketName` union both use — unless `options.default` names another
327
- * bucket (then the bare accessor takes that name). The binding it delegates to is
328
- * `options.default`, else the `"default"` key when present, else the first
329
- * registered bucket. Named buckets are reached with `bucket(name)`.
330
- */
251
+ * Compose several per-bucket {@link Storage} instances into one bucket-aware
252
+ * accessor. The bare methods (`download` / `store` / …) target the default
253
+ * bucket; `bucket(name)` switches to another. Each accessor is tagged with its
254
+ * `bucketName` so `storageRules(...)` can enforce per-bucket.
255
+ *
256
+ * ```ts
257
+ * storage: (env) => createBucketStorage({
258
+ * default: createStorage({ bucket: env.FILES, bucketName: "default" }),
259
+ * avatars: createStorage({ bucket: env.AVATARS, bucketName: "avatars" }),
260
+ * }),
261
+ * // → ctx.storage.download(key) // default bucket
262
+ * // → ctx.storage.bucket("avatars").store() // the avatars bucket
263
+ * ```
264
+ *
265
+ * The bare accessor is tagged with the name of the binding it actually
266
+ * delegates to: `options.default` when given, else `"default"` when that key
267
+ * exists (the canonical name a `defineStorageRule({ bucket: "default" })` rule
268
+ * and the generated `StorageBucketName` union use), else the first registered
269
+ * bucket. Tag and binding must agree or a `{ bucket: "avatars" }` rule would
270
+ * gate `bucket("avatars").download()` and not the identical bare
271
+ * `ctx.storage.download()` reaching the same R2 bucket. Named buckets are
272
+ * reached with `bucket(name)`.
273
+ */
331
274
  declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
332
275
  default?: string;
333
276
  }) => BucketStorage;
334
- /**
335
- * Compose a per-tenant key from a scope prefix and a caller-supplied key.
336
- * Both halves are validated — the prefix may not contain `..` or NUL either,
337
- * and the resulting key must stay under R2's length ceiling. Recommended for
338
- * any multi-tenant deployment so client-supplied keys can't address peer data.
339
- */
340
277
  declare const scopeKey: (prefix: string, key: string) => string;
341
278
  declare const createStorage: (options: LunoraStorageOptions) => Storage;
342
279
  /** Parameters accepted by {@link buildPresignedUrl}. */
343
280
  interface PresignedUrlParams {
344
281
  /** R2 S3 API credentials + bucket/account. */
345
282
  credentials: R2S3Credentials;
346
- /** Seconds the URL stays valid; clamped to [1, 604800]. Default 900. */
283
+ /** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
347
284
  expiresInSeconds?: number;
348
285
  /** Object key (path-style; not URL-encoded by the caller). */
349
286
  key: string;
@@ -353,54 +290,67 @@ interface PresignedUrlParams {
353
290
  now?: () => number;
354
291
  }
355
292
  /**
356
- * Build a native S3 presigned URL for an R2 object using SigV4 query-string
357
- * auth. The returned URL points at R2's S3 endpoint and carries the full
358
- * signature, so it authorizes a single `GET`/`PUT` on `key` until it expires —
359
- * no Worker round-trip.
360
- */
293
+ * Build a native S3 presigned URL for an R2 object using SigV4 query-string
294
+ * auth. The returned URL points at R2's S3 endpoint and carries the full
295
+ * signature, so it authorizes a single `GET`/`PUT` on `key` until it expires —
296
+ * no Worker round-trip.
297
+ */
361
298
  declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<string>;
362
299
  /**
363
- * Worker-signed URL: the `publicBaseUrl` joined to the object `key`, plus a query
364
- * string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
365
- * (a base64url HMAC).
366
- *
367
- * The HMAC canonical includes the URL host so a signature minted for one bucket
368
- * cannot be replayed against another host on the same signing secret. Even so,
369
- * the signing secret MUST NOT be shared across buckets/tenants — host binding
370
- * narrows replay surface but is not a substitute for per-tenant key isolation.
371
- *
372
- * The Worker handling `GET /storage/:key` should call {@link verifySignedUrl}
373
- * to validate the signature + expiry before streaming the R2 body.
374
- */
375
- declare const buildSignedUrl: (args: SignedUrlOptions & {
300
+ * Worker-signed URL: the `publicBaseUrl` joined to the object `key`, plus a query
301
+ * string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
302
+ * (a base64url HMAC).
303
+ *
304
+ * The HMAC canonical includes the URL host AND the `bucketName`, so a signature
305
+ * minted for one bucket cannot be replayed against another bucket (all buckets
306
+ * of one `.storage()` declaration share a base URL and signing secret) or
307
+ * another host. Even so, the signing secret MUST NOT be shared across
308
+ * tenants — this binding narrows replay surface but is not a substitute for
309
+ * per-tenant key isolation.
310
+ *
311
+ * The Worker route handling the signed download/upload (mounted at
312
+ * `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
313
+ * {@link verifySignedUrl} to validate the signature + expiry before streaming
314
+ * the R2 body. `baseUrl` must be a bare origin (no path) — see the module
315
+ * docstring.
316
+ */
317
+ declare const buildSignedUrl: (args: {
376
318
  baseUrl: string;
319
+ /** The bucket the URL addresses — bound into the HMAC and mirrored as `&bucket=`. */
320
+ bucketName: string;
377
321
  key: string;
378
322
  secret: string;
379
- }) => Promise<string>;
323
+ } & SignedUrlOptions) => Promise<string>;
380
324
  interface VerifyResult {
325
+ /**
326
+ * The bucket the URL was minted for. The serving route MUST resolve its R2
327
+ * binding from this (never from a caller-supplied bucket), since one signing
328
+ * secret covers every bucket of a `.storage()` declaration.
329
+ */
330
+ bucketName?: string;
381
331
  /** The pinned upload `Content-Type` carried by a PUT URL, when present. */
382
332
  contentType?: string;
383
333
  key?: string;
384
334
  method?: "GET" | "PUT";
385
335
  /**
386
- * Internal-only failure reason for server logs/diagnostics. **Do not echo
387
- * to clients** — a precise reason ("expired" vs "bad_signature") is a
388
- * signing oracle. Public responses should expose only `valid`.
389
- */
336
+ * Internal-only failure reason for server logs/diagnostics. **Do not echo
337
+ * to clients** — a precise reason ("expired" vs "bad_signature") is a
338
+ * signing oracle. Public responses should expose only `valid`.
339
+ */
390
340
  reason?: "bad_signature" | "expired" | "malformed";
391
341
  valid: boolean;
392
342
  }
393
343
  /**
394
- * Verify a {@link buildSignedUrl} output. By default the signature is
395
- * canonicalized against the inbound `url.host`, which matches the build-side
396
- * host whenever the URL being verified is the URL that was minted. In a
397
- * topology where the host the Worker sees differs from the configured
398
- * `publicBaseUrl` host (e.g. a CDN host vs a Worker route that rewrites
399
- * `Host`), pass `expectedHost` (the `publicBaseUrl` host) so verification
400
- * canonicalizes against the same host the signature was minted for instead of
401
- * failing every request as `bad_signature`.
402
- */
344
+ * Verify a {@link buildSignedUrl} output. By default the signature is
345
+ * canonicalized against the inbound `url.host`, which matches the build-side
346
+ * host whenever the URL being verified is the URL that was minted. In a
347
+ * topology where the host the Worker sees differs from the configured
348
+ * `publicBaseUrl` host (e.g. a CDN host vs a Worker route that rewrites
349
+ * `Host`), pass `expectedHost` (the `publicBaseUrl` host) so verification
350
+ * canonicalizes against the same host the signature was minted for instead of
351
+ * failing every request as `bad_signature`.
352
+ */
403
353
  declare const verifySignedUrl: (input: string | URL, secret: string, options?: {
404
354
  expectedHost?: string;
405
355
  }) => Promise<VerifyResult>;
406
- export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type R2BucketLike, type R2MultipartUploadLike, type R2ObjectBodyLike, type R2ObjectLike, type R2RangeLike, type R2S3Credentials, type R2UploadedPartLike, type SignedUrlOptions, type Storage, type UploadOptions, type VerifyResult, buildPresignedUrl, buildSignedUrl, createBucketStorage, createStorage, scopeKey, verifySignedUrl };
356
+ export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type R2S3Credentials, type SignedUrlOptions, type Storage, type UploadOptions, type VerifyResult, buildPresignedUrl, buildSignedUrl, createBucketStorage, createStorage, scopeKey, verifySignedUrl };