@lunora/storage 1.0.0-alpha.5 → 1.0.0-alpha.51
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/LICENSE.md +6 -0
- package/README.md +15 -7
- package/dist/index.d.mts +191 -257
- package/dist/index.d.ts +191 -257
- package/dist/index.mjs +1 -4
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-Dhj02auN.mjs +1 -0
- package/dist/packem_shared/buildPresignedUrl-U55ktL8x.mjs +4 -0
- package/dist/packem_shared/buildSignedUrl-czz1xjLU.mjs +6 -0
- package/dist/packem_shared/createBucketStorage-D9fBoaEi.mjs +1 -0
- package/dist/packem_shared/createStorage-B-E6dH3Q.mjs +1 -0
- package/dist/packem_shared/internal-B5yyxF10.mjs +1 -0
- package/dist/upload.d.mts +172 -0
- package/dist/upload.d.ts +172 -0
- package/dist/upload.mjs +1 -0
- package/package.json +9 -2
- package/dist/packem_shared/buildPresignedUrl-DzPwi1bY.mjs +0 -70
- package/dist/packem_shared/buildSignedUrl-DX535yKv.mjs +0 -116
- package/dist/packem_shared/createBucketStorage-JYdrS4e5.mjs +0 -28
- package/dist/packem_shared/createStorage-fwSr3UdU.mjs +0 -255
package/LICENSE.md
CHANGED
|
@@ -103,3 +103,9 @@ Unless required by applicable law or agreed to in writing, software distributed
|
|
|
103
103
|
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
104
|
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
105
|
specific language governing permissions and limitations under the License.
|
|
106
|
+
|
|
107
|
+
<!-- DEPENDENCIES -->
|
|
108
|
+
<!-- /DEPENDENCIES -->
|
|
109
|
+
|
|
110
|
+
<!-- TYPE_DEPENDENCIES -->
|
|
111
|
+
<!-- /TYPE_DEPENDENCIES -->
|
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
|
|
79
|
-
// (generateUploadUrl/store/delete/
|
|
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
|
|
84
|
-
//
|
|
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/
|
|
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
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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,14 +58,23 @@ interface UploadOptions {
|
|
|
173
58
|
contentType?: string;
|
|
174
59
|
customMetadata?: Record<string, string>;
|
|
175
60
|
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
61
|
+
* Maximum body size in bytes. For `ArrayBuffer`/`Blob` sources the length is
|
|
62
|
+
* known up front and rejected before the upload starts. For a
|
|
63
|
+
* `ReadableStream` the length isn't known synchronously, so the stream is
|
|
64
|
+
* piped through a byte counter that aborts the upload once the limit is
|
|
65
|
+
* exceeded — this also guards against R2 silently accepting/truncating an
|
|
66
|
+
* unbounded stream.
|
|
67
|
+
*/
|
|
183
68
|
maxSize?: number;
|
|
69
|
+
/**
|
|
70
|
+
* SHA-256 of the body (hex or a 32-byte buffer), recorded with the object.
|
|
71
|
+
*
|
|
72
|
+
* R2 only reports a checksum it was given: without this, `list()`/`head()`
|
|
73
|
+
* return no `sha256` and any later integrity check degrades to comparing
|
|
74
|
+
* sizes. R2 also verifies the digest itself on write, so supplying it turns
|
|
75
|
+
* the upload into a checked one.
|
|
76
|
+
*/
|
|
77
|
+
sha256?: ArrayBuffer | string;
|
|
184
78
|
}
|
|
185
79
|
interface ListOptions {
|
|
186
80
|
cursor?: string;
|
|
@@ -191,20 +85,20 @@ interface ListOptions {
|
|
|
191
85
|
}
|
|
192
86
|
interface SignedUrlOptions {
|
|
193
87
|
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
88
|
+
* Pin the `Content-Type` an uploader must send on a `method: "PUT"` URL.
|
|
89
|
+
* Baked into the HMAC canonical so the signature only authorizes a PUT with
|
|
90
|
+
* exactly this content-type; mirrored on the URL as `&ct=...`. Ignored for
|
|
91
|
+
* `GET` URLs (a download has no request body content-type to pin).
|
|
92
|
+
*/
|
|
199
93
|
contentType?: string;
|
|
200
94
|
expiresInSeconds?: number;
|
|
201
95
|
method?: "GET" | "PUT";
|
|
202
96
|
}
|
|
203
97
|
/**
|
|
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
|
-
*/
|
|
98
|
+
* Per-object metadata returned by {@link Storage.getMetadata} — a flat,
|
|
99
|
+
* body-free projection of {@link R2ObjectLike}. Mirrors the shape Convex
|
|
100
|
+
* surfaces for `ctx.storage.getMetadata` / the `_storage` system table.
|
|
101
|
+
*/
|
|
208
102
|
interface ObjectMetadata {
|
|
209
103
|
/** The object's `Content-Type` (R2 `httpMetadata.contentType`), if recorded. */
|
|
210
104
|
contentType?: string;
|
|
@@ -221,68 +115,99 @@ interface ObjectMetadata {
|
|
|
221
115
|
}
|
|
222
116
|
interface Storage {
|
|
223
117
|
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
118
|
+
* The bucket name this accessor operates under — the same value
|
|
119
|
+
* {@link Storage.getSignedUrl} puts into the HMAC canonical.
|
|
120
|
+
*
|
|
121
|
+
* Exposed so everything downstream agrees on one name: `asBucketStorage`
|
|
122
|
+
* tags a single-bucket storage with it, and `storageRules(...)` matches
|
|
123
|
+
* `(bucket, operation)` rules against that tag. Without it a
|
|
124
|
+
* `createStorage({ bucketName: "avatars" })` signed URLs as `avatars` while
|
|
125
|
+
* the rules engine only ever saw `"default"`.
|
|
126
|
+
*/
|
|
127
|
+
readonly bucketName: string;
|
|
128
|
+
/**
|
|
129
|
+
* Begin a native R2 **multipart upload** for very large objects — upload
|
|
130
|
+
* parts (each uniform in size except the last), then `complete` with the
|
|
131
|
+
* returned parts (or `abort`). Wraps R2's `createMultipartUpload`; throws if
|
|
132
|
+
* the bound bucket doesn't support it. For ordinary uploads use
|
|
133
|
+
* {@link Storage.upload} / {@link Storage.store}.
|
|
134
|
+
*/
|
|
230
135
|
createMultipartUpload: (key: string, options?: {
|
|
231
136
|
contentType?: string;
|
|
232
137
|
customMetadata?: Record<string, string>;
|
|
233
138
|
}) => Promise<R2MultipartUploadLike>;
|
|
234
139
|
delete: (key: string) => Promise<void>;
|
|
235
140
|
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
141
|
+
* Fetch a stored object's metadata + body. Pass `options.range` to stream
|
|
142
|
+
* only a byte window (R2 resolves the range server-side, so the unwanted
|
|
143
|
+
* bytes never reach the Worker) — `download(key)` reads the whole object.
|
|
144
|
+
*/
|
|
240
145
|
download: (key: string, options?: {
|
|
241
146
|
range?: R2RangeLike;
|
|
242
147
|
}) => Promise<R2ObjectBodyLike | null>;
|
|
243
148
|
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
149
|
+
* Mint a short-lived signed `PUT` URL a client can upload directly to,
|
|
150
|
+
* optionally pinning the request `Content-Type`. Convex-compatible alias
|
|
151
|
+
* built on {@link Storage.getSignedUrl} with `method: "PUT"`.
|
|
152
|
+
*/
|
|
248
153
|
generateUploadUrl: (key: string, options?: {
|
|
249
154
|
contentType?: string;
|
|
250
155
|
expiresInSeconds?: number;
|
|
251
156
|
}) => Promise<string>;
|
|
252
157
|
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
158
|
+
* Read a stored object's metadata (size, content-type, sha256, upload time,
|
|
159
|
+
* custom metadata) without fetching its body, as a flat serializable shape.
|
|
160
|
+
* Returns `null` when the object is absent. A projection of
|
|
161
|
+
* {@link Storage.head}, so it makes the same single body-free read. Mirrors
|
|
162
|
+
* Convex's `ctx.storage.getMetadata`.
|
|
163
|
+
*/
|
|
259
164
|
getMetadata: (key: string) => Promise<ObjectMetadata | null>;
|
|
260
165
|
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
166
|
+
* Mint a native S3 **presigned URL** (SigV4) that hits R2 directly, bypassing
|
|
167
|
+
* the Worker. Use for large downloads/uploads where you don't need per-request
|
|
168
|
+
* app gating and want the bytes off the Worker's CPU/bandwidth budget. Requires
|
|
169
|
+
* {@link LunoraStorageOptions.s3} credentials; throws if they're absent. For
|
|
170
|
+
* app-gated access (auth/policy/rate-limit) prefer {@link Storage.getSignedUrl}.
|
|
171
|
+
*/
|
|
267
172
|
getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
|
|
268
173
|
getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
|
|
269
174
|
getUrl: (key: string) => string;
|
|
175
|
+
/**
|
|
176
|
+
* Read an object's R2 metadata WITHOUT its body — `size` (the full object
|
|
177
|
+
* size), `etag`, `httpMetadata`, `checksums`, plus the `sha256`/`sha256Base64`
|
|
178
|
+
* projection `download()` adds. Returns `null` when the object is absent.
|
|
179
|
+
*
|
|
180
|
+
* Backed by an R2 HEAD when the binding exposes one, falling back to a
|
|
181
|
+
* 0-length ranged `get()` otherwise. Prefer this over `download()` whenever
|
|
182
|
+
* only the metadata is wanted — notably to resolve a `Range` header, where a
|
|
183
|
+
* plain `download()` starts a full-object body transfer that is then thrown
|
|
184
|
+
* away. {@link Storage.getMetadata} is the flat, serializable projection of
|
|
185
|
+
* the same read.
|
|
186
|
+
*/
|
|
187
|
+
head: (key: string) => Promise<R2ObjectLike | null>;
|
|
188
|
+
/**
|
|
189
|
+
* List objects under `prefix`. With `options.delimiter` set, keys sharing a
|
|
190
|
+
* segment are rolled up into `delimitedPrefixes` (the "folders") and are NOT
|
|
191
|
+
* in `objects` — a folder browser needs both, so a listing whose `objects` is
|
|
192
|
+
* empty is not an empty directory.
|
|
193
|
+
*/
|
|
270
194
|
list: (prefix?: string, options?: ListOptions) => Promise<{
|
|
271
195
|
cursor?: string;
|
|
196
|
+
delimitedPrefixes?: string[];
|
|
272
197
|
objects: R2ObjectLike[];
|
|
273
198
|
truncated?: boolean;
|
|
274
199
|
}>;
|
|
275
200
|
/**
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
201
|
+
* Resume an in-progress multipart upload by its `uploadId` (e.g. across
|
|
202
|
+
* requests). Wraps R2's `resumeMultipartUpload`; the id is not validated by
|
|
203
|
+
* R2, so a stale id surfaces as an error on the first `uploadPart`/`complete`.
|
|
204
|
+
*/
|
|
280
205
|
resumeMultipartUpload: (key: string, uploadId: string) => R2MultipartUploadLike;
|
|
281
206
|
/**
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
207
|
+
* Upload `body` to `key`, returning the stored key + etag. Convex-compatible
|
|
208
|
+
* alias for {@link Storage.upload} — it accepts the same {@link UploadOptions}
|
|
209
|
+
* so the `maxSize` / `allowedContentTypes` guards aren't lost behind the alias.
|
|
210
|
+
*/
|
|
286
211
|
store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: UploadOptions) => Promise<{
|
|
287
212
|
etag: string;
|
|
288
213
|
httpEtag: string;
|
|
@@ -295,11 +220,11 @@ interface Storage {
|
|
|
295
220
|
}>;
|
|
296
221
|
}
|
|
297
222
|
/**
|
|
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
|
-
*/
|
|
223
|
+
* A bucket-aware {@link Storage}: the methods target a default bucket, and
|
|
224
|
+
* `bucket(name)` selects a different named bucket (declared via
|
|
225
|
+
* `v.storage("name")`). `bucketName` is the bucket the current accessor targets
|
|
226
|
+
* — the storage-rules middleware reads it to scope `(bucket, operation)` rules.
|
|
227
|
+
*/
|
|
303
228
|
interface BucketStorage extends Storage {
|
|
304
229
|
/** Select a named bucket. Unknown names throw with the list of registered buckets. */
|
|
305
230
|
bucket: (name: string) => BucketStorage;
|
|
@@ -307,43 +232,39 @@ interface BucketStorage extends Storage {
|
|
|
307
232
|
readonly bucketName: string;
|
|
308
233
|
}
|
|
309
234
|
/**
|
|
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
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
|
|
235
|
+
* Compose several per-bucket {@link Storage} instances into one bucket-aware
|
|
236
|
+
* accessor. The bare methods (`download` / `store` / …) target the default
|
|
237
|
+
* bucket; `bucket(name)` switches to another. Each accessor is tagged with its
|
|
238
|
+
* `bucketName` so `storageRules(...)` can enforce per-bucket.
|
|
239
|
+
*
|
|
240
|
+
* ```ts
|
|
241
|
+
* storage: (env) => createBucketStorage({
|
|
242
|
+
* default: createStorage({ bucket: env.FILES, bucketName: "default" }),
|
|
243
|
+
* avatars: createStorage({ bucket: env.AVATARS, bucketName: "avatars" }),
|
|
244
|
+
* }),
|
|
245
|
+
* // → ctx.storage.download(key) // default bucket
|
|
246
|
+
* // → ctx.storage.bucket("avatars").store() // the avatars bucket
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* The bare accessor is tagged with the name of the binding it actually
|
|
250
|
+
* delegates to: `options.default` when given, else `"default"` when that key
|
|
251
|
+
* exists (the canonical name a `defineStorageRule({ bucket: "default" })` rule
|
|
252
|
+
* and the generated `StorageBucketName` union use), else the first registered
|
|
253
|
+
* bucket. Tag and binding must agree or a `{ bucket: "avatars" }` rule would
|
|
254
|
+
* gate `bucket("avatars").download()` and not the identical bare
|
|
255
|
+
* `ctx.storage.download()` reaching the same R2 bucket. Named buckets are
|
|
256
|
+
* reached with `bucket(name)`.
|
|
257
|
+
*/
|
|
331
258
|
declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
|
|
332
259
|
default?: string;
|
|
333
260
|
}) => 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
261
|
declare const scopeKey: (prefix: string, key: string) => string;
|
|
341
262
|
declare const createStorage: (options: LunoraStorageOptions) => Storage;
|
|
342
263
|
/** Parameters accepted by {@link buildPresignedUrl}. */
|
|
343
264
|
interface PresignedUrlParams {
|
|
344
265
|
/** R2 S3 API credentials + bucket/account. */
|
|
345
266
|
credentials: R2S3Credentials;
|
|
346
|
-
/** Seconds the URL stays valid
|
|
267
|
+
/** Seconds the URL stays valid: 1 to 604800 (7 days); an out-of-range value throws. Default 900. */
|
|
347
268
|
expiresInSeconds?: number;
|
|
348
269
|
/** Object key (path-style; not URL-encoded by the caller). */
|
|
349
270
|
key: string;
|
|
@@ -353,54 +274,67 @@ interface PresignedUrlParams {
|
|
|
353
274
|
now?: () => number;
|
|
354
275
|
}
|
|
355
276
|
/**
|
|
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
|
-
*/
|
|
277
|
+
* Build a native S3 presigned URL for an R2 object using SigV4 query-string
|
|
278
|
+
* auth. The returned URL points at R2's S3 endpoint and carries the full
|
|
279
|
+
* signature, so it authorizes a single `GET`/`PUT` on `key` until it expires —
|
|
280
|
+
* no Worker round-trip.
|
|
281
|
+
*/
|
|
361
282
|
declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<string>;
|
|
362
283
|
/**
|
|
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
|
|
368
|
-
* cannot be replayed against another
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
|
|
375
|
-
|
|
284
|
+
* Worker-signed URL: the `publicBaseUrl` joined to the object `key`, plus a query
|
|
285
|
+
* string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
|
|
286
|
+
* (a base64url HMAC).
|
|
287
|
+
*
|
|
288
|
+
* The HMAC canonical includes the URL host AND the `bucketName`, so a signature
|
|
289
|
+
* minted for one bucket cannot be replayed against another bucket (all buckets
|
|
290
|
+
* of one `.storage()` declaration share a base URL and signing secret) or
|
|
291
|
+
* another host. Even so, the signing secret MUST NOT be shared across
|
|
292
|
+
* tenants — this binding narrows replay surface but is not a substitute for
|
|
293
|
+
* per-tenant key isolation.
|
|
294
|
+
*
|
|
295
|
+
* The Worker route handling the signed download/upload (mounted at
|
|
296
|
+
* `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
|
|
297
|
+
* {@link verifySignedUrl} to validate the signature + expiry before streaming
|
|
298
|
+
* the R2 body. `baseUrl` must be a bare origin (no path) — see the module
|
|
299
|
+
* docstring.
|
|
300
|
+
*/
|
|
301
|
+
declare const buildSignedUrl: (args: {
|
|
376
302
|
baseUrl: string;
|
|
303
|
+
/** The bucket the URL addresses — bound into the HMAC and mirrored as `&bucket=`. */
|
|
304
|
+
bucketName: string;
|
|
377
305
|
key: string;
|
|
378
306
|
secret: string;
|
|
379
|
-
}) => Promise<string>;
|
|
307
|
+
} & SignedUrlOptions) => Promise<string>;
|
|
380
308
|
interface VerifyResult {
|
|
309
|
+
/**
|
|
310
|
+
* The bucket the URL was minted for. The serving route MUST resolve its R2
|
|
311
|
+
* binding from this (never from a caller-supplied bucket), since one signing
|
|
312
|
+
* secret covers every bucket of a `.storage()` declaration.
|
|
313
|
+
*/
|
|
314
|
+
bucketName?: string;
|
|
381
315
|
/** The pinned upload `Content-Type` carried by a PUT URL, when present. */
|
|
382
316
|
contentType?: string;
|
|
383
317
|
key?: string;
|
|
384
318
|
method?: "GET" | "PUT";
|
|
385
319
|
/**
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
320
|
+
* Internal-only failure reason for server logs/diagnostics. **Do not echo
|
|
321
|
+
* to clients** — a precise reason ("expired" vs "bad_signature") is a
|
|
322
|
+
* signing oracle. Public responses should expose only `valid`.
|
|
323
|
+
*/
|
|
390
324
|
reason?: "bad_signature" | "expired" | "malformed";
|
|
391
325
|
valid: boolean;
|
|
392
326
|
}
|
|
393
327
|
/**
|
|
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
|
-
*/
|
|
328
|
+
* Verify a {@link buildSignedUrl} output. By default the signature is
|
|
329
|
+
* canonicalized against the inbound `url.host`, which matches the build-side
|
|
330
|
+
* host whenever the URL being verified is the URL that was minted. In a
|
|
331
|
+
* topology where the host the Worker sees differs from the configured
|
|
332
|
+
* `publicBaseUrl` host (e.g. a CDN host vs a Worker route that rewrites
|
|
333
|
+
* `Host`), pass `expectedHost` (the `publicBaseUrl` host) so verification
|
|
334
|
+
* canonicalizes against the same host the signature was minted for instead of
|
|
335
|
+
* failing every request as `bad_signature`.
|
|
336
|
+
*/
|
|
403
337
|
declare const verifySignedUrl: (input: string | URL, secret: string, options?: {
|
|
404
338
|
expectedHost?: string;
|
|
405
339
|
}) => Promise<VerifyResult>;
|
|
406
|
-
export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type
|
|
340
|
+
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 };
|