@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.
- package/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/index.d.mts +126 -252
- package/dist/index.d.ts +126 -252
- package/dist/index.mjs +1 -4
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-CeVvqDXZ.mjs +1 -0
- package/dist/packem_shared/buildPresignedUrl-TCv-5TAT.mjs +4 -0
- package/dist/packem_shared/buildSignedUrl-DNIwBOK4.mjs +1 -0
- package/dist/packem_shared/createBucketStorage-DAip6cEw.mjs +1 -0
- package/dist/packem_shared/createStorage-BK_1WMQA.mjs +1 -0
- package/dist/packem_shared/internal-DpjlEJPE.mjs +1 -0
- package/dist/packem_shared/signed-url-Dm2EFS18.mjs +5 -0
- package/dist/upload.d.mts +118 -0
- package/dist/upload.d.ts +118 -0
- package/dist/upload.mjs +1 -0
- package/package.json +12 -2
- package/dist/packem_shared/buildPresignedUrl-DzPwi1bY.mjs +0 -70
- package/dist/packem_shared/buildSignedUrl-ZzB16yPl.mjs +0 -107
- package/dist/packem_shared/createBucketStorage-4Xk7-5CN.mjs +0 -26
- package/dist/packem_shared/scopeKey-Bs_iJ1Mx.mjs +0 -251
package/dist/index.d.ts
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;
|
|
@@ -159,10 +30,10 @@ interface LunoraStorageOptions {
|
|
|
159
30
|
/** Public base URL used by `getSignedUrl()`. Required for signed URLs. */
|
|
160
31
|
publicBaseUrl?: string;
|
|
161
32
|
/**
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
33
|
+
* R2 S3-API credentials enabling {@link Storage.getPresignedUrl} (native S3
|
|
34
|
+
* presigned URLs that hit R2 directly, bypassing the Worker). Omit to use
|
|
35
|
+
* only the worker-signed URL path.
|
|
36
|
+
*/
|
|
166
37
|
s3?: R2S3Credentials;
|
|
167
38
|
/** HMAC secret used by the worker-signed URL helper. Required for signed URLs. */
|
|
168
39
|
signingSecret?: string;
|
|
@@ -173,13 +44,13 @@ interface UploadOptions {
|
|
|
173
44
|
contentType?: string;
|
|
174
45
|
customMetadata?: Record<string, string>;
|
|
175
46
|
/**
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
47
|
+
* Maximum body size in bytes. For `ArrayBuffer`/`Blob` sources the length is
|
|
48
|
+
* known up front and rejected before the upload starts. For a
|
|
49
|
+
* `ReadableStream` the length isn't known synchronously, so the stream is
|
|
50
|
+
* piped through a byte counter that aborts the upload once the limit is
|
|
51
|
+
* exceeded — this also guards against R2 silently accepting/truncating an
|
|
52
|
+
* unbounded stream.
|
|
53
|
+
*/
|
|
183
54
|
maxSize?: number;
|
|
184
55
|
}
|
|
185
56
|
interface ListOptions {
|
|
@@ -191,20 +62,20 @@ interface ListOptions {
|
|
|
191
62
|
}
|
|
192
63
|
interface SignedUrlOptions {
|
|
193
64
|
/**
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
65
|
+
* Pin the `Content-Type` an uploader must send on a `method: "PUT"` URL.
|
|
66
|
+
* Baked into the HMAC canonical so the signature only authorizes a PUT with
|
|
67
|
+
* exactly this content-type; mirrored on the URL as `&ct=...`. Ignored for
|
|
68
|
+
* `GET` URLs (a download has no request body content-type to pin).
|
|
69
|
+
*/
|
|
199
70
|
contentType?: string;
|
|
200
71
|
expiresInSeconds?: number;
|
|
201
72
|
method?: "GET" | "PUT";
|
|
202
73
|
}
|
|
203
74
|
/**
|
|
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
|
-
*/
|
|
75
|
+
* Per-object metadata returned by {@link Storage.getMetadata} — a flat,
|
|
76
|
+
* body-free projection of {@link R2ObjectLike}. Mirrors the shape Convex
|
|
77
|
+
* surfaces for `ctx.storage.getMetadata` / the `_storage` system table.
|
|
78
|
+
*/
|
|
208
79
|
interface ObjectMetadata {
|
|
209
80
|
/** The object's `Content-Type` (R2 `httpMetadata.contentType`), if recorded. */
|
|
210
81
|
contentType?: string;
|
|
@@ -221,49 +92,49 @@ interface ObjectMetadata {
|
|
|
221
92
|
}
|
|
222
93
|
interface Storage {
|
|
223
94
|
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
95
|
+
* Begin a native R2 **multipart upload** for very large objects — upload
|
|
96
|
+
* parts (each uniform in size except the last), then `complete` with the
|
|
97
|
+
* returned parts (or `abort`). Wraps R2's `createMultipartUpload`; throws if
|
|
98
|
+
* the bound bucket doesn't support it. For ordinary uploads use
|
|
99
|
+
* {@link Storage.upload} / {@link Storage.store}.
|
|
100
|
+
*/
|
|
230
101
|
createMultipartUpload: (key: string, options?: {
|
|
231
102
|
contentType?: string;
|
|
232
103
|
customMetadata?: Record<string, string>;
|
|
233
104
|
}) => Promise<R2MultipartUploadLike>;
|
|
234
105
|
delete: (key: string) => Promise<void>;
|
|
235
106
|
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
107
|
+
* Fetch a stored object's metadata + body. Pass `options.range` to stream
|
|
108
|
+
* only a byte window (R2 resolves the range server-side, so the unwanted
|
|
109
|
+
* bytes never reach the Worker) — `download(key)` reads the whole object.
|
|
110
|
+
*/
|
|
240
111
|
download: (key: string, options?: {
|
|
241
112
|
range?: R2RangeLike;
|
|
242
113
|
}) => Promise<R2ObjectBodyLike | null>;
|
|
243
114
|
/**
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
115
|
+
* Mint a short-lived signed `PUT` URL a client can upload directly to,
|
|
116
|
+
* optionally pinning the request `Content-Type`. Convex-compatible alias
|
|
117
|
+
* built on {@link Storage.getSignedUrl} with `method: "PUT"`.
|
|
118
|
+
*/
|
|
248
119
|
generateUploadUrl: (key: string, options?: {
|
|
249
120
|
contentType?: string;
|
|
250
121
|
expiresInSeconds?: number;
|
|
251
122
|
}) => Promise<string>;
|
|
252
123
|
/**
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
124
|
+
* Read a stored object's metadata (size, content-type, sha256, upload time,
|
|
125
|
+
* custom metadata) without fetching its body. Returns `null` when the object
|
|
126
|
+
* is absent. Backed by an R2 HEAD (`bucket.head`) when available, falling
|
|
127
|
+
* back to a 0-length ranged `get()` otherwise. Mirrors Convex's
|
|
128
|
+
* `ctx.storage.getMetadata`.
|
|
129
|
+
*/
|
|
259
130
|
getMetadata: (key: string) => Promise<ObjectMetadata | null>;
|
|
260
131
|
/**
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
132
|
+
* Mint a native S3 **presigned URL** (SigV4) that hits R2 directly, bypassing
|
|
133
|
+
* the Worker. Use for large downloads/uploads where you don't need per-request
|
|
134
|
+
* app gating and want the bytes off the Worker's CPU/bandwidth budget. Requires
|
|
135
|
+
* {@link LunoraStorageOptions.s3} credentials; throws if they're absent. For
|
|
136
|
+
* app-gated access (auth/policy/rate-limit) prefer {@link Storage.getSignedUrl}.
|
|
137
|
+
*/
|
|
267
138
|
getPresignedUrl: (key: string, options?: PresignedUrlOptions) => Promise<string>;
|
|
268
139
|
getSignedUrl: (key: string, options?: SignedUrlOptions) => Promise<string>;
|
|
269
140
|
getUrl: (key: string) => string;
|
|
@@ -273,16 +144,16 @@ interface Storage {
|
|
|
273
144
|
truncated?: boolean;
|
|
274
145
|
}>;
|
|
275
146
|
/**
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
147
|
+
* Resume an in-progress multipart upload by its `uploadId` (e.g. across
|
|
148
|
+
* requests). Wraps R2's `resumeMultipartUpload`; the id is not validated by
|
|
149
|
+
* R2, so a stale id surfaces as an error on the first `uploadPart`/`complete`.
|
|
150
|
+
*/
|
|
280
151
|
resumeMultipartUpload: (key: string, uploadId: string) => R2MultipartUploadLike;
|
|
281
152
|
/**
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
153
|
+
* Upload `body` to `key`, returning the stored key + etag. Convex-compatible
|
|
154
|
+
* alias for {@link Storage.upload} — it accepts the same {@link UploadOptions}
|
|
155
|
+
* so the `maxSize` / `allowedContentTypes` guards aren't lost behind the alias.
|
|
156
|
+
*/
|
|
286
157
|
store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: UploadOptions) => Promise<{
|
|
287
158
|
etag: string;
|
|
288
159
|
httpEtag: string;
|
|
@@ -295,11 +166,11 @@ interface Storage {
|
|
|
295
166
|
}>;
|
|
296
167
|
}
|
|
297
168
|
/**
|
|
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
|
-
*/
|
|
169
|
+
* A bucket-aware {@link Storage}: the methods target a default bucket, and
|
|
170
|
+
* `bucket(name)` selects a different named bucket (declared via
|
|
171
|
+
* `v.storage("name")`). `bucketName` is the bucket the current accessor targets
|
|
172
|
+
* — the storage-rules middleware reads it to scope `(bucket, operation)` rules.
|
|
173
|
+
*/
|
|
303
174
|
interface BucketStorage extends Storage {
|
|
304
175
|
/** Select a named bucket. Unknown names throw with the list of registered buckets. */
|
|
305
176
|
bucket: (name: string) => BucketStorage;
|
|
@@ -307,36 +178,36 @@ interface BucketStorage extends Storage {
|
|
|
307
178
|
readonly bucketName: string;
|
|
308
179
|
}
|
|
309
180
|
/**
|
|
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
|
-
*/
|
|
181
|
+
* Compose several per-bucket {@link Storage} instances into one bucket-aware
|
|
182
|
+
* accessor. The bare methods (`download` / `store` / …) target the default
|
|
183
|
+
* bucket; `bucket(name)` switches to another. Each accessor is tagged with its
|
|
184
|
+
* `bucketName` so `storageRules(...)` can enforce per-bucket.
|
|
185
|
+
*
|
|
186
|
+
* ```ts
|
|
187
|
+
* storage: (env) => createBucketStorage({
|
|
188
|
+
* default: createStorage({ bucket: env.FILES }),
|
|
189
|
+
* avatars: createStorage({ bucket: env.AVATARS }),
|
|
190
|
+
* }),
|
|
191
|
+
* // → ctx.storage.download(key) // default bucket
|
|
192
|
+
* // → ctx.storage.bucket("avatars").store() // the avatars bucket
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* The bare accessor is tagged `"default"` — the canonical name a
|
|
196
|
+
* `defineStorageRule({ bucket: "default" })` rule and the generated
|
|
197
|
+
* `StorageBucketName` union both use — unless `options.default` names another
|
|
198
|
+
* bucket (then the bare accessor takes that name). The binding it delegates to is
|
|
199
|
+
* `options.default`, else the `"default"` key when present, else the first
|
|
200
|
+
* registered bucket. Named buckets are reached with `bucket(name)`.
|
|
201
|
+
*/
|
|
331
202
|
declare const createBucketStorage: (buckets: Record<string, Storage>, options?: {
|
|
332
203
|
default?: string;
|
|
333
204
|
}) => BucketStorage;
|
|
334
205
|
/**
|
|
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
|
-
*/
|
|
206
|
+
* Compose a per-tenant key from a scope prefix and a caller-supplied key.
|
|
207
|
+
* Both halves are validated — the prefix may not contain `..` or NUL either,
|
|
208
|
+
* and the resulting key must stay under R2's length ceiling. Recommended for
|
|
209
|
+
* any multi-tenant deployment so client-supplied keys can't address peer data.
|
|
210
|
+
*/
|
|
340
211
|
declare const scopeKey: (prefix: string, key: string) => string;
|
|
341
212
|
declare const createStorage: (options: LunoraStorageOptions) => Storage;
|
|
342
213
|
/** Parameters accepted by {@link buildPresignedUrl}. */
|
|
@@ -353,25 +224,28 @@ interface PresignedUrlParams {
|
|
|
353
224
|
now?: () => number;
|
|
354
225
|
}
|
|
355
226
|
/**
|
|
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
|
-
*/
|
|
227
|
+
* Build a native S3 presigned URL for an R2 object using SigV4 query-string
|
|
228
|
+
* auth. The returned URL points at R2's S3 endpoint and carries the full
|
|
229
|
+
* signature, so it authorizes a single `GET`/`PUT` on `key` until it expires —
|
|
230
|
+
* no Worker round-trip.
|
|
231
|
+
*/
|
|
361
232
|
declare const buildPresignedUrl: (parameters: PresignedUrlParams) => Promise<string>;
|
|
362
233
|
/**
|
|
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
|
|
373
|
-
*
|
|
374
|
-
|
|
234
|
+
* Worker-signed URL: the `publicBaseUrl` joined to the object `key`, plus a query
|
|
235
|
+
* string carrying `exp` (unix seconds), `method` (`GET` or `PUT`) and `sig`
|
|
236
|
+
* (a base64url HMAC).
|
|
237
|
+
*
|
|
238
|
+
* The HMAC canonical includes the URL host so a signature minted for one bucket
|
|
239
|
+
* cannot be replayed against another host on the same signing secret. Even so,
|
|
240
|
+
* the signing secret MUST NOT be shared across buckets/tenants — host binding
|
|
241
|
+
* narrows replay surface but is not a substitute for per-tenant key isolation.
|
|
242
|
+
*
|
|
243
|
+
* The Worker route handling the signed download/upload (mounted at
|
|
244
|
+
* `publicBaseUrl`'s origin, e.g. `GET /:key`) should call
|
|
245
|
+
* {@link verifySignedUrl} to validate the signature + expiry before streaming
|
|
246
|
+
* the R2 body. `baseUrl` must be a bare origin (no path) — see the module
|
|
247
|
+
* docstring.
|
|
248
|
+
*/
|
|
375
249
|
declare const buildSignedUrl: (args: SignedUrlOptions & {
|
|
376
250
|
baseUrl: string;
|
|
377
251
|
key: string;
|
|
@@ -383,24 +257,24 @@ interface VerifyResult {
|
|
|
383
257
|
key?: string;
|
|
384
258
|
method?: "GET" | "PUT";
|
|
385
259
|
/**
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
260
|
+
* Internal-only failure reason for server logs/diagnostics. **Do not echo
|
|
261
|
+
* to clients** — a precise reason ("expired" vs "bad_signature") is a
|
|
262
|
+
* signing oracle. Public responses should expose only `valid`.
|
|
263
|
+
*/
|
|
390
264
|
reason?: "bad_signature" | "expired" | "malformed";
|
|
391
265
|
valid: boolean;
|
|
392
266
|
}
|
|
393
267
|
/**
|
|
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
|
-
*/
|
|
268
|
+
* Verify a {@link buildSignedUrl} output. By default the signature is
|
|
269
|
+
* canonicalized against the inbound `url.host`, which matches the build-side
|
|
270
|
+
* host whenever the URL being verified is the URL that was minted. In a
|
|
271
|
+
* topology where the host the Worker sees differs from the configured
|
|
272
|
+
* `publicBaseUrl` host (e.g. a CDN host vs a Worker route that rewrites
|
|
273
|
+
* `Host`), pass `expectedHost` (the `publicBaseUrl` host) so verification
|
|
274
|
+
* canonicalizes against the same host the signature was minted for instead of
|
|
275
|
+
* failing every request as `bad_signature`.
|
|
276
|
+
*/
|
|
403
277
|
declare const verifySignedUrl: (input: string | URL, secret: string, options?: {
|
|
404
278
|
expectedHost?: string;
|
|
405
279
|
}) => Promise<VerifyResult>;
|
|
406
|
-
export { type BucketStorage, type ListOptions, type LunoraStorageOptions, type ObjectMetadata, type PresignedUrlOptions, type PresignedUrlParams, type
|
|
280
|
+
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 };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { createStorage, scopeKey } from './packem_shared/scopeKey-Bs_iJ1Mx.mjs';
|
|
3
|
-
export { buildPresignedUrl } from './packem_shared/buildPresignedUrl-DzPwi1bY.mjs';
|
|
4
|
-
export { buildSignedUrl, verifySignedUrl } from './packem_shared/buildSignedUrl-ZzB16yPl.mjs';
|
|
1
|
+
import{createBucketStorage as o}from"./packem_shared/createBucketStorage-DAip6cEw.mjs";import{createStorage as a,scopeKey as i}from"./packem_shared/createStorage-BK_1WMQA.mjs";import{buildPresignedUrl as f}from"./packem_shared/buildPresignedUrl-TCv-5TAT.mjs";import{b as l,v as p}from"./packem_shared/signed-url-Dm2EFS18.mjs";export{f as buildPresignedUrl,l as buildSignedUrl,o as createBucketStorage,a as createStorage,i as scopeKey,p as verifySignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Rest as d,Multipart as l,Tus as p}from"@visulima/storage/handler/http/fetch";import{AwsLightStorage as h}from"@visulima/storage/provider/aws-light";const m=100*1024*1024,g="1.0.0",c=(e,t,r)=>{const o={"content-type":"application/json"};return e==="tus"&&(o["Tus-Resumable"]=g),Response.json({error:r},{headers:o,status:t})},i=e=>c(e,403,{code:"FORBIDDEN",message:"Upload denied by authorization policy",name:"ForbiddenError"}),f=e=>c(e,413,{code:"REQUEST_ENTITY_TOO_LARGE",message:"Upload exceeds the configured maxFileSize",name:"RequestEntityTooLargeError"}),b=(e,t)=>{if(t==="multipart")return;const r=e.headers.get("Upload-Length")??e.headers.get("Content-Length");if(r===null)return;const o=Number(r);return Number.isFinite(o)?o:void 0},S=(e,t)=>e==="chunked-rest"?new d(t):e==="multipart"?new l(t):new p(t),z=e=>{const t=e.protocol??"tus",r=e.maxFileSize??m,o={maxFileSize:r,storage:e.storage},u=S(t,o),{authorize:n}=e;return n===void 0&&!e.silent&&!e.public&&console.warn("@lunora/storage: createUploadHandler() has no `authorize` — this mounts an unauthenticated, unbounded-write endpoint. Pass an RLS `authorize` gate, or set `public: true` (or `silent: true`) to confirm this bucket is intentionally open."),{fetch:async a=>{const s=b(a,t);if(s!==void 0&&s>r)return f(t);if(n!==void 0)try{if(!await n({method:a.method,protocol:t,request:a,url:new URL(a.url)}))return i(t)}catch{return i(t)}return u.fetch(a)},protocol:t}},E=e=>new h({accessKeyId:e.accessKeyId,bucket:e.bucket,endpoint:e.endpoint??`https://${e.accountId}.r2.cloudflarestorage.com`,path:e.path??"/",region:"auto",secretAccessKey:e.secretAccessKey,...e.partSize===void 0?{}:{partSize:e.partSize}});export{m as DEFAULT_MAX_UPLOAD_BYTES,E as createR2UploadStorage,z as createUploadHandler};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{a as g}from"./internal-DpjlEJPE.mjs";const h="auto",y="s3",S="AWS4-HMAC-SHA256",M=1,X=10080*60,w=900,d=new TextEncoder,j=(e,t)=>e[0]<t[0]?-1:e[0]>t[0]?1:0,s=e=>encodeURIComponent(e).replaceAll(/[!'()*]/gu,t=>`%${t.codePointAt(0)?.toString(16).toUpperCase()??""}`),f=e=>e.split("/").map(t=>s(t)).join("/"),U=async e=>g(await crypto.subtle.digest("SHA-256",d.encode(e))),n=async(e,t)=>{const a=await crypto.subtle.importKey("raw",e,{hash:"SHA-256",name:"HMAC"},!1,["sign"]);return crypto.subtle.sign("HMAC",a,d.encode(t))},E=async(e,t)=>{const a=await n(d.encode(`AWS4${e}`),t),r=await n(a,h),o=await n(r,y);return n(o,"aws4_request")},K=e=>{const t=e.jurisdiction===void 0?"":`${e.jurisdiction}.`;return`${e.accountId}.${t}r2.cloudflarestorage.com`},P=e=>{const t=`${e.toISOString().replaceAll(/[:-]/gu,"").slice(0,15)}Z`;return{amzDate:t,dateStamp:t.slice(0,8)}},k=async e=>{const{credentials:t,key:a}=e,r=e.method??"GET",o=e.expiresInSeconds??w,z=Number.isFinite(o)?o:w,H=Math.min(Math.max(M,Math.floor(z)),X),m=K(t),D=new Date(e.now?.()??Date.now()),{amzDate:l,dateStamp:u}=P(D),A=`${u}/${h}/${y}/aws4_request`,p=`/${s(t.bucket)}/${f(a)}`,$=[["X-Amz-Algorithm",S],["X-Amz-Credential",`${t.accessKeyId}/${A}`],["X-Amz-Date",l],["X-Amz-Expires",H.toString()],["X-Amz-SignedHeaders","host"]].map(([i,c])=>[s(i),s(c)]).toSorted(j).map(([i,c])=>`${i}=${c}`).join("&"),b=[r,p,$,`host:${m}
|
|
2
|
+
`,"host","UNSIGNED-PAYLOAD"].join(`
|
|
3
|
+
`),x=[S,l,A,await U(b)].join(`
|
|
4
|
+
`),C=await E(t.secretAccessKey,u),I=g(await n(C,x));return`https://${m}${p}?${$}&X-Amz-Signature=${I}`};export{k as buildPresignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"@lunora/errors";import{b as d,v as l}from"./signed-url-Dm2EFS18.mjs";import"./internal-DpjlEJPE.mjs";export{d as buildSignedUrl,l as verifySignedUrl};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";const d=(t,o={})=>{const r=Object.keys(t),[n]=r;if(n===void 0)throw new u("INTERNAL","@lunora/storage: createBucketStorage requires at least one bucket");if(o.default!==void 0&&!t[o.default])throw new u("INTERNAL",`@lunora/storage: default bucket "${o.default}" is not in the bucket map (have: ${r.join(", ")})`);const e=o.default??"default",c=t[e]??t[n];if(c===void 0)throw new u("INTERNAL",`@lunora/storage: default bucket "${e}" is not in the bucket map (have: ${r.join(", ")})`);const f=[...new Set([e,...r])],i=a=>{const s=a===e?c:t[a];if(!s)throw new u("INTERNAL",`@lunora/storage: no bucket registered for "${a}". Known buckets: ${f.join(", ")}`);return{...s,bucket:k=>i(k),bucketName:a}};return i(e)};export{d as createBucketStorage};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as a}from"@lunora/errors";import{b as R,d as T}from"./signed-url-Dm2EFS18.mjs";import{t as k,a as f}from"./internal-DpjlEJPE.mjs";import{buildPresignedUrl as I}from"./buildPresignedUrl-TCv-5TAT.mjs";const m=1024,A=1e3,U=100,b=e=>{const n=new Uint8Array(e);let i="";for(const p of n)i+=String.fromCodePoint(p);return btoa(i)},S=e=>{const n=e.checksums?.sha256;if(n===void 0)return e;const i=f(n),p=b(n);return new Proxy(e,{get(u,s){if(s==="sha256")return i;if(s==="sha256Base64")return p;const l=Reflect.get(u,s,u);return typeof l=="function"?l.bind(u):l},has(u,s){return s==="sha256"||s==="sha256Base64"||Reflect.has(u,s)}})},w=e=>{const n=e.checksums?.sha256;return{contentType:e.httpMetadata?.contentType,customMetadata:e.customMetadata,key:e.key,sha256:n===void 0?void 0:f(n),size:e.size,uploaded:e.uploaded===void 0?void 0:e.uploaded.getTime()}},M=e=>{const n=e.checksums?.sha256;return{checksums:e.checksums,customMetadata:e.customMetadata,etag:e.etag,httpEtag:e.httpEtag,httpMetadata:e.httpMetadata,key:e.key,sha256:n===void 0?void 0:f(n),sha256Base64:n===void 0?void 0:b(n),size:e.size,uploaded:e.uploaded}},N=(e,n)=>{let i=0;const p=s=>s instanceof ArrayBuffer||ArrayBuffer.isView(s)?s.byteLength:void 0,u=new TransformStream({transform(s,l){const h=p(s);if(h===void 0){l.error(new Error("@lunora/storage: stream chunk is not a byte chunk; cannot enforce maxSize"));return}if(i+=h,i>n){l.error(new Error(`@lunora/storage: stream body exceeds maxSize (> ${String(n)} bytes)`));return}l.enqueue(s)}});return e.pipeThrough(u)},c=e=>{if(typeof e!="string"||e.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: key must be a non-empty string");if(e.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: key exceeds ${String(m)}-byte limit`);if(e.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: key contains NUL byte");if(T(e))throw new a("VALIDATION_ERROR","@lunora/storage: key contains a control character (including CR/LF)");if(e.startsWith("/"))throw new a("VALIDATION_ERROR","@lunora/storage: key must not start with `/`");if(e.split("/").includes(".."))throw new a("VALIDATION_ERROR","@lunora/storage: key contains a `..` path component")},v=(e,n)=>{c(e),c(n);const i=`${e.endsWith("/")?e.slice(0,-1):e}/${n}`;if(i.length>m)throw new a("VALIDATION_ERROR",`@lunora/storage: scoped key exceeds ${String(m)}-byte limit`);return i},B=e=>{if(!e.bucket)throw new a("INTERNAL","@lunora/storage: `bucket` is required");const n=async(r,t,o={})=>{if(c(r),o.allowedContentTypes!==void 0){if(o.contentType===void 0)throw new a("VALIDATION_ERROR","@lunora/storage: contentType is required when allowedContentTypes is set");if(!o.allowedContentTypes.includes(o.contentType))throw new a("VALIDATION_ERROR",`@lunora/storage: contentType "${o.contentType}" not in allowedContentTypes`)}let y=t;if(typeof o.maxSize=="number"){let g;if(t instanceof ArrayBuffer?g=t.byteLength:t instanceof Blob&&(g=t.size),g!==void 0&&g>o.maxSize)throw new a("PAYLOAD_TOO_LARGE",`@lunora/storage: body exceeds maxSize (${String(g)} > ${String(o.maxSize)})`);t instanceof ReadableStream&&(y=N(t,o.maxSize))}const d=await e.bucket.put(r,y,{customMetadata:o.customMetadata,httpMetadata:o.contentType?{contentType:o.contentType}:void 0});return{etag:d.etag,httpEtag:d.httpEtag??`"${d.etag}"`,key:d.key}},i=async(r,t={})=>{c(r);const o=await(t.range?e.bucket.get(r,{range:t.range}):e.bucket.get(r));return o&&S(o)},p=async r=>{c(r),await e.bucket.delete(r)},u=async r=>{if(c(r),e.bucket.head){const o=await e.bucket.head(r);return o&&w(o)}const t=await e.bucket.get(r,{range:{length:0}});return t&&w(t)},s=async(r,t={})=>{if(r?.includes("\0"))throw new a("VALIDATION_ERROR","@lunora/storage: prefix contains NUL byte");const o=t.limit??U,y=Math.min(Math.max(1,Math.floor(o)),A),d=await e.bucket.list({cursor:t.cursor,delimiter:t.delimiter,limit:y,prefix:r});return{cursor:d.cursor,objects:d.objects.map(g=>M(g)),truncated:d.truncated}},l=r=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getUrl()");c(r);const t=r.split("/").map(o=>encodeURIComponent(o)).join("/");return`${k(e.publicBaseUrl)}/${t}`},h=async(r,t={})=>{if(!e.publicBaseUrl)throw new a("INTERNAL","@lunora/storage: `publicBaseUrl` is required for getSignedUrl()");if(!e.signingSecret)throw new a("INTERNAL","@lunora/storage: `signingSecret` is required for getSignedUrl()");return c(r),R({baseUrl:e.publicBaseUrl,contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method,secret:e.signingSecret})};return{createMultipartUpload:async(r,t={})=>{if(c(r),!e.bucket.createMultipartUpload)throw new a("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (createMultipartUpload)");return e.bucket.createMultipartUpload(r,{customMetadata:t.customMetadata,httpMetadata:t.contentType?{contentType:t.contentType}:void 0})},delete:p,download:i,generateUploadUrl:async(r,t={})=>h(r,{contentType:t.contentType,expiresInSeconds:t.expiresInSeconds,method:"PUT"}),getMetadata:u,getPresignedUrl:async(r,t={})=>{if(!e.s3)throw new a("INTERNAL","@lunora/storage: `s3` credentials are required for getPresignedUrl() — pass { accountId, accessKeyId, secretAccessKey, bucket }");return c(r),I({credentials:e.s3,expiresInSeconds:t.expiresInSeconds,key:r,method:t.method})},getSignedUrl:h,getUrl:l,list:s,resumeMultipartUpload:(r,t)=>{if(c(r),typeof t!="string"||t.length===0)throw new a("VALIDATION_ERROR","@lunora/storage: resumeMultipartUpload requires a non-empty uploadId");if(!e.bucket.resumeMultipartUpload)throw new a("INTERNAL","@lunora/storage: bucket binding does not support multipart uploads (resumeMultipartUpload)");return e.bucket.resumeMultipartUpload(r,t)},store:n,upload:n}};export{B as createStorage,v as scopeKey};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=e=>{const t=new Uint8Array(e);let n="";for(const r of t)n+=r.toString(16).padStart(2,"0");return n},a=e=>{let t=e.length;for(;t>0&&e[t-1]==="/";)t-=1;return e.slice(0,t)};export{o as a,a as t};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{LunoraError as u}from"@lunora/errors";import{t as R}from"./internal-DpjlEJPE.mjs";const U=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},f=new TextEncoder,h=10080*60,$=/^[a-z][a-z0-9+\-.]*:\/\//i,x=Array.from({length:32},(e,r)=>r),y=new RegExp(`[${x.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),I=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},L=e=>{const r=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),t=atob(r),n=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)n[a]=t.codePointAt(a)??0;return n},T=64,p=new Map,g=async e=>{const r=p.get(e);if(r)return r;U(p,T);const t=crypto.subtle.importKey("raw",f.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return p.set(e,t),t},w=e=>{try{return new URL(e).host}catch{return e.replace($,"").split("/")[0]??""}},v=e=>{if(y.test(e))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},H=e=>y.test(e),S=async(e,r)=>{const t=await g(e),n=await crypto.subtle.sign("HMAC",t,f.encode(r));return I(new Uint8Array(n))},C=async(e,r,t)=>{const n=await g(e);return crypto.subtle.verify("HMAC",n,t,f.encode(r))},E=/^\//,N=/^\/+$/u,b=(e,r,t,n,a)=>{const o=`${e}
|
|
2
|
+
${r.toLowerCase()}
|
|
3
|
+
${t}
|
|
4
|
+
${String(n)}`;return a===void 0?o:`${o}
|
|
5
|
+
${a}`},M=async e=>{const r=e.method??"GET",t=e.expiresInSeconds??3600;if(!Number.isFinite(t)||t<=0)throw new u("VALIDATION_ERROR","@lunora/storage: expiresInSeconds must be a positive finite number");if(t>h)throw new u("VALIDATION_ERROR",`@lunora/storage: expiresInSeconds must not exceed ${String(h)} (7 days)`);const n=r==="PUT"?e.contentType:void 0;let a="";try{a=new URL(e.baseUrl).pathname}catch{}if(a!==""&&!N.test(a))throw new u("VALIDATION_ERROR",`@lunora/storage: baseUrl must not carry a path ("${a}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);v(e.key);const o=Math.floor(Date.now()/1e3)+t,l=w(e.baseUrl),s=await S(e.secret,b(r,l,e.key,o,n)),c=R(e.baseUrl),i=e.key.split("/").map(m=>encodeURIComponent(m)).join("/"),d=n===void 0?"":`&ct=${encodeURIComponent(n)}`;return`${c}/${i}?exp=${String(o)}&method=${r}&sig=${s}${d}`},O=async(e,r,t)=>{let n;try{n=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const a=n.searchParams.get("exp"),o=a===null?Number.NaN:Number(a),l=n.searchParams.get("sig"),s=n.searchParams.get("method")??"GET",c=n.searchParams.get("ct")??void 0;if(!l||!Number.isInteger(o))return{reason:"malformed",valid:!1};if(o<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};if(s!=="GET"&&s!=="PUT")return{reason:"malformed",valid:!1};let i,d;try{i=n.pathname.replace(E,"").split("/").map(A=>decodeURIComponent(A)).join("/"),v(i),d=L(l)}catch{return{reason:"malformed",valid:!1}}const m=t?.expectedHost===void 0?n.host:w(t.expectedHost);return await C(r,b(s,m,i,o,c),d)?{contentType:c,key:i,method:s,valid:!0}:{reason:"bad_signature",valid:!1}};export{M as b,H as d,O as v};
|