@net-mesh/browser 0.37.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 (110) hide show
  1. package/CHANGELOG.md +517 -0
  2. package/LICENSE-APACHE +190 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +666 -0
  5. package/dist/async-queue.d.ts +26 -0
  6. package/dist/async-queue.js +62 -0
  7. package/dist/credential.d.ts +70 -0
  8. package/dist/credential.js +95 -0
  9. package/dist/errors.d.ts +400 -0
  10. package/dist/errors.js +726 -0
  11. package/dist/events.d.ts +232 -0
  12. package/dist/events.js +298 -0
  13. package/dist/identity.d.ts +40 -0
  14. package/dist/identity.js +78 -0
  15. package/dist/index.bundle.js +2 -0
  16. package/dist/index.d.ts +38 -0
  17. package/dist/index.js +46 -0
  18. package/dist/leader/events.d.ts +111 -0
  19. package/dist/leader/events.js +116 -0
  20. package/dist/leader/index.d.ts +14 -0
  21. package/dist/leader/index.js +11 -0
  22. package/dist/leader/session.d.ts +539 -0
  23. package/dist/leader/session.js +770 -0
  24. package/dist/leader/wasm.d.ts +123 -0
  25. package/dist/leader/wasm.js +39 -0
  26. package/dist/lobby.d.ts +183 -0
  27. package/dist/lobby.js +504 -0
  28. package/dist/local.d.ts +80 -0
  29. package/dist/local.js +160 -0
  30. package/dist/net_leaf.d.ts +1494 -0
  31. package/dist/net_leaf.js +3753 -0
  32. package/dist/net_leaf_bg.wasm +0 -0
  33. package/dist/net_leaf_bg.wasm.d.ts +137 -0
  34. package/dist/netcode/client.d.ts +102 -0
  35. package/dist/netcode/client.js +296 -0
  36. package/dist/netcode/clock.d.ts +45 -0
  37. package/dist/netcode/clock.js +54 -0
  38. package/dist/netcode/host.d.ts +118 -0
  39. package/dist/netcode/host.js +276 -0
  40. package/dist/netcode/index.d.ts +19 -0
  41. package/dist/netcode/index.js +14 -0
  42. package/dist/netcode/interpolate.d.ts +50 -0
  43. package/dist/netcode/interpolate.js +100 -0
  44. package/dist/netcode/wire.d.ts +125 -0
  45. package/dist/netcode/wire.js +117 -0
  46. package/dist/node.d.ts +908 -0
  47. package/dist/node.js +1085 -0
  48. package/dist/org.d.ts +412 -0
  49. package/dist/org.js +567 -0
  50. package/dist/peer-driver.d.ts +117 -0
  51. package/dist/peer-driver.js +224 -0
  52. package/dist/store/assembly.d.ts +176 -0
  53. package/dist/store/assembly.js +325 -0
  54. package/dist/store/chunker.d.ts +83 -0
  55. package/dist/store/chunker.js +190 -0
  56. package/dist/store/core.d.ts +136 -0
  57. package/dist/store/core.js +477 -0
  58. package/dist/store/definition.d.ts +23 -0
  59. package/dist/store/definition.js +83 -0
  60. package/dist/store/errors.d.ts +77 -0
  61. package/dist/store/errors.js +94 -0
  62. package/dist/store/host.d.ts +247 -0
  63. package/dist/store/host.js +675 -0
  64. package/dist/store/index.d.ts +50 -0
  65. package/dist/store/index.js +40 -0
  66. package/dist/store/interest.d.ts +40 -0
  67. package/dist/store/interest.js +77 -0
  68. package/dist/store/inventory.d.ts +81 -0
  69. package/dist/store/inventory.js +140 -0
  70. package/dist/store/join.d.ts +118 -0
  71. package/dist/store/join.js +724 -0
  72. package/dist/store/json.d.ts +50 -0
  73. package/dist/store/json.js +283 -0
  74. package/dist/store/ledger.d.ts +187 -0
  75. package/dist/store/ledger.js +292 -0
  76. package/dist/store/owner.d.ts +616 -0
  77. package/dist/store/owner.js +1711 -0
  78. package/dist/store/patch.d.ts +70 -0
  79. package/dist/store/patch.js +166 -0
  80. package/dist/store/player.d.ts +65 -0
  81. package/dist/store/player.js +261 -0
  82. package/dist/store/replica.d.ts +187 -0
  83. package/dist/store/replica.js +740 -0
  84. package/dist/store/state.d.ts +42 -0
  85. package/dist/store/state.js +165 -0
  86. package/dist/store/types.d.ts +200 -0
  87. package/dist/store/types.js +25 -0
  88. package/dist/store/visibility.d.ts +118 -0
  89. package/dist/store/visibility.js +282 -0
  90. package/dist/store/wire.d.ts +309 -0
  91. package/dist/store/wire.js +834 -0
  92. package/dist/stream.d.ts +174 -0
  93. package/dist/stream.js +238 -0
  94. package/dist/three/index.d.ts +109 -0
  95. package/dist/three/index.js +130 -0
  96. package/dist/udp-probe.d.ts +159 -0
  97. package/dist/udp-probe.js +245 -0
  98. package/dist/wasm.d.ts +642 -0
  99. package/dist/wasm.js +98 -0
  100. package/dist/world/border.d.ts +84 -0
  101. package/dist/world/border.js +99 -0
  102. package/dist/world/handoff.d.ts +187 -0
  103. package/dist/world/handoff.js +222 -0
  104. package/dist/world/index.d.ts +13 -0
  105. package/dist/world/index.js +9 -0
  106. package/dist/world/link.d.ts +179 -0
  107. package/dist/world/link.js +476 -0
  108. package/dist/world/regions.d.ts +154 -0
  109. package/dist/world/regions.js +314 -0
  110. package/package.json +72 -0
package/dist/node.d.ts ADDED
@@ -0,0 +1,908 @@
1
+ /**
2
+ * `BrowserNode` — the surface a page actually uses.
3
+ *
4
+ * A thin, typed, camelCased layer over the wasm node: it owns the one
5
+ * `on_event` registration, re-types everything the boundary throws,
6
+ * parses `query`'s JSON without rounding 64-bit ids, and applies the
7
+ * Stage 5 failure-typing correction to an ICE failure when — and only
8
+ * when — the distinguishing evidence can be obtained.
9
+ */
10
+ import { type LeafEvent, type LeafEventOf, type Unsubscribe } from './events.js';
11
+ import { type LeafError } from './errors.js';
12
+ import { type OrgAccess, type OrgByteStream, type OrgCallOptions, type OrgClientStreamHandler, type OrgDuplexHandler, type OrgDuplexHandles, type OrgServeHandle, type OrgServeOptions, type OrgStreamingHandler, type OrgUnaryHandler, type OrgUploadCall } from './org.js';
13
+ import { LeafStream, type OpenStreamOptions } from './stream.js';
14
+ import { type BootstrapProbeOptions, type StunProbeOptions } from './udp-probe.js';
15
+ import { type LeafWasmConnectOptions, type WasmSource } from './wasm.js';
16
+ /**
17
+ * One node the mesh knows about, from {@link BrowserNode.query}.
18
+ *
19
+ * **`nodeId` and the peer methods do not take the same spelling.**
20
+ * `nodeId` is the exact DECIMAL `u64` the announcement carries;
21
+ * {@link BrowserNode.connectPeer} and its siblings take 16
22
+ * lowercase HEX digits. The two are different strings for the same
23
+ * number, and a decimal string can even be 16 digits long — so
24
+ * `connectPeer(descriptor.nodeId)` addresses a different node, or is
25
+ * refused, rather than doing what it reads like. Neither value is
26
+ * silently normalised into the other: {@link peerIdHex} is the
27
+ * conversion, and it is also pre-computed here as
28
+ * {@link NodeDescriptor.peerIdHex}.
29
+ */
30
+ export interface NodeDescriptor {
31
+ /**
32
+ * The node's mesh id, exact decimal — as the announcement spells
33
+ * it. NOT what the peer methods take; see
34
+ * {@link NodeDescriptor.peerIdHex}.
35
+ */
36
+ readonly nodeId: string;
37
+ /**
38
+ * The same id in the spelling every peer method takes: 16
39
+ * lowercase hex digits.
40
+ *
41
+ * Derived from {@link NodeDescriptor.nodeId} by {@link peerIdHex},
42
+ * and carried beside it rather than instead of it — the decimal
43
+ * literal is what the announcement signed and what
44
+ * {@link BrowserNode.query}'s consumers may be matching on.
45
+ */
46
+ readonly peerIdHex: string;
47
+ /** Its Ed25519 entity id, hex — the identity the announcement signed. */
48
+ readonly entityId: string | null;
49
+ readonly capabilities: readonly string[];
50
+ /** The RTC address it published, when it published one. */
51
+ readonly rtcAddr: string | null;
52
+ /** Its Noise static public key, hex, when the announcement carried one. */
53
+ readonly noisePubkey: string | null;
54
+ /** The announcement's version counter, exact decimal. */
55
+ readonly version: string | null;
56
+ }
57
+ /**
58
+ * Convert a decimal `u64` node id — {@link NodeDescriptor.nodeId},
59
+ * or any other decimal mesh id — into the 16-lowercase-hex-digit
60
+ * spelling the peer methods require.
61
+ *
62
+ * Explicit, because the alternative is worse in both directions. The
63
+ * leaf refuses a decimal peer id deliberately (one contract: `0009`
64
+ * must not name two different nodes depending on which method read
65
+ * it), and a wrapper that guessed which spelling it had been handed
66
+ * would dial whichever node its guess produced. So the caller says
67
+ * which conversion it means.
68
+ *
69
+ * `BigInt` rather than `Number`: a mesh id is a full `u64` and
70
+ * `parseInt` would round it.
71
+ *
72
+ * @throws TypeError if `nodeId` is not a decimal integer inside
73
+ * `u64`.
74
+ */
75
+ export declare function peerIdHex(nodeId: string): string;
76
+ /** How hard to work at classifying an ICE failure. */
77
+ export interface FailureTypingOptions {
78
+ /**
79
+ * Run the STUN probe when an attempt fails with an ICE timeout.
80
+ * Default `true`. Turning it off does not change which failures are
81
+ * possible — it only means an ICE timeout always stays
82
+ * `ice-timeout`, never `udp-blocked`.
83
+ */
84
+ readonly probeOnIceTimeout?: boolean;
85
+ /** Passed to the STUN probe. */
86
+ readonly stun?: StunProbeOptions;
87
+ /** Passed to the HTTPS reachability probe. */
88
+ readonly bootstrap?: BootstrapProbeOptions;
89
+ }
90
+ /**
91
+ * Where one browser ↔ browser attempt ended (plan §9).
92
+ *
93
+ * A **typed result**, not a rejection: every member below is a
94
+ * disposition a drive loop acts on rather than an exception. `direct`
95
+ * is the only success; the rest are the four the stage brief names,
96
+ * plus `udpBlocked`, which is `iceTimeout` with the evidence that
97
+ * narrows it actually established (`UdpBlockedEvidence` — never
98
+ * claimed without it).
99
+ *
100
+ * `dialog` is the attempt's id, 16 lowercase hex digits, the same
101
+ * value both sides number the exchange with.
102
+ */
103
+ export type PeerConnectOutcome =
104
+ /** A direct session is installed and the pair is off the relay. */
105
+ {
106
+ readonly type: 'direct';
107
+ readonly peer: string;
108
+ readonly dialog: string;
109
+ }
110
+ /**
111
+ * ICE did not connect inside the attempt's deadline. The routed
112
+ * session was never replaced (§9 step 6), which is a relayed
113
+ * session and not a failure.
114
+ */
115
+ | {
116
+ readonly type: 'iceTimeout';
117
+ readonly peer: string;
118
+ readonly dialog: string;
119
+ }
120
+ /**
121
+ * As `iceTimeout`, and the STUN probe against the address the
122
+ * anchor published also went unanswered — so this browser's UDP is
123
+ * blocked, with the two observations that establish it.
124
+ */
125
+ | {
126
+ readonly type: 'udpBlocked';
127
+ readonly peer: string;
128
+ readonly dialog: string;
129
+ }
130
+ /**
131
+ * This node holds no verified announcement for the peer, so there
132
+ * is no key to handshake against. Discover it by capability query
133
+ * first: a peer's Noise key comes from its signed announcement and
134
+ * never from a caller.
135
+ */
136
+ | {
137
+ readonly type: 'noAnnouncement';
138
+ readonly peer: string;
139
+ readonly detail: string;
140
+ }
141
+ /** The DataChannel opened and the Noise handshake did not install. */
142
+ | {
143
+ readonly type: 'handshakeFailed';
144
+ readonly peer: string;
145
+ readonly dialog: string;
146
+ readonly detail: string;
147
+ }
148
+ /**
149
+ * A newer attempt with the same peer replaced this one. `liveDialog`
150
+ * is the attempt that now holds the peer, or `null` when the
151
+ * attempt simply ended.
152
+ */
153
+ | {
154
+ readonly type: 'superseded';
155
+ readonly peer: string;
156
+ readonly dialog: string;
157
+ readonly liveDialog: string | null;
158
+ };
159
+ /** One `peer_candidate` reading, as the leaf reports it. */
160
+ export interface PeerAttemptStatus {
161
+ /** The attempt CURRENTLY live for the peer. */
162
+ readonly dialog: string;
163
+ /**
164
+ * Where the attempt stands.
165
+ *
166
+ * `gathering` and `open` are about ICE. The other three are the
167
+ * attempt's terminal transition, which spans ICE, Noise AND
168
+ * installation: `iceTimeout` and `udpBlocked` are a deadline with
169
+ * no channel, and `failed` is everything else that ended it — a
170
+ * channel that opened and a session that never installed, a
171
+ * responder step that failed. A terminal reading is final; the
172
+ * leaf neither drives nor re-settles the attempt after it.
173
+ */
174
+ readonly state: 'gathering' | 'open' | 'iceTimeout' | 'udpBlocked' | 'failed';
175
+ /** Signed `Candidate` envelopes sent by this call. */
176
+ readonly sent: number;
177
+ /** Remote candidates the browser accepted on this call. */
178
+ readonly applied: number;
179
+ /** Whether the peer's answer was applied on this call. */
180
+ readonly answered: boolean;
181
+ /**
182
+ * Whether a direct session is installed and the relay is gone — a
183
+ * different fact from `state === 'open'`, which is only the
184
+ * DataChannel.
185
+ */
186
+ readonly direct: boolean;
187
+ /** Milliseconds left on the attempt's ICE deadline, exact decimal. */
188
+ readonly remainingMs: string;
189
+ /**
190
+ * The engine's own text for the first trickled candidate it
191
+ * refused on this call, or `null` when it accepted every one.
192
+ *
193
+ * Machine-readable on purpose. "ICE is working on it" and "ICE has
194
+ * nothing to work with" are different states and a console warning
195
+ * is not a value a page, a witness or a diagnostic can read.
196
+ */
197
+ readonly candidateError: string | null;
198
+ }
199
+ /** {@link connect}'s argument. */
200
+ export interface ConnectOptions extends WasmSource {
201
+ /** The bootstrap credential, base64. */
202
+ credentialB64?: string;
203
+ /** The bootstrap credential as bytes, or base64 — either is fine. */
204
+ credential?: Uint8Array | string;
205
+ /**
206
+ * The anchor's HTTPS bootstrap endpoint. Optional — the credential
207
+ * carries one, and this overrides it. Naming it also gives the
208
+ * failure classifier its HTTPS reachability probe before the first
209
+ * `connected` event.
210
+ */
211
+ bootstrapUrl?: string;
212
+ /**
213
+ * The origin this node's identity belongs to. Defaults to
214
+ * `location.origin`, which is the trust boundary the identity is
215
+ * stored under.
216
+ */
217
+ origin?: string;
218
+ /**
219
+ * The ICE servers this node's connections gather against.
220
+ *
221
+ * Optional, and **defaulted for you**: omitted, the leaf uses the
222
+ * STUN endpoint the anchor announces as `stun_addr` on
223
+ * `GET /rtc/anchor`, so the advertised configuration works without
224
+ * a page choosing a STUN service. An anchor that announces none
225
+ * leaves the connection with no ICE servers, which is host
226
+ * candidates only.
227
+ *
228
+ * **Absence, not emptiness, triggers the default.** An explicit
229
+ * `iceServers: []` is a caller saying *none*, and is honoured as
230
+ * given; only an omitted option takes the announced endpoint. A
231
+ * default that also fired on `[]` would override an intent rather
232
+ * than supply a missing one, and the only symptom would be a
233
+ * connection that gathered more than the page asked for.
234
+ *
235
+ * Supplied, it is honoured verbatim — with one refusal. An entry
236
+ * naming this connection's peer RTC endpoint as its STUN server is
237
+ * rejected with {@link IceServerConflictError} before any ICE
238
+ * work, because a peer cannot be its own STUN server. In
239
+ * particular, do not build an entry out of
240
+ * {@link diagnosticStunUrl}(anchorRtcAddr): that helper is for the
241
+ * throwaway UDP probe.
242
+ */
243
+ iceServers?: readonly RTCIceServer[];
244
+ /**
245
+ * Custodial identity: the Ed25519 entity secret as 32 bytes of hex.
246
+ *
247
+ * Supplied, the leaf builds its identity from this instead of
248
+ * generating one, so two tabs (or two pages) handed the same secret
249
+ * are the same node id. Absent, the leaf generates from the platform
250
+ * CSPRNG.
251
+ */
252
+ entitySecretHex?: string;
253
+ /**
254
+ * The Noise X25519 static secret as 32 bytes of hex. Only read when
255
+ * {@link ConnectOptions.entitySecretHex} is also given — the leaf
256
+ * generates this half otherwise, which would leave two tabs sharing
257
+ * an entity but not a static key.
258
+ */
259
+ noiseSecretHex?: string;
260
+ /**
261
+ * The `rtc_addr` the anchor publishes, supplied out of band.
262
+ *
263
+ * The STUN probe needs a subject, and before a `connected` event
264
+ * arrives this package has not been told one. A page that already
265
+ * knows the anchor's address (a test harness, a pinned deployment)
266
+ * passes it here and gets `udp-blocked` classified on the very first
267
+ * failed attempt; without it, and before the first `connected`, an
268
+ * ICE timeout correctly stays `ice-timeout`.
269
+ */
270
+ anchorRtcAddr?: string;
271
+ /** Failure-typing knobs. */
272
+ failureTyping?: FailureTypingOptions;
273
+ }
274
+ /** A browser-native Net node. */
275
+ export declare class BrowserNode {
276
+ private readonly inner;
277
+ private readonly bootstrapUrl;
278
+ private readonly failureTyping;
279
+ private readonly hub;
280
+ private readonly nodeId;
281
+ private closed;
282
+ /**
283
+ * Streams this node handed out and that are still open, so closing
284
+ * the node can end them.
285
+ *
286
+ * The direct surface's half of the same guarantee `MeshSession`
287
+ * gives across a leader change: after `close()` the wasm node
288
+ * delivers nothing and no stream will ever emit again, so a
289
+ * consumer sitting in `await iterator.next()` would never settle.
290
+ * Nothing else can end it — the queue is fed only by the stream's
291
+ * `on_message` callback, and a closed node does not call it. So
292
+ * the node owns the set and drains it, which turns "no more data,
293
+ * ever" into the end of the iteration a page is already written to
294
+ * handle.
295
+ */
296
+ private readonly streams;
297
+ /**
298
+ * The live org handles (plan §4.5), so {@link BrowserNode.close}
299
+ * can retire ownership: pending calls fail typed and each dropped
300
+ * caller handle emits exactly one CANCEL.
301
+ */
302
+ private readonly orgHandles;
303
+ /** The last `rtc_addr` an anchor told us about; the probe's subject. */
304
+ private anchorRtcAddr;
305
+ /** Set once the leaf reports a session — a bootstrap that demonstrably worked. */
306
+ private bootstrapObserved;
307
+ /** This node's mesh id, hex. */
308
+ nodeIdHex(): string;
309
+ /** The anchor's mesh id, hex. */
310
+ anchorIdHex(): string;
311
+ /**
312
+ * This node's origin hash, hex.
313
+ *
314
+ * Distinct from {@link BrowserNode.nodeIdHex} and not a synonym:
315
+ * the origin hash rides every packet header this node seals, names
316
+ * its nRPC reply channels (`<service>.replies.<origin>`), and is
317
+ * what a receiver compares an event payload's
318
+ * `EventMeta.origin_hash` against — a direct peer whose header
319
+ * origin and payload origin disagree has the frame dropped before
320
+ * admission. A page that hands this node a pre-encoded event
321
+ * payload to send must build it with THIS value.
322
+ */
323
+ originHashHex(): string;
324
+ /**
325
+ * Run the enrollment exchange.
326
+ *
327
+ * {@link connect} already awaits it, so a page needs this only to
328
+ * drive or observe the step — a harness proving that an unenrolled
329
+ * leaf is exactly what a provisional peer looks like, for instance.
330
+ * Until a leaf is enrolled the anchor keeps its peer provisional and
331
+ * §12 refuses everything above the transport, which surfaces here as
332
+ * `rpc-timeout` on calls rather than as a transport failure.
333
+ */
334
+ enroll(): Promise<void>;
335
+ /**
336
+ * Whether the anchor has admitted this leaf. `false` means the
337
+ * session is still §12-provisional, so a call will die on its
338
+ * deadline — check this before blaming a `rpc-timeout` on a slow
339
+ * anchor.
340
+ */
341
+ isEnrolled(): boolean;
342
+ /**
343
+ * Call an nRPC service and resolve its reply.
344
+ *
345
+ * Rejects with {@link RpcError} (`rpc-refused`, `rpc-timeout`,
346
+ * `session-lost`, `leader-lost`, `rpc-malformed`) — never with a
347
+ * silent retry, which is §8's rule for a call whose leader or
348
+ * session went away.
349
+ */
350
+ call(service: string, payload: Uint8Array, timeoutMs?: number): Promise<Uint8Array>;
351
+ /**
352
+ * Subscribe to a channel. Messages arrive as `channel_message`
353
+ * events; the leaf only delivers channels this node subscribed to,
354
+ * and `channelHash` is its hash of the name.
355
+ */
356
+ subscribe(channel: string): Promise<void>;
357
+ /** Publish one payload to a channel. */
358
+ publish(channel: string, payload: Uint8Array): Promise<void>;
359
+ /**
360
+ * Open a reliable or fire-and-forget stream.
361
+ *
362
+ * The stream is retained until it or the node closes, so
363
+ * {@link BrowserNode.close} can end the consumers it handed out.
364
+ *
365
+ * On a **closed** node this is a typed refusal, not a dead handle:
366
+ * the leaf's own fence (`Inner::admit`) rejects every outbound
367
+ * operation on a node that no longer holds the origin's identity,
368
+ * and that is re-typed here as `SessionError` (`kind: 'session'`)
369
+ * like every other boundary failure. There is deliberately no
370
+ * second check in TypeScript — one fence, on the side that owns
371
+ * the node's lifetime.
372
+ */
373
+ openStream(options: OpenStreamOptions): LeafStream;
374
+ /**
375
+ * Call an org-protected unary service.
376
+ *
377
+ * Rejects with the §4.3 typed error: an **opening denial** is
378
+ * {@link OrgAdmissionDeniedError} carrying the coarse reason and
379
+ * nothing finer — a precise remote reason would be a credential
380
+ * oracle. Never a silent retry: the caller is told and decides.
381
+ *
382
+ * **Suspension and closure (plan §4.5).** Deadlines are absolute: a
383
+ * frozen tab extends no lease and no window, so a call whose
384
+ * deadline passes while its tab is suspended retires with its
385
+ * deadline terminal on wake — {@link OrgTimeoutError}, or
386
+ * {@link OrgIndeterminateError} where the work may already be
387
+ * executing elsewhere. Nothing resumes automatically: a retired
388
+ * call is never transparently re-opened, because re-opening is a
389
+ * fresh call with a fresh proof and MAY repeat effects. Node or tab
390
+ * close retires ownership — pending calls fail typed, and each
391
+ * dropped caller handle emits exactly one CANCEL.
392
+ */
393
+ callOrg(service: string, payload: Uint8Array, options: OrgCallOptions): Promise<Uint8Array>;
394
+ /**
395
+ * Call an org-protected server-streaming service; resolve to the
396
+ * response stream.
397
+ *
398
+ * An **opening denial** rejects here with
399
+ * {@link OrgAdmissionDeniedError} (coarse) — a denial is a
400
+ * rejection, never a terminal item. Midstream, the stream's FINAL
401
+ * error item carries the §4.3 outcome: revocation is
402
+ * `AdmissionDenied('denied')` ({@link OrgRevokedError} where the
403
+ * cause is known), deadline/cancel retirement is
404
+ * {@link OrgTimeoutError}/{@link OrgCancelledError}. The async
405
+ * iterator throws that typed error at the terminal error item.
406
+ *
407
+ * **Suspension and closure (plan §4.5).** Deadlines are absolute: a
408
+ * frozen tab extends no lease and no window, so a call whose
409
+ * deadline passes while its tab is suspended retires with its
410
+ * deadline terminal on wake — {@link OrgTimeoutError}, or
411
+ * {@link OrgIndeterminateError} where the work may already be
412
+ * executing elsewhere. Nothing resumes automatically: a retired
413
+ * call is never transparently re-opened, because re-opening is a
414
+ * fresh call with a fresh proof and MAY repeat effects. Node or tab
415
+ * close retires ownership — pending calls fail typed, and each
416
+ * dropped caller handle emits exactly one CANCEL.
417
+ */
418
+ callOrgStreaming(service: string, payload: Uint8Array, options: OrgCallOptions): Promise<OrgByteStream>;
419
+ /**
420
+ * Open an org-protected client-streaming upload. `finish()` is the
421
+ * half-close and the reply await, and it rejects with the typed
422
+ * terminal error (§4.3).
423
+ *
424
+ * An **opening denial** rejects here with
425
+ * {@link OrgAdmissionDeniedError} (coarse).
426
+ *
427
+ * **Suspension and closure (plan §4.5).** Deadlines are absolute: a
428
+ * frozen tab extends no lease and no window, so a call whose
429
+ * deadline passes while its tab is suspended retires with its
430
+ * deadline terminal on wake — {@link OrgTimeoutError}, or
431
+ * {@link OrgIndeterminateError} where the work may already be
432
+ * executing elsewhere. Nothing resumes automatically: a retired
433
+ * call is never transparently re-opened, because re-opening is a
434
+ * fresh call with a fresh proof and MAY repeat effects. Node or tab
435
+ * close retires ownership — pending calls fail typed, and each
436
+ * dropped caller handle emits exactly one CANCEL.
437
+ */
438
+ callOrgClientStream(service: string, options: OrgCallOptions): Promise<OrgUploadCall>;
439
+ /**
440
+ * Open an org-protected duplex call: an upload sink and an
441
+ * independent response stream.
442
+ *
443
+ * An **opening denial** rejects here with
444
+ * {@link OrgAdmissionDeniedError} (coarse). Midstream outcomes are
445
+ * the §4.3 terminals on each half.
446
+ *
447
+ * **Suspension and closure (plan §4.5).** Deadlines are absolute: a
448
+ * frozen tab extends no lease and no window, so a call whose
449
+ * deadline passes while its tab is suspended retires with its
450
+ * deadline terminal on wake — {@link OrgTimeoutError}, or
451
+ * {@link OrgIndeterminateError} where the work may already be
452
+ * executing elsewhere. Nothing resumes automatically: a retired
453
+ * call is never transparently re-opened, because re-opening is a
454
+ * fresh call with a fresh proof and MAY repeat effects. Node or tab
455
+ * close retires ownership — pending calls fail typed, and each
456
+ * dropped caller handle emits exactly one CANCEL.
457
+ */
458
+ callOrgDuplex(service: string, options: OrgCallOptions): Promise<OrgDuplexHandles>;
459
+ /**
460
+ * Serve an org-protected unary service. `access` is `'same-org'`
461
+ * (owner-org traffic only) or `'granted'` (capability grants too);
462
+ * anything else is refused loudly at registration.
463
+ *
464
+ * **Handler drop (F-S3.1-2).** The retire supervisor may drop the
465
+ * handler future without a final poll — cancellation is observed
466
+ * through the retirement observables (the terminal item, the sink's
467
+ * typed closed refusal, and `retired`), never assumed as a
468
+ * handler-side event; a detached observer holding `retired`
469
+ * observes the signal. After retirement a handler sees typed
470
+ * refusals and any return value it produces is discarded.
471
+ *
472
+ * **`close()` is a retirement, not a graceful unregister (C9's
473
+ * protected split).** Closing the returned handle retires its live
474
+ * protected calls — each with its exact retirement terminal, the
475
+ * deadline/cancel/revocation vocabulary — and refuses new openings
476
+ * on the service. Deliberately NOT the public path's
477
+ * let-existing-calls-finish behavior.
478
+ */
479
+ serveOrg(service: string, access: OrgAccess, handler: OrgUnaryHandler, options: OrgServeOptions): OrgServeHandle;
480
+ /**
481
+ * Serve an org-protected server-streaming service: one request in,
482
+ * items out through the {@link OrgResponseSink}.
483
+ *
484
+ * **Handler drop (F-S3.1-2).** The retire supervisor may drop the
485
+ * handler future without a final poll — cancellation is observed
486
+ * through the retirement observables (the terminal item, the sink's
487
+ * typed closed refusal, and `retired`), never assumed as a
488
+ * handler-side event; a detached observer holding `retired`
489
+ * observes the signal. After retirement a handler sees typed
490
+ * refusals and any return value it produces is discarded.
491
+ *
492
+ * **`close()` is a retirement, not a graceful unregister (C9's
493
+ * protected split).** Closing the returned handle retires its live
494
+ * protected calls — each with its exact retirement terminal, the
495
+ * deadline/cancel/revocation vocabulary — and refuses new openings
496
+ * on the service. Deliberately NOT the public path's
497
+ * let-existing-calls-finish behavior.
498
+ */
499
+ serveOrgStreaming(service: string, access: OrgAccess, handler: OrgStreamingHandler, options: OrgServeOptions): OrgServeHandle;
500
+ /**
501
+ * Serve an org-protected client-streaming service: requests in
502
+ * through the {@link OrgRequestStream}, one reply out.
503
+ *
504
+ * **Handler drop (F-S3.1-2).** The retire supervisor may drop the
505
+ * handler future without a final poll — cancellation is observed
506
+ * through the retirement observables (the terminal item, the sink's
507
+ * typed closed refusal, and `retired`), never assumed as a
508
+ * handler-side event; a detached observer holding `retired`
509
+ * observes the signal. After retirement a handler sees typed
510
+ * refusals and any return value it produces is discarded.
511
+ *
512
+ * **`close()` is a retirement, not a graceful unregister (C9's
513
+ * protected split).** Closing the returned handle retires its live
514
+ * protected calls — each with its exact retirement terminal, the
515
+ * deadline/cancel/revocation vocabulary — and refuses new openings
516
+ * on the service. Deliberately NOT the public path's
517
+ * let-existing-calls-finish behavior.
518
+ */
519
+ serveOrgClientStream(service: string, access: OrgAccess, handler: OrgClientStreamHandler, options: OrgServeOptions): OrgServeHandle;
520
+ /**
521
+ * Serve an org-protected duplex service: requests in and items out,
522
+ * independently, through the request stream and the sink.
523
+ *
524
+ * **Handler drop (F-S3.1-2).** The retire supervisor may drop the
525
+ * handler future without a final poll — cancellation is observed
526
+ * through the retirement observables (the terminal item, the sink's
527
+ * typed closed refusal, and `retired`), never assumed as a
528
+ * handler-side event; a detached observer holding `retired`
529
+ * observes the signal. After retirement a handler sees typed
530
+ * refusals and any return value it produces is discarded.
531
+ *
532
+ * **`close()` is a retirement, not a graceful unregister (C9's
533
+ * protected split).** Closing the returned handle retires its live
534
+ * protected calls — each with its exact retirement terminal, the
535
+ * deadline/cancel/revocation vocabulary — and refuses new openings
536
+ * on the service. Deliberately NOT the public path's
537
+ * let-existing-calls-finish behavior.
538
+ */
539
+ serveOrgDuplex(service: string, access: OrgAccess, handler: OrgDuplexHandler, options: OrgServeOptions): OrgServeHandle;
540
+ /**
541
+ * Sign and send a `0x0D02` signalling envelope to `peer` — the
542
+ * session-independent path, so no session with `peer` is needed.
543
+ *
544
+ * `dialogHex` is the attempt's id, 16 lowercase hex digits — the
545
+ * same value {@link BrowserNode.offerPeer} resolves to. Never a
546
+ * number: a `u64` dialog through a JS number comes back rounded,
547
+ * and an envelope signed for a dialog that names no attempt kills
548
+ * the attempt at its ICE deadline.
549
+ */
550
+ signal(peerHex: string, dialogHex: string, kind: string, payload: Uint8Array): Promise<void>;
551
+ /**
552
+ * Connect **directly** to another browser node — plan §9, from a
553
+ * page.
554
+ *
555
+ * The whole sequence, driven here so a page does not have to:
556
+ * bring up the relayed session through the anchor if there is not
557
+ * one, offer, trickle candidates both ways as signed envelopes,
558
+ * and run the Noise handshake over the DataChannel that results.
559
+ * The direct session then **replaces** the relayed one rather than
560
+ * joining it (§9 step 4), through the node's own session-table
561
+ * fence.
562
+ *
563
+ * `nodeIdHex` is the peer's mesh id in the spelling every peer
564
+ * method takes: 16 lowercase HEX digits —
565
+ * {@link BrowserNode.nodeIdHex} on the other tab, or
566
+ * {@link NodeDescriptor.peerIdHex} from a
567
+ * {@link BrowserNode.query} descriptor. It is **not**
568
+ * {@link NodeDescriptor.nodeId}, which is that id's decimal
569
+ * spelling; pass {@link peerIdHex} over it, or the decimal digits
570
+ * name a different node.
571
+ *
572
+ * **Nothing else is passed, and nothing else can be.** No SDP, no
573
+ * candidate, no key: the peer's Noise static comes from its
574
+ * signature-verified announcement and the pre-shared key from the
575
+ * credential this node connected with. A page that could supply a
576
+ * key could supply any key.
577
+ *
578
+ * **Idempotent on a healthy pair.** When the pair is already direct
579
+ * and its channel open, this resolves `direct` with the live dialog
580
+ * and sends nothing: a second offer would replace the working link
581
+ * and close it under the other side. So a library may call it "to be
582
+ * sure" before opening a stream.
583
+ *
584
+ * Returns a {@link PeerConnectOutcome}. It **rejects** only for
585
+ * something that is not a disposition of the attempt — a closed
586
+ * node, a peer that answered `Reject`, a malformed peer id.
587
+ */
588
+ connectPeer(nodeIdHex: string): Promise<PeerConnectOutcome>;
589
+ /**
590
+ * The four primitives the shared drive loop calls, bound to this
591
+ * tab's own node.
592
+ *
593
+ * The dialog-named `*_in` forms, not the peer-only ones: one
594
+ * primitive set for both surfaces means one classification, and a
595
+ * direct caller loses nothing by naming the dialog it already
596
+ * holds.
597
+ */
598
+ private peerPrimitives;
599
+ /**
600
+ * Run the offerer's Noise handshake on an attempt whose
601
+ * DataChannel is already open, and type the result the way
602
+ * {@link BrowserNode.connectPeer} types it.
603
+ *
604
+ * `connectPeer` is the drive loop plus exactly this call, so there
605
+ * is one classifier and not two. It is public because the four
606
+ * `#[wasm_bindgen]` methods are a page surface in their own right:
607
+ * a page that drove `peer_offer`, `peer_accept_offer` and
608
+ * `peer_candidate` itself — because it wanted to observe or
609
+ * intervene between them — would otherwise have to re-derive the
610
+ * typing of the last step, and a second classifier is a second
611
+ * set of bugs.
612
+ *
613
+ * `dialog` is the id `peer_offer` returned, and it is CHECKED
614
+ * rather than assumed: `peer_handshake` resolves to the dialog it
615
+ * actually ran for, and any mismatch is this caller's attempt
616
+ * having been replaced underneath it. Reporting `direct` with the
617
+ * caller's own dialog on any success is how a stale caller was
618
+ * told its superseded attempt had connected — while the leaf had
619
+ * handshaken the attempt that replaced it.
620
+ */
621
+ handshakePeer(nodeIdHex: string, dialog: string): Promise<PeerConnectOutcome>;
622
+ /**
623
+ * Accept the offer `nodeIdHex` sent, and drive the attempt to a
624
+ * direct session — {@link BrowserNode.connectPeer}'s counterpart.
625
+ *
626
+ * Call it when a `signal` event arrives from that peer: the offer
627
+ * it answers is the **verified envelope** the leaf already holds,
628
+ * so the page neither sees nor supplies the SDP.
629
+ *
630
+ * The answerer does not run the handshake — §9 step 4 puts Noise
631
+ * in the offerer's role, so this waits for the direct session to
632
+ * install from the inbound message instead. Same
633
+ * {@link PeerConnectOutcome}, same meanings.
634
+ */
635
+ acceptPeer(nodeIdHex: string): Promise<PeerConnectOutcome>;
636
+ /**
637
+ * Read one `peer_candidate` status for a live attempt.
638
+ *
639
+ * Exposed because it is the only way to observe an attempt without
640
+ * driving it to a conclusion — which is what a witness asserting
641
+ * the shape of the exchange needs, and what a page showing
642
+ * connection progress wants.
643
+ */
644
+ peerAttempt(nodeIdHex: string): Promise<PeerAttemptStatus>;
645
+ /**
646
+ * Every leaf counter, including each drop reason. Values are exact
647
+ * decimal strings because they are `u64` on the Rust side.
648
+ */
649
+ counters(): Record<string, string>;
650
+ /**
651
+ * The RTC transport's `RtcStats`, in the **same field names** the
652
+ * native `RtcStats` uses — plan §10's telemetry surface on the
653
+ * leaf.
654
+ *
655
+ * Values are exact decimal strings, for the reason
656
+ * {@link BrowserNode.counters} gives. `notApplicable` is not a
657
+ * counter: it maps each native field a leaf has no meaning for to
658
+ * the reason it has none, because a field frozen at `0` reads as
659
+ * an observation ("no ingress overflow", "no STUN requests
660
+ * answered") and a browser leaf is not entitled to those claims.
661
+ * Native makes the same choice in the other direction — it
662
+ * carries no `udpBlocked`/`udp_blocked` field at all — so
663
+ * `udp_blocked` is the one term here with no native counterpart.
664
+ *
665
+ * `ice_pending` is absent on both sides on purpose: it is
666
+ * `ice_attempted - (direct + relayed + failed + udp_blocked)`,
667
+ * the attempts still in flight, and the §10 identity is exact
668
+ * only where that residual is zero. Derive it; do not expect it.
669
+ */
670
+ rtcStats(): RtcStatsReading;
671
+ /**
672
+ * Arm the network-change re-attempt trigger: `online` events and
673
+ * an ICE `disconnected` → `failed` transition drive **one**
674
+ * bounded re-attempt per network change, per peer.
675
+ *
676
+ * Opt-in, and that is a decision rather than an omission.
677
+ * Re-dialling is policy and policy is the application's: a page
678
+ * may be holding a pair on the routed path deliberately, may be
679
+ * tearing down, may be on a metered link. §9 step 6 makes the
680
+ * routed session a supported disposition rather than a degraded
681
+ * one, so "this pair is relayed" is not a fault the library may
682
+ * assume it should fix.
683
+ *
684
+ * Idempotent — a second call installs no second listener. Only
685
+ * pairs this node took direct **as the offerer** are
686
+ * re-attempted: `connectPeer`'s side owns the repair, and both
687
+ * ends re-offering one network change would be two attempts per
688
+ * pair plus a supersession race.
689
+ *
690
+ * Once armed, {@link BrowserNode.retryReport} is how a page
691
+ * observes it.
692
+ */
693
+ enableNetworkRetry(): void;
694
+ /**
695
+ * What the re-attempt owner has seen and done.
696
+ *
697
+ * `started` is the assertion that matters: one network change, one
698
+ * re-attempt, however many observations of it the browser
699
+ * delivered. `triggers` is how many arrived and `coalesced` is
700
+ * the difference the owner absorbed — reported rather than
701
+ * hidden, because "the trigger never fired" and "it fired and was
702
+ * absorbed" are different facts and only one of them is a defect.
703
+ */
704
+ retryReport(): RetryReport;
705
+ /** Publish this node's capabilities as a signed fold announcement. */
706
+ announce(capabilities: readonly string[]): Promise<void>;
707
+ /** Find the nodes offering a capability. */
708
+ query(capability: string): Promise<NodeDescriptor[]>;
709
+ /** Listen for one event tag. Returns a cancel handle. */
710
+ on<T extends LeafEvent['type']>(type: T, handler: (event: LeafEventOf<T>) => void): Unsubscribe;
711
+ /** Listen for every event. Returns a cancel handle. */
712
+ onEvent(handler: (event: LeafEvent) => void): Unsubscribe;
713
+ /** Consume events as an async iterable. */
714
+ events(): AsyncIterableIterator<LeafEvent>;
715
+ /**
716
+ * Narrow an ICE failure to `udp-blocked` when the evidence is there.
717
+ *
718
+ * Anything that is not an `ice-timeout` is returned untouched. For an
719
+ * `ice-timeout` it gathers the two observations the correction
720
+ * requires — the anchor answering over HTTPS, and a STUN binding to
721
+ * the address that anchor published going unanswered — and returns
722
+ * the classification {@link classifyRtcError} makes of them. With no
723
+ * address to probe there is no evidence, and the error comes back
724
+ * unchanged.
725
+ *
726
+ * Public because a page holding an `rtc_failure` event's error, or a
727
+ * Playwright witness holding a rejection, wants the same narrowing
728
+ * the connect path gets.
729
+ */
730
+ refineIceFailure(error: LeafError): Promise<LeafError>;
731
+ /**
732
+ * Close the node, its streams and its event surface.
733
+ *
734
+ * **Every stream this node handed out is ended first**, and that
735
+ * is a behaviour change: an iterator parked in `for await`
736
+ * completes here rather than hanging forever on a node that will
737
+ * never emit again, and `onMessage` listeners are dropped. It is
738
+ * the disposition the leader-proxied surface already gave its
739
+ * streams on a generation change (`MeshSession.endStreams`), so
740
+ * the two surfaces now dispose of a stream the same way; the
741
+ * direct one was the outlier.
742
+ *
743
+ * The order is load-bearing: the streams go before
744
+ * `inner.close()`, because a wasm `LeafStream.close` retires its
745
+ * handle *through the node* and a node already closed would
746
+ * report that as a failed close instead of performing it.
747
+ *
748
+ * **Every one of those closes is attempted.** `close` latches and
749
+ * empties its set before the loop, so there is no second chance at
750
+ * any of it: a child whose `close` threw out of the loop left the
751
+ * next child open with its iterator parked forever, the hub
752
+ * dispatching, the leaf never closed — and the retry a page then
753
+ * makes returns immediately, because the latch is set. So each
754
+ * obligation is discharged independently and the failures are
755
+ * collected, which is the only reading under which the latch is
756
+ * honest.
757
+ *
758
+ * Reported, not swallowed: if anything threw, this throws an
759
+ * `AggregateError` whose `errors` are the {@link LeafError}s in
760
+ * teardown order — always an aggregate, however many failed,
761
+ * because "how many parts of the close failed" is not a shape a
762
+ * caller should have to branch on to find out that one did. A
763
+ * host-supplied {@link LeafWasmStreamLike} is the surface that
764
+ * makes this reachable; the leaf's own `close` does not throw.
765
+ */
766
+ close(): void;
767
+ /**
768
+ * The one `on_event` callback. Two events teach the probe something:
769
+ * `connected` proves the HTTPS bootstrap worked and carries the
770
+ * `rtc_addr` the anchor published, which is the only address this
771
+ * package may aim a STUN probe at.
772
+ */
773
+ private ingest;
774
+ }
775
+ /**
776
+ * Connect to the mesh through an anchor.
777
+ *
778
+ * Loads and instantiates the leaf wasm (once per page, whatever the
779
+ * number of `connect()` calls), hands it the credential, and resolves a
780
+ * {@link BrowserNode}. Rejects with a typed {@link LeafError}; an ICE
781
+ * failure is classified per {@link BrowserNode.refineIceFailure}.
782
+ */
783
+ export declare function connect(options: ConnectOptions): Promise<BrowserNode>;
784
+ /**
785
+ * Build exactly the options object `LeafNode.connect` reads, credential
786
+ * resolved, origin defaulted, custodial identity validated.
787
+ *
788
+ * Exported because `src/leader/` builds `MeshSession.open`'s options on
789
+ * top of it: the two surfaces take the same connect keys, and a key
790
+ * added here must not be silently missing there. A dropped option is
791
+ * invisible from a page — it has already cost one witness — so the two
792
+ * share the builder rather than each listing the keys.
793
+ */
794
+ export declare function buildConnectRequest(options: ConnectOptions): LeafWasmConnectOptions;
795
+ /**
796
+ * Parse a `counters_json()` payload. Values stay strings because they
797
+ * are `u64` on the Rust side and a JS number would round the large
798
+ * ones. Exported for the session surface, whose `counters_json` is a
799
+ * promise but whose payload is identical.
800
+ */
801
+ export declare function parseCounters(json: string): Record<string, string>;
802
+ /**
803
+ * Parse one `peer_candidate` reading.
804
+ *
805
+ * `remainingMs` stays a string for the reason every other `u64` on
806
+ * this boundary does, and `state` is validated rather than cast: an
807
+ * unrecognised state would otherwise fall through both the
808
+ * `iceTimeout` and the `open` arm of a drive loop and spin until the
809
+ * leaf's own deadline, which is the slowest possible way to report a
810
+ * boundary that changed shape.
811
+ *
812
+ * `candidateError` is carried through as a value. The leaf warns it
813
+ * to the console as well, but a console line is not something a
814
+ * page, a witness or a diagnostic can read — and this parser used to
815
+ * drop the field, which made the leaf's machine-readable diagnostic
816
+ * unreachable from TypeScript.
817
+ *
818
+ * Exported for the unit tests, which drive it with fixed payloads.
819
+ */
820
+ export declare function parseAttemptStatus(json: string): PeerAttemptStatus;
821
+ /**
822
+ * One `rtc_stats_json()` reading — plan §10's `RtcStats` on the leaf.
823
+ *
824
+ * `counters` are exact decimal strings under the NATIVE field names;
825
+ * `notApplicable` maps each native field a leaf has no meaning for
826
+ * to the reason it has none. Kept apart in the type because they are
827
+ * different kinds of thing: one is measurement, the other is the
828
+ * refusal to pretend to measure.
829
+ */
830
+ export interface RtcStatsReading {
831
+ readonly counters: Record<string, string>;
832
+ readonly notApplicable: Record<string, string>;
833
+ }
834
+ /** What {@link BrowserNode.retryReport} reports. */
835
+ export interface RetryReport {
836
+ /** Whether {@link BrowserNode.enableNetworkRetry} has been called. */
837
+ readonly armed: boolean;
838
+ /** Triggers filed by the `online` listener. */
839
+ readonly online: string;
840
+ /** Triggers filed by the ICE `disconnected` → `failed` watcher. */
841
+ readonly iceFailed: string;
842
+ /** Every trigger seen, whatever the owner decided. */
843
+ readonly triggers: string;
844
+ /** Re-attempts STARTED. One per network change, per peer. */
845
+ readonly started: string;
846
+ /** Triggers absorbed into an episode that already had its attempt. */
847
+ readonly coalesced: string;
848
+ /** Triggers for a peer with nothing to repair. */
849
+ readonly notEligible: string;
850
+ /** Episodes open right now. */
851
+ readonly openEpisodes: number;
852
+ /** Pairs this node took direct as the offerer, i.e. the eligible set. */
853
+ readonly owned: number;
854
+ /**
855
+ * The last re-attempt's disposition, in
856
+ * {@link PeerConnectOutcome}'s vocabulary, or `null` before the
857
+ * first one. `offerRefused` is the one name not in that union: it
858
+ * is a repair that could not even re-offer, which `connectPeer`
859
+ * surfaces as a thrown error rather than an outcome.
860
+ */
861
+ readonly last: string | null;
862
+ }
863
+ /**
864
+ * Parse an `rtc_stats_json()` payload.
865
+ *
866
+ * `not_applicable` is lifted out rather than left among the
867
+ * counters: a consumer iterating the reading must not find a field
868
+ * whose value is a sentence where every other value is a number.
869
+ *
870
+ * Exported for the unit tests, which drive it with fixed payloads.
871
+ */
872
+ export declare function parseRtcStats(json: string): RtcStatsReading;
873
+ /**
874
+ * Parse a `retry_report()` payload.
875
+ *
876
+ * Counts stay strings because they are `u64`, for the reason
877
+ * {@link parseCounters} gives. Exported for the unit tests.
878
+ */
879
+ export declare function parseRetryReport(json: string): RetryReport;
880
+ /** What {@link refineIceFailure} is allowed to know. */
881
+ interface RefineContext {
882
+ /** `null` when the credential carried the endpoint and the page never named it. */
883
+ readonly bootstrapUrl: string | null;
884
+ /** The leaf already demonstrated a working bootstrap this session. */
885
+ readonly bootstrapObserved: boolean;
886
+ readonly anchorRtcAddr: string | null;
887
+ readonly failureTyping: FailureTypingOptions;
888
+ }
889
+ /**
890
+ * The orchestration half of the correction: gather the observations,
891
+ * then let the pure classifier decide. Exported for the unit tests,
892
+ * which drive it with fake probes.
893
+ */
894
+ export declare function refineIceFailure(error: LeafError, context: RefineContext): Promise<LeafError>;
895
+ /**
896
+ * Parse `query`'s JSON array — `{ node_id, entity_id, capabilities,
897
+ * rtc_addr, noise_pubkey, version }` per entry, with `node_id` and
898
+ * `version` kept as exact decimal strings. Exported for the unit tests.
899
+ *
900
+ * `peerIdHex` is derived beside the decimal `nodeId`, never instead
901
+ * of it: the peer methods take hex and the announcement carries
902
+ * decimal, and a consumer that cannot see both cannot tell which it
903
+ * is holding. A `node_id` that is not a decimal `u64` is a boundary
904
+ * that changed shape and is refused rather than turned into an
905
+ * address.
906
+ */
907
+ export declare function parseDescriptors(json: string): NodeDescriptor[];
908
+ export {};