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/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
- async function receive(config, options, init) {
198
- const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
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
- const { timeoutMs } = options;
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: requestTimeout(timeoutMs) });
223
- }, timeoutMs);
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 failed = (error) => ({
1048
+ const failed2 = (error) => ({
674
1049
  data: { provider, url: null },
675
1050
  error
676
1051
  });
677
- if (!OAUTH_PROVIDERS.includes(provider)) return failed(invalidProvider());
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 failed(cryptoUnavailable());
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 timedOut = waitedMs !== null;
3419
+ const timedOut2 = waitedMs !== null;
2746
3420
  detach(ws);
2747
3421
  this.#ws = null;
2748
3422
  this.#clearTimers();
2749
- if (timedOut) closeQuietly(ws, 1e3);
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 (!timedOut && code === 4008) this.#flooded = true;
2754
- const action = timedOut ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
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 = timedOut ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
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
- timedOut ? "TIMED_OUT" : "CHANNEL_ERROR"
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 failure = {
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
- failure.error ??= uploadAborted();
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 || failure.error !== null || !retryable(tried.error, tried.status))
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 (failure.error !== null) return tried.error;
4879
+ if (failure2.error !== null) return tried.error;
4206
4880
  }
4207
4881
  };
4208
4882
  const worker = async () => {
4209
- while (failure.error === null && next <= partCount) {
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) failure.error ??= error;
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 (failure.error !== null) {
4895
+ if (failure2.error !== null) {
4222
4896
  await callOff();
4223
- return fail(failure.error);
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 unreadable = headerProblem(options.contentType, cache);
4455
- if (unreadable !== null) return fail(unreadable);
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 unreadable = headerProblem(given.contentType, cache);
4757
- if (unreadable !== null) return fail(unreadable);
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 MAX_TIMER_MS = 2147483647;
5517
+ var MAX_TIMER_MS2 = 2147483647;
4842
5518
  function isMilliseconds(value) {
4843
- return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS;
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
- accessJwt: new TokenStore(storage, accessJwtKey(baseUrl))
5656
+ service,
5657
+ ...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
4942
5658
  };
4943
- this.auth = new AuthClient(http, {
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.#run = queryRunner(http, this.auth);
4955
- this.storage = new StorageClient(http, this.auth);
4956
- this.#newRealtime = () => new RealtimeClient({
4957
- http,
4958
- auth: this.auth,
4959
- service,
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
- return this.#client().removeChannel(channel);
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,