@lunora/storage 1.0.0-alpha.13 → 1.0.0-alpha.131
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 +15 -7
- package/dist/index.d.mts +130 -169
- package/dist/index.d.ts +130 -169
- package/dist/index.mjs +1 -1
- package/dist/packem_shared/DEFAULT_MAX_UPLOAD_BYTES-BOaXqwP5.mjs +1 -0
- package/dist/packem_shared/R2_PART_SIZE-Dw8vEttI.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-AqIMq050.mjs +1 -0
- package/dist/packem_shared/createR2BindingUploadStorage-Vjs0vyJJ.mjs +1 -0
- package/dist/packem_shared/createR2UploadStorage-DGB1xJiD.mjs +1 -0
- package/dist/packem_shared/createStorage-YPzdUgGl.mjs +1 -0
- package/dist/packem_shared/internal-B5yyxF10.mjs +1 -0
- package/dist/packem_shared/r2-upload-part-writer-SdQlWaAu.mjs +1 -0
- package/dist/upload.d.mts +242 -18
- package/dist/upload.d.ts +242 -18
- package/dist/upload.mjs +1 -1
- package/package.json +4 -3
- package/dist/packem_shared/buildPresignedUrl-TCv-5TAT.mjs +0 -4
- package/dist/packem_shared/buildSignedUrl-DocLLDGy.mjs +0 -5
- package/dist/packem_shared/createBucketStorage-DAip6cEw.mjs +0 -1
- package/dist/packem_shared/createR2UploadStorage-BSgOjRvv.mjs +0 -1
- package/dist/packem_shared/createStorage-CWKguFEG.mjs +0 -1
- package/dist/packem_shared/internal-DpjlEJPE.mjs +0 -1
package/dist/upload.d.mts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
import { BaseStorageOptions } from '@visulima/storage';
|
|
2
|
+
import { R2BucketLike } from '@lunora/platform';
|
|
1
3
|
import { Tus } from '@visulima/storage/handler/http/fetch';
|
|
2
4
|
import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
|
|
5
|
+
/**
|
|
6
|
+
* An R2 binding with the multipart API: a Worker's `env.BUCKET` (and
|
|
7
|
+
* miniflare's). The provider also relies on `put`'s `onlyIf` preconditions,
|
|
8
|
+
* which every R2 binding honors.
|
|
9
|
+
*/
|
|
10
|
+
type R2UploadBucket = R2BucketLike & Required<Pick<R2BucketLike, "createMultipartUpload" | "resumeMultipartUpload">>;
|
|
3
11
|
/** Resumable upload wire protocols the handler can speak. */
|
|
4
12
|
type UploadProtocol = "chunked-rest" | "multipart" | "tus";
|
|
13
|
+
/**
|
|
14
|
+
* Default ceiling applied to `maxFileSize` when the caller doesn't supply one
|
|
15
|
+
* (100 MiB). Without SOME cap, an unauthenticated or loosely-authorized
|
|
16
|
+
* handler accepts an unbounded request body — pass an explicit `maxFileSize`
|
|
17
|
+
* to raise or lower this for your app.
|
|
18
|
+
*/
|
|
19
|
+
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
5
20
|
type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
|
|
6
|
-
/** A `@visulima/storage` storage provider (e.g.
|
|
21
|
+
/** A `@visulima/storage` storage provider (e.g. `createR2UploadStorage` or a memory provider in tests). */
|
|
7
22
|
type UploadStorage = UploadHandlerOptions["storage"];
|
|
8
23
|
/**
|
|
9
24
|
* The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
|
|
@@ -20,19 +35,128 @@ interface UploadAuthzContext {
|
|
|
20
35
|
/** The parsed request URL (query params, upload-id path segment). */
|
|
21
36
|
url: URL;
|
|
22
37
|
}
|
|
38
|
+
declare const GRANTED: unique symbol;
|
|
39
|
+
/**
|
|
40
|
+
* A grant `authorize` answers to admit a request and hand the rest of it a
|
|
41
|
+
* `Context`. Opaque: only {@link UploadContext.grant} issues one, so a value
|
|
42
|
+
* the gate builds by hand, or picks up by mistake (`{ valid: false, context }`),
|
|
43
|
+
* is not a grant and is denied.
|
|
44
|
+
*/
|
|
45
|
+
interface UploadGrant<Context = unknown> {
|
|
46
|
+
readonly [GRANTED]: Context;
|
|
47
|
+
}
|
|
48
|
+
/** What {@link CreateUploadHandlerOptions.authorize} answers: allow, deny, a grant, or the response to refuse with. */
|
|
49
|
+
type UploadAuthorizeResult<Context = unknown> = boolean | Response | UploadGrant<Context>;
|
|
50
|
+
/**
|
|
51
|
+
* A typed channel from `authorize` to the storage provider's callbacks
|
|
52
|
+
* (`filename`, `onCreate`, `onComplete`, …), which get only the file.
|
|
53
|
+
* `authorize` answers `grant(context)`, and the callbacks read the context back
|
|
54
|
+
* with `get()`, so one handler and one provider serve every caller.
|
|
55
|
+
*/
|
|
56
|
+
interface UploadContext<Context> {
|
|
57
|
+
/**
|
|
58
|
+
* The context the current request was granted. Throws when there is none:
|
|
59
|
+
* outside an upload request, or when `authorize` answered `true` or a grant
|
|
60
|
+
* from another `UploadContext`.
|
|
61
|
+
*/
|
|
62
|
+
get: () => Context;
|
|
63
|
+
/** A grant that admits the request and carries `context` to {@link UploadContext.get}. */
|
|
64
|
+
grant: (context: Context) => UploadGrant<Context>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create an {@link UploadContext}, typed by what `authorize` grants:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const caller = createUploadContext<{ userId: string }>();
|
|
71
|
+
*
|
|
72
|
+
* filename: (file) => `uploads/${caller.get().userId}/${file.id}`,
|
|
73
|
+
* authorize: async ({ request }) => caller.grant({ userId: await userIdFrom(request) }),
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare const createUploadContext: <Context>() => UploadContext<Context>;
|
|
77
|
+
/**
|
|
78
|
+
* The context handed to {@link CreateUploadHandlerOptions.maxFileSizeFor}: the
|
|
79
|
+
* authorization context plus what the create request declares about the file.
|
|
80
|
+
* Everything here comes from the client, so it caps an upload by what the
|
|
81
|
+
* client SAYS it is; `allowMIME` on the provider is what checks the type.
|
|
82
|
+
*/
|
|
83
|
+
interface UploadSizeContext<Context = unknown> extends UploadAuthzContext {
|
|
84
|
+
/**
|
|
85
|
+
* The declared MIME type, resolved the way the stored file's type is: on
|
|
86
|
+
* chunked REST the request's `Content-Type`; on TUS the `Upload-Metadata`
|
|
87
|
+
* `mimeType`, else `type`, else `filetype`; else
|
|
88
|
+
* `application/octet-stream`.
|
|
89
|
+
*/
|
|
90
|
+
contentType: string;
|
|
91
|
+
/**
|
|
92
|
+
* The largest size the request declares (`Upload-Length`, `X-Total-Size`,
|
|
93
|
+
* `Content-Length`). A declared size that is not a non-negative integer
|
|
94
|
+
* reads as `Infinity`.
|
|
95
|
+
*/
|
|
96
|
+
declaredSize: number | undefined;
|
|
97
|
+
/** The context `authorize` granted this request, or `undefined` when it answered `true` or there is no `authorize`. */
|
|
98
|
+
granted: Context | undefined;
|
|
99
|
+
/** The decoded TUS `Upload-Metadata`, or the chunked-REST `X-File-Metadata` JSON, as strings. */
|
|
100
|
+
metadata: Record<string, string>;
|
|
101
|
+
}
|
|
23
102
|
/** Options for {@link createUploadHandler}. */
|
|
24
|
-
interface CreateUploadHandlerOptions {
|
|
103
|
+
interface CreateUploadHandlerOptions<Context = unknown> {
|
|
25
104
|
/**
|
|
26
105
|
* The RLS gate. Runs before every upload request and denies fail-closed:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
106
|
+
* only `true` or an {@link UploadGrant} (from {@link UploadContext.grant})
|
|
107
|
+
* lets the request through; `false`, anything else, **or throwing** yields
|
|
108
|
+
* a `403`. A `Response` is answered instead of the `403`, for a refusal of
|
|
109
|
+
* your own (say a `429` with `Retry-After`): it is passed through as it
|
|
110
|
+
* is, except that a TUS route adds `Tus-Resumable` when it has none. It is
|
|
111
|
+
* meant for 4xx/5xx; a 2xx would tell the client an upload happened. Omit
|
|
112
|
+
* only for a fully public bucket — the whole point of this handler over
|
|
113
|
+
* the admin path is that uploads are gated by *your* per-user policy, not
|
|
114
|
+
* an admin token.
|
|
115
|
+
*
|
|
116
|
+
* Omitting it mounts an unauthenticated, unbounded-write endpoint, so
|
|
117
|
+
* doing so logs a one-time warning (per handler) unless `silent`/`public`
|
|
118
|
+
* says the omission is intentional.
|
|
119
|
+
*/
|
|
120
|
+
authorize?: (context: UploadAuthzContext) => UploadAuthorizeResult<Context> | Promise<UploadAuthorizeResult<Context>>;
|
|
121
|
+
/**
|
|
122
|
+
* Maximum accepted file size in bytes. Forwarded to the multipart parser
|
|
123
|
+
* (protocol `"multipart"`) and, for `"tus"`/`"chunked-rest"`, enforced by
|
|
124
|
+
* this handler itself against the largest size the request declares across
|
|
125
|
+
* `Upload-Length` (TUS), `X-Total-Size` (chunked REST) and `Content-Length`
|
|
126
|
+
* — see {@link declaredUploadSize}. Defaults to
|
|
127
|
+
* {@link DEFAULT_MAX_UPLOAD_BYTES} (100 MiB) — pass this to raise or lower
|
|
128
|
+
* the ceiling; there is no unbounded option. Must be a finite, non-negative
|
|
129
|
+
* number: anything else (notably a `NaN` from an unset env var) throws at
|
|
130
|
+
* construction rather than disabling the cap.
|
|
30
131
|
*/
|
|
31
|
-
authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
|
|
32
|
-
/** Maximum accepted file size in bytes (forwarded to the multipart parser). */
|
|
33
132
|
maxFileSize?: number;
|
|
133
|
+
/**
|
|
134
|
+
* A per-upload size cap, below `maxFileSize`: return the most bytes this
|
|
135
|
+
* upload may hold (say 10 MiB for `image/*`, 2 GiB for `video/*`), or
|
|
136
|
+
* `undefined` to leave `maxFileSize` as the only cap. It can only lower the
|
|
137
|
+
* cap, never raise it past `maxFileSize`.
|
|
138
|
+
*
|
|
139
|
+
* Runs after `authorize`, on the requests that create an upload (`POST`,
|
|
140
|
+
* and the chunked-REST `PUT`) of the `"tus"` and `"chunked-rest"`
|
|
141
|
+
* protocols; later chunks are bounded by the size declared at creation.
|
|
142
|
+
* Not applied to `"multipart"`, whose size is only known once the form is
|
|
143
|
+
* parsed. A create that declares no size is refused (`413`) when this
|
|
144
|
+
* returns a cap, since there is nothing to check it against. Throwing, or
|
|
145
|
+
* returning something that is not a finite, non-negative number, denies the
|
|
146
|
+
* request (`403`), fail-closed like `authorize`.
|
|
147
|
+
*/
|
|
148
|
+
maxFileSizeFor?: (context: UploadSizeContext<Context>) => number | undefined | Promise<number | undefined>;
|
|
34
149
|
/** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
|
|
35
150
|
protocol?: UploadProtocol;
|
|
151
|
+
/**
|
|
152
|
+
* Set when omitting `authorize` is intentional (a fully public upload
|
|
153
|
+
* bucket) — suppresses the one-time "no authorize gate" warning that would
|
|
154
|
+
* otherwise print when the handler is constructed. Has no effect when
|
|
155
|
+
* `authorize` is provided.
|
|
156
|
+
*/
|
|
157
|
+
public?: boolean;
|
|
158
|
+
/** Suppress the one-time default-open-authorize warning. Alias of `public`. */
|
|
159
|
+
silent?: boolean;
|
|
36
160
|
/** The storage provider the bytes land in (R2 in prod, memory in tests). */
|
|
37
161
|
storage: UploadStorage;
|
|
38
162
|
}
|
|
@@ -47,6 +171,62 @@ interface UploadHandler {
|
|
|
47
171
|
/** The protocol this handler speaks. */
|
|
48
172
|
protocol: UploadProtocol;
|
|
49
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Build an RLS-gated resumable upload handler over a `@visulima/storage`
|
|
176
|
+
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
177
|
+
* uploads to and drive it with `@visulima/storage-client`.
|
|
178
|
+
*/
|
|
179
|
+
declare const createUploadHandler: <Context = unknown>(options: CreateUploadHandlerOptions<Context>) => UploadHandler;
|
|
180
|
+
/** Every part of an R2 multipart upload but the last is exactly this size: R2's 5 MiB minimum. */
|
|
181
|
+
declare const R2_PART_SIZE: number;
|
|
182
|
+
/**
|
|
183
|
+
* `T` without its index signatures. `BaseStorageOptions` has a
|
|
184
|
+
* `[key: string]: unknown` one, and `Omit` over it maps over `string`, which
|
|
185
|
+
* erases every declared option (each would be `unknown`).
|
|
186
|
+
*/
|
|
187
|
+
type DeclaredKeys<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; };
|
|
188
|
+
/**
|
|
189
|
+
* Options for {@link createR2BindingUploadStorage}. A type alias, not an
|
|
190
|
+
* interface: an interface gets no implicit index signature, so it would not
|
|
191
|
+
* pass to `AbstractBaseStorage`'s constructor.
|
|
192
|
+
*/
|
|
193
|
+
type R2BindingUploadStorageOptions = Omit<DeclaredKeys<BaseStorageOptions>, "metaStorage"> & {
|
|
194
|
+
/**
|
|
195
|
+
* Key prefix the upload state objects and buffered segments are stored
|
|
196
|
+
* under, in the same bucket. Default `"_lunora/uploads/"`. No uploaded
|
|
197
|
+
* object may be named under it.
|
|
198
|
+
*
|
|
199
|
+
* The state of a finished upload stays (a few hundred bytes, so a `HEAD`
|
|
200
|
+
* can still answer that the upload is complete), and so does the state of
|
|
201
|
+
* one that was abandoned. Both accumulate: add an R2 object lifecycle rule
|
|
202
|
+
* that deletes objects under this prefix after a few days, longer than
|
|
203
|
+
* any upload you expect to take.
|
|
204
|
+
*/
|
|
205
|
+
statePrefix?: string;
|
|
206
|
+
/**
|
|
207
|
+
* The scope a request's upload state is kept under, appended to `statePrefix`: the state of an upload
|
|
208
|
+
* is then readable only by requests in the same scope. Read it from the request's grant, e.g.
|
|
209
|
+
* `() => \`${caller.get().userId}/\``. Must return a string ending in `/`, or `""` for the unscoped root.
|
|
210
|
+
*
|
|
211
|
+
* `list()` reads only the unscoped root, so an upload kept under a scope is not listed.
|
|
212
|
+
*/
|
|
213
|
+
stateScope?: () => string;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Build a resumable-upload provider for `createUploadHandler` over a Worker's
|
|
217
|
+
* R2 binding: no S3 credentials, and the same behaviour under `wrangler dev` /
|
|
218
|
+
* miniflare as in production.
|
|
219
|
+
*
|
|
220
|
+
* Speaks TUS (without the `checksum`, `concatenation` and
|
|
221
|
+
* `creation-defer-length` extensions), chunked REST, and multipart forms.
|
|
222
|
+
*
|
|
223
|
+
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
224
|
+
* offset other than the upload's current one, or one sent while another is
|
|
225
|
+
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
226
|
+
* completion. The bundled `@visulima/storage-client` chunked-REST client
|
|
227
|
+
* sends one chunk at a time (since 1.0.8), so its uploads complete here.
|
|
228
|
+
*/
|
|
229
|
+
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
50
230
|
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
51
231
|
interface R2UploadStorageOptions {
|
|
52
232
|
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
@@ -56,9 +236,11 @@ interface R2UploadStorageOptions {
|
|
|
56
236
|
/** Target R2 bucket name. */
|
|
57
237
|
bucket: string;
|
|
58
238
|
/**
|
|
59
|
-
* Explicit R2 S3 endpoint.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
239
|
+
* Explicit R2 S3 account endpoint, an absolute `https://` URL. The bucket
|
|
240
|
+
* is appended as a path segment, unless the endpoint already names it
|
|
241
|
+
* (as its last path segment, or as the host's first label). Defaults to
|
|
242
|
+
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
243
|
+
* jurisdiction (e.g. `https://<accountId>.eu.r2.cloudflarestorage.com`).
|
|
62
244
|
*/
|
|
63
245
|
endpoint?: string;
|
|
64
246
|
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
@@ -70,13 +252,7 @@ interface R2UploadStorageOptions {
|
|
|
70
252
|
path?: string;
|
|
71
253
|
}
|
|
72
254
|
/**
|
|
73
|
-
* Build an
|
|
74
|
-
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
75
|
-
* uploads to and drive it with `@visulima/storage-client`.
|
|
76
|
-
*/
|
|
77
|
-
declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
|
|
78
|
-
/**
|
|
79
|
-
* Build an R2-backed storage provider for {@link createUploadHandler} using
|
|
255
|
+
* Build an R2-backed storage provider for `createUploadHandler` using
|
|
80
256
|
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
81
257
|
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
82
258
|
*
|
|
@@ -84,8 +260,56 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
|
|
|
84
260
|
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
85
261
|
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
86
262
|
* `node:stream`).
|
|
263
|
+
*
|
|
264
|
+
* The bucket is addressed path-style, `<endpoint>/<bucket>/<key>`.
|
|
265
|
+
*
|
|
266
|
+
* Each chunk becomes one multipart part, so every chunk but the last has to be
|
|
267
|
+
* the same size and at least 5 MiB, and chunks have to arrive in order (an
|
|
268
|
+
* out-of-order or repeated one is a `409`). The R2 binding provider
|
|
269
|
+
* (`./r2-binding-upload-storage`) has neither limit, needs no S3 credentials
|
|
270
|
+
* and runs under `wrangler dev` too.
|
|
271
|
+
*
|
|
272
|
+
* The provider takes no `@visulima/storage` limits of its own (its upload cap
|
|
273
|
+
* is the 5 TB default), so `maxFileSize` / `maxFileSizeFor` on the handler are
|
|
274
|
+
* the caps that apply.
|
|
87
275
|
*/
|
|
88
276
|
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
89
277
|
secretAccessKey: string;
|
|
90
278
|
}) => AwsLightStorage;
|
|
91
|
-
export { type CreateUploadHandlerOptions,
|
|
279
|
+
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
|
280
|
+
/**
|
|
281
|
+
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
|
282
|
+
*
|
|
283
|
+
* A separate subpath so the base `@lunora/storage` entry stays lean: the
|
|
284
|
+
* `@visulima/storage` handler dependency only loads for apps that mount an
|
|
285
|
+
* end-user upload endpoint. Pair with `@visulima/storage-client` (or the
|
|
286
|
+
* `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
|
|
287
|
+
*
|
|
288
|
+
* On Workers this subpath statically imports `node:async_hooks` (the grant
|
|
289
|
+
* scope) and, through the R2 providers, `node:stream`, so it needs Node.js
|
|
290
|
+
* compatibility to load at all: the import fails at load time otherwise, and
|
|
291
|
+
* the subpath never reaches `createUploadHandler` / `createUploadContext`. A
|
|
292
|
+
* `compatibility_date` of `2026-08-04` or later enables `nodejs_compat` by
|
|
293
|
+
* default; earlier dates need `nodejs_compat` in `compatibility_flags`.
|
|
294
|
+
* `nodejs_als` alone satisfies the `AsyncLocalStorage` import but not
|
|
295
|
+
* `node:stream`, so it is not enough for this subpath. Node.js is unaffected.
|
|
296
|
+
*/
|
|
297
|
+
type R2BindingUploadStorageOptions,
|
|
298
|
+
/**
|
|
299
|
+
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
|
300
|
+
*
|
|
301
|
+
* A separate subpath so the base `@lunora/storage` entry stays lean: the
|
|
302
|
+
* `@visulima/storage` handler dependency only loads for apps that mount an
|
|
303
|
+
* end-user upload endpoint. Pair with `@visulima/storage-client` (or the
|
|
304
|
+
* `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
|
|
305
|
+
*
|
|
306
|
+
* On Workers this subpath statically imports `node:async_hooks` (the grant
|
|
307
|
+
* scope) and, through the R2 providers, `node:stream`, so it needs Node.js
|
|
308
|
+
* compatibility to load at all: the import fails at load time otherwise, and
|
|
309
|
+
* the subpath never reaches `createUploadHandler` / `createUploadContext`. A
|
|
310
|
+
* `compatibility_date` of `2026-08-04` or later enables `nodejs_compat` by
|
|
311
|
+
* default; earlier dates need `nodejs_compat` in `compatibility_flags`.
|
|
312
|
+
* `nodejs_als` alone satisfies the `AsyncLocalStorage` import but not
|
|
313
|
+
* `node:stream`, so it is not enough for this subpath. Node.js is unaffected.
|
|
314
|
+
*/
|
|
315
|
+
type R2UploadBucket, type R2UploadStorageOptions, R2_PART_SIZE, type UploadAuthorizeResult, type UploadAuthzContext, type UploadContext, type UploadGrant, type UploadHandler, type UploadProtocol, type UploadSizeContext, type UploadStorage, createR2BindingUploadStorage, createR2UploadStorage, createUploadContext, createUploadHandler };
|
package/dist/upload.d.ts
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
import { BaseStorageOptions } from '@visulima/storage';
|
|
2
|
+
import { R2BucketLike } from '@lunora/platform';
|
|
1
3
|
import { Tus } from '@visulima/storage/handler/http/fetch';
|
|
2
4
|
import { AwsLightStorage } from '@visulima/storage/provider/aws-light';
|
|
5
|
+
/**
|
|
6
|
+
* An R2 binding with the multipart API: a Worker's `env.BUCKET` (and
|
|
7
|
+
* miniflare's). The provider also relies on `put`'s `onlyIf` preconditions,
|
|
8
|
+
* which every R2 binding honors.
|
|
9
|
+
*/
|
|
10
|
+
type R2UploadBucket = R2BucketLike & Required<Pick<R2BucketLike, "createMultipartUpload" | "resumeMultipartUpload">>;
|
|
3
11
|
/** Resumable upload wire protocols the handler can speak. */
|
|
4
12
|
type UploadProtocol = "chunked-rest" | "multipart" | "tus";
|
|
13
|
+
/**
|
|
14
|
+
* Default ceiling applied to `maxFileSize` when the caller doesn't supply one
|
|
15
|
+
* (100 MiB). Without SOME cap, an unauthenticated or loosely-authorized
|
|
16
|
+
* handler accepts an unbounded request body — pass an explicit `maxFileSize`
|
|
17
|
+
* to raise or lower this for your app.
|
|
18
|
+
*/
|
|
19
|
+
declare const DEFAULT_MAX_UPLOAD_BYTES: number;
|
|
5
20
|
type UploadHandlerOptions = ConstructorParameters<typeof Tus>[0];
|
|
6
|
-
/** A `@visulima/storage` storage provider (e.g.
|
|
21
|
+
/** A `@visulima/storage` storage provider (e.g. `createR2UploadStorage` or a memory provider in tests). */
|
|
7
22
|
type UploadStorage = UploadHandlerOptions["storage"];
|
|
8
23
|
/**
|
|
9
24
|
* The context handed to {@link CreateUploadHandlerOptions.authorize}. Everything
|
|
@@ -20,19 +35,128 @@ interface UploadAuthzContext {
|
|
|
20
35
|
/** The parsed request URL (query params, upload-id path segment). */
|
|
21
36
|
url: URL;
|
|
22
37
|
}
|
|
38
|
+
declare const GRANTED: unique symbol;
|
|
39
|
+
/**
|
|
40
|
+
* A grant `authorize` answers to admit a request and hand the rest of it a
|
|
41
|
+
* `Context`. Opaque: only {@link UploadContext.grant} issues one, so a value
|
|
42
|
+
* the gate builds by hand, or picks up by mistake (`{ valid: false, context }`),
|
|
43
|
+
* is not a grant and is denied.
|
|
44
|
+
*/
|
|
45
|
+
interface UploadGrant<Context = unknown> {
|
|
46
|
+
readonly [GRANTED]: Context;
|
|
47
|
+
}
|
|
48
|
+
/** What {@link CreateUploadHandlerOptions.authorize} answers: allow, deny, a grant, or the response to refuse with. */
|
|
49
|
+
type UploadAuthorizeResult<Context = unknown> = boolean | Response | UploadGrant<Context>;
|
|
50
|
+
/**
|
|
51
|
+
* A typed channel from `authorize` to the storage provider's callbacks
|
|
52
|
+
* (`filename`, `onCreate`, `onComplete`, …), which get only the file.
|
|
53
|
+
* `authorize` answers `grant(context)`, and the callbacks read the context back
|
|
54
|
+
* with `get()`, so one handler and one provider serve every caller.
|
|
55
|
+
*/
|
|
56
|
+
interface UploadContext<Context> {
|
|
57
|
+
/**
|
|
58
|
+
* The context the current request was granted. Throws when there is none:
|
|
59
|
+
* outside an upload request, or when `authorize` answered `true` or a grant
|
|
60
|
+
* from another `UploadContext`.
|
|
61
|
+
*/
|
|
62
|
+
get: () => Context;
|
|
63
|
+
/** A grant that admits the request and carries `context` to {@link UploadContext.get}. */
|
|
64
|
+
grant: (context: Context) => UploadGrant<Context>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Create an {@link UploadContext}, typed by what `authorize` grants:
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const caller = createUploadContext<{ userId: string }>();
|
|
71
|
+
*
|
|
72
|
+
* filename: (file) => `uploads/${caller.get().userId}/${file.id}`,
|
|
73
|
+
* authorize: async ({ request }) => caller.grant({ userId: await userIdFrom(request) }),
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare const createUploadContext: <Context>() => UploadContext<Context>;
|
|
77
|
+
/**
|
|
78
|
+
* The context handed to {@link CreateUploadHandlerOptions.maxFileSizeFor}: the
|
|
79
|
+
* authorization context plus what the create request declares about the file.
|
|
80
|
+
* Everything here comes from the client, so it caps an upload by what the
|
|
81
|
+
* client SAYS it is; `allowMIME` on the provider is what checks the type.
|
|
82
|
+
*/
|
|
83
|
+
interface UploadSizeContext<Context = unknown> extends UploadAuthzContext {
|
|
84
|
+
/**
|
|
85
|
+
* The declared MIME type, resolved the way the stored file's type is: on
|
|
86
|
+
* chunked REST the request's `Content-Type`; on TUS the `Upload-Metadata`
|
|
87
|
+
* `mimeType`, else `type`, else `filetype`; else
|
|
88
|
+
* `application/octet-stream`.
|
|
89
|
+
*/
|
|
90
|
+
contentType: string;
|
|
91
|
+
/**
|
|
92
|
+
* The largest size the request declares (`Upload-Length`, `X-Total-Size`,
|
|
93
|
+
* `Content-Length`). A declared size that is not a non-negative integer
|
|
94
|
+
* reads as `Infinity`.
|
|
95
|
+
*/
|
|
96
|
+
declaredSize: number | undefined;
|
|
97
|
+
/** The context `authorize` granted this request, or `undefined` when it answered `true` or there is no `authorize`. */
|
|
98
|
+
granted: Context | undefined;
|
|
99
|
+
/** The decoded TUS `Upload-Metadata`, or the chunked-REST `X-File-Metadata` JSON, as strings. */
|
|
100
|
+
metadata: Record<string, string>;
|
|
101
|
+
}
|
|
23
102
|
/** Options for {@link createUploadHandler}. */
|
|
24
|
-
interface CreateUploadHandlerOptions {
|
|
103
|
+
interface CreateUploadHandlerOptions<Context = unknown> {
|
|
25
104
|
/**
|
|
26
105
|
* The RLS gate. Runs before every upload request and denies fail-closed:
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
106
|
+
* only `true` or an {@link UploadGrant} (from {@link UploadContext.grant})
|
|
107
|
+
* lets the request through; `false`, anything else, **or throwing** yields
|
|
108
|
+
* a `403`. A `Response` is answered instead of the `403`, for a refusal of
|
|
109
|
+
* your own (say a `429` with `Retry-After`): it is passed through as it
|
|
110
|
+
* is, except that a TUS route adds `Tus-Resumable` when it has none. It is
|
|
111
|
+
* meant for 4xx/5xx; a 2xx would tell the client an upload happened. Omit
|
|
112
|
+
* only for a fully public bucket — the whole point of this handler over
|
|
113
|
+
* the admin path is that uploads are gated by *your* per-user policy, not
|
|
114
|
+
* an admin token.
|
|
115
|
+
*
|
|
116
|
+
* Omitting it mounts an unauthenticated, unbounded-write endpoint, so
|
|
117
|
+
* doing so logs a one-time warning (per handler) unless `silent`/`public`
|
|
118
|
+
* says the omission is intentional.
|
|
119
|
+
*/
|
|
120
|
+
authorize?: (context: UploadAuthzContext) => UploadAuthorizeResult<Context> | Promise<UploadAuthorizeResult<Context>>;
|
|
121
|
+
/**
|
|
122
|
+
* Maximum accepted file size in bytes. Forwarded to the multipart parser
|
|
123
|
+
* (protocol `"multipart"`) and, for `"tus"`/`"chunked-rest"`, enforced by
|
|
124
|
+
* this handler itself against the largest size the request declares across
|
|
125
|
+
* `Upload-Length` (TUS), `X-Total-Size` (chunked REST) and `Content-Length`
|
|
126
|
+
* — see {@link declaredUploadSize}. Defaults to
|
|
127
|
+
* {@link DEFAULT_MAX_UPLOAD_BYTES} (100 MiB) — pass this to raise or lower
|
|
128
|
+
* the ceiling; there is no unbounded option. Must be a finite, non-negative
|
|
129
|
+
* number: anything else (notably a `NaN` from an unset env var) throws at
|
|
130
|
+
* construction rather than disabling the cap.
|
|
30
131
|
*/
|
|
31
|
-
authorize?: (context: UploadAuthzContext) => boolean | Promise<boolean>;
|
|
32
|
-
/** Maximum accepted file size in bytes (forwarded to the multipart parser). */
|
|
33
132
|
maxFileSize?: number;
|
|
133
|
+
/**
|
|
134
|
+
* A per-upload size cap, below `maxFileSize`: return the most bytes this
|
|
135
|
+
* upload may hold (say 10 MiB for `image/*`, 2 GiB for `video/*`), or
|
|
136
|
+
* `undefined` to leave `maxFileSize` as the only cap. It can only lower the
|
|
137
|
+
* cap, never raise it past `maxFileSize`.
|
|
138
|
+
*
|
|
139
|
+
* Runs after `authorize`, on the requests that create an upload (`POST`,
|
|
140
|
+
* and the chunked-REST `PUT`) of the `"tus"` and `"chunked-rest"`
|
|
141
|
+
* protocols; later chunks are bounded by the size declared at creation.
|
|
142
|
+
* Not applied to `"multipart"`, whose size is only known once the form is
|
|
143
|
+
* parsed. A create that declares no size is refused (`413`) when this
|
|
144
|
+
* returns a cap, since there is nothing to check it against. Throwing, or
|
|
145
|
+
* returning something that is not a finite, non-negative number, denies the
|
|
146
|
+
* request (`403`), fail-closed like `authorize`.
|
|
147
|
+
*/
|
|
148
|
+
maxFileSizeFor?: (context: UploadSizeContext<Context>) => number | undefined | Promise<number | undefined>;
|
|
34
149
|
/** Which protocol to speak. Default `"tus"` (the resumable, pause/resume-capable one). */
|
|
35
150
|
protocol?: UploadProtocol;
|
|
151
|
+
/**
|
|
152
|
+
* Set when omitting `authorize` is intentional (a fully public upload
|
|
153
|
+
* bucket) — suppresses the one-time "no authorize gate" warning that would
|
|
154
|
+
* otherwise print when the handler is constructed. Has no effect when
|
|
155
|
+
* `authorize` is provided.
|
|
156
|
+
*/
|
|
157
|
+
public?: boolean;
|
|
158
|
+
/** Suppress the one-time default-open-authorize warning. Alias of `public`. */
|
|
159
|
+
silent?: boolean;
|
|
36
160
|
/** The storage provider the bytes land in (R2 in prod, memory in tests). */
|
|
37
161
|
storage: UploadStorage;
|
|
38
162
|
}
|
|
@@ -47,6 +171,62 @@ interface UploadHandler {
|
|
|
47
171
|
/** The protocol this handler speaks. */
|
|
48
172
|
protocol: UploadProtocol;
|
|
49
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Build an RLS-gated resumable upload handler over a `@visulima/storage`
|
|
176
|
+
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
177
|
+
* uploads to and drive it with `@visulima/storage-client`.
|
|
178
|
+
*/
|
|
179
|
+
declare const createUploadHandler: <Context = unknown>(options: CreateUploadHandlerOptions<Context>) => UploadHandler;
|
|
180
|
+
/** Every part of an R2 multipart upload but the last is exactly this size: R2's 5 MiB minimum. */
|
|
181
|
+
declare const R2_PART_SIZE: number;
|
|
182
|
+
/**
|
|
183
|
+
* `T` without its index signatures. `BaseStorageOptions` has a
|
|
184
|
+
* `[key: string]: unknown` one, and `Omit` over it maps over `string`, which
|
|
185
|
+
* erases every declared option (each would be `unknown`).
|
|
186
|
+
*/
|
|
187
|
+
type DeclaredKeys<T> = { [K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]; };
|
|
188
|
+
/**
|
|
189
|
+
* Options for {@link createR2BindingUploadStorage}. A type alias, not an
|
|
190
|
+
* interface: an interface gets no implicit index signature, so it would not
|
|
191
|
+
* pass to `AbstractBaseStorage`'s constructor.
|
|
192
|
+
*/
|
|
193
|
+
type R2BindingUploadStorageOptions = Omit<DeclaredKeys<BaseStorageOptions>, "metaStorage"> & {
|
|
194
|
+
/**
|
|
195
|
+
* Key prefix the upload state objects and buffered segments are stored
|
|
196
|
+
* under, in the same bucket. Default `"_lunora/uploads/"`. No uploaded
|
|
197
|
+
* object may be named under it.
|
|
198
|
+
*
|
|
199
|
+
* The state of a finished upload stays (a few hundred bytes, so a `HEAD`
|
|
200
|
+
* can still answer that the upload is complete), and so does the state of
|
|
201
|
+
* one that was abandoned. Both accumulate: add an R2 object lifecycle rule
|
|
202
|
+
* that deletes objects under this prefix after a few days, longer than
|
|
203
|
+
* any upload you expect to take.
|
|
204
|
+
*/
|
|
205
|
+
statePrefix?: string;
|
|
206
|
+
/**
|
|
207
|
+
* The scope a request's upload state is kept under, appended to `statePrefix`: the state of an upload
|
|
208
|
+
* is then readable only by requests in the same scope. Read it from the request's grant, e.g.
|
|
209
|
+
* `() => \`${caller.get().userId}/\``. Must return a string ending in `/`, or `""` for the unscoped root.
|
|
210
|
+
*
|
|
211
|
+
* `list()` reads only the unscoped root, so an upload kept under a scope is not listed.
|
|
212
|
+
*/
|
|
213
|
+
stateScope?: () => string;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Build a resumable-upload provider for `createUploadHandler` over a Worker's
|
|
217
|
+
* R2 binding: no S3 credentials, and the same behaviour under `wrangler dev` /
|
|
218
|
+
* miniflare as in production.
|
|
219
|
+
*
|
|
220
|
+
* Speaks TUS (without the `checksum`, `concatenation` and
|
|
221
|
+
* `creation-defer-length` extensions), chunked REST, and multipart forms.
|
|
222
|
+
*
|
|
223
|
+
* Chunked-REST chunks have to arrive one at a time and in order. A chunk at any
|
|
224
|
+
* offset other than the upload's current one, or one sent while another is
|
|
225
|
+
* still streaming, is a `409` and stores nothing, and it never counts towards
|
|
226
|
+
* completion. The bundled `@visulima/storage-client` chunked-REST client
|
|
227
|
+
* sends one chunk at a time (since 1.0.8), so its uploads complete here.
|
|
228
|
+
*/
|
|
229
|
+
declare const createR2BindingUploadStorage: (bucket: R2UploadBucket, options?: R2BindingUploadStorageOptions) => UploadStorage;
|
|
50
230
|
/** R2 (S3-compatible) credentials + bucket for {@link createR2UploadStorage}. */
|
|
51
231
|
interface R2UploadStorageOptions {
|
|
52
232
|
/** R2 S3 API Access Key ID (from an R2 API token). */
|
|
@@ -56,9 +236,11 @@ interface R2UploadStorageOptions {
|
|
|
56
236
|
/** Target R2 bucket name. */
|
|
57
237
|
bucket: string;
|
|
58
238
|
/**
|
|
59
|
-
* Explicit R2 S3 endpoint.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
239
|
+
* Explicit R2 S3 account endpoint, an absolute `https://` URL. The bucket
|
|
240
|
+
* is appended as a path segment, unless the endpoint already names it
|
|
241
|
+
* (as its last path segment, or as the host's first label). Defaults to
|
|
242
|
+
* `https://<accountId>.r2.cloudflarestorage.com`. Pass this to pin a
|
|
243
|
+
* jurisdiction (e.g. `https://<accountId>.eu.r2.cloudflarestorage.com`).
|
|
62
244
|
*/
|
|
63
245
|
endpoint?: string;
|
|
64
246
|
/** Client-side multipart part size (bytes or a size string like `"16MB"`). */
|
|
@@ -70,13 +252,7 @@ interface R2UploadStorageOptions {
|
|
|
70
252
|
path?: string;
|
|
71
253
|
}
|
|
72
254
|
/**
|
|
73
|
-
* Build an
|
|
74
|
-
* provider. Mount its {@link UploadHandler.fetch} on the route your client
|
|
75
|
-
* uploads to and drive it with `@visulima/storage-client`.
|
|
76
|
-
*/
|
|
77
|
-
declare const createUploadHandler: (options: CreateUploadHandlerOptions) => UploadHandler;
|
|
78
|
-
/**
|
|
79
|
-
* Build an R2-backed storage provider for {@link createUploadHandler} using
|
|
255
|
+
* Build an R2-backed storage provider for `createUploadHandler` using
|
|
80
256
|
* `@visulima/storage`'s dependency-light `aws-light` provider (`aws4fetch`, no
|
|
81
257
|
* AWS SDK). R2's S3 region alias is always `auto`.
|
|
82
258
|
*
|
|
@@ -84,8 +260,56 @@ declare const createUploadHandler: (options: CreateUploadHandlerOptions) => Uplo
|
|
|
84
260
|
* same credential shape `@lunora/storage`'s presigned-URL helpers take. In a
|
|
85
261
|
* Worker the `aws-light` provider needs `nodejs_compat` (it imports
|
|
86
262
|
* `node:stream`).
|
|
263
|
+
*
|
|
264
|
+
* The bucket is addressed path-style, `<endpoint>/<bucket>/<key>`.
|
|
265
|
+
*
|
|
266
|
+
* Each chunk becomes one multipart part, so every chunk but the last has to be
|
|
267
|
+
* the same size and at least 5 MiB, and chunks have to arrive in order (an
|
|
268
|
+
* out-of-order or repeated one is a `409`). The R2 binding provider
|
|
269
|
+
* (`./r2-binding-upload-storage`) has neither limit, needs no S3 credentials
|
|
270
|
+
* and runs under `wrangler dev` too.
|
|
271
|
+
*
|
|
272
|
+
* The provider takes no `@visulima/storage` limits of its own (its upload cap
|
|
273
|
+
* is the 5 TB default), so `maxFileSize` / `maxFileSizeFor` on the handler are
|
|
274
|
+
* the caps that apply.
|
|
87
275
|
*/
|
|
88
276
|
declare const createR2UploadStorage: (options: R2UploadStorageOptions & {
|
|
89
277
|
secretAccessKey: string;
|
|
90
278
|
}) => AwsLightStorage;
|
|
91
|
-
export { type CreateUploadHandlerOptions,
|
|
279
|
+
export { type CreateUploadHandlerOptions, DEFAULT_MAX_UPLOAD_BYTES,
|
|
280
|
+
/**
|
|
281
|
+
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
|
282
|
+
*
|
|
283
|
+
* A separate subpath so the base `@lunora/storage` entry stays lean: the
|
|
284
|
+
* `@visulima/storage` handler dependency only loads for apps that mount an
|
|
285
|
+
* end-user upload endpoint. Pair with `@visulima/storage-client` (or the
|
|
286
|
+
* `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
|
|
287
|
+
*
|
|
288
|
+
* On Workers this subpath statically imports `node:async_hooks` (the grant
|
|
289
|
+
* scope) and, through the R2 providers, `node:stream`, so it needs Node.js
|
|
290
|
+
* compatibility to load at all: the import fails at load time otherwise, and
|
|
291
|
+
* the subpath never reaches `createUploadHandler` / `createUploadContext`. A
|
|
292
|
+
* `compatibility_date` of `2026-08-04` or later enables `nodejs_compat` by
|
|
293
|
+
* default; earlier dates need `nodejs_compat` in `compatibility_flags`.
|
|
294
|
+
* `nodejs_als` alone satisfies the `AsyncLocalStorage` import but not
|
|
295
|
+
* `node:stream`, so it is not enough for this subpath. Node.js is unaffected.
|
|
296
|
+
*/
|
|
297
|
+
type R2BindingUploadStorageOptions,
|
|
298
|
+
/**
|
|
299
|
+
* `@lunora/storage/upload` — the RLS-gated, non-admin resumable upload handler.
|
|
300
|
+
*
|
|
301
|
+
* A separate subpath so the base `@lunora/storage` entry stays lean: the
|
|
302
|
+
* `@visulima/storage` handler dependency only loads for apps that mount an
|
|
303
|
+
* end-user upload endpoint. Pair with `@visulima/storage-client` (or the
|
|
304
|
+
* `@lunora/react` / `@lunora/vue` / … `useUpload` re-exports) on the client.
|
|
305
|
+
*
|
|
306
|
+
* On Workers this subpath statically imports `node:async_hooks` (the grant
|
|
307
|
+
* scope) and, through the R2 providers, `node:stream`, so it needs Node.js
|
|
308
|
+
* compatibility to load at all: the import fails at load time otherwise, and
|
|
309
|
+
* the subpath never reaches `createUploadHandler` / `createUploadContext`. A
|
|
310
|
+
* `compatibility_date` of `2026-08-04` or later enables `nodejs_compat` by
|
|
311
|
+
* default; earlier dates need `nodejs_compat` in `compatibility_flags`.
|
|
312
|
+
* `nodejs_als` alone satisfies the `AsyncLocalStorage` import but not
|
|
313
|
+
* `node:stream`, so it is not enough for this subpath. Node.js is unaffected.
|
|
314
|
+
*/
|
|
315
|
+
type R2UploadBucket, type R2UploadStorageOptions, R2_PART_SIZE, type UploadAuthorizeResult, type UploadAuthzContext, type UploadContext, type UploadGrant, type UploadHandler, type UploadProtocol, type UploadSizeContext, type UploadStorage, createR2BindingUploadStorage, createR2UploadStorage, createUploadContext, createUploadHandler };
|
package/dist/upload.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createR2UploadStorage as
|
|
1
|
+
import{createR2BindingUploadStorage as r}from"./packem_shared/createR2BindingUploadStorage-Vjs0vyJJ.mjs";import{createR2UploadStorage as t}from"./packem_shared/createR2UploadStorage-DGB1xJiD.mjs";import{DEFAULT_MAX_UPLOAD_BYTES as d,createUploadContext as U,createUploadHandler as l}from"./packem_shared/DEFAULT_MAX_UPLOAD_BYTES-BOaXqwP5.mjs";import{R}from"./packem_shared/r2-upload-part-writer-SdQlWaAu.mjs";export{d as DEFAULT_MAX_UPLOAD_BYTES,R as R2_PART_SIZE,r as createR2BindingUploadStorage,t as createR2UploadStorage,U as createUploadContext,l as createUploadHandler};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/storage",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.131",
|
|
4
4
|
"description": "R2-backed storage for Lunora: typed buckets and signed URLs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
54
|
-
"@
|
|
53
|
+
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
54
|
+
"@lunora/platform": "1.0.0-alpha.63",
|
|
55
|
+
"@visulima/storage": "2.0.37",
|
|
55
56
|
"aws4fetch": "1.0.20"
|
|
56
57
|
},
|
|
57
58
|
"engines": {
|
|
@@ -1,4 +0,0 @@
|
|
|
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};
|