naro-js 0.3.0 → 0.5.0
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 +125 -9
- package/dist/auth/storage.d.ts +2 -0
- package/dist/errors.d.ts +5 -0
- package/dist/http.d.ts +24 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +434 -57
- package/dist/storage/answer.d.ts +9 -0
- package/dist/storage/client.d.ts +41 -2
- package/dist/storage/multipart.d.ts +28 -0
- package/dist/storage/path.d.ts +15 -3
- package/dist/storage/types.d.ts +59 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -483,20 +483,129 @@ await avatars.remove([`${user.id}/old.png`]);
|
|
|
483
483
|
const { data: link } = await naro.storage
|
|
484
484
|
.from("docs")
|
|
485
485
|
.createSignedUrl("team-1/plan.pdf", 600);
|
|
486
|
+
const { data: links } = await naro.storage
|
|
487
|
+
.from("docs")
|
|
488
|
+
.createSignedUrls(["team-1/a.pdf", "team-1/b.pdf"], 600);
|
|
489
|
+
const { data: plans } = await naro.storage
|
|
490
|
+
.from("docs")
|
|
491
|
+
.list("team-1", { search: "plan" });
|
|
486
492
|
```
|
|
487
493
|
|
|
488
494
|
- `upload`, `update`, `download`, `info`, `exists`, `list`, `remove`,
|
|
489
|
-
`createSignedUrl` and `getPublicUrl` take
|
|
490
|
-
answer its shapes (`FileObject` in
|
|
495
|
+
`createSignedUrl`, `createSignedUrls` and `getPublicUrl` take
|
|
496
|
+
supabase-js's arguments and answer its shapes (`FileObject` in
|
|
497
|
+
snake_case), as `{ data, error }`.
|
|
491
498
|
One exception: supabase-js's `info` answers a camelCase object
|
|
492
499
|
(`bucketId`, `createdAt`, `size`, `contentType`, …); naro-js's `info`
|
|
493
500
|
answers the same snake_case `FileObject` as `list`, with the size and type
|
|
494
501
|
in `metadata.size` and `metadata.mimetype`.
|
|
495
|
-
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
502
|
+
- `createSignedUrls(paths, expiresIn, { download })` signs 1 to 100 paths in
|
|
503
|
+
one request (naro-js 0.5.0) and answers one `{ path, signedUrl, error }`
|
|
504
|
+
per path, in the order given — a path given twice is answered twice. A
|
|
505
|
+
file that is not there, or that the caller cannot read, has
|
|
506
|
+
`signedUrl: null` and the `error`
|
|
507
|
+
`Object <path> was not found in bucket <bucket>.` — the same words for
|
|
508
|
+
both. `signedUrl` is absolute, as `createSignedUrl`'s. The paths go in the
|
|
509
|
+
body unchecked: a bad one fails the call with `INVALID_REQUEST`
|
|
510
|
+
(`details.path` `paths[<i>]`). Unlike supabase-js, `download` goes in the
|
|
511
|
+
request — the token covers it — so adding `&download` to a URL afterwards
|
|
512
|
+
answers `400 INVALID_SIGNATURE`.
|
|
513
|
+
- `list(prefix, { search })` keeps the names directly below `prefix` that
|
|
514
|
+
start with `search`. As in Supabase, `%` and `_` are plain characters, not
|
|
515
|
+
wildcards. Unlike Supabase, only ASCII letters ignore case (Postgres folds
|
|
516
|
+
case by the database's locale) and `search` takes up to 256 characters. A
|
|
517
|
+
folder shows when a name under it starts with `search`.
|
|
518
|
+
- A backend whose Storage was turned on before these two came answers
|
|
519
|
+
`createSignedUrls` with `404 NOT_FOUND` and a `search` with
|
|
520
|
+
`400 INVALID_REQUEST` (`details.path` `search`, `details.reason`
|
|
521
|
+
`search is not supported yet`): update it once with `naro backend enable`.
|
|
522
|
+
- A public file is kept at Naro's edge for up to 5 minutes (or for its own
|
|
523
|
+
`cacheControl`, when that is shorter): after it is deleted, its bucket is
|
|
524
|
+
made private or it is replaced, `getPublicUrl`'s URL can still serve the
|
|
525
|
+
old bytes that long. Put a version in names that change
|
|
526
|
+
(`avatar-<time>.png`) or keep `cacheControl` short. Signed URLs are not
|
|
527
|
+
kept at the edge: replacing or deleting a file ends its signed links at
|
|
528
|
+
once.
|
|
529
|
+
- An upload sends the file's own bytes, typed as the file is (`Blob.type`,
|
|
530
|
+
or `application/octet-stream`; a string is `text/plain;charset=utf-8`).
|
|
531
|
+
The body is a `Blob`, `File`, `ArrayBuffer`, typed array or string — not
|
|
532
|
+
a stream or `FormData`, and not a buffer transferred away (detached),
|
|
533
|
+
which has no bytes left: `INVALID_BODY`.
|
|
534
|
+
- One request carries 50 MiB. A larger body goes in parts by itself, up to
|
|
535
|
+
the plan's largest object: 16–64 MiB parts, four at a time, each tried
|
|
536
|
+
again up to three times after `NETWORK_ERROR`, a `5xx` or
|
|
537
|
+
`OBJECT_RATE_LIMITED` (waiting 1, 2, then 4 seconds, or the seconds the
|
|
538
|
+
429 says) — not after `507 STORAGE_QUOTA_EXCEEDED`, which lasts until
|
|
539
|
+
files are deleted. A part that still fails fails the upload with its
|
|
540
|
+
error, and the upload is called off. The last step is never tried again: if its
|
|
541
|
+
answer is lost (`NETWORK_ERROR`), ask `info(path)` whether the file is
|
|
542
|
+
there; if the backend refuses it, the upload is called off too and the
|
|
543
|
+
refusal is the answer. A part answered `UPLOAD_NOT_FOUND` belongs to an
|
|
544
|
+
upload that has ended — one left unsent for about an hour has. An upload in
|
|
545
|
+
parts must finish within 24 hours of its start (R2 ends unfinished
|
|
546
|
+
multipart uploads after a day). Replacing a file in parts (`update`, or
|
|
547
|
+
`upload` with `upsert` over a file that is there) needs room in the
|
|
548
|
+
project's capacity for the old file and the new one until it completes: if
|
|
549
|
+
the two together are over it, the upload answers
|
|
550
|
+
`507 STORAGE_QUOTA_EXCEEDED` before any part is sent.
|
|
551
|
+
- `onProgress: (sentBytes, totalBytes) => void` is called after each part
|
|
552
|
+
of an upload over 50 MiB, the last time with both the same. An upload of
|
|
553
|
+
50 MiB or less is one request and does not call it. A `File` is sliced
|
|
554
|
+
into the parts, never read whole first. What the callback throws — or an
|
|
555
|
+
`async` one rejects with — does not stop the upload.
|
|
556
|
+
|
|
557
|
+
- `signal: AbortSignal` stops an upload (`upload` and `update`): what has
|
|
558
|
+
not been answered yet is called off, and the upload answers `ABORTED`
|
|
559
|
+
("The upload was stopped before it finished."). An upload in parts is
|
|
560
|
+
called off on the backend too — no part starts after the signal, and
|
|
561
|
+
`onProgress` is not called again — so its name and the space it reserved
|
|
562
|
+
are free at once, not after the hour its lock lasts. Its first step is
|
|
563
|
+
never cut short (a signal during it is acted on once it answers), and once
|
|
564
|
+
its last step is sent the signal stops nothing: that step's answer is the
|
|
565
|
+
upload's. A signal already aborted sends nothing. A page closed or a
|
|
566
|
+
network lost mid-upload sends no abort: the name waits for the lock.
|
|
567
|
+
|
|
568
|
+
```ts
|
|
569
|
+
const stop = new AbortController();
|
|
570
|
+
cancelButton.onclick = () => stop.abort();
|
|
571
|
+
await naro.storage.from("videos").upload(`${user.id}/talk.mp4`, file, {
|
|
572
|
+
onProgress: (sent, total) => setPercent(Math.round((sent / total) * 100)),
|
|
573
|
+
signal: stop.signal,
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
- `createSignedUploadUrl(path, { upsert })` and
|
|
578
|
+
`uploadToSignedUrl(path, token, body, options)` are supabase-js's signed
|
|
579
|
+
upload (naro-js 0.5.0). A server with the service key — or anyone the
|
|
580
|
+
policies let upload there, anon or a signed-in user — makes a token for
|
|
581
|
+
one path; a page with only the anon key uploads by it. The token stands
|
|
582
|
+
in for the policies; the bucket's and the plan's limits and the
|
|
583
|
+
project's usage stops still apply. It works for 2 hours. Made with
|
|
584
|
+
`upsert: true` it may replace the file until then; without it a second
|
|
585
|
+
upload answers `409 OBJECT_EXISTS`.
|
|
586
|
+
`uploadToSignedUrl` sends no session, takes `contentType`,
|
|
587
|
+
`cacheControl`, `onProgress` and `signal` as `upload` does, and answers
|
|
588
|
+
`{ path, fullPath }`. Over 50 MiB it goes in parts with the token on
|
|
589
|
+
every step (the `x-naro-upload-token` header), and once started may
|
|
590
|
+
finish after the 2 hours. A token that is not the path's answers
|
|
591
|
+
`400 INVALID_SIGNATURE` (one no URL can carry is refused so before a
|
|
592
|
+
request), an expired one `400 SIGNED_URL_EXPIRED`. A token is neither
|
|
593
|
+
used up nor revocable: until its 2 hours end it can create its file again
|
|
594
|
+
after the file is deleted, and it keeps working if the user it was made
|
|
595
|
+
for is deleted or banned.
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
// On the server, with the service key:
|
|
599
|
+
const { data, error } = await admin.storage
|
|
600
|
+
.from("avatars")
|
|
601
|
+
.createSignedUploadUrl(`${userId}/me.png`, { upsert: true });
|
|
602
|
+
if (error) throw new Error(error.message);
|
|
603
|
+
// In the browser, with the anon key:
|
|
604
|
+
await naro.storage
|
|
605
|
+
.from("avatars")
|
|
606
|
+
.uploadToSignedUrl(data.path, data.token, file);
|
|
607
|
+
```
|
|
608
|
+
|
|
500
609
|
- A `contentType` or `cacheControl` no header can carry (a CR, LF or NUL
|
|
501
610
|
inside, or a character past U+00FF) is answered before a request, as the
|
|
502
611
|
backend answers one it cannot read: `INVALID_CONTENT_TYPE`, and
|
|
@@ -512,7 +621,14 @@ const { data: link } = await naro.storage
|
|
|
512
621
|
check.
|
|
513
622
|
- `exists` answers `false` for `OBJECT_NOT_FOUND` and passes every other
|
|
514
623
|
error on. `getPublicUrl` makes no request and answers `{ data, error }`.
|
|
624
|
+
- `objectRoute(bucket, path)` names an object the way the client does:
|
|
625
|
+
`{ ok: true, encoded }` with `<bucket>/<path>` as the object routes carry
|
|
626
|
+
it (each segment `encodeURIComponent`-ed), or `{ ok: false, error }` with
|
|
627
|
+
the client's own `INVALID_PATH` (type `ObjectRouteResult`). A tool that
|
|
628
|
+
streams an object itself builds `/_naro/v1/storage/object/<encoded>` from
|
|
629
|
+
it and gets the URL and the refusals the client would — `naro storage cp`
|
|
630
|
+
downloads that way.
|
|
515
631
|
- `OBJECT_RATE_LIMITED` and `STORAGE_OPS_QUOTA_EXCEEDED` carry
|
|
516
632
|
`details.retryAfter`, in seconds.
|
|
517
|
-
- Not yet: `move`, `copy`,
|
|
518
|
-
|
|
633
|
+
- Not yet: `move`, `copy`, resuming an upload in another session, and bucket
|
|
634
|
+
methods (buckets are made with `naro storage bucket`).
|
package/dist/auth/storage.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare function memoryStorage(): AuthStorage;
|
|
|
18
18
|
export declare function defaultStorage(): AuthStorage;
|
|
19
19
|
/** One token per project URL, so two clients on one page do not collide. */
|
|
20
20
|
export declare function storageKey(baseUrl: string): string;
|
|
21
|
+
/** The access JWT of the signed-in session, beside the session token. */
|
|
22
|
+
export declare function accessJwtKey(baseUrl: string): string;
|
|
21
23
|
/**
|
|
22
24
|
* The PKCE verifiers of the provider sign-ins this browser began, one per
|
|
23
25
|
* flow (P2-6 §6, `flows.ts`), kept where the token is.
|
package/dist/errors.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export declare function networkError(cause: unknown): NaroError;
|
|
|
25
25
|
* code exchange a page runs on load (PR #68 review 10). It is called off.
|
|
26
26
|
*/
|
|
27
27
|
export declare function requestTimeout(ms: number): NaroError;
|
|
28
|
+
/**
|
|
29
|
+
* An upload the caller's `signal` stopped (PR #111 review r1): what was not
|
|
30
|
+
* answered yet was called off. Never tried again.
|
|
31
|
+
*/
|
|
32
|
+
export declare function uploadAborted(): NaroError;
|
|
28
33
|
/** An answer, but not one a Naro worker gives: HTML, empty, or the wrong shape. */
|
|
29
34
|
export declare function invalidResponse(status: number): NaroError;
|
|
30
35
|
/**
|
package/dist/http.d.ts
CHANGED
|
@@ -13,13 +13,24 @@ export type HttpConfig = {
|
|
|
13
13
|
apiKey: string;
|
|
14
14
|
fetch: FetchLike | undefined;
|
|
15
15
|
tokens: TokenStore;
|
|
16
|
+
/**
|
|
17
|
+
* The short-lived access JWT the sign-in answers carry. Data and Storage
|
|
18
|
+
* requests send it; the auth routes keep the session token in `tokens`.
|
|
19
|
+
* Without it (not signed in) those requests go as anon.
|
|
20
|
+
*/
|
|
21
|
+
accessJwt?: TokenStore;
|
|
16
22
|
};
|
|
17
23
|
export type CallOptions = {
|
|
18
|
-
method: "GET" | "POST" | "DELETE";
|
|
24
|
+
method: "GET" | "POST" | "PUT" | "DELETE";
|
|
19
25
|
/** Below `/_naro/v1`, e.g. `/data/query`. */
|
|
20
26
|
path: string;
|
|
21
27
|
/** Sent as JSON. */
|
|
22
28
|
body?: unknown;
|
|
29
|
+
/**
|
|
30
|
+
* Headers beside `apikey` and the JSON type — a signed upload's
|
|
31
|
+
* `x-naro-upload-token` (Storage 3-1e-2). `apikey` is always the client's.
|
|
32
|
+
*/
|
|
33
|
+
headers?: Record<string, string>;
|
|
23
34
|
/** Whether to send the stored session token. */
|
|
24
35
|
withToken: boolean;
|
|
25
36
|
/**
|
|
@@ -60,9 +71,21 @@ export type SendOptions = {
|
|
|
60
71
|
headers?: Record<string, string>;
|
|
61
72
|
body?: BodyInit;
|
|
62
73
|
withToken: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* An upload's `signal` (PR #111 review r1): it calls the request off until
|
|
76
|
+
* its answer comes — ABORTED, not NETWORK_ERROR — and never the reading
|
|
77
|
+
* of an answer that came, which may be a success.
|
|
78
|
+
*/
|
|
79
|
+
signal?: AbortSignal | undefined;
|
|
63
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* A failure carries the answer's HTTP status, or null when nothing answered
|
|
83
|
+
* (NETWORK_ERROR): a multipart upload tries a part again on a 5xx, whatever
|
|
84
|
+
* the body said (Storage 3-1f, plan C7).
|
|
85
|
+
*/
|
|
64
86
|
export type SendResult = {
|
|
65
87
|
error: NaroError;
|
|
88
|
+
status: number | null;
|
|
66
89
|
} | {
|
|
67
90
|
error: null;
|
|
68
91
|
response: Response;
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,8 @@ export type { AuthStorage } from "./auth/storage.js";
|
|
|
5
5
|
export type { NaroError } from "./errors.js";
|
|
6
6
|
export type { FetchLike } from "./http.js";
|
|
7
7
|
export type { StorageClient, StorageFileApi } from "./storage/client.js";
|
|
8
|
-
export
|
|
8
|
+
export { objectRoute, type Encoded as ObjectRouteResult, } from "./storage/path.js";
|
|
9
|
+
export type { CreateSignedUploadUrlOptions, DownloadOption, FileObject, FolderEntry, ListOptions, SignedUploadResult, SignedUploadUrl, SignedUrlEntry, UpdateOptions, UploadBody, UploadOptions, UploadResult, } from "./storage/types.js";
|
|
9
10
|
export type { ArrayValues, FilterGroup, FilterGroupCallback, InsertBuilder, MatchQuery, MutationBuilder, RawFilterValue, SelectBuilder, TableBuilder, } from "./query/builder.js";
|
|
10
11
|
export { quoteFilterValue, type FilterOperatorName } from "./query/filters.js";
|
|
11
12
|
export type { AdminUser, AdminUserAttributes, AdminUserListResponse, AdminUserResponse, AuthActionResponse, AuthErrorCode, AuthFlowType, AuthResponse, DatabaseShape, EmailRedirectOptions, OAuthProvider, OAuthResponse, SignInWithOAuthOptions, UrlSessionResponse, UserAttributes, GenericTable, GenericView, NaroResponse, OrderOptions, PageParams, Session, SessionResponse, SignInCredentials, SignOutResponse, SignUpCredentials, UntypedDatabase, User, UserResponse, } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,12 @@ function requestTimeout(ms) {
|
|
|
14
14
|
message: `The Naro backend did not answer within ${String(Math.round(ms / 1e3))} seconds. Check the network and try again.`
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
+
function uploadAborted() {
|
|
18
|
+
return {
|
|
19
|
+
code: "ABORTED",
|
|
20
|
+
message: "The upload was stopped before it finished."
|
|
21
|
+
};
|
|
22
|
+
}
|
|
17
23
|
function invalidResponse(status) {
|
|
18
24
|
return {
|
|
19
25
|
code: "INVALID_RESPONSE",
|
|
@@ -85,6 +91,10 @@ function errorFromBody(status, body) {
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
// src/http.ts
|
|
94
|
+
var AUTH_PREFIX = "/auth/";
|
|
95
|
+
function storeFor(config, path) {
|
|
96
|
+
return path.startsWith(AUTH_PREFIX) ? config.tokens : config.accessJwt ?? null;
|
|
97
|
+
}
|
|
88
98
|
async function receive(config, options, init) {
|
|
89
99
|
const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
|
|
90
100
|
if (controller) init.signal = controller.signal;
|
|
@@ -128,8 +138,12 @@ async function call(config, options) {
|
|
|
128
138
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
129
139
|
}
|
|
130
140
|
}
|
|
131
|
-
const
|
|
132
|
-
const
|
|
141
|
+
const store = storeFor(config, options.path);
|
|
142
|
+
const token = options.withToken && store ? await store.get() : null;
|
|
143
|
+
const headers = {
|
|
144
|
+
...options.headers,
|
|
145
|
+
apikey: config.apiKey
|
|
146
|
+
};
|
|
133
147
|
if (token !== null) headers.Authorization = `Bearer ${token}`;
|
|
134
148
|
const init = {
|
|
135
149
|
method: options.method,
|
|
@@ -159,17 +173,18 @@ async function call(config, options) {
|
|
|
159
173
|
const error = errorFromBody(status, body);
|
|
160
174
|
if (error === null) return { error: null, answer };
|
|
161
175
|
return {
|
|
162
|
-
error: await settled(
|
|
176
|
+
error: await settled(store, error, responseHeaders, token),
|
|
163
177
|
answer
|
|
164
178
|
};
|
|
165
179
|
}
|
|
166
|
-
async function settled(
|
|
167
|
-
if (error.code === "INVALID_SESSION" && token !== null)
|
|
168
|
-
await
|
|
180
|
+
async function settled(store, error, headers, token) {
|
|
181
|
+
if (error.code === "INVALID_SESSION" && token !== null && store)
|
|
182
|
+
await store.clearIf(token);
|
|
169
183
|
return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
|
|
170
184
|
}
|
|
171
185
|
async function send(config, options) {
|
|
172
|
-
const
|
|
186
|
+
const store = storeFor(config, options.path);
|
|
187
|
+
const token = options.withToken && store ? await store.get() : null;
|
|
173
188
|
const headers = {
|
|
174
189
|
...options.headers,
|
|
175
190
|
apikey: config.apiKey
|
|
@@ -181,6 +196,12 @@ async function send(config, options) {
|
|
|
181
196
|
credentials: "omit"
|
|
182
197
|
};
|
|
183
198
|
if (options.body !== void 0) init.body = options.body;
|
|
199
|
+
const { signal } = options;
|
|
200
|
+
if (signal?.aborted) return { error: uploadAborted(), status: null };
|
|
201
|
+
const controller = signal ? new AbortController() : null;
|
|
202
|
+
const stop = () => controller?.abort();
|
|
203
|
+
if (controller) init.signal = controller.signal;
|
|
204
|
+
signal?.addEventListener("abort", stop);
|
|
184
205
|
let response;
|
|
185
206
|
try {
|
|
186
207
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -189,26 +210,34 @@ async function send(config, options) {
|
|
|
189
210
|
init
|
|
190
211
|
);
|
|
191
212
|
} catch (error2) {
|
|
192
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
error: signal?.aborted ? uploadAborted() : networkError(error2),
|
|
215
|
+
status: null
|
|
216
|
+
};
|
|
217
|
+
} finally {
|
|
218
|
+
signal?.removeEventListener("abort", stop);
|
|
193
219
|
}
|
|
194
|
-
|
|
195
|
-
|
|
220
|
+
const { status } = response;
|
|
221
|
+
if (status >= 200 && status < 300) return { error: null, response };
|
|
196
222
|
let text;
|
|
197
223
|
try {
|
|
198
224
|
text = await response.text();
|
|
199
225
|
} catch (error2) {
|
|
200
|
-
return { error: networkError(error2) };
|
|
226
|
+
return { error: networkError(error2), status: null };
|
|
201
227
|
}
|
|
202
228
|
let body;
|
|
203
229
|
if (text.trim() !== "") {
|
|
204
230
|
try {
|
|
205
231
|
body = JSON.parse(text);
|
|
206
232
|
} catch {
|
|
207
|
-
return { error: invalidResponse(
|
|
233
|
+
return { error: invalidResponse(status), status };
|
|
208
234
|
}
|
|
209
235
|
}
|
|
210
|
-
const error = errorFromBody(
|
|
211
|
-
return {
|
|
236
|
+
const error = errorFromBody(status, body) ?? invalidResponse(status);
|
|
237
|
+
return {
|
|
238
|
+
error: await settled(store, error, response.headers, token),
|
|
239
|
+
status
|
|
240
|
+
};
|
|
212
241
|
}
|
|
213
242
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
214
243
|
"RATE_LIMITED",
|
|
@@ -401,6 +430,7 @@ async function createPkcePair() {
|
|
|
401
430
|
|
|
402
431
|
// src/auth/client.ts
|
|
403
432
|
var TOKEN_HEADER = "set-auth-token";
|
|
433
|
+
var JWT_HEADER = "set-auth-jwt";
|
|
404
434
|
var FRAGMENT_PREFIX = "naro_";
|
|
405
435
|
var OAUTH_PROVIDERS = ["google", "github"];
|
|
406
436
|
var FLOW_TYPES = [
|
|
@@ -419,6 +449,9 @@ function nonEmpty(value) {
|
|
|
419
449
|
function tokenFrom(answer) {
|
|
420
450
|
return nonEmpty(answer.headers.get(TOKEN_HEADER));
|
|
421
451
|
}
|
|
452
|
+
function jwtFrom(answer) {
|
|
453
|
+
return nonEmpty(answer.headers.get(JWT_HEADER));
|
|
454
|
+
}
|
|
422
455
|
function userFrom(body) {
|
|
423
456
|
return isRecord(body) && isRecord(body.user) ? body.user : null;
|
|
424
457
|
}
|
|
@@ -581,7 +614,7 @@ var AuthClient = class {
|
|
|
581
614
|
const user = userFrom(body);
|
|
582
615
|
if (token === null || user === null || !isRecord(body) || !isRecord(body.session))
|
|
583
616
|
return { data: signedOut, error: invalidResponse(result.answer.status) };
|
|
584
|
-
await this.#
|
|
617
|
+
await this.#storeSession(token, jwtFrom(result.answer));
|
|
585
618
|
await this.#flow.recovery.forget();
|
|
586
619
|
return { data: { session: body.session, user }, error: null };
|
|
587
620
|
}
|
|
@@ -617,6 +650,7 @@ var AuthClient = class {
|
|
|
617
650
|
}
|
|
618
651
|
if (type === "recovery" && fragment.naro_token) {
|
|
619
652
|
await this.#http.tokens.clear();
|
|
653
|
+
await this.#http.accessJwt?.clear();
|
|
620
654
|
await this.#flow.recovery.keep(fragment.naro_token);
|
|
621
655
|
return { data: none, error: null };
|
|
622
656
|
}
|
|
@@ -707,6 +741,7 @@ var AuthClient = class {
|
|
|
707
741
|
withToken: true
|
|
708
742
|
});
|
|
709
743
|
await tokens.clearIf(result.answer?.token ?? token);
|
|
744
|
+
await this.#http.accessJwt?.clear();
|
|
710
745
|
return { data: null, error: result.error };
|
|
711
746
|
}
|
|
712
747
|
/**
|
|
@@ -728,7 +763,10 @@ var AuthClient = class {
|
|
|
728
763
|
if (result.error !== null) return { data: signedOut, error: result.error };
|
|
729
764
|
const { body, token } = result.answer;
|
|
730
765
|
if (body === null) {
|
|
731
|
-
if (token !== null)
|
|
766
|
+
if (token !== null) {
|
|
767
|
+
await tokens.clearIf(token);
|
|
768
|
+
await this.#http.accessJwt?.clear();
|
|
769
|
+
}
|
|
732
770
|
return { data: signedOut, error: invalidSession() };
|
|
733
771
|
}
|
|
734
772
|
const user = userFrom(body);
|
|
@@ -759,6 +797,15 @@ var AuthClient = class {
|
|
|
759
797
|
async #pendingUrl() {
|
|
760
798
|
if (this.#fromUrl) await this.#fromUrl;
|
|
761
799
|
}
|
|
800
|
+
/**
|
|
801
|
+
* Keeps a new session: its token, and the access JWT beside it. An answer
|
|
802
|
+
* without a JWT drops the old one — it belonged to the previous session.
|
|
803
|
+
*/
|
|
804
|
+
async #storeSession(token, jwt) {
|
|
805
|
+
await this.#http.tokens.set(token);
|
|
806
|
+
if (jwt !== null) await this.#http.accessJwt?.set(jwt);
|
|
807
|
+
else await this.#http.accessJwt?.clear();
|
|
808
|
+
}
|
|
762
809
|
async #signIn(path, body, needsToken) {
|
|
763
810
|
await this.#flow.recovery.forget();
|
|
764
811
|
const result = await call(this.#http, {
|
|
@@ -778,7 +825,7 @@ var AuthClient = class {
|
|
|
778
825
|
data: { user: null },
|
|
779
826
|
error: invalidResponse(result.answer.status)
|
|
780
827
|
};
|
|
781
|
-
if (token !== null) await this.#
|
|
828
|
+
if (token !== null) await this.#storeSession(token, jwtFrom(result.answer));
|
|
782
829
|
return { data: { user }, error: null };
|
|
783
830
|
}
|
|
784
831
|
};
|
|
@@ -932,6 +979,9 @@ function defaultStorage() {
|
|
|
932
979
|
function storageKey(baseUrl) {
|
|
933
980
|
return `naro-auth-token:${baseUrl}`;
|
|
934
981
|
}
|
|
982
|
+
function accessJwtKey(baseUrl) {
|
|
983
|
+
return `naro-auth-jwt:${baseUrl}`;
|
|
984
|
+
}
|
|
935
985
|
function verifierKey(baseUrl) {
|
|
936
986
|
return `naro-auth-code-verifiers:${baseUrl}`;
|
|
937
987
|
}
|
|
@@ -1806,13 +1856,192 @@ var TableBuilder = class {
|
|
|
1806
1856
|
}
|
|
1807
1857
|
};
|
|
1808
1858
|
|
|
1859
|
+
// src/storage/answer.ts
|
|
1860
|
+
function fail(error) {
|
|
1861
|
+
return { data: null, error };
|
|
1862
|
+
}
|
|
1863
|
+
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
1864
|
+
function dataOf2(status, body, guard) {
|
|
1865
|
+
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
1866
|
+
return { data: body.data, error: null };
|
|
1867
|
+
return fail(invalidResponse(status));
|
|
1868
|
+
}
|
|
1869
|
+
async function readJson(response, guard) {
|
|
1870
|
+
let text;
|
|
1871
|
+
try {
|
|
1872
|
+
text = await response.text();
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
return fail(networkError(error));
|
|
1875
|
+
}
|
|
1876
|
+
let body;
|
|
1877
|
+
try {
|
|
1878
|
+
body = JSON.parse(text);
|
|
1879
|
+
} catch {
|
|
1880
|
+
return fail(invalidResponse(response.status));
|
|
1881
|
+
}
|
|
1882
|
+
return dataOf2(response.status, body, guard);
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
// src/storage/multipart.ts
|
|
1886
|
+
var ROUTE = "/storage/upload/multipart";
|
|
1887
|
+
var UPLOAD_TOKEN_HEADER = "x-naro-upload-token";
|
|
1888
|
+
var CONCURRENCY = 4;
|
|
1889
|
+
var ATTEMPTS = 4;
|
|
1890
|
+
var BACKOFF_MS = [1e3, 2e3, 4e3];
|
|
1891
|
+
var UPLOAD_ID = /^[\w-][\w.-]*$/;
|
|
1892
|
+
var positive = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
1893
|
+
var planFor = (size) => (value) => isRecord(value) && typeof value.uploadId === "string" && UPLOAD_ID.test(value.uploadId) && positive(value.partSize) && positive(value.partCount) && (value.partCount - 1) * value.partSize < size && value.partCount * value.partSize >= size;
|
|
1894
|
+
var partAnswer = (n) => (value) => isRecord(value) && value.partNumber === n && typeof value.etag === "string" && value.etag !== "";
|
|
1895
|
+
var retryable = (error, status) => error.code === "NETWORK_ERROR" || error.code === "OBJECT_RATE_LIMITED" || status !== null && status >= 500 && error.code !== "STORAGE_QUOTA_EXCEEDED";
|
|
1896
|
+
function waitAfter(error, attempt2) {
|
|
1897
|
+
const after = isRecord(error.details) ? error.details.retryAfter : void 0;
|
|
1898
|
+
if (typeof after === "number") return after * 1e3;
|
|
1899
|
+
if (error.code === "OBJECT_RATE_LIMITED") return 1e3;
|
|
1900
|
+
return BACKOFF_MS[attempt2 - 1] ?? 4e3;
|
|
1901
|
+
}
|
|
1902
|
+
var sleepFor = (ms, signal) => new Promise((done) => {
|
|
1903
|
+
if (signal?.aborted) {
|
|
1904
|
+
done();
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
const end = () => {
|
|
1908
|
+
clearTimeout(timer);
|
|
1909
|
+
signal?.removeEventListener("abort", end);
|
|
1910
|
+
done();
|
|
1911
|
+
};
|
|
1912
|
+
const timer = setTimeout(end, ms);
|
|
1913
|
+
signal?.addEventListener("abort", end);
|
|
1914
|
+
});
|
|
1915
|
+
async function uploadInParts(a) {
|
|
1916
|
+
const { signal } = a;
|
|
1917
|
+
const sleep = a.sleep ?? sleepFor;
|
|
1918
|
+
const signed = a.uploadToken === void 0 ? {} : { [UPLOAD_TOKEN_HEADER]: a.uploadToken };
|
|
1919
|
+
const withToken = a.uploadToken === void 0;
|
|
1920
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
1921
|
+
const started = await call(a.http, {
|
|
1922
|
+
method: a.method,
|
|
1923
|
+
path: ROUTE,
|
|
1924
|
+
body: {
|
|
1925
|
+
bucket: a.bucket,
|
|
1926
|
+
path: a.path,
|
|
1927
|
+
size: a.size,
|
|
1928
|
+
contentType: a.contentType,
|
|
1929
|
+
cacheControl: a.cacheControl,
|
|
1930
|
+
// PUT always replaces: the worker refuses `upsert` on it.
|
|
1931
|
+
...a.method === "POST" && a.upsert ? { upsert: true } : {}
|
|
1932
|
+
},
|
|
1933
|
+
headers: signed,
|
|
1934
|
+
withToken
|
|
1935
|
+
});
|
|
1936
|
+
if (started.error !== null) return fail(started.error);
|
|
1937
|
+
const plan = dataOf2(
|
|
1938
|
+
started.answer.status,
|
|
1939
|
+
started.answer.body,
|
|
1940
|
+
planFor(a.size)
|
|
1941
|
+
);
|
|
1942
|
+
if (plan.error !== null) return fail(plan.error);
|
|
1943
|
+
const { uploadId, partSize, partCount } = plan.data;
|
|
1944
|
+
const base = `${ROUTE}/${uploadId}`;
|
|
1945
|
+
const progress = (sentBytes2) => {
|
|
1946
|
+
if (signal?.aborted) return;
|
|
1947
|
+
try {
|
|
1948
|
+
const result = a.onProgress?.(sentBytes2, a.size);
|
|
1949
|
+
if (isThenable(result)) result.then(void 0, () => void 0);
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
const callOff = async () => {
|
|
1954
|
+
await call(a.http, {
|
|
1955
|
+
method: "DELETE",
|
|
1956
|
+
path: base,
|
|
1957
|
+
headers: signed,
|
|
1958
|
+
withToken
|
|
1959
|
+
}).catch(() => void 0);
|
|
1960
|
+
};
|
|
1961
|
+
const etags = [];
|
|
1962
|
+
const failure = {
|
|
1963
|
+
error: signal?.aborted ? uploadAborted() : null
|
|
1964
|
+
};
|
|
1965
|
+
let next = 1;
|
|
1966
|
+
let sentBytes = 0;
|
|
1967
|
+
const onAbort = () => {
|
|
1968
|
+
failure.error ??= uploadAborted();
|
|
1969
|
+
};
|
|
1970
|
+
const tryPart = async (n, bytes) => {
|
|
1971
|
+
const sent = await send(a.http, {
|
|
1972
|
+
method: "PUT",
|
|
1973
|
+
path: `${base}/${String(n)}`,
|
|
1974
|
+
headers: { "content-type": "application/octet-stream", ...signed },
|
|
1975
|
+
body: bytes,
|
|
1976
|
+
withToken,
|
|
1977
|
+
signal
|
|
1978
|
+
});
|
|
1979
|
+
if (sent.error !== null) return sent;
|
|
1980
|
+
const read = await readJson(sent.response, partAnswer(n));
|
|
1981
|
+
if (read.error !== null)
|
|
1982
|
+
return { error: read.error, status: sent.response.status };
|
|
1983
|
+
return { etag: read.data.etag };
|
|
1984
|
+
};
|
|
1985
|
+
const sendPart = async (n) => {
|
|
1986
|
+
const from = (n - 1) * partSize;
|
|
1987
|
+
const to = Math.min(from + partSize, a.size);
|
|
1988
|
+
const bytes = a.body instanceof Uint8Array ? a.body.subarray(from, to) : a.body.slice(from, to);
|
|
1989
|
+
for (let attempt2 = 1; ; attempt2 += 1) {
|
|
1990
|
+
const tried = await tryPart(n, bytes);
|
|
1991
|
+
if ("etag" in tried) {
|
|
1992
|
+
etags[n - 1] = tried.etag;
|
|
1993
|
+
sentBytes += to - from;
|
|
1994
|
+
progress(sentBytes);
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
if (attempt2 === ATTEMPTS || failure.error !== null || !retryable(tried.error, tried.status))
|
|
1998
|
+
return tried.error;
|
|
1999
|
+
await sleep(waitAfter(tried.error, attempt2), signal);
|
|
2000
|
+
if (failure.error !== null) return tried.error;
|
|
2001
|
+
}
|
|
2002
|
+
};
|
|
2003
|
+
const worker = async () => {
|
|
2004
|
+
while (failure.error === null && next <= partCount) {
|
|
2005
|
+
const n = next;
|
|
2006
|
+
next += 1;
|
|
2007
|
+
const error = await sendPart(n);
|
|
2008
|
+
if (error !== null) failure.error ??= error;
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
signal?.addEventListener("abort", onAbort);
|
|
2012
|
+
await Promise.all(
|
|
2013
|
+
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
2014
|
+
);
|
|
2015
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2016
|
+
if (failure.error !== null) {
|
|
2017
|
+
await callOff();
|
|
2018
|
+
return fail(failure.error);
|
|
2019
|
+
}
|
|
2020
|
+
const completed = await call(a.http, {
|
|
2021
|
+
method: "POST",
|
|
2022
|
+
path: `${base}/complete`,
|
|
2023
|
+
body: {
|
|
2024
|
+
parts: etags.map((etag, i) => ({ partNumber: i + 1, etag }))
|
|
2025
|
+
},
|
|
2026
|
+
headers: signed,
|
|
2027
|
+
withToken
|
|
2028
|
+
});
|
|
2029
|
+
if (completed.error !== null) {
|
|
2030
|
+
if (completed.answer !== null && completed.error.code !== "INVALID_RESPONSE")
|
|
2031
|
+
await callOff();
|
|
2032
|
+
return fail(completed.error);
|
|
2033
|
+
}
|
|
2034
|
+
return dataOf2(completed.answer.status, completed.answer.body, isUploadResult);
|
|
2035
|
+
}
|
|
2036
|
+
var isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
2037
|
+
|
|
1809
2038
|
// src/storage/path.ts
|
|
1810
2039
|
var MAX_SINGLE_UPLOAD_BYTES = 52428800;
|
|
1811
2040
|
var STORAGE_MESSAGES = {
|
|
1812
2041
|
bucket: "The bucket name is not valid.",
|
|
1813
|
-
tooLarge: "Uploads over 50 MiB are not supported yet.",
|
|
1814
2042
|
request: "The storage request is not valid.",
|
|
1815
|
-
body: "upload takes a Blob, File, ArrayBuffer, typed array or string."
|
|
2043
|
+
body: "upload takes a Blob, File, ArrayBuffer, typed array or string.",
|
|
2044
|
+
signature: "The signed URL is not valid."
|
|
1816
2045
|
};
|
|
1817
2046
|
var RESERVED_BUCKETS = /* @__PURE__ */ new Set([
|
|
1818
2047
|
"authenticated",
|
|
@@ -1835,9 +2064,6 @@ function invalidPath(reason) {
|
|
|
1835
2064
|
message: `The object path is not valid: it ${reason}.`
|
|
1836
2065
|
};
|
|
1837
2066
|
}
|
|
1838
|
-
function objectTooLarge() {
|
|
1839
|
-
return { code: "OBJECT_TOO_LARGE", message: STORAGE_MESSAGES.tooLarge };
|
|
1840
|
-
}
|
|
1841
2067
|
function encodePath(path) {
|
|
1842
2068
|
if (typeof path !== "string" || path === "")
|
|
1843
2069
|
return { ok: false, error: invalidPath("is empty") };
|
|
@@ -1874,6 +2100,16 @@ function encodeBucket(bucket) {
|
|
|
1874
2100
|
};
|
|
1875
2101
|
return { ok: true, encoded: encodeURIComponent(bucket) };
|
|
1876
2102
|
}
|
|
2103
|
+
function objectRoute(bucket, path) {
|
|
2104
|
+
const encodedBucket = encodeBucket(bucket);
|
|
2105
|
+
if (!encodedBucket.ok) return encodedBucket;
|
|
2106
|
+
const encodedPath = encodePath(path);
|
|
2107
|
+
if (!encodedPath.ok) return encodedPath;
|
|
2108
|
+
return {
|
|
2109
|
+
ok: true,
|
|
2110
|
+
encoded: `${encodedBucket.encoded}/${encodedPath.encoded}`
|
|
2111
|
+
};
|
|
2112
|
+
}
|
|
1877
2113
|
var isArrayBuffer = (value) => Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
1878
2114
|
function isDetached(buffer) {
|
|
1879
2115
|
if (!isArrayBuffer(buffer)) return false;
|
|
@@ -1956,36 +2192,26 @@ function payloadOf(body, contentType) {
|
|
|
1956
2192
|
|
|
1957
2193
|
// src/storage/client.ts
|
|
1958
2194
|
var SIGNED_PREFIX = "/_naro/v1/storage/object/sign/";
|
|
1959
|
-
|
|
1960
|
-
|
|
2195
|
+
var SIGNED_UPLOAD_PREFIX = "/_naro/v1/storage/object/upload/sign/";
|
|
2196
|
+
var TOKEN_TEXT = /^[\x21-\x7e]+$/;
|
|
2197
|
+
var TOKEN_PARAMETER = /([?&]token=)[^&#\s"'<>]*/g;
|
|
2198
|
+
function withoutToken(error, token) {
|
|
2199
|
+
const { details } = error;
|
|
2200
|
+
if (!isRecord(details) || typeof details.cause !== "string") return error;
|
|
2201
|
+
let cause = details.cause.replace(TOKEN_PARAMETER, "$1[redacted]");
|
|
2202
|
+
for (const form of [encodeURIComponent(token), token])
|
|
2203
|
+
cause = cause.replaceAll(form, "[redacted]");
|
|
2204
|
+
return { ...error, details: { ...details, cause } };
|
|
1961
2205
|
}
|
|
1962
|
-
var isUploadResult = (value) => isRecord(value) && typeof value.id === "string" && typeof value.path === "string" && typeof value.fullPath === "string";
|
|
1963
2206
|
var isFileObject = (value) => isRecord(value) && typeof value.id === "string" && typeof value.name === "string" && isRecord(value.metadata);
|
|
1964
2207
|
var isEntry2 = (value) => isRecord(value) && typeof value.name === "string" && (value.id === null || isFileObject(value));
|
|
1965
2208
|
var isSigned = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX);
|
|
2209
|
+
var isSignedEntry = (value) => isRecord(value) && typeof value.path === "string" && (value.signedUrl === null || typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_PREFIX)) && (value.error === null || typeof value.error === "string");
|
|
2210
|
+
var isSignedUpload = (value) => isRecord(value) && typeof value.signedUrl === "string" && value.signedUrl.startsWith(SIGNED_UPLOAD_PREFIX) && typeof value.token === "string" && value.token !== "" && typeof value.path === "string";
|
|
2211
|
+
var partsBody = (body) => isArrayBuffer(body) ? new Uint8Array(body) : body;
|
|
1966
2212
|
function listOf(guard) {
|
|
1967
2213
|
return (value) => Array.isArray(value) && value.every(guard);
|
|
1968
2214
|
}
|
|
1969
|
-
function dataOf2(status, body, guard) {
|
|
1970
|
-
if (isRecord(body) && Object.hasOwn(body, "data") && guard(body.data))
|
|
1971
|
-
return { data: body.data, error: null };
|
|
1972
|
-
return fail(invalidResponse(status));
|
|
1973
|
-
}
|
|
1974
|
-
async function readJson(response, guard) {
|
|
1975
|
-
let text;
|
|
1976
|
-
try {
|
|
1977
|
-
text = await response.text();
|
|
1978
|
-
} catch (error) {
|
|
1979
|
-
return fail(networkError(error));
|
|
1980
|
-
}
|
|
1981
|
-
let body;
|
|
1982
|
-
try {
|
|
1983
|
-
body = JSON.parse(text);
|
|
1984
|
-
} catch {
|
|
1985
|
-
return fail(invalidResponse(response.status));
|
|
1986
|
-
}
|
|
1987
|
-
return dataOf2(response.status, body, guard);
|
|
1988
|
-
}
|
|
1989
2215
|
var StorageFileApi = class {
|
|
1990
2216
|
#http;
|
|
1991
2217
|
#auth;
|
|
@@ -1997,14 +2223,9 @@ var StorageFileApi = class {
|
|
|
1997
2223
|
}
|
|
1998
2224
|
/** `/storage/object/<route><bucket>/<path>`, or the refusal no request needs. */
|
|
1999
2225
|
#objectPath(route, path) {
|
|
2000
|
-
const
|
|
2001
|
-
if (!
|
|
2002
|
-
|
|
2003
|
-
if (!name.ok) return name;
|
|
2004
|
-
return {
|
|
2005
|
-
ok: true,
|
|
2006
|
-
path: `/storage/object/${route}${bucket.encoded}/${name.encoded}`
|
|
2007
|
-
};
|
|
2226
|
+
const target = objectRoute(this.#bucket, path);
|
|
2227
|
+
if (!target.ok) return target;
|
|
2228
|
+
return { ok: true, path: `/storage/object/${route}${target.encoded}` };
|
|
2008
2229
|
}
|
|
2009
2230
|
/** `/storage/object/<route><bucket>`, for the routes that take names in the body. */
|
|
2010
2231
|
#bucketPath(route) {
|
|
@@ -2026,8 +2247,23 @@ var StorageFileApi = class {
|
|
|
2026
2247
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
2027
2248
|
const unreadable = headerProblem(options.contentType, cache);
|
|
2028
2249
|
if (unreadable !== null) return fail(unreadable);
|
|
2029
|
-
|
|
2250
|
+
const { signal } = options;
|
|
2251
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
2030
2252
|
await this.#auth.ready();
|
|
2253
|
+
if (payload.size > MAX_SINGLE_UPLOAD_BYTES)
|
|
2254
|
+
return uploadInParts({
|
|
2255
|
+
http: this.#http,
|
|
2256
|
+
method,
|
|
2257
|
+
bucket: this.#bucket,
|
|
2258
|
+
path,
|
|
2259
|
+
body: partsBody(payload.body),
|
|
2260
|
+
size: payload.size,
|
|
2261
|
+
contentType: payload.type,
|
|
2262
|
+
cacheControl: cache,
|
|
2263
|
+
upsert,
|
|
2264
|
+
...options.onProgress ? { onProgress: options.onProgress } : {},
|
|
2265
|
+
signal
|
|
2266
|
+
});
|
|
2031
2267
|
const headers = {
|
|
2032
2268
|
"content-type": payload.type,
|
|
2033
2269
|
"cache-control": cache
|
|
@@ -2038,7 +2274,9 @@ var StorageFileApi = class {
|
|
|
2038
2274
|
path: target.path,
|
|
2039
2275
|
headers,
|
|
2040
2276
|
body: payload.body,
|
|
2041
|
-
withToken: true
|
|
2277
|
+
withToken: true,
|
|
2278
|
+
// The fetch stops with the body: the worker undoes its claim at once.
|
|
2279
|
+
signal
|
|
2042
2280
|
});
|
|
2043
2281
|
if (sent.error !== null) return fail(sent.error);
|
|
2044
2282
|
return readJson(sent.response, isUploadResult);
|
|
@@ -2098,7 +2336,11 @@ var StorageFileApi = class {
|
|
|
2098
2336
|
}
|
|
2099
2337
|
/**
|
|
2100
2338
|
* What is directly below `prefix` (the whole bucket by default): folders
|
|
2101
|
-
* first, then files.
|
|
2339
|
+
* first, then files. `search` keeps the names below the prefix that start
|
|
2340
|
+
* with it, ASCII case ignored (Storage 3-1g); a backend whose Storage was
|
|
2341
|
+
* turned on before it answers `INVALID_REQUEST` (`details.path`
|
|
2342
|
+
* `search`) — update it with `naro backend enable`. The options go to the
|
|
2343
|
+
* worker as they are given.
|
|
2102
2344
|
*/
|
|
2103
2345
|
async list(prefix, options = {}) {
|
|
2104
2346
|
const target = this.#bucketPath("list/");
|
|
@@ -2146,6 +2388,139 @@ var StorageFileApi = class {
|
|
|
2146
2388
|
error: null
|
|
2147
2389
|
};
|
|
2148
2390
|
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Links to several objects at once (Storage 3-1g): up to 100 paths, each
|
|
2393
|
+
* for `expiresIn` seconds (1–604,800), with the same `download`. One entry
|
|
2394
|
+
* per path, in the order given — a path given twice is answered twice.
|
|
2395
|
+
* A path that is not there, or that the caller cannot read, has
|
|
2396
|
+
* `signedUrl: null` and `error` "Object <path> was not found in bucket
|
|
2397
|
+
* <bucket>." — the same words for both. The paths go in the body, so the
|
|
2398
|
+
* backend checks them: a bad one fails the call with `INVALID_REQUEST`
|
|
2399
|
+
* (`details.path` `paths[<i>]`). A backend whose Storage was turned on
|
|
2400
|
+
* before this method came answers `NOT_FOUND`: update it with
|
|
2401
|
+
* `naro backend enable`.
|
|
2402
|
+
*/
|
|
2403
|
+
async createSignedUrls(paths, expiresIn, options = {}) {
|
|
2404
|
+
const target = this.#bucketPath("sign/");
|
|
2405
|
+
if (!target.ok) return fail(target.error);
|
|
2406
|
+
const { download } = options ?? {};
|
|
2407
|
+
const signed = await this.#json(
|
|
2408
|
+
{
|
|
2409
|
+
method: "POST",
|
|
2410
|
+
path: target.path,
|
|
2411
|
+
body: {
|
|
2412
|
+
expiresIn,
|
|
2413
|
+
paths,
|
|
2414
|
+
...download === void 0 || download === false ? {} : { download }
|
|
2415
|
+
}
|
|
2416
|
+
},
|
|
2417
|
+
listOf(isSignedEntry)
|
|
2418
|
+
);
|
|
2419
|
+
if (signed.error !== null) return signed;
|
|
2420
|
+
return {
|
|
2421
|
+
data: signed.data.map((entry) => ({
|
|
2422
|
+
path: entry.path,
|
|
2423
|
+
signedUrl: entry.signedUrl === null ? null : `${this.#http.baseUrl}${entry.signedUrl}`,
|
|
2424
|
+
error: entry.error
|
|
2425
|
+
})),
|
|
2426
|
+
error: null
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* A token that lets whoever holds it upload `path` for two hours (Storage
|
|
2431
|
+
* 3-1e-2), as supabase-js's: made where the key is — a server with the
|
|
2432
|
+
* service key for any path, anon or a signed-in user only where the
|
|
2433
|
+
* policies let them upload. With `upsert: true` it may replace a file
|
|
2434
|
+
* that is there.
|
|
2435
|
+
* The token goes to `uploadToSignedUrl`; `signedUrl` is where a client
|
|
2436
|
+
* without naro-js PUTs the bytes.
|
|
2437
|
+
*/
|
|
2438
|
+
async createSignedUploadUrl(path, options = {}) {
|
|
2439
|
+
const target = this.#objectPath("upload/sign/", path);
|
|
2440
|
+
if (!target.ok) return fail(target.error);
|
|
2441
|
+
const upsert = (options ?? {}).upsert === true;
|
|
2442
|
+
const signed = await this.#json(
|
|
2443
|
+
{
|
|
2444
|
+
method: "POST",
|
|
2445
|
+
path: target.path,
|
|
2446
|
+
body: upsert ? { upsert: true } : {}
|
|
2447
|
+
},
|
|
2448
|
+
isSignedUpload
|
|
2449
|
+
);
|
|
2450
|
+
if (signed.error !== null) return signed;
|
|
2451
|
+
return {
|
|
2452
|
+
data: {
|
|
2453
|
+
signedUrl: `${this.#http.baseUrl}${signed.data.signedUrl}`,
|
|
2454
|
+
token: signed.data.token,
|
|
2455
|
+
path: signed.data.path
|
|
2456
|
+
},
|
|
2457
|
+
error: null
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
/**
|
|
2461
|
+
* Uploads `body` to `path` with a token from `createSignedUploadUrl`
|
|
2462
|
+
* (Storage 3-1e-2). No session is sent, or needed: the token is the
|
|
2463
|
+
* permission, so a page's anon-key client uploads where only a server
|
|
2464
|
+
* could sign. It works until the token expires, two hours from when it
|
|
2465
|
+
* was made; one made without `upsert` uploads one new file
|
|
2466
|
+
* (`OBJECT_EXISTS` for a name that is there). A body over 50 MiB goes in
|
|
2467
|
+
* parts with the token on every step, and once started may finish after
|
|
2468
|
+
* the two hours. `contentType`, `cacheControl`, `onProgress` and `signal`
|
|
2469
|
+
* are `upload`'s.
|
|
2470
|
+
*/
|
|
2471
|
+
async uploadToSignedUrl(path, token, body, options = {}) {
|
|
2472
|
+
const given = options ?? {};
|
|
2473
|
+
const target = this.#objectPath("upload/sign/", path);
|
|
2474
|
+
if (!target.ok) return fail(target.error);
|
|
2475
|
+
if (typeof token !== "string" || !TOKEN_TEXT.test(token))
|
|
2476
|
+
return fail({
|
|
2477
|
+
code: "INVALID_SIGNATURE",
|
|
2478
|
+
message: STORAGE_MESSAGES.signature
|
|
2479
|
+
});
|
|
2480
|
+
const payload = payloadOf(body, given.contentType);
|
|
2481
|
+
if (!payload.ok) return fail(payload.error);
|
|
2482
|
+
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
2483
|
+
const unreadable = headerProblem(given.contentType, cache);
|
|
2484
|
+
if (unreadable !== null) return fail(unreadable);
|
|
2485
|
+
const { signal } = given;
|
|
2486
|
+
if (signal?.aborted) return fail(uploadAborted());
|
|
2487
|
+
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES ? await uploadInParts({
|
|
2488
|
+
http: this.#http,
|
|
2489
|
+
method: "POST",
|
|
2490
|
+
bucket: this.#bucket,
|
|
2491
|
+
path,
|
|
2492
|
+
body: partsBody(payload.body),
|
|
2493
|
+
size: payload.size,
|
|
2494
|
+
contentType: payload.type,
|
|
2495
|
+
cacheControl: cache,
|
|
2496
|
+
upsert: false,
|
|
2497
|
+
uploadToken: token,
|
|
2498
|
+
...given.onProgress ? { onProgress: given.onProgress } : {},
|
|
2499
|
+
signal
|
|
2500
|
+
}) : await this.#putSigned(target.path, token, payload, cache, signal);
|
|
2501
|
+
if (uploaded.error !== null) return fail(uploaded.error);
|
|
2502
|
+
return {
|
|
2503
|
+
data: { path: uploaded.data.path, fullPath: uploaded.data.fullPath },
|
|
2504
|
+
error: null
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* The one-request signed upload: the token in the query, no session — it
|
|
2509
|
+
* is the permission. A failure's cause never carries the token.
|
|
2510
|
+
*/
|
|
2511
|
+
async #putSigned(path, token, payload, cache, signal) {
|
|
2512
|
+
const sent = await send(this.#http, {
|
|
2513
|
+
method: "PUT",
|
|
2514
|
+
path: `${path}?token=${encodeURIComponent(token)}`,
|
|
2515
|
+
headers: { "content-type": payload.type, "cache-control": cache },
|
|
2516
|
+
body: payload.body,
|
|
2517
|
+
withToken: false,
|
|
2518
|
+
signal
|
|
2519
|
+
});
|
|
2520
|
+
if (sent.error !== null) return fail(withoutToken(sent.error, token));
|
|
2521
|
+
const read = await readJson(sent.response, isUploadResult);
|
|
2522
|
+
return read.error === null ? read : fail(withoutToken(read.error, token));
|
|
2523
|
+
}
|
|
2149
2524
|
/**
|
|
2150
2525
|
* A public bucket's URL for the object. No request is made, so whether
|
|
2151
2526
|
* the bucket is public is not checked (as in supabase-js); a path no URL
|
|
@@ -2255,7 +2630,8 @@ var NaroClient = class {
|
|
|
2255
2630
|
baseUrl,
|
|
2256
2631
|
apiKey,
|
|
2257
2632
|
fetch: options.fetch,
|
|
2258
|
-
tokens: new TokenStore(storage, storageKey(baseUrl))
|
|
2633
|
+
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
2634
|
+
accessJwt: new TokenStore(storage, accessJwtKey(baseUrl))
|
|
2259
2635
|
};
|
|
2260
2636
|
this.auth = new AuthClient(http, {
|
|
2261
2637
|
verifiers: new FlowVerifiers(
|
|
@@ -2282,5 +2658,6 @@ function createClient(options) {
|
|
|
2282
2658
|
export {
|
|
2283
2659
|
NaroClient,
|
|
2284
2660
|
createClient,
|
|
2661
|
+
objectRoute,
|
|
2285
2662
|
quoteFilterValue
|
|
2286
2663
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type NaroError } from "../errors.js";
|
|
2
|
+
import type { NaroResponse } from "../types.js";
|
|
3
|
+
import type { UploadResult } from "./types.js";
|
|
4
|
+
export declare function fail<T>(error: NaroError): NaroResponse<T>;
|
|
5
|
+
export declare const isUploadResult: (value: unknown) => value is UploadResult;
|
|
6
|
+
/** `{ data }` of a worker's JSON answer that passes `guard`, else INVALID_RESPONSE. */
|
|
7
|
+
export declare function dataOf<T>(status: number, body: unknown, guard: (value: unknown) => value is T): NaroResponse<T>;
|
|
8
|
+
/** A 2xx answer `send` handed back unread, read as `dataOf` reads one. */
|
|
9
|
+
export declare function readJson<T>(response: Response, guard: (value: unknown) => value is T): Promise<NaroResponse<T>>;
|
package/dist/storage/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AuthClient } from "../auth/client.js";
|
|
2
2
|
import { type HttpConfig } from "../http.js";
|
|
3
3
|
import type { NaroResponse } from "../types.js";
|
|
4
|
-
import type { DownloadOption, FileObject, FolderEntry, ListOptions, UpdateOptions, UploadBody, UploadOptions, UploadResult } from "./types.js";
|
|
4
|
+
import type { CreateSignedUploadUrlOptions, DownloadOption, FileObject, FolderEntry, ListOptions, SignedUploadResult, SignedUploadUrl, SignedUrlEntry, UpdateOptions, UploadBody, UploadOptions, UploadResult } from "./types.js";
|
|
5
5
|
/** One bucket's objects: supabase-js's StorageFileApi. */
|
|
6
6
|
export declare class StorageFileApi {
|
|
7
7
|
#private;
|
|
@@ -31,7 +31,11 @@ export declare class StorageFileApi {
|
|
|
31
31
|
exists(path: string): Promise<NaroResponse<boolean>>;
|
|
32
32
|
/**
|
|
33
33
|
* What is directly below `prefix` (the whole bucket by default): folders
|
|
34
|
-
* first, then files.
|
|
34
|
+
* first, then files. `search` keeps the names below the prefix that start
|
|
35
|
+
* with it, ASCII case ignored (Storage 3-1g); a backend whose Storage was
|
|
36
|
+
* turned on before it answers `INVALID_REQUEST` (`details.path`
|
|
37
|
+
* `search`) — update it with `naro backend enable`. The options go to the
|
|
38
|
+
* worker as they are given.
|
|
35
39
|
*/
|
|
36
40
|
list(prefix?: string, options?: ListOptions): Promise<NaroResponse<(FileObject | FolderEntry)[]>>;
|
|
37
41
|
/** Removes the objects named — 1 to 1,000 — and answers the ones it removed. */
|
|
@@ -43,6 +47,41 @@ export declare class StorageFileApi {
|
|
|
43
47
|
createSignedUrl(path: string, expiresIn: number, options?: DownloadOption): Promise<NaroResponse<{
|
|
44
48
|
signedUrl: string;
|
|
45
49
|
}>>;
|
|
50
|
+
/**
|
|
51
|
+
* Links to several objects at once (Storage 3-1g): up to 100 paths, each
|
|
52
|
+
* for `expiresIn` seconds (1–604,800), with the same `download`. One entry
|
|
53
|
+
* per path, in the order given — a path given twice is answered twice.
|
|
54
|
+
* A path that is not there, or that the caller cannot read, has
|
|
55
|
+
* `signedUrl: null` and `error` "Object <path> was not found in bucket
|
|
56
|
+
* <bucket>." — the same words for both. The paths go in the body, so the
|
|
57
|
+
* backend checks them: a bad one fails the call with `INVALID_REQUEST`
|
|
58
|
+
* (`details.path` `paths[<i>]`). A backend whose Storage was turned on
|
|
59
|
+
* before this method came answers `NOT_FOUND`: update it with
|
|
60
|
+
* `naro backend enable`.
|
|
61
|
+
*/
|
|
62
|
+
createSignedUrls(paths: string[], expiresIn: number, options?: DownloadOption): Promise<NaroResponse<SignedUrlEntry[]>>;
|
|
63
|
+
/**
|
|
64
|
+
* A token that lets whoever holds it upload `path` for two hours (Storage
|
|
65
|
+
* 3-1e-2), as supabase-js's: made where the key is — a server with the
|
|
66
|
+
* service key for any path, anon or a signed-in user only where the
|
|
67
|
+
* policies let them upload. With `upsert: true` it may replace a file
|
|
68
|
+
* that is there.
|
|
69
|
+
* The token goes to `uploadToSignedUrl`; `signedUrl` is where a client
|
|
70
|
+
* without naro-js PUTs the bytes.
|
|
71
|
+
*/
|
|
72
|
+
createSignedUploadUrl(path: string, options?: CreateSignedUploadUrlOptions): Promise<NaroResponse<SignedUploadUrl>>;
|
|
73
|
+
/**
|
|
74
|
+
* Uploads `body` to `path` with a token from `createSignedUploadUrl`
|
|
75
|
+
* (Storage 3-1e-2). No session is sent, or needed: the token is the
|
|
76
|
+
* permission, so a page's anon-key client uploads where only a server
|
|
77
|
+
* could sign. It works until the token expires, two hours from when it
|
|
78
|
+
* was made; one made without `upsert` uploads one new file
|
|
79
|
+
* (`OBJECT_EXISTS` for a name that is there). A body over 50 MiB goes in
|
|
80
|
+
* parts with the token on every step, and once started may finish after
|
|
81
|
+
* the two hours. `contentType`, `cacheControl`, `onProgress` and `signal`
|
|
82
|
+
* are `upload`'s.
|
|
83
|
+
*/
|
|
84
|
+
uploadToSignedUrl(path: string, token: string, body: UploadBody, options?: UpdateOptions): Promise<NaroResponse<SignedUploadResult>>;
|
|
46
85
|
/**
|
|
47
86
|
* A public bucket's URL for the object. No request is made, so whether
|
|
48
87
|
* the bucket is public is not checked (as in supabase-js); a path no URL
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HttpConfig } from "../http.js";
|
|
2
|
+
import type { NaroResponse } from "../types.js";
|
|
3
|
+
import type { UploadResult } from "./types.js";
|
|
4
|
+
export type PartsArgs = {
|
|
5
|
+
http: HttpConfig;
|
|
6
|
+
/** POST for `upload` (a new name, or `upsert`), PUT for `update`. */
|
|
7
|
+
method: "POST" | "PUT";
|
|
8
|
+
bucket: string;
|
|
9
|
+
/** The object's name as given: the start carries it in JSON, not a URL. */
|
|
10
|
+
path: string;
|
|
11
|
+
body: Blob | Uint8Array<ArrayBuffer>;
|
|
12
|
+
size: number;
|
|
13
|
+
contentType: string;
|
|
14
|
+
/** As the upload's header says it: `max-age=<seconds>`. */
|
|
15
|
+
cacheControl: string;
|
|
16
|
+
upsert: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* A signed upload's token (Storage 3-1e-2): every step carries it and
|
|
19
|
+
* none carries the session.
|
|
20
|
+
*/
|
|
21
|
+
uploadToken?: string | undefined;
|
|
22
|
+
onProgress?: (sentBytes: number, totalBytes: number) => void;
|
|
23
|
+
/** The caller's: stops the upload until the complete is sent. */
|
|
24
|
+
signal?: AbortSignal | undefined;
|
|
25
|
+
/** How a retry waits — `ms`, or until the signal aborts; tests pass their own. */
|
|
26
|
+
sleep?: (ms: number, signal: AbortSignal | undefined) => Promise<void>;
|
|
27
|
+
};
|
|
28
|
+
export declare function uploadInParts(a: PartsArgs): Promise<NaroResponse<UploadResult>>;
|
package/dist/storage/path.d.ts
CHANGED
|
@@ -3,15 +3,15 @@ import type { NaroError } from "../errors.js";
|
|
|
3
3
|
* The most one upload request carries (original spec §9.1): 50 MiB.
|
|
4
4
|
* data-core's MAX_SINGLE_UPLOAD_BYTES — storage/wire.test.ts holds them
|
|
5
5
|
* equal. Over it, the edge cuts the body off and the browser often cannot
|
|
6
|
-
* read the answer: the SDK
|
|
6
|
+
* read the answer: the SDK sends a larger body in parts (Storage 3-1f).
|
|
7
7
|
*/
|
|
8
8
|
export declare const MAX_SINGLE_UPLOAD_BYTES = 52428800;
|
|
9
9
|
/** The worker's words (data-core's catalog) for what the SDK refuses itself. */
|
|
10
10
|
export declare const STORAGE_MESSAGES: {
|
|
11
11
|
readonly bucket: "The bucket name is not valid.";
|
|
12
|
-
readonly tooLarge: "Uploads over 50 MiB are not supported yet.";
|
|
13
12
|
readonly request: "The storage request is not valid.";
|
|
14
13
|
readonly body: "upload takes a Blob, File, ArrayBuffer, typed array or string.";
|
|
14
|
+
readonly signature: "The signed URL is not valid.";
|
|
15
15
|
};
|
|
16
16
|
/**
|
|
17
17
|
* The words the worker's routes take as the first segment after `/object/`
|
|
@@ -29,7 +29,6 @@ export type Encoded = {
|
|
|
29
29
|
error: NaroError;
|
|
30
30
|
};
|
|
31
31
|
export declare function invalidPath(reason: string): NaroError;
|
|
32
|
-
export declare function objectTooLarge(): NaroError;
|
|
33
32
|
/**
|
|
34
33
|
* An object path as a URL carries it, each segment encoded once and `/`
|
|
35
34
|
* kept as the separator — or the worker's INVALID_PATH for the paths no
|
|
@@ -53,6 +52,19 @@ export declare function downloadQuery(download: boolean | string | undefined): E
|
|
|
53
52
|
* empty, `.`, `..`, a lone surrogate and the routes' own words.
|
|
54
53
|
*/
|
|
55
54
|
export declare function encodeBucket(bucket: string): Encoded;
|
|
55
|
+
/**
|
|
56
|
+
* `<bucket>/<path>` as the object routes carry it — the bucket by
|
|
57
|
+
* `encodeBucket`, the path by `encodePath` — or the INVALID_PATH either is
|
|
58
|
+
* refused with. Every object call builds its URL from it, and the package
|
|
59
|
+
* exports it for a tool that streams an object itself (naro-cli's
|
|
60
|
+
* download): it names the same URL, and is refused the same names.
|
|
61
|
+
*/
|
|
62
|
+
export declare function objectRoute(bucket: string, path: string): Encoded;
|
|
63
|
+
/**
|
|
64
|
+
* `instanceof ArrayBuffer` is false for one made in another realm (an
|
|
65
|
+
* iframe, a vm context); the tag is the same in every realm.
|
|
66
|
+
*/
|
|
67
|
+
export declare const isArrayBuffer: (value: unknown) => value is ArrayBuffer;
|
|
56
68
|
/** Why the worker refuses a cache-control it cannot read (its upload.ts). */
|
|
57
69
|
export declare const CACHE_CONTROL_REASON = "must be max-age=<seconds> from 0 to 31536000";
|
|
58
70
|
/**
|
package/dist/storage/types.d.ts
CHANGED
|
@@ -34,6 +34,25 @@ export type UploadOptions = {
|
|
|
34
34
|
cacheControl?: string;
|
|
35
35
|
/** Replace an object of the same name. Default false. */
|
|
36
36
|
upsert?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Called each time a part of an upload over 50 MiB is sent: the bytes
|
|
39
|
+
* sent so far and the body's size, ending with both the same. An upload
|
|
40
|
+
* of 50 MiB or less is one request and never calls it. What it throws is
|
|
41
|
+
* ignored: the upload goes on.
|
|
42
|
+
*/
|
|
43
|
+
onProgress?: (sentBytes: number, totalBytes: number) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Stops the upload: what has not been answered yet is called off, and the
|
|
46
|
+
* upload answers `ABORTED` ("The upload was stopped before it finished.").
|
|
47
|
+
* One over 50 MiB is called off on the backend too, so its name and the
|
|
48
|
+
* space it reserved are free at once — not after the hour its lock lasts;
|
|
49
|
+
* no part starts and `onProgress` is not called after the signal. Its
|
|
50
|
+
* first and last steps are never cut short: a signal during the first is
|
|
51
|
+
* acted on once it answers, and once the last is sent the signal stops
|
|
52
|
+
* nothing and that step's answer is the upload's. A signal already aborted
|
|
53
|
+
* sends nothing.
|
|
54
|
+
*/
|
|
55
|
+
signal?: AbortSignal;
|
|
37
56
|
};
|
|
38
57
|
export type UpdateOptions = Omit<UploadOptions, "upsert">;
|
|
39
58
|
export type UploadResult = {
|
|
@@ -51,8 +70,48 @@ export type ListOptions = {
|
|
|
51
70
|
column?: "name" | "created_at" | "updated_at";
|
|
52
71
|
order?: "asc" | "desc";
|
|
53
72
|
};
|
|
73
|
+
/**
|
|
74
|
+
* Names below the prefix that start with it, ASCII case ignored; % and _
|
|
75
|
+
* are plain characters. Up to 256 characters. A folder shows when a name
|
|
76
|
+
* under it starts with it. A backend whose Storage was turned on before
|
|
77
|
+
* search came answers INVALID_REQUEST (`details.path` "search"): update
|
|
78
|
+
* it with `naro backend enable`.
|
|
79
|
+
*/
|
|
80
|
+
search?: string;
|
|
54
81
|
};
|
|
55
82
|
/** `true` downloads under the object's own name, a string under that name. */
|
|
56
83
|
export type DownloadOption = {
|
|
57
84
|
download?: boolean | string;
|
|
58
85
|
};
|
|
86
|
+
/** `createSignedUploadUrl`'s options (Storage 3-1e-2). */
|
|
87
|
+
export type CreateSignedUploadUrlOptions = {
|
|
88
|
+
/** Let the token replace a file of the same name. Default false: a second upload is `OBJECT_EXISTS`. */
|
|
89
|
+
upsert?: boolean;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* A signed upload's token (Storage 3-1e-2): `token` for `uploadToSignedUrl`,
|
|
93
|
+
* and `signedUrl` — on the project's URL — where a client without naro-js
|
|
94
|
+
* PUTs the file's bytes with no key. It works for two hours from when it
|
|
95
|
+
* was made.
|
|
96
|
+
*/
|
|
97
|
+
export type SignedUploadUrl = {
|
|
98
|
+
signedUrl: string;
|
|
99
|
+
token: string;
|
|
100
|
+
path: string;
|
|
101
|
+
};
|
|
102
|
+
/** What `uploadToSignedUrl` answers: supabase-js's shape. */
|
|
103
|
+
export type SignedUploadResult = {
|
|
104
|
+
path: string;
|
|
105
|
+
fullPath: string;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* One path's answer of `createSignedUrls` (Storage 3-1g): supabase-js's
|
|
109
|
+
* shape without its `signedURL` copy. `signedUrl` is null, and `error`
|
|
110
|
+
* says `Object <path> was not found in bucket <bucket>.`, for a file that
|
|
111
|
+
* is not there or that the caller cannot read — the same words for both.
|
|
112
|
+
*/
|
|
113
|
+
export type SignedUrlEntry = {
|
|
114
|
+
path: string;
|
|
115
|
+
signedUrl: string | null;
|
|
116
|
+
error: string | null;
|
|
117
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Auth, data and files for your Naro project, from the browser or the server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"tsup": "8.5.0",
|
|
24
24
|
"typescript": "7.0.2",
|
|
25
25
|
"vitest": "3.2.7",
|
|
26
|
-
"@next-deploy/backend-worker": "0.1.0",
|
|
27
26
|
"@next-deploy/config": "0.1.0",
|
|
27
|
+
"@next-deploy/backend-worker": "0.1.0",
|
|
28
28
|
"@next-deploy/data-core": "0.1.0",
|
|
29
29
|
"@next-deploy/sdk": "0.1.0"
|
|
30
30
|
},
|