naro-js 0.6.1 → 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",
@@ -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,12 @@ 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
+ // 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"
508
725
  ]);
509
726
  function withRetryAfter(error, headers) {
510
727
  const header = headers.get("x-retry-after")?.trim() ?? "";
@@ -828,14 +1045,14 @@ var AuthClient = class {
828
1045
  */
829
1046
  async signInWithOAuth(options) {
830
1047
  const { provider } = options;
831
- const failed = (error) => ({
1048
+ const failed2 = (error) => ({
832
1049
  data: { provider, url: null },
833
1050
  error
834
1051
  });
835
- if (!OAUTH_PROVIDERS.includes(provider)) return failed(invalidProvider());
1052
+ if (!OAUTH_PROVIDERS.includes(provider)) return failed2(invalidProvider());
836
1053
  await this.#flow.recovery.forget();
837
1054
  const pair = await createPkcePair();
838
- if (!pair) return failed(cryptoUnavailable());
1055
+ if (!pair) return failed2(cryptoUnavailable());
839
1056
  await this.#flow.verifiers.add(pair.challenge, pair.verifier);
840
1057
  const redirectTo = options.redirectTo ?? currentPage();
841
1058
  const url = new URL(
@@ -1408,6 +1625,288 @@ var TokenStore = class {
1408
1625
  }
1409
1626
  };
1410
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
+
1411
1910
  // src/query/columns.ts
1412
1911
  function isSpace(char) {
1413
1912
  return char.trim() === "";
@@ -2917,19 +3416,19 @@ var RealtimeSocket = class {
2917
3416
  #closed(ws, code, reason, waitedMs) {
2918
3417
  if (this.#ws !== ws) return;
2919
3418
  const wasOpen = this.#state === "open";
2920
- const timedOut = waitedMs !== null;
3419
+ const timedOut2 = waitedMs !== null;
2921
3420
  detach(ws);
2922
3421
  this.#ws = null;
2923
3422
  this.#clearTimers();
2924
- if (timedOut) closeQuietly(ws, 1e3);
3423
+ if (timedOut2) closeQuietly(ws, 1e3);
2925
3424
  if (!wasOpen) this.#unopened += 1;
2926
3425
  const said = this.#said;
2927
3426
  this.#said = null;
2928
- if (!timedOut && code === 4008) this.#flooded = true;
2929
- 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);
2930
3429
  if (action.kind === "stop")
2931
3430
  return this.#stop(said ?? realtimeStopped(code, reason));
2932
- const error = timedOut ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
3431
+ const error = timedOut2 ? realtimeTimedOut(waitedMs) : said ?? realtimeClosed(code);
2933
3432
  this.#failReplies(error);
2934
3433
  this.#state = "waiting";
2935
3434
  const delay = delayAfter(
@@ -2942,7 +3441,7 @@ var RealtimeSocket = class {
2942
3441
  safely(
2943
3442
  () => this.#config.events.dropped(
2944
3443
  error,
2945
- timedOut ? "TIMED_OUT" : "CHANNEL_ERROR"
3444
+ timedOut2 ? "TIMED_OUT" : "CHANNEL_ERROR"
2946
3445
  )
2947
3446
  );
2948
3447
  }
@@ -4339,13 +4838,13 @@ async function uploadInParts(a) {
4339
4838
  }).catch(() => void 0);
4340
4839
  };
4341
4840
  const etags = [];
4342
- const failure = {
4841
+ const failure2 = {
4343
4842
  error: signal?.aborted ? uploadAborted() : null
4344
4843
  };
4345
4844
  let next = 1;
4346
4845
  let sentBytes = 0;
4347
4846
  const onAbort = () => {
4348
- failure.error ??= uploadAborted();
4847
+ failure2.error ??= uploadAborted();
4349
4848
  };
4350
4849
  const tryPart = async (n, bytes) => {
4351
4850
  const sent2 = await send(a.http, {
@@ -4374,18 +4873,18 @@ async function uploadInParts(a) {
4374
4873
  progress(sentBytes);
4375
4874
  return null;
4376
4875
  }
4377
- if (attempt2 === ATTEMPTS || failure.error !== null || !retryable(tried.error, tried.status))
4876
+ if (attempt2 === ATTEMPTS || failure2.error !== null || !retryable(tried.error, tried.status))
4378
4877
  return tried.error;
4379
4878
  await sleep(waitAfter(tried.error, attempt2), signal);
4380
- if (failure.error !== null) return tried.error;
4879
+ if (failure2.error !== null) return tried.error;
4381
4880
  }
4382
4881
  };
4383
4882
  const worker = async () => {
4384
- while (failure.error === null && next <= partCount) {
4883
+ while (failure2.error === null && next <= partCount) {
4385
4884
  const n = next;
4386
4885
  next += 1;
4387
4886
  const error = await sendPart(n);
4388
- if (error !== null) failure.error ??= error;
4887
+ if (error !== null) failure2.error ??= error;
4389
4888
  }
4390
4889
  };
4391
4890
  signal?.addEventListener("abort", onAbort);
@@ -4393,9 +4892,9 @@ async function uploadInParts(a) {
4393
4892
  Array.from({ length: Math.min(CONCURRENCY, partCount) }, worker)
4394
4893
  );
4395
4894
  signal?.removeEventListener("abort", onAbort);
4396
- if (failure.error !== null) {
4895
+ if (failure2.error !== null) {
4397
4896
  await callOff();
4398
- return fail(failure.error);
4897
+ return fail(failure2.error);
4399
4898
  }
4400
4899
  const completed = await call(a.http, {
4401
4900
  method: "POST",
@@ -4626,8 +5125,8 @@ var StorageFileApi = class {
4626
5125
  const payload = payloadOf(body, options.contentType);
4627
5126
  if (!payload.ok) return fail(payload.error);
4628
5127
  const cache = `max-age=${options.cacheControl ?? "3600"}`;
4629
- const unreadable = headerProblem(options.contentType, cache);
4630
- if (unreadable !== null) return fail(unreadable);
5128
+ const unreadable2 = headerProblem(options.contentType, cache);
5129
+ if (unreadable2 !== null) return fail(unreadable2);
4631
5130
  const { signal } = options;
4632
5131
  if (signal?.aborted) return fail(uploadAborted());
4633
5132
  await this.#auth.ready();
@@ -4928,8 +5427,8 @@ var StorageFileApi = class {
4928
5427
  const payload = payloadOf(body, given.contentType);
4929
5428
  if (!payload.ok) return fail(payload.error);
4930
5429
  const cache = `max-age=${given.cacheControl ?? "3600"}`;
4931
- const unreadable = headerProblem(given.contentType, cache);
4932
- if (unreadable !== null) return fail(unreadable);
5430
+ const unreadable2 = headerProblem(given.contentType, cache);
5431
+ if (unreadable2 !== null) return fail(unreadable2);
4933
5432
  const { signal } = given;
4934
5433
  if (signal?.aborted) return fail(uploadAborted());
4935
5434
  const uploaded = payload.size > MAX_SINGLE_UPLOAD_BYTES ? await uploadInParts({
@@ -5010,12 +5509,14 @@ var CONFIG_MESSAGES = {
5010
5509
  bothKeys: "Pass createClient an anonKey or a serviceKey, not both.",
5011
5510
  serviceKeyOffServer: SERVICE_KEY_OFF_SERVER_MESSAGE,
5012
5511
  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."
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."
5014
5515
  };
5015
5516
  var SERVICE_KEY_PREFIX = "naro_sk_";
5016
- var MAX_TIMER_MS = 2147483647;
5517
+ var MAX_TIMER_MS2 = 2147483647;
5017
5518
  function isMilliseconds(value) {
5018
- 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;
5019
5520
  }
5020
5521
  function realtimeOptionsOf(value) {
5021
5522
  if (value === void 0) return {};
@@ -5065,6 +5566,35 @@ function apiKeyOf(options) {
5065
5566
  throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
5066
5567
  return { key, service };
5067
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
+ }
5068
5598
  function queryRunner(http, auth) {
5069
5599
  return async (query) => {
5070
5600
  await auth.ready();
@@ -5093,7 +5623,14 @@ var NaroClient = class {
5093
5623
  * them, as supabase-js does.
5094
5624
  */
5095
5625
  storage;
5626
+ /**
5627
+ * The project's Naro Functions (design §9): `functions.invoke(name, {
5628
+ * body })`, as in supabase-js.
5629
+ */
5630
+ functions;
5096
5631
  #run;
5632
+ /** Made with `accessToken`: `auth` and `channel()` refuse. */
5633
+ #accessToken;
5097
5634
  #newRealtime;
5098
5635
  /**
5099
5636
  * Made by the first channel(), not by the constructor: it listens to every
@@ -5104,37 +5641,38 @@ var NaroClient = class {
5104
5641
  constructor(options) {
5105
5642
  const baseUrl = baseUrlOf(options.url);
5106
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);
5107
5647
  if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
5108
5648
  throw new Error(CONFIG_MESSAGES.fetch);
5109
5649
  const realtime = realtimeOptionsOf(options.realtime);
5110
- const storage = options.auth?.storage ?? defaultStorage();
5650
+ const storage = accessToken !== void 0 ? memoryStorage() : options.auth?.storage ?? defaultStorage();
5111
5651
  const http = {
5112
5652
  baseUrl,
5113
5653
  apiKey,
5114
5654
  fetch: options.fetch,
5115
5655
  tokens: new TokenStore(storage, storageKey(baseUrl)),
5116
- accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)),
5117
- service
5656
+ service,
5657
+ ...accessToken !== void 0 ? { accessToken } : { accessJwt: new TokenStore(storage, accessJwtKey(baseUrl)) }
5118
5658
  };
5119
- this.auth = new AuthClient(http, {
5659
+ const auth = new AuthClient(http, {
5120
5660
  verifiers: new FlowVerifiers(
5121
5661
  new TokenStore(storage, verifierKey(baseUrl))
5122
5662
  ),
5123
5663
  recovery: new RecoveryToken(
5124
5664
  new TokenStore(storage, recoveryKey(baseUrl))
5125
5665
  ),
5126
- detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
5666
+ detectSessionInUrl: accessToken === void 0 && (options.auth?.detectSessionInUrl ?? true),
5127
5667
  shared: { storage, key: storageKey(baseUrl) },
5128
5668
  serviceKey: service
5129
5669
  });
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
- });
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 });
5138
5676
  }
5139
5677
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
5140
5678
  from(table) {
@@ -5146,6 +5684,7 @@ var NaroClient = class {
5146
5684
  * `removeChannel`. Nothing connects until the first `subscribe()`.
5147
5685
  */
5148
5686
  channel(name, options) {
5687
+ if (this.#accessToken) throw new Error(ACCESS_TOKEN_CHANNEL_MESSAGE);
5149
5688
  return this.#client().channel(name, options);
5150
5689
  }
5151
5690
  /** The channels made with `channel()` and not yet removed. */
@@ -5157,7 +5696,9 @@ var NaroClient = class {
5157
5696
  * one; the connection closes with the last channel.
5158
5697
  */
5159
5698
  removeChannel(channel) {
5160
- 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);
5161
5702
  }
5162
5703
  /** `removeChannel()` for every channel; the connection closes. */
5163
5704
  removeAllChannels() {
@@ -5174,6 +5715,10 @@ function createClient(options) {
5174
5715
  return new NaroClient(options);
5175
5716
  }
5176
5717
  export {
5718
+ FunctionsError,
5719
+ FunctionsFetchError,
5720
+ FunctionsHttpError,
5721
+ FunctionsRelayError,
5177
5722
  NaroClient,
5178
5723
  createClient,
5179
5724
  objectRoute,