@calimero-network/mero-js 26.11.0 → 27.0.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.
Files changed (220) hide show
  1. package/README.md +73 -288
  2. package/dist/cloud-v2/attestation.d.ts +91 -0
  3. package/dist/cloud-v2/attestation.js +135 -0
  4. package/dist/cloud-v2/capabilities.d.ts +30 -0
  5. package/dist/cloud-v2/capabilities.js +31 -0
  6. package/dist/cloud-v2/client.d.ts +426 -0
  7. package/dist/cloud-v2/client.js +646 -0
  8. package/dist/cloud-v2/errors.d.ts +98 -0
  9. package/dist/cloud-v2/errors.js +165 -0
  10. package/dist/cloud-v2/index.d.ts +10 -0
  11. package/dist/cloud-v2/index.js +5 -0
  12. package/dist/cloud-v2/passkey.d.ts +27 -0
  13. package/dist/cloud-v2/passkey.js +73 -0
  14. package/dist/crypto/internal.d.ts +3 -37
  15. package/dist/crypto/internal.js +3 -72
  16. package/dist/index.browser.mjs +4 -2
  17. package/dist/index.browser.mjs.map +4 -4
  18. package/dist/index.cjs +1750 -12772
  19. package/dist/index.cjs.map +4 -4
  20. package/dist/index.d.ts +3 -56
  21. package/dist/index.js +14 -75
  22. package/dist/index.mjs +1750 -12772
  23. package/dist/index.mjs.map +4 -4
  24. package/dist/sealed/index.d.ts +4 -4
  25. package/dist/sealed/index.js +2 -2
  26. package/dist/sealed/release.d.ts +21 -76
  27. package/dist/sealed/release.js +24 -108
  28. package/dist/sealed/sealed.d.ts +8 -56
  29. package/dist/sealed/sealed.js +11 -89
  30. package/dist/sealed/verify.d.ts +7 -13
  31. package/dist/sealed/verify.js +3 -7
  32. package/package.json +8 -13
  33. package/dist/account/account-admin.d.ts +0 -80
  34. package/dist/account/account-admin.js +0 -346
  35. package/dist/account/account.d.ts +0 -196
  36. package/dist/account/account.js +0 -269
  37. package/dist/account/application-id.d.ts +0 -50
  38. package/dist/account/application-id.js +0 -83
  39. package/dist/account/bootstrap-from-invitation.d.ts +0 -110
  40. package/dist/account/bootstrap-from-invitation.js +0 -80
  41. package/dist/account/create-context.d.ts +0 -103
  42. package/dist/account/create-context.js +0 -267
  43. package/dist/account/govern.d.ts +0 -24
  44. package/dist/account/govern.js +0 -50
  45. package/dist/account/index.d.ts +0 -26
  46. package/dist/account/index.js +0 -15
  47. package/dist/account/join-as-account.d.ts +0 -23
  48. package/dist/account/join-as-account.js +0 -34
  49. package/dist/account/join-with-node.d.ts +0 -115
  50. package/dist/account/join-with-node.js +0 -161
  51. package/dist/account/node-admin.d.ts +0 -20
  52. package/dist/account/node-admin.js +0 -33
  53. package/dist/account/relay-from-invitation.d.ts +0 -137
  54. package/dist/account/relay-from-invitation.js +0 -180
  55. package/dist/account/relay-login-error.d.ts +0 -32
  56. package/dist/account/relay-login-error.js +0 -34
  57. package/dist/account/session.d.ts +0 -266
  58. package/dist/account/session.js +0 -915
  59. package/dist/account/wordlist.d.ts +0 -16
  60. package/dist/account/wordlist.js +0 -273
  61. package/dist/admin-api/admin-client.d.ts +0 -688
  62. package/dist/admin-api/admin-client.js +0 -1230
  63. package/dist/admin-api/admin-factory.d.ts +0 -14
  64. package/dist/admin-api/admin-factory.js +0 -22
  65. package/dist/admin-api/admin-types.d.ts +0 -2046
  66. package/dist/admin-api/admin-types.js +0 -12
  67. package/dist/admin-api/index.d.ts +0 -4
  68. package/dist/admin-api/index.js +0 -5
  69. package/dist/admin-api/redeem-invitation.d.ts +0 -101
  70. package/dist/admin-api/redeem-invitation.js +0 -200
  71. package/dist/auth/index.d.ts +0 -34
  72. package/dist/auth/index.js +0 -67
  73. package/dist/auth-api/auth-client.d.ts +0 -31
  74. package/dist/auth-api/auth-client.js +0 -117
  75. package/dist/auth-api/auth-factory.d.ts +0 -7
  76. package/dist/auth-api/auth-factory.js +0 -41
  77. package/dist/auth-api/auth-types.d.ts +0 -130
  78. package/dist/auth-api/auth-types.js +0 -2
  79. package/dist/auth-api/index.d.ts +0 -3
  80. package/dist/auth-api/index.js +0 -4
  81. package/dist/capabilities.d.ts +0 -84
  82. package/dist/capabilities.js +0 -117
  83. package/dist/cloud/account-ownership.d.ts +0 -63
  84. package/dist/cloud/account-ownership.js +0 -87
  85. package/dist/cloud/cloud-client.d.ts +0 -949
  86. package/dist/cloud/cloud-client.js +0 -1068
  87. package/dist/cloud/connect.d.ts +0 -213
  88. package/dist/cloud/connect.js +0 -260
  89. package/dist/cloud/enrol-redirect.d.ts +0 -181
  90. package/dist/cloud/enrol-redirect.js +0 -244
  91. package/dist/cloud/index.d.ts +0 -12
  92. package/dist/cloud/index.js +0 -10
  93. package/dist/cloud/link-redirect.d.ts +0 -95
  94. package/dist/cloud/link-redirect.js +0 -121
  95. package/dist/cloud/link-state.d.ts +0 -36
  96. package/dist/cloud/link-state.js +0 -104
  97. package/dist/cloud/routing-proof.d.ts +0 -113
  98. package/dist/cloud/routing-proof.js +0 -96
  99. package/dist/device-cert/device-cert.d.ts +0 -189
  100. package/dist/device-cert/device-cert.js +0 -239
  101. package/dist/device-cert/device-scope.d.ts +0 -58
  102. package/dist/device-cert/device-scope.js +0 -76
  103. package/dist/device-cert/index.d.ts +0 -4
  104. package/dist/device-cert/index.js +0 -3
  105. package/dist/ephemeral/codec.d.ts +0 -4
  106. package/dist/ephemeral/codec.js +0 -12
  107. package/dist/ephemeral/index.d.ts +0 -57
  108. package/dist/ephemeral/index.js +0 -101
  109. package/dist/ephemeral/types.d.ts +0 -27
  110. package/dist/ephemeral/types.js +0 -1
  111. package/dist/errors/classify.d.ts +0 -65
  112. package/dist/errors/classify.js +0 -117
  113. package/dist/errors/index.d.ts +0 -2
  114. package/dist/errors/index.js +0 -1
  115. package/dist/events/group.d.ts +0 -95
  116. package/dist/events/group.js +0 -10
  117. package/dist/events/index.d.ts +0 -5
  118. package/dist/events/index.js +0 -2
  119. package/dist/events/sse.d.ts +0 -127
  120. package/dist/events/sse.js +0 -469
  121. package/dist/events/ws.d.ts +0 -57
  122. package/dist/events/ws.js +0 -212
  123. package/dist/http-client/api-response.d.ts +0 -15
  124. package/dist/http-client/api-response.js +0 -1
  125. package/dist/http-client/http-factory.d.ts +0 -71
  126. package/dist/http-client/http-factory.js +0 -68
  127. package/dist/http-client/http-types.d.ts +0 -87
  128. package/dist/http-client/http-types.js +0 -1
  129. package/dist/http-client/index.d.ts +0 -7
  130. package/dist/http-client/index.js +0 -11
  131. package/dist/http-client/path-segment.d.ts +0 -1
  132. package/dist/http-client/path-segment.js +0 -8
  133. package/dist/http-client/retry.d.ts +0 -5
  134. package/dist/http-client/retry.js +0 -82
  135. package/dist/http-client/signal-utils.d.ts +0 -2
  136. package/dist/http-client/signal-utils.js +0 -41
  137. package/dist/http-client/web-client.d.ts +0 -80
  138. package/dist/http-client/web-client.js +0 -557
  139. package/dist/invitation/index.d.ts +0 -2
  140. package/dist/invitation/index.js +0 -2
  141. package/dist/invitation/invitation.d.ts +0 -106
  142. package/dist/invitation/invitation.js +0 -104
  143. package/dist/login/index.d.ts +0 -4
  144. package/dist/login/index.js +0 -3
  145. package/dist/login/login.d.ts +0 -111
  146. package/dist/login/login.js +0 -110
  147. package/dist/login/session.d.ts +0 -124
  148. package/dist/login/session.js +0 -141
  149. package/dist/mero-js.d.ts +0 -231
  150. package/dist/mero-js.js +0 -469
  151. package/dist/namespace-op/index.d.ts +0 -2
  152. package/dist/namespace-op/index.js +0 -3
  153. package/dist/namespace-op/namespace-op.d.ts +0 -150
  154. package/dist/namespace-op/namespace-op.js +0 -191
  155. package/dist/nodeDiscovery.d.ts +0 -47
  156. package/dist/nodeDiscovery.js +0 -101
  157. package/dist/owner-op/index.d.ts +0 -2
  158. package/dist/owner-op/index.js +0 -3
  159. package/dist/owner-op/owner-op.d.ts +0 -119
  160. package/dist/owner-op/owner-op.js +0 -214
  161. package/dist/presence/relay-presence.d.ts +0 -56
  162. package/dist/presence/relay-presence.js +0 -107
  163. package/dist/presence/statement.d.ts +0 -12
  164. package/dist/presence/statement.js +0 -17
  165. package/dist/relay/index.d.ts +0 -4
  166. package/dist/relay/index.js +0 -2
  167. package/dist/relay/nonce-source.d.ts +0 -72
  168. package/dist/relay/nonce-source.js +0 -111
  169. package/dist/relay/relay-client.d.ts +0 -472
  170. package/dist/relay/relay-client.js +0 -518
  171. package/dist/relay-attestation/attest-node-key.d.ts +0 -66
  172. package/dist/relay-attestation/attest-node-key.js +0 -110
  173. package/dist/relay-attestation/index.d.ts +0 -1
  174. package/dist/relay-attestation/index.js +0 -1
  175. package/dist/request/caller-proof.d.ts +0 -43
  176. package/dist/request/caller-proof.js +0 -36
  177. package/dist/request/index.d.ts +0 -3
  178. package/dist/request/index.js +0 -3
  179. package/dist/request/proof-signer.d.ts +0 -49
  180. package/dist/request/proof-signer.js +0 -59
  181. package/dist/request/request-sig.d.ts +0 -99
  182. package/dist/request/request-sig.js +0 -95
  183. package/dist/request-proof/authorizer.d.ts +0 -41
  184. package/dist/request-proof/authorizer.js +0 -60
  185. package/dist/request-proof/index.d.ts +0 -4
  186. package/dist/request-proof/index.js +0 -2
  187. package/dist/request-proof/request-proof.d.ts +0 -53
  188. package/dist/request-proof/request-proof.js +0 -95
  189. package/dist/rpc/index.d.ts +0 -64
  190. package/dist/rpc/index.js +0 -78
  191. package/dist/signer/index.d.ts +0 -2
  192. package/dist/signer/index.js +0 -3
  193. package/dist/signer/signer.d.ts +0 -92
  194. package/dist/signer/signer.js +0 -147
  195. package/dist/token-store/index.d.ts +0 -27
  196. package/dist/token-store/index.js +0 -62
  197. package/dist/transport/index.d.ts +0 -6
  198. package/dist/transport/index.js +0 -3
  199. package/dist/transport/mero-client.d.ts +0 -294
  200. package/dist/transport/mero-client.js +0 -386
  201. package/dist/transport/relay-observer.d.ts +0 -152
  202. package/dist/transport/relay-observer.js +0 -173
  203. package/dist/transport/relay-transport.d.ts +0 -73
  204. package/dist/transport/relay-transport.js +0 -94
  205. package/dist/transport/types.d.ts +0 -100
  206. package/dist/transport/types.js +0 -32
  207. package/dist/types/api-response.d.ts +0 -15
  208. package/dist/types/api-response.js +0 -1
  209. package/dist/types/index.d.ts +0 -1
  210. package/dist/types/index.js +0 -1
  211. package/dist/warrant/creation-warrant.d.ts +0 -163
  212. package/dist/warrant/creation-warrant.js +0 -194
  213. package/dist/warrant/governance-op.d.ts +0 -202
  214. package/dist/warrant/governance-op.js +0 -355
  215. package/dist/warrant/governance-warrant.d.ts +0 -124
  216. package/dist/warrant/governance-warrant.js +0 -164
  217. package/dist/warrant/index.d.ts +0 -8
  218. package/dist/warrant/index.js +0 -5
  219. package/dist/warrant/warrant.d.ts +0 -188
  220. package/dist/warrant/warrant.js +0 -202
@@ -1,915 +0,0 @@
1
- /**
2
- * The delegated half of connecting: an account, a certified device, and a relay
3
- * that writes on their behalf.
4
- *
5
- * Kept deliberately apart from the node-login path in `MeroContext`. That path
6
- * is a redirect flow with token adoption, single-use refresh tokens and node-URL
7
- * trust validation — none of which applies here, because there is no session
8
- * token at all. A delegated client authenticates every request from a warrant it
9
- * signs itself. Weaving the two together would put a token-rotation decision in
10
- * front of a flow that has no tokens.
11
- *
12
- * Per-tab on purpose. The record lives in `sessionStorage`, so two tabs can hold
13
- * two different connections — one a node you run, one a relay you do not — with
14
- * the same application code in both.
15
- */
16
- import { attestRelayNodeKey, } from '../relay-attestation/index.js';
17
- import { cloudNodeReleaseUrl, createAttestedSealedFetch, createSignedReleaseVerifier, DEFAULT_RELEASE_MIRROR, fetchNodeRelease, fetchNodeReleaseVersion, } from '../sealed/index.js';
18
- import { createMeroClient, defaultAudience, } from '../transport/index.js';
19
- import { login, } from '../login/index.js';
20
- import { RelayLoginError } from './relay-login-error.js';
21
- import { SseClient, } from '../events/index.js';
22
- /** Where the per-tab record lives. */
23
- const KEY = 'calimero.delegated.connection';
24
- /**
25
- * Where the enrolled credential lives when there is no relay for it yet.
26
- *
27
- * Separate from {@link KEY}, and deliberately not "a connection with a null
28
- * relay": a record under that key means a usable client can be built from it,
29
- * and every reader relies on that. An account that has just enrolled is a
30
- * member of nothing, so the cloud names no relay for it and there is nothing to
31
- * build — but the certificate is real and must survive, or the only way to
32
- * bootstrap from an invitation would be to enrol again.
33
- */
34
- const CREDENTIAL_KEY = 'calimero.delegated.credential';
35
- export function readDelegatedSession() {
36
- if (typeof sessionStorage === 'undefined')
37
- return null;
38
- try {
39
- const raw = sessionStorage.getItem(KEY);
40
- return raw ? JSON.parse(raw) : null;
41
- }
42
- catch {
43
- // Private mode, blocked site data, or a half-written record. Treat all
44
- // three as "not connected" rather than throwing during render.
45
- return null;
46
- }
47
- }
48
- export function saveDelegatedSession(s) {
49
- try {
50
- sessionStorage.setItem(KEY, JSON.stringify(s));
51
- }
52
- catch {
53
- // Non-fatal: the client still works for this page view, it just will not
54
- // survive a reload. Better than refusing to connect.
55
- }
56
- }
57
- export function clearDelegatedSession() {
58
- try {
59
- sessionStorage.removeItem(KEY);
60
- }
61
- catch {
62
- /* nothing to clear if storage is unavailable */
63
- }
64
- }
65
- /**
66
- * The enrolled credential, whether or not a relay was ever found for it.
67
- *
68
- * A connected session is authoritative: it carries the same three fields and a
69
- * relay, so it is read first and the standalone record is the fallback. Without
70
- * that order a stale record from an earlier enrolment could out-rank the account
71
- * actually connected.
72
- */
73
- export function readDelegatedCredential() {
74
- const session = readDelegatedSession();
75
- if (session) {
76
- const { account, credential, deviceSecret } = session;
77
- return { account, credential, deviceSecret };
78
- }
79
- if (typeof sessionStorage === 'undefined')
80
- return null;
81
- try {
82
- const raw = sessionStorage.getItem(CREDENTIAL_KEY);
83
- if (!raw)
84
- return null;
85
- const parsed = JSON.parse(raw);
86
- // A half-written record is worse than none: it would be carried into a
87
- // signing call and fail as a malformed credential, somewhere that cannot say
88
- // where the bad value came from.
89
- if (!parsed.account || !parsed.credential || !parsed.deviceSecret)
90
- return null;
91
- return parsed;
92
- }
93
- catch {
94
- return null;
95
- }
96
- }
97
- export function saveDelegatedCredential(c) {
98
- try {
99
- sessionStorage.setItem(CREDENTIAL_KEY, JSON.stringify(c));
100
- }
101
- catch {
102
- /* this page view still holds it; it just will not survive a reload */
103
- }
104
- }
105
- export function clearDelegatedCredential() {
106
- try {
107
- sessionStorage.removeItem(CREDENTIAL_KEY);
108
- }
109
- catch {
110
- /* nothing to clear if storage is unavailable */
111
- }
112
- }
113
- /**
114
- * A warrant nonce is spent on the node, so the counter must outlive the page.
115
- *
116
- * In `localStorage`, not session: a reload that restarted from zero would
117
- * replay a spent nonce and be refused. Keyed by relay and context because the
118
- * ledger the node keeps is per `(context, author device)`.
119
- *
120
- * No node serves a "where does my sequence stand" lookup (core #4018), so
121
- * holding the counter locally is the only option, and losing storage means
122
- * burning refusals until the counter catches up.
123
- */
124
- export function persistedNonces(relayUrl) {
125
- // ONE sequence per relay, not one per context. A client is rebuilt whenever
126
- // the chosen context changes, and every client calls `execute` for any
127
- // context — so a per-context counter handed the same (context, device) ledger
128
- // two sequences, and whichever lagged was refused as a replay. The ledger is a
129
- // window that accepts any unseen nonce above its floor, so a single rising
130
- // sequence shared by every context only ever skips, which is free.
131
- const key = nonceKey(relayUrl);
132
- return {
133
- next: async () => {
134
- let n = 0n;
135
- try {
136
- n = readNonceFloor(relayUrl);
137
- }
138
- catch {
139
- n = 0n;
140
- }
141
- try {
142
- localStorage.setItem(key, String(n + 1n));
143
- }
144
- catch {
145
- /* unpersisted: this page view still advances in memory */
146
- }
147
- return n;
148
- },
149
- };
150
- }
151
- function nonceKey(relayUrl) {
152
- return `calimero.nonce.${relayUrl}`;
153
- }
154
- /**
155
- * Where this relay's sequence stands: its own counter, or — the first time,
156
- * before one exists — the highest of the per-context counters an earlier build
157
- * kept, so moving to one sequence replays nothing.
158
- */
159
- function readNonceFloor(relayUrl) {
160
- const own = localStorage.getItem(nonceKey(relayUrl));
161
- if (own !== null)
162
- return BigInt(own);
163
- const legacy = `${nonceKey(relayUrl)}.`;
164
- let max = 0n;
165
- for (let i = 0; i < localStorage.length; i++) {
166
- const k = localStorage.key(i);
167
- if (!k?.startsWith(legacy))
168
- continue;
169
- const v = BigInt(localStorage.getItem(k) ?? '0');
170
- if (v > max)
171
- max = v;
172
- }
173
- return max;
174
- }
175
- /**
176
- * Record that `nonce` is spent for this device, so later intents start above
177
- * it. A creation warrant's nonce is spent in the NEW context's own window; one
178
- * sequence per relay covers that context like any other.
179
- */
180
- export function markContextNonceSpent(relayUrl, _contextId, nonce) {
181
- try {
182
- if (readNonceFloor(relayUrl) <= nonce)
183
- localStorage.setItem(nonceKey(relayUrl), String(nonce + 1n));
184
- }
185
- catch {
186
- /* unpersisted: the first write meets a spent nonce once, and retries above it */
187
- }
188
- }
189
- const RELAY_NODE_KEY_PREFIX = 'calimero.delegated.relay-node-key.';
190
- function relayOrigin(relayUrl) {
191
- return relayUrl.replace(/\/+$/, '');
192
- }
193
- /**
194
- * Pin a relay's device signing key, hex (32 bytes), learned out of band.
195
- *
196
- * `login()` binds the session to this key, and it must never be read from the
197
- * relay being logged in to — the answering party would choose what the device
198
- * signs about. Until the cloud publishes it (mdma#312), it comes from the
199
- * relay's operator.
200
- */
201
- export function pinRelayNodeKey(relayUrl, nodeKey) {
202
- localStorage.setItem(RELAY_NODE_KEY_PREFIX + relayOrigin(relayUrl), nodeKey.trim().toLowerCase());
203
- }
204
- /** The pinned key for a relay, or `null` if none was pinned. */
205
- export function readPinnedRelayNodeKey(relayUrl) {
206
- try {
207
- const v = localStorage.getItem(RELAY_NODE_KEY_PREFIX + relayOrigin(relayUrl));
208
- return v && /^[0-9a-f]{64}$/.test(v) ? v : null;
209
- }
210
- catch {
211
- return null;
212
- }
213
- }
214
- function isLoopback(relayUrl) {
215
- try {
216
- const { hostname } = new URL(relayUrl);
217
- return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
218
- }
219
- catch {
220
- return false;
221
- }
222
- }
223
- /** The one relay image trusted: no shell, so nobody reads the TD. */
224
- const TRUSTED_RELAY_PROFILE = 'locked-read-only';
225
- /**
226
- * The oldest relay release trusted: the first that serves delegated context
227
- * creation and governance at the namespace-op schema this client signs.
228
- */
229
- const MIN_RELAY_RELEASE = '2.3.99';
230
- let loadedDcap;
231
- /**
232
- * `verify` out of a dynamically imported `@phala/dcap-qvl`, whatever shape the
233
- * bundler gave the module.
234
- *
235
- * The package is CommonJS (`module.exports = { verify, … }`). Node and Rollup
236
- * expose its keys as named exports; a browser pre-bundle — Vite's dev optimizer
237
- * among them — hands back a namespace whose only export is `default`. Reading
238
- * only the named export there yields `undefined`, the quote check throws, no
239
- * relay key is learned, and every admin read on a hosted relay goes out with no
240
- * credential.
241
- */
242
- function dcapVerifyFrom(mod) {
243
- const ns = mod;
244
- const verify = typeof ns.verify === 'function' ? ns.verify : ns.default?.verify;
245
- if (typeof verify !== 'function') {
246
- throw new TypeError('@phala/dcap-qvl loaded without a verify function');
247
- }
248
- return verify;
249
- }
250
- /**
251
- * Thrown by every step of learning a relay's key that only moves bytes: reading
252
- * the release the relay names, fetching that release, loading the quote
253
- * library, reaching the relay for its quote. Such a step failing says nothing
254
- * about the relay's image, so the attempt is worth making again.
255
- *
256
- * Anything else thrown while learning the key is the verification itself
257
- * saying no, and that is final.
258
- */
259
- class RelayKeyTransportError extends Error {
260
- constructor(step, cause) {
261
- super(`${step}: ${cause instanceof Error ? cause.message : String(cause)}`);
262
- this.step = step;
263
- this.cause = cause;
264
- this.name = 'RelayKeyTransportError';
265
- }
266
- }
267
- /** `step` run so that whatever it throws is a transport failure. */
268
- async function transport(step, run) {
269
- try {
270
- return await run();
271
- }
272
- catch (e) {
273
- throw e instanceof RelayKeyTransportError ? e : new RelayKeyTransportError(step, e);
274
- }
275
- }
276
- /**
277
- * `fetch` for the relay's own endpoints, failing as transport on no answer or
278
- * a non-2xx one. The response is still handed back on a non-2xx status — the
279
- * caller's own reading of it stays what it was — but the attempt is marked, so
280
- * whatever the caller then throws about it is read as transport too.
281
- */
282
- function relayFetch(marked) {
283
- return async (input, init) => {
284
- let response;
285
- try {
286
- response = await globalThis.fetch(input, init);
287
- }
288
- catch (e) {
289
- marked.failure = e;
290
- throw new RelayKeyTransportError(`reaching ${String(input)}`, e);
291
- }
292
- if (!response.ok)
293
- marked.failure ?? (marked.failure = new Error(`${String(input)} answered HTTP ${response.status}`));
294
- return response;
295
- };
296
- }
297
- /**
298
- * A verifier for a hosted relay's quote: the signed mero-tee release it says it
299
- * runs (fetched from the public mirror, trusted only for its signature), Intel's
300
- * chain, and all five registers of that release's image.
301
- *
302
- * The cloud's mirror is the only source a browser can read. The same two files
303
- * are assets of the mero-tee GitHub release, but GitHub serves release assets
304
- * from `release-assets.githubusercontent.com` with no CORS header (and
305
- * `github.com/.../releases/download` redirects there with none either), so a
306
- * page cannot read them. A mirror that does not answer is therefore waited out
307
- * by the caller ({@link learnRelayNodeKey}) rather than routed around.
308
- *
309
- * `@phala/dcap-qvl` is loaded on the first quote it checks: it is most of the
310
- * weight, and an app that never meets a real relay never needs it. A failed
311
- * download is forgotten, so the next attempt tries again.
312
- */
313
- function relayQuoteVerifier(relayUrl, fetchRelay) {
314
- const verify = async (attestation) => {
315
- const dcapVerify = await transport('loading the quote verifier', () => {
316
- loadedDcap ?? (loadedDcap = import('@phala/dcap-qvl').then((mod) => dcapVerifyFrom(mod), (error) => {
317
- loadedDcap = undefined;
318
- throw error;
319
- }));
320
- return loadedDcap;
321
- });
322
- return createSignedReleaseVerifier({
323
- dcapVerify,
324
- // Fetched only. Its signature, its version and the quote's registers are
325
- // all checked by the verifier after this returns, and none of that is a
326
- // transport question.
327
- release: () => transport('fetching the release the relay runs', async () => fetchNodeRelease(cloudNodeReleaseUrl(DEFAULT_RELEASE_MIRROR, await fetchNodeReleaseVersion(relayUrl, { fetch: fetchRelay })))),
328
- profile: TRUSTED_RELAY_PROFILE,
329
- minReleaseVersion: MIN_RELAY_RELEASE,
330
- })(attestation);
331
- };
332
- return Object.assign(verify, { includeCollateral: true });
333
- }
334
- /**
335
- * The relay's node key: the pinned one, or else learned from the relay's TEE
336
- * attestation (see mero-js `attestRelayNodeKey`) and pinned for next time.
337
- *
338
- * A mock quote proves nothing about hardware, so one is accepted only from a
339
- * loopback relay — a local rig. A real one must be a signed release of the
340
- * trusted profile ({@link relayQuoteVerifier}).
341
- *
342
- * `fresh` skips the pin and attests now, for a step that hands the relay
343
- * something it keeps — founding a namespace through it. A pinned key was
344
- * checked once, on some earlier day, against whatever answered then.
345
- */
346
- export async function attemptRelayNodeKey(relayUrl, options = {}) {
347
- const pinned = options.fresh ? null : readPinnedRelayNodeKey(relayUrl);
348
- if (pinned)
349
- return { kind: 'learned', nodeKey: pinned };
350
- const marked = { failure: null };
351
- const fetchRelay = relayFetch(marked);
352
- try {
353
- // A loopback relay is a dev rig answering with mock quotes; anything else
354
- // must prove its image.
355
- const local = isLoopback(relayUrl);
356
- const { nodeKey } = await attestRelayNodeKey({
357
- relayUrl,
358
- allowMock: local,
359
- fetch: fetchRelay,
360
- ...(local ? {} : { verify: relayQuoteVerifier(relayUrl, fetchRelay) }),
361
- });
362
- pinRelayNodeKey(relayUrl, nodeKey);
363
- return { kind: 'learned', nodeKey };
364
- }
365
- catch (e) {
366
- if (e instanceof RelayKeyTransportError || marked.failure)
367
- return { kind: 'unavailable', error: e };
368
- return { kind: 'refused', error: e };
369
- }
370
- }
371
- /**
372
- * The relay's node key: the pinned one, or else learned from the relay's TEE
373
- * attestation in one attempt; `null` when that attempt did not learn it, for
374
- * whatever reason. Callers that must end up with the key use
375
- * {@link learnRelayNodeKey}.
376
- */
377
- export async function resolveRelayNodeKey(relayUrl) {
378
- const attempt = await attemptRelayNodeKey(relayUrl);
379
- if (attempt.kind === 'learned')
380
- return attempt.nodeKey;
381
- console.warn('[mero-js] could not learn the relay node key from its attestation', attempt.error);
382
- return null;
383
- }
384
- /**
385
- * How long to wait before each retry of an unavailable relay key: quickly at
386
- * first, for a blip or a cold cache, then every 30 s for as long as it takes.
387
- */
388
- export const RELAY_NODE_KEY_RETRY_MS = [1000, 2000, 5000, 10000, 30000];
389
- function pause(ms, signal) {
390
- return new Promise((resolve) => {
391
- if (signal?.aborted)
392
- return resolve();
393
- const timer = setTimeout(done, ms);
394
- function done() {
395
- clearTimeout(timer);
396
- signal?.removeEventListener('abort', done);
397
- resolve();
398
- }
399
- signal?.addEventListener('abort', done);
400
- });
401
- }
402
- /**
403
- * The relay's node key, however long the way to it is down.
404
- *
405
- * One failed fetch of the release used to cost the whole session: no key, so no
406
- * relay session, so every admin read and every event stream unauthenticated
407
- * until a reload. A transport failure here is now waited out with backoff
408
- * ({@link RELAY_NODE_KEY_RETRY_MS}) until the key is learned or `signal` aborts.
409
- *
410
- * A refusal ends it at once with `null`. A quote that did not verify is an
411
- * answer, and asking again is not how it becomes a different one.
412
- */
413
- export async function learnRelayNodeKey(relayUrl, options = {}) {
414
- const { signal } = options;
415
- for (let failures = 0; !signal?.aborted; failures += 1) {
416
- const attempt = await attemptRelayNodeKey(relayUrl);
417
- if (attempt.kind === 'learned')
418
- return attempt.nodeKey;
419
- if (attempt.kind === 'refused') {
420
- console.warn("[mero-js] refusing the relay's attestation; its node key stays unknown", attempt.error);
421
- return null;
422
- }
423
- const wait = RELAY_NODE_KEY_RETRY_MS[Math.min(failures, RELAY_NODE_KEY_RETRY_MS.length - 1)];
424
- console.warn(`[mero-js] could not reach what proves the relay's node key; retrying in ${wait / 1000} s`, attempt.error);
425
- await pause(wait, signal);
426
- }
427
- return null;
428
- }
429
- /** Milliseconds-since-epoch a JWT expires at, or `null` if unreadable. */
430
- function jwtExpiryMs(token) {
431
- try {
432
- const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
433
- return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
434
- }
435
- catch {
436
- return null;
437
- }
438
- }
439
- // ------------------------------------------------------------------- sealing
440
- //
441
- // Everything a delegated session sends a hosted relay is sealed to the relay's
442
- // attested TD (mero-js `createAttestedSealedFetch`), so TLS that ends at the
443
- // relay's ingress, and whoever holds its certificate, reads none of it: not the
444
- // login, not the token, not a query or its result, not an event.
445
- //
446
- // A relay whose node leaves auth to its proxy, and has no
447
- // `[server.sealed] forward_auth`, cannot guard a logged-in route inside the
448
- // envelope and answers `403 sealed_route_unguarded` from inside it. Only then
449
- // does a request go out plain, as it always did before sealing: that answer is
450
- // sealed under the session, so nobody on the way can forge it to force the
451
- // downgrade. Anything else that goes wrong, an attestation refused or a relay
452
- // that does not answer, fails the request; it never falls back.
453
- const sealedFetches = new Map();
454
- /** Forget every relay's sealed transport. For tests. */
455
- export function forgetRelaySealing() {
456
- sealedFetches.clear();
457
- }
458
- /** Whether a sealed answer is the TD saying it cannot guard this route sealed. */
459
- async function isUnguardedRefusal(response) {
460
- if (response.status !== 403)
461
- return false;
462
- try {
463
- const body = (await response.clone().json());
464
- return body.error?.code === 'sealed_route_unguarded';
465
- }
466
- catch {
467
- return false;
468
- }
469
- }
470
- /** Whether `request` needs a session the relay's proxy checks: a token, or a login. */
471
- function needsSession(request) {
472
- return request.headers.has('authorization') || new URL(request.url).pathname.startsWith('/auth/');
473
- }
474
- function sealedRelayFetch(relayUrl) {
475
- const plain = (input, init) => globalThis.fetch(input, init);
476
- const sealed = createAttestedSealedFetch({
477
- baseUrl: relayUrl,
478
- verify: relayQuoteVerifier(relayUrl, plain),
479
- fetch: plain,
480
- });
481
- // Set once the TD has said it cannot guard a logged-in call: those go plain
482
- // straight away instead of paying a refused round trip each.
483
- let unguarded = false;
484
- return async (input, init) => {
485
- const request = new Request(input, init);
486
- if (unguarded && needsSession(request))
487
- return plain(request);
488
- const response = await sealed(request.clone());
489
- if (!(await isUnguardedRefusal(response)))
490
- return response;
491
- if (!unguarded) {
492
- unguarded = true;
493
- console.warn(`[mero-js] ${relayOrigin(relayUrl)} cannot guard logged-in calls sealed (no forward_auth); ` +
494
- 'they go over its TLS until it does');
495
- }
496
- return plain(request);
497
- };
498
- }
499
- /**
500
- * The `fetch` a delegated session uses for `relayUrl`: sealed to its attested
501
- * TD, one per relay so its session is shared. `undefined` for a loopback relay,
502
- * a dev rig answering mock quotes, which keeps the global `fetch`.
503
- */
504
- export function relayTransportFetch(relayUrl) {
505
- if (isLoopback(relayUrl))
506
- return undefined;
507
- const origin = relayOrigin(relayUrl);
508
- let found = sealedFetches.get(origin);
509
- if (!found) {
510
- found = sealedRelayFetch(relayUrl);
511
- sealedFetches.set(origin, found);
512
- }
513
- return found;
514
- }
515
- /**
516
- * A lazily minted `account_proof` session on the relay.
517
- *
518
- * A hosted relay's `/admin-api/` sits behind the node's forward-auth, which
519
- * knows no request proof — it answers a Bearer token or 401. Logging in with
520
- * the device certificate is how a keyholder gets that token. Minted on first
521
- * use and re-minted once it has expired. On failure it throws a
522
- * {@link RelayLoginError}, which the HTTP client lets through instead of sending
523
- * the admin request unauthenticated: the caller sees why the login failed —
524
- * `signed-out` for a refused credential, `unavailable` for a relay that did not
525
- * answer — rather than the node's 401 or the same transport error twice.
526
- * Nothing is cached on failure, so the next call tries to log in again.
527
- */
528
- function relaySession(s, nodeKey) {
529
- let token = null;
530
- let inflight = null;
531
- return async () => {
532
- const exp = token ? jwtExpiryMs(token) : null;
533
- if (token && (exp === null || exp - Date.now() > 30000))
534
- return token;
535
- if (!inflight) {
536
- inflight = login({
537
- nodeUrl: s.relayUrl,
538
- // Sealed: the token is the session, and the TLS ends outside the TD.
539
- fetch: relayTransportFetch(s.relayUrl),
540
- node: nodeKey,
541
- deviceSecret: s.deviceSecret,
542
- accountProof: s.credential,
543
- audience: defaultAudience(),
544
- })
545
- .then((minted) => {
546
- token = minted.accessToken;
547
- return token;
548
- })
549
- .catch((e) => {
550
- token = null;
551
- throw new RelayLoginError(s.relayUrl, e);
552
- })
553
- .finally(() => {
554
- inflight = null;
555
- });
556
- }
557
- return inflight;
558
- };
559
- }
560
- /** What the node said each `relay|context|method` is; survives client rebuilds. */
561
- const methodKinds = new Map();
562
- /** Forget what was learned about which methods read. For tests. */
563
- export function forgetMethodKinds() {
564
- methodKinds.clear();
565
- }
566
- /** Core's refusal of a query for a method the ABI does not declare read-only. */
567
- function isNotAView(e) {
568
- return typeof e === 'object' && e !== null && e.status === 409;
569
- }
570
- /**
571
- * `client.rpc` with reads routed through `client.admin.queryContext`: the
572
- * session-authenticated query for a method the node knows as a view, the
573
- * warrant for everything else. Only for a client with a session, since the
574
- * query route wants one.
575
- */
576
- function readsThroughQuery(client, relayUrl) {
577
- const inner = client.rpc;
578
- const origin = relayOrigin(relayUrl);
579
- const run = async (params) => {
580
- const key = `${origin}|${params.contextId}|${params.method}`;
581
- if (methodKinds.get(key) !== 'write') {
582
- try {
583
- const { returns } = await client.admin.queryContext(params.contextId, {
584
- method: params.method,
585
- // The node defaults a warrant's missing `argsJson` to `{}`; the same here.
586
- argsJson: params.argsJson ?? {},
587
- });
588
- methodKinds.set(key, 'read');
589
- // No root hash: a read changes nothing, and `undefined` is "unknown
590
- // here", never "nothing changed" (see mero-js `ExecuteResult`).
591
- return { returns: returns, transport: 'relay' };
592
- }
593
- catch (e) {
594
- if (isNotAView(e))
595
- methodKinds.set(key, 'write');
596
- }
597
- }
598
- return inner.executeWithMetadata(params);
599
- };
600
- return {
601
- get kind() {
602
- return inner.kind;
603
- },
604
- get canSubscribe() {
605
- return inner.canSubscribe;
606
- },
607
- execute: async (params) => (await run(params)).returns,
608
- executeWithMetadata: run,
609
- migrateMyEntries: (contextId) => inner.migrateMyEntries(contextId),
610
- countMyPending: (contextId) => inner.countMyPending(contextId),
611
- };
612
- }
613
- /**
614
- * An event stream that opens nothing: for a hosted relay whose node key is not
615
- * attested yet.
616
- *
617
- * A hosted relay's `/sse` sits behind forward-auth that knows a Bearer token
618
- * and nothing else, so the proof-authenticated stream mero-js builds without a
619
- * node key is answered 401 `missing_token` — not one of the terminal auth
620
- * errors, so `SseClient` reconnected on a timer for as long as the key stayed
621
- * unknown. The hooks take `client.events` and connect without asking, so the
622
- * stream handed out meanwhile is a real `SseClient` whose connect waits for a
623
- * close instead of fetching: it holds its subscriptions, emits no error, and
624
- * reaches the network never. The client built once the key is learned brings
625
- * the session-backed stream, and the hooks' effects move onto it.
626
- */
627
- function dormantEvents(relayUrl) {
628
- return new SseClient({
629
- baseUrl: relayUrl,
630
- authorize: async () => ({}),
631
- fetch: (_url, init) => new Promise((_, reject) => {
632
- const abort = () => reject(Object.assign(new Error('the stream was closed before the relay was attested'), { name: 'AbortError' }));
633
- if (init?.signal?.aborted)
634
- return abort();
635
- init?.signal?.addEventListener('abort', abort, { once: true });
636
- }),
637
- });
638
- }
639
- /**
640
- * The relay client as a delegated session uses it: `rpc` reading by query when
641
- * the client has a session, and no events until the relay is attested.
642
- * Everything else is the client's own, `this` kept.
643
- */
644
- function delegatedClient(client, opts) {
645
- const rpc = opts.session ? readsThroughQuery(client, opts.relayUrl) : null;
646
- let dormant = null;
647
- return new Proxy(client, {
648
- get(target, prop) {
649
- if (prop === 'rpc' && rpc)
650
- return rpc;
651
- if (!opts.attested) {
652
- if (prop === 'canSubscribe')
653
- return false;
654
- if (prop === 'events')
655
- return (dormant ?? (dormant = dormantEvents(opts.relayUrl)));
656
- if (prop === 'close') {
657
- return () => {
658
- dormant?.close();
659
- target.close();
660
- };
661
- }
662
- }
663
- const value = Reflect.get(target, prop, target);
664
- return typeof value === 'function' ? value.bind(target) : value;
665
- },
666
- });
667
- }
668
- /**
669
- * Build the client a delegated session talks through.
670
- *
671
- * The result is a relay-transport `MeroClient`. Its `rpc.execute` takes the same
672
- * named arguments a node client's does, which is what lets a generated ABI
673
- * client — and every hook that only calls `execute` — work unchanged. What it
674
- * also has is `admin` and `events`, pointed at the relay and authenticated as
675
- * the account by the device certificate, so the caller-scoped reads answer this
676
- * account's own contexts and namespaces rather than the node's.
677
- */
678
- export function buildDelegatedClient(s,
679
- // Kept for callers; the client no longer depends on the chosen context.
680
- _contextId) {
681
- // No relay, no client — and deliberately no placeholder either.
682
- //
683
- // A relay-transport client IS a URL plus a credential; with no URL there is
684
- // nothing to post an intent to. `null` propagates as "not connected for
685
- // writing" while the session stays authenticated, which is the honest shape
686
- // for a brand-new account: it exists, it has a certificate, and it has not
687
- // been invited anywhere yet. A stand-in URL would make every write fail as a
688
- // refused warrant against a node that was never chosen.
689
- if (!s.relayUrl)
690
- return null;
691
- const nodeKey = readPinnedRelayNodeKey(s.relayUrl);
692
- const client = createMeroClient({
693
- transport: 'relay',
694
- // Every request sealed to the relay's attested TD: intents, admin reads,
695
- // queries, the login and events (see `relayTransportFetch`).
696
- fetch: relayTransportFetch(s.relayUrl),
697
- relay: {
698
- relayUrl: s.relayUrl,
699
- authorAccount: s.account,
700
- authorProof: s.credential,
701
- deviceSecret: s.deviceSecret,
702
- // One sequence per relay, whatever context is chosen: `execute` takes a
703
- // context per call, so the nonce source must not depend on this one.
704
- nonces: persistedNonces(s.relayUrl),
705
- },
706
- // The certificate authenticates the requests too, not just the writes.
707
- //
708
- // This is the whole of what reads and events need: the device key signs each
709
- // request, and the node verifies it against the account's certificate with no
710
- // store read and no prior relationship. Measured against a node built from
711
- // core `aecba573b`: `listNamespaces` and `getContexts` answer `200` and are
712
- // caller-scoped, `/sse` answers `200`, and a real write arrives on the stream
713
- // as a `StateMutation` frame.
714
- //
715
- // What this replaces is a session minted by `login()` plus the relay node's
716
- // signing key learned out of band — two inputs, one of which the cloud does
717
- // not publish, for an answer the certificate already gives.
718
- proof: { credential: s.credential, deviceSecret: s.deviceSecret },
719
- // A session, when this relay's node key is pinned. A hosted relay's
720
- // forward-auth accepts a Bearer token and not the proof above, so without
721
- // one every admin read there is a 401. Given a session the client uses it
722
- // for every admin call; a relay with no pinned key keeps the proof path.
723
- session: nodeKey ? relaySession({ ...s, relayUrl: s.relayUrl }, nodeKey) : undefined,
724
- // Events too. `/sse` on a relay sits behind the same forward-auth, so the
725
- // proof above gets a 401 there; with the node key the client observes
726
- // through its own device-certificate login instead.
727
- observe: nodeKey ? { nodeKey } : undefined,
728
- });
729
- // Events wait for the relay's node key on a hosted relay, whose forward-auth
730
- // answers the proof-only stream 401 and would have it reconnect forever. A
731
- // loopback relay is a dev rig that takes the proof as a node does.
732
- return delegatedClient(client, {
733
- relayUrl: s.relayUrl,
734
- session: nodeKey !== null,
735
- attested: nodeKey !== null || isLoopback(s.relayUrl),
736
- });
737
- }
738
- // ------------------------------------------------------------------ relay map
739
- //
740
- // An account's namespaces may be served by different relays. A relay admits
741
- // the account to a namespace and then serves that namespace's contexts, so the
742
- // client keeps, per account, which relay serves which namespace and context:
743
- //
744
- // namespaces: { <namespace id>: <relay url> } — learned when a join succeeds
745
- // contexts: { <context id>: <relay url> } — learned from each relay's
746
- // caller-scoped listing
747
- //
748
- // In localStorage, beside the pinned node keys: URLs and ids only, nothing
749
- // secret, and it has to survive a reload for the open context to find its
750
- // relay. Losing it costs a re-listing, not access.
751
- const RELAY_MAP_PREFIX = 'calimero.delegated.relays.';
752
- export function readRelayMap(account) {
753
- try {
754
- const raw = localStorage.getItem(RELAY_MAP_PREFIX + account);
755
- const parsed = raw ? JSON.parse(raw) : {};
756
- return { namespaces: parsed.namespaces ?? {}, contexts: parsed.contexts ?? {} };
757
- }
758
- catch {
759
- return { namespaces: {}, contexts: {} };
760
- }
761
- }
762
- function writeRelayMap(account, map) {
763
- try {
764
- localStorage.setItem(RELAY_MAP_PREFIX + account, JSON.stringify(map));
765
- }
766
- catch {
767
- /* unpersisted: this page view still routes from what it lists */
768
- }
769
- }
770
- /** Record the relay that admitted this account to a namespace (and, when known, the context it came for). */
771
- export function rememberRelay(account, relayUrl, at) {
772
- const map = readRelayMap(account);
773
- const url = relayOrigin(relayUrl);
774
- if (at.namespaceId)
775
- map.namespaces[at.namespaceId] = url;
776
- if (at.contextId)
777
- map.contexts[at.contextId] = url;
778
- writeRelayMap(account, map);
779
- }
780
- /** The relay that serves a context, if this account has learned it. */
781
- export function relayForContext(account, contextId) {
782
- return readRelayMap(account).contexts[contextId] ?? null;
783
- }
784
- /**
785
- * A session moving to `next`, keeping the relay's executor account the previous
786
- * one knew when both are the same relay.
787
- *
788
- * A join hands back a session built from the admitting node alone, which has no
789
- * executor on it. When that node is the relay the account was already on — the
790
- * one the cloud assigned it, say — dropping the executor would cost nothing
791
- * today (the joined namespace can answer for it) but leaves the session knowing
792
- * less than it did. On a different relay the old executor is the wrong account,
793
- * so it is left behind.
794
- */
795
- export function carryExecutorAccount(prev, next) {
796
- if (next.executorAccount || !prev?.executorAccount || !prev.relayUrl || !next.relayUrl)
797
- return next;
798
- if (relayOrigin(prev.relayUrl) !== relayOrigin(next.relayUrl))
799
- return next;
800
- return { ...next, executorAccount: prev.executorAccount };
801
- }
802
- /** Every relay this account is known to use: the session's, and each in the map. */
803
- export function knownRelays(s) {
804
- const map = readRelayMap(s.account);
805
- const all = [s.relayUrl, ...Object.values(map.namespaces), ...Object.values(map.contexts)]
806
- .filter((u) => typeof u === 'string' && u.length > 0)
807
- .map(relayOrigin);
808
- return [...new Set(all)];
809
- }
810
- export class RelaysUnavailableError extends Error {
811
- constructor(failures) {
812
- super(`none of this account's relays answered: ${failures.map((f) => `${f.relayUrl} (${f.kind})`).join(', ')}`);
813
- this.failures = failures;
814
- this.name = 'RelaysUnavailableError';
815
- this.relays = failures.map((f) => f.relayUrl);
816
- }
817
- /** Every relay refused its attestation: retrying will not help. */
818
- get refused() {
819
- return this.failures.every((f) => f.kind === 'refused');
820
- }
821
- }
822
- /**
823
- * One relay's client for a listing, or the failure that stopped it. The key's
824
- * own outcome is kept, so a refused attestation is not reported as a relay
825
- * that merely did not answer.
826
- */
827
- async function listingClient(s, relayUrl) {
828
- const key = await attemptRelayNodeKey(relayUrl);
829
- if (key.kind !== 'learned') {
830
- console.warn('[mero-js] could not learn the relay node key from its attestation', key.error);
831
- return { failure: { relayUrl, kind: key.kind, error: key.error } };
832
- }
833
- const client = buildDelegatedClient({ ...s, relayUrl }, null);
834
- if (!client)
835
- return { failure: { relayUrl, kind: 'unavailable', error: new Error(`no client for ${relayUrl}`) } };
836
- return { client };
837
- }
838
- /**
839
- * This account's contexts on every relay it knows, each tagged with the relay
840
- * that listed it — and that relay recorded as the one serving it.
841
- *
842
- * Each relay answers only for itself and only for this caller, so the union is
843
- * exactly the account's contexts across relays. A relay that cannot be reached
844
- * or logged in to is skipped (its contexts simply do not appear), not fatal,
845
- * unless it is every relay: then {@link RelaysUnavailableError}.
846
- */
847
- export async function listDelegatedContexts(s) {
848
- const out = [];
849
- const map = readRelayMap(s.account);
850
- const relays = knownRelays(s);
851
- const failures = [];
852
- for (const relayUrl of relays) {
853
- const got = await listingClient(s, relayUrl);
854
- if ('failure' in got) {
855
- failures.push(got.failure);
856
- continue;
857
- }
858
- const { client } = got;
859
- try {
860
- const { contexts } = await client.admin.getContexts();
861
- for (const c of contexts ?? []) {
862
- out.push({ ...c, relayUrl });
863
- map.contexts[c.id] = relayUrl;
864
- }
865
- }
866
- catch (e) {
867
- failures.push({ relayUrl, kind: 'unavailable', error: e });
868
- console.warn(`[mero-js] could not list contexts on ${relayUrl}`, e);
869
- }
870
- }
871
- writeRelayMap(s.account, map);
872
- if (relays.length > 0 && failures.length === relays.length)
873
- throw new RelaysUnavailableError(failures);
874
- return out;
875
- }
876
- /**
877
- * This account's namespaces on every relay it knows, each tagged with the relay
878
- * that listed it and recorded as that namespace's relay. Same rules as
879
- * {@link listDelegatedContexts}: each relay answers for itself and this caller
880
- * only (`namespace:list-own`); an unreachable relay is skipped, and every
881
- * relay unreachable is {@link RelaysUnavailableError}.
882
- */
883
- export async function listDelegatedNamespaces(s) {
884
- var _a, _b;
885
- const out = [];
886
- const seen = new Set();
887
- const map = readRelayMap(s.account);
888
- const relays = knownRelays(s);
889
- const failures = [];
890
- for (const relayUrl of relays) {
891
- const got = await listingClient(s, relayUrl);
892
- if ('failure' in got) {
893
- failures.push(got.failure);
894
- continue;
895
- }
896
- const { client } = got;
897
- try {
898
- for (const ns of await client.admin.listNamespaces()) {
899
- if (seen.has(ns.namespaceId))
900
- continue;
901
- seen.add(ns.namespaceId);
902
- out.push({ ...ns, relayUrl });
903
- (_a = map.namespaces)[_b = ns.namespaceId] ?? (_a[_b] = relayUrl);
904
- }
905
- }
906
- catch (e) {
907
- failures.push({ relayUrl, kind: 'unavailable', error: e });
908
- console.warn(`[mero-js] could not list namespaces on ${relayUrl}`, e);
909
- }
910
- }
911
- writeRelayMap(s.account, map);
912
- if (relays.length > 0 && failures.length === relays.length)
913
- throw new RelaysUnavailableError(failures);
914
- return out;
915
- }