@calimero-network/mero-js 22.7.0 → 22.9.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 (76) hide show
  1. package/dist/account/account.d.ts +99 -4
  2. package/dist/account/account.js +0 -0
  3. package/dist/account/index.d.ts +6 -2
  4. package/dist/account/index.js +3 -1
  5. package/dist/admin-api/admin-client.d.ts +154 -2
  6. package/dist/admin-api/admin-client.js +255 -1
  7. package/dist/admin-api/admin-types.d.ts +193 -5
  8. package/dist/cloud/cloud-client.d.ts +38 -8
  9. package/dist/cloud/cloud-client.js +38 -21
  10. package/dist/cloud/connect.d.ts +101 -2
  11. package/dist/cloud/connect.js +84 -8
  12. package/dist/cloud/enrol-redirect.d.ts +195 -0
  13. package/dist/cloud/enrol-redirect.js +236 -0
  14. package/dist/cloud/index.d.ts +6 -2
  15. package/dist/cloud/index.js +7 -1
  16. package/dist/cloud/link-redirect.d.ts +85 -0
  17. package/dist/cloud/link-redirect.js +105 -0
  18. package/dist/crypto/internal.d.ts +15 -0
  19. package/dist/crypto/internal.js +18 -0
  20. package/dist/device-cert/device-cert.d.ts +96 -2
  21. package/dist/device-cert/device-cert.js +101 -11
  22. package/dist/device-cert/device-scope.d.ts +58 -0
  23. package/dist/device-cert/device-scope.js +76 -0
  24. package/dist/device-cert/index.d.ts +4 -2
  25. package/dist/device-cert/index.js +2 -1
  26. package/dist/events/sse.d.ts +22 -2
  27. package/dist/events/sse.js +20 -11
  28. package/dist/http-client/http-types.d.ts +18 -0
  29. package/dist/http-client/web-client.js +21 -1
  30. package/dist/index.browser.mjs +2 -2
  31. package/dist/index.browser.mjs.map +4 -4
  32. package/dist/index.cjs +1955 -496
  33. package/dist/index.cjs.map +4 -4
  34. package/dist/index.d.ts +13 -4
  35. package/dist/index.js +21 -2
  36. package/dist/index.mjs +1955 -496
  37. package/dist/index.mjs.map +4 -4
  38. package/dist/invitation/index.d.ts +2 -0
  39. package/dist/invitation/index.js +2 -0
  40. package/dist/invitation/invitation.d.ts +106 -0
  41. package/dist/invitation/invitation.js +104 -0
  42. package/dist/login/login.d.ts +11 -1
  43. package/dist/login/login.js +5 -4
  44. package/dist/login/session.d.ts +21 -5
  45. package/dist/login/session.js +7 -15
  46. package/dist/namespace-op/namespace-op.d.ts +14 -2
  47. package/dist/namespace-op/namespace-op.js +13 -19
  48. package/dist/relay/index.d.ts +2 -2
  49. package/dist/relay/index.js +1 -1
  50. package/dist/relay/nonce-source.d.ts +135 -0
  51. package/dist/relay/nonce-source.js +187 -19
  52. package/dist/relay/relay-client.d.ts +60 -5
  53. package/dist/relay/relay-client.js +84 -4
  54. package/dist/relay-attestation/attest-node-key.d.ts +66 -0
  55. package/dist/relay-attestation/attest-node-key.js +110 -0
  56. package/dist/relay-attestation/index.d.ts +1 -0
  57. package/dist/relay-attestation/index.js +1 -0
  58. package/dist/request-proof/authorizer.d.ts +48 -0
  59. package/dist/request-proof/authorizer.js +65 -0
  60. package/dist/request-proof/index.d.ts +4 -0
  61. package/dist/request-proof/index.js +2 -0
  62. package/dist/request-proof/request-proof.d.ts +56 -0
  63. package/dist/request-proof/request-proof.js +111 -0
  64. package/dist/rpc/index.d.ts +21 -0
  65. package/dist/rpc/index.js +20 -0
  66. package/dist/transport/index.d.ts +6 -0
  67. package/dist/transport/index.js +3 -0
  68. package/dist/transport/mero-client.d.ts +286 -0
  69. package/dist/transport/mero-client.js +379 -0
  70. package/dist/transport/relay-observer.d.ts +152 -0
  71. package/dist/transport/relay-observer.js +173 -0
  72. package/dist/transport/relay-transport.d.ts +73 -0
  73. package/dist/transport/relay-transport.js +94 -0
  74. package/dist/transport/types.d.ts +100 -0
  75. package/dist/transport/types.js +32 -0
  76. package/package.json +1 -1
@@ -0,0 +1,379 @@
1
+ /**
2
+ * One client, two transports — chosen once, at construction.
3
+ *
4
+ * # The problem this exists for
5
+ *
6
+ * An app that wants to move from "the user has a node" to "the user has a key
7
+ * and a relay" had to rewrite itself, because the two write paths are different
8
+ * objects with different method shapes. That makes the transport a property of
9
+ * the *application source*, when it is really a property of the connection: the
10
+ * same screen, the same hook, the same `execute` call wants to work either way.
11
+ *
12
+ * `createMeroClient` takes the decision at construction and nowhere else. After
13
+ * that, `client.rpc.execute({ contextId, method, argsJson })` is the same call
14
+ * against a node and against a relay — same arguments, same return type. The
15
+ * node shape is the one both speak, because shipped apps already write it and
16
+ * the alternative would be a breaking change wearing an abstraction's clothes.
17
+ *
18
+ * # Defaulting to the node is a compatibility guarantee
19
+ *
20
+ * `createMeroClient({ baseUrl })` is `createMeroJs({ baseUrl })` with a facade
21
+ * in front. No `transport` field means `'node'`, so nothing an existing app
22
+ * does can accidentally select the relay.
23
+ *
24
+ * # Events work on both transports, given one input
25
+ *
26
+ * A relay is an ordinary node, so it serves `/sse` and `/ws` to a credential it
27
+ * issued, and a keyholder can obtain one by logging in with its account proof.
28
+ * `client.events` and `client.ws` are therefore the same call on both
29
+ * transports — provided the caller supplies the relay node's device signing
30
+ * key, which must be learned out of band and is the one thing that cannot be
31
+ * inferred. See `./relay-observer.ts` for why, and mdma #312 for the hosted
32
+ * case. Without it, {@link MeroClient.canSubscribe} is `false` and the
33
+ * accessors throw naming exactly that.
34
+ *
35
+ * # What is deliberately NOT unified
36
+ *
37
+ * Reads and admin. A relay serves exactly two routes to a caller with no
38
+ * credential — `GET` and `POST /admin-api/contexts/:id/intents` — and while a
39
+ * session obtained by logging in carries `context:query`, `sdk.admin` wants
40
+ * `admin`, which the `account_proof` provider does not grant and a keyholder
41
+ * should never ask for.
42
+ *
43
+ * So those accessors throw on a relay client, with a message naming what is
44
+ * missing, rather than returning something that quietly connects somewhere
45
+ * else. Falling back to "some node" would be the worst of the options: the app
46
+ * would observe a node it never chose, under a credential it never granted, and
47
+ * find out only when the two disagreed. `canSubscribe` and `transport` are
48
+ * there so an app can branch *before* touching any of it.
49
+ */
50
+ import { MeroJs } from '../mero-js.js';
51
+ import { RelayClient } from '../relay/relay-client.js';
52
+ import { RelayTransport } from './relay-transport.js';
53
+ import { RelayObserver, defaultAudience } from './relay-observer.js';
54
+ import { createAdminApiClient, createAdminApiClientFromHttpClient } from '../admin-api/index.js';
55
+ import { createHttpClient } from '../http-client/index.js';
56
+ import { createProofAuthorizer } from '../request-proof/index.js';
57
+ import { SseClient } from '../events/sse.js';
58
+ /** Message shared by every node-only accessor, so the reason is stated once. */
59
+ function relayHasNo(surface, why) {
60
+ return new Error(`this client was constructed with the relay transport, which has no ${surface}: ${why}. ` +
61
+ 'Construct a node client (createMeroClient({ baseUrl })) for that surface — ' +
62
+ 'nothing here will silently connect to a node you did not choose.');
63
+ }
64
+ /**
65
+ * The one thing a relay client can be missing, named precisely.
66
+ *
67
+ * Deliberately not the same message as {@link relayHasNo}: this is not "the
68
+ * relay transport has no such surface". The relay has the surface — it serves
69
+ * `/sse` and `/ws`, and the `account_proof` provider grants
70
+ * `context:subscribe`. What is missing is a single 32-byte value, and a caller
71
+ * who can obtain it needs to be told that, not told to go and build a node
72
+ * client.
73
+ */
74
+ function missingNodeKey(surface) {
75
+ return new Error(`this relay-transport client cannot open ${surface}: it was given no relay node key. ` +
76
+ 'A relay is an ordinary node and does serve /sse and /ws — but the login statement must be ' +
77
+ "signed against the node's device signing key, learned out of band (never read from the node, " +
78
+ 'and never derived from its peerId). Pass it as `observe: { nodeKey }`. ' +
79
+ 'For a cloud-hosted relay the cloud does not publish it yet — see mdma #312.');
80
+ }
81
+ /**
82
+ * Resolve an observe config against the relay client that will do the writing.
83
+ *
84
+ * Returns `null` for "no key, so no observation" — the single place that
85
+ * decision is made, so `canSubscribe`, `events` and `ws` cannot disagree about
86
+ * it. The signer and proof default to the relay's own: the same device that
87
+ * authors is the device that observes, and taking a second copy from the caller
88
+ * is how the two come to name different keys.
89
+ */
90
+ function buildObserver(relay, observe) {
91
+ const nodeKey = observe?.nodeKey;
92
+ if (!nodeKey)
93
+ return null;
94
+ return new RelayObserver({
95
+ nodeUrl: observe?.nodeUrl ?? relay.relayUrl,
96
+ nodeKey,
97
+ accountProof: observe?.accountProof ?? relay.authorProof,
98
+ audience: observe?.audience ?? defaultAudience(),
99
+ signer: observe?.signer ? async () => observe.signer : () => relay.authorSigner(),
100
+ clientName: observe?.clientName,
101
+ ttlSeconds: observe?.ttlSeconds,
102
+ fetch: observe?.fetch,
103
+ timeoutMs: observe?.timeoutMs,
104
+ });
105
+ }
106
+ /**
107
+ * A connection whose transport was decided at construction.
108
+ *
109
+ * Everything reachable on a node client is reachable in exactly the shape
110
+ * `MeroJs` exposes it, by delegation rather than re-implementation — this adds
111
+ * no behaviour to the node path and cannot drift from it.
112
+ */
113
+ export class MeroClient {
114
+ constructor(config) {
115
+ /** The relay's event session, or `null` on a node client / with no node key. */
116
+ this.relayObserver = null;
117
+ /** Admin reads against the relay, when a session or a proof was supplied. */
118
+ this.relayAdmin = null;
119
+ /**
120
+ * Events off the relay authenticated by a request proof, built lazily.
121
+ *
122
+ * Separate from {@link relayObserver} rather than a mode inside it: the
123
+ * observer's whole subject is a session addressed to a node key, and a proof
124
+ * has neither. Holding the authorizer instead of the client keeps construction
125
+ * free of the first signature.
126
+ */
127
+ this.proofAuthorizer = null;
128
+ this.relayUrl = null;
129
+ this.proofEvents = null;
130
+ if (config.transport === 'relay') {
131
+ this.transport = 'relay';
132
+ this.nodeClient = null;
133
+ const relay = config.relay instanceof RelayClient ? config.relay : new RelayClient(config.relay);
134
+ this.relayObserver = buildObserver(relay, config.observe);
135
+ this.rpcTransport = new RelayTransport(relay, this.relayObserver);
136
+ this.relayUrl = relay.relayUrl;
137
+ if (config.proof) {
138
+ this.proofAuthorizer = createProofAuthorizer(config.proof);
139
+ }
140
+ // A session wins where both are present. Not a preference between
141
+ // credentials but the node's own precedence: it answers a request carrying
142
+ // a token by that token and consults a proof only when there is none, so
143
+ // sending both would authenticate by the session anyway — and a client
144
+ // whose reads and whose events disagreed about which identity they ran as
145
+ // is the one outcome worth ruling out here.
146
+ if (config.session) {
147
+ const token = config.session;
148
+ this.relayAdmin = createAdminApiClient({
149
+ baseUrl: relay.relayUrl,
150
+ getAuthToken: typeof token === 'string' ? async () => token : token,
151
+ });
152
+ }
153
+ else if (this.proofAuthorizer) {
154
+ const authorizeRequest = this.proofAuthorizer;
155
+ this.relayAdmin = createAdminApiClientFromHttpClient(createHttpClient({
156
+ fetch: (url, init) => globalThis.fetch(url, init),
157
+ baseUrl: relay.relayUrl,
158
+ authorizeRequest,
159
+ }), { baseUrl: relay.relayUrl });
160
+ }
161
+ return;
162
+ }
163
+ this.transport = 'node';
164
+ // `transport` is a marker this class reads and `MeroJs` has never seen.
165
+ // Stripped rather than passed through, so nothing downstream has to know
166
+ // that the field exists.
167
+ const { transport: _ignored, ...nodeConfig } = config;
168
+ this.nodeClient = new MeroJs(nodeConfig);
169
+ this.rpcTransport = this.nodeClient.rpc;
170
+ }
171
+ /**
172
+ * The write surface. Identical on both transports — this is the whole point.
173
+ *
174
+ * On a node this IS the `RpcClient` a `MeroJs` would have handed out, not a
175
+ * wrapper around it, so `client.rpc` and `client.node.rpc` are the same
176
+ * object.
177
+ */
178
+ get rpc() {
179
+ return this.rpcTransport;
180
+ }
181
+ /**
182
+ * Whether `events` / `ws` will work on this client.
183
+ *
184
+ * Always `true` on the node transport. On the relay transport it says whether
185
+ * a node key was supplied — the relay can be observed, but only by a caller
186
+ * who can name the key the login statement is signed against.
187
+ */
188
+ get canSubscribe() {
189
+ return this.rpcTransport.canSubscribe || this.proofAuthorizer !== null;
190
+ }
191
+ /** The underlying `MeroJs`. Node transport only. */
192
+ get node() {
193
+ if (!this.nodeClient) {
194
+ throw relayHasNo('node connection', 'it holds a signing key and a relay URL, not a node');
195
+ }
196
+ return this.nodeClient;
197
+ }
198
+ /** The underlying `RelayClient`, for `describe()`. Relay transport only. */
199
+ get relay() {
200
+ if (!(this.rpcTransport instanceof RelayTransport)) {
201
+ throw new Error('this client was constructed with the node transport and has no relay');
202
+ }
203
+ return this.rpcTransport.client;
204
+ }
205
+ /**
206
+ * Admin API. Node transport only.
207
+ *
208
+ * Two different things live behind this accessor today, and only one of them
209
+ * is genuinely unavailable through a relay.
210
+ *
211
+ * The **mutations** — `createNamespace`, `setMemberCapabilities`,
212
+ * `upgradeGroup` and the rest — are operator actions on somebody else's
213
+ * machine. A keyholder does not get those, and should not; admin is for
214
+ * admins.
215
+ *
216
+ * The **reads** are a different matter. `getContext`, `getContexts`,
217
+ * `getApplication`, `listNamespaces`, `getBlob` are an app asking what it is
218
+ * looking at, not an operator administering anything. They sit under
219
+ * `/admin-api/` for historical reasons, and a delegated session is refused
220
+ * them (measured: `403`, while the same session reads context state through
221
+ * `POST /contexts/{ctx}/query` and writes through `/intents` quite happily).
222
+ *
223
+ * So this is the one place where "the same code runs on both transports"
224
+ * does not hold yet, and it is a missing surface rather than a missing
225
+ * permission. A transport-independent read API is designed but not built; the
226
+ * throw below is deliberate until it exists, because the alternative — quietly
227
+ * reaching for a node the caller did not choose — is worse than a clear stop.
228
+ */
229
+ /**
230
+ * The admin surface.
231
+ *
232
+ * On a node client this is the node's own. On a relay client it exists only
233
+ * when a `session` was supplied, and it is **the relay's admin API
234
+ * authenticated as the account** — so the caller-scoped reads answer:
235
+ * `getContexts` and `listNamespaces` return this account's own, because
236
+ * `account_proof` grants `context:list-own` and `namespace:list-own`.
237
+ *
238
+ * Mutations are not filtered out here, deliberately. A delegated session does
239
+ * not carry the grants they require, so the node refuses them with a `403`
240
+ * that names the missing permission — which is a better answer than a
241
+ * client-side guess, and keeps the policy in the one place that owns it.
242
+ */
243
+ get admin() {
244
+ if (this.nodeClient)
245
+ return this.nodeClient.admin;
246
+ if (this.relayAdmin)
247
+ return this.relayAdmin;
248
+ throw new Error('this relay-transport client has no admin surface because it was given neither a `proof` ' +
249
+ 'nor a `session`. A warrant authorises one intent and is not a credential for reading. ' +
250
+ 'The caller-scoped reads (getContexts, listNamespaces) want one of the other two: pass ' +
251
+ '`proof: { credential, deviceSecret }` — the device cert you already hold, which needs ' +
252
+ 'no login and no node key — or a `session` minted with `login()`. Context STATE reads ' +
253
+ 'work through execute/query either way.');
254
+ }
255
+ /** Auth API. Node transport only — a relay issues no node credential. */
256
+ get auth() {
257
+ return this.node.auth;
258
+ }
259
+ /**
260
+ * SSE events — on **both** transports.
261
+ *
262
+ * On a relay client this is an `SseClient` against the relay node, fed by a
263
+ * session minted with the author's account proof. The session is minted
264
+ * lazily, on the first token the stream asks for, so reading this getter
265
+ * performs no network call.
266
+ *
267
+ * Throws only when the relay client was given no node key: the one input
268
+ * that cannot be inferred. See {@link missingNodeKey}.
269
+ */
270
+ get events() {
271
+ if (this.nodeClient)
272
+ return this.nodeClient.events;
273
+ if (this.relayObserver)
274
+ return this.relayObserver.events;
275
+ if (this.proofAuthorizer && this.relayUrl) {
276
+ // Built on first use and then kept: a second `SseClient` would open a
277
+ // second stream and split the subscriptions across two sessions, so the
278
+ // caller would see some of its events on an object it no longer holds.
279
+ this.proofEvents ?? (this.proofEvents = new SseClient({
280
+ baseUrl: this.relayUrl,
281
+ authorize: this.proofAuthorizer,
282
+ }));
283
+ return this.proofEvents;
284
+ }
285
+ throw missingNodeKey('SSE events');
286
+ }
287
+ /** WebSocket events, on both transports. @experimental — prefer `events`. */
288
+ get ws() {
289
+ if (this.nodeClient)
290
+ return this.nodeClient.ws;
291
+ if (this.relayObserver)
292
+ return this.relayObserver.ws;
293
+ if (this.proofAuthorizer) {
294
+ // Not a missing credential: a proof DOES open `/ws` — measured, `101`
295
+ // against a node built from core `aecba573b`. What is missing is a way to
296
+ // carry it, and that is the browser's. `WebSocket` cannot set a request
297
+ // header, and the guard reads the proof from a header only; the `?token=`
298
+ // query parameter this client uses for a session exists for exactly that
299
+ // reason and has no proof equivalent yet.
300
+ throw new Error('this client authenticates with a request proof, which cannot open a WebSocket from a ' +
301
+ "browser: `WebSocket` cannot set the X-Calimero-Proof header, and core's guard reads " +
302
+ 'the proof from a header only (the ?token= query parameter is the session equivalent ' +
303
+ 'and has no proof counterpart). Use `client.events` — SSE is fetch-based, carries the ' +
304
+ 'header, and is the recommended transport anyway.');
305
+ }
306
+ throw missingNodeKey('WebSocket events');
307
+ }
308
+ /**
309
+ * Ephemeral presence. Node transport only.
310
+ *
311
+ * Not extended to the relay even though a logged-in session could reach both
312
+ * halves of it mechanically. Publishing resolves the author from an *owned
313
+ * context identity on the node*, and whether a delegated session has one has
314
+ * not been established — a presence client that silently published nothing,
315
+ * or published as the wrong identity, is worse than one that says it is not
316
+ * available. The read half is not lost: presence arrives on `client.events`
317
+ * like any other context event, and can be filtered there.
318
+ *
319
+ * The rejected alternative was to expose it over the relay session and let
320
+ * `set` fail at the node. That trades a clear construction-time answer for a
321
+ * runtime failure inside a UI that has already rendered.
322
+ */
323
+ get ephemeral() {
324
+ if (!this.nodeClient) {
325
+ throw relayHasNo('ephemeral presence', 'publishing resolves the author from an owned context identity on the node, which a delegated session is not known to have — read presence off `client.events` instead');
326
+ }
327
+ return this.nodeClient.ephemeral;
328
+ }
329
+ /**
330
+ * Calimero Cloud.
331
+ *
332
+ * Available on both transports, because it is neither: the cloud session is
333
+ * independent of the node credential and of the relay's signing key, and a
334
+ * relay-transport app still needs it to list namespaces and manage billing.
335
+ */
336
+ get cloud() {
337
+ if (!this.nodeClient) {
338
+ throw relayHasNo('bundled cloud client', 'a relay client is built from a cloud connection rather than owning one — reach it through the `cloud` field that `connectCloud` returns, or construct a `CloudClient` directly');
339
+ }
340
+ return this.nodeClient.cloud;
341
+ }
342
+ /**
343
+ * Release whatever the transport is holding.
344
+ *
345
+ * Safe on a relay client, where there is nothing open to close — a caller
346
+ * that tears down on unmount should not have to know which transport it got.
347
+ */
348
+ close() {
349
+ this.nodeClient?.close();
350
+ // The observer and the proof stream, not the relay: a relay client holds
351
+ // nothing open. Tearing down on unmount must not have to know which
352
+ // transport — or which credential — it got.
353
+ this.relayObserver?.close();
354
+ this.proofEvents?.close();
355
+ }
356
+ }
357
+ /**
358
+ * Build a client for the chosen transport. Node unless told otherwise.
359
+ *
360
+ * ```ts
361
+ * // unchanged, and still the default
362
+ * const client = createMeroClient({ baseUrl: 'http://localhost:2428' });
363
+ *
364
+ * // the same call sites, through a relay
365
+ * const client = createMeroClient({
366
+ * transport: 'relay',
367
+ * relay: connection.relay,
368
+ * // the relay node's signing key, learned out of band — without it the
369
+ * // client writes but does not observe
370
+ * observe: { nodeKey },
371
+ * });
372
+ *
373
+ * await client.rpc.execute({ contextId, method: 'set', argsJson: { key, value } });
374
+ * if (client.canSubscribe) client.events.on('event', onEvent);
375
+ * ```
376
+ */
377
+ export function createMeroClient(config) {
378
+ return new MeroClient(config);
379
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Reading from the relay — because a relay is a node.
3
+ *
4
+ * # What was wrong with "a relay-transport client cannot observe"
5
+ *
6
+ * The first version of this transport reported `canSubscribe === false` for
7
+ * every relay, on the reasoning that a relay serves only `GET`/`POST
8
+ * /admin-api/contexts/:id/intents`. That is true of the *unauthenticated*
9
+ * surface, and it is the wrong conclusion: a relay is an ordinary node that
10
+ * happens to also run intents for other people. The same origin serves
11
+ * `/auth/challenge`, `/auth/token`, `/sse` and `/ws`, and a keyholder holding
12
+ * an `AccountProof<DeviceCert>` can log in to it with {@link login} — the
13
+ * `account_proof` provider grants exactly `context:intent`, `context:query`,
14
+ * `context:subscribe`. Subscribe is in the provider's own default grant, not
15
+ * something extra that has to be asked for, and a delegated write on such a
16
+ * session was observed arriving as a `StateMutation` frame whose `newRoot`
17
+ * matched the write's `rootHash`.
18
+ *
19
+ * So the gap was never "a relay cannot be observed". It is exactly one missing
20
+ * input.
21
+ *
22
+ * # The one missing input, and why it is not filled in from somewhere
23
+ *
24
+ * {@link login} requires the node's device signing key as a *separately
25
+ * supplied* parameter. It is not read out of the challenge response on purpose:
26
+ * a value learned from the party you are about to authenticate to lets whoever
27
+ * answered choose what your device signs a statement about. That is the whole
28
+ * attack the parameter exists to stop.
29
+ *
30
+ * For a self-hosted relay the operator can hand the key over out of band, and
31
+ * this module works today. For a cloud-hosted relay, `CloudRelay` carries
32
+ * `peerId`, `relayUrl`, `executorAccount`, `status` and `authorshipReady` —
33
+ * and no node key. `peerId` is a libp2p identity and is **not** a substitute:
34
+ * signing a statement about a libp2p peer id would be signing about a different
35
+ * key than the one the node authenticates with. That is filed upstream as mdma
36
+ * #312; until it lands, the hosted case has no key and reports
37
+ * `canSubscribe === false` rather than pretending.
38
+ *
39
+ * Nothing here invents a key, and nothing here falls back to "some other node".
40
+ */
41
+ import { SseClient } from '../events/sse.js';
42
+ import { WsClient } from '../events/ws.js';
43
+ import { type Audience } from '../login/index.js';
44
+ import type { Signer } from '../signer/signer.js';
45
+ /**
46
+ * How a relay-transport client observes the relay node.
47
+ *
48
+ * Every field but {@link RelayObserveConfig.nodeKey} has a correct default
49
+ * taken from the `RelayClient` already configured — the author's proof, its
50
+ * signer and the relay's own URL are the same values a login needs, and asking
51
+ * for a second copy is how two copies come to disagree.
52
+ */
53
+ export interface RelayObserveConfig {
54
+ /**
55
+ * The relay node's ed25519 device signing key, hex (32 bytes), **as this
56
+ * client pinned it**.
57
+ *
58
+ * Learn it out of band: from the operator of a self-hosted relay, or from a
59
+ * certificate you already trust. Never from the node itself — see the module
60
+ * note.
61
+ *
62
+ * `null` / `undefined` is a first-class answer meaning "nobody told me", and
63
+ * it is what the hosted path passes today (mdma #312). It makes
64
+ * `canSubscribe` false; it never makes the client guess.
65
+ */
66
+ nodeKey?: string | null;
67
+ /**
68
+ * The client surface the session is bound to.
69
+ *
70
+ * Defaults to this runtime's honest self-description: the browser's own
71
+ * `location.origin`, or `{ kind: 'cli' }` where there is no browser. Passing
72
+ * something else is for a client that knows better — a signed native app
73
+ * naming its code-signing identity, say. A wrong audience fails the login
74
+ * loudly rather than quietly widening anything.
75
+ */
76
+ audience?: Audience;
77
+ /** The node to log in to. Defaults to the relay's own URL — they are the same node. */
78
+ nodeUrl?: string;
79
+ /** The author's `AccountProof<DeviceCert>`, hex. Defaults to the relay client's. */
80
+ accountProof?: string;
81
+ /** The device signer. Defaults to the relay client's — the same device authors and observes. */
82
+ signer?: Signer;
83
+ /** How this client names itself to the node. Defaults to the audience's own spelling. */
84
+ clientName?: string;
85
+ /** Seconds the login statement stays valid. Defaults to `login`'s own default. */
86
+ ttlSeconds?: number;
87
+ /** Injected for tests and non-browser runtimes. */
88
+ fetch?: typeof fetch;
89
+ timeoutMs?: number;
90
+ }
91
+ /** What {@link RelayObserver} needs once the defaults have been resolved. */
92
+ export interface ResolvedObserveConfig {
93
+ nodeUrl: string;
94
+ nodeKey: string;
95
+ accountProof: string;
96
+ audience: Audience;
97
+ signer: () => Promise<Signer>;
98
+ clientName?: string;
99
+ ttlSeconds?: number;
100
+ fetch?: typeof fetch;
101
+ timeoutMs?: number;
102
+ }
103
+ /**
104
+ * This runtime's honest self-description, used when no audience is given.
105
+ *
106
+ * The origin is taken verbatim from the browser rather than normalized: the
107
+ * node compares it byte for byte, and two spellings of one origin would mean a
108
+ * client that disagrees with itself about which surface a token was for.
109
+ */
110
+ export declare function defaultAudience(): Audience;
111
+ /**
112
+ * A node session over the relay, and the event clients it feeds.
113
+ *
114
+ * The session is minted lazily, on the first thing that actually needs a token.
115
+ * That is not an optimization: `client.events` is a *getter*, so constructing a
116
+ * client would otherwise have to perform two HTTP calls and a signature before
117
+ * anyone had asked to observe anything.
118
+ */
119
+ export declare class RelayObserver {
120
+ private readonly config;
121
+ private session;
122
+ private sessionExpiresAt;
123
+ /** In-flight login, so two surfaces asking at once mint one session, not two. */
124
+ private pending;
125
+ private sseClient;
126
+ private wsClient;
127
+ constructor(config: ResolvedObserveConfig);
128
+ /** The node key this observer logs in with. Read for diagnostics. */
129
+ get nodeKey(): string;
130
+ /**
131
+ * A usable access token, logging in if there is not one.
132
+ *
133
+ * Re-mints once the current token is within {@link EXPIRY_SKEW_MS} of
134
+ * expiring, and on every call when the token's lifetime cannot be read —
135
+ * reconnects are backed off, so an extra handshake costs far less than a
136
+ * stream that silently stops. The refresh token is *held* but not spent:
137
+ * rotating it is tracked in #87/#88 and a second login is correct in the
138
+ * meantime, where re-presenting an unrotated refresh token would not be.
139
+ */
140
+ accessToken(): Promise<string>;
141
+ private loginNow;
142
+ /**
143
+ * SSE events over the relay node — the same `SseClient` a node client hands
144
+ * out, constructed the same way, so nothing downstream can tell which
145
+ * transport produced it.
146
+ */
147
+ get events(): SseClient;
148
+ /** WebSocket events over the relay node. @experimental — prefer `events`. */
149
+ get ws(): WsClient;
150
+ /** Close whatever was opened. Never touches the relay's write path. */
151
+ close(): void;
152
+ }