@calimero-network/mero-js 29.14.0 → 29.16.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.mjs CHANGED
@@ -1406,6 +1406,14 @@ var ReleaseDeadlineError = class extends CalimeroError {
1406
1406
  this.after = typeof details.after === "string" ? details.after : "";
1407
1407
  }
1408
1408
  };
1409
+ var RuntimeTooOld = class extends CalimeroError {
1410
+ constructor(message, details = {}) {
1411
+ super(409, "RUNTIME_TOO_OLD", message, details);
1412
+ this.name = "RuntimeTooOld";
1413
+ this.minRuntimeVersion = typeof details.minRuntimeVersion === "string" ? details.minRuntimeVersion : "";
1414
+ this.nodes = Array.isArray(details.nodes) ? details.nodes : [];
1415
+ }
1416
+ };
1409
1417
  var ConsentStale = class extends CalimeroError {
1410
1418
  constructor(message, details = {}) {
1411
1419
  super(409, "CONSENT_STALE", message, details);
@@ -1541,6 +1549,8 @@ function errorFrom(status, body, retryAfterHeader) {
1541
1549
  return new IdempotencyMismatch(message, details);
1542
1550
  case "409 RELEASE_DEADLINE":
1543
1551
  return new ReleaseDeadlineError(message, details);
1552
+ case "409 RUNTIME_TOO_OLD":
1553
+ return new RuntimeTooOld(message, details);
1544
1554
  case "409 CONSENT_STALE":
1545
1555
  return new ConsentStale(message, details);
1546
1556
  case "403 ORIGIN_NOT_REGISTERED":
@@ -1659,6 +1669,440 @@ function browserPasskey() {
1659
1669
  };
1660
1670
  }
1661
1671
 
1672
+ // src/runtime/transport.ts
1673
+ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
1674
+ function retryDelayMs(retry, options) {
1675
+ return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
1676
+ }
1677
+ function userVerificationOf(named, fallback) {
1678
+ return named === "required" || named === "preferred" || named === "discouraged" ? named : fallback;
1679
+ }
1680
+ function promptFailure(error) {
1681
+ const e = error;
1682
+ const name = e?.name;
1683
+ const message = typeof e?.message === "string" ? e.message : "";
1684
+ if (name === "InvalidStateError" || /not focused|already pending/i.test(message)) {
1685
+ return new ConfirmPromptBlocked(error);
1686
+ }
1687
+ if (name === "NotAllowedError" || name === "AbortError") return new ConfirmCancelled(error);
1688
+ return error;
1689
+ }
1690
+ function sealedError(error) {
1691
+ if (error instanceof SealedTransportError && error.status === 413) {
1692
+ return errorFrom(413, {
1693
+ error: "TOO_LARGE",
1694
+ message: error.message.replace(/^Sealed request refused: /, "")
1695
+ });
1696
+ }
1697
+ return error;
1698
+ }
1699
+ async function refusal(response) {
1700
+ const text = await response.text().catch(() => "");
1701
+ let body = {};
1702
+ try {
1703
+ body = text ? JSON.parse(text) : {};
1704
+ } catch {
1705
+ body = { message: text };
1706
+ }
1707
+ return errorFrom(response.status, body, response.headers.get("retry-after"));
1708
+ }
1709
+ async function* sseFrames(body) {
1710
+ const reader = body.getReader();
1711
+ const decoder = new TextDecoder();
1712
+ let buffer = "";
1713
+ for (; ; ) {
1714
+ const { value, done } = await reader.read();
1715
+ if (done) return;
1716
+ buffer += decoder.decode(value, { stream: true });
1717
+ let cut;
1718
+ while ((cut = buffer.indexOf("\n\n")) >= 0) {
1719
+ const frame = buffer.slice(0, cut);
1720
+ buffer = buffer.slice(cut + 2);
1721
+ let id = "";
1722
+ let kind = "";
1723
+ let data = "";
1724
+ for (const line of frame.split("\n")) {
1725
+ if (line.startsWith("id:")) id = line.slice(3).trim();
1726
+ else if (line.startsWith("event:")) kind = line.slice(6).trim();
1727
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
1728
+ }
1729
+ if (!kind && !data) continue;
1730
+ let parsed = data;
1731
+ try {
1732
+ parsed = JSON.parse(data);
1733
+ } catch {
1734
+ }
1735
+ yield { id, kind, data: parsed };
1736
+ }
1737
+ }
1738
+ }
1739
+ function newIdempotencyKey() {
1740
+ return `node:c-${b64url(crypto.getRandomValues(new Uint8Array(16)))}`;
1741
+ }
1742
+ function routePath(template, params = {}) {
1743
+ return template.replace(/\{([^}]+)\}/g, (_, name) => {
1744
+ const value = params[name];
1745
+ if (value === void 0) throw new Error(`no value for path parameter ${name} of ${template}`);
1746
+ return encodeURIComponent(String(value));
1747
+ });
1748
+ }
1749
+ function queryString(query) {
1750
+ if (!query) return "";
1751
+ const search = new URLSearchParams();
1752
+ for (const [name, value] of Object.entries(query)) {
1753
+ for (const one of Array.isArray(value) ? value : [value]) {
1754
+ if (one !== void 0 && one !== null) search.append(name, String(one));
1755
+ }
1756
+ }
1757
+ const text = search.toString();
1758
+ return text ? `?${text}` : "";
1759
+ }
1760
+ function ceremony(path) {
1761
+ return path === "auth/refresh" || path.startsWith("auth/passkey/") || RECOVERY_CEREMONIES.has(path);
1762
+ }
1763
+ var RECOVERY_CEREMONIES = /* @__PURE__ */ new Set(["auth/recovery/begin", "auth/recovery/start", "auth/recovery/finish"]);
1764
+ function relative(path) {
1765
+ return path.replace(/^\/+/, "");
1766
+ }
1767
+ var NodeTransport = class {
1768
+ constructor(options) {
1769
+ /** The one in use: an index into {@link nodeUrls}. */
1770
+ this.at = 0;
1771
+ /** A sealed fetch per node, each attesting its own node on first use. */
1772
+ this.channels = /* @__PURE__ */ new Map();
1773
+ /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
1774
+ this.networkFailures = /* @__PURE__ */ new WeakSet();
1775
+ const trim = (url2) => url2.replace(/\/+$/, "");
1776
+ this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
1777
+ this.policy = options.policy;
1778
+ this.retry = { ...DEFAULT_RETRY, ...options.retry };
1779
+ const given = options.fetch ?? ((input, init) => fetch(input, init));
1780
+ const raw = async (input, init) => {
1781
+ try {
1782
+ return await given(input, init);
1783
+ } catch (error) {
1784
+ if (error instanceof TypeError) this.networkFailures.add(error);
1785
+ throw error;
1786
+ }
1787
+ };
1788
+ this.raw = raw;
1789
+ }
1790
+ /** Re-read the node list from `source` when it is due, and after a failover. */
1791
+ useDirectory(source) {
1792
+ this.source = source;
1793
+ }
1794
+ /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved on. */
1795
+ get nodeUrl() {
1796
+ return this.nodeUrls[this.at];
1797
+ }
1798
+ /** Every node this transport may use, in the order it moves through them. */
1799
+ get nodes() {
1800
+ return [...this.nodeUrls];
1801
+ }
1802
+ /** Authenticate every request with an API key. */
1803
+ useApiKey(key) {
1804
+ this.auth = key;
1805
+ this.current = void 0;
1806
+ }
1807
+ /** Authenticate every request with `session`, refreshed when a node answers `401` to it. */
1808
+ useSession(session) {
1809
+ this.auth = session.sessionToken;
1810
+ this.current = session;
1811
+ }
1812
+ /** Drop the credential: no request after this carries it. */
1813
+ signOut() {
1814
+ this.auth = void 0;
1815
+ this.current = void 0;
1816
+ }
1817
+ get session() {
1818
+ return this.current;
1819
+ }
1820
+ /** The sealed channel to `url`, attested under the policy before its first request. */
1821
+ channel(url2) {
1822
+ let sealed = this.channels.get(url2);
1823
+ if (!sealed) {
1824
+ sealed = createSealedFetch({
1825
+ baseUrl: url2,
1826
+ transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
1827
+ fetch: this.raw
1828
+ });
1829
+ this.channels.set(url2, sealed);
1830
+ }
1831
+ return sealed;
1832
+ }
1833
+ /**
1834
+ * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
1835
+ * next one. False when there is no other. A request that saw the same node
1836
+ * refuse after another request moved on already does not move again.
1837
+ */
1838
+ moveOn(from) {
1839
+ if (this.nodeUrls.length < 2) return false;
1840
+ if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
1841
+ return true;
1842
+ }
1843
+ /**
1844
+ * Fail over from node `from`, where a request's retries ran out, to the next
1845
+ * node it has not `tried`. False when it has tried them all. A request whose
1846
+ * node another request moved away from already goes to that node, if untried.
1847
+ */
1848
+ failOver(from, tried) {
1849
+ if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
1850
+ const count = this.nodeUrls.length;
1851
+ for (let step = 1; step < count; step++) {
1852
+ const next = (this.at + step) % count;
1853
+ if (!tried.has(this.nodeUrls[next])) {
1854
+ this.at = next;
1855
+ this.reread();
1856
+ return true;
1857
+ }
1858
+ }
1859
+ return false;
1860
+ }
1861
+ /**
1862
+ * Read the node list again from the directory, in the background: the node
1863
+ * in use stays first, then the directory's order; a node no longer listed is
1864
+ * dropped. A failed read is ignored. One read at a time.
1865
+ */
1866
+ reread() {
1867
+ const source = this.source;
1868
+ if (!source || source.reading) return;
1869
+ source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
1870
+ const current = this.nodeUrl;
1871
+ const listed = orderNodes(listing.nodes, source.zone, source.random);
1872
+ this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
1873
+ this.at = 0;
1874
+ source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
1875
+ }).catch(() => void 0).finally(() => {
1876
+ source.reading = void 0;
1877
+ });
1878
+ }
1879
+ /**
1880
+ * Send one request to the node in use, with the credential held now, and
1881
+ * answer the response, whatever its status. {@link send} judges it.
1882
+ */
1883
+ async once(method, path, options) {
1884
+ const headers = {};
1885
+ if (this.auth) headers.authorization = `Bearer ${this.auth}`;
1886
+ let payload;
1887
+ if (options.bytes) {
1888
+ headers["content-type"] = options.contentType ?? "application/wasm";
1889
+ payload = options.bytes;
1890
+ } else if (options.body !== void 0) {
1891
+ headers["content-type"] = "application/json";
1892
+ payload = JSON.stringify(options.body);
1893
+ }
1894
+ if (this.source && Date.now() >= this.source.due) this.reread();
1895
+ const url2 = this.nodeUrl;
1896
+ try {
1897
+ return await this.channel(url2)(`${url2}/${path}${queryString(options.query)}`, {
1898
+ method,
1899
+ headers,
1900
+ body: payload,
1901
+ signal: options.signal
1902
+ });
1903
+ } catch (error) {
1904
+ throw sealedError(error);
1905
+ }
1906
+ }
1907
+ /**
1908
+ * {@link once}, answering what a lost issuing node or a node on the wrong
1909
+ * release does to a session: a successful response, or the refusal as a
1910
+ * typed error ({@link errorFrom}).
1911
+ *
1912
+ * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
1913
+ * signed the session may be gone, replaced, or out of reach of the node
1914
+ * asked, which any node of the cluster answers with a refresh. The
1915
+ * transport refreshes once, shared by every request that needs it at the
1916
+ * same time ({@link refresh}), and repeats the request once. A refresh that
1917
+ * is refused surfaces as {@link LoginRequired}.
1918
+ * - `421 WRONG_NODE`: this node's release may not serve the account. The
1919
+ * transport moves to the next node it knows and repeats the request once;
1920
+ * with none, the {@link WrongNode} reaches the caller.
1921
+ *
1922
+ * Repeating is safe even for a request that is not idempotent: the node
1923
+ * answers `401` and `421` when it judges the credential, before it reads what
1924
+ * the request asks, so the first attempt ran nothing. The exceptions are the
1925
+ * ceremonies that mint a session ({@link ceremony}), never repeated here.
1926
+ */
1927
+ async send(method, path, options) {
1928
+ let refreshed = false;
1929
+ let moved = false;
1930
+ for (; ; ) {
1931
+ const [token, at] = [this.auth, this.nodeUrl];
1932
+ const response = await this.once(method, path, options);
1933
+ if (response.ok) return response;
1934
+ const error = await refusal(response);
1935
+ if (error instanceof WrongNode && !moved) {
1936
+ moved = true;
1937
+ if (this.moveOn(at) && !ceremony(path)) continue;
1938
+ } else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
1939
+ refreshed = true;
1940
+ await this.renew(token);
1941
+ continue;
1942
+ }
1943
+ throw error;
1944
+ }
1945
+ }
1946
+ /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
1947
+ mayRefresh(token, path) {
1948
+ return token !== void 0 && this.current !== void 0 && !ceremony(path);
1949
+ }
1950
+ /**
1951
+ * A fresh session in place of the one `token` belonged to: by the refresh
1952
+ * another request already made, or by one this request starts.
1953
+ */
1954
+ async renew(token) {
1955
+ if (this.auth !== token) return;
1956
+ await this.refresh();
1957
+ }
1958
+ async read(method, path, options) {
1959
+ const response = await this.send(method, path, options);
1960
+ const text = await response.text();
1961
+ return text ? JSON.parse(text) : {};
1962
+ }
1963
+ /**
1964
+ * {@link read} for a call or a view (D70): while the node answers 503 or
1965
+ * cannot be reached, the same request, with the same idempotency key, is sent
1966
+ * again after {@link retryDelayMs} (or the pause the node asked for, if
1967
+ * longer), at most `retries` times. Every other answer is final.
1968
+ *
1969
+ * When a node's retries run out, the request fails over to the next node the
1970
+ * transport knows and its retries start again there, with the same key: the
1971
+ * key is kept in the app's replicated log, so the cluster answers it once
1972
+ * whichever node it reaches. Each node is tried once per request. The last
1973
+ * failure surfaces as {@link UnavailableError}: the node's 503, or reason
1974
+ * `network` for a connection that failed, whose `committed` is `'unknown'`.
1975
+ *
1976
+ * `once` sends the request exactly once, to the node in use (a confirmed
1977
+ * attempt, whose challenge is single-use).
1978
+ */
1979
+ async retried(method, path, options, once) {
1980
+ const retries = once ? 0 : this.retry.retries;
1981
+ const tried = /* @__PURE__ */ new Set();
1982
+ for (; ; ) {
1983
+ let unavailable;
1984
+ let node = this.nodeUrl;
1985
+ for (let attempt = 0; ; attempt++) {
1986
+ node = this.nodeUrl;
1987
+ tried.add(node);
1988
+ try {
1989
+ return await this.read(method, path, options);
1990
+ } catch (error) {
1991
+ if (error instanceof UnavailableError) unavailable = error;
1992
+ else if (error instanceof TypeError && this.networkFailures.has(error)) {
1993
+ unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
1994
+ } else throw error;
1995
+ }
1996
+ if (attempt >= retries) break;
1997
+ const backoff = retryDelayMs(attempt, this.retry);
1998
+ await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
1999
+ }
2000
+ if (once || !this.failOver(node, tried)) throw unavailable;
2001
+ }
2002
+ }
2003
+ /**
2004
+ * One JSON request, retried as `options.retry` says. The confirmed attempt
2005
+ * of a step-up (`body.confirm` set) goes out exactly once.
2006
+ */
2007
+ attempt(method, path, options) {
2008
+ if (!options.retry) return this.read(method, path, options);
2009
+ const confirming = options.body?.confirm !== void 0;
2010
+ return this.retried(method, path, options, options.retry === "once" || confirming);
2011
+ }
2012
+ /**
2013
+ * {@link attempt}, answering a `412 CONFIRM_REQUIRED` with `passkey`'s
2014
+ * assertion over the node's challenge: the same body again, with `confirm`.
2015
+ * Without a passkey the `412` reaches the caller as {@link ConfirmRequired}.
2016
+ */
2017
+ async confirmed(method, path, options) {
2018
+ const { passkey } = options;
2019
+ const signal = options.promptSignal ?? options.signal;
2020
+ try {
2021
+ return await this.attempt(method, path, options);
2022
+ } catch (error) {
2023
+ if (!(error instanceof ConfirmRequired) || !passkey) throw error;
2024
+ const d = error.details;
2025
+ let assertion;
2026
+ try {
2027
+ assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
2028
+ userVerification: userVerificationOf(d.userVerification, options.userVerification ?? "required"),
2029
+ ...signal ? { signal } : {}
2030
+ });
2031
+ } catch (declined) {
2032
+ throw promptFailure(declined);
2033
+ }
2034
+ const body = { ...options.body, confirm: { challengeId: d.challengeId, assertion } };
2035
+ return this.attempt(method, path, { ...options, body });
2036
+ }
2037
+ }
2038
+ request(method, path, options = {}) {
2039
+ return this.send(method, relative(path), options);
2040
+ }
2041
+ json(method, path, options = {}) {
2042
+ return this.confirmed(method, relative(path), options);
2043
+ }
2044
+ async *events(path, options = {}) {
2045
+ const response = await this.send("GET", relative(path), options);
2046
+ if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
2047
+ yield* sseFrames(response.body);
2048
+ }
2049
+ /**
2050
+ * `GET path` in the clear, from the node in use: only for the route a node
2051
+ * serves unsealed (`attestation`), and never with the credential.
2052
+ */
2053
+ async unsealed(path, options = {}) {
2054
+ const response = await this.raw(`${this.nodeUrl}/${relative(path)}${queryString(options.query)}`);
2055
+ if (!response.ok) throw await refusal(response);
2056
+ return response;
2057
+ }
2058
+ /**
2059
+ * Swap the refresh token for a new session, on whichever node the transport
2060
+ * uses: any node of the cluster takes it. A refresh token is used once, so
2061
+ * requests made while one refresh is under way share it rather than sending
2062
+ * the same token again (which would revoke the session).
2063
+ */
2064
+ refresh() {
2065
+ if (!this.refreshing) {
2066
+ const pending = this.exchangeRefresh();
2067
+ this.refreshing = pending;
2068
+ const clear = () => {
2069
+ if (this.refreshing === pending) this.refreshing = void 0;
2070
+ };
2071
+ pending.then(clear, clear);
2072
+ }
2073
+ return this.refreshing;
2074
+ }
2075
+ async exchangeRefresh() {
2076
+ const current = this.current;
2077
+ if (!current) throw new Error("no session to refresh");
2078
+ let session;
2079
+ for (let tried = 1; session === void 0; tried++) {
2080
+ try {
2081
+ session = await this.json("POST", "auth/refresh", { body: { refreshToken: current.refreshToken } });
2082
+ } catch (error) {
2083
+ if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
2084
+ }
2085
+ }
2086
+ if (this.current === current) this.useSession(session);
2087
+ return session;
2088
+ }
2089
+ };
2090
+ async function directoryTransport(options) {
2091
+ const random = options.random ?? Math.random;
2092
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
2093
+ const listing = await readDirectory(options.directory, options.cluster, fetcher);
2094
+ const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
2095
+ const transport = new NodeTransport({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
2096
+ transport.useDirectory({
2097
+ directory: options.directory,
2098
+ cluster: options.cluster,
2099
+ zone: options.zone,
2100
+ random,
2101
+ due: Date.now() + listing.refreshAfterSeconds * 1e3
2102
+ });
2103
+ return transport;
2104
+ }
2105
+
1662
2106
  // src/cloud-v2/recovery.ts
1663
2107
  var RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
1664
2108
  var MAX_RECOVERY_NONCE_BYTES = 64;
@@ -1808,12 +2252,16 @@ function pendingApprovalsFrom(report, rules, now = Date.now() / 1e3) {
1808
2252
  for (const a of report.apps) {
1809
2253
  const available = a.availableVersions ?? [];
1810
2254
  if (a.package === null || available.length === 0) continue;
2255
+ const latest = available[available.length - 1];
2256
+ const reachable = a.latestReachable === void 0 ? latest : a.latestReachable;
1811
2257
  upgrades.push({
1812
2258
  app: a.app,
1813
2259
  name: a.name ?? null,
1814
2260
  package: a.package.package,
1815
2261
  from: a.package.version,
1816
- to: available[available.length - 1],
2262
+ to: reachable ?? latest,
2263
+ reachable: reachable !== null,
2264
+ blocked: a.latestBlocked ?? null,
1817
2265
  available,
1818
2266
  actionable: upgradesActionable
1819
2267
  });
@@ -1843,114 +2291,36 @@ function upgradeConsent(app, toVersion, returnTo) {
1843
2291
  return { kind: "upgrade", subject: { app, toVersion }, returnTo };
1844
2292
  }
1845
2293
 
1846
- // src/cloud-v2/client.ts
1847
- function deployerKey(deployer) {
1848
- return "account" in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
1849
- }
1850
- async function frontendManifest(origin, fetcher = fetch) {
1851
- const base = origin ?? globalThis.location?.origin;
1852
- if (!base) throw new Error("no page origin to read the frontend manifest from; pass `origin`");
1853
- const response = await fetcher(new URL("/.well-known/calimero.json", base).toString());
1854
- if (!response.ok) throw await refusal(response);
1855
- const manifest = await response.json();
1856
- return { ...manifest, deployments: manifest.deployments ?? [] };
1857
- }
1858
- function pageDeployment(href) {
1859
- const at = href ?? globalThis.location?.href;
1860
- if (!at) return null;
1861
- try {
1862
- return new URL(at).searchParams.get("deployment") || null;
1863
- } catch {
1864
- return null;
1865
- }
1866
- }
1867
- function userVerificationOf(named, fallback) {
1868
- return named === "required" || named === "preferred" || named === "discouraged" ? named : fallback;
1869
- }
1870
- function promptFailure(error) {
1871
- const e = error;
1872
- const name = e?.name;
1873
- const message = typeof e?.message === "string" ? e.message : "";
1874
- if (name === "InvalidStateError" || /not focused|already pending/i.test(message)) {
1875
- return new ConfirmPromptBlocked(error);
1876
- }
1877
- if (name === "NotAllowedError" || name === "AbortError") return new ConfirmCancelled(error);
1878
- return error;
1879
- }
1880
- function seg(value) {
1881
- return encodeURIComponent(value);
1882
- }
1883
- function sealedError(error) {
1884
- if (error instanceof SealedTransportError && error.status === 413) {
1885
- return errorFrom(413, {
1886
- error: "TOO_LARGE",
1887
- message: error.message.replace(/^Sealed request refused: /, "")
1888
- });
1889
- }
1890
- return error;
1891
- }
1892
- async function refusal(response) {
1893
- const text = await response.text().catch(() => "");
1894
- let body = {};
1895
- try {
1896
- body = text ? JSON.parse(text) : {};
1897
- } catch {
1898
- body = { message: text };
1899
- }
1900
- return errorFrom(response.status, body, response.headers.get("retry-after"));
1901
- }
1902
- async function* sseFrames(body) {
1903
- const reader = body.getReader();
1904
- const decoder = new TextDecoder();
1905
- let buffer = "";
1906
- for (; ; ) {
1907
- const { value, done } = await reader.read();
1908
- if (done) return;
1909
- buffer += decoder.decode(value, { stream: true });
1910
- let cut;
1911
- while ((cut = buffer.indexOf("\n\n")) >= 0) {
1912
- const frame = buffer.slice(0, cut);
1913
- buffer = buffer.slice(cut + 2);
1914
- let id = "";
1915
- let kind = "";
1916
- let data = "";
1917
- for (const line of frame.split("\n")) {
1918
- if (line.startsWith("id:")) id = line.slice(3).trim();
1919
- else if (line.startsWith("event:")) kind = line.slice(6).trim();
1920
- else if (line.startsWith("data:")) data += line.slice(5).trim();
1921
- }
1922
- if (!kind && !data) continue;
1923
- let parsed = data;
1924
- try {
1925
- parsed = JSON.parse(data);
1926
- } catch {
1927
- }
1928
- yield { id, kind, data: parsed };
1929
- }
1930
- }
2294
+ // src/cloud-v2/client.ts
2295
+ function deployerKey(deployer) {
2296
+ return "account" in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
1931
2297
  }
1932
- function toHex(bytes) {
1933
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2298
+ async function frontendManifest(origin, fetcher = fetch) {
2299
+ const base = origin ?? globalThis.location?.origin;
2300
+ if (!base) throw new Error("no page origin to read the frontend manifest from; pass `origin`");
2301
+ const response = await fetcher(new URL("/.well-known/calimero.json", base).toString());
2302
+ if (!response.ok) throw await refusal(response);
2303
+ const manifest = await response.json();
2304
+ return { ...manifest, deployments: manifest.deployments ?? [] };
1934
2305
  }
1935
- function newIdempotencyKey() {
1936
- return `node:c-${b64url(crypto.getRandomValues(new Uint8Array(16)))}`;
2306
+ function pageDeployment(href) {
2307
+ const at = href ?? globalThis.location?.href;
2308
+ if (!at) return null;
2309
+ try {
2310
+ return new URL(at).searchParams.get("deployment") || null;
2311
+ } catch {
2312
+ return null;
2313
+ }
1937
2314
  }
1938
- var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
1939
- function retryDelayMs(retry, options) {
1940
- return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
2315
+ function seg(value) {
2316
+ return encodeURIComponent(value);
1941
2317
  }
1942
- function ceremony(path) {
1943
- return path === "auth/refresh" || path.startsWith("auth/passkey/") || RECOVERY_CEREMONIES.has(path);
2318
+ function toHex(bytes) {
2319
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1944
2320
  }
1945
- var RECOVERY_CEREMONIES = /* @__PURE__ */ new Set(["auth/recovery/begin", "auth/recovery/start", "auth/recovery/finish"]);
1946
2321
  var CalimeroClient = class _CalimeroClient {
2322
+ /** A client of the nodes `options` names, or one sending through an existing `transport`. */
1947
2323
  constructor(options) {
1948
- /** The one in use: an index into {@link nodeUrls}. */
1949
- this.at = 0;
1950
- /** A sealed fetch per node, each attesting its own node on first use. */
1951
- this.channels = /* @__PURE__ */ new Map();
1952
- /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
1953
- this.networkFailures = /* @__PURE__ */ new WeakSet();
1954
2324
  // ---------------------------------------------------------------- package origins
1955
2325
  /**
1956
2326
  * The web origins `{package}`'s managers registered so a frontend they host
@@ -1976,20 +2346,7 @@ var CalimeroClient = class _CalimeroClient {
1976
2346
  ),
1977
2347
  remove: (pkg, origin, passkey) => this.confirmed("DELETE", `package-origins/${seg(pkg)}`, { origin }, passkey)
1978
2348
  };
1979
- const trim = (url2) => url2.replace(/\/+$/, "");
1980
- this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
1981
- this.policy = options.policy;
1982
- this.retry = { ...DEFAULT_RETRY, ...options.retry };
1983
- const given = options.fetch ?? ((input, init) => fetch(input, init));
1984
- const raw = async (input, init) => {
1985
- try {
1986
- return await given(input, init);
1987
- } catch (error) {
1988
- if (error instanceof TypeError) this.networkFailures.add(error);
1989
- throw error;
1990
- }
1991
- };
1992
- this.raw = raw;
2349
+ this.transport = options instanceof NodeTransport ? options : new NodeTransport(options);
1993
2350
  }
1994
2351
  /**
1995
2352
  * A client for the Calimero Cloud cluster named `cluster`: its node list read
@@ -2002,219 +2359,31 @@ var CalimeroClient = class _CalimeroClient {
2002
2359
  * first request once the directory's `refreshAfterSeconds` has passed.
2003
2360
  */
2004
2361
  static async connect(options) {
2005
- const directory = options.directory ?? DEFAULT_DIRECTORY;
2006
- const random = options.random ?? Math.random;
2007
- const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
2008
- const listing = await readDirectory(directory, options.cluster, fetcher);
2009
- const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
2010
- const client = new _CalimeroClient({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
2011
- client.source = {
2012
- directory,
2013
- cluster: options.cluster,
2014
- zone: options.zone,
2015
- random,
2016
- due: Date.now() + listing.refreshAfterSeconds * 1e3
2017
- };
2018
- return client;
2362
+ const transport = await directoryTransport({ ...options, directory: options.directory ?? DEFAULT_DIRECTORY });
2363
+ return new _CalimeroClient(transport);
2019
2364
  }
2020
2365
  /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved the client on. */
2021
2366
  get nodeUrl() {
2022
- return this.nodeUrls[this.at];
2367
+ return this.transport.nodeUrl;
2023
2368
  }
2024
2369
  /** Every node this client may use, in the order it moves through them. */
2025
2370
  get nodes() {
2026
- return [...this.nodeUrls];
2027
- }
2028
- /** The sealed channel to `url`, attested under the client's policy before its first request. */
2029
- channel(url2) {
2030
- let sealed = this.channels.get(url2);
2031
- if (!sealed) {
2032
- sealed = createSealedFetch({
2033
- baseUrl: url2,
2034
- transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
2035
- fetch: this.raw
2036
- });
2037
- this.channels.set(url2, sealed);
2038
- }
2039
- return sealed;
2040
- }
2041
- /**
2042
- * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
2043
- * next one. False when there is no other. A request that saw the same node
2044
- * refuse after another request moved on already does not move again.
2045
- */
2046
- moveOn(from) {
2047
- if (this.nodeUrls.length < 2) return false;
2048
- if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
2049
- return true;
2050
- }
2051
- /**
2052
- * Fail over from node `from`, where a request's retries ran out, to the next
2053
- * node it has not `tried`. False when it has tried them all. A request whose
2054
- * node another request moved away from already goes to that node, if untried.
2055
- */
2056
- failOver(from, tried) {
2057
- if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
2058
- const count = this.nodeUrls.length;
2059
- for (let step = 1; step < count; step++) {
2060
- const next = (this.at + step) % count;
2061
- if (!tried.has(this.nodeUrls[next])) {
2062
- this.at = next;
2063
- this.reread();
2064
- return true;
2065
- }
2066
- }
2067
- return false;
2068
- }
2069
- /**
2070
- * Read the node list again from the directory, in the background, for a
2071
- * client made by {@link connect}: the node in use stays first, then the
2072
- * directory's order; a node no longer listed is dropped. A failed read is
2073
- * ignored. One read at a time.
2074
- */
2075
- reread() {
2076
- const source = this.source;
2077
- if (!source || source.reading) return;
2078
- source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
2079
- const current = this.nodeUrl;
2080
- const listed = orderNodes(listing.nodes, source.zone, source.random);
2081
- this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
2082
- this.at = 0;
2083
- source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
2084
- }).catch(() => void 0).finally(() => {
2085
- source.reading = void 0;
2086
- });
2371
+ return this.transport.nodes;
2087
2372
  }
2088
2373
  /** Authenticate every request with an API key. */
2089
2374
  useApiKey(key) {
2090
- this.auth = key;
2091
- this.current = void 0;
2375
+ this.transport.useApiKey(key);
2092
2376
  }
2093
2377
  useSession(session) {
2094
- this.auth = session.sessionToken;
2095
- this.current = session;
2378
+ this.transport.useSession(session);
2096
2379
  }
2097
2380
  get session() {
2098
- return this.current;
2099
- }
2100
- /**
2101
- * Send one request to the node in use, with the credential held now, and
2102
- * answer the response, whatever its status. {@link send} judges it.
2103
- */
2104
- async request(method, path, body, raw, contentType = "application/wasm", signal) {
2105
- const headers = {};
2106
- if (this.auth) headers.authorization = `Bearer ${this.auth}`;
2107
- let payload;
2108
- if (raw) {
2109
- headers["content-type"] = contentType;
2110
- payload = raw;
2111
- } else if (body !== void 0) {
2112
- headers["content-type"] = "application/json";
2113
- payload = JSON.stringify(body);
2114
- }
2115
- if (this.source && Date.now() >= this.source.due) this.reread();
2116
- const url2 = this.nodeUrl;
2117
- try {
2118
- return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
2119
- } catch (error) {
2120
- throw sealedError(error);
2121
- }
2122
- }
2123
- /**
2124
- * {@link request}, answering what a lost issuing node or a node on the
2125
- * wrong release does to a session: a successful response, or the refusal as a
2126
- * typed error ({@link errorFrom}).
2127
- *
2128
- * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
2129
- * signed the session may be gone, replaced, or out of reach of the node
2130
- * asked, which any node of the cluster answers with a refresh. The client
2131
- * refreshes once, shared by every request that needs it at the same time
2132
- * ({@link refresh}), and repeats the request once. A refresh that is
2133
- * refused surfaces as {@link LoginRequired}.
2134
- * - `421 WRONG_NODE`: this node's release may not serve the account. The
2135
- * client moves to the next node it knows and repeats the request once; with
2136
- * none, the {@link WrongNode} reaches the caller.
2137
- *
2138
- * Repeating is safe even for a request that is not idempotent: the node
2139
- * answers `401` and `421` when it judges the credential, before it reads what
2140
- * the request asks, so the first attempt ran nothing. The exceptions are the
2141
- * ceremonies that mint a session ({@link ceremony}), never repeated here.
2142
- */
2143
- async send(method, path, body, raw, contentType, signal) {
2144
- let refreshed = false;
2145
- let moved = false;
2146
- for (; ; ) {
2147
- const [token, at] = [this.auth, this.nodeUrl];
2148
- const response = await this.request(method, path, body, raw, contentType, signal);
2149
- if (response.ok) return response;
2150
- const error = await refusal(response);
2151
- if (error instanceof WrongNode && !moved) {
2152
- moved = true;
2153
- if (this.moveOn(at) && !ceremony(path)) continue;
2154
- } else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
2155
- refreshed = true;
2156
- await this.renew(token);
2157
- continue;
2158
- }
2159
- throw error;
2160
- }
2381
+ return this.transport.session;
2161
2382
  }
2162
- /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
2163
- mayRefresh(token, path) {
2164
- return token !== void 0 && this.current !== void 0 && !ceremony(path);
2165
- }
2166
- /**
2167
- * A fresh session in place of the one `token` belonged to: by the refresh
2168
- * another request already made, or by one this request starts.
2169
- */
2170
- async renew(token) {
2171
- if (this.auth !== token) return;
2172
- await this.refresh();
2173
- }
2174
- async json(method, path, body, raw, contentType) {
2175
- const response = await this.send(method, path, body, raw, contentType);
2176
- const text = await response.text();
2177
- return text ? JSON.parse(text) : {};
2178
- }
2179
- /**
2180
- * {@link json} for a call or a view (D70): while the node answers 503 or
2181
- * cannot be reached, the same request, with the same idempotency key, is sent
2182
- * again after {@link retryDelayMs} (or the pause the node asked for, if
2183
- * longer), at most `retries` times. Every other answer is final.
2184
- *
2185
- * When a node's retries run out, the request fails over to the next node the
2186
- * client knows and its retries start again there, with the same key: the
2187
- * key is kept in the app's replicated log, so the cluster answers it once
2188
- * whichever node it reaches. Each node is tried once per request. The last
2189
- * failure surfaces as {@link UnavailableError}: the node's 503, or reason
2190
- * `network` for a connection that failed, whose `committed` is `'unknown'`.
2191
- *
2192
- * `once` sends the request exactly once, to the node in use (the confirmed
2193
- * attempt of {@link callConfirmed}, whose challenge is single-use).
2194
- */
2195
- async retried(method, path, body, once = false) {
2196
- const retries = once ? 0 : this.retry.retries;
2197
- const tried = /* @__PURE__ */ new Set();
2198
- for (; ; ) {
2199
- let unavailable;
2200
- let node = this.nodeUrl;
2201
- for (let attempt = 0; ; attempt++) {
2202
- node = this.nodeUrl;
2203
- tried.add(node);
2204
- try {
2205
- return await this.json(method, path, body);
2206
- } catch (error) {
2207
- if (error instanceof UnavailableError) unavailable = error;
2208
- else if (error instanceof TypeError && this.networkFailures.has(error)) {
2209
- unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
2210
- } else throw error;
2211
- }
2212
- if (attempt >= retries) break;
2213
- const backoff = retryDelayMs(attempt, this.retry);
2214
- await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
2215
- }
2216
- if (once || !this.failOver(node, tried)) throw unavailable;
2217
- }
2383
+ /** A JSON request, or a raw one when `raw` is given (as `contentType`, the transport's default when absent). */
2384
+ json(method, path, body, raw, contentType) {
2385
+ const options = raw ? { bytes: raw, ...contentType === void 0 ? {} : { contentType } } : { body };
2386
+ return this.transport.json(method, path, options);
2218
2387
  }
2219
2388
  post(path, body = {}) {
2220
2389
  return this.json("POST", path, body);
@@ -2258,7 +2427,6 @@ var CalimeroClient = class _CalimeroClient {
2258
2427
  "auth/passkey/register/finish",
2259
2428
  { challengeId: begin.challengeId, registration, ...invite },
2260
2429
  passkey,
2261
- void 0,
2262
2430
  "preferred",
2263
2431
  signal
2264
2432
  );
@@ -2299,7 +2467,7 @@ var CalimeroClient = class _CalimeroClient {
2299
2467
  const page = globalThis.location?.origin;
2300
2468
  if (!page || page === "null") return void 0;
2301
2469
  const nodes = new Set(
2302
- this.nodeUrls.map((url2) => {
2470
+ this.transport.nodes.map((url2) => {
2303
2471
  try {
2304
2472
  return new URL(url2).origin;
2305
2473
  } catch {
@@ -2327,29 +2495,7 @@ var CalimeroClient = class _CalimeroClient {
2327
2495
  * same token again (which would revoke the session).
2328
2496
  */
2329
2497
  refresh() {
2330
- if (!this.refreshing) {
2331
- const pending = this.exchangeRefresh();
2332
- this.refreshing = pending;
2333
- const clear = () => {
2334
- if (this.refreshing === pending) this.refreshing = void 0;
2335
- };
2336
- pending.then(clear, clear);
2337
- }
2338
- return this.refreshing;
2339
- }
2340
- async exchangeRefresh() {
2341
- const current = this.current;
2342
- if (!current) throw new Error("no session to refresh");
2343
- let session;
2344
- for (let tried = 1; session === void 0; tried++) {
2345
- try {
2346
- session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
2347
- } catch (error) {
2348
- if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
2349
- }
2350
- }
2351
- if (this.current === current) this.useSession(session);
2352
- return session;
2498
+ return this.transport.refresh();
2353
2499
  }
2354
2500
  /**
2355
2501
  * End this session. The client is signed out whether or not the node
@@ -2409,8 +2555,7 @@ var CalimeroClient = class _CalimeroClient {
2409
2555
  try {
2410
2556
  await this.post(path);
2411
2557
  } finally {
2412
- this.auth = void 0;
2413
- this.current = void 0;
2558
+ this.transport.signOut();
2414
2559
  }
2415
2560
  }
2416
2561
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
@@ -2516,8 +2661,7 @@ var CalimeroClient = class _CalimeroClient {
2516
2661
  */
2517
2662
  async attestation() {
2518
2663
  const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
2519
- const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
2520
- if (!response.ok) throw await refusal(response);
2664
+ const response = await this.transport.unsealed("attestation", { query: { nonce: nonce2 } });
2521
2665
  return await response.json();
2522
2666
  }
2523
2667
  /** The account's API keys (never the keys themselves). */
@@ -2578,8 +2722,9 @@ var CalimeroClient = class _CalimeroClient {
2578
2722
  * app; `application/octet-stream` otherwise.
2579
2723
  */
2580
2724
  async blobContent(app, id, options = {}) {
2581
- const query = options.grant === void 0 ? "" : `?grant=${encodeURIComponent(options.grant)}`;
2582
- const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}${query}`);
2725
+ const response = await this.transport.request("GET", `apps/${seg(app)}/blobs/${seg(id)}`, {
2726
+ query: { grant: options.grant }
2727
+ });
2583
2728
  return {
2584
2729
  data: new Uint8Array(await response.arrayBuffer()),
2585
2730
  contentType: response.headers.get("content-type") ?? "application/octet-stream"
@@ -2690,8 +2835,10 @@ var CalimeroClient = class _CalimeroClient {
2690
2835
  }
2691
2836
  /**
2692
2837
  * Move an app to another release of its package (`{ version }`), another
2693
- * package, or a bundle hash, running its migration. Answers the upgrade's
2694
- * entry; a failed migration rejects with code `UPGRADE_FAILED` (422).
2838
+ * package, or a bundle hash. An upgrade is code only: the app moves to any
2839
+ * newer release in one step. Answers the upgrade's entry; one the app's
2840
+ * group refuses rejects with code `UPGRADE_FAILED` (422), and one a
2841
+ * replica's node is too old to run with {@link RuntimeTooOld} (409).
2695
2842
  * Where owners hold app upgrades, the node asks for the approval of
2696
2843
  * whoever the package's manifest names (`412 CONFIRM_REQUIRED`), which
2697
2844
  * `passkey` gives. Refused (`403`) to a session opened on an app's
@@ -2722,23 +2869,12 @@ var CalimeroClient = class _CalimeroClient {
2722
2869
  deleteConfirmed(path, passkey, body = {}) {
2723
2870
  return this.confirmed("DELETE", path, body, passkey);
2724
2871
  }
2725
- async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b), userVerification = "required", signal) {
2726
- try {
2727
- return await send(body);
2728
- } catch (error) {
2729
- if (!(error instanceof ConfirmRequired) || !passkey) throw error;
2730
- const d = error.details;
2731
- let assertion;
2732
- try {
2733
- assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
2734
- userVerification: userVerificationOf(d.userVerification, userVerification),
2735
- ...signal ? { signal } : {}
2736
- });
2737
- } catch (declined) {
2738
- throw promptFailure(declined);
2739
- }
2740
- return send({ ...body, confirm: { challengeId: d.challengeId, assertion } });
2741
- }
2872
+ /**
2873
+ * `body` to `path`, answering a `412 CONFIRM_REQUIRED` with `passkey` when one
2874
+ * is given; `signal` aborts only the passkey prompt.
2875
+ */
2876
+ confirmed(method, path, body, passkey, userVerification, signal) {
2877
+ return this.transport.json(method, path, { body, passkey, userVerification, promptSignal: signal });
2742
2878
  }
2743
2879
  /**
2744
2880
  * Create an app instance from a bundle hash, or from a package release
@@ -2762,7 +2898,7 @@ var CalimeroClient = class _CalimeroClient {
2762
2898
  */
2763
2899
  call(app, method, args = {}, idempotencyKey) {
2764
2900
  const key = idempotencyKey ?? newIdempotencyKey();
2765
- return this.retried("POST", `apps/${seg(app)}/intents`, { method, args, idempotencyKey: key });
2901
+ return this.transport.json("POST", `apps/${seg(app)}/intents`, { body: { method, args, idempotencyKey: key }, retry: true });
2766
2902
  }
2767
2903
  /**
2768
2904
  * Call a method that needs the account holder's approval: the node returns a
@@ -2772,32 +2908,23 @@ var CalimeroClient = class _CalimeroClient {
2772
2908
  async callConfirmed(app, method, args, passkey, idempotencyKey) {
2773
2909
  const path = `apps/${seg(app)}/intents`;
2774
2910
  const key = idempotencyKey ?? newIdempotencyKey();
2775
- return this.confirmed(
2776
- "POST",
2777
- path,
2778
- { method, args, idempotencyKey: key },
2779
- passkey,
2780
- (body) => this.retried("POST", path, body, body.confirm !== void 0)
2781
- );
2911
+ return this.transport.json("POST", path, { body: { method, args, idempotencyKey: key }, passkey, retry: true });
2782
2912
  }
2783
2913
  /** Run a read-only method: strong by default, or stale from any replica. */
2784
2914
  view(app, method, args = {}, options = {}) {
2785
- return this.retried("POST", `apps/${seg(app)}/views`, {
2786
- method,
2787
- args,
2788
- consistency: options.stale ? "stale" : "strong",
2789
- minIndex: options.minIndex ?? 0
2915
+ return this.transport.json("POST", `apps/${seg(app)}/views`, {
2916
+ body: {
2917
+ method,
2918
+ args,
2919
+ consistency: options.stale ? "stale" : "strong",
2920
+ minIndex: options.minIndex ?? 0
2921
+ },
2922
+ retry: true
2790
2923
  });
2791
2924
  }
2792
2925
  /** Committed events from index `from`, then live, until `signal` aborts. */
2793
2926
  events(app, from = 0, signal) {
2794
- return this.stream(`apps/${seg(app)}/events?from=${from}`, signal);
2795
- }
2796
- /** A server-sent event stream on `path`, sealed like every other answer. */
2797
- async *stream(path, signal) {
2798
- const response = await this.send("GET", path, void 0, void 0, void 0, signal);
2799
- if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
2800
- yield* sseFrames(response.body);
2927
+ return this.transport.events(`apps/${seg(app)}/events`, { query: { from }, signal });
2801
2928
  }
2802
2929
  // ---------------------------------------------------------------- presence
2803
2930
  /**
@@ -2818,7 +2945,7 @@ var CalimeroClient = class _CalimeroClient {
2818
2945
  * entries present), then a `presence` event (`data` one entry) per change.
2819
2946
  */
2820
2947
  presenceStream(app, signal) {
2821
- return this.stream(`apps/${seg(app)}/presence/stream`, signal);
2948
+ return this.transport.events(`apps/${seg(app)}/presence/stream`, { signal });
2822
2949
  }
2823
2950
  // ---------------------------------------------------------------- aliases
2824
2951
  /** Name an app (its id or context id) for this account. */
@@ -3833,6 +3960,7 @@ export {
3833
3960
  DEFAULT_DIRECTORY,
3834
3961
  DEFAULT_ORG_CAPABILITIES,
3835
3962
  DEFAULT_RELEASE_MIRROR,
3963
+ DEFAULT_RETRY,
3836
3964
  DirectoryError,
3837
3965
  GITHUB_RELEASES,
3838
3966
  HANDSHAKE_PATH,
@@ -3843,6 +3971,7 @@ export {
3843
3971
  MAX_REPORT_PAGE,
3844
3972
  MOCK_QUOTE_HEADER,
3845
3973
  NODE_RELEASE_SIGNER,
3974
+ NodeTransport,
3846
3975
  NotFound,
3847
3976
  NotServing,
3848
3977
  ORG_CAPABILITIES,
@@ -3855,6 +3984,7 @@ export {
3855
3984
  RELEASE_VERIFIER,
3856
3985
  RecoveryNotReady,
3857
3986
  ReleaseDeadlineError,
3987
+ RuntimeTooOld,
3858
3988
  SEALED_CONTENT_TYPE,
3859
3989
  SEALED_PATH,
3860
3990
  SealedTransportError,
@@ -3879,6 +4009,7 @@ export {
3879
4009
  createSealedFetch,
3880
4010
  createSignedReleaseVerifier,
3881
4011
  deployerKey,
4012
+ directoryTransport,
3882
4013
  errorFrom,
3883
4014
  fetchAttestedKey,
3884
4015
  fetchGithubNodeRelease,
@@ -3887,6 +4018,7 @@ export {
3887
4018
  fromB64url,
3888
4019
  frontendManifest,
3889
4020
  generateOwnerKey,
4021
+ newIdempotencyKey,
3890
4022
  newRecoverySeed,
3891
4023
  orgCapabilities,
3892
4024
  ownerPublicKey,
@@ -3898,13 +4030,18 @@ export {
3898
4030
  provisionInfo,
3899
4031
  publisherBindingBytes,
3900
4032
  publisherBindingMessage,
4033
+ queryString,
3901
4034
  recoveryMessage,
3902
4035
  recoveryPublicKey,
3903
4036
  recoverySeedText,
4037
+ refusal,
3904
4038
  releaseAgent,
3905
4039
  releaseConsent,
3906
4040
  releaseNotesUrl,
4041
+ retryDelayMs,
4042
+ routePath,
3907
4043
  signRecovery,
4044
+ sseFrames,
3908
4045
  stalledBy,
3909
4046
  transportKeyBinding,
3910
4047
  trustSignedRelease,