@robota-sdk/agent-transport-webrtc 3.0.0-beta.100

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 (84) hide show
  1. package/CHANGELOG.md +694 -0
  2. package/LICENSE +661 -0
  3. package/README.md +111 -0
  4. package/dist/node/index.cjs +1 -0
  5. package/dist/node/index.d.cts +1650 -0
  6. package/dist/node/index.d.cts.map +1 -0
  7. package/dist/node/index.d.ts +1650 -0
  8. package/dist/node/index.d.ts.map +1 -0
  9. package/dist/node/index.js +2 -0
  10. package/dist/node/index.js.map +1 -0
  11. package/package.json +93 -0
  12. package/src/__tests__/admission-secure-by-default.test.ts +63 -0
  13. package/src/__tests__/admission-steps.test.ts +46 -0
  14. package/src/__tests__/device-mesh-relay.test.ts +441 -0
  15. package/src/__tests__/device-mesh.test.ts +682 -0
  16. package/src/__tests__/discovering-mesh-relay.test.ts +234 -0
  17. package/src/__tests__/enrollment-link.test.ts +103 -0
  18. package/src/__tests__/fake-datachannel.ts +83 -0
  19. package/src/__tests__/handoff-grant-gate.test.ts +403 -0
  20. package/src/__tests__/local-peer-proof-gate.test.ts +215 -0
  21. package/src/__tests__/mesh-admission-contract.test.ts +38 -0
  22. package/src/__tests__/mesh-discovery.test.ts +820 -0
  23. package/src/__tests__/mesh-fixtures.ts +148 -0
  24. package/src/__tests__/mesh-internet.test.ts +942 -0
  25. package/src/__tests__/mesh-turn-relay.test.ts +161 -0
  26. package/src/__tests__/negotiated-certificate.test.ts +76 -0
  27. package/src/__tests__/nostr-source-condition.test.ts +27 -0
  28. package/src/__tests__/operator-approval-gate.test.ts +308 -0
  29. package/src/__tests__/outbound-delivery-carrier.test.ts +155 -0
  30. package/src/__tests__/pairing-e2e.test.ts +237 -0
  31. package/src/__tests__/pairing-gate-e3.test.ts +194 -0
  32. package/src/__tests__/pairing-gate-e4.test.ts +110 -0
  33. package/src/__tests__/pairing-gate.test.ts +296 -0
  34. package/src/__tests__/pairing-relay-in-the-middle.test.ts +225 -0
  35. package/src/__tests__/rtc-channel-close.test.ts +52 -0
  36. package/src/__tests__/rtc-peer-candidates.test.ts +61 -0
  37. package/src/__tests__/session-attachment-app-view.test.ts +66 -0
  38. package/src/__tests__/session-attachment-budget.test.ts +136 -0
  39. package/src/__tests__/turn-server-bind.test.ts +59 -0
  40. package/src/__tests__/turn-server.test.ts +335 -0
  41. package/src/__tests__/turn-test-client.ts +256 -0
  42. package/src/__tests__/webrtc-implementation.test.ts +83 -0
  43. package/src/__tests__/webrtc-transport.test.ts +349 -0
  44. package/src/__tests__/ws-mesh-relay-client.test.ts +95 -0
  45. package/src/__tests__/ws-signaling-client.test.ts +108 -0
  46. package/src/admission-steps.ts +55 -0
  47. package/src/channel-delivery.ts +44 -0
  48. package/src/datachannel-loader.ts +86 -0
  49. package/src/device-mesh-node.ts +1019 -0
  50. package/src/discovering-mesh-relay.ts +675 -0
  51. package/src/enrollment-link.ts +465 -0
  52. package/src/handoff-grant-gate.ts +186 -0
  53. package/src/index.ts +139 -0
  54. package/src/local-peer-proof.ts +135 -0
  55. package/src/mainline-dht-store.ts +174 -0
  56. package/src/mesh-dht.ts +446 -0
  57. package/src/mesh-discovery.ts +87 -0
  58. package/src/mesh-item-store.ts +287 -0
  59. package/src/mesh-lan-listener.ts +193 -0
  60. package/src/mesh-mdns.ts +406 -0
  61. package/src/mesh-peer-link.ts +600 -0
  62. package/src/mesh-public-relays.ts +25 -0
  63. package/src/mesh-records.ts +357 -0
  64. package/src/mesh-relay.ts +98 -0
  65. package/src/mesh-signal.ts +116 -0
  66. package/src/mesh-turn-relay.ts +238 -0
  67. package/src/negotiated-certificate.ts +72 -0
  68. package/src/nostr-mesh-relay.ts +391 -0
  69. package/src/pairing-channel-lifecycle.ts +31 -0
  70. package/src/pairing-controllers.ts +81 -0
  71. package/src/pairing-frames.ts +34 -0
  72. package/src/pairing-gate-options.ts +133 -0
  73. package/src/pairing-gate.ts +397 -0
  74. package/src/rtc-peer.ts +429 -0
  75. package/src/session-attachment.ts +96 -0
  76. package/src/signaling.ts +62 -0
  77. package/src/stun-message.ts +341 -0
  78. package/src/transport-lifecycle-error.ts +11 -0
  79. package/src/turn-server.ts +1028 -0
  80. package/src/webrtc-delivery-lifecycle.ts +54 -0
  81. package/src/webrtc-transport-options.ts +87 -0
  82. package/src/webrtc-transport.ts +368 -0
  83. package/src/ws-mesh-relay-client.ts +117 -0
  84. package/src/ws-signaling-client.ts +139 -0
@@ -0,0 +1,1650 @@
1
+ import { IProtocolSession, ISessionMessageHandlerOptions, SessionResumeBridge, createSessionMessageHandler } from "@robota-sdk/agent-transport";
2
+ import { IDeviceHandshakeIdentity, IDeviceHandshakeResult, IDeviceMeshAdmission, IDeviceRevocationList, IIdentityContext, IListUpdate, IPairRendezvous, IPairingResult, ISessionDescriptor, ISigningKeyRevocation, TDeviceCapability, startPairingHandshake } from "@robota-sdk/agent-remote-pairing";
3
+ import { ConnectionAuthority, IFileFrameChannel, IOperatorApprover, IPeerAdmission } from "@robota-sdk/agent-interface-session-mobility";
4
+ import { IConfigurableTransport } from "@robota-sdk/agent-interface-transport";
5
+ //#region src/handoff-grant-gate.d.ts
6
+ /** The frame a source device presents to show it holds a grant for this transfer. */
7
+ interface IHandoffGrantFrame {
8
+ readonly t: 'handoff-grant';
9
+ /**
10
+ * The signed grant, carried opaquely.
11
+ *
12
+ * `unknown` on purpose: this package cannot import the grant type without importing the package
13
+ * that owns the crypto, and typing it structurally here would create a second declaration of a
14
+ * signed object's shape — which is how a field ends up checked in one copy and not the other.
15
+ * The verifier owns the type; this gate owns the envelope.
16
+ */
17
+ readonly grant: unknown;
18
+ }
19
+ /**
20
+ * The cross-device authorization this gate requires before exposing the session.
21
+ *
22
+ * Absent → the gate behaves exactly as before. Requiring a grant is opt-in because an ordinary
23
+ * remote peer has no hand-off to authorize, and a gate that demanded one unconditionally would
24
+ * refuse every legitimate remote session.
25
+ */
26
+ interface IHandoffGrantProof {
27
+ /**
28
+ * Judge a presented grant. Owned by the verifier — this package asks and does not decide.
29
+ *
30
+ * Async because verifying a signature is: `verifyHandoffGrant` returns a promise, and a
31
+ * synchronous seam here would force the owner to block or to pre-verify, and pre-verifying means
32
+ * deciding before the channel that the grant is bound to even exists.
33
+ *
34
+ * Returns the admission the consumer receives, so the trust level travels as a value.
35
+ */
36
+ readonly verify: (grant: unknown) => Promise<IPeerAdmission>;
37
+ /**
38
+ * Ask the person at THIS machine whether to accept the transfer. Absent → no consent is required.
39
+ *
40
+ * Called ONLY on a verdict the verifier admitted, and given that verdict, for two reasons that
41
+ * both matter:
42
+ *
43
+ * The prompt can then name a PROVEN origin. Asking before verification would let anyone who can
44
+ * open a channel raise a dialog on someone's machine claiming to be any device they like, which
45
+ * turns the consent step into an attack surface instead of a control.
46
+ *
47
+ * And consent is not cryptographic policy, so it does not belong inside `verify`. A verifier that
48
+ * also asked a human would make "is this grant valid" and "does this person want it" one answer,
49
+ * and the first is the one that must be decidable without a person present.
50
+ *
51
+ * Returning false — or throwing, or having no renderer to ask — refuses. Denial fails CLOSED.
52
+ */
53
+ readonly consent?: (admission: IPeerAdmission) => Promise<boolean>;
54
+ /**
55
+ * Fired on the outcome, admitted or not.
56
+ *
57
+ * On BOTH, deliberately: a consumer told only about successes cannot distinguish "no hand-off was
58
+ * offered" from "a hand-off was refused", and those call for different operator responses — the
59
+ * second is the one that belongs in front of a person.
60
+ */
61
+ readonly onAdmission?: (admission: IPeerAdmission) => void;
62
+ }
63
+ /**
64
+ * Judge one pre-accept frame against the configured grant proof.
65
+ *
66
+ * Every path that is not an admitted verification returns a refusal — including a missing config,
67
+ * which cannot happen through the gate but would be a fail-OPEN if it ever did.
68
+ */
69
+ declare function judgeHandoffGrant(parsed: unknown, proof: IHandoffGrantProof | undefined): Promise<IPeerAdmission>;
70
+ /**
71
+ * Build the frame a source device presents on the channel.
72
+ *
73
+ * Lives beside the judge that reads it, so the two cannot drift: a sender that hand-built the object
74
+ * would keep compiling after the frame gains a field, and the failure would surface as a refusal on
75
+ * the far side with no hint that the SENDER is the stale half.
76
+ *
77
+ * Deliberately not a "send" — it returns the frame and leaves transmission to whoever owns the
78
+ * channel, for the same reason `localProofFrame` does.
79
+ */
80
+ declare function handoffGrantFrame(grant: unknown): IHandoffGrantFrame;
81
+ //#endregion
82
+ //#region src/local-peer-proof.d.ts
83
+ /** The frame a local peer presents to show it reached the guarded rendezvous. */
84
+ interface ILocalProofFrame {
85
+ readonly t: 'local-proof';
86
+ readonly nonce: string;
87
+ }
88
+ /**
89
+ * The local-environment proof this gate requires before exposing the session.
90
+ *
91
+ * Absent → the gate behaves exactly as before. Requiring the proof is opt-in because a remote peer
92
+ * over WebRTC has no rendezvous to have reached, and a gate that demanded one unconditionally would
93
+ * refuse every legitimate remote session.
94
+ */
95
+ interface ILocalPeerProof {
96
+ /**
97
+ * Redeem a presented nonce. Owned by the ledger — this package asks and does not decide.
98
+ *
99
+ * Returns the admission the consumer receives, so the trust level travels as a value rather than
100
+ * being re-derived from a boolean by whoever reads it next.
101
+ */
102
+ readonly redeem: (nonce: string) => IPeerAdmission;
103
+ /**
104
+ * Fired on the outcome, admitted or not.
105
+ *
106
+ * On BOTH, deliberately: a consumer told only about successes cannot distinguish "no local peer
107
+ * connected" from "a local peer was refused", and those call for different operator responses.
108
+ */
109
+ readonly onAdmission?: (admission: IPeerAdmission) => void;
110
+ }
111
+ /**
112
+ * Build the frame a local peer presents on the channel.
113
+ *
114
+ * Lives beside the judge that reads it, so the two cannot drift: a sender that hand-built the object
115
+ * would keep compiling after the frame gains a field, and the failure would surface as a refusal on
116
+ * the far side with no hint that the SENDER is the stale half.
117
+ *
118
+ * Deliberately not a "send" — it returns the frame and leaves transmission to whoever owns the
119
+ * channel. A helper that both built and sent would need a channel to be testable, and would put the
120
+ * frame's shape and the carrier's lifecycle in one place.
121
+ */
122
+ declare function localProofFrame(nonce: string): ILocalProofFrame;
123
+ //#endregion
124
+ //#region src/pairing-gate-options.d.ts
125
+ /** E3 host reconnect/enrollment config. When present, the gate runs reactive (first-frame) mode detection. */
126
+ interface IHostReconnectConfig {
127
+ readonly hostIdentityId: string;
128
+ /** base64url SPKI advertised to a device at first-pair enrollment. */
129
+ readonly hostPublicSpki: string;
130
+ /** The host identity private key (signs reconnect challenges). */
131
+ readonly hostPrivateKey: CryptoKey;
132
+ /** Resolve a pinned device public key by id (undefined → unknown/revoked → fail closed). */
133
+ readonly resolveDevicePublicKey: (deviceId: string) => Promise<CryptoKey | undefined>;
134
+ /** Pin a device's public key on first-pair enrollment; throw to refuse before session attachment. */
135
+ readonly onEnroll: (deviceId: string, deviceSpki: string) => void;
136
+ }
137
+ /** What the receiving operator is told about a connection that would drive the session. */
138
+ interface IConnectionApprovalContext {
139
+ /** The device the handshake proved, when the carrier has a stable id for it. */
140
+ readonly deviceId?: string;
141
+ /** A trusted device coming back, rather than one pairing now. */
142
+ readonly viaReconnect: boolean;
143
+ /** Aborted when the connection goes away before an answer: withdraw the question. */
144
+ readonly signal: AbortSignal;
145
+ }
146
+ /**
147
+ * The receiving operator's say over one connection. `approve` resolves `true` only on the operator's
148
+ * explicit yes; `false` or a rejection refuses the connection.
149
+ */
150
+ interface IConnectionApproval {
151
+ approve(context: IConnectionApprovalContext): Promise<boolean>;
152
+ }
153
+ //#endregion
154
+ //#region src/signaling.d.ts
155
+ /**
156
+ * Signaling port (REMOTE-002). The WebRTC transport is decoupled from any concrete signaling server via this
157
+ * port: it exchanges opaque SDP/ICE blobs by rendezvous id. The port carries **only** SDP offers/answers + ICE
158
+ * candidates — never session content — so a signaling server (or the in-memory test pair) is content-blind.
159
+ */
160
+ /** The kinds of signal a peer exchanges through the relay. */
161
+ type TSignalKind = 'offer' | 'answer' | 'ice';
162
+ /** An opaque signaling message relayed by rendezvous id. `data` is an SDP description or an ICE candidate. */
163
+ interface ISignalMessage {
164
+ readonly kind: TSignalKind;
165
+ /** Opaque payload (serialized SDP or ICE candidate). The signaling layer never inspects it. */
166
+ readonly data: unknown;
167
+ }
168
+ /** Port a WebRTC peer uses to reach its counterpart at a rendezvous id. */
169
+ interface ISignalingClient {
170
+ /** Send a signal to the counterpart at the rendezvous. */
171
+ send(message: ISignalMessage): void;
172
+ /** Subscribe to signals from the counterpart. Returns an unsubscribe function. */
173
+ onSignal(handler: (message: ISignalMessage) => void): () => void;
174
+ /** Tear down the signaling channel. */
175
+ close(): void;
176
+ }
177
+ /**
178
+ * An in-process pair of {@link ISignalingClient}s wired directly to each other — for tests + loopback only
179
+ * (no server, no network). Each side's `send` is delivered to the other side's handlers on a microtask.
180
+ */
181
+ declare function createInMemorySignalingPair(): [ISignalingClient, ISignalingClient];
182
+ //#endregion
183
+ //#region src/datachannel-loader.d.ts
184
+ /** The slice of a `node-datachannel` data channel this transport drives. */
185
+ interface INdcDataChannel {
186
+ close(): void;
187
+ sendMessage(message: string): boolean;
188
+ isOpen(): boolean;
189
+ bufferedAmount(): number;
190
+ /** The label the opening side gave the channel. */
191
+ getLabel?(): string;
192
+ onOpen(callback: () => void): void;
193
+ onClosed(callback: () => void): void;
194
+ onError(callback: (error: string) => void): void;
195
+ onMessage(callback: (message: string | Buffer | ArrayBuffer) => void): void;
196
+ }
197
+ /** An ICE server as `node-datachannel` takes it. */
198
+ interface INdcIceServer {
199
+ readonly hostname: string;
200
+ readonly port: number;
201
+ readonly username?: string;
202
+ readonly password?: string;
203
+ readonly relayType?: 'TurnUdp' | 'TurnTcp' | 'TurnTls';
204
+ }
205
+ interface INdcConfig {
206
+ iceServers: (string | INdcIceServer)[];
207
+ iceTransportPolicy?: 'all' | 'relay';
208
+ disableAutoNegotiation?: boolean;
209
+ }
210
+ /** The slice of a `node-datachannel` peer connection this transport drives. */
211
+ interface INdcPeerConnection {
212
+ close(): void;
213
+ setLocalDescription(type?: 'offer' | 'answer'): void;
214
+ setRemoteDescription(sdp: string, type: 'offer' | 'answer'): void;
215
+ localDescription(): {
216
+ type: string;
217
+ sdp: string;
218
+ } | null;
219
+ remoteFingerprint(): {
220
+ value: string;
221
+ algorithm: string;
222
+ };
223
+ addRemoteCandidate(candidate: string, mid: string): void;
224
+ createDataChannel(label: string): INdcDataChannel;
225
+ state(): string;
226
+ /** Diagnostics; not every build exposes them. */
227
+ iceState?(): string;
228
+ gatheringState?(): string;
229
+ signalingState?(): string;
230
+ onLocalDescription(callback: (sdp: string, type: string) => void): void;
231
+ onLocalCandidate(callback: (candidate: string, mid: string) => void): void;
232
+ onStateChange(callback: (state: string) => void): void;
233
+ onDataChannel(callback: (channel: INdcDataChannel) => void): void;
234
+ }
235
+ /** The subset of the `node-datachannel` module surface this transport constructs. */
236
+ interface IDataChannelModule {
237
+ PeerConnection: new (name: string, config: INdcConfig) => INdcPeerConnection;
238
+ }
239
+ /** Resolve a module by id. The default resolves the real module; tests inject a throwing resolver. */
240
+ type TModuleResolver = (id: string) => unknown;
241
+ declare function loadDataChannel(resolve?: TModuleResolver): IDataChannelModule;
242
+ //#endregion
243
+ //#region src/webrtc-transport-options.d.ts
244
+ /**
245
+ * A single ICE (STUN/TURN) server for the host transport. `urls` is one `stun:`/`stuns:`/`turn:`/`turns:` url
246
+ * (optionally `?transport=tcp`); TURN servers carry username/credential. The browser peer uses the native DOM
247
+ * `RTCIceServer`, a separate, wider shape. Kept a plain interface (no DOM dependency here).
248
+ */
249
+ interface IIceServer {
250
+ readonly urls: string;
251
+ readonly username?: string;
252
+ readonly credential?: string;
253
+ }
254
+ /**
255
+ * Construction options for {@link WebRtcTransport}. The signaling client is injected; the transport declares no
256
+ * settings schema of its own (`optionsSchema` is empty).
257
+ */
258
+ interface IWebRtcTransportOptions {
259
+ readonly cryptoContext: IIdentityContext;
260
+ /** Host-owned usage read models, available only after admission. */
261
+ readonly personalUsageReporter?: ISessionMessageHandlerOptions['personalUsageReporter'];
262
+ readonly usageReporter?: ISessionMessageHandlerOptions['usageReporter'];
263
+ readonly storedSessionUsageReporter?: ISessionMessageHandlerOptions['storedSessionUsageReporter'];
264
+ /** Signaling port used to exchange SDP/ICE with the remote peer by rendezvous id. */
265
+ readonly signaling: ISignalingClient;
266
+ /** Optional ICE servers (STUN/TURN). Omitted → host-candidate/loopback only. */
267
+ readonly iceServers?: readonly IIceServer[];
268
+ /**
269
+ * REMOTE-004 defense-in-depth: when true, restrict ICE to **relay (TURN) candidates only**, so
270
+ * host/server-reflexive candidates are never used. Requires a TURN server in `iceServers`. Mapped to the
271
+ * connection's `iceTransportPolicy: 'relay'`.
272
+ */
273
+ readonly forceTurn?: boolean;
274
+ /**
275
+ * REMOTE-008 pairing secret. When set, the data channel is **pairing-gated**: it carries only pairing frames
276
+ * until the directional-HMAC handshake accepts (channel-bound to the DTLS fingerprints), and only THEN is the
277
+ * session exposed — fail closed on mismatch/timeout. When omitted, the constructor throws unless the caller
278
+ * explicitly opts out with `open: true` and an `openReason` (loopback / tests); only then is the channel
279
+ * exposed immediately with no pairing.
280
+ */
281
+ readonly secret?: string;
282
+ /**
283
+ * SEC-008: run with NO pairing gate. Requires `openReason`.
284
+ *
285
+ * Omitting `secret` used to mean this implicitly, which is how a remote peer reached the session
286
+ * because a field was left unset. It is still a legitimate mode — loopback, tests — but it is now
287
+ * a thing the host says rather than a thing that happens.
288
+ */
289
+ readonly open?: boolean;
290
+ /** SEC-008: why running with no pairing gate is correct here. Required when `open` is true. */
291
+ readonly openReason?: string;
292
+ /** REMOTE-008: fired when pairing accepts + the session is exposed (host lifecycle → status 'paired'). Carries the first-pair result (E4 uses its sessionKey). */
293
+ readonly onPaired?: (result?: IPairingResult) => void;
294
+ /** REMOTE-008: fired when pairing rejects/times out (host lifecycle → teardown; the channel is already closed). */
295
+ readonly onPairingFailed?: () => void;
296
+ /** REMOTE-012 E3: host reconnect/enrollment config. When set, the gate admits first-pair (with enrollment) OR a pinned-device reconnect. */
297
+ readonly reconnect?: IHostReconnectConfig;
298
+ /** SEC-010 (#1810): require a guarded-rendezvous nonce before the session is exposed. Absent → unchanged. */
299
+ readonly localPeer?: ILocalPeerProof;
300
+ /** Ask the receiving operator before each connection reaches the session. Absent → unchanged. */
301
+ readonly connectionApproval?: IConnectionApproval;
302
+ /** REMOTE-013 E4: a session-scoped resume bridge (owned by the controller across reconnects). Passed to the gate so the paired session flows through it (seq/buffer) and survives channel drops. */
303
+ readonly resumeBridge?: SessionResumeBridge;
304
+ /** REMOTE-013 E4: fired when a PAIRED data channel drops (so the controller can run the reconnect loop). Not fired for a pre-accept failure (that is `onPairingFailed`). */
305
+ readonly onDropped?: () => void;
306
+ /** Observe an outbound session-event delivery failure before the carrier drops. */
307
+ readonly onDeliveryError?: (error: Error, event: string) => void;
308
+ /** Test seam: inject the `node-datachannel` module (defaults to the real lazy loader). */
309
+ readonly loadDataChannel?: () => IDataChannelModule;
310
+ }
311
+ //#endregion
312
+ //#region src/webrtc-transport.d.ts
313
+ /**
314
+ * WebRTC P2P transport (REMOTE-001/002): carries an `IProtocolSession` over an `RTCDataChannel` using the
315
+ * SAME transport-neutral session bridge as the WebSocket transport (`createSessionMessageHandler` from
316
+ * `@robota-sdk/agent-transport`). The host is the offerer: it creates the data channel + offer and wires the
317
+ * channel as soon as it is created. `defaultEnabled: false` — nothing starts it automatically. Admission is
318
+ * fixed at construction: a pairing `secret` gates the session behind the pairing handshake, and running
319
+ * without one requires `open: true` with an `openReason`. The signaling client is injected and can be an
320
+ * in-memory loopback for tests.
321
+ */
322
+ declare class WebRtcTransport implements IConfigurableTransport<IProtocolSession> {
323
+ private readonly options;
324
+ readonly name = "webrtc";
325
+ readonly lifecycle: Readonly<{
326
+ kind: "service";
327
+ }>;
328
+ readonly defaultEnabled = false;
329
+ readonly optionsSchema: {};
330
+ private session?;
331
+ private peer?;
332
+ private unsubscribeSignal?;
333
+ private cleanupHandler?;
334
+ /** Invalidates pending async startup work and scopes pairing/drop state to one start generation. */
335
+ private generation;
336
+ /** Local DTLS fingerprint captured for pairing channel binding. */
337
+ private localFingerprint?;
338
+ /** Pairing gate for the current channel. */
339
+ private pairingGate?;
340
+ /** Whether this start generation has sent its offer; local candidates wait for it. */
341
+ private offerSent;
342
+ private localCandidates;
343
+ /** Whether this start generation has taken its one answer; any later answer is ignored. */
344
+ private answered;
345
+ /** Pre-gate channel frames, replayed into the gate once it exists. Bounded. */
346
+ private pendingFrames;
347
+ private stopAwaitingCertificate?;
348
+ private readonly deliveryLifecycle;
349
+ constructor(options: IWebRtcTransportOptions);
350
+ validateOptions(): boolean;
351
+ attach(session: IProtocolSession): void;
352
+ private createPeer;
353
+ private wireSignaling;
354
+ private sendCandidate;
355
+ private requireCurrentPeer;
356
+ start(): Promise<void>;
357
+ /**
358
+ * With a pairing secret, the answer must advertise exactly one DTLS fingerprint; its algorithm is the one the
359
+ * verified certificate is hashed with. A refused answer fails pairing and closes the channel.
360
+ */
361
+ private remoteFingerprintAlgorithm;
362
+ private failPairing;
363
+ /** Build the pairing gate once the DTLS layer has verified the remote certificate. */
364
+ private awaitVerifiedCertificate;
365
+ private startPairing;
366
+ private wireChannel;
367
+ stop(): Promise<void>;
368
+ }
369
+ //#endregion
370
+ //#region src/ws-signaling-client.d.ts
371
+ /** A minimal WebSocket-like surface — so tests can inject a fake without a real socket. */
372
+ interface IWebSocketLike {
373
+ send(data: string): void;
374
+ close(): void;
375
+ readyState: number;
376
+ on(event: 'open' | 'message' | 'error' | 'close', handler: (arg: unknown) => void): void;
377
+ }
378
+ interface IWsSignalingClientOptions {
379
+ /** Relay URL, e.g. `ws://127.0.0.1:1234`. */
380
+ readonly url: string;
381
+ /** Rendezvous id to join. */
382
+ readonly rendezvous: string;
383
+ /**
384
+ * Explicit error sink. Called on a relay `error` frame, a socket error, or a close-before-join — the signaling
385
+ * channel cannot function and the caller must know (no silent degrade).
386
+ */
387
+ readonly onError?: (error: Error) => void;
388
+ /** Called once the relay confirms the rendezvous `join` (the `joined` frame). */
389
+ readonly onReady?: () => void;
390
+ /** Injectable socket factory (defaults to a real `ws` WebSocket) — for tests. */
391
+ readonly createSocket?: (url: string) => IWebSocketLike;
392
+ }
393
+ declare class WsSignalingClient implements ISignalingClient {
394
+ private readonly options;
395
+ private readonly socket;
396
+ private readonly handlers;
397
+ private readonly outbox;
398
+ private joined;
399
+ private closed;
400
+ constructor(options: IWsSignalingClientOptions);
401
+ private handleOpen;
402
+ private handleMessage;
403
+ private handleClose;
404
+ private fail;
405
+ private writeSignal;
406
+ send(message: ISignalMessage): void;
407
+ onSignal(handler: (message: ISignalMessage) => void): () => void;
408
+ close(): void;
409
+ }
410
+ //#endregion
411
+ //#region src/rtc-peer.d.ts
412
+ type TRtcPeerState = 'new' | 'connecting' | 'connected' | 'disconnected' | 'failed' | 'closed';
413
+ type TRtcChannelState = 'connecting' | 'open' | 'closed';
414
+ /** Why and how a connection or channel ended, for a refusal a person can act on. */
415
+ interface IRtcPeerDiagnostics {
416
+ readonly state: TRtcPeerState;
417
+ readonly ice?: string;
418
+ readonly gathering?: string;
419
+ readonly signaling?: string;
420
+ /** The states the connection went through, in order. */
421
+ readonly history: readonly TRtcPeerState[];
422
+ /** `local`: this side closed it; `handler-error`: a handler threw and the connection was closed. */
423
+ readonly closedBy?: 'local' | 'handler-error';
424
+ readonly handlerError?: string;
425
+ readonly localCandidates: number;
426
+ readonly remoteCandidates: number;
427
+ }
428
+ interface IRtcCandidate {
429
+ readonly candidate: string;
430
+ readonly mid: string;
431
+ }
432
+ interface IRtcPeerOptions {
433
+ readonly iceServers?: readonly IIceServer[];
434
+ /** Relay (TURN) candidates only. */
435
+ readonly forceTurn?: boolean;
436
+ /** Test seam: the `node-datachannel` module. */
437
+ readonly loadDataChannel?: () => IDataChannelModule;
438
+ }
439
+ declare class RtcChannel {
440
+ private readonly native;
441
+ private readonly fault;
442
+ private stateValue;
443
+ private closeCauseValue?;
444
+ private closedLocally;
445
+ private readonly messageHandlers;
446
+ private readonly stateHandlers;
447
+ constructor(native: INdcDataChannel, fault: (error: unknown) => void);
448
+ private guard;
449
+ private closedWith;
450
+ /** Why the channel closed, once it has. */
451
+ get closeCause(): string | undefined;
452
+ private setState;
453
+ get readyState(): TRtcChannelState;
454
+ /** The label the opening side gave the channel; empty when the implementation does not say. */
455
+ get label(): string;
456
+ get bufferedAmount(): number;
457
+ /** Throws when the channel is not open or the message cannot be sent. */
458
+ send(text: string): void;
459
+ onMessage(handler: (text: string) => void): () => void;
460
+ onStateChange(handler: (state: TRtcChannelState) => void): () => void;
461
+ /**
462
+ * Closed for this side at once; the stream is reset after a grace, so what was sent just before
463
+ * still reaches the peer.
464
+ */
465
+ close(): void;
466
+ }
467
+ declare class RtcPeer {
468
+ private readonly native;
469
+ private stateValue;
470
+ private readonly stateHandlers;
471
+ private readonly candidateHandlers;
472
+ private readonly channelHandlers;
473
+ private readonly descriptionWaiters;
474
+ private hasRemoteDescription;
475
+ private readonly history;
476
+ private closedBy?;
477
+ private handlerError?;
478
+ private localCandidateCount;
479
+ private remoteCandidateCount;
480
+ /**
481
+ * Local candidates gathered before the remote description, handed out right after it. An offerer's
482
+ * candidates let the answerer reach it and start DTLS; the binding makes the remote description the
483
+ * DTLS layer checks the peer's certificate against only after the ICE layer has it, so a handshake
484
+ * that arrives while the answer is being applied is refused. Holding the candidates until the
485
+ * answer is applied keeps the answerer from reaching this side before then.
486
+ */
487
+ private readonly heldLocalCandidates;
488
+ /** Remote candidates that arrived before the remote description, applied right after it. */
489
+ private readonly earlyCandidates;
490
+ private closed;
491
+ constructor(options?: IRtcPeerOptions);
492
+ private guard;
493
+ private fault;
494
+ private remember;
495
+ /** What the connection went through, for explaining why it ended. */
496
+ diagnostics(): IRtcPeerDiagnostics;
497
+ get state(): TRtcPeerState;
498
+ createDataChannel(label: string): RtcChannel;
499
+ private localDescription;
500
+ /** Create and apply the local offer; resolves with its SDP. */
501
+ createOffer(): Promise<string>;
502
+ /** Apply the remote offer, then create and apply the answer; resolves with its SDP. */
503
+ acceptOffer(sdp: string): Promise<string>;
504
+ acceptAnswer(sdp: string): void;
505
+ private remoteDescriptionApplied;
506
+ private handOut;
507
+ /**
508
+ * Apply a remote candidate. One that arrives before the remote description waits for it (bounded);
509
+ * one the binding refuses is dropped.
510
+ */
511
+ addRemoteCandidate(candidate: IRtcCandidate): void;
512
+ /**
513
+ * The fingerprint of the certificate the DTLS layer accepted, once connected; `undefined` before.
514
+ * The DTLS layer (OpenSSL) accepts a certificate only after verifying the peer's handshake signature
515
+ * with it and matching it against the remote description's fingerprint.
516
+ */
517
+ remoteFingerprint(): {
518
+ readonly value: string;
519
+ readonly algorithm: string;
520
+ } | undefined;
521
+ onStateChange(handler: (state: TRtcPeerState) => void): () => void;
522
+ onLocalCandidate(handler: (candidate: IRtcCandidate) => void): () => void;
523
+ onDataChannel(handler: (channel: RtcChannel) => void): () => void;
524
+ close(): void;
525
+ }
526
+ //#endregion
527
+ //#region src/mesh-peer-link.d.ts
528
+ /** The offerer creates the data channel and the offer; the answerer answers it. */
529
+ type TMeshLinkRole = 'offerer' | 'answerer';
530
+ /** Why a link ended. `closed` is an orderly close by either side after or before admission. */
531
+ type TMeshLinkEnd = 'closed' | 'signaling' | 'channel-binding' | 'handshake' | 'protocol' | 'timeout';
532
+ /** How far a link got before it ended. */
533
+ type TMeshLinkStage = 'signaling' | 'connecting' | 'awaiting-channel' | 'handshake' | 'confirming' | 'admitted';
534
+ /**
535
+ * Why a link ended, with what the connection under it went through, so a refusal says more than
536
+ * "closed". `cause` is the underlying error when there was one (e.g. the device handshake refusal).
537
+ */
538
+ declare class MeshLinkEndedError extends Error {
539
+ readonly end: TMeshLinkEnd;
540
+ readonly stage: TMeshLinkStage;
541
+ readonly role: TMeshLinkRole;
542
+ readonly peer?: IRtcPeerDiagnostics;
543
+ /** Why the data channel closed, when it had. */
544
+ readonly channelClose?: string;
545
+ readonly cause?: unknown;
546
+ constructor(details: {
547
+ readonly end: TMeshLinkEnd;
548
+ readonly stage: TMeshLinkStage;
549
+ readonly role: TMeshLinkRole;
550
+ readonly detail?: string;
551
+ readonly peer?: IRtcPeerDiagnostics;
552
+ readonly channelClose?: string;
553
+ readonly cause?: unknown;
554
+ });
555
+ }
556
+ /** The DTLS fingerprints of one admitted connection, as each side sees them. */
557
+ interface IMeshChannelBinding {
558
+ readonly localFingerprint: string;
559
+ /** Of the certificate the DTLS layer verified. */
560
+ readonly remoteFingerprint: string;
561
+ }
562
+ //#endregion
563
+ //#region src/stun-message.d.ts
564
+ interface ITransportAddress {
565
+ readonly address: string;
566
+ readonly port: number;
567
+ }
568
+ //#endregion
569
+ //#region src/turn-server.d.ts
570
+ /** Who a username belongs to, and the password it was issued with. */
571
+ interface ITurnAuthorization {
572
+ /** The owner quotas are counted against and {@link TurnServer.retain} names. */
573
+ readonly owner: string;
574
+ readonly password: string;
575
+ }
576
+ interface ITurnQuotas {
577
+ /** Allocations one owner may hold at once. */
578
+ readonly allocationsPerOwner: number;
579
+ /** Allocations the server holds at once. */
580
+ readonly totalAllocations: number;
581
+ /** Bytes relayed per second for one owner, both directions together; more is dropped. */
582
+ readonly bytesPerSecondPerOwner: number;
583
+ /** The longest lifetime one Allocate or Refresh grants. */
584
+ readonly maxLifetimeSeconds: number;
585
+ /** How long an allocation may live however often it is refreshed. */
586
+ readonly maxAllocationAgeMs: number;
587
+ readonly permissionsPerAllocation: number;
588
+ readonly channelsPerAllocation: number;
589
+ }
590
+ declare const DEFAULT_TURN_QUOTAS: ITurnQuotas;
591
+ /**
592
+ * Answers to requests nobody has authenticated, per second. Such a request may carry a forged
593
+ * source address, so what it is answered with must not be worth sending it for.
594
+ */
595
+ interface IUnauthenticatedLimits {
596
+ readonly perSourcePerSecond: number;
597
+ readonly totalPerSecond: number;
598
+ }
599
+ declare const DEFAULT_UNAUTHENTICATED_LIMITS: IUnauthenticatedLimits;
600
+ interface ITurnServerOptions {
601
+ /** The address the server and its relayed sockets bind (default every IPv4 interface). */
602
+ readonly host?: string;
603
+ /** The server's UDP port (default 3478; 0 picks a free one). */
604
+ readonly port?: number;
605
+ /**
606
+ * The ports relayed sockets take, inclusive (default: any free port). A relay behind a NAT needs
607
+ * this range forwarded to it as well as its own port.
608
+ */
609
+ readonly relayPorts?: {
610
+ readonly min: number;
611
+ readonly max: number;
612
+ };
613
+ /** The address relayed transport addresses carry (default `host`, or this machine's first external IPv4). */
614
+ readonly relayAddress?: string;
615
+ /**
616
+ * Default `r`: a realm travels in the clear, so it names nothing. At most 4 bytes: a challenge is
617
+ * never larger than the request it answers, and a longer realm would not fit the first request of
618
+ * the mesh's WebRTC stack.
619
+ */
620
+ readonly realm?: string;
621
+ /** Who `username` belongs to and its password; `undefined` refuses it. Asked once per allocation. */
622
+ readonly authorize: (username: string) => Promise<ITurnAuthorization | undefined>;
623
+ readonly quotas?: Partial<ITurnQuotas>;
624
+ /** How many requests nobody authenticated are answered, per source address and in all. */
625
+ readonly unauthenticatedLimits?: Partial<IUnauthenticatedLimits>;
626
+ /**
627
+ * Whether the default peer rule lets data reach private, link-local and shared (carrier-grade
628
+ * NAT) ranges: the relay host's own network (default true — a relayed connection to a device on
629
+ * that network needs it).
630
+ */
631
+ readonly allowPrivatePeers?: boolean;
632
+ /**
633
+ * Whether data may be relayed to or from `address`. Default: not unspecified, multicast or
634
+ * broadcast addresses, not loopback unless the relay itself is on loopback, and not private
635
+ * ranges when `allowPrivatePeers` is false.
636
+ */
637
+ readonly allowPeer?: (address: string) => boolean;
638
+ readonly now?: () => number;
639
+ /** A socket failed; the server keeps running. */
640
+ readonly onError?: (error: Error) => void;
641
+ }
642
+ declare class TurnServer {
643
+ private readonly socket;
644
+ private readonly options;
645
+ private readonly bindHost;
646
+ private readonly relayAddress;
647
+ private readonly allocations;
648
+ private readonly buckets;
649
+ private readonly quotas;
650
+ private readonly realm;
651
+ private readonly limits;
652
+ /** Answers to requests nobody has authenticated yet, per source address and in all. */
653
+ private readonly answerBuckets;
654
+ private answerBudget?;
655
+ private readonly nonceSecret;
656
+ private readonly ownAddresses;
657
+ private readonly allowPeer;
658
+ private readonly sweeper;
659
+ /** Once {@link TurnServer.retain} has named them: the only owners that may allocate. */
660
+ private retained?;
661
+ private closed;
662
+ private constructor();
663
+ /** Bind the server's socket and start serving. */
664
+ static start(options: ITurnServerOptions): Promise<TurnServer>;
665
+ /** Where clients reach the server. */
666
+ get address(): ITransportAddress;
667
+ /** Allocations held, by one owner or in all. */
668
+ allocationCount(owner?: string): number;
669
+ /**
670
+ * Close every allocation whose owner is not one of `owners`, and allocate for these owners only
671
+ * from now on — also for a request whose authorization was answered before this call.
672
+ */
673
+ retain(owners: ReadonlySet<string>): void;
674
+ close(): Promise<void>;
675
+ private now;
676
+ private receive;
677
+ private send;
678
+ private reply;
679
+ private fail;
680
+ /** A nonce: when it was issued and a MAC binding that to the client's address, 16 characters. */
681
+ private nonce;
682
+ private nonceMac;
683
+ private nonceValid;
684
+ /**
685
+ * Answer a request nobody has authenticated. Its source may be forged, so the answer is never
686
+ * larger than the request (no reflection gain), and answers are limited per source address and in
687
+ * all; what is over is dropped unanswered.
688
+ */
689
+ private answerUnauthenticated;
690
+ /** An error answer to an unauthenticated request; the reason phrase is left empty to keep it small. */
691
+ private refuseUnauthenticated;
692
+ private challenge;
693
+ /**
694
+ * Authenticate a request. On an existing allocation only its own username and key count;
695
+ * otherwise the caller's authorization is asked. `undefined`: already answered with an error.
696
+ */
697
+ private authenticate;
698
+ private unknownAttributes;
699
+ private request;
700
+ private authenticatedRequest;
701
+ private allocate;
702
+ /** A socket for a relayed address: any free port, or a free one of the configured range. */
703
+ private relayedSocket;
704
+ private allocationPorts;
705
+ /** Whether `peer` is this server's own listening socket: relaying there would loop TURN into itself. */
706
+ private isServerItself;
707
+ /** The lifetime granted: what was asked, within the quota and the allocation's remaining age. */
708
+ private grantedLifetime;
709
+ private refresh;
710
+ private peerAddresses;
711
+ private permit;
712
+ private bindChannel;
713
+ private permitted;
714
+ /** Take `bytes` from the owner's budget for this second; false when it is spent. */
715
+ private spend;
716
+ private live;
717
+ private fromClientSend;
718
+ private fromClientChannel;
719
+ private fromPeer;
720
+ private remove;
721
+ private sweep;
722
+ }
723
+ //#endregion
724
+ //#region src/mesh-turn-relay.d.ts
725
+ /** Where a paired device's relay listens. */
726
+ interface IMeshRelayEndpoint {
727
+ readonly host: string;
728
+ readonly port: number;
729
+ }
730
+ /** A device the relay serves, with the pair's rendezvous. */
731
+ interface IMeshRelayPeer {
732
+ readonly deviceId: string;
733
+ readonly rendezvous: IPairRendezvous;
734
+ }
735
+ /** How long a relay credential is good for, by default. */
736
+ declare const DEFAULT_RELAY_CREDENTIAL_TTL_MS: number;
737
+ /** The longest a relay accepts: a credential cannot outlive the tag epochs its relay looks up. */
738
+ declare const MAX_RELAY_CREDENTIAL_TTL_MS: number;
739
+ /** A relay credential this device presents to `rendezvous`'s peer's relay, good until `now + ttlMs`. */
740
+ declare function meshRelayCredential(rendezvous: IPairRendezvous, now: number, ttlMs?: number): Promise<{
741
+ readonly username: string;
742
+ readonly credential: string;
743
+ }>;
744
+ /** ICE servers for a paired device's relay endpoints, with this pair's credential. */
745
+ declare function meshRelayIceServers(rendezvous: IPairRendezvous, endpoints: readonly IMeshRelayEndpoint[], now: number, ttlMs?: number): Promise<IIceServer[]>;
746
+ /**
747
+ * Why a relay was needed: only relayed connections are allowed, or a direct attempt got as far as
748
+ * trying paths and found none.
749
+ */
750
+ type TMeshRelayNeed = 'relay-only' | 'no-direct-path';
751
+ /**
752
+ * A connection needed a relay and none could be tried. Never a silent degrade — the connection is
753
+ * refused with this error, which carries what the direct attempt went through as its `cause`.
754
+ */
755
+ declare class MeshRelayNeededError extends Error {
756
+ readonly deviceId: string;
757
+ readonly need: TMeshRelayNeed;
758
+ constructor(deviceId: string, need: TMeshRelayNeed, options?: {
759
+ readonly cause?: unknown;
760
+ });
761
+ }
762
+ interface IMeshTurnRelayOptions {
763
+ /** Default every IPv4 interface. */
764
+ readonly host?: string;
765
+ /** Default 3478; 0 picks a free port. */
766
+ readonly port?: number;
767
+ /** The address relayed transport addresses carry, e.g. this device's public address. */
768
+ readonly relayAddress?: string;
769
+ /** The ports relayed addresses take; behind a NAT, forward them with the relay's own port. */
770
+ readonly relayPorts?: {
771
+ readonly min: number;
772
+ readonly max: number;
773
+ };
774
+ readonly quotas?: Partial<ITurnQuotas>;
775
+ /** Default true: see {@link TurnServer}. */
776
+ readonly allowPrivatePeers?: boolean;
777
+ /** Default: see {@link TurnServer}. */
778
+ readonly allowPeer?: (address: string) => boolean;
779
+ readonly now?: () => number;
780
+ readonly onError?: (error: Error) => void;
781
+ }
782
+ /** This device's relay for its paired devices. */
783
+ declare class MeshTurnRelay {
784
+ private readonly server;
785
+ private readonly now;
786
+ private peers;
787
+ /** Whose each relay-user tag is, for the peers in force and one epoch: derived once, not per request. */
788
+ private owners?;
789
+ private constructor();
790
+ static start(options?: IMeshTurnRelayOptions): Promise<MeshTurnRelay>;
791
+ /** Where the relay listens. */
792
+ get endpoint(): IMeshRelayEndpoint;
793
+ /** Allocations held, by one device or in all. */
794
+ allocationCount(deviceId?: string): number;
795
+ /**
796
+ * The devices the relay serves: the current, unrevoked peers of the lists in force. Any other
797
+ * device's credentials stop working, and its allocations close at once.
798
+ */
799
+ declarePeers(peers: readonly IMeshRelayPeer[]): void;
800
+ private authorize;
801
+ /** The peers' relay-user tags for `epoch` and the adjacent ones, by tag. */
802
+ private tagOwners;
803
+ close(): Promise<void>;
804
+ }
805
+ //#endregion
806
+ //#region src/mesh-discovery.d.ts
807
+ /** A direct signaling endpoint a peer might be listening on. */
808
+ interface IMeshCandidate {
809
+ readonly host: string;
810
+ readonly port: number;
811
+ }
812
+ /** A peer device a node signals, with the pair's rendezvous and relay inbox topics. */
813
+ interface IMeshPeerRoute {
814
+ readonly deviceId: string;
815
+ /** Where this device listens for the peer on a relay. */
816
+ readonly inbound: string;
817
+ /** Where this device sends to reach the peer on a relay. */
818
+ readonly outbound: string;
819
+ readonly rendezvous: IPairRendezvous;
820
+ }
821
+ /** One way of finding a peer's direct signaling endpoint. */
822
+ interface IMeshCandidateSource {
823
+ /** How long a lookup of this source may take; default: the caller's. */
824
+ readonly timeoutMs?: number;
825
+ /** Candidates for `peer`, best first; empty when this source knows none. Never throws. */
826
+ candidates(peer: IMeshPeerRoute, signal: AbortSignal): Promise<readonly IMeshCandidate[]>;
827
+ }
828
+ /** The last candidates that worked for each peer device, most recent first. */
829
+ interface IMeshAddressCache {
830
+ recall(deviceId: string): readonly IMeshCandidate[];
831
+ remember(deviceId: string, candidate: IMeshCandidate): void;
832
+ /** Forget every device but these: a device that left the lists has no address worth keeping. */
833
+ retain(deviceIds: readonly string[]): void;
834
+ /** The port this device listened on last, to listen on again so peers' caches stay useful. */
835
+ lastListenPort(): number | undefined;
836
+ rememberListenPort(port: number): void;
837
+ }
838
+ /** Candidates per device the cache keeps. */
839
+ declare const MAX_CACHED_CANDIDATES = 3;
840
+ /** The cache, as the first candidate source. */
841
+ declare function addressCacheSource(cache: IMeshAddressCache): IMeshCandidateSource;
842
+ /** An address cache that lives as long as the process — for tests and callers without a state dir. */
843
+ declare function createInMemoryMeshAddressCache(): IMeshAddressCache;
844
+ //#endregion
845
+ //#region src/mesh-relay.d.ts
846
+ interface IMeshRelay {
847
+ /** Be present at exactly these topics (replaces any earlier declaration). */
848
+ declarePresence(topics: readonly string[]): void;
849
+ /** Deliver `data` to whoever is present at `topic`. */
850
+ send(topic: string, data: unknown): void;
851
+ /** Messages delivered to a topic this relay client is present at. Returns an unsubscribe. */
852
+ onMessage(handler: (topic: string, data: unknown) => void): () => void;
853
+ /** Nobody was present at `topic` when a message was sent there. Returns an unsubscribe. */
854
+ onAbsent(handler: (topic: string) => void): () => void;
855
+ /**
856
+ * The peers the node signals now (replaces any earlier declaration), for a relay that looks for
857
+ * peers itself rather than only delivering to topics.
858
+ */
859
+ declarePeers?(peers: readonly IMeshPeerRoute[]): void;
860
+ /** A connection to `deviceId` was admitted: the way its signals went worked. */
861
+ confirmPeer?(deviceId: string): void;
862
+ close(): void;
863
+ }
864
+ interface IInMemoryMeshRelayHub {
865
+ /** A new relay client attached to this hub. */
866
+ connect(): IMeshRelay;
867
+ }
868
+ /**
869
+ * An in-process relay hub with the relay's delivery rules — for tests and loopback only: one holder
870
+ * per topic (the newest), never an echo to the sender, `absent` when nobody holds the topic.
871
+ * Deliveries run on a microtask, as a network would never deliver synchronously.
872
+ */
873
+ declare function createInMemoryMeshRelayHub(): IInMemoryMeshRelayHub;
874
+ //#endregion
875
+ //#region src/device-mesh-node.d.ts
876
+ /**
877
+ * How a connection reaches a peer no direct path reaches: a relay a paired device runs first, then
878
+ * the TURN servers the user configured. With neither, a connection that needs a relay is refused
879
+ * with {@link MeshRelayNeededError} rather than left to fail silently.
880
+ */
881
+ interface IDeviceMeshRelayOptions {
882
+ /** The relays `peers` advertise, by device id, as found over their pairwise records. */
883
+ readonly advertised?: (peers: readonly IMeshPeerRoute[], signal: AbortSignal) => Promise<ReadonlyMap<string, readonly IMeshRelayEndpoint[]>>;
884
+ /** TURN servers the user configured, tried once the advertised relays did not carry a connection. */
885
+ readonly configured?: readonly IIceServer[];
886
+ /** Relay candidates only: for a network where no direct path can work. */
887
+ readonly relayOnly?: boolean;
888
+ /** How long a relay credential lasts (default: see `meshRelayCredential`). */
889
+ readonly credentialTtlMs?: number;
890
+ }
891
+ interface IDeviceMeshNodeOptions {
892
+ readonly cryptoContext: IIdentityContext;
893
+ readonly identity: IDeviceHandshakeIdentity;
894
+ /** This device's signed descriptor of the session it offers to peers. */
895
+ readonly sessionDescriptor: ISessionDescriptor;
896
+ /** What this device lets a peer be asked for; an admission grants the intersection. */
897
+ readonly localPolicy: readonly TDeviceCapability[];
898
+ readonly relay: IMeshRelay;
899
+ /**
900
+ * The operator of this session, asked before a peer may use a capability that needs approval.
901
+ * Absent: such capabilities are refused.
902
+ */
903
+ readonly operatorApprover?: IOperatorApprover;
904
+ readonly iceServers?: readonly IIceServer[];
905
+ readonly relays?: IDeviceMeshRelayOptions;
906
+ /** This device's own relay: told which devices it serves whenever the lists in force change. */
907
+ readonly relayServer?: {
908
+ declarePeers(peers: readonly IMeshRelayPeer[]): void;
909
+ };
910
+ /** Before a remote admission, ask a signing-key holder for newer lists (see the device handshake). */
911
+ readonly fetchLatestLists?: (signal: AbortSignal) => Promise<unknown>;
912
+ /** Newer verified lists adopted during a handshake, for the caller to persist. */
913
+ readonly onListsAdopted?: (update: IListUpdate) => void;
914
+ /** From the first signal of an attempt to its admission (default 20 s). */
915
+ readonly connectTimeoutMs?: number;
916
+ readonly handshakeTimeoutMs?: number;
917
+ /** Test seam: inject the `node-datachannel` module. */
918
+ readonly loadDataChannel?: () => IDataChannelModule;
919
+ readonly now?: () => number;
920
+ }
921
+ /** An admitted connection to one peer device. */
922
+ interface IDeviceMeshLink {
923
+ readonly admission: IDeviceMeshAdmission;
924
+ readonly result: IDeviceHandshakeResult;
925
+ /** The DTLS fingerprints the handshake bound this connection to. */
926
+ readonly channelBinding: IMeshChannelBinding;
927
+ /** What the peer may do on this connection; ask it before acting on the peer's request. */
928
+ readonly authority: ConnectionAuthority;
929
+ send(body: string): void;
930
+ onMessage(handler: (body: string) => void): () => void;
931
+ /**
932
+ * A channel of its own for one file transfer. What travels on it is the file carrier's; the peer
933
+ * decides on it with its operator, and this side's authority is not asked.
934
+ */
935
+ openFileChannel(): Promise<IFileFrameChannel>;
936
+ /**
937
+ * Channels the peer opened to send a file. Whoever takes them decides on each transfer, asking
938
+ * {@link IDeviceMeshLink.authority}; with no handler, such channels are closed unread.
939
+ */
940
+ onFileChannel(handler: (channel: IFileFrameChannel) => void): () => void;
941
+ onClose(handler: () => void): () => void;
942
+ close(): void;
943
+ }
944
+ /** A connection attempt that ended before admission. */
945
+ interface IDeviceMeshRefusal {
946
+ readonly deviceId: string;
947
+ readonly end: TMeshLinkEnd;
948
+ /**
949
+ * Why: a {@link MeshLinkEndedError} (its `cause` is e.g. the handshake refusal), a
950
+ * {@link MeshRelayNeededError} when the peer needs a relay and none could be tried, or why the
951
+ * device was dropped.
952
+ */
953
+ readonly error?: unknown;
954
+ }
955
+ declare class DeviceMeshNode {
956
+ private readonly options;
957
+ /** This run's instance id. */
958
+ readonly instance: string;
959
+ private identity;
960
+ private sessionDescriptor;
961
+ private readonly peers;
962
+ private readonly byInbound;
963
+ private readonly linkHandlers;
964
+ private readonly refusalHandlers;
965
+ private readonly waiters;
966
+ private readonly unsubscribes;
967
+ private started;
968
+ private stopped;
969
+ private sync;
970
+ constructor(options: IDeviceMeshNodeOptions);
971
+ get deviceId(): string;
972
+ /** Derive the inbox topics, be present at them, and announce this run to every peer device. */
973
+ start(): Promise<void>;
974
+ /**
975
+ * Use newer lists or a new session descriptor from the next handshake on. A list older than the
976
+ * one in force is ignored. Peers the lists no longer name, or revoke, are dropped with their
977
+ * connections; new ones are announced to.
978
+ */
979
+ refresh(update: {
980
+ readonly identity?: IDeviceHandshakeIdentity;
981
+ readonly sessionDescriptor?: ISessionDescriptor;
982
+ }): Promise<void>;
983
+ /** Send the lists in force over every admitted connection. */
984
+ private pushLists;
985
+ /**
986
+ * Lists a peer pushed: taken only when newer than the ones in force, issued by this user's signing
987
+ * key (a signing-key revocation by the master key), and verifying for this device; anything else is
988
+ * ignored and the connection stays.
989
+ */
990
+ private receiveLists;
991
+ /** Recompute the peer set from the current lists; serialized so two refreshes never interleave. */
992
+ private syncPeers;
993
+ private drop;
994
+ /** The offerer for a pair is the device with the lower id. */
995
+ roleFor(deviceId: string): TMeshLinkRole | undefined;
996
+ /** The admitted link to `deviceId`, if there is one. */
997
+ link(deviceId: string): IDeviceMeshLink | undefined;
998
+ /**
999
+ * An admitted link to `deviceId`: the existing one, or a new one once the peer is reachable.
1000
+ * Rejects for a device this node will not talk to, when an attempt is refused, on timeout, or when
1001
+ * the node stops.
1002
+ */
1003
+ connect(deviceId: string, timeoutMs?: number): Promise<IDeviceMeshLink>;
1004
+ onLink(handler: (link: IDeviceMeshLink) => void): () => void;
1005
+ onRefusal(handler: (refusal: IDeviceMeshRefusal) => void): () => void;
1006
+ stop(): void;
1007
+ private hello;
1008
+ private receive;
1009
+ /** The run already being served, or being connected to. */
1010
+ private serves;
1011
+ private onHello;
1012
+ private onOffer;
1013
+ /** A new attempt replaces any other pending one, never the admitted connection. */
1014
+ private startAttempt;
1015
+ /** Lists the handshake verified and found newer: in force here at once, then handed to the caller. */
1016
+ private adoptLists;
1017
+ private admitted;
1018
+ private ended;
1019
+ /**
1020
+ * The relays the next attempt to `state`'s peer uses: the ones paired devices advertise, then the
1021
+ * configured ones once those did not carry a connection, and round again. None: a direct attempt,
1022
+ * or — when only relay candidates may be used — a refusal naming the missing relay.
1023
+ */
1024
+ private relayStage;
1025
+ /** The routes of every peer, `first`'s first: a lookup bounded to some peers covers it. */
1026
+ private peerRoutes;
1027
+ /**
1028
+ * ICE servers for the relays paired devices advertise, with this pair's credential for each: the
1029
+ * peer's own relay first. Only a device the lists in force name, unrevoked, is asked to relay.
1030
+ */
1031
+ private embeddedRelays;
1032
+ /**
1033
+ * A connection with a DTLS certificate of its own (the implementation makes one per connection):
1034
+ * a per-process certificate would be a stable identifier the relay could link across connections.
1035
+ * No ICE server is contacted unless one is configured or a paired device advertises its relay.
1036
+ */
1037
+ private createPeer;
1038
+ }
1039
+ //#endregion
1040
+ //#region src/mesh-signal.d.ts
1041
+ /**
1042
+ * What two device mesh nodes say to each other through the relay, and the frames their data channel
1043
+ * carries. Everything here arrives from an untrusted relay or an unadmitted peer, so every field is
1044
+ * bounded and shape-checked before it is used, and a failure names no value.
1045
+ *
1046
+ * `hello` is presence: a node announces its instance to a peer's inbox. The instance is a random id
1047
+ * per node run, so a restarted peer is told apart from its previous run without naming the device.
1048
+ * An attempt (`cid`) scopes an offer, its answer and their ICE candidates to one connection.
1049
+ */
1050
+ /** Largest message body a data-channel `mesh-msg` frame carries. */
1051
+ declare const MAX_MESH_MESSAGE_CHARS = 60000;
1052
+ //#endregion
1053
+ //#region src/ws-mesh-relay-client.d.ts
1054
+ interface IWsMeshRelayClientOptions {
1055
+ /** Relay URL, e.g. `wss://relay.example`. */
1056
+ readonly url: string;
1057
+ readonly onError?: (error: Error) => void;
1058
+ /** Called each time the relay confirms a presence declaration. */
1059
+ readonly onPresent?: () => void;
1060
+ /** Injectable socket factory (defaults to a real `ws` WebSocket) — for tests. */
1061
+ readonly createSocket?: (url: string) => IWebSocketLike;
1062
+ }
1063
+ declare class WsMeshRelayClient implements IMeshRelay {
1064
+ private readonly options;
1065
+ private readonly socket;
1066
+ private readonly messages;
1067
+ private readonly absents;
1068
+ private readonly outbox;
1069
+ private topics;
1070
+ private closed;
1071
+ constructor(options: IWsMeshRelayClientOptions);
1072
+ private flush;
1073
+ private write;
1074
+ private receive;
1075
+ declarePresence(topics: readonly string[]): void;
1076
+ send(topic: string, data: unknown): void;
1077
+ onMessage(handler: (topic: string, data: unknown) => void): () => void;
1078
+ onAbsent(handler: (topic: string) => void): () => void;
1079
+ close(): void;
1080
+ }
1081
+ //#endregion
1082
+ //#region src/mesh-lan-listener.d.ts
1083
+ interface IMeshLanListener {
1084
+ /** The port it listens on. */
1085
+ readonly port: number;
1086
+ /** Hold exactly these topics (replaces any earlier set). */
1087
+ setPresence(topics: readonly string[]): void;
1088
+ /** A signal delivered to a held topic, named by the topic. Returns an unsubscribe. */
1089
+ onMessage(handler: (topic: string, data: unknown) => void): () => void;
1090
+ close(): Promise<void>;
1091
+ }
1092
+ interface IMeshLanListenerOptions {
1093
+ /** The port to listen on; when it is taken, any free port. Default: any free port. */
1094
+ readonly port?: number;
1095
+ /** The address to bind; default: every interface. */
1096
+ readonly host?: string;
1097
+ }
1098
+ declare function startMeshLanListener(options?: IMeshLanListenerOptions): Promise<IMeshLanListener>;
1099
+ //#endregion
1100
+ //#region src/mesh-mdns.d.ts
1101
+ /** The DNS-SD service type. Generic on purpose: it must not name the product. */
1102
+ declare const MESH_MDNS_SERVICE = "_rdv._tcp.local";
1103
+ /** A DNS resource record, in the shape `multicast-dns` reads and writes. */
1104
+ interface IMdnsRecord {
1105
+ readonly name: string;
1106
+ readonly type: string;
1107
+ readonly ttl?: number;
1108
+ readonly data?: unknown;
1109
+ }
1110
+ interface IMdnsQuestion {
1111
+ readonly name: string;
1112
+ readonly type: string;
1113
+ }
1114
+ interface IMdnsPacket {
1115
+ readonly questions?: readonly IMdnsQuestion[];
1116
+ readonly answers?: readonly IMdnsRecord[];
1117
+ readonly additionals?: readonly IMdnsRecord[];
1118
+ }
1119
+ interface IMdnsRemote {
1120
+ readonly address: string;
1121
+ }
1122
+ /** The slice of a `multicast-dns` instance this module drives. */
1123
+ interface IMdnsTransport {
1124
+ on(event: 'query' | 'response', listener: (packet: IMdnsPacket, rinfo: IMdnsRemote) => void): void;
1125
+ on(event: 'error' | 'warning', listener: (error: Error) => void): void;
1126
+ removeListener(event: 'response', listener: (packet: IMdnsPacket, rinfo: IMdnsRemote) => void): void;
1127
+ query(packet: {
1128
+ readonly questions: readonly IMdnsQuestion[];
1129
+ }): void;
1130
+ respond(packet: {
1131
+ readonly answers: readonly IMdnsRecord[];
1132
+ readonly additionals?: readonly IMdnsRecord[];
1133
+ }): void;
1134
+ destroy(): void;
1135
+ }
1136
+ interface IMeshMdnsOptions {
1137
+ /** Default: a `multicast-dns` socket on the standard group and port. */
1138
+ readonly createTransport?: () => IMdnsTransport;
1139
+ /** This device's addresses for the A/AAAA records. Default: its non-internal interfaces. */
1140
+ readonly addresses?: () => readonly string[];
1141
+ readonly lookupTimeoutMs?: number;
1142
+ /** After the first match, how much longer a lookup listens (within `lookupTimeoutMs`). */
1143
+ readonly answerGraceMs?: number;
1144
+ readonly minAnswerIntervalMs?: number;
1145
+ /** mDNS cannot run (e.g. the port is unavailable): the other candidate sources still do. */
1146
+ readonly onError?: (error: Error) => void;
1147
+ readonly now?: () => number;
1148
+ }
1149
+ /** This device's non-internal interface addresses (no IPv6 link-local). */
1150
+ declare function localInterfaceAddresses(): string[];
1151
+ /** How many instances `count` real ones are announced as. */
1152
+ declare function paddedInstanceCount(count: number): number;
1153
+ declare class MeshMdns implements IMeshCandidateSource {
1154
+ private readonly options;
1155
+ private transport?;
1156
+ private routes;
1157
+ private port?;
1158
+ private records?;
1159
+ private instances;
1160
+ private timer?;
1161
+ private generation;
1162
+ private closed;
1163
+ /** This epoch's padding labels and host name. */
1164
+ private epochCover?;
1165
+ /** What was last announced, to announce again only when it changes. */
1166
+ private announced?;
1167
+ private lastAnswer?;
1168
+ private answerTimer?;
1169
+ constructor(options?: IMeshMdnsOptions);
1170
+ private now;
1171
+ /** The transport, created on first use. `undefined` once it failed or the source is closed. */
1172
+ private open;
1173
+ /** Announce this device to `routes`' peers, as a signaling endpoint on `port`. */
1174
+ advertise(routes: readonly IMeshPeerRoute[], port: number): Promise<void>;
1175
+ private rebuild;
1176
+ private schedule;
1177
+ private onQuery;
1178
+ /** Answer now, or once the interval since the last answer has passed; queries meanwhile share it. */
1179
+ private answer;
1180
+ candidates(peer: IMeshPeerRoute, signal: AbortSignal): Promise<readonly IMeshCandidate[]>;
1181
+ close(): void;
1182
+ }
1183
+ interface IInMemoryMdnsBus {
1184
+ /** A transport on this bus whose answers come from `address`. */
1185
+ transport(address: string): IMdnsTransport;
1186
+ /** Every packet answered on the bus, in order. */
1187
+ readonly responses: readonly IMdnsPacket[];
1188
+ }
1189
+ /**
1190
+ * An in-process multicast group — for tests and loopback only. A query reaches every member,
1191
+ * itself included, as multicast loops back; an answer reaches every member with the answering
1192
+ * member's address. Packets cross as JSON and are delivered on a microtask, as a network would.
1193
+ */
1194
+ declare function createInMemoryMdnsBus(): IInMemoryMdnsBus;
1195
+ //#endregion
1196
+ //#region src/mesh-records.d.ts
1197
+ /** A one-time Ed25519 key: the public half, and a signer. */
1198
+ interface IItemKey {
1199
+ readonly publicKey: Uint8Array;
1200
+ sign(message: Uint8Array): Promise<Uint8Array>;
1201
+ }
1202
+ /** A BEP 44 mutable item, as signed. */
1203
+ interface IMutableItem {
1204
+ readonly k: Uint8Array;
1205
+ readonly salt?: Uint8Array;
1206
+ readonly seq: number;
1207
+ readonly v: Uint8Array;
1208
+ readonly sig: Uint8Array;
1209
+ }
1210
+ /** Where one pair's record of one purpose lives in one direction at one epoch. */
1211
+ interface IItemAddress {
1212
+ readonly key: IItemKey;
1213
+ readonly salt: Uint8Array;
1214
+ }
1215
+ //#endregion
1216
+ //#region src/mesh-item-store.d.ts
1217
+ interface IRendezvousItemStore {
1218
+ /** Publish `value` at `address`, as of `now` (ms); rejects when no holder took it. */
1219
+ put(address: IItemAddress, value: Uint8Array, now: number, signal?: AbortSignal): Promise<void>;
1220
+ /** The verified value at `publicKey` and `salt`; `undefined` when none verifies. Never throws. */
1221
+ get(publicKey: Uint8Array, salt: Uint8Array, signal: AbortSignal): Promise<Uint8Array | undefined>;
1222
+ close(): void;
1223
+ }
1224
+ interface IInMemoryItemNetwork {
1225
+ /** A store client attached to this network. */
1226
+ store(): IRendezvousItemStore;
1227
+ /** Every item the network holds, as stored. */
1228
+ items(): readonly IMutableItem[];
1229
+ /** Replace what the network serves (a hostile node): return the item to serve, or `undefined`. */
1230
+ serve(rewrite: (item: IMutableItem) => IMutableItem | undefined): void;
1231
+ }
1232
+ /**
1233
+ * An in-process item network with a DHT node's rules: it refuses an item whose signature does not
1234
+ * verify, and a sequence number lower than the one it holds.
1235
+ */
1236
+ declare function createInMemoryItemNetwork(): IInMemoryItemNetwork;
1237
+ interface IPkarrRelayStoreOptions {
1238
+ /** Relay base URLs (`https://…`), each run by whoever the user chose; several operators. */
1239
+ readonly relays: readonly string[];
1240
+ /** Default: the global `fetch`. */
1241
+ readonly fetch?: typeof fetch;
1242
+ }
1243
+ /**
1244
+ * A store over several pkarr relays. A put goes to every relay and succeeds when one took it; a get
1245
+ * asks every relay and keeps the newest item that verifies. A relay is only a cache in front of the
1246
+ * DHT: it can withhold or serve stale items, never forge one.
1247
+ */
1248
+ declare function createPkarrRelayStore(options: IPkarrRelayStoreOptions): IRendezvousItemStore;
1249
+ //#endregion
1250
+ //#region src/mesh-dht.d.ts
1251
+ /** Longest delay before a pair's records are published, by default. */
1252
+ declare const DEFAULT_MAX_PUBLISH_JITTER_MS = 90000;
1253
+ /** The device lists this device hands on to its peers. */
1254
+ interface IPublishedLists {
1255
+ readonly revocation: IDeviceRevocationList;
1256
+ readonly signingKeyRevocation?: ISigningKeyRevocation;
1257
+ }
1258
+ /** What a freshness lookup found, as received, newest first: each candidate is verified before use. */
1259
+ interface IFetchedLists {
1260
+ readonly revocation?: readonly unknown[];
1261
+ readonly signingKeyRevocation?: readonly unknown[];
1262
+ }
1263
+ interface IMeshDhtOptions {
1264
+ /** Where records go and come from; every one is written to and read from. */
1265
+ readonly stores: readonly IRendezvousItemStore[];
1266
+ /** This device's addresses to publish with its endpoint port. */
1267
+ readonly addresses: () => readonly string[];
1268
+ /** Where this device's relay listens, published to each paired device with its hints; none: no relay. */
1269
+ readonly relayEndpoints?: () => readonly IMeshCandidate[];
1270
+ /** The newest lists this device holds; absent or `undefined`: no list records are published. */
1271
+ readonly lists?: () => IPublishedLists | undefined;
1272
+ readonly maxPublishJitterMs?: number;
1273
+ /** How long a candidate lookup may take. */
1274
+ readonly lookupTimeoutMs?: number;
1275
+ readonly random?: () => number;
1276
+ readonly now?: () => number;
1277
+ /** A record could not be published; lookups still run. */
1278
+ readonly onError?: (error: Error) => void;
1279
+ }
1280
+ declare class MeshDht implements IMeshCandidateSource {
1281
+ private readonly options;
1282
+ readonly timeoutMs: number;
1283
+ private readonly scheduled;
1284
+ private routes;
1285
+ /** The lists found by the last lookup that finished, for a lookup that runs out of time. */
1286
+ private listsFound;
1287
+ private prefetchedEpoch?;
1288
+ /** The relays peers advertised, by device: the epoch and time they were looked up. */
1289
+ private readonly relaysFound;
1290
+ /** Relay lookups running, by device. */
1291
+ private readonly relayLookups;
1292
+ private closed;
1293
+ constructor(options: IMeshDhtOptions);
1294
+ private now;
1295
+ /** The peer's endpoint addresses from its hints record, at this epoch or an adjacent one. */
1296
+ candidates(peer: IMeshPeerRoute, signal: AbortSignal): Promise<readonly IMeshCandidate[]>;
1297
+ /** The relay endpoints in the newest of `found`, the peer's hints records. */
1298
+ private rememberRelays;
1299
+ /**
1300
+ * The relays `peers` advertise to this device, by device id. What a lookup found holds for the
1301
+ * epoch, and "no relay" for a short while, so connection attempts do not wait on lookups; a
1302
+ * lookup still running when `signal` ends goes on, and the next call has its answer. Only a
1303
+ * peer's own sealed hints record says where its relay is, so no one else learns it and no one
1304
+ * else can plant one.
1305
+ */
1306
+ relayAdverts(peers: readonly IMeshPeerRoute[], signal: AbortSignal): Promise<ReadonlyMap<string, readonly IMeshCandidate[]>>;
1307
+ /** One lookup of a peer's relay at a time, bounded by the lookup timeout, not by any caller. */
1308
+ private lookUpRelay;
1309
+ /** The records of `purpose` the peer published for this device, opened; empty when none. */
1310
+ private lookup;
1311
+ /** One chunk of the peer's record, opened; `undefined` when it is not there or not the peer's. */
1312
+ private read;
1313
+ /** The device lists the peer published for this device, whole; one per store and epoch found. */
1314
+ private lookupLists;
1315
+ /**
1316
+ * Publish this device's records for `routes`, each pair after its own random delay. A pair already
1317
+ * waiting to publish for this epoch and port keeps its delay.
1318
+ */
1319
+ advertise(routes: readonly IMeshPeerRoute[], port: number): Promise<void>;
1320
+ private publish;
1321
+ /**
1322
+ * The lists the peers published for this device, as received — candidates for the freshness
1323
+ * lookup before a remote admission, each verified there. Any paired device can publish anything
1324
+ * here, so candidates are returned rather than the one that claims to be newest, and each peer's
1325
+ * newest few come before any peer's next, so one peer's records cannot crowd out another's. When
1326
+ * `signal` ends first, what the last finished lookup found. `undefined`: none.
1327
+ */
1328
+ latestLists(signal: AbortSignal): Promise<IFetchedLists | undefined>;
1329
+ private findLists;
1330
+ close(): void;
1331
+ }
1332
+ //#endregion
1333
+ //#region src/discovering-mesh-relay.d.ts
1334
+ /** Announces this device to its peers somewhere they look. */
1335
+ interface IMeshAdvertiser {
1336
+ advertise(routes: readonly IMeshPeerRoute[], port: number): Promise<void>;
1337
+ close(): void;
1338
+ }
1339
+ interface IDiscoveringMeshRelayOptions {
1340
+ /**
1341
+ * The self-hosted relay: the last resort. Absent with no `signaling` either: a pair no candidate
1342
+ * reaches cannot be signaled.
1343
+ */
1344
+ readonly relay?: IMeshRelay;
1345
+ /** Public signaling carriers, tried in order before `relay` when no candidate answers. */
1346
+ readonly signaling?: readonly IMeshRelay[];
1347
+ /** This device's direct signaling endpoint. */
1348
+ readonly listener: IMeshLanListener;
1349
+ /** Where to look for a peer's endpoint, in order. */
1350
+ readonly sources: readonly IMeshCandidateSource[];
1351
+ /** Remembers candidates that carried an admitted connection. */
1352
+ readonly cache?: IMeshAddressCache;
1353
+ /** Told the peers and this device's endpoint port, to announce this device; closed with this relay. */
1354
+ readonly advertisers?: readonly IMeshAdvertiser[];
1355
+ /** Test seam: open a socket to a candidate. */
1356
+ readonly connect?: (candidate: IMeshCandidate) => IWebSocketLike;
1357
+ readonly probeTimeoutMs?: number;
1358
+ readonly sourceTimeoutMs?: number;
1359
+ readonly relayRecheckMs?: number;
1360
+ /** How long a direct path may carry signals without an admission before it is set aside. */
1361
+ readonly admissionTimeoutMs?: number;
1362
+ readonly onError?: (error: Error) => void;
1363
+ readonly now?: () => number;
1364
+ }
1365
+ declare class DiscoveringMeshRelay implements IMeshRelay {
1366
+ private readonly options;
1367
+ private readonly routes;
1368
+ private readonly byDevice;
1369
+ /** LAN topic → the relay inbox topic the node knows the pair by. */
1370
+ private lanIn;
1371
+ private readonly messages;
1372
+ private readonly absents;
1373
+ private readonly unsubscribes;
1374
+ private readonly abort;
1375
+ private sync;
1376
+ /** Peer declarations not yet in force; signals sent meanwhile wait for them, in order. */
1377
+ private pendingPeers;
1378
+ private waiting;
1379
+ private timer?;
1380
+ private closed;
1381
+ constructor(options: IDiscoveringMeshRelayOptions);
1382
+ private now;
1383
+ /** Where a pair's signals go when no candidate answers, in order. */
1384
+ private carriers;
1385
+ private deliver;
1386
+ private absent;
1387
+ declarePresence(topics: readonly string[]): void;
1388
+ declarePeers(peers: readonly IMeshPeerRoute[]): void;
1389
+ /** Recompute the pairs' LAN topics for the current epoch and hold them. */
1390
+ private applyPeers;
1391
+ private scheduleEpoch;
1392
+ confirmPeer(deviceId: string): void;
1393
+ /** A cache that cannot be written costs the next connection a lookup, nothing more. */
1394
+ private cacheWrite;
1395
+ send(topic: string, data: unknown): void;
1396
+ private sendDirect;
1397
+ /** A signal for a topic that is no declared pair's: the last carrier, if there is one. */
1398
+ private viaRelay;
1399
+ private viaCarrier;
1400
+ /** Try the sources in order; the first candidate that holds the pair's topic carries its signals. */
1401
+ private resolve;
1402
+ /**
1403
+ * The first carrier not set aside carries the pair's signals for a while (the last one when all
1404
+ * are). Without a carrier there is nothing to wait on: the next signal looks again, since the
1405
+ * peer's endpoint may simply not have been up yet.
1406
+ */
1407
+ private fallBack;
1408
+ private recheckMs;
1409
+ private look;
1410
+ /** A socket to `candidate` once it proves it holds `lanTopic`; `undefined` otherwise. */
1411
+ private probe;
1412
+ /**
1413
+ * An endpoint or carrier that carries an attempt but no admission is set aside, so it cannot keep
1414
+ * the pair from the next way — whether it never forwarded, or forwarded one attempt and swallows
1415
+ * the next. One that fails again is set aside for longer, so it is not tried every recheck.
1416
+ */
1417
+ private armDeadline;
1418
+ private useDirect;
1419
+ private reset;
1420
+ onMessage(handler: (topic: string, data: unknown) => void): () => void;
1421
+ onAbsent(handler: (topic: string) => void): () => void;
1422
+ close(): void;
1423
+ }
1424
+ interface IStartLanMeshRelayOptions {
1425
+ /** The self-hosted relay: the last carrier. */
1426
+ readonly relay?: IMeshRelay;
1427
+ /**
1428
+ * Beyond the local network: public records to find peers by (closed with the relay), and public
1429
+ * signaling carriers tried before the self-hosted relay (the caller's to close).
1430
+ */
1431
+ readonly internet?: {
1432
+ readonly dht?: MeshDht;
1433
+ readonly signaling?: readonly IMeshRelay[];
1434
+ };
1435
+ /** Default: one that lives as long as the process. */
1436
+ readonly cache?: IMeshAddressCache;
1437
+ /** The address the direct signaling endpoint binds; default: every interface. */
1438
+ readonly host?: string;
1439
+ /** mDNS options, or `false` for none (the cache and the relay still work). */
1440
+ readonly mdns?: IMeshMdnsOptions | false;
1441
+ readonly onError?: (error: Error) => void;
1442
+ }
1443
+ /**
1444
+ * A {@link DiscoveringMeshRelay} over this device's direct signaling endpoint, looking for peers in
1445
+ * the address cache, with mDNS, then in public records, and signaling through the public carriers,
1446
+ * then the self-hosted relay, when no candidate answers.
1447
+ */
1448
+ declare function startLanMeshRelay(options?: IStartLanMeshRelayOptions): Promise<DiscoveringMeshRelay>;
1449
+ //#endregion
1450
+ //#region src/mainline-dht-store.d.ts
1451
+ /** A BEP 44 item as `bittorrent-dht` hands it over. */
1452
+ interface IDhtGetResult {
1453
+ readonly v?: Uint8Array;
1454
+ readonly k?: Uint8Array;
1455
+ readonly sig?: Uint8Array;
1456
+ readonly seq?: number;
1457
+ readonly salt?: Uint8Array;
1458
+ }
1459
+ /** The slice of a `bittorrent-dht` instance this store drives. */
1460
+ interface IMainlineDht {
1461
+ put(item: {
1462
+ readonly k: Buffer;
1463
+ readonly salt: Buffer;
1464
+ readonly seq: number;
1465
+ readonly v: Buffer;
1466
+ readonly sig: Buffer;
1467
+ }, callback: (error: Error | null, hash: Buffer, responses?: number) => void): void;
1468
+ get(target: Buffer, options: {
1469
+ readonly salt: Buffer;
1470
+ readonly cache: false;
1471
+ }, callback: (error: Error | null, result: IDhtGetResult | null) => void): void;
1472
+ on(event: 'error' | 'warning', listener: (error: Error) => void): void;
1473
+ destroy(callback?: () => void): void;
1474
+ }
1475
+ interface IMainlineDhtStoreOptions {
1476
+ /** Test seam: the DHT client; default a `bittorrent-dht` node on the public bootstrap nodes. */
1477
+ readonly createDht?: (verify: TDhtVerify) => IMainlineDht | Promise<IMainlineDht>;
1478
+ readonly onError?: (error: Error) => void;
1479
+ }
1480
+ type TDhtVerify = (signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array) => boolean;
1481
+ /**
1482
+ * A Mainline DHT store. The node joins the DHT when the store is created and stays for the life of
1483
+ * the store, since joining takes seconds a lookup should not wait on each time.
1484
+ */
1485
+ declare function startMainlineDhtStore(options?: IMainlineDhtStoreOptions): Promise<IRendezvousItemStore>;
1486
+ //#endregion
1487
+ //#region src/nostr-mesh-relay.d.ts
1488
+ /** A Nostr event as relays carry it. */
1489
+ interface INostrEvent {
1490
+ readonly id: string;
1491
+ readonly pubkey: string;
1492
+ readonly created_at: number;
1493
+ readonly kind: number;
1494
+ readonly tags: readonly (readonly string[])[];
1495
+ readonly content: string;
1496
+ readonly sig: string;
1497
+ }
1498
+ interface INostrFilter {
1499
+ readonly kinds: readonly number[];
1500
+ readonly authors: readonly string[];
1501
+ readonly since?: number;
1502
+ }
1503
+ /** The relays a device signals through. */
1504
+ interface INostrRelayPool {
1505
+ /** Send `event` to every relay; resolves `true` when at least one took it. Never rejects. */
1506
+ publish(event: INostrEvent): Promise<boolean>;
1507
+ /** Events matching `filter` from any relay, as received. */
1508
+ subscribe(filter: INostrFilter, onEvent: (event: unknown) => void): {
1509
+ close(): void;
1510
+ };
1511
+ close(): void;
1512
+ }
1513
+ interface INostrMeshRelayOptions {
1514
+ readonly pool: INostrRelayPool;
1515
+ readonly maxEventAgeS?: number;
1516
+ readonly now?: () => number;
1517
+ readonly onError?: (error: Error) => void;
1518
+ }
1519
+ /**
1520
+ * A mesh relay over Nostr. It needs the peers (see {@link IMeshRelay.declarePeers}) to derive their
1521
+ * keys, and addresses a pair by its outbound topic. It reports a topic absent only when it knows no
1522
+ * pair by it: whether the peer is listening, a relay that forwards ephemeral events cannot tell.
1523
+ */
1524
+ declare class NostrMeshRelay implements IMeshRelay {
1525
+ private readonly options;
1526
+ private readonly messages;
1527
+ private readonly absents;
1528
+ private outbound;
1529
+ private inbound;
1530
+ private subscription?;
1531
+ private chain;
1532
+ private readonly seen;
1533
+ private routes;
1534
+ private epoch?;
1535
+ private timer?;
1536
+ private closed;
1537
+ constructor(options: INostrMeshRelayOptions);
1538
+ private now;
1539
+ private report;
1540
+ /** Run `task` after everything queued before it, so keys are in place before a send uses them. */
1541
+ private enqueue;
1542
+ /** Resolves once every declaration, send and receipt queued so far has run. */
1543
+ whenIdle(): Promise<void>;
1544
+ declarePresence(): void;
1545
+ declarePeers(peers: readonly IMeshPeerRoute[]): void;
1546
+ /** The pairs' keys and kinds for this epoch, and a subscription for the peers' adjacent epochs. */
1547
+ private derive;
1548
+ send(topic: string, data: unknown): void;
1549
+ private receive;
1550
+ private remember;
1551
+ onMessage(handler: (topic: string, data: unknown) => void): () => void;
1552
+ onAbsent(handler: (topic: string) => void): () => void;
1553
+ close(): void;
1554
+ }
1555
+ interface IInMemoryNostrHub {
1556
+ /** A client of every relay of this hub. */
1557
+ pool(): INostrRelayPool;
1558
+ /** Every event the hub forwarded, as received. */
1559
+ readonly events: readonly INostrEvent[];
1560
+ /** Deliver `event` as a relay would, to every matching subscription (a hostile relay's injection). */
1561
+ inject(event: unknown): void;
1562
+ }
1563
+ /**
1564
+ * In-process relays with a relay's rules for ephemeral events: an event whose signature does not
1565
+ * verify is refused, and one that passes is forwarded to matching subscriptions and not stored.
1566
+ * Deliveries run on a microtask, as a network would never deliver synchronously.
1567
+ */
1568
+ declare function createInMemoryNostrHub(): IInMemoryNostrHub;
1569
+ /**
1570
+ * A pool over `relays` (`wss://…`, several operators) with `nostr-tools`, connecting on first use.
1571
+ * The relays see one-time keys, rotating kinds and ciphertext; they are trusted with nothing.
1572
+ */
1573
+ declare function createNostrRelayPool(relays: readonly string[]): INostrRelayPool;
1574
+ //#endregion
1575
+ //#region src/mesh-public-relays.d.ts
1576
+ /**
1577
+ * Default public infrastructure for finding and signaling a peer device beyond the local network.
1578
+ *
1579
+ * These are well-known relays run by different operators, chosen so that no single operator sees
1580
+ * every record or every signal. None of them is trusted: they carry signed ciphertext under
1581
+ * one-time keys, and a peer is admitted by the device handshake alone. They are defaults only —
1582
+ * settings replace them, and a self-hosted relay remains an option.
1583
+ */
1584
+ /** Nostr relays for live signaling, one per operator. */
1585
+ declare const DEFAULT_NOSTR_RELAYS: readonly string[];
1586
+ /**
1587
+ * pkarr relays, for a client that cannot reach the Mainline DHT directly (a browser, or a network
1588
+ * that blocks UDP). Two operators: Synonym (pubky) and n0 (iroh).
1589
+ */
1590
+ declare const DEFAULT_PKARR_RELAYS: readonly string[];
1591
+ //#endregion
1592
+ //#region src/enrollment-link.d.ts
1593
+ /** A data channel bound to one negotiated connection, carrying JSON frames. */
1594
+ interface IEnrollmentChannel {
1595
+ readonly localFingerprint: string;
1596
+ /** Of the certificate the DTLS layer verified. */
1597
+ readonly remoteFingerprint: string;
1598
+ /** Send one frame; a closed channel drops it (the peer learns of the close instead). */
1599
+ send(frame: unknown): void;
1600
+ /** Each inbound frame, JSON-decoded; text that is not JSON arrives as `undefined`. */
1601
+ onFrame(handler: (frame: unknown) => void): () => void;
1602
+ onClose(handler: () => void): () => void;
1603
+ close(): void;
1604
+ }
1605
+ /**
1606
+ * Why no channel came about. `absent`: nobody waits at the topic — the code is wrong, expired or
1607
+ * already used.
1608
+ */
1609
+ type TEnrollmentLinkFailure = 'absent' | 'timeout' | 'signaling' | 'channel-binding' | 'closed';
1610
+ declare class EnrollmentLinkError extends Error {
1611
+ readonly reason: TEnrollmentLinkFailure;
1612
+ constructor(reason: TEnrollmentLinkFailure, detail?: string);
1613
+ }
1614
+ interface IEnrollmentRendezvous {
1615
+ readonly relay: IMeshRelay;
1616
+ /** The topic this side waits at. */
1617
+ readonly inbound: string;
1618
+ /** The topic the other side waits at. */
1619
+ readonly outbound: string;
1620
+ readonly iceServers?: readonly IIceServer[];
1621
+ /** Test seam: inject the `node-datachannel` module. */
1622
+ readonly loadDataChannel?: () => IDataChannelModule;
1623
+ /** From the first signal of an attempt to a bound, open channel (default 20 s). */
1624
+ readonly connectTimeoutMs?: number;
1625
+ }
1626
+ /**
1627
+ * New device: connect to whoever waits at `outbound`. Resolves with the bound channel; rejects when
1628
+ * nobody waits there (`absent`), on timeout, or when the connection fails. The relay stays the
1629
+ * caller's.
1630
+ */
1631
+ declare function dialEnrollment(options: IEnrollmentRendezvous): Promise<IEnrollmentChannel>;
1632
+ interface IEnrollmentListener {
1633
+ /**
1634
+ * Stop taking new attempts. A channel already handed over stays open. The topic is held until the
1635
+ * caller closes the relay or declares other topics.
1636
+ */
1637
+ close(): void;
1638
+ }
1639
+ /**
1640
+ * Existing device: wait at `inbound` for a new device, one attempt at a time. Each bound channel goes
1641
+ * to `onChannel`; the next attempt is taken once that channel closes. The relay stays the caller's.
1642
+ */
1643
+ declare function listenForEnrollment(options: IEnrollmentRendezvous & {
1644
+ readonly onChannel: (channel: IEnrollmentChannel) => void;
1645
+ /** An attempt that ended before its channel was bound. */
1646
+ readonly onAttemptFailed?: (error: EnrollmentLinkError) => void;
1647
+ }): IEnrollmentListener;
1648
+ //#endregion
1649
+ export { DEFAULT_MAX_PUBLISH_JITTER_MS, DEFAULT_NOSTR_RELAYS, DEFAULT_PKARR_RELAYS, DEFAULT_RELAY_CREDENTIAL_TTL_MS, DEFAULT_TURN_QUOTAS, DEFAULT_UNAUTHENTICATED_LIMITS, DeviceMeshNode, DiscoveringMeshRelay, EnrollmentLinkError, type IConnectionApproval, type IConnectionApprovalContext, type IDataChannelModule, type IDeviceMeshLink, type IDeviceMeshNodeOptions, type IDeviceMeshRefusal, type IDeviceMeshRelayOptions, type IDiscoveringMeshRelayOptions, type IEnrollmentChannel, type IEnrollmentListener, type IEnrollmentRendezvous, type IFetchedLists, type IHandoffGrantFrame, type IHandoffGrantProof, type IHostReconnectConfig, type IIceServer, type IInMemoryItemNetwork, type IInMemoryMdnsBus, type IInMemoryMeshRelayHub, type IInMemoryNostrHub, type ILocalPeerProof, type ILocalProofFrame, type IMainlineDht, type IMainlineDhtStoreOptions, type IMdnsPacket, type IMdnsRecord, type IMdnsTransport, type IMeshAddressCache, type IMeshAdvertiser, type IMeshCandidate, type IMeshCandidateSource, type IMeshChannelBinding, type IMeshDhtOptions, type IMeshLanListener, type IMeshLanListenerOptions, type IMeshMdnsOptions, type IMeshPeerRoute, type IMeshRelay, type IMeshRelayEndpoint, type IMeshRelayPeer, type IMeshTurnRelayOptions, type INostrEvent, type INostrFilter, type INostrMeshRelayOptions, type INostrRelayPool, type IPkarrRelayStoreOptions, type IPublishedLists, type IRendezvousItemStore, type IRtcCandidate, type IRtcPeerDiagnostics, type IRtcPeerOptions, type ISignalMessage, type ISignalingClient, type IStartLanMeshRelayOptions, type ITurnAuthorization, type ITurnQuotas, type ITurnServerOptions, type IUnauthenticatedLimits, type IWebRtcTransportOptions, type IWebSocketLike, type IWsMeshRelayClientOptions, type IWsSignalingClientOptions, MAX_CACHED_CANDIDATES, MAX_MESH_MESSAGE_CHARS, MAX_RELAY_CREDENTIAL_TTL_MS, MESH_MDNS_SERVICE, MeshDht, MeshLinkEndedError, MeshMdns, MeshRelayNeededError, MeshTurnRelay, NostrMeshRelay, RtcChannel, RtcPeer, type TEnrollmentLinkFailure, type TMeshLinkEnd, type TMeshLinkRole, type TMeshLinkStage, type TMeshRelayNeed, type TModuleResolver, type TRtcChannelState, type TRtcPeerState, type TSignalKind, TurnServer, WebRtcTransport, WsMeshRelayClient, WsSignalingClient, addressCacheSource, createInMemoryItemNetwork, createInMemoryMdnsBus, createInMemoryMeshAddressCache, createInMemoryMeshRelayHub, createInMemoryNostrHub, createInMemorySignalingPair, createNostrRelayPool, createPkarrRelayStore, dialEnrollment, handoffGrantFrame, judgeHandoffGrant, listenForEnrollment, loadDataChannel, localInterfaceAddresses, localProofFrame, meshRelayCredential, meshRelayIceServers, paddedInstanceCount, startLanMeshRelay, startMainlineDhtStore, startMeshLanListener };
1650
+ //# sourceMappingURL=index.d.ts.map