naro-js 0.6.1 → 0.7.1

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",
@@ -195,6 +206,16 @@ function storeFor(config, path) {
195
206
  return SESSION_PREFIXES.some((prefix) => path.startsWith(prefix)) ? config.tokens : config.accessJwt ?? null;
196
207
  }
197
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
+ }
198
219
  function timesOf(jwt) {
199
220
  const payload = jwt.split(".")[1];
200
221
  if (payload === void 0 || payload === "") return null;
@@ -290,6 +311,13 @@ async function accessJwtFor(config, jwtStore) {
290
311
  async function bearerFor(config, path, withToken) {
291
312
  if (!withToken) return { token: null, session: null };
292
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
+ }
293
321
  if (store === null) return { token: null, session: null };
294
322
  if (store === config.tokens) {
295
323
  const token = await store.get();
@@ -304,9 +332,12 @@ async function retryBearer(config, path, error, first) {
304
332
  if ("error" in renewed) return renewed;
305
333
  return renewed.token === null || renewed.token === first.token || renewed.session !== first.session ? null : renewed;
306
334
  }
307
- async function receive(config, options, init) {
308
- const controller = options.timeoutMs !== void 0 && typeof AbortController === "function" ? new AbortController() : null;
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;
309
339
  if (controller) init.signal = controller.signal;
340
+ let gaveUp = false;
310
341
  const request = (async () => {
311
342
  try {
312
343
  const fetchImpl = config.fetch ?? globalThis.fetch;
@@ -314,6 +345,10 @@ async function receive(config, options, init) {
314
345
  `${config.baseUrl}/_naro/v1${options.path}`,
315
346
  init
316
347
  );
348
+ if (gaveUp) {
349
+ discard(response);
350
+ return { error: timedOut(deadline) };
351
+ }
317
352
  return {
318
353
  status: response.status,
319
354
  headers: response.headers,
@@ -323,14 +358,14 @@ async function receive(config, options, init) {
323
358
  return { error: networkError(error) };
324
359
  }
325
360
  })();
326
- const { timeoutMs } = options;
327
- if (timeoutMs === void 0) return request;
361
+ if (deadline === null) return request;
328
362
  let timer;
329
363
  const expired = new Promise((resolve) => {
330
364
  timer = setTimeout(() => {
365
+ gaveUp = true;
331
366
  controller?.abort();
332
- resolve({ error: requestTimeout(timeoutMs) });
333
- }, timeoutMs);
367
+ resolve({ error: timedOut(deadline) });
368
+ }, remainingMs(deadline));
334
369
  });
335
370
  try {
336
371
  return await Promise.race([request, expired]);
@@ -347,15 +382,34 @@ async function call(config, options) {
347
382
  return { error: invalidQuery("", "is not JSON"), answer: null };
348
383
  }
349
384
  }
350
- const bearer = await bearerFor(config, options.path, options.withToken);
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 };
351
393
  if ("error" in bearer) return { error: bearer.error, answer: null };
352
- const result = await callWith(config, options, payload, bearer.token);
353
- const again = await retryBearer(config, options.path, result.error, bearer);
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 };
354
408
  if (again === null) return result;
355
409
  if ("error" in again) return { error: again.error, answer: null };
356
- return callWith(config, options, payload, again.token);
410
+ return callWith(config, options, payload, again.token, deadline);
357
411
  }
358
- async function callWith(config, options, payload, token) {
412
+ async function callWith(config, options, payload, token, deadline = null) {
359
413
  const store = storeFor(config, options.path);
360
414
  const headers = {
361
415
  ...options.headers,
@@ -375,7 +429,7 @@ async function callWith(config, options, payload, token) {
375
429
  headers["Content-Type"] = "application/json";
376
430
  init.body = payload;
377
431
  }
378
- const received = await receive(config, options, init);
432
+ const received = await receive(config, options, init, deadline);
379
433
  if ("error" in received) return { error: received.error, answer: null };
380
434
  const { status, headers: responseHeaders, text } = received;
381
435
  let body;
@@ -437,6 +491,27 @@ function untilAborted(promise, signal) {
437
491
  );
438
492
  });
439
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
+ }
440
515
  function isStream(body) {
441
516
  return typeof ReadableStream === "function" && body instanceof ReadableStream;
442
517
  }
@@ -496,6 +571,143 @@ async function sendWith(config, options, token) {
496
571
  status
497
572
  };
498
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
+ }
499
711
  var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
500
712
  "RATE_LIMITED",
501
713
  "WRITE_QUOTA_EXCEEDED",
@@ -504,7 +716,15 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
504
716
  "OBJECT_RATE_LIMITED",
505
717
  "STORAGE_OPS_QUOTA_EXCEEDED",
506
718
  // Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
507
- "TOO_MANY_MESSAGES"
719
+ "TOO_MANY_MESSAGES",
720
+ // The dispatcher's REST 429 of a project over its month of Realtime (3-2e):
721
+ // `x-retry-after` is the seconds to the 1st at 00:00 UTC.
722
+ "REALTIME_QUOTA_EXCEEDED",
723
+ // Naro Functions (design F20): a team over its monthly function CPU, and
724
+ // the control plane's Cloudflare limit. `invoke` reads x-retry-after on
725
+ // every relay error anyway (invoke/client.ts).
726
+ "FUNCTIONS_QUOTA_EXCEEDED",
727
+ "FUNCTIONS_RATE_LIMITED"
508
728
  ]);
509
729
  function withRetryAfter(error, headers) {
510
730
  const header = headers.get("x-retry-after")?.trim() ?? "";
@@ -828,14 +1048,14 @@ var AuthClient = class {
828
1048
  */
829
1049
  async signInWithOAuth(options) {
830
1050
  const { provider } = options;
831
- const failed = (error) => ({
1051
+ const failed2 = (error) => ({
832
1052
  data: { provider, url: null },
833
1053
  error
834
1054
  });
835
- if (!OAUTH_PROVIDERS.includes(provider)) return failed(invalidProvider());
1055
+ if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
836
1056
  await this.#flow.recovery.forget();
837
1057
  const pair = await createPkcePair();
838
- if (!pair) return failed(cryptoUnavailable());
1058
+ if (!pair) return failed2(cryptoUnavailable());
839
1059
  await this.#flow.verifiers.add(pair.challenge, pair.verifier);
840
1060
  const redirectTo = options.redirectTo ?? currentPage();
841
1061
  const url = new URL(
@@ -1408,6 +1628,288 @@ var TokenStore = class {
1408
1628
  }
1409
1629
  };
1410
1630
 
1631
+ // src/invoke/body.ts
1632
+ function prepareBody(body) {
1633
+ if (body === void 0) return { body: void 0, contentType: null };
1634
+ if (typeof body === "string") return { body, contentType: "text/plain" };
1635
+ if (typeof Blob === "function" && body instanceof Blob)
1636
+ return { body, contentType: "application/octet-stream" };
1637
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body))
1638
+ return { body, contentType: "application/octet-stream" };
1639
+ if (typeof FormData === "function" && body instanceof FormData)
1640
+ return { body, contentType: null };
1641
+ if (typeof URLSearchParams === "function" && body instanceof URLSearchParams)
1642
+ return { body, contentType: null };
1643
+ if (typeof ReadableStream === "function" && body instanceof ReadableStream)
1644
+ return { body, contentType: null };
1645
+ try {
1646
+ const text = JSON.stringify(body);
1647
+ return text === void 0 ? null : { body: text, contentType: "application/json" };
1648
+ } catch {
1649
+ return null;
1650
+ }
1651
+ }
1652
+ async function readAnswer(response, method) {
1653
+ if (response.status === 204 || method === "HEAD") return { data: null };
1654
+ const type = (response.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
1655
+ try {
1656
+ if (type === "application/json") {
1657
+ const text = await response.text();
1658
+ return {
1659
+ data: text.trim() === "" ? null : JSON.parse(text)
1660
+ };
1661
+ }
1662
+ if (type === "application/octet-stream" || type === "application/pdf")
1663
+ return { data: await response.blob() };
1664
+ if (type === "text/event-stream") return { data: response };
1665
+ if (type === "multipart/form-data")
1666
+ return { data: await response.formData() };
1667
+ return { data: await response.text() };
1668
+ } catch (cause) {
1669
+ return { unreadable: cause };
1670
+ }
1671
+ }
1672
+
1673
+ // src/invoke/errors.ts
1674
+ var FunctionsError = class extends Error {
1675
+ code;
1676
+ context;
1677
+ constructor(code, message, context, details) {
1678
+ super(message);
1679
+ this.name = "FunctionsError";
1680
+ this.code = code;
1681
+ this.context = context;
1682
+ if (details !== void 0)
1683
+ Object.defineProperty(this, "details", {
1684
+ value: details,
1685
+ enumerable: true
1686
+ });
1687
+ }
1688
+ };
1689
+ var FunctionsHttpError = class extends FunctionsError {
1690
+ constructor(response) {
1691
+ super(
1692
+ "FUNCTION_HTTP_ERROR",
1693
+ `The function answered with HTTP ${String(response.status)}.`,
1694
+ response
1695
+ );
1696
+ this.name = "FunctionsHttpError";
1697
+ }
1698
+ };
1699
+ var FunctionsRelayError = class extends FunctionsError {
1700
+ constructor(code, message, response, details) {
1701
+ super(code, message, response, details);
1702
+ this.name = "FunctionsRelayError";
1703
+ }
1704
+ };
1705
+ var FunctionsFetchError = class extends FunctionsError {
1706
+ constructor(code, message, cause, details) {
1707
+ super(code, message, cause, details);
1708
+ this.name = "FunctionsFetchError";
1709
+ }
1710
+ };
1711
+ var FUNCTIONS_MESSAGES = {
1712
+ timeout: (ms) => `The function did not answer within ${String(ms)} ms.`,
1713
+ aborted: "The function call was stopped before the function answered.",
1714
+ 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.",
1715
+ invalidTimeout: "functions.invoke's timeout is whole milliseconds from 1 to 2147483647.",
1716
+ invalidHeaders: "functions.invoke's headers are not valid HTTP header names and values.",
1717
+ invalidName: "functions.invoke's name is a string: the function's name, then any path and query.",
1718
+ unencodableName: "functions.invoke's name is not well-formed Unicode: it has a lone surrogate.",
1719
+ dotSegments: `functions.invoke's name cannot be "." or "..", and ".." in its path cannot leave /functions/.`,
1720
+ invalidSignal: "functions.invoke's signal is not an AbortSignal.",
1721
+ bodyWithGetOrHead: "functions.invoke cannot send a body with GET or HEAD.",
1722
+ /** `RELAY_ERROR`: a HEAD answer has no body, so Naro's code is not in it. */
1723
+ 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.`,
1724
+ unreadable: (status) => `The function answered HTTP ${String(status)}, but its body could not be read as its content-type says.`
1725
+ };
1726
+
1727
+ // src/invoke/client.ts
1728
+ var MAX_TIMER_MS = 2147483647;
1729
+ function functionPath(name) {
1730
+ const cut = name.search(/[/?#]/);
1731
+ const head = cut === -1 ? name : name.slice(0, cut);
1732
+ const tail = cut === -1 ? "" : name.slice(cut);
1733
+ return `/functions/${encodeURIComponent(head)}${tail}`;
1734
+ }
1735
+ function dotSegment(segment) {
1736
+ const dots = segment.toLowerCase().replace(/%2e/g, ".");
1737
+ return dots === "." ? 1 : dots === ".." ? 2 : 0;
1738
+ }
1739
+ function leavesFunctions(input) {
1740
+ const name = input.replace(/[\t\n\r]/g, "");
1741
+ const cut = name.search(/[/?#]/);
1742
+ const head = cut === -1 ? name : name.slice(0, cut);
1743
+ if (head === "." || head === "..") return true;
1744
+ if (cut === -1 || name[cut] !== "/") return false;
1745
+ const rest = name.slice(cut + 1);
1746
+ const end = rest.search(/[?#]/);
1747
+ let depth = 1;
1748
+ for (const segment of (end === -1 ? rest : rest.slice(0, end)).split(
1749
+ /[/\\]/
1750
+ )) {
1751
+ const dots = dotSegment(segment);
1752
+ if (dots === 0) depth += 1;
1753
+ if (dots === 2) depth -= 1;
1754
+ if (depth < 0) return true;
1755
+ }
1756
+ return false;
1757
+ }
1758
+ function pathOf(name) {
1759
+ if (typeof name !== "string")
1760
+ return { invalid: FUNCTIONS_MESSAGES.invalidName };
1761
+ if (leavesFunctions(name)) return { invalid: FUNCTIONS_MESSAGES.dotSegments };
1762
+ try {
1763
+ return { path: functionPath(name) };
1764
+ } catch {
1765
+ return { invalid: FUNCTIONS_MESSAGES.unencodableName };
1766
+ }
1767
+ }
1768
+ function isSignalOrNone(value) {
1769
+ if (value === void 0 || value === null) return true;
1770
+ if (typeof value !== "object") return false;
1771
+ const signal = value;
1772
+ return typeof signal.aborted === "boolean" && typeof signal.addEventListener === "function" && typeof signal.removeEventListener === "function";
1773
+ }
1774
+ function invalidOptions(message) {
1775
+ return failed(new FunctionsFetchError("INVALID_OPTIONS", message, null));
1776
+ }
1777
+ function failed(error) {
1778
+ return { data: null, error };
1779
+ }
1780
+ function fromNaroError(error, cause) {
1781
+ return new FunctionsFetchError(
1782
+ error.code,
1783
+ error.message,
1784
+ cause,
1785
+ error.details
1786
+ );
1787
+ }
1788
+ function failure(kind, cause, timeout) {
1789
+ if (kind === "timeout")
1790
+ return new FunctionsFetchError(
1791
+ "TIMEOUT",
1792
+ FUNCTIONS_MESSAGES.timeout(timeout ?? 0),
1793
+ cause
1794
+ );
1795
+ if (kind === "aborted")
1796
+ return new FunctionsFetchError(
1797
+ "ABORTED",
1798
+ FUNCTIONS_MESSAGES.aborted,
1799
+ cause
1800
+ );
1801
+ return fromNaroError(networkError(cause), cause);
1802
+ }
1803
+ function unreadable(cause, status, signal) {
1804
+ if (signal?.aborted)
1805
+ return new FunctionsFetchError(
1806
+ "ABORTED",
1807
+ FUNCTIONS_MESSAGES.aborted,
1808
+ cause
1809
+ );
1810
+ if (cause instanceof SyntaxError)
1811
+ return new FunctionsFetchError(
1812
+ "INVALID_RESPONSE",
1813
+ FUNCTIONS_MESSAGES.unreadable(status),
1814
+ cause
1815
+ );
1816
+ return fromNaroError(networkError(cause), cause);
1817
+ }
1818
+ async function relayError(response, head) {
1819
+ let body;
1820
+ try {
1821
+ body = await response.clone().json();
1822
+ } catch {
1823
+ body = void 0;
1824
+ }
1825
+ const { status } = response;
1826
+ const error = withRetryAfter(
1827
+ head && body === void 0 ? {
1828
+ code: "RELAY_ERROR",
1829
+ message: FUNCTIONS_MESSAGES.headRelay(status),
1830
+ details: { status }
1831
+ } : errorFromBody(status, body) ?? invalidResponse(status),
1832
+ response.headers
1833
+ );
1834
+ return new FunctionsRelayError(
1835
+ error.code,
1836
+ error.message,
1837
+ response,
1838
+ error.details
1839
+ );
1840
+ }
1841
+ var FunctionsClient = class {
1842
+ #http;
1843
+ #auth;
1844
+ constructor(http, auth) {
1845
+ this.#http = http;
1846
+ this.#auth = auth;
1847
+ }
1848
+ async invoke(name, options = {}) {
1849
+ const method = options.method ?? "POST";
1850
+ const { timeout, signal } = options;
1851
+ const target = pathOf(name);
1852
+ if ("invalid" in target) return invalidOptions(target.invalid);
1853
+ if (timeout !== void 0 && !(Number.isInteger(timeout) && timeout > 0 && timeout <= MAX_TIMER_MS))
1854
+ return invalidOptions(FUNCTIONS_MESSAGES.invalidTimeout);
1855
+ if (!isSignalOrNone(signal))
1856
+ return invalidOptions(FUNCTIONS_MESSAGES.invalidSignal);
1857
+ const verb = typeof method === "string" ? method.toUpperCase() : "";
1858
+ if (options.body !== void 0 && (verb === "GET" || verb === "HEAD"))
1859
+ return invalidOptions(FUNCTIONS_MESSAGES.bodyWithGetOrHead);
1860
+ let headers;
1861
+ try {
1862
+ headers = new Headers(options.headers);
1863
+ } catch (cause) {
1864
+ return failed(
1865
+ new FunctionsFetchError(
1866
+ "INVALID_OPTIONS",
1867
+ FUNCTIONS_MESSAGES.invalidHeaders,
1868
+ cause
1869
+ )
1870
+ );
1871
+ }
1872
+ const prepared = prepareBody(options.body);
1873
+ if (prepared === null)
1874
+ return failed(
1875
+ new FunctionsFetchError(
1876
+ "INVALID_BODY",
1877
+ FUNCTIONS_MESSAGES.invalidBody,
1878
+ null
1879
+ )
1880
+ );
1881
+ if (prepared.contentType !== null && !headers.has("content-type"))
1882
+ headers.set("content-type", prepared.contentType);
1883
+ const result = await fetchUnread(this.#http, {
1884
+ method,
1885
+ path: target.path,
1886
+ headers,
1887
+ body: prepared.body,
1888
+ timeoutMs: timeout,
1889
+ signal,
1890
+ // A sign-in the page URL carried goes first, as for a query (P2-6) —
1891
+ // within the call's timeout and signal; the exchange is not cut.
1892
+ ready: () => this.#auth.ready()
1893
+ });
1894
+ if ("refused" in result) return failed(fromNaroError(result.refused, null));
1895
+ if ("failed" in result)
1896
+ return failed(failure(result.failed, result.cause, timeout));
1897
+ const { response } = result;
1898
+ if (!response.ok) {
1899
+ const error = response.headers.get("x-relay-error") === "true" ? await relayError(response, verb === "HEAD") : new FunctionsHttpError(response);
1900
+ return { data: null, error, response };
1901
+ }
1902
+ const answer = await readAnswer(response, method);
1903
+ if ("unreadable" in answer)
1904
+ return {
1905
+ data: null,
1906
+ error: unreadable(answer.unreadable, response.status, signal),
1907
+ response
1908
+ };
1909
+ return { data: answer.data, error: null, response };
1910
+ }
1911
+ };
1912
+
1411
1913
  // src/query/columns.ts
1412
1914
  function isSpace(char) {
1413
1915
  return char.trim() === "";
@@ -2173,6 +2675,7 @@ var TableBuilder = class {
2173
2675
  var BACKOFF_STEPS_MS = [1e3, 2e3, 5e3, 1e4];
2174
2676
  var RATE_LIMITED_MIN_MS = 5e3;
2175
2677
  var IP_LIMIT_WAIT_MS = 6e4;
2678
+ var QUOTA_WAIT_MS = 9e5;
2176
2679
  var RESTART_JITTER_MS = 1e4;
2177
2680
  var UNOPENED_SLOWDOWN_AFTER = 6;
2178
2681
  var UNOPENED_DELAY_MS = 6e4;
@@ -2189,7 +2692,7 @@ function closeAction(code, reason) {
2189
2692
  if (code === 4401) return { kind: "session" };
2190
2693
  if (code === 4503) return { kind: "restart" };
2191
2694
  if (code === 4029)
2192
- return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
2695
+ return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : reason === "REALTIME_QUOTA_EXCEEDED" ? { kind: "wait", ms: QUOTA_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
2193
2696
  return { kind: "retry", minMs: 0 };
2194
2697
  }
2195
2698
  function backoffMs(attempt2, random) {
@@ -2917,19 +3420,19 @@ var RealtimeSocket = class {
2917
3420
  #closed(ws, code, reason, waitedMs) {
2918
3421
  if (this.#ws !== ws) return;
2919
3422
  const wasOpen = this.#state === "open";
2920
- const timedOut = waitedMs !== null;
3423
+ const timedOut2 = waitedMs !== null;
2921
3424
  detach(ws);
2922
3425
  this.#ws = null;
2923
3426
  this.#clearTimers();
2924
- if (timedOut) closeQuietly(ws, 1e3);
3427
+ if (timedOut2) closeQuietly(ws, 1e3);
2925
3428
  if (!wasOpen) this.#unopened += 1;
2926
3429
  const said = this.#said;
2927
3430
  this.#said = null;
2928
- if (!timedOut && code === 4008) this.#flooded = true;
2929
- const action = timedOut ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
3431
+ if (!timedOut2 && code === 4008) this.#flooded = true;
3432
+ const action = timedOut2 ? { kind: "retry", minMs: 0 } : closeAction(code, said?.code ?? reason);
2930
3433
  if (action.kind === "stop")
2931
3434
  return this.#stop(said ?? realtimeStopped(code, reason));
2932
- const error = timedOut ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
3435
+ const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
2933
3436
  this.#failReplies(error);
2934
3437
  this.#state = "waiting";
2935
3438
  const delay = delayAfter(
@@ -2942,7 +3445,7 @@ var RealtimeSocket = class {
2942
3445
  safely(
2943
3446
  () => this.#config.events.dropped(
2944
3447
  error,
2945
- timedOut ? "TIMED_OUT" : "CHANNEL_ERROR"
3448
+ timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
2946
3449
  )
2947
3450
  );
2948
3451
  }
@@ -4339,13 +4842,13 @@ async function uploadInParts(a) {
4339
4842
  }).catch(() => void 0);
4340
4843
  };
4341
4844
  const etags = [];
4342
- const failure = {
4845
+ const failure2 = {
4343
4846
  error: signal?.aborted ? uploadAborted() : null
4344
4847
  };
4345
4848
  let next = 1;
4346
4849
  let sentBytes = 0;
4347
4850
  const onAbort = () => {
4348
- failure.error ??= uploadAborted();
4851
+ failure2.error ??= uploadAborted();
4349
4852
  };
4350
4853
  const tryPart = async (n, bytes) => {
4351
4854
  const sent2 = await send(a.http, {
@@ -4374,18 +4877,18 @@ async function uploadInParts(a) {
4374
4877
  progress(sentBytes);
4375
4878
  return null;
4376
4879
  }
4377
- if (attempt2 === ATTEMPTS || failure.error !== null || !retryable(tried.error, tried.status))
4880
+ if (attempt2 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
4378
4881
  return tried.error;
4379
4882
  await sleep(waitAfter(tried.error, attempt2), signal);
4380
- if (failure.error !== null) return tried.error;
4883
+ if (failure2.error !== null) return tried.error;
4381
4884
  }
4382
4885
  };
4383
4886
  const worker = async () => {
4384
- while (failure.error === null && next <= partCount) {
4887
+ while (failure2.error === null && next <= partCount) {
4385
4888
  const n = next;
4386
4889
  next += 1;
4387
4890
  const error = await sendPart(n);
4388
- if (error !== null) failure.error ??= error;
4891
+ if (error !== null) failure2.error ??= error;
4389
4892
  }
4390
4893
  };
4391
4894
  signal?.addEventListener("abort", onAbort);
@@ -4393,9 +4896,9 @@ async function uploadInParts(a) {
4393
4896
  Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
4394
4897
  );
4395
4898
  signal?.removeEventListener("abort", onAbort);
4396
- if (failure.error !== null) {
4899
+ if (failure2.error !== null) {
4397
4900
  await callOff();
4398
- return fail(failure.error);
4901
+ return fail(failure2.error);
4399
4902
  }
4400
4903
  const completed = await call(a.http, {
4401
4904
  method: "POST",
@@ -4626,8 +5129,8 @@ var StorageFileApi = class {
4626
5129
  const payload = payloadOf(body, options.contentType);
4627
5130
  if (!payload.ok) return fail(payload.error);
4628
5131
  const cache = `max-age=${options.cacheControl ?? "3600"}`;
4629
- const unreadable = headerProblem(options.contentType, cache);
4630
- if (unreadable !== null) return fail(unreadable);
5132
+ const unreadable2 = headerProblem(options.contentType, cache);
5133
+ if (unreadable2 !== null) return fail(unreadable2);
4631
5134
  const { signal } = options;
4632
5135
  if (signal?.aborted) return fail(uploadAborted());
4633
5136
  await this.#auth.ready();
@@ -4928,8 +5431,8 @@ var StorageFileApi = class {
4928
5431
  const payload = payloadOf(body, given.contentType);
4929
5432
  if (!payload.ok) return fail(payload.error);
4930
5433
  const cache = `max-age=${given.cacheControl ?? "3600"}`;
4931
- const unreadable = headerProblem(given.contentType, cache);
4932
- if (unreadable !== null) return fail(unreadable);
5434
+ const unreadable2 = headerProblem(given.contentType, cache);
5435
+ if (unreadable2 !== null) return fail(unreadable2);
4933
5436
  const { signal } = given;
4934
5437
  if (signal?.aborted) return fail(uploadAborted());
4935
5438
  const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES ? await uploadInParts({
@@ -5010,12 +5513,14 @@ var CONFIG_MESSAGES = {
5010
5513
  bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
5011
5514
  serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
5012
5515
  fetch: "This runtime has no fetch. Pass one to createClient({ fetch }).",
5013
- realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647."
5516
+ realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.",
5517
+ accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.",
5518
+ accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none."
5014
5519
  };
5015
5520
  var SERVICE_KEY_PREFIX = "naro_sk_";
5016
- var MAX_TIMER_MS = 2147483647;
5521
+ var MAX_TIMER_MS2 = 2147483647;
5017
5522
  function isMilliseconds(value) {
5018
- return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS;
5523
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= MAX_TIMER_MS2;
5019
5524
  }
5020
5525
  function realtimeOptionsOf(value) {
5021
5526
  if (value === void 0) return {};
@@ -5065,6 +5570,35 @@ function apiKeyOf(options) {
5065
5570
  throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
5066
5571
  return { key, service };
5067
5572
  }
5573
+ function accessTokenOf(value) {
5574
+ if (value === void 0) return void 0;
5575
+ if (typeof value !== "function") throw new Error(CONFIG_MESSAGES.accessToken);
5576
+ const read = value;
5577
+ return {
5578
+ async get() {
5579
+ const token = await read();
5580
+ if (token === null || token === void 0 || token === "") return null;
5581
+ if (typeof token !== "string")
5582
+ throw new TypeError(`accessToken returned a ${typeof token}`);
5583
+ if (!/^[\x21-\x7E]+$/.test(token))
5584
+ throw new TypeError(
5585
+ "accessToken returned a string that is not a token"
5586
+ );
5587
+ return token;
5588
+ }
5589
+ };
5590
+ }
5591
+ function refusingAuth() {
5592
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
5593
+ get(_target, property) {
5594
+ if (typeof property === "symbol" || property === "then" || property === "toJSON" || property === "constructor" || property === "asymmetricMatch")
5595
+ return void 0;
5596
+ if (property === "toString") return Object.prototype.toString;
5597
+ if (property === "valueOf") return Object.prototype.valueOf;
5598
+ throw new Error(accessTokenAuthMessage(property));
5599
+ }
5600
+ });
5601
+ }
5068
5602
  function queryRunner(http, auth) {
5069
5603
  return async (query) => {
5070
5604
  await auth.ready();
@@ -5093,7 +5627,14 @@ var NaroClient = class {
5093
5627
  * them, as supabase-js does.
5094
5628
  */
5095
5629
  storage;
5630
+ /**
5631
+ * The project's Naro Functions (design §9): `functions.invoke(name, {
5632
+ * body })`, as in supabase-js.
5633
+ */
5634
+ functions;
5096
5635
  #run;
5636
+ /** Made with `accessToken`: `auth` and `channel()` refuse. */
5637
+ #accessToken;
5097
5638
  #newRealtime;
5098
5639
  /**
5099
5640
  * Made by the first channel(), not by the constructor: it listens to every
@@ -5104,37 +5645,38 @@ var NaroClient = class {
5104
5645
  constructor(options) {
5105
5646
  const baseUrl = baseUrlOf(options.url);
5106
5647
  const { key: apiKey, service } = apiKeyOf(options);
5648
+ const accessToken = accessTokenOf(options.accessToken);
5649
+ if (accessToken !== void 0 && service)
5650
+ throw new Error(CONFIG_MESSAGES.accessTokenWithServiceKey);
5107
5651
  if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
5108
5652
  throw new Error(CONFIG_MESSAGES.fetch);
5109
5653
  const realtime = realtimeOptionsOf(options.realtime);
5110
- const storage = options.auth?.storage ?? defaultStorage();
5654
+ const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
5111
5655
  const http = {
5112
5656
  baseUrl,
5113
5657
  apiKey,
5114
5658
  fetch: options.fetch,
5115
5659
  tokens: new TokenStore(storage, storageKey(baseUrl)),
5116
- accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)),
5117
- service
5660
+ service,
5661
+ ...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
5118
5662
  };
5119
- this.auth = new AuthClient(http, {
5663
+ const auth = new AuthClient(http, {
5120
5664
  verifiers: new FlowVerifiers(
5121
5665
  new TokenStore(storage, verifierKey(baseUrl))
5122
5666
  ),
5123
5667
  recovery: new RecoveryToken(
5124
5668
  new TokenStore(storage, recoveryKey(baseUrl))
5125
5669
  ),
5126
- detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
5670
+ detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
5127
5671
  shared: { storage, key: storageKey(baseUrl) },
5128
5672
  serviceKey: service
5129
5673
  });
5130
- this.#run = queryRunner(http, this.auth);
5131
- this.storage = new StorageClient(http, this.auth);
5132
- this.#newRealtime = () => new RealtimeClient({
5133
- http,
5134
- auth: this.auth,
5135
- service,
5136
- options: realtime
5137
- });
5674
+ this.#accessToken = accessToken !== void 0;
5675
+ this.auth = this.#accessToken ? refusingAuth() : auth;
5676
+ this.#run = queryRunner(http, auth);
5677
+ this.storage = new StorageClient(http, auth);
5678
+ this.functions = new FunctionsClient(http, auth);
5679
+ this.#newRealtime = () => new RealtimeClient({ http, auth, service, options: realtime });
5138
5680
  }
5139
5681
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
5140
5682
  from(table) {
@@ -5146,6 +5688,7 @@ var NaroClient = class {
5146
5688
  * `removeChannel`. Nothing connects until the first `subscribe()`.
5147
5689
  */
5148
5690
  channel(name, options) {
5691
+ if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
5149
5692
  return this.#client().channel(name, options);
5150
5693
  }
5151
5694
  /** The channels made with `channel()` and not yet removed. */
@@ -5157,7 +5700,9 @@ var NaroClient = class {
5157
5700
  * one; the connection closes with the last channel.
5158
5701
  */
5159
5702
  removeChannel(channel) {
5160
- return this.#client().removeChannel(channel);
5703
+ if (this.#realtime === void 0)
5704
+ return Promise.resolve({ data: null, error: null });
5705
+ return this.#realtime.removeChannel(channel);
5161
5706
  }
5162
5707
  /** `removeChannel()` for every channel; the connection closes. */
5163
5708
  removeAllChannels() {
@@ -5174,6 +5719,10 @@ function createClient(options) {
5174
5719
  return new NaroClient(options);
5175
5720
  }
5176
5721
  export {
5722
+ FunctionsError,
5723
+ FunctionsFetchError,
5724
+ FunctionsHttpError,
5725
+ FunctionsRelayError,
5177
5726
  NaroClient,
5178
5727
  createClient,
5179
5728
  objectRoute,