@calimero-network/mero-js 29.15.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.cjs CHANGED
@@ -46,6 +46,7 @@ __export(index_exports, {
46
46
  DEFAULT_DIRECTORY: () => DEFAULT_DIRECTORY,
47
47
  DEFAULT_ORG_CAPABILITIES: () => DEFAULT_ORG_CAPABILITIES,
48
48
  DEFAULT_RELEASE_MIRROR: () => DEFAULT_RELEASE_MIRROR,
49
+ DEFAULT_RETRY: () => DEFAULT_RETRY,
49
50
  DirectoryError: () => DirectoryError,
50
51
  GITHUB_RELEASES: () => GITHUB_RELEASES,
51
52
  HANDSHAKE_PATH: () => HANDSHAKE_PATH,
@@ -56,6 +57,7 @@ __export(index_exports, {
56
57
  MAX_REPORT_PAGE: () => MAX_REPORT_PAGE,
57
58
  MOCK_QUOTE_HEADER: () => MOCK_QUOTE_HEADER,
58
59
  NODE_RELEASE_SIGNER: () => NODE_RELEASE_SIGNER,
60
+ NodeTransport: () => NodeTransport,
59
61
  NotFound: () => NotFound,
60
62
  NotServing: () => NotServing,
61
63
  ORG_CAPABILITIES: () => ORG_CAPABILITIES,
@@ -93,6 +95,7 @@ __export(index_exports, {
93
95
  createSealedFetch: () => createSealedFetch,
94
96
  createSignedReleaseVerifier: () => createSignedReleaseVerifier,
95
97
  deployerKey: () => deployerKey,
98
+ directoryTransport: () => directoryTransport,
96
99
  errorFrom: () => errorFrom,
97
100
  fetchAttestedKey: () => fetchAttestedKey,
98
101
  fetchGithubNodeRelease: () => fetchGithubNodeRelease,
@@ -101,6 +104,7 @@ __export(index_exports, {
101
104
  fromB64url: () => fromB64url,
102
105
  frontendManifest: () => frontendManifest,
103
106
  generateOwnerKey: () => generateOwnerKey,
107
+ newIdempotencyKey: () => newIdempotencyKey,
104
108
  newRecoverySeed: () => newRecoverySeed,
105
109
  orgCapabilities: () => orgCapabilities,
106
110
  ownerPublicKey: () => ownerPublicKey,
@@ -112,13 +116,18 @@ __export(index_exports, {
112
116
  provisionInfo: () => provisionInfo,
113
117
  publisherBindingBytes: () => publisherBindingBytes,
114
118
  publisherBindingMessage: () => publisherBindingMessage,
119
+ queryString: () => queryString,
115
120
  recoveryMessage: () => recoveryMessage,
116
121
  recoveryPublicKey: () => recoveryPublicKey,
117
122
  recoverySeedText: () => recoverySeedText,
123
+ refusal: () => refusal,
118
124
  releaseAgent: () => releaseAgent,
119
125
  releaseConsent: () => releaseConsent,
120
126
  releaseNotesUrl: () => releaseNotesUrl,
127
+ retryDelayMs: () => retryDelayMs,
128
+ routePath: () => routePath,
121
129
  signRecovery: () => signRecovery,
130
+ sseFrames: () => sseFrames,
122
131
  stalledBy: () => stalledBy,
123
132
  transportKeyBinding: () => transportKeyBinding,
124
133
  trustSignedRelease: () => trustSignedRelease,
@@ -1803,6 +1812,440 @@ function browserPasskey() {
1803
1812
  };
1804
1813
  }
1805
1814
 
1815
+ // src/runtime/transport.ts
1816
+ var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
1817
+ function retryDelayMs(retry, options) {
1818
+ return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
1819
+ }
1820
+ function userVerificationOf(named, fallback) {
1821
+ return named === "required" || named === "preferred" || named === "discouraged" ? named : fallback;
1822
+ }
1823
+ function promptFailure(error) {
1824
+ const e = error;
1825
+ const name = e?.name;
1826
+ const message = typeof e?.message === "string" ? e.message : "";
1827
+ if (name === "InvalidStateError" || /not focused|already pending/i.test(message)) {
1828
+ return new ConfirmPromptBlocked(error);
1829
+ }
1830
+ if (name === "NotAllowedError" || name === "AbortError") return new ConfirmCancelled(error);
1831
+ return error;
1832
+ }
1833
+ function sealedError(error) {
1834
+ if (error instanceof SealedTransportError && error.status === 413) {
1835
+ return errorFrom(413, {
1836
+ error: "TOO_LARGE",
1837
+ message: error.message.replace(/^Sealed request refused: /, "")
1838
+ });
1839
+ }
1840
+ return error;
1841
+ }
1842
+ async function refusal(response) {
1843
+ const text = await response.text().catch(() => "");
1844
+ let body = {};
1845
+ try {
1846
+ body = text ? JSON.parse(text) : {};
1847
+ } catch {
1848
+ body = { message: text };
1849
+ }
1850
+ return errorFrom(response.status, body, response.headers.get("retry-after"));
1851
+ }
1852
+ async function* sseFrames(body) {
1853
+ const reader = body.getReader();
1854
+ const decoder = new TextDecoder();
1855
+ let buffer = "";
1856
+ for (; ; ) {
1857
+ const { value, done } = await reader.read();
1858
+ if (done) return;
1859
+ buffer += decoder.decode(value, { stream: true });
1860
+ let cut;
1861
+ while ((cut = buffer.indexOf("\n\n")) >= 0) {
1862
+ const frame = buffer.slice(0, cut);
1863
+ buffer = buffer.slice(cut + 2);
1864
+ let id = "";
1865
+ let kind = "";
1866
+ let data = "";
1867
+ for (const line of frame.split("\n")) {
1868
+ if (line.startsWith("id:")) id = line.slice(3).trim();
1869
+ else if (line.startsWith("event:")) kind = line.slice(6).trim();
1870
+ else if (line.startsWith("data:")) data += line.slice(5).trim();
1871
+ }
1872
+ if (!kind && !data) continue;
1873
+ let parsed = data;
1874
+ try {
1875
+ parsed = JSON.parse(data);
1876
+ } catch {
1877
+ }
1878
+ yield { id, kind, data: parsed };
1879
+ }
1880
+ }
1881
+ }
1882
+ function newIdempotencyKey() {
1883
+ return `node:c-${b64url(crypto.getRandomValues(new Uint8Array(16)))}`;
1884
+ }
1885
+ function routePath(template, params = {}) {
1886
+ return template.replace(/\{([^}]+)\}/g, (_, name) => {
1887
+ const value = params[name];
1888
+ if (value === void 0) throw new Error(`no value for path parameter ${name} of ${template}`);
1889
+ return encodeURIComponent(String(value));
1890
+ });
1891
+ }
1892
+ function queryString(query) {
1893
+ if (!query) return "";
1894
+ const search = new URLSearchParams();
1895
+ for (const [name, value] of Object.entries(query)) {
1896
+ for (const one of Array.isArray(value) ? value : [value]) {
1897
+ if (one !== void 0 && one !== null) search.append(name, String(one));
1898
+ }
1899
+ }
1900
+ const text = search.toString();
1901
+ return text ? `?${text}` : "";
1902
+ }
1903
+ function ceremony(path) {
1904
+ return path === "auth/refresh" || path.startsWith("auth/passkey/") || RECOVERY_CEREMONIES.has(path);
1905
+ }
1906
+ var RECOVERY_CEREMONIES = /* @__PURE__ */ new Set(["auth/recovery/begin", "auth/recovery/start", "auth/recovery/finish"]);
1907
+ function relative(path) {
1908
+ return path.replace(/^\/+/, "");
1909
+ }
1910
+ var NodeTransport = class {
1911
+ constructor(options) {
1912
+ /** The one in use: an index into {@link nodeUrls}. */
1913
+ this.at = 0;
1914
+ /** A sealed fetch per node, each attesting its own node on first use. */
1915
+ this.channels = /* @__PURE__ */ new Map();
1916
+ /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
1917
+ this.networkFailures = /* @__PURE__ */ new WeakSet();
1918
+ const trim = (url2) => url2.replace(/\/+$/, "");
1919
+ this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
1920
+ this.policy = options.policy;
1921
+ this.retry = { ...DEFAULT_RETRY, ...options.retry };
1922
+ const given = options.fetch ?? ((input, init) => fetch(input, init));
1923
+ const raw = async (input, init) => {
1924
+ try {
1925
+ return await given(input, init);
1926
+ } catch (error) {
1927
+ if (error instanceof TypeError) this.networkFailures.add(error);
1928
+ throw error;
1929
+ }
1930
+ };
1931
+ this.raw = raw;
1932
+ }
1933
+ /** Re-read the node list from `source` when it is due, and after a failover. */
1934
+ useDirectory(source) {
1935
+ this.source = source;
1936
+ }
1937
+ /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved on. */
1938
+ get nodeUrl() {
1939
+ return this.nodeUrls[this.at];
1940
+ }
1941
+ /** Every node this transport may use, in the order it moves through them. */
1942
+ get nodes() {
1943
+ return [...this.nodeUrls];
1944
+ }
1945
+ /** Authenticate every request with an API key. */
1946
+ useApiKey(key) {
1947
+ this.auth = key;
1948
+ this.current = void 0;
1949
+ }
1950
+ /** Authenticate every request with `session`, refreshed when a node answers `401` to it. */
1951
+ useSession(session) {
1952
+ this.auth = session.sessionToken;
1953
+ this.current = session;
1954
+ }
1955
+ /** Drop the credential: no request after this carries it. */
1956
+ signOut() {
1957
+ this.auth = void 0;
1958
+ this.current = void 0;
1959
+ }
1960
+ get session() {
1961
+ return this.current;
1962
+ }
1963
+ /** The sealed channel to `url`, attested under the policy before its first request. */
1964
+ channel(url2) {
1965
+ let sealed = this.channels.get(url2);
1966
+ if (!sealed) {
1967
+ sealed = createSealedFetch({
1968
+ baseUrl: url2,
1969
+ transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
1970
+ fetch: this.raw
1971
+ });
1972
+ this.channels.set(url2, sealed);
1973
+ }
1974
+ return sealed;
1975
+ }
1976
+ /**
1977
+ * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
1978
+ * next one. False when there is no other. A request that saw the same node
1979
+ * refuse after another request moved on already does not move again.
1980
+ */
1981
+ moveOn(from) {
1982
+ if (this.nodeUrls.length < 2) return false;
1983
+ if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
1984
+ return true;
1985
+ }
1986
+ /**
1987
+ * Fail over from node `from`, where a request's retries ran out, to the next
1988
+ * node it has not `tried`. False when it has tried them all. A request whose
1989
+ * node another request moved away from already goes to that node, if untried.
1990
+ */
1991
+ failOver(from, tried) {
1992
+ if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
1993
+ const count = this.nodeUrls.length;
1994
+ for (let step = 1; step < count; step++) {
1995
+ const next = (this.at + step) % count;
1996
+ if (!tried.has(this.nodeUrls[next])) {
1997
+ this.at = next;
1998
+ this.reread();
1999
+ return true;
2000
+ }
2001
+ }
2002
+ return false;
2003
+ }
2004
+ /**
2005
+ * Read the node list again from the directory, in the background: the node
2006
+ * in use stays first, then the directory's order; a node no longer listed is
2007
+ * dropped. A failed read is ignored. One read at a time.
2008
+ */
2009
+ reread() {
2010
+ const source = this.source;
2011
+ if (!source || source.reading) return;
2012
+ source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
2013
+ const current = this.nodeUrl;
2014
+ const listed = orderNodes(listing.nodes, source.zone, source.random);
2015
+ this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
2016
+ this.at = 0;
2017
+ source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
2018
+ }).catch(() => void 0).finally(() => {
2019
+ source.reading = void 0;
2020
+ });
2021
+ }
2022
+ /**
2023
+ * Send one request to the node in use, with the credential held now, and
2024
+ * answer the response, whatever its status. {@link send} judges it.
2025
+ */
2026
+ async once(method, path, options) {
2027
+ const headers = {};
2028
+ if (this.auth) headers.authorization = `Bearer ${this.auth}`;
2029
+ let payload;
2030
+ if (options.bytes) {
2031
+ headers["content-type"] = options.contentType ?? "application/wasm";
2032
+ payload = options.bytes;
2033
+ } else if (options.body !== void 0) {
2034
+ headers["content-type"] = "application/json";
2035
+ payload = JSON.stringify(options.body);
2036
+ }
2037
+ if (this.source && Date.now() >= this.source.due) this.reread();
2038
+ const url2 = this.nodeUrl;
2039
+ try {
2040
+ return await this.channel(url2)(`${url2}/${path}${queryString(options.query)}`, {
2041
+ method,
2042
+ headers,
2043
+ body: payload,
2044
+ signal: options.signal
2045
+ });
2046
+ } catch (error) {
2047
+ throw sealedError(error);
2048
+ }
2049
+ }
2050
+ /**
2051
+ * {@link once}, answering what a lost issuing node or a node on the wrong
2052
+ * release does to a session: a successful response, or the refusal as a
2053
+ * typed error ({@link errorFrom}).
2054
+ *
2055
+ * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
2056
+ * signed the session may be gone, replaced, or out of reach of the node
2057
+ * asked, which any node of the cluster answers with a refresh. The
2058
+ * transport refreshes once, shared by every request that needs it at the
2059
+ * same time ({@link refresh}), and repeats the request once. A refresh that
2060
+ * is refused surfaces as {@link LoginRequired}.
2061
+ * - `421 WRONG_NODE`: this node's release may not serve the account. The
2062
+ * transport moves to the next node it knows and repeats the request once;
2063
+ * with none, the {@link WrongNode} reaches the caller.
2064
+ *
2065
+ * Repeating is safe even for a request that is not idempotent: the node
2066
+ * answers `401` and `421` when it judges the credential, before it reads what
2067
+ * the request asks, so the first attempt ran nothing. The exceptions are the
2068
+ * ceremonies that mint a session ({@link ceremony}), never repeated here.
2069
+ */
2070
+ async send(method, path, options) {
2071
+ let refreshed = false;
2072
+ let moved = false;
2073
+ for (; ; ) {
2074
+ const [token, at] = [this.auth, this.nodeUrl];
2075
+ const response = await this.once(method, path, options);
2076
+ if (response.ok) return response;
2077
+ const error = await refusal(response);
2078
+ if (error instanceof WrongNode && !moved) {
2079
+ moved = true;
2080
+ if (this.moveOn(at) && !ceremony(path)) continue;
2081
+ } else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
2082
+ refreshed = true;
2083
+ await this.renew(token);
2084
+ continue;
2085
+ }
2086
+ throw error;
2087
+ }
2088
+ }
2089
+ /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
2090
+ mayRefresh(token, path) {
2091
+ return token !== void 0 && this.current !== void 0 && !ceremony(path);
2092
+ }
2093
+ /**
2094
+ * A fresh session in place of the one `token` belonged to: by the refresh
2095
+ * another request already made, or by one this request starts.
2096
+ */
2097
+ async renew(token) {
2098
+ if (this.auth !== token) return;
2099
+ await this.refresh();
2100
+ }
2101
+ async read(method, path, options) {
2102
+ const response = await this.send(method, path, options);
2103
+ const text = await response.text();
2104
+ return text ? JSON.parse(text) : {};
2105
+ }
2106
+ /**
2107
+ * {@link read} for a call or a view (D70): while the node answers 503 or
2108
+ * cannot be reached, the same request, with the same idempotency key, is sent
2109
+ * again after {@link retryDelayMs} (or the pause the node asked for, if
2110
+ * longer), at most `retries` times. Every other answer is final.
2111
+ *
2112
+ * When a node's retries run out, the request fails over to the next node the
2113
+ * transport knows and its retries start again there, with the same key: the
2114
+ * key is kept in the app's replicated log, so the cluster answers it once
2115
+ * whichever node it reaches. Each node is tried once per request. The last
2116
+ * failure surfaces as {@link UnavailableError}: the node's 503, or reason
2117
+ * `network` for a connection that failed, whose `committed` is `'unknown'`.
2118
+ *
2119
+ * `once` sends the request exactly once, to the node in use (a confirmed
2120
+ * attempt, whose challenge is single-use).
2121
+ */
2122
+ async retried(method, path, options, once) {
2123
+ const retries = once ? 0 : this.retry.retries;
2124
+ const tried = /* @__PURE__ */ new Set();
2125
+ for (; ; ) {
2126
+ let unavailable;
2127
+ let node = this.nodeUrl;
2128
+ for (let attempt = 0; ; attempt++) {
2129
+ node = this.nodeUrl;
2130
+ tried.add(node);
2131
+ try {
2132
+ return await this.read(method, path, options);
2133
+ } catch (error) {
2134
+ if (error instanceof UnavailableError) unavailable = error;
2135
+ else if (error instanceof TypeError && this.networkFailures.has(error)) {
2136
+ unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
2137
+ } else throw error;
2138
+ }
2139
+ if (attempt >= retries) break;
2140
+ const backoff = retryDelayMs(attempt, this.retry);
2141
+ await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
2142
+ }
2143
+ if (once || !this.failOver(node, tried)) throw unavailable;
2144
+ }
2145
+ }
2146
+ /**
2147
+ * One JSON request, retried as `options.retry` says. The confirmed attempt
2148
+ * of a step-up (`body.confirm` set) goes out exactly once.
2149
+ */
2150
+ attempt(method, path, options) {
2151
+ if (!options.retry) return this.read(method, path, options);
2152
+ const confirming = options.body?.confirm !== void 0;
2153
+ return this.retried(method, path, options, options.retry === "once" || confirming);
2154
+ }
2155
+ /**
2156
+ * {@link attempt}, answering a `412 CONFIRM_REQUIRED` with `passkey`'s
2157
+ * assertion over the node's challenge: the same body again, with `confirm`.
2158
+ * Without a passkey the `412` reaches the caller as {@link ConfirmRequired}.
2159
+ */
2160
+ async confirmed(method, path, options) {
2161
+ const { passkey } = options;
2162
+ const signal = options.promptSignal ?? options.signal;
2163
+ try {
2164
+ return await this.attempt(method, path, options);
2165
+ } catch (error) {
2166
+ if (!(error instanceof ConfirmRequired) || !passkey) throw error;
2167
+ const d = error.details;
2168
+ let assertion;
2169
+ try {
2170
+ assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
2171
+ userVerification: userVerificationOf(d.userVerification, options.userVerification ?? "required"),
2172
+ ...signal ? { signal } : {}
2173
+ });
2174
+ } catch (declined) {
2175
+ throw promptFailure(declined);
2176
+ }
2177
+ const body = { ...options.body, confirm: { challengeId: d.challengeId, assertion } };
2178
+ return this.attempt(method, path, { ...options, body });
2179
+ }
2180
+ }
2181
+ request(method, path, options = {}) {
2182
+ return this.send(method, relative(path), options);
2183
+ }
2184
+ json(method, path, options = {}) {
2185
+ return this.confirmed(method, relative(path), options);
2186
+ }
2187
+ async *events(path, options = {}) {
2188
+ const response = await this.send("GET", relative(path), options);
2189
+ if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
2190
+ yield* sseFrames(response.body);
2191
+ }
2192
+ /**
2193
+ * `GET path` in the clear, from the node in use: only for the route a node
2194
+ * serves unsealed (`attestation`), and never with the credential.
2195
+ */
2196
+ async unsealed(path, options = {}) {
2197
+ const response = await this.raw(`${this.nodeUrl}/${relative(path)}${queryString(options.query)}`);
2198
+ if (!response.ok) throw await refusal(response);
2199
+ return response;
2200
+ }
2201
+ /**
2202
+ * Swap the refresh token for a new session, on whichever node the transport
2203
+ * uses: any node of the cluster takes it. A refresh token is used once, so
2204
+ * requests made while one refresh is under way share it rather than sending
2205
+ * the same token again (which would revoke the session).
2206
+ */
2207
+ refresh() {
2208
+ if (!this.refreshing) {
2209
+ const pending = this.exchangeRefresh();
2210
+ this.refreshing = pending;
2211
+ const clear = () => {
2212
+ if (this.refreshing === pending) this.refreshing = void 0;
2213
+ };
2214
+ pending.then(clear, clear);
2215
+ }
2216
+ return this.refreshing;
2217
+ }
2218
+ async exchangeRefresh() {
2219
+ const current = this.current;
2220
+ if (!current) throw new Error("no session to refresh");
2221
+ let session;
2222
+ for (let tried = 1; session === void 0; tried++) {
2223
+ try {
2224
+ session = await this.json("POST", "auth/refresh", { body: { refreshToken: current.refreshToken } });
2225
+ } catch (error) {
2226
+ if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
2227
+ }
2228
+ }
2229
+ if (this.current === current) this.useSession(session);
2230
+ return session;
2231
+ }
2232
+ };
2233
+ async function directoryTransport(options) {
2234
+ const random = options.random ?? Math.random;
2235
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
2236
+ const listing = await readDirectory(options.directory, options.cluster, fetcher);
2237
+ const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
2238
+ const transport = new NodeTransport({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
2239
+ transport.useDirectory({
2240
+ directory: options.directory,
2241
+ cluster: options.cluster,
2242
+ zone: options.zone,
2243
+ random,
2244
+ due: Date.now() + listing.refreshAfterSeconds * 1e3
2245
+ });
2246
+ return transport;
2247
+ }
2248
+
1806
2249
  // src/cloud-v2/recovery.ts
1807
2250
  var RECOVERY_DOMAIN = "calimero.org-recovery.v1\0";
1808
2251
  var MAX_RECOVERY_NONCE_BYTES = 64;
@@ -1991,114 +2434,36 @@ function upgradeConsent(app, toVersion, returnTo) {
1991
2434
  return { kind: "upgrade", subject: { app, toVersion }, returnTo };
1992
2435
  }
1993
2436
 
1994
- // src/cloud-v2/client.ts
1995
- function deployerKey(deployer) {
1996
- return "account" in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
1997
- }
1998
- async function frontendManifest(origin, fetcher = fetch) {
1999
- const base = origin ?? globalThis.location?.origin;
2000
- if (!base) throw new Error("no page origin to read the frontend manifest from; pass `origin`");
2001
- const response = await fetcher(new URL("/.well-known/calimero.json", base).toString());
2002
- if (!response.ok) throw await refusal(response);
2003
- const manifest = await response.json();
2004
- return { ...manifest, deployments: manifest.deployments ?? [] };
2005
- }
2006
- function pageDeployment(href) {
2007
- const at = href ?? globalThis.location?.href;
2008
- if (!at) return null;
2009
- try {
2010
- return new URL(at).searchParams.get("deployment") || null;
2011
- } catch {
2012
- return null;
2013
- }
2014
- }
2015
- function userVerificationOf(named, fallback) {
2016
- return named === "required" || named === "preferred" || named === "discouraged" ? named : fallback;
2017
- }
2018
- function promptFailure(error) {
2019
- const e = error;
2020
- const name = e?.name;
2021
- const message = typeof e?.message === "string" ? e.message : "";
2022
- if (name === "InvalidStateError" || /not focused|already pending/i.test(message)) {
2023
- return new ConfirmPromptBlocked(error);
2024
- }
2025
- if (name === "NotAllowedError" || name === "AbortError") return new ConfirmCancelled(error);
2026
- return error;
2027
- }
2028
- function seg(value) {
2029
- return encodeURIComponent(value);
2030
- }
2031
- function sealedError(error) {
2032
- if (error instanceof SealedTransportError && error.status === 413) {
2033
- return errorFrom(413, {
2034
- error: "TOO_LARGE",
2035
- message: error.message.replace(/^Sealed request refused: /, "")
2036
- });
2037
- }
2038
- return error;
2039
- }
2040
- async function refusal(response) {
2041
- const text = await response.text().catch(() => "");
2042
- let body = {};
2043
- try {
2044
- body = text ? JSON.parse(text) : {};
2045
- } catch {
2046
- body = { message: text };
2047
- }
2048
- return errorFrom(response.status, body, response.headers.get("retry-after"));
2049
- }
2050
- async function* sseFrames(body) {
2051
- const reader = body.getReader();
2052
- const decoder = new TextDecoder();
2053
- let buffer = "";
2054
- for (; ; ) {
2055
- const { value, done } = await reader.read();
2056
- if (done) return;
2057
- buffer += decoder.decode(value, { stream: true });
2058
- let cut;
2059
- while ((cut = buffer.indexOf("\n\n")) >= 0) {
2060
- const frame = buffer.slice(0, cut);
2061
- buffer = buffer.slice(cut + 2);
2062
- let id = "";
2063
- let kind = "";
2064
- let data = "";
2065
- for (const line of frame.split("\n")) {
2066
- if (line.startsWith("id:")) id = line.slice(3).trim();
2067
- else if (line.startsWith("event:")) kind = line.slice(6).trim();
2068
- else if (line.startsWith("data:")) data += line.slice(5).trim();
2069
- }
2070
- if (!kind && !data) continue;
2071
- let parsed = data;
2072
- try {
2073
- parsed = JSON.parse(data);
2074
- } catch {
2075
- }
2076
- yield { id, kind, data: parsed };
2077
- }
2078
- }
2437
+ // src/cloud-v2/client.ts
2438
+ function deployerKey(deployer) {
2439
+ return "account" in deployer ? `account:${deployer.account}` : `org:${deployer.org}`;
2079
2440
  }
2080
- function toHex(bytes) {
2081
- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2441
+ async function frontendManifest(origin, fetcher = fetch) {
2442
+ const base = origin ?? globalThis.location?.origin;
2443
+ if (!base) throw new Error("no page origin to read the frontend manifest from; pass `origin`");
2444
+ const response = await fetcher(new URL("/.well-known/calimero.json", base).toString());
2445
+ if (!response.ok) throw await refusal(response);
2446
+ const manifest = await response.json();
2447
+ return { ...manifest, deployments: manifest.deployments ?? [] };
2082
2448
  }
2083
- function newIdempotencyKey() {
2084
- return `node:c-${b64url(crypto.getRandomValues(new Uint8Array(16)))}`;
2449
+ function pageDeployment(href) {
2450
+ const at = href ?? globalThis.location?.href;
2451
+ if (!at) return null;
2452
+ try {
2453
+ return new URL(at).searchParams.get("deployment") || null;
2454
+ } catch {
2455
+ return null;
2456
+ }
2085
2457
  }
2086
- var DEFAULT_RETRY = { retries: 6, baseDelayMs: 200, maxDelayMs: 2e3 };
2087
- function retryDelayMs(retry, options) {
2088
- return Math.min(options.baseDelayMs * 2 ** retry, options.maxDelayMs);
2458
+ function seg(value) {
2459
+ return encodeURIComponent(value);
2089
2460
  }
2090
- function ceremony(path) {
2091
- return path === "auth/refresh" || path.startsWith("auth/passkey/") || RECOVERY_CEREMONIES.has(path);
2461
+ function toHex(bytes) {
2462
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
2092
2463
  }
2093
- var RECOVERY_CEREMONIES = /* @__PURE__ */ new Set(["auth/recovery/begin", "auth/recovery/start", "auth/recovery/finish"]);
2094
2464
  var CalimeroClient = class _CalimeroClient {
2465
+ /** A client of the nodes `options` names, or one sending through an existing `transport`. */
2095
2466
  constructor(options) {
2096
- /** The one in use: an index into {@link nodeUrls}. */
2097
- this.at = 0;
2098
- /** A sealed fetch per node, each attesting its own node on first use. */
2099
- this.channels = /* @__PURE__ */ new Map();
2100
- /** Rejections of the underlying `fetch` itself: the node could not be reached or dropped the connection. */
2101
- this.networkFailures = /* @__PURE__ */ new WeakSet();
2102
2467
  // ---------------------------------------------------------------- package origins
2103
2468
  /**
2104
2469
  * The web origins `{package}`'s managers registered so a frontend they host
@@ -2124,20 +2489,7 @@ var CalimeroClient = class _CalimeroClient {
2124
2489
  ),
2125
2490
  remove: (pkg, origin, passkey) => this.confirmed("DELETE", `package-origins/${seg(pkg)}`, { origin }, passkey)
2126
2491
  };
2127
- const trim = (url2) => url2.replace(/\/+$/, "");
2128
- this.nodeUrls = [...new Set([options.baseUrl, ...options.nodes ?? []].map(trim))];
2129
- this.policy = options.policy;
2130
- this.retry = { ...DEFAULT_RETRY, ...options.retry };
2131
- const given = options.fetch ?? ((input, init) => fetch(input, init));
2132
- const raw = async (input, init) => {
2133
- try {
2134
- return await given(input, init);
2135
- } catch (error) {
2136
- if (error instanceof TypeError) this.networkFailures.add(error);
2137
- throw error;
2138
- }
2139
- };
2140
- this.raw = raw;
2492
+ this.transport = options instanceof NodeTransport ? options : new NodeTransport(options);
2141
2493
  }
2142
2494
  /**
2143
2495
  * A client for the Calimero Cloud cluster named `cluster`: its node list read
@@ -2150,219 +2502,31 @@ var CalimeroClient = class _CalimeroClient {
2150
2502
  * first request once the directory's `refreshAfterSeconds` has passed.
2151
2503
  */
2152
2504
  static async connect(options) {
2153
- const directory = options.directory ?? DEFAULT_DIRECTORY;
2154
- const random = options.random ?? Math.random;
2155
- const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
2156
- const listing = await readDirectory(directory, options.cluster, fetcher);
2157
- const [baseUrl, ...nodes] = orderNodes(listing.nodes, options.zone, random);
2158
- const client = new _CalimeroClient({ baseUrl, nodes, policy: options.policy, fetch: options.fetch, retry: options.retry });
2159
- client.source = {
2160
- directory,
2161
- cluster: options.cluster,
2162
- zone: options.zone,
2163
- random,
2164
- due: Date.now() + listing.refreshAfterSeconds * 1e3
2165
- };
2166
- return client;
2505
+ const transport = await directoryTransport({ ...options, directory: options.directory ?? DEFAULT_DIRECTORY });
2506
+ return new _CalimeroClient(transport);
2167
2507
  }
2168
2508
  /** The node requests go to now: `baseUrl`, until a `421 WRONG_NODE` or a failover moved the client on. */
2169
2509
  get nodeUrl() {
2170
- return this.nodeUrls[this.at];
2510
+ return this.transport.nodeUrl;
2171
2511
  }
2172
2512
  /** Every node this client may use, in the order it moves through them. */
2173
2513
  get nodes() {
2174
- return [...this.nodeUrls];
2175
- }
2176
- /** The sealed channel to `url`, attested under the client's policy before its first request. */
2177
- channel(url2) {
2178
- let sealed = this.channels.get(url2);
2179
- if (!sealed) {
2180
- sealed = createSealedFetch({
2181
- baseUrl: url2,
2182
- transportPublicKey: () => fetchAttestedKey(url2, this.policy, this.raw),
2183
- fetch: this.raw
2184
- });
2185
- this.channels.set(url2, sealed);
2186
- }
2187
- return sealed;
2188
- }
2189
- /**
2190
- * Move on from node `from` (the one that answered `421 WRONG_NODE`) to the
2191
- * next one. False when there is no other. A request that saw the same node
2192
- * refuse after another request moved on already does not move again.
2193
- */
2194
- moveOn(from) {
2195
- if (this.nodeUrls.length < 2) return false;
2196
- if (this.nodeUrl === from) this.at = (this.at + 1) % this.nodeUrls.length;
2197
- return true;
2198
- }
2199
- /**
2200
- * Fail over from node `from`, where a request's retries ran out, to the next
2201
- * node it has not `tried`. False when it has tried them all. A request whose
2202
- * node another request moved away from already goes to that node, if untried.
2203
- */
2204
- failOver(from, tried) {
2205
- if (this.nodeUrl !== from && !tried.has(this.nodeUrl)) return true;
2206
- const count = this.nodeUrls.length;
2207
- for (let step = 1; step < count; step++) {
2208
- const next = (this.at + step) % count;
2209
- if (!tried.has(this.nodeUrls[next])) {
2210
- this.at = next;
2211
- this.reread();
2212
- return true;
2213
- }
2214
- }
2215
- return false;
2216
- }
2217
- /**
2218
- * Read the node list again from the directory, in the background, for a
2219
- * client made by {@link connect}: the node in use stays first, then the
2220
- * directory's order; a node no longer listed is dropped. A failed read is
2221
- * ignored. One read at a time.
2222
- */
2223
- reread() {
2224
- const source = this.source;
2225
- if (!source || source.reading) return;
2226
- source.reading = readDirectory(source.directory, source.cluster, this.raw).then((listing) => {
2227
- const current = this.nodeUrl;
2228
- const listed = orderNodes(listing.nodes, source.zone, source.random);
2229
- this.nodeUrls = [current, ...listed.filter((url2) => url2 !== current)];
2230
- this.at = 0;
2231
- source.due = Date.now() + listing.refreshAfterSeconds * 1e3;
2232
- }).catch(() => void 0).finally(() => {
2233
- source.reading = void 0;
2234
- });
2514
+ return this.transport.nodes;
2235
2515
  }
2236
2516
  /** Authenticate every request with an API key. */
2237
2517
  useApiKey(key) {
2238
- this.auth = key;
2239
- this.current = void 0;
2518
+ this.transport.useApiKey(key);
2240
2519
  }
2241
2520
  useSession(session) {
2242
- this.auth = session.sessionToken;
2243
- this.current = session;
2521
+ this.transport.useSession(session);
2244
2522
  }
2245
2523
  get session() {
2246
- return this.current;
2247
- }
2248
- /**
2249
- * Send one request to the node in use, with the credential held now, and
2250
- * answer the response, whatever its status. {@link send} judges it.
2251
- */
2252
- async request(method, path, body, raw, contentType = "application/wasm", signal) {
2253
- const headers = {};
2254
- if (this.auth) headers.authorization = `Bearer ${this.auth}`;
2255
- let payload;
2256
- if (raw) {
2257
- headers["content-type"] = contentType;
2258
- payload = raw;
2259
- } else if (body !== void 0) {
2260
- headers["content-type"] = "application/json";
2261
- payload = JSON.stringify(body);
2262
- }
2263
- if (this.source && Date.now() >= this.source.due) this.reread();
2264
- const url2 = this.nodeUrl;
2265
- try {
2266
- return await this.channel(url2)(`${url2}/${path}`, { method, headers, body: payload, signal });
2267
- } catch (error) {
2268
- throw sealedError(error);
2269
- }
2270
- }
2271
- /**
2272
- * {@link request}, answering what a lost issuing node or a node on the
2273
- * wrong release does to a session: a successful response, or the refusal as a
2274
- * typed error ({@link errorFrom}).
2275
- *
2276
- * - `401 UNAUTHENTICATED` to a request that carried a session: the node that
2277
- * signed the session may be gone, replaced, or out of reach of the node
2278
- * asked, which any node of the cluster answers with a refresh. The client
2279
- * refreshes once, shared by every request that needs it at the same time
2280
- * ({@link refresh}), and repeats the request once. A refresh that is
2281
- * refused surfaces as {@link LoginRequired}.
2282
- * - `421 WRONG_NODE`: this node's release may not serve the account. The
2283
- * client moves to the next node it knows and repeats the request once; with
2284
- * none, the {@link WrongNode} reaches the caller.
2285
- *
2286
- * Repeating is safe even for a request that is not idempotent: the node
2287
- * answers `401` and `421` when it judges the credential, before it reads what
2288
- * the request asks, so the first attempt ran nothing. The exceptions are the
2289
- * ceremonies that mint a session ({@link ceremony}), never repeated here.
2290
- */
2291
- async send(method, path, body, raw, contentType, signal) {
2292
- let refreshed = false;
2293
- let moved = false;
2294
- for (; ; ) {
2295
- const [token, at] = [this.auth, this.nodeUrl];
2296
- const response = await this.request(method, path, body, raw, contentType, signal);
2297
- if (response.ok) return response;
2298
- const error = await refusal(response);
2299
- if (error instanceof WrongNode && !moved) {
2300
- moved = true;
2301
- if (this.moveOn(at) && !ceremony(path)) continue;
2302
- } else if (error instanceof LoginRequired && !refreshed && this.mayRefresh(token, path)) {
2303
- refreshed = true;
2304
- await this.renew(token);
2305
- continue;
2306
- }
2307
- throw error;
2308
- }
2524
+ return this.transport.session;
2309
2525
  }
2310
- /** Whether a `401` to a request sent with `token` is one a refresh may answer. */
2311
- mayRefresh(token, path) {
2312
- return token !== void 0 && this.current !== void 0 && !ceremony(path);
2313
- }
2314
- /**
2315
- * A fresh session in place of the one `token` belonged to: by the refresh
2316
- * another request already made, or by one this request starts.
2317
- */
2318
- async renew(token) {
2319
- if (this.auth !== token) return;
2320
- await this.refresh();
2321
- }
2322
- async json(method, path, body, raw, contentType) {
2323
- const response = await this.send(method, path, body, raw, contentType);
2324
- const text = await response.text();
2325
- return text ? JSON.parse(text) : {};
2326
- }
2327
- /**
2328
- * {@link json} for a call or a view (D70): while the node answers 503 or
2329
- * cannot be reached, the same request, with the same idempotency key, is sent
2330
- * again after {@link retryDelayMs} (or the pause the node asked for, if
2331
- * longer), at most `retries` times. Every other answer is final.
2332
- *
2333
- * When a node's retries run out, the request fails over to the next node the
2334
- * client knows and its retries start again there, with the same key: the
2335
- * key is kept in the app's replicated log, so the cluster answers it once
2336
- * whichever node it reaches. Each node is tried once per request. The last
2337
- * failure surfaces as {@link UnavailableError}: the node's 503, or reason
2338
- * `network` for a connection that failed, whose `committed` is `'unknown'`.
2339
- *
2340
- * `once` sends the request exactly once, to the node in use (the confirmed
2341
- * attempt of {@link callConfirmed}, whose challenge is single-use).
2342
- */
2343
- async retried(method, path, body, once = false) {
2344
- const retries = once ? 0 : this.retry.retries;
2345
- const tried = /* @__PURE__ */ new Set();
2346
- for (; ; ) {
2347
- let unavailable;
2348
- let node = this.nodeUrl;
2349
- for (let attempt = 0; ; attempt++) {
2350
- node = this.nodeUrl;
2351
- tried.add(node);
2352
- try {
2353
- return await this.json(method, path, body);
2354
- } catch (error) {
2355
- if (error instanceof UnavailableError) unavailable = error;
2356
- else if (error instanceof TypeError && this.networkFailures.has(error)) {
2357
- unavailable = new UnavailableError("NETWORK", `the node could not be reached: ${error.message}`, {}, void 0, error);
2358
- } else throw error;
2359
- }
2360
- if (attempt >= retries) break;
2361
- const backoff = retryDelayMs(attempt, this.retry);
2362
- await new Promise((resolve) => setTimeout(resolve, Math.max(backoff, (unavailable.retryAfter ?? 0) * 1e3)));
2363
- }
2364
- if (once || !this.failOver(node, tried)) throw unavailable;
2365
- }
2526
+ /** A JSON request, or a raw one when `raw` is given (as `contentType`, the transport's default when absent). */
2527
+ json(method, path, body, raw, contentType) {
2528
+ const options = raw ? { bytes: raw, ...contentType === void 0 ? {} : { contentType } } : { body };
2529
+ return this.transport.json(method, path, options);
2366
2530
  }
2367
2531
  post(path, body = {}) {
2368
2532
  return this.json("POST", path, body);
@@ -2406,7 +2570,6 @@ var CalimeroClient = class _CalimeroClient {
2406
2570
  "auth/passkey/register/finish",
2407
2571
  { challengeId: begin.challengeId, registration, ...invite },
2408
2572
  passkey,
2409
- void 0,
2410
2573
  "preferred",
2411
2574
  signal
2412
2575
  );
@@ -2447,7 +2610,7 @@ var CalimeroClient = class _CalimeroClient {
2447
2610
  const page = globalThis.location?.origin;
2448
2611
  if (!page || page === "null") return void 0;
2449
2612
  const nodes = new Set(
2450
- this.nodeUrls.map((url2) => {
2613
+ this.transport.nodes.map((url2) => {
2451
2614
  try {
2452
2615
  return new URL(url2).origin;
2453
2616
  } catch {
@@ -2475,29 +2638,7 @@ var CalimeroClient = class _CalimeroClient {
2475
2638
  * same token again (which would revoke the session).
2476
2639
  */
2477
2640
  refresh() {
2478
- if (!this.refreshing) {
2479
- const pending = this.exchangeRefresh();
2480
- this.refreshing = pending;
2481
- const clear = () => {
2482
- if (this.refreshing === pending) this.refreshing = void 0;
2483
- };
2484
- pending.then(clear, clear);
2485
- }
2486
- return this.refreshing;
2487
- }
2488
- async exchangeRefresh() {
2489
- const current = this.current;
2490
- if (!current) throw new Error("no session to refresh");
2491
- let session;
2492
- for (let tried = 1; session === void 0; tried++) {
2493
- try {
2494
- session = await this.post("auth/refresh", { refreshToken: current.refreshToken });
2495
- } catch (error) {
2496
- if (!(error instanceof WrongNode) || tried >= this.nodeUrls.length) throw error;
2497
- }
2498
- }
2499
- if (this.current === current) this.useSession(session);
2500
- return session;
2641
+ return this.transport.refresh();
2501
2642
  }
2502
2643
  /**
2503
2644
  * End this session. The client is signed out whether or not the node
@@ -2557,8 +2698,7 @@ var CalimeroClient = class _CalimeroClient {
2557
2698
  try {
2558
2699
  await this.post(path);
2559
2700
  } finally {
2560
- this.auth = void 0;
2561
- this.current = void 0;
2701
+ this.transport.signOut();
2562
2702
  }
2563
2703
  }
2564
2704
  /** Who the credential is: the account, how it signed in, and the package (`app`) and `origin` a session is bound to. */
@@ -2664,8 +2804,7 @@ var CalimeroClient = class _CalimeroClient {
2664
2804
  */
2665
2805
  async attestation() {
2666
2806
  const nonce2 = toHex(crypto.getRandomValues(new Uint8Array(32)));
2667
- const response = await this.raw(`${this.nodeUrl}/attestation?nonce=${nonce2}`);
2668
- if (!response.ok) throw await refusal(response);
2807
+ const response = await this.transport.unsealed("attestation", { query: { nonce: nonce2 } });
2669
2808
  return await response.json();
2670
2809
  }
2671
2810
  /** The account's API keys (never the keys themselves). */
@@ -2726,8 +2865,9 @@ var CalimeroClient = class _CalimeroClient {
2726
2865
  * app; `application/octet-stream` otherwise.
2727
2866
  */
2728
2867
  async blobContent(app, id, options = {}) {
2729
- const query = options.grant === void 0 ? "" : `?grant=${encodeURIComponent(options.grant)}`;
2730
- const response = await this.send("GET", `apps/${seg(app)}/blobs/${seg(id)}${query}`);
2868
+ const response = await this.transport.request("GET", `apps/${seg(app)}/blobs/${seg(id)}`, {
2869
+ query: { grant: options.grant }
2870
+ });
2731
2871
  return {
2732
2872
  data: new Uint8Array(await response.arrayBuffer()),
2733
2873
  contentType: response.headers.get("content-type") ?? "application/octet-stream"
@@ -2872,23 +3012,12 @@ var CalimeroClient = class _CalimeroClient {
2872
3012
  deleteConfirmed(path, passkey, body = {}) {
2873
3013
  return this.confirmed("DELETE", path, body, passkey);
2874
3014
  }
2875
- async confirmed(method, path, body, passkey, send = (b) => this.json(method, path, b), userVerification = "required", signal) {
2876
- try {
2877
- return await send(body);
2878
- } catch (error) {
2879
- if (!(error instanceof ConfirmRequired) || !passkey) throw error;
2880
- const d = error.details;
2881
- let assertion;
2882
- try {
2883
- assertion = await passkey.get(fromB64url(d.challenge), d.rpId, d.allowCredentials, {
2884
- userVerification: userVerificationOf(d.userVerification, userVerification),
2885
- ...signal ? { signal } : {}
2886
- });
2887
- } catch (declined) {
2888
- throw promptFailure(declined);
2889
- }
2890
- return send({ ...body, confirm: { challengeId: d.challengeId, assertion } });
2891
- }
3015
+ /**
3016
+ * `body` to `path`, answering a `412 CONFIRM_REQUIRED` with `passkey` when one
3017
+ * is given; `signal` aborts only the passkey prompt.
3018
+ */
3019
+ confirmed(method, path, body, passkey, userVerification, signal) {
3020
+ return this.transport.json(method, path, { body, passkey, userVerification, promptSignal: signal });
2892
3021
  }
2893
3022
  /**
2894
3023
  * Create an app instance from a bundle hash, or from a package release
@@ -2912,7 +3041,7 @@ var CalimeroClient = class _CalimeroClient {
2912
3041
  */
2913
3042
  call(app, method, args = {}, idempotencyKey) {
2914
3043
  const key = idempotencyKey ?? newIdempotencyKey();
2915
- return this.retried("POST", `apps/${seg(app)}/intents`, { method, args, idempotencyKey: key });
3044
+ return this.transport.json("POST", `apps/${seg(app)}/intents`, { body: { method, args, idempotencyKey: key }, retry: true });
2916
3045
  }
2917
3046
  /**
2918
3047
  * Call a method that needs the account holder's approval: the node returns a
@@ -2922,32 +3051,23 @@ var CalimeroClient = class _CalimeroClient {
2922
3051
  async callConfirmed(app, method, args, passkey, idempotencyKey) {
2923
3052
  const path = `apps/${seg(app)}/intents`;
2924
3053
  const key = idempotencyKey ?? newIdempotencyKey();
2925
- return this.confirmed(
2926
- "POST",
2927
- path,
2928
- { method, args, idempotencyKey: key },
2929
- passkey,
2930
- (body) => this.retried("POST", path, body, body.confirm !== void 0)
2931
- );
3054
+ return this.transport.json("POST", path, { body: { method, args, idempotencyKey: key }, passkey, retry: true });
2932
3055
  }
2933
3056
  /** Run a read-only method: strong by default, or stale from any replica. */
2934
3057
  view(app, method, args = {}, options = {}) {
2935
- return this.retried("POST", `apps/${seg(app)}/views`, {
2936
- method,
2937
- args,
2938
- consistency: options.stale ? "stale" : "strong",
2939
- minIndex: options.minIndex ?? 0
3058
+ return this.transport.json("POST", `apps/${seg(app)}/views`, {
3059
+ body: {
3060
+ method,
3061
+ args,
3062
+ consistency: options.stale ? "stale" : "strong",
3063
+ minIndex: options.minIndex ?? 0
3064
+ },
3065
+ retry: true
2940
3066
  });
2941
3067
  }
2942
3068
  /** Committed events from index `from`, then live, until `signal` aborts. */
2943
3069
  events(app, from = 0, signal) {
2944
- return this.stream(`apps/${seg(app)}/events?from=${from}`, signal);
2945
- }
2946
- /** A server-sent event stream on `path`, sealed like every other answer. */
2947
- async *stream(path, signal) {
2948
- const response = await this.send("GET", path, void 0, void 0, void 0, signal);
2949
- if (!response.body) throw new CalimeroError(response.status, "EVENTS", "the event stream has no body");
2950
- yield* sseFrames(response.body);
3070
+ return this.transport.events(`apps/${seg(app)}/events`, { query: { from }, signal });
2951
3071
  }
2952
3072
  // ---------------------------------------------------------------- presence
2953
3073
  /**
@@ -2968,7 +3088,7 @@ var CalimeroClient = class _CalimeroClient {
2968
3088
  * entries present), then a `presence` event (`data` one entry) per change.
2969
3089
  */
2970
3090
  presenceStream(app, signal) {
2971
- return this.stream(`apps/${seg(app)}/presence/stream`, signal);
3091
+ return this.transport.events(`apps/${seg(app)}/presence/stream`, { signal });
2972
3092
  }
2973
3093
  // ---------------------------------------------------------------- aliases
2974
3094
  /** Name an app (its id or context id) for this account. */