naro-js 0.6.0 → 0.7.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 +158 -5
- package/dist/auth/storage.d.ts +11 -0
- package/dist/client.d.ts +21 -2
- package/dist/errors.d.ts +6 -0
- package/dist/functions-caller.d.ts +13 -0
- package/dist/functions.d.ts +65 -0
- package/dist/functions.js +74 -0
- package/dist/http.d.ts +95 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +768 -47
- package/dist/invoke/body.d.ts +19 -0
- package/dist/invoke/client.d.ts +52 -0
- package/dist/invoke/errors.d.ts +38 -0
- package/package.json +15 -3
package/dist/index.js
CHANGED
|
@@ -127,6 +127,17 @@ function serviceKeyOffServer() {
|
|
|
127
127
|
message: SERVICE_KEY_OFF_SERVER_MESSAGE
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
function accessTokenUnavailable(cause) {
|
|
131
|
+
return {
|
|
132
|
+
code: "ACCESS_TOKEN_UNAVAILABLE",
|
|
133
|
+
message: "createClient's accessToken function failed or returned something that is not a string or null, so the request was not sent.",
|
|
134
|
+
details: { cause: describe(cause) }
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function accessTokenAuthMessage(name) {
|
|
138
|
+
return `naro.auth.${name} is not available on a client made with accessToken: its token comes from your accessToken function. Use a client made without accessToken to sign in.`;
|
|
139
|
+
}
|
|
140
|
+
var ACCESS_TOKEN_CHANNEL_MESSAGE = "naro.channel() is not available on a client made with accessToken: Realtime signs in with a session, not an access token. Broadcast from a function with naroAdmin.";
|
|
130
141
|
function notJoined(channel) {
|
|
131
142
|
return {
|
|
132
143
|
code: "NOT_JOINED",
|
|
@@ -194,9 +205,139 @@ var SESSION_PREFIXES = ["/auth/", "/realtime/"];
|
|
|
194
205
|
function storeFor(config, path) {
|
|
195
206
|
return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
|
|
196
207
|
}
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
var ACCESS_JWT_MARGIN_MS = 6e4;
|
|
209
|
+
function deadlineOf(timeoutMs) {
|
|
210
|
+
return timeoutMs === void 0 ? null : { at: Date.now() + timeoutMs, ms: timeoutMs };
|
|
211
|
+
}
|
|
212
|
+
function remainingMs(deadline) {
|
|
213
|
+
return Math.max(0, deadline.at - Date.now());
|
|
214
|
+
}
|
|
215
|
+
var EXPIRED = Symbol("expired");
|
|
216
|
+
function timedOut(deadline) {
|
|
217
|
+
return requestTimeout(deadline?.ms ?? 0);
|
|
218
|
+
}
|
|
219
|
+
function timesOf(jwt) {
|
|
220
|
+
const payload = jwt.split(".")[1];
|
|
221
|
+
if (payload === void 0 || payload === "") return null;
|
|
222
|
+
try {
|
|
223
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
224
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
225
|
+
const claims = JSON.parse(atob(padded));
|
|
226
|
+
if (!isRecord(claims)) return null;
|
|
227
|
+
const { exp, iat } = claims;
|
|
228
|
+
if (typeof exp !== "number" || !Number.isFinite(exp)) return null;
|
|
229
|
+
return {
|
|
230
|
+
exp: exp * 1e3,
|
|
231
|
+
iat: typeof iat === "number" && Number.isFinite(iat) ? iat * 1e3 : null
|
|
232
|
+
};
|
|
233
|
+
} catch {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function expiryOf(jwt) {
|
|
238
|
+
return timesOf(jwt)?.exp ?? null;
|
|
239
|
+
}
|
|
240
|
+
var skews = /* @__PURE__ */ new WeakMap();
|
|
241
|
+
function localExpiry(config, jwt) {
|
|
242
|
+
const exp = expiryOf(jwt);
|
|
243
|
+
return exp === null ? null : exp - (skews.get(config.tokens) ?? 0);
|
|
244
|
+
}
|
|
245
|
+
var SESSION_GONE_CODES = /* @__PURE__ */ new Set([
|
|
246
|
+
"UNAUTHORIZED",
|
|
247
|
+
"INVALID_SESSION",
|
|
248
|
+
"FAILED_TO_GET_SESSION"
|
|
249
|
+
]);
|
|
250
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
251
|
+
function renewAccessJwt(config, jwtStore, session) {
|
|
252
|
+
const pending = renewing.get(config.tokens);
|
|
253
|
+
if (pending?.session === session) return pending.done;
|
|
254
|
+
const done = (async () => {
|
|
255
|
+
const result = await callWith(
|
|
256
|
+
config,
|
|
257
|
+
{ method: "GET", path: "/auth/token", withToken: true },
|
|
258
|
+
void 0,
|
|
259
|
+
session
|
|
260
|
+
);
|
|
261
|
+
if (result.error !== null) {
|
|
262
|
+
if (result.answer?.status !== 401 || !SESSION_GONE_CODES.has(result.error.code))
|
|
263
|
+
return result.error;
|
|
264
|
+
await config.tokens.clearIf(session);
|
|
265
|
+
await config.tokens.whileIs(null, () => jwtStore.clear());
|
|
266
|
+
return invalidSession();
|
|
267
|
+
}
|
|
268
|
+
const { body, status } = result.answer;
|
|
269
|
+
const jwt = isRecord(body) ? body.token : void 0;
|
|
270
|
+
if (typeof jwt !== "string" || jwt === "") return invalidResponse(status);
|
|
271
|
+
const arrived = Date.now();
|
|
272
|
+
await config.tokens.whileIs(session, async () => {
|
|
273
|
+
const iat = timesOf(jwt)?.iat;
|
|
274
|
+
if (iat !== void 0 && iat !== null)
|
|
275
|
+
skews.set(config.tokens, iat - arrived);
|
|
276
|
+
await jwtStore.set(jwt);
|
|
277
|
+
});
|
|
278
|
+
return null;
|
|
279
|
+
})();
|
|
280
|
+
const entry = { session, done };
|
|
281
|
+
renewing.set(config.tokens, entry);
|
|
282
|
+
void done.finally(() => {
|
|
283
|
+
if (renewing.get(config.tokens) === entry) renewing.delete(config.tokens);
|
|
284
|
+
});
|
|
285
|
+
return done;
|
|
286
|
+
}
|
|
287
|
+
async function accessJwtFor(config, jwtStore) {
|
|
288
|
+
for (let round = 0; round < 2; round += 1) {
|
|
289
|
+
const session = await config.tokens.get();
|
|
290
|
+
const jwt = await jwtStore.get();
|
|
291
|
+
if (session === null || config.service) return { token: jwt, session };
|
|
292
|
+
const expiry = jwt === null ? null : localExpiry(config, jwt);
|
|
293
|
+
if (jwt !== null && (expiry === null || expiry - ACCESS_JWT_MARGIN_MS > Date.now()))
|
|
294
|
+
return { token: jwt, session };
|
|
295
|
+
const error = await renewAccessJwt(config, jwtStore, session);
|
|
296
|
+
const stored = await config.tokens.get();
|
|
297
|
+
if (stored !== session) {
|
|
298
|
+
if (stored === null) return error ? { error } : { token: null, session };
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (error === null) {
|
|
302
|
+
const renewed = await jwtStore.get();
|
|
303
|
+
return renewed === null ? { error: invalidSession() } : { token: renewed, session };
|
|
304
|
+
}
|
|
305
|
+
if (jwt !== null && expiry !== null && expiry > Date.now())
|
|
306
|
+
return { token: jwt, session };
|
|
307
|
+
return { error };
|
|
308
|
+
}
|
|
309
|
+
return { error: invalidSession() };
|
|
310
|
+
}
|
|
311
|
+
async function bearerFor(config, path, withToken) {
|
|
312
|
+
if (!withToken) return { token: null, session: null };
|
|
313
|
+
const store = storeFor(config, path);
|
|
314
|
+
if (config.accessToken !== void 0 && store !== config.tokens) {
|
|
315
|
+
try {
|
|
316
|
+
return { token: await config.accessToken.get(), session: null };
|
|
317
|
+
} catch (cause) {
|
|
318
|
+
return { error: accessTokenUnavailable(cause) };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (store === null) return { token: null, session: null };
|
|
322
|
+
if (store === config.tokens) {
|
|
323
|
+
const token = await store.get();
|
|
324
|
+
return { token, session: token };
|
|
325
|
+
}
|
|
326
|
+
return accessJwtFor(config, store);
|
|
327
|
+
}
|
|
328
|
+
async function retryBearer(config, path, error, first) {
|
|
329
|
+
if (error?.code !== "INVALID_SESSION" || first.token === null || first.session === null || config.accessJwt === void 0 || storeFor(config, path) !== config.accessJwt || await config.tokens.get() !== first.session)
|
|
330
|
+
return null;
|
|
331
|
+
const renewed = await bearerFor(config, path, true);
|
|
332
|
+
if ("error" in renewed) return renewed;
|
|
333
|
+
return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
|
|
334
|
+
}
|
|
335
|
+
async function receive(config, options, init, deadline) {
|
|
336
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
337
|
+
return { error: timedOut(deadline) };
|
|
338
|
+
const controller = deadline !== null && typeof AbortController === "function" ? new AbortController() : null;
|
|
199
339
|
if (controller) init.signal = controller.signal;
|
|
340
|
+
let gaveUp = false;
|
|
200
341
|
const request = (async () => {
|
|
201
342
|
try {
|
|
202
343
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
@@ -204,6 +345,10 @@ async function receive(config, options, init) {
|
|
|
204
345
|
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
205
346
|
init
|
|
206
347
|
);
|
|
348
|
+
if (gaveUp) {
|
|
349
|
+
discard(response);
|
|
350
|
+
return { error: timedOut(deadline) };
|
|
351
|
+
}
|
|
207
352
|
return {
|
|
208
353
|
status: response.status,
|
|
209
354
|
headers: response.headers,
|
|
@@ -213,14 +358,14 @@ async function receive(config, options, init) {
|
|
|
213
358
|
return { error: networkError(error) };
|
|
214
359
|
}
|
|
215
360
|
})();
|
|
216
|
-
|
|
217
|
-
if (timeoutMs === void 0) return request;
|
|
361
|
+
if (deadline === null) return request;
|
|
218
362
|
let timer;
|
|
219
363
|
const expired = new Promise((resolve) => {
|
|
220
364
|
timer = setTimeout(() => {
|
|
365
|
+
gaveUp = true;
|
|
221
366
|
controller?.abort();
|
|
222
|
-
resolve({ error:
|
|
223
|
-
},
|
|
367
|
+
resolve({ error: timedOut(deadline) });
|
|
368
|
+
}, remainingMs(deadline));
|
|
224
369
|
});
|
|
225
370
|
try {
|
|
226
371
|
return await Promise.race([request, expired]);
|
|
@@ -237,8 +382,35 @@ async function call(config, options) {
|
|
|
237
382
|
return { error: invalidQuery("", "is not JSON"), answer: null };
|
|
238
383
|
}
|
|
239
384
|
}
|
|
385
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
386
|
+
const bearer = await untilStopped(
|
|
387
|
+
bearerFor(config, options.path, options.withToken),
|
|
388
|
+
void 0,
|
|
389
|
+
deadline
|
|
390
|
+
);
|
|
391
|
+
if (bearer === EXPIRED || bearer === ABORTED)
|
|
392
|
+
return { error: timedOut(deadline), answer: null };
|
|
393
|
+
if ("error" in bearer) return { error: bearer.error, answer: null };
|
|
394
|
+
const result = await callWith(
|
|
395
|
+
config,
|
|
396
|
+
options,
|
|
397
|
+
payload,
|
|
398
|
+
bearer.token,
|
|
399
|
+
deadline
|
|
400
|
+
);
|
|
401
|
+
const again = await untilStopped(
|
|
402
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
403
|
+
void 0,
|
|
404
|
+
deadline
|
|
405
|
+
);
|
|
406
|
+
if (again === EXPIRED || again === ABORTED)
|
|
407
|
+
return { error: timedOut(deadline), answer: null };
|
|
408
|
+
if (again === null) return result;
|
|
409
|
+
if ("error" in again) return { error: again.error, answer: null };
|
|
410
|
+
return callWith(config, options, payload, again.token, deadline);
|
|
411
|
+
}
|
|
412
|
+
async function callWith(config, options, payload, token, deadline = null) {
|
|
240
413
|
const store = storeFor(config, options.path);
|
|
241
|
-
const token = options.withToken && store ? await store.get() : null;
|
|
242
414
|
const headers = {
|
|
243
415
|
...options.headers,
|
|
244
416
|
apikey: config.apiKey
|
|
@@ -257,7 +429,7 @@ async function call(config, options) {
|
|
|
257
429
|
headers["Content-Type"] = "application/json";
|
|
258
430
|
init.body = payload;
|
|
259
431
|
}
|
|
260
|
-
const received = await receive(config, options, init);
|
|
432
|
+
const received = await receive(config, options, init, deadline);
|
|
261
433
|
if ("error" in received) return { error: received.error, answer: null };
|
|
262
434
|
const { status, headers: responseHeaders, text } = received;
|
|
263
435
|
let body;
|
|
@@ -282,8 +454,69 @@ async function settled(store, error, headers, token) {
|
|
|
282
454
|
return RETRY_AFTER_CODES.has(error.code) ? withRetryAfter(error, headers) : error;
|
|
283
455
|
}
|
|
284
456
|
async function send(config, options) {
|
|
457
|
+
const bearer = await untilAborted(
|
|
458
|
+
bearerFor(config, options.path, options.withToken),
|
|
459
|
+
options.signal
|
|
460
|
+
);
|
|
461
|
+
if (bearer === ABORTED) return { error: uploadAborted(), status: null };
|
|
462
|
+
if ("error" in bearer) return { error: bearer.error, status: null };
|
|
463
|
+
const result = await sendWith(config, options, bearer.token);
|
|
464
|
+
if (result.error?.code !== "INVALID_SESSION" || isStream(options.body))
|
|
465
|
+
return result;
|
|
466
|
+
const again = await untilAborted(
|
|
467
|
+
retryBearer(config, options.path, result.error, bearer),
|
|
468
|
+
options.signal
|
|
469
|
+
);
|
|
470
|
+
if (again === ABORTED) return { error: uploadAborted(), status: null };
|
|
471
|
+
if (again === null) return result;
|
|
472
|
+
if ("error" in again) return { error: again.error, status: null };
|
|
473
|
+
return sendWith(config, options, again.token);
|
|
474
|
+
}
|
|
475
|
+
var ABORTED = Symbol("aborted");
|
|
476
|
+
function untilAborted(promise, signal) {
|
|
477
|
+
if (!signal) return promise;
|
|
478
|
+
if (signal.aborted) return Promise.resolve(ABORTED);
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
const stop = () => resolve(ABORTED);
|
|
481
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
482
|
+
promise.then(
|
|
483
|
+
(value) => {
|
|
484
|
+
signal.removeEventListener("abort", stop);
|
|
485
|
+
resolve(value);
|
|
486
|
+
},
|
|
487
|
+
(error) => {
|
|
488
|
+
signal.removeEventListener("abort", stop);
|
|
489
|
+
reject(error);
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
function untilStopped(promise, signal, deadline) {
|
|
495
|
+
if (signal?.aborted) return Promise.resolve(ABORTED);
|
|
496
|
+
if (!signal && deadline === null) return promise;
|
|
497
|
+
return new Promise((resolve, reject) => {
|
|
498
|
+
let timer;
|
|
499
|
+
const stop = () => finish(ABORTED);
|
|
500
|
+
const finish = (value) => {
|
|
501
|
+
clearTimeout(timer);
|
|
502
|
+
signal?.removeEventListener("abort", stop);
|
|
503
|
+
resolve(value);
|
|
504
|
+
};
|
|
505
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
506
|
+
if (deadline !== null)
|
|
507
|
+
timer = setTimeout(() => finish(EXPIRED), remainingMs(deadline));
|
|
508
|
+
promise.then(finish, (error) => {
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
signal?.removeEventListener("abort", stop);
|
|
511
|
+
reject(error);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
function isStream(body) {
|
|
516
|
+
return typeof ReadableStream === "function" && body instanceof ReadableStream;
|
|
517
|
+
}
|
|
518
|
+
async function sendWith(config, options, token) {
|
|
285
519
|
const store = storeFor(config, options.path);
|
|
286
|
-
const token = options.withToken && store ? await store.get() : null;
|
|
287
520
|
const headers = {
|
|
288
521
|
...options.headers,
|
|
289
522
|
apikey: config.apiKey
|
|
@@ -338,6 +571,143 @@ async function send(config, options) {
|
|
|
338
571
|
status
|
|
339
572
|
};
|
|
340
573
|
}
|
|
574
|
+
async function fetchUnread(config, options) {
|
|
575
|
+
const { signal } = options;
|
|
576
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
577
|
+
const deadline = deadlineOf(options.timeoutMs);
|
|
578
|
+
if (options.ready) {
|
|
579
|
+
const ready = await untilStopped(options.ready(), signal, deadline);
|
|
580
|
+
if (ready === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
581
|
+
if (ready === EXPIRED) return { failed: "timeout", cause: null };
|
|
582
|
+
}
|
|
583
|
+
const own = options.headers.has("authorization");
|
|
584
|
+
const bearer = await untilStopped(
|
|
585
|
+
bearerFor(config, options.path, !own),
|
|
586
|
+
signal,
|
|
587
|
+
deadline
|
|
588
|
+
);
|
|
589
|
+
if (bearer === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
590
|
+
if (bearer === EXPIRED) return { failed: "timeout", cause: null };
|
|
591
|
+
if ("error" in bearer) return { refused: bearer.error };
|
|
592
|
+
const first = await fetchOnce(config, options, bearer.token, deadline);
|
|
593
|
+
if (!("response" in first) || bearer.token === null) return first;
|
|
594
|
+
const refused = await refusesToken(first.response, signal, deadline);
|
|
595
|
+
if (refused === EXPIRED) {
|
|
596
|
+
discard(first.response);
|
|
597
|
+
return { failed: "timeout", cause: null };
|
|
598
|
+
}
|
|
599
|
+
if (refused === ABORTED) {
|
|
600
|
+
discard(first.response);
|
|
601
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
602
|
+
}
|
|
603
|
+
if (!refused) return first;
|
|
604
|
+
await storeFor(config, options.path)?.clearIf(bearer.token);
|
|
605
|
+
if (isStream(options.body)) return first;
|
|
606
|
+
const again = await untilStopped(
|
|
607
|
+
retryBearer(config, options.path, invalidSession(), bearer),
|
|
608
|
+
signal,
|
|
609
|
+
deadline
|
|
610
|
+
);
|
|
611
|
+
if (again === EXPIRED) {
|
|
612
|
+
discard(first.response);
|
|
613
|
+
return { failed: "timeout", cause: null };
|
|
614
|
+
}
|
|
615
|
+
if (again === null || again !== ABORTED && "error" in again) return first;
|
|
616
|
+
discard(first.response);
|
|
617
|
+
if (again === ABORTED) return { failed: "aborted", cause: signal?.reason };
|
|
618
|
+
const second = await fetchOnce(config, options, again.token, deadline);
|
|
619
|
+
if (!("response" in second) || again.token === null) return second;
|
|
620
|
+
const refusedAgain = await refusesToken(second.response, signal, deadline);
|
|
621
|
+
if (refusedAgain === ABORTED) {
|
|
622
|
+
discard(second.response);
|
|
623
|
+
return { failed: "aborted", cause: signal?.reason };
|
|
624
|
+
}
|
|
625
|
+
if (refusedAgain === true || refusedAgain === EXPIRED)
|
|
626
|
+
await storeFor(config, options.path)?.clearIf(again.token);
|
|
627
|
+
return second;
|
|
628
|
+
}
|
|
629
|
+
async function refusesToken(response, signal, deadline) {
|
|
630
|
+
if (response.status !== 401 || response.headers.get("x-relay-error") !== "true")
|
|
631
|
+
return false;
|
|
632
|
+
let body;
|
|
633
|
+
try {
|
|
634
|
+
body = await untilStopped(response.clone().json(), signal, deadline);
|
|
635
|
+
} catch {
|
|
636
|
+
return signal?.aborted ? ABORTED : false;
|
|
637
|
+
}
|
|
638
|
+
if (body === ABORTED || body === EXPIRED) return body;
|
|
639
|
+
return isRecord(body) && isRecord(body.error) && body.error.code === "INVALID_SESSION";
|
|
640
|
+
}
|
|
641
|
+
var releases = /* @__PURE__ */ new WeakMap();
|
|
642
|
+
function discard(response) {
|
|
643
|
+
releases.get(response)?.();
|
|
644
|
+
releases.delete(response);
|
|
645
|
+
response.body?.cancel().catch(() => {
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
async function fetchOnce(config, options, token, deadline) {
|
|
649
|
+
const { signal } = options;
|
|
650
|
+
if (signal?.aborted) return { failed: "aborted", cause: signal.reason };
|
|
651
|
+
if (deadline !== null && remainingMs(deadline) === 0)
|
|
652
|
+
return { failed: "timeout", cause: null };
|
|
653
|
+
const headers = new Headers(options.headers);
|
|
654
|
+
headers.set("apikey", config.apiKey);
|
|
655
|
+
if (token !== null) headers.set("authorization", `Bearer ${token}`);
|
|
656
|
+
const init = {
|
|
657
|
+
method: options.method,
|
|
658
|
+
headers,
|
|
659
|
+
credentials: "omit"
|
|
660
|
+
};
|
|
661
|
+
if (options.body !== void 0) init.body = options.body;
|
|
662
|
+
if (isStream(options.body)) init.duplex = "half";
|
|
663
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
664
|
+
if (controller) init.signal = controller.signal;
|
|
665
|
+
let timer;
|
|
666
|
+
let stop = () => {
|
|
667
|
+
};
|
|
668
|
+
const stopped = new Promise((resolve) => {
|
|
669
|
+
stop = resolve;
|
|
670
|
+
});
|
|
671
|
+
const onAbort = () => {
|
|
672
|
+
stop({ failed: "aborted", cause: signal?.reason });
|
|
673
|
+
controller?.abort(signal?.reason);
|
|
674
|
+
};
|
|
675
|
+
if (deadline !== null)
|
|
676
|
+
timer = setTimeout(() => {
|
|
677
|
+
stop({ failed: "timeout", cause: null });
|
|
678
|
+
controller?.abort();
|
|
679
|
+
}, remainingMs(deadline));
|
|
680
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
681
|
+
const request = (async () => {
|
|
682
|
+
try {
|
|
683
|
+
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
684
|
+
return {
|
|
685
|
+
response: await fetchImpl(
|
|
686
|
+
`${config.baseUrl}/_naro/v1${options.path}`,
|
|
687
|
+
init
|
|
688
|
+
)
|
|
689
|
+
};
|
|
690
|
+
} catch (cause) {
|
|
691
|
+
return { failed: "network", cause };
|
|
692
|
+
}
|
|
693
|
+
})();
|
|
694
|
+
const release = () => signal?.removeEventListener("abort", onAbort);
|
|
695
|
+
let outcome;
|
|
696
|
+
try {
|
|
697
|
+
outcome = await Promise.race([request, stopped]);
|
|
698
|
+
return outcome;
|
|
699
|
+
} finally {
|
|
700
|
+
clearTimeout(timer);
|
|
701
|
+
if (outcome !== void 0 && "response" in outcome)
|
|
702
|
+
releases.set(outcome.response, release);
|
|
703
|
+
else {
|
|
704
|
+
release();
|
|
705
|
+
void request.then((late) => {
|
|
706
|
+
if ("response" in late) discard(late.response);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
341
711
|
var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
342
712
|
"RATE_LIMITED",
|
|
343
713
|
"WRITE_QUOTA_EXCEEDED",
|
|
@@ -346,7 +716,12 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
|
346
716
|
"OBJECT_RATE_LIMITED",
|
|
347
717
|
"STORAGE_OPS_QUOTA_EXCEEDED",
|
|
348
718
|
// Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
|
|
349
|
-
"TOO_MANY_MESSAGES"
|
|
719
|
+
"TOO_MANY_MESSAGES",
|
|
720
|
+
// Naro Functions (design F20): a team over its monthly function CPU, and
|
|
721
|
+
// the control plane's Cloudflare limit. `invoke` reads x-retry-after on
|
|
722
|
+
// every relay error anyway (invoke/client.ts).
|
|
723
|
+
"FUNCTIONS_QUOTA_EXCEEDED",
|
|
724
|
+
"FUNCTIONS_RATE_LIMITED"
|
|
350
725
|
]);
|
|
351
726
|
function withRetryAfter(error, headers) {
|
|
352
727
|
const header = headers.get("x-retry-after")?.trim() ?? "";
|
|
@@ -670,14 +1045,14 @@ var AuthClient = class {
|
|
|
670
1045
|
*/
|
|
671
1046
|
async signInWithOAuth(options) {
|
|
672
1047
|
const { provider } = options;
|
|
673
|
-
const
|
|
1048
|
+
const failed2 = (error) => ({
|
|
674
1049
|
data: { provider, url: null },
|
|
675
1050
|
error
|
|
676
1051
|
});
|
|
677
|
-
if (!OAUTH_PROVIDERS.includes(provider)) return
|
|
1052
|
+
if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
|
|
678
1053
|
await this.#flow.recovery.forget();
|
|
679
1054
|
const pair = await createPkcePair();
|
|
680
|
-
if (!pair) return
|
|
1055
|
+
if (!pair) return failed2(cryptoUnavailable());
|
|
681
1056
|
await this.#flow.verifiers.add(pair.challenge, pair.verifier);
|
|
682
1057
|
const redirectTo = options.redirectTo ?? currentPage();
|
|
683
1058
|
const url = new URL(
|
|
@@ -1153,6 +1528,23 @@ var TokenStore = class {
|
|
|
1153
1528
|
this.#changed();
|
|
1154
1529
|
});
|
|
1155
1530
|
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Runs `operation` only while `expected` is the stored token (null: none
|
|
1533
|
+
* is stored), inside this
|
|
1534
|
+
* store's queue, and says whether it ran: no write to this store lands
|
|
1535
|
+
* between the check and the end of the operation. A refreshed access JWT
|
|
1536
|
+
* is kept this way (`http.ts`), so one minted for a session that a
|
|
1537
|
+
* sign-in replaced meanwhile never lands on top of the new session's.
|
|
1538
|
+
* `operation` must not call this store's public methods (it would wait
|
|
1539
|
+
* on itself); another store's are fine.
|
|
1540
|
+
*/
|
|
1541
|
+
whileIs(expected, operation) {
|
|
1542
|
+
return this.#serial(async () => {
|
|
1543
|
+
if (await this.#get() !== expected) return false;
|
|
1544
|
+
await operation();
|
|
1545
|
+
return true;
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1156
1548
|
/**
|
|
1157
1549
|
* Calls `listener` after every write of the token — a sign-in, a
|
|
1158
1550
|
* sign-out, a renewed session, a cleared INVALID_SESSION — from inside the
|
|
@@ -1233,6 +1625,288 @@ var TokenStore = class {
|
|
|
1233
1625
|
}
|
|
1234
1626
|
};
|
|
1235
1627
|
|
|
1628
|
+
// src/invoke/body.ts
|
|
1629
|
+
function prepareBody(body) {
|
|
1630
|
+
if (body === void 0) return { body: void 0, contentType: null };
|
|
1631
|
+
if (typeof body === "string") return { body, contentType: "text/plain" };
|
|
1632
|
+
if (typeof Blob === "function" && body instanceof Blob)
|
|
1633
|
+
return { body, contentType: "application/octet-stream" };
|
|
1634
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
|
|
1635
|
+
return { body, contentType: "application/octet-stream" };
|
|
1636
|
+
if (typeof FormData === "function" && body instanceof FormData)
|
|
1637
|
+
return { body, contentType: null };
|
|
1638
|
+
if (typeof URLSearchParams === "function" && body instanceof URLSearchParams)
|
|
1639
|
+
return { body, contentType: null };
|
|
1640
|
+
if (typeof ReadableStream === "function" && body instanceof ReadableStream)
|
|
1641
|
+
return { body, contentType: null };
|
|
1642
|
+
try {
|
|
1643
|
+
const text = JSON.stringify(body);
|
|
1644
|
+
return text === void 0 ? null : { body: text, contentType: "application/json" };
|
|
1645
|
+
} catch {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
async function readAnswer(response, method) {
|
|
1650
|
+
if (response.status === 204 || method === "HEAD") return { data: null };
|
|
1651
|
+
const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
1652
|
+
try {
|
|
1653
|
+
if (type === "application/json") {
|
|
1654
|
+
const text = await response.text();
|
|
1655
|
+
return {
|
|
1656
|
+
data: text.trim() === "" ? null : JSON.parse(text)
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (type === "application/octet-stream" || type === "application/pdf")
|
|
1660
|
+
return { data: await response.blob() };
|
|
1661
|
+
if (type === "text/event-stream") return { data: response };
|
|
1662
|
+
if (type === "multipart/form-data")
|
|
1663
|
+
return { data: await response.formData() };
|
|
1664
|
+
return { data: await response.text() };
|
|
1665
|
+
} catch (cause) {
|
|
1666
|
+
return { unreadable: cause };
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/invoke/errors.ts
|
|
1671
|
+
var FunctionsError = class extends Error {
|
|
1672
|
+
code;
|
|
1673
|
+
context;
|
|
1674
|
+
constructor(code, message, context, details) {
|
|
1675
|
+
super(message);
|
|
1676
|
+
this.name = "FunctionsError";
|
|
1677
|
+
this.code = code;
|
|
1678
|
+
this.context = context;
|
|
1679
|
+
if (details !== void 0)
|
|
1680
|
+
Object.defineProperty(this, "details", {
|
|
1681
|
+
value: details,
|
|
1682
|
+
enumerable: true
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
};
|
|
1686
|
+
var FunctionsHttpError = class extends FunctionsError {
|
|
1687
|
+
constructor(response) {
|
|
1688
|
+
super(
|
|
1689
|
+
"FUNCTION_HTTP_ERROR",
|
|
1690
|
+
`The function answered with HTTP ${String(response.status)}.`,
|
|
1691
|
+
response
|
|
1692
|
+
);
|
|
1693
|
+
this.name = "FunctionsHttpError";
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
var FunctionsRelayError = class extends FunctionsError {
|
|
1697
|
+
constructor(code, message, response, details) {
|
|
1698
|
+
super(code, message, response, details);
|
|
1699
|
+
this.name = "FunctionsRelayError";
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
var FunctionsFetchError = class extends FunctionsError {
|
|
1703
|
+
constructor(code, message, cause, details) {
|
|
1704
|
+
super(code, message, cause, details);
|
|
1705
|
+
this.name = "FunctionsFetchError";
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
var FUNCTIONS_MESSAGES = {
|
|
1709
|
+
timeout: (ms) => `The function did not answer within ${String(ms)} ms.`,
|
|
1710
|
+
aborted: "The function call was stopped before the function answered.",
|
|
1711
|
+
invalidBody: "functions.invoke could not send this body: JSON cannot hold it (a BigInt or a cycle). Pass a string, a Blob, bytes, FormData, URLSearchParams or a stream instead.",
|
|
1712
|
+
invalidTimeout: "functions.invoke's timeout is whole milliseconds from 1 to 2147483647.",
|
|
1713
|
+
invalidHeaders: "functions.invoke's headers are not valid HTTP header names and values.",
|
|
1714
|
+
invalidName: "functions.invoke's name is a string: the function's name, then any path and query.",
|
|
1715
|
+
unencodableName: "functions.invoke's name is not well-formed Unicode: it has a lone surrogate.",
|
|
1716
|
+
dotSegments: `functions.invoke's name cannot be "." or "..", and ".." in its path cannot leave /functions/.`,
|
|
1717
|
+
invalidSignal: "functions.invoke's signal is not an AbortSignal.",
|
|
1718
|
+
bodyWithGetOrHead: "functions.invoke cannot send a body with GET or HEAD.",
|
|
1719
|
+
/** `RELAY_ERROR`: a HEAD answer has no body, so Naro's code is not in it. */
|
|
1720
|
+
headRelay: (status) => `Naro answered HTTP ${String(status)} for the function, but a HEAD answer has no body to say why. Call it with GET to read the reason.`,
|
|
1721
|
+
unreadable: (status) => `The function answered HTTP ${String(status)}, but its body could not be read as its content-type says.`
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/invoke/client.ts
|
|
1725
|
+
var MAX_TIMER_MS = 2147483647;
|
|
1726
|
+
function functionPath(name) {
|
|
1727
|
+
const cut = name.search(/[/?#]/);
|
|
1728
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
1729
|
+
const tail = cut === -1 ? "" : name.slice(cut);
|
|
1730
|
+
return `/functions/${encodeURIComponent(head)}${tail}`;
|
|
1731
|
+
}
|
|
1732
|
+
function dotSegment(segment) {
|
|
1733
|
+
const dots = segment.toLowerCase().replace(/%2e/g, ".");
|
|
1734
|
+
return dots === "." ? 1 : dots === ".." ? 2 : 0;
|
|
1735
|
+
}
|
|
1736
|
+
function leavesFunctions(input) {
|
|
1737
|
+
const name = input.replace(/[\t\n\r]/g, "");
|
|
1738
|
+
const cut = name.search(/[/?#]/);
|
|
1739
|
+
const head = cut === -1 ? name : name.slice(0, cut);
|
|
1740
|
+
if (head === "." || head === "..") return true;
|
|
1741
|
+
if (cut === -1 || name[cut] !== "/") return false;
|
|
1742
|
+
const rest = name.slice(cut + 1);
|
|
1743
|
+
const end = rest.search(/[?#]/);
|
|
1744
|
+
let depth = 1;
|
|
1745
|
+
for (const segment of (end === -1 ? rest : rest.slice(0, end)).split(
|
|
1746
|
+
/[/\\]/
|
|
1747
|
+
)) {
|
|
1748
|
+
const dots = dotSegment(segment);
|
|
1749
|
+
if (dots === 0) depth += 1;
|
|
1750
|
+
if (dots === 2) depth -= 1;
|
|
1751
|
+
if (depth < 0) return true;
|
|
1752
|
+
}
|
|
1753
|
+
return false;
|
|
1754
|
+
}
|
|
1755
|
+
function pathOf(name) {
|
|
1756
|
+
if (typeof name !== "string")
|
|
1757
|
+
return { invalid: FUNCTIONS_MESSAGES.invalidName };
|
|
1758
|
+
if (leavesFunctions(name)) return { invalid: FUNCTIONS_MESSAGES.dotSegments };
|
|
1759
|
+
try {
|
|
1760
|
+
return { path: functionPath(name) };
|
|
1761
|
+
} catch {
|
|
1762
|
+
return { invalid: FUNCTIONS_MESSAGES.unencodableName };
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function isSignalOrNone(value) {
|
|
1766
|
+
if (value === void 0 || value === null) return true;
|
|
1767
|
+
if (typeof value !== "object") return false;
|
|
1768
|
+
const signal = value;
|
|
1769
|
+
return typeof signal.aborted === "boolean" && typeof signal.addEventListener === "function" && typeof signal.removeEventListener === "function";
|
|
1770
|
+
}
|
|
1771
|
+
function invalidOptions(message) {
|
|
1772
|
+
return failed(new FunctionsFetchError("INVALID_OPTIONS", message, null));
|
|
1773
|
+
}
|
|
1774
|
+
function failed(error) {
|
|
1775
|
+
return { data: null, error };
|
|
1776
|
+
}
|
|
1777
|
+
function fromNaroError(error, cause) {
|
|
1778
|
+
return new FunctionsFetchError(
|
|
1779
|
+
error.code,
|
|
1780
|
+
error.message,
|
|
1781
|
+
cause,
|
|
1782
|
+
error.details
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
function failure(kind, cause, timeout) {
|
|
1786
|
+
if (kind === "timeout")
|
|
1787
|
+
return new FunctionsFetchError(
|
|
1788
|
+
"TIMEOUT",
|
|
1789
|
+
FUNCTIONS_MESSAGES.timeout(timeout ?? 0),
|
|
1790
|
+
cause
|
|
1791
|
+
);
|
|
1792
|
+
if (kind === "aborted")
|
|
1793
|
+
return new FunctionsFetchError(
|
|
1794
|
+
"ABORTED",
|
|
1795
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
1796
|
+
cause
|
|
1797
|
+
);
|
|
1798
|
+
return fromNaroError(networkError(cause), cause);
|
|
1799
|
+
}
|
|
1800
|
+
function unreadable(cause, status, signal) {
|
|
1801
|
+
if (signal?.aborted)
|
|
1802
|
+
return new FunctionsFetchError(
|
|
1803
|
+
"ABORTED",
|
|
1804
|
+
FUNCTIONS_MESSAGES.aborted,
|
|
1805
|
+
cause
|
|
1806
|
+
);
|
|
1807
|
+
if (cause instanceof SyntaxError)
|
|
1808
|
+
return new FunctionsFetchError(
|
|
1809
|
+
"INVALID_RESPONSE",
|
|
1810
|
+
FUNCTIONS_MESSAGES.unreadable(status),
|
|
1811
|
+
cause
|
|
1812
|
+
);
|
|
1813
|
+
return fromNaroError(networkError(cause), cause);
|
|
1814
|
+
}
|
|
1815
|
+
async function relayError(response, head) {
|
|
1816
|
+
let body;
|
|
1817
|
+
try {
|
|
1818
|
+
body = await response.clone().json();
|
|
1819
|
+
} catch {
|
|
1820
|
+
body = void 0;
|
|
1821
|
+
}
|
|
1822
|
+
const { status } = response;
|
|
1823
|
+
const error = withRetryAfter(
|
|
1824
|
+
head && body === void 0 ? {
|
|
1825
|
+
code: "RELAY_ERROR",
|
|
1826
|
+
message: FUNCTIONS_MESSAGES.headRelay(status),
|
|
1827
|
+
details: { status }
|
|
1828
|
+
} : errorFromBody(status, body) ?? invalidResponse(status),
|
|
1829
|
+
response.headers
|
|
1830
|
+
);
|
|
1831
|
+
return new FunctionsRelayError(
|
|
1832
|
+
error.code,
|
|
1833
|
+
error.message,
|
|
1834
|
+
response,
|
|
1835
|
+
error.details
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
var FunctionsClient = class {
|
|
1839
|
+
#http;
|
|
1840
|
+
#auth;
|
|
1841
|
+
constructor(http, auth) {
|
|
1842
|
+
this.#http = http;
|
|
1843
|
+
this.#auth = auth;
|
|
1844
|
+
}
|
|
1845
|
+
async invoke(name, options = {}) {
|
|
1846
|
+
const method = options.method ?? "POST";
|
|
1847
|
+
const { timeout, signal } = options;
|
|
1848
|
+
const target = pathOf(name);
|
|
1849
|
+
if ("invalid" in target) return invalidOptions(target.invalid);
|
|
1850
|
+
if (timeout !== void 0 && !(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_MS))
|
|
1851
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidTimeout);
|
|
1852
|
+
if (!isSignalOrNone(signal))
|
|
1853
|
+
return invalidOptions(FUNCTIONS_MESSAGES.invalidSignal);
|
|
1854
|
+
const verb = typeof method === "string" ? method.toUpperCase() : "";
|
|
1855
|
+
if (options.body !== void 0 && (verb === "GET" || verb === "HEAD"))
|
|
1856
|
+
return invalidOptions(FUNCTIONS_MESSAGES.bodyWithGetOrHead);
|
|
1857
|
+
let headers;
|
|
1858
|
+
try {
|
|
1859
|
+
headers = new Headers(options.headers);
|
|
1860
|
+
} catch (cause) {
|
|
1861
|
+
return failed(
|
|
1862
|
+
new FunctionsFetchError(
|
|
1863
|
+
"INVALID_OPTIONS",
|
|
1864
|
+
FUNCTIONS_MESSAGES.invalidHeaders,
|
|
1865
|
+
cause
|
|
1866
|
+
)
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
const prepared = prepareBody(options.body);
|
|
1870
|
+
if (prepared === null)
|
|
1871
|
+
return failed(
|
|
1872
|
+
new FunctionsFetchError(
|
|
1873
|
+
"INVALID_BODY",
|
|
1874
|
+
FUNCTIONS_MESSAGES.invalidBody,
|
|
1875
|
+
null
|
|
1876
|
+
)
|
|
1877
|
+
);
|
|
1878
|
+
if (prepared.contentType !== null && !headers.has("content-type"))
|
|
1879
|
+
headers.set("content-type", prepared.contentType);
|
|
1880
|
+
const result = await fetchUnread(this.#http, {
|
|
1881
|
+
method,
|
|
1882
|
+
path: target.path,
|
|
1883
|
+
headers,
|
|
1884
|
+
body: prepared.body,
|
|
1885
|
+
timeoutMs: timeout,
|
|
1886
|
+
signal,
|
|
1887
|
+
// A sign-in the page URL carried goes first, as for a query (P2-6) —
|
|
1888
|
+
// within the call's timeout and signal; the exchange is not cut.
|
|
1889
|
+
ready: () => this.#auth.ready()
|
|
1890
|
+
});
|
|
1891
|
+
if ("refused" in result) return failed(fromNaroError(result.refused, null));
|
|
1892
|
+
if ("failed" in result)
|
|
1893
|
+
return failed(failure(result.failed, result.cause, timeout));
|
|
1894
|
+
const { response } = result;
|
|
1895
|
+
if (!response.ok) {
|
|
1896
|
+
const error = response.headers.get("x-relay-error") === "true" ? await relayError(response, verb === "HEAD") : new FunctionsHttpError(response);
|
|
1897
|
+
return { data: null, error, response };
|
|
1898
|
+
}
|
|
1899
|
+
const answer = await readAnswer(response, method);
|
|
1900
|
+
if ("unreadable" in answer)
|
|
1901
|
+
return {
|
|
1902
|
+
data: null,
|
|
1903
|
+
error: unreadable(answer.unreadable, response.status, signal),
|
|
1904
|
+
response
|
|
1905
|
+
};
|
|
1906
|
+
return { data: answer.data, error: null, response };
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1236
1910
|
// src/query/columns.ts
|
|
1237
1911
|
function isSpace(char) {
|
|
1238
1912
|
return char.trim() === "";
|
|
@@ -2742,19 +3416,19 @@ var RealtimeSocket = class {
|
|
|
2742
3416
|
#closed(ws, code, reason, waitedMs) {
|
|
2743
3417
|
if (this.#ws !== ws) return;
|
|
2744
3418
|
const wasOpen = this.#state === "open";
|
|
2745
|
-
const
|
|
3419
|
+
const timedOut2 = waitedMs !== null;
|
|
2746
3420
|
detach(ws);
|
|
2747
3421
|
this.#ws = null;
|
|
2748
3422
|
this.#clearTimers();
|
|
2749
|
-
if (
|
|
3423
|
+
if (timedOut2) closeQuietly(ws, 1e3);
|
|
2750
3424
|
if (!wasOpen) this.#unopened += 1;
|
|
2751
3425
|
const said = this.#said;
|
|
2752
3426
|
this.#said = null;
|
|
2753
|
-
if (!
|
|
2754
|
-
const action =
|
|
3427
|
+
if (!timedOut2 && code === 4008) this.#flooded = true;
|
|
3428
|
+
const action = timedOut2 ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
|
|
2755
3429
|
if (action.kind === "stop")
|
|
2756
3430
|
return this.#stop(said ?? realtimeStopped(code, reason));
|
|
2757
|
-
const error =
|
|
3431
|
+
const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
|
|
2758
3432
|
this.#failReplies(error);
|
|
2759
3433
|
this.#state = "waiting";
|
|
2760
3434
|
const delay = delayAfter(
|
|
@@ -2767,7 +3441,7 @@ var RealtimeSocket = class {
|
|
|
2767
3441
|
safely(
|
|
2768
3442
|
() => this.#config.events.dropped(
|
|
2769
3443
|
error,
|
|
2770
|
-
|
|
3444
|
+
timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
|
|
2771
3445
|
)
|
|
2772
3446
|
);
|
|
2773
3447
|
}
|
|
@@ -4164,13 +4838,13 @@ async function uploadInParts(a) {
|
|
|
4164
4838
|
}).catch(() => void 0);
|
|
4165
4839
|
};
|
|
4166
4840
|
const etags = [];
|
|
4167
|
-
const
|
|
4841
|
+
const failure2 = {
|
|
4168
4842
|
error: signal?.aborted ? uploadAborted() : null
|
|
4169
4843
|
};
|
|
4170
4844
|
let next = 1;
|
|
4171
4845
|
let sentBytes = 0;
|
|
4172
4846
|
const onAbort = () => {
|
|
4173
|
-
|
|
4847
|
+
failure2.error ??= uploadAborted();
|
|
4174
4848
|
};
|
|
4175
4849
|
const tryPart = async (n, bytes) => {
|
|
4176
4850
|
const sent2 = await send(a.http, {
|
|
@@ -4199,18 +4873,18 @@ async function uploadInParts(a) {
|
|
|
4199
4873
|
progress(sentBytes);
|
|
4200
4874
|
return null;
|
|
4201
4875
|
}
|
|
4202
|
-
if (attempt2 === ATTEMPTS ||
|
|
4876
|
+
if (attempt2 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
|
|
4203
4877
|
return tried.error;
|
|
4204
4878
|
await sleep(waitAfter(tried.error, attempt2), signal);
|
|
4205
|
-
if (
|
|
4879
|
+
if (failure2.error !== null) return tried.error;
|
|
4206
4880
|
}
|
|
4207
4881
|
};
|
|
4208
4882
|
const worker = async () => {
|
|
4209
|
-
while (
|
|
4883
|
+
while (failure2.error === null && next <= partCount) {
|
|
4210
4884
|
const n = next;
|
|
4211
4885
|
next += 1;
|
|
4212
4886
|
const error = await sendPart(n);
|
|
4213
|
-
if (error !== null)
|
|
4887
|
+
if (error !== null) failure2.error ??= error;
|
|
4214
4888
|
}
|
|
4215
4889
|
};
|
|
4216
4890
|
signal?.addEventListener("abort", onAbort);
|
|
@@ -4218,9 +4892,9 @@ async function uploadInParts(a) {
|
|
|
4218
4892
|
Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
|
|
4219
4893
|
);
|
|
4220
4894
|
signal?.removeEventListener("abort", onAbort);
|
|
4221
|
-
if (
|
|
4895
|
+
if (failure2.error !== null) {
|
|
4222
4896
|
await callOff();
|
|
4223
|
-
return fail(
|
|
4897
|
+
return fail(failure2.error);
|
|
4224
4898
|
}
|
|
4225
4899
|
const completed = await call(a.http, {
|
|
4226
4900
|
method: "POST",
|
|
@@ -4451,8 +5125,8 @@ var StorageFileApi = class {
|
|
|
4451
5125
|
const payload = payloadOf(body, options.contentType);
|
|
4452
5126
|
if (!payload.ok) return fail(payload.error);
|
|
4453
5127
|
const cache = `max-age=${options.cacheControl ?? "3600"}`;
|
|
4454
|
-
const
|
|
4455
|
-
if (
|
|
5128
|
+
const unreadable2 = headerProblem(options.contentType, cache);
|
|
5129
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
4456
5130
|
const { signal } = options;
|
|
4457
5131
|
if (signal?.aborted) return fail(uploadAborted());
|
|
4458
5132
|
await this.#auth.ready();
|
|
@@ -4753,8 +5427,8 @@ var StorageFileApi = class {
|
|
|
4753
5427
|
const payload = payloadOf(body, given.contentType);
|
|
4754
5428
|
if (!payload.ok) return fail(payload.error);
|
|
4755
5429
|
const cache = `max-age=${given.cacheControl ?? "3600"}`;
|
|
4756
|
-
const
|
|
4757
|
-
if (
|
|
5430
|
+
const unreadable2 = headerProblem(given.contentType, cache);
|
|
5431
|
+
if (unreadable2 !== null) return fail(unreadable2);
|
|
4758
5432
|
const { signal } = given;
|
|
4759
5433
|
if (signal?.aborted) return fail(uploadAborted());
|
|
4760
5434
|
const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES ? await uploadInParts({
|
|
@@ -4835,12 +5509,14 @@ var CONFIG_MESSAGES = {
|
|
|
4835
5509
|
bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
|
|
4836
5510
|
serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
|
|
4837
5511
|
fetch: "This runtime has no fetch. Pass one to createClient({ fetch }).",
|
|
4838
|
-
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647."
|
|
5512
|
+
realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.",
|
|
5513
|
+
accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.",
|
|
5514
|
+
accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none."
|
|
4839
5515
|
};
|
|
4840
5516
|
var SERVICE_KEY_PREFIX = "naro_sk_";
|
|
4841
|
-
var
|
|
5517
|
+
var MAX_TIMER_MS2 = 2147483647;
|
|
4842
5518
|
function isMilliseconds(value) {
|
|
4843
|
-
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <=
|
|
5519
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS2;
|
|
4844
5520
|
}
|
|
4845
5521
|
function realtimeOptionsOf(value) {
|
|
4846
5522
|
if (value === void 0) return {};
|
|
@@ -4890,6 +5566,35 @@ function apiKeyOf(options) {
|
|
|
4890
5566
|
throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
|
|
4891
5567
|
return { key, service };
|
|
4892
5568
|
}
|
|
5569
|
+
function accessTokenOf(value) {
|
|
5570
|
+
if (value === void 0) return void 0;
|
|
5571
|
+
if (typeof value !== "function") throw new Error(CONFIG_MESSAGES.accessToken);
|
|
5572
|
+
const read = value;
|
|
5573
|
+
return {
|
|
5574
|
+
async get() {
|
|
5575
|
+
const token = await read();
|
|
5576
|
+
if (token === null || token === void 0 || token === "") return null;
|
|
5577
|
+
if (typeof token !== "string")
|
|
5578
|
+
throw new TypeError(`accessToken returned a ${typeof token}`);
|
|
5579
|
+
if (!/^[\x21-\x7E]+$/.test(token))
|
|
5580
|
+
throw new TypeError(
|
|
5581
|
+
"accessToken returned a string that is not a token"
|
|
5582
|
+
);
|
|
5583
|
+
return token;
|
|
5584
|
+
}
|
|
5585
|
+
};
|
|
5586
|
+
}
|
|
5587
|
+
function refusingAuth() {
|
|
5588
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
5589
|
+
get(_target, property) {
|
|
5590
|
+
if (typeof property === "symbol" || property === "then" || property === "toJSON" || property === "constructor" || property === "asymmetricMatch")
|
|
5591
|
+
return void 0;
|
|
5592
|
+
if (property === "toString") return Object.prototype.toString;
|
|
5593
|
+
if (property === "valueOf") return Object.prototype.valueOf;
|
|
5594
|
+
throw new Error(accessTokenAuthMessage(property));
|
|
5595
|
+
}
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
4893
5598
|
function queryRunner(http, auth) {
|
|
4894
5599
|
return async (query) => {
|
|
4895
5600
|
await auth.ready();
|
|
@@ -4918,7 +5623,14 @@ var NaroClient = class {
|
|
|
4918
5623
|
* them, as supabase-js does.
|
|
4919
5624
|
*/
|
|
4920
5625
|
storage;
|
|
5626
|
+
/**
|
|
5627
|
+
* The project's Naro Functions (design §9): `functions.invoke(name, {
|
|
5628
|
+
* body })`, as in supabase-js.
|
|
5629
|
+
*/
|
|
5630
|
+
functions;
|
|
4921
5631
|
#run;
|
|
5632
|
+
/** Made with `accessToken`: `auth` and `channel()` refuse. */
|
|
5633
|
+
#accessToken;
|
|
4922
5634
|
#newRealtime;
|
|
4923
5635
|
/**
|
|
4924
5636
|
* Made by the first channel(), not by the constructor: it listens to every
|
|
@@ -4929,36 +5641,38 @@ var NaroClient = class {
|
|
|
4929
5641
|
constructor(options) {
|
|
4930
5642
|
const baseUrl = baseUrlOf(options.url);
|
|
4931
5643
|
const { key: apiKey, service } = apiKeyOf(options);
|
|
5644
|
+
const accessToken = accessTokenOf(options.accessToken);
|
|
5645
|
+
if (accessToken !== void 0 && service)
|
|
5646
|
+
throw new Error(CONFIG_MESSAGES.accessTokenWithServiceKey);
|
|
4932
5647
|
if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
|
|
4933
5648
|
throw new Error(CONFIG_MESSAGES.fetch);
|
|
4934
5649
|
const realtime = realtimeOptionsOf(options.realtime);
|
|
4935
|
-
const storage = options.auth?.storage ?? defaultStorage();
|
|
5650
|
+
const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
|
|
4936
5651
|
const http = {
|
|
4937
5652
|
baseUrl,
|
|
4938
5653
|
apiKey,
|
|
4939
5654
|
fetch: options.fetch,
|
|
4940
5655
|
tokens: new TokenStore(storage, storageKey(baseUrl)),
|
|
4941
|
-
|
|
5656
|
+
service,
|
|
5657
|
+
...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
|
|
4942
5658
|
};
|
|
4943
|
-
|
|
5659
|
+
const auth = new AuthClient(http, {
|
|
4944
5660
|
verifiers: new FlowVerifiers(
|
|
4945
5661
|
new TokenStore(storage, verifierKey(baseUrl))
|
|
4946
5662
|
),
|
|
4947
5663
|
recovery: new RecoveryToken(
|
|
4948
5664
|
new TokenStore(storage, recoveryKey(baseUrl))
|
|
4949
5665
|
),
|
|
4950
|
-
detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
|
|
5666
|
+
detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
|
|
4951
5667
|
shared: { storage, key: storageKey(baseUrl) },
|
|
4952
5668
|
serviceKey: service
|
|
4953
5669
|
});
|
|
4954
|
-
this.#
|
|
4955
|
-
this.
|
|
4956
|
-
this.#
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
options: realtime
|
|
4961
|
-
});
|
|
5670
|
+
this.#accessToken = accessToken !== void 0;
|
|
5671
|
+
this.auth = this.#accessToken ? refusingAuth() : auth;
|
|
5672
|
+
this.#run = queryRunner(http, auth);
|
|
5673
|
+
this.storage = new StorageClient(http, auth);
|
|
5674
|
+
this.functions = new FunctionsClient(http, auth);
|
|
5675
|
+
this.#newRealtime = () => new RealtimeClient({ http, auth, service, options: realtime });
|
|
4962
5676
|
}
|
|
4963
5677
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
|
4964
5678
|
from(table) {
|
|
@@ -4970,6 +5684,7 @@ var NaroClient = class {
|
|
|
4970
5684
|
* `removeChannel`. Nothing connects until the first `subscribe()`.
|
|
4971
5685
|
*/
|
|
4972
5686
|
channel(name, options) {
|
|
5687
|
+
if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
|
|
4973
5688
|
return this.#client().channel(name, options);
|
|
4974
5689
|
}
|
|
4975
5690
|
/** The channels made with `channel()` and not yet removed. */
|
|
@@ -4981,7 +5696,9 @@ var NaroClient = class {
|
|
|
4981
5696
|
* one; the connection closes with the last channel.
|
|
4982
5697
|
*/
|
|
4983
5698
|
removeChannel(channel) {
|
|
4984
|
-
|
|
5699
|
+
if (this.#realtime === void 0)
|
|
5700
|
+
return Promise.resolve({ data: null, error: null });
|
|
5701
|
+
return this.#realtime.removeChannel(channel);
|
|
4985
5702
|
}
|
|
4986
5703
|
/** `removeChannel()` for every channel; the connection closes. */
|
|
4987
5704
|
removeAllChannels() {
|
|
@@ -4998,6 +5715,10 @@ function createClient(options) {
|
|
|
4998
5715
|
return new NaroClient(options);
|
|
4999
5716
|
}
|
|
5000
5717
|
export {
|
|
5718
|
+
FunctionsError,
|
|
5719
|
+
FunctionsFetchError,
|
|
5720
|
+
FunctionsHttpError,
|
|
5721
|
+
FunctionsRelayError,
|
|
5001
5722
|
NaroClient,
|
|
5002
5723
|
createClient,
|
|
5003
5724
|
objectRoute,
|