@cello-protocol/client 0.0.20 → 0.0.22

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 (53) hide show
  1. package/dist/client-send-helpers.d.ts +25 -0
  2. package/dist/client-send-helpers.d.ts.map +1 -0
  3. package/dist/client-send-helpers.js +118 -0
  4. package/dist/client-send-helpers.js.map +1 -0
  5. package/dist/client-startup.d.ts +74 -0
  6. package/dist/client-startup.d.ts.map +1 -0
  7. package/dist/client-startup.js +337 -0
  8. package/dist/client-startup.js.map +1 -0
  9. package/dist/client-wiring.d.ts +120 -0
  10. package/dist/client-wiring.d.ts.map +1 -0
  11. package/dist/client-wiring.js +289 -0
  12. package/dist/client-wiring.js.map +1 -0
  13. package/dist/client.d.ts +29 -169
  14. package/dist/client.d.ts.map +1 -1
  15. package/dist/client.js +222 -5372
  16. package/dist/client.js.map +1 -1
  17. package/dist/connection-inbound-handler.d.ts +47 -0
  18. package/dist/connection-inbound-handler.d.ts.map +1 -0
  19. package/dist/connection-inbound-handler.js +325 -0
  20. package/dist/connection-inbound-handler.js.map +1 -0
  21. package/dist/connection-manager.d.ts +191 -0
  22. package/dist/connection-manager.d.ts.map +1 -0
  23. package/dist/connection-manager.js +692 -0
  24. package/dist/connection-manager.js.map +1 -0
  25. package/dist/frame-dispatch.d.ts +28 -0
  26. package/dist/frame-dispatch.d.ts.map +1 -0
  27. package/dist/frame-dispatch.js +118 -0
  28. package/dist/frame-dispatch.js.map +1 -0
  29. package/dist/registration-manager.d.ts +54 -0
  30. package/dist/registration-manager.d.ts.map +1 -0
  31. package/dist/registration-manager.js +248 -0
  32. package/dist/registration-manager.js.map +1 -0
  33. package/dist/relay-stream-manager.d.ts +136 -0
  34. package/dist/relay-stream-manager.d.ts.map +1 -0
  35. package/dist/relay-stream-manager.js +834 -0
  36. package/dist/relay-stream-manager.js.map +1 -0
  37. package/dist/seal-manager.d.ts +133 -0
  38. package/dist/seal-manager.d.ts.map +1 -0
  39. package/dist/seal-manager.js +803 -0
  40. package/dist/seal-manager.js.map +1 -0
  41. package/dist/session-assignment-parser.d.ts +33 -0
  42. package/dist/session-assignment-parser.d.ts.map +1 -0
  43. package/dist/session-assignment-parser.js +149 -0
  44. package/dist/session-assignment-parser.js.map +1 -0
  45. package/dist/session-manager.d.ts +132 -0
  46. package/dist/session-manager.d.ts.map +1 -0
  47. package/dist/session-manager.js +605 -0
  48. package/dist/session-manager.js.map +1 -0
  49. package/dist/signaling-manager.d.ts +85 -0
  50. package/dist/signaling-manager.d.ts.map +1 -0
  51. package/dist/signaling-manager.js +597 -0
  52. package/dist/signaling-manager.js.map +1 -0
  53. package/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ /**
2
+ * SignalingManager — persistent signaling stream, frame router, per-session directory streams.
3
+ *
4
+ * Extracted from CelloClientImpl. Owns all persistent signaling stream state:
5
+ * #persistentSignalingStream, #persistentSignalingIter, #openingSignalingStream,
6
+ * #pendingSessionRequestResolve, #pendingRegisterResolve, #pendingDkgReadyResolve.
7
+ */
8
+ import type { Stream } from "@libp2p/interface";
9
+ import type { Logger } from "@cello-protocol/interfaces";
10
+ import type { CelloNode } from "@cello-protocol/transport";
11
+ import type { KeyProvider } from "@cello-protocol/crypto";
12
+ import type { SessionAssignment } from "@cello-protocol/protocol-types";
13
+ import type { InitiateSessionResult, ReceiveAssignmentResult, SessionRecord } from "./types.js";
14
+ /**
15
+ * Narrow interface exposing only what SignalingManager needs from CelloClientImpl.
16
+ */
17
+ export interface SignalingContext {
18
+ readonly node: CelloNode;
19
+ readonly keyProvider: KeyProvider;
20
+ readonly logger: Logger;
21
+ getMyPubkeyHex(): string | null;
22
+ setMyPubkeyHex(hex: string): void;
23
+ getDirectoryEndpoint(): {
24
+ peer_id: string;
25
+ multiaddrs: string[];
26
+ } | null;
27
+ getDirectoryStream(sessionIdHex: string): Stream | undefined;
28
+ setDirectoryStream(sessionIdHex: string, stream: Stream): void;
29
+ deleteDirectoryStream(sessionIdHex: string): void;
30
+ getPendingConnectionResolverCount(): number;
31
+ dispatchSignalingFrame(stream: Stream, frame: Record<string, unknown>): void;
32
+ onSignalingStreamClosed(stream: Stream): void;
33
+ getConnectionIdForPeer(pubkeyHex: string): string | undefined;
34
+ hasConnectionPolicy(): boolean;
35
+ receiveSessionAssignment(assignment: SessionAssignment, myPubkey: Uint8Array): Promise<ReceiveAssignmentResult>;
36
+ getSession(sessionIdHex: string): SessionRecord | undefined;
37
+ }
38
+ export declare class SignalingManager {
39
+ #private;
40
+ constructor(ctx: SignalingContext);
41
+ getPersistentSignalingStream(): Stream | null;
42
+ setPersistentSignalingStream(stream: Stream | null): void;
43
+ setPersistentSignalingIter(iter: AsyncIterator<Uint8Array> | null): void;
44
+ hasPendingSessionRequest(): boolean;
45
+ hasPendingRegister(): boolean;
46
+ hasPendingDkgReady(): boolean;
47
+ getPendingSessionRequestResolve(): ((frame: Record<string, unknown>) => void) | null;
48
+ setPendingSessionRequestResolve(resolve: ((frame: Record<string, unknown>) => void) | null): void;
49
+ setPendingRegisterResolve(resolve: ((frame: Record<string, unknown>) => void) | null): void;
50
+ setPendingDkgReadyResolve(resolve: ((frame: Record<string, unknown>) => void) | null): void;
51
+ resolvePendingRegister(frame: Record<string, unknown>): void;
52
+ resolvePendingDkgReady(frame: Record<string, unknown>): void;
53
+ /**
54
+ * Called when the signaling stream closes. Unblocks all pending resolvers.
55
+ */
56
+ onStreamClosed(stream: Stream): void;
57
+ /**
58
+ * Open and authenticate the persistent directory signaling stream.
59
+ * Serializes concurrent calls.
60
+ */
61
+ openPersistentSignalingStream(directoryPeerId?: string, directoryMultiaddr?: string): Promise<boolean>;
62
+ /**
63
+ * Reconnect the persistent signaling stream.
64
+ */
65
+ reconnectDirectory(): Promise<boolean>;
66
+ /**
67
+ * Open per-session directory signaling stream (SESSION-003).
68
+ */
69
+ connectDirectorySignalingStream(sessionIdHex: string, assignment: SessionAssignment, myPubkey: Uint8Array): Promise<void>;
70
+ /**
71
+ * FEDERATION-003 AC-004: Look up a relay's registered public key from the directory.
72
+ */
73
+ getRelayPublicKey(relayId: string): Promise<string | undefined>;
74
+ runPersistentSignalingReader(stream: Stream, iter: AsyncIterator<Uint8Array>): void;
75
+ /**
76
+ * ADAPTER-003: Send session_request on the persistent signaling stream and await
77
+ * session_assignment or error. SI-002: K_local never appears in any frame or log.
78
+ */
79
+ initiateSession(targetPubkeyHex: string, opts?: {
80
+ directoryPeerId?: string;
81
+ directoryMultiaddr?: string;
82
+ timeoutMs?: number;
83
+ }): Promise<InitiateSessionResult>;
84
+ }
85
+ //# sourceMappingURL=signaling-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signaling-manager.d.ts","sourceRoot":"","sources":["../src/signaling-manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,597 @@
1
+ /**
2
+ * SignalingManager — persistent signaling stream, frame router, per-session directory streams.
3
+ *
4
+ * Extracted from CelloClientImpl. Owns all persistent signaling stream state:
5
+ * #persistentSignalingStream, #persistentSignalingIter, #openingSignalingStream,
6
+ * #pendingSessionRequestResolve, #pendingRegisterResolve, #pendingDkgReadyResolve.
7
+ */
8
+ import { createHash } from "node:crypto";
9
+ import { Encoder, decode } from "cbor-x";
10
+ import * as lp from "it-length-prefixed";
11
+ import { parseSessionAssignment, mapSessionRequestErrorFrame } from "./session-assignment-parser.js";
12
+ const SIGNALING_PROTOCOL_ID = "/cello/signaling/1.0.0";
13
+ const AUTH_DOMAIN_DIR = "CELLO-DIR-AUTH-v1";
14
+ const RELAY_AUTH_TIMEOUT_MS = 5_000;
15
+ const CBOR_ENC = new Encoder({ tagUint8Array: false });
16
+ function toU8(v) {
17
+ if (v instanceof Uint8Array)
18
+ return v;
19
+ if (Buffer.isBuffer(v))
20
+ return new Uint8Array(v);
21
+ if (typeof v.slice === "function") {
22
+ return v.slice();
23
+ }
24
+ throw new Error(`expected bytes, got ${typeof v}`);
25
+ }
26
+ async function nextWithTimeout(iter, timeoutMs) {
27
+ return Promise.race([
28
+ iter.next(),
29
+ new Promise((_, reject) => setTimeout(() => reject(new Error("auth_timeout")), timeoutMs)),
30
+ ]);
31
+ }
32
+ export class SignalingManager {
33
+ #ctx;
34
+ // Signaling state owned by this manager
35
+ #persistentSignalingStream = null;
36
+ #persistentSignalingIter = null;
37
+ #openingSignalingStream = null;
38
+ #pendingSessionRequestResolve = null;
39
+ #pendingRegisterResolve = null;
40
+ #pendingDkgReadyResolve = null;
41
+ constructor(ctx) {
42
+ this.#ctx = ctx;
43
+ }
44
+ // ─── Public state accessors (used by facade to wire other managers) ─────────
45
+ getPersistentSignalingStream() {
46
+ return this.#persistentSignalingStream;
47
+ }
48
+ setPersistentSignalingStream(stream) {
49
+ this.#persistentSignalingStream = stream;
50
+ }
51
+ setPersistentSignalingIter(iter) {
52
+ this.#persistentSignalingIter = iter;
53
+ }
54
+ hasPendingSessionRequest() {
55
+ return this.#pendingSessionRequestResolve !== null;
56
+ }
57
+ hasPendingRegister() {
58
+ return this.#pendingRegisterResolve !== null;
59
+ }
60
+ hasPendingDkgReady() {
61
+ return this.#pendingDkgReadyResolve !== null;
62
+ }
63
+ getPendingSessionRequestResolve() {
64
+ return this.#pendingSessionRequestResolve;
65
+ }
66
+ setPendingSessionRequestResolve(resolve) {
67
+ this.#pendingSessionRequestResolve = resolve;
68
+ }
69
+ setPendingRegisterResolve(resolve) {
70
+ this.#pendingRegisterResolve = resolve;
71
+ }
72
+ setPendingDkgReadyResolve(resolve) {
73
+ this.#pendingDkgReadyResolve = resolve;
74
+ }
75
+ resolvePendingRegister(frame) {
76
+ const resolve = this.#pendingRegisterResolve;
77
+ if (resolve) {
78
+ this.#pendingRegisterResolve = null;
79
+ resolve(frame);
80
+ }
81
+ }
82
+ resolvePendingDkgReady(frame) {
83
+ const resolve = this.#pendingDkgReadyResolve;
84
+ if (resolve) {
85
+ this.#pendingDkgReadyResolve = null;
86
+ resolve(frame);
87
+ }
88
+ }
89
+ /**
90
+ * Called when the signaling stream closes. Unblocks all pending resolvers.
91
+ */
92
+ onStreamClosed(stream) {
93
+ if (this.#persistentSignalingStream !== stream)
94
+ return;
95
+ this.#persistentSignalingStream = null;
96
+ this.#persistentSignalingIter = null;
97
+ const dkgReadyResolve = this.#pendingDkgReadyResolve;
98
+ if (dkgReadyResolve) {
99
+ this.#pendingDkgReadyResolve = null;
100
+ dkgReadyResolve({ type: "register_error", reason: "stream_closed" });
101
+ }
102
+ const regResolve = this.#pendingRegisterResolve;
103
+ if (regResolve) {
104
+ this.#pendingRegisterResolve = null;
105
+ regResolve({ type: "register_error", reason: "stream_closed" });
106
+ }
107
+ const sessionResolve = this.#pendingSessionRequestResolve;
108
+ if (sessionResolve) {
109
+ this.#pendingSessionRequestResolve = null;
110
+ sessionResolve({ type: "session_request_error", reason: "directory_unreachable" });
111
+ }
112
+ }
113
+ /**
114
+ * Open and authenticate the persistent directory signaling stream.
115
+ * Serializes concurrent calls.
116
+ */
117
+ openPersistentSignalingStream(directoryPeerId, directoryMultiaddr) {
118
+ if (this.#persistentSignalingStream)
119
+ return Promise.resolve(true);
120
+ const existing = this.#openingSignalingStream;
121
+ if (existing)
122
+ return existing;
123
+ const p = this.#doOpen(directoryPeerId, directoryMultiaddr).finally(() => {
124
+ if (this.#openingSignalingStream === p) {
125
+ this.#openingSignalingStream = null;
126
+ }
127
+ });
128
+ this.#openingSignalingStream = p;
129
+ return p;
130
+ }
131
+ /**
132
+ * Reconnect the persistent signaling stream.
133
+ */
134
+ async reconnectDirectory() {
135
+ const stream = this.#persistentSignalingStream;
136
+ if (stream) {
137
+ try {
138
+ stream.abort(new Error("reconnect"));
139
+ }
140
+ catch { }
141
+ this.#persistentSignalingStream = null;
142
+ this.#persistentSignalingIter = null;
143
+ }
144
+ return this.openPersistentSignalingStream();
145
+ }
146
+ /**
147
+ * Open per-session directory signaling stream (SESSION-003).
148
+ */
149
+ async connectDirectorySignalingStream(sessionIdHex, assignment, myPubkey) {
150
+ const dirPeerId = assignment.directory_endpoint.peer_id;
151
+ const dirMultiaddr = assignment.directory_endpoint.multiaddrs[0];
152
+ if (!dirPeerId)
153
+ return;
154
+ try {
155
+ if (dirMultiaddr) {
156
+ try {
157
+ await this.#ctx.node.dial(dirMultiaddr);
158
+ }
159
+ catch { /* already connected */ }
160
+ }
161
+ let dirStream;
162
+ try {
163
+ dirStream = await this.#ctx.node.newStream(dirPeerId, SIGNALING_PROTOCOL_ID);
164
+ }
165
+ catch {
166
+ return;
167
+ }
168
+ this.#ctx.setDirectoryStream(sessionIdHex, dirStream);
169
+ const iter = lp.decode(dirStream)[Symbol.asyncIterator]();
170
+ const { value: challengeRaw, done } = await iter.next();
171
+ if (done || challengeRaw === undefined) {
172
+ dirStream.abort(new Error("dir_auth_error"));
173
+ return;
174
+ }
175
+ let challengeFrame;
176
+ try {
177
+ challengeFrame = decode(toU8(challengeRaw));
178
+ }
179
+ catch {
180
+ dirStream.abort(new Error("dir_auth_error"));
181
+ return;
182
+ }
183
+ if (challengeFrame["type"] !== "signaling_auth_challenge") {
184
+ dirStream.abort(new Error("dir_auth_error"));
185
+ return;
186
+ }
187
+ const nonce = toU8(challengeFrame["nonce"]);
188
+ const domain = Buffer.from(AUTH_DOMAIN_DIR, "utf8");
189
+ const authMsg = new Uint8Array(Buffer.concat([domain, nonce, myPubkey]));
190
+ const msgHash = new Uint8Array(createHash("sha256").update(authMsg).digest());
191
+ const sig = await this.#ctx.keyProvider.sign(msgHash);
192
+ const authResponseFrame = CBOR_ENC.encode({
193
+ type: "signaling_auth_response",
194
+ pubkey: myPubkey,
195
+ signature: sig,
196
+ });
197
+ dirStream.send(lp.encode.single(authResponseFrame));
198
+ const { value: ackRaw2, done: ackDone2 } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
199
+ if (ackDone2 || ackRaw2 === undefined) {
200
+ dirStream.abort(new Error("dir_auth_error"));
201
+ return;
202
+ }
203
+ let ackFrame2;
204
+ try {
205
+ ackFrame2 = decode(toU8(ackRaw2));
206
+ }
207
+ catch {
208
+ dirStream.abort(new Error("dir_auth_error"));
209
+ return;
210
+ }
211
+ if (ackFrame2["type"] !== "signaling_auth_ok") {
212
+ dirStream.abort(new Error("dir_auth_error"));
213
+ return;
214
+ }
215
+ void this.#runDirectoryStreamReader(sessionIdHex, dirStream, assignment.directory_pubkey, iter);
216
+ }
217
+ catch {
218
+ // Directory connection failure — session still active
219
+ }
220
+ }
221
+ /**
222
+ * FEDERATION-003 AC-004: Look up a relay's registered public key from the directory.
223
+ */
224
+ async getRelayPublicKey(relayId) {
225
+ const directoryEndpoint = this.#ctx.getDirectoryEndpoint();
226
+ if (!directoryEndpoint)
227
+ return undefined;
228
+ const dirPeerId = directoryEndpoint.peer_id;
229
+ const dirMultiaddr = directoryEndpoint.multiaddrs[0];
230
+ try {
231
+ if (dirMultiaddr) {
232
+ try {
233
+ await this.#ctx.node.dial(dirMultiaddr);
234
+ }
235
+ catch { /* already connected */ }
236
+ }
237
+ let sigStream;
238
+ try {
239
+ sigStream = await this.#ctx.node.newStream(dirPeerId, SIGNALING_PROTOCOL_ID);
240
+ }
241
+ catch (err) {
242
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: err instanceof Error ? err.message : "stream_open_failed" });
243
+ return undefined;
244
+ }
245
+ const iter = lp.decode(sigStream)[Symbol.asyncIterator]();
246
+ const { value: challengeRaw, done } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
247
+ if (done || challengeRaw === undefined) {
248
+ sigStream.abort(new Error("dir_auth_error"));
249
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "no_auth_challenge" });
250
+ return undefined;
251
+ }
252
+ let challengeFrame;
253
+ try {
254
+ challengeFrame = decode(toU8(challengeRaw));
255
+ }
256
+ catch {
257
+ sigStream.abort(new Error("dir_auth_error"));
258
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "decode_failed" });
259
+ return undefined;
260
+ }
261
+ if (challengeFrame["type"] !== "signaling_auth_challenge") {
262
+ sigStream.abort(new Error("dir_auth_error"));
263
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "unexpected_frame" });
264
+ return undefined;
265
+ }
266
+ const nonce = toU8(challengeFrame["nonce"]);
267
+ if (!this.#ctx.getMyPubkeyHex()) {
268
+ const pubkey = await this.#ctx.keyProvider.getPublicKey();
269
+ this.#ctx.setMyPubkeyHex(Buffer.from(pubkey).toString("hex"));
270
+ }
271
+ const myPubkey = Buffer.from(this.#ctx.getMyPubkeyHex(), "hex");
272
+ const domain = Buffer.from(AUTH_DOMAIN_DIR, "utf8");
273
+ const authMsg = new Uint8Array(Buffer.concat([domain, nonce, myPubkey]));
274
+ const msgHash = new Uint8Array(createHash("sha256").update(authMsg).digest());
275
+ const sig = await this.#ctx.keyProvider.sign(msgHash);
276
+ sigStream.send(lp.encode.single(CBOR_ENC.encode({
277
+ type: "signaling_auth_response",
278
+ pubkey: myPubkey,
279
+ signature: sig,
280
+ })));
281
+ const { value: ackRaw, done: ackDone } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
282
+ if (ackDone || ackRaw === undefined) {
283
+ sigStream.abort(new Error("dir_auth_error"));
284
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "no_auth_ack" });
285
+ return undefined;
286
+ }
287
+ let ackFrame;
288
+ try {
289
+ ackFrame = decode(toU8(ackRaw));
290
+ }
291
+ catch {
292
+ sigStream.abort(new Error("dir_auth_error"));
293
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "decode_failed" });
294
+ return undefined;
295
+ }
296
+ if (ackFrame["type"] !== "signaling_auth_ok") {
297
+ sigStream.abort(new Error("dir_auth_error"));
298
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "auth_failed" });
299
+ return undefined;
300
+ }
301
+ sigStream.send(lp.encode.single(CBOR_ENC.encode({
302
+ type: "relay_pubkey_request",
303
+ relay_id: relayId,
304
+ })));
305
+ const { value: respRaw, done: respDone } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
306
+ if (respDone || respRaw === undefined) {
307
+ sigStream.abort(new Error("no_response"));
308
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "no_response" });
309
+ return undefined;
310
+ }
311
+ let respFrame;
312
+ try {
313
+ respFrame = decode(toU8(respRaw));
314
+ }
315
+ catch {
316
+ sigStream.abort(new Error("decode_failed"));
317
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: "decode_failed" });
318
+ return undefined;
319
+ }
320
+ sigStream.close().catch(() => { });
321
+ if (respFrame["type"] === "relay_pubkey_response") {
322
+ return respFrame["public_key_hex"];
323
+ }
324
+ const errorReason = respFrame["reason"] ?? "not_found";
325
+ if (errorReason !== "not_found") {
326
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: errorReason });
327
+ }
328
+ return undefined;
329
+ }
330
+ catch (err) {
331
+ this.#ctx.logger.warn("relay.pubkey.lookup.failed", { relayId, reason: err instanceof Error ? err.message : "unknown" });
332
+ return undefined;
333
+ }
334
+ }
335
+ // ─── Persistent signaling reader ──────────────────────────────────────────────
336
+ runPersistentSignalingReader(stream, iter) {
337
+ void this.#doRunPersistentSignalingReader(stream, iter);
338
+ }
339
+ async #doRunPersistentSignalingReader(stream, iter) {
340
+ this.#ctx.logger.debug("signaling.stream.frame.received", { frameType: "reader_started" });
341
+ try {
342
+ while (true) {
343
+ let result;
344
+ try {
345
+ result = await iter.next();
346
+ }
347
+ catch (iterErr) {
348
+ this.#ctx.logger.debug("signaling.stream.iter.error", { error: iterErr instanceof Error ? iterErr.message : String(iterErr) });
349
+ break;
350
+ }
351
+ if (result.done || result.value === undefined) {
352
+ this.#ctx.logger.debug("signaling.stream.closed", {
353
+ pendingSessionRequest: this.hasPendingSessionRequest(),
354
+ pendingRegister: this.hasPendingRegister(),
355
+ pendingDkgReady: this.hasPendingDkgReady(),
356
+ pendingConnectionResolvers: this.#ctx.getPendingConnectionResolverCount(),
357
+ });
358
+ break;
359
+ }
360
+ let frame;
361
+ try {
362
+ frame = decode(toU8(result.value));
363
+ }
364
+ catch (decErr) {
365
+ this.#ctx.logger.debug("signaling.stream.decode.error", { error: decErr instanceof Error ? decErr.message : String(decErr) });
366
+ continue;
367
+ }
368
+ this.#ctx.logger.debug("signaling.stream.frame.received", { frameType: String(frame["type"]) });
369
+ // Dispatch all frames to the facade which routes them to the appropriate manager
370
+ this.#ctx.dispatchSignalingFrame(stream, frame);
371
+ }
372
+ }
373
+ catch (outerErr) {
374
+ this.#ctx.logger.debug("signaling.stream.iter.error", { error: outerErr instanceof Error ? outerErr.message : String(outerErr) });
375
+ }
376
+ // Notify facade that stream is closed — clears stream refs and unblocks pending resolvers
377
+ this.onStreamClosed(stream);
378
+ // Also notify facade for seal reconnect scheduling
379
+ this.#ctx.onSignalingStreamClosed(stream);
380
+ }
381
+ // ─── Per-session directory stream reader ───────────────────────────────────────
382
+ async #runDirectoryStreamReader(sessionIdHex, stream, directoryPubkey, iter) {
383
+ try {
384
+ while (true) {
385
+ let result;
386
+ try {
387
+ result = await iter.next();
388
+ }
389
+ catch {
390
+ break;
391
+ }
392
+ if (result.done || result.value === undefined)
393
+ break;
394
+ let frame;
395
+ try {
396
+ frame = decode(toU8(result.value));
397
+ }
398
+ catch {
399
+ continue;
400
+ }
401
+ // Wrap frame with session context and dispatch
402
+ frame["__session_id_hex"] = sessionIdHex;
403
+ frame["__directory_pubkey"] = directoryPubkey;
404
+ this.#ctx.dispatchSignalingFrame(stream, frame);
405
+ }
406
+ }
407
+ catch { /* stream closed */ }
408
+ if (this.#ctx.getDirectoryStream(sessionIdHex) === stream) {
409
+ this.#ctx.deleteDirectoryStream(sessionIdHex);
410
+ }
411
+ }
412
+ // ─── Private: open implementation ──────────────────────────────────────────────
413
+ async #doOpen(directoryPeerId, directoryMultiaddr) {
414
+ const directoryEndpoint = this.#ctx.getDirectoryEndpoint();
415
+ if (!directoryEndpoint && !directoryPeerId)
416
+ return false;
417
+ if (this.#persistentSignalingStream)
418
+ return true;
419
+ const dirPeerId = directoryPeerId ?? directoryEndpoint.peer_id;
420
+ const dirMultiaddr = directoryMultiaddr ?? directoryEndpoint?.multiaddrs[0];
421
+ try {
422
+ if (dirMultiaddr) {
423
+ try {
424
+ await this.#ctx.node.dial(dirMultiaddr);
425
+ }
426
+ catch { /* non-fatal */ }
427
+ }
428
+ let sigStream;
429
+ try {
430
+ sigStream = await this.#ctx.node.newStream(dirPeerId, SIGNALING_PROTOCOL_ID);
431
+ }
432
+ catch {
433
+ return false;
434
+ }
435
+ const iter = lp.decode(sigStream)[Symbol.asyncIterator]();
436
+ const { value: challengeRaw, done } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
437
+ if (done || challengeRaw === undefined) {
438
+ sigStream.abort(new Error("dir_auth_error"));
439
+ return false;
440
+ }
441
+ let challengeFrame;
442
+ try {
443
+ challengeFrame = decode(toU8(challengeRaw));
444
+ }
445
+ catch {
446
+ sigStream.abort(new Error("dir_auth_error"));
447
+ return false;
448
+ }
449
+ if (challengeFrame["type"] !== "signaling_auth_challenge") {
450
+ sigStream.abort(new Error("dir_auth_error"));
451
+ return false;
452
+ }
453
+ const nonce = toU8(challengeFrame["nonce"]);
454
+ if (nonce.length !== 32) {
455
+ sigStream.abort(new Error("dir_auth_error"));
456
+ return false;
457
+ }
458
+ if (!this.#ctx.getMyPubkeyHex()) {
459
+ const pubkey = await this.#ctx.keyProvider.getPublicKey();
460
+ this.#ctx.setMyPubkeyHex(Buffer.from(pubkey).toString("hex"));
461
+ }
462
+ const myPubkey = Buffer.from(this.#ctx.getMyPubkeyHex(), "hex");
463
+ const domain = Buffer.from(AUTH_DOMAIN_DIR, "utf8");
464
+ const authMsg = new Uint8Array(Buffer.concat([domain, nonce, myPubkey]));
465
+ const msgHash = new Uint8Array(createHash("sha256").update(authMsg).digest());
466
+ const sig = await this.#ctx.keyProvider.sign(msgHash);
467
+ const authResponseFrame = CBOR_ENC.encode({
468
+ type: "signaling_auth_response",
469
+ pubkey: myPubkey,
470
+ signature: sig,
471
+ });
472
+ sigStream.send(lp.encode.single(authResponseFrame));
473
+ const { value: ackRaw, done: ackDone } = await nextWithTimeout(iter, RELAY_AUTH_TIMEOUT_MS);
474
+ if (ackDone || ackRaw === undefined) {
475
+ sigStream.abort(new Error("dir_auth_error"));
476
+ return false;
477
+ }
478
+ let ackFrame;
479
+ try {
480
+ ackFrame = decode(toU8(ackRaw));
481
+ }
482
+ catch {
483
+ sigStream.abort(new Error("dir_auth_error"));
484
+ return false;
485
+ }
486
+ if (ackFrame["type"] !== "signaling_auth_ok") {
487
+ sigStream.abort(new Error("dir_auth_error"));
488
+ return false;
489
+ }
490
+ const peerInfoFrame = CBOR_ENC.encode({
491
+ type: "peer_info_announce",
492
+ peer_id: this.#ctx.node.getPeerId(),
493
+ multiaddrs: this.#ctx.node.listenAddresses(),
494
+ });
495
+ sigStream.send(lp.encode.single(peerInfoFrame));
496
+ this.#persistentSignalingStream = sigStream;
497
+ this.#persistentSignalingIter = iter;
498
+ this.runPersistentSignalingReader(sigStream, iter);
499
+ return true;
500
+ }
501
+ catch {
502
+ return false;
503
+ }
504
+ }
505
+ // ─── ADAPTER-003: initiateSession ────────────────────────────────────────────
506
+ /**
507
+ * ADAPTER-003: Send session_request on the persistent signaling stream and await
508
+ * session_assignment or error. SI-002: K_local never appears in any frame or log.
509
+ */
510
+ async initiateSession(targetPubkeyHex, opts) {
511
+ const timeoutMs = opts?.timeoutMs ?? 30_000;
512
+ // SESSION-006: check local connections map before touching the signaling stream.
513
+ const connectionId = this.#ctx.getConnectionIdForPeer(targetPubkeyHex);
514
+ if (this.#ctx.hasConnectionPolicy() && !connectionId) {
515
+ return { ok: false, reason: "no_connection" };
516
+ }
517
+ if (!this.#ctx.getDirectoryEndpoint() && !opts?.directoryPeerId) {
518
+ return { ok: false, reason: "directory_unreachable" };
519
+ }
520
+ // Ensure myPubkeyHex is set
521
+ if (!this.#ctx.getMyPubkeyHex()) {
522
+ const pubkey = await this.#ctx.keyProvider.getPublicKey();
523
+ this.#ctx.setMyPubkeyHex(Buffer.from(pubkey).toString("hex"));
524
+ }
525
+ const myPubkey = Buffer.from(this.#ctx.getMyPubkeyHex(), "hex");
526
+ // Open persistent signaling stream if not already open (DB-001)
527
+ if (!this.#persistentSignalingStream) {
528
+ const opened = await this.openPersistentSignalingStream(opts?.directoryPeerId, opts?.directoryMultiaddr);
529
+ if (!opened) {
530
+ const retried = await this.openPersistentSignalingStream(opts?.directoryPeerId, opts?.directoryMultiaddr);
531
+ if (!retried) {
532
+ return { ok: false, reason: "directory_unreachable" };
533
+ }
534
+ }
535
+ }
536
+ // SESSION-006: include connection_id in session_request if available
537
+ const sessionRequestPayload = {
538
+ type: "session_request",
539
+ target_pubkey: new Uint8Array(Buffer.from(targetPubkeyHex, "hex")),
540
+ };
541
+ if (connectionId) {
542
+ sessionRequestPayload["connection_id"] = connectionId;
543
+ }
544
+ const sessionRequestFrame = CBOR_ENC.encode(sessionRequestPayload);
545
+ // Set up Promise that resolves when the directory responds
546
+ let responseResolve;
547
+ const responsePromise = new Promise((resolve) => {
548
+ responseResolve = resolve;
549
+ });
550
+ this.#pendingSessionRequestResolve = responseResolve;
551
+ try {
552
+ this.#persistentSignalingStream.send(lp.encode.single(sessionRequestFrame));
553
+ }
554
+ catch {
555
+ this.#pendingSessionRequestResolve = null;
556
+ return { ok: false, reason: "directory_unreachable" };
557
+ }
558
+ // Race: directory response vs timeout
559
+ let responseFrame = null;
560
+ let timedOut = false;
561
+ await Promise.race([
562
+ responsePromise.then((f) => { responseFrame = f; }),
563
+ new Promise((resolve) => setTimeout(() => { timedOut = true; resolve(); }, timeoutMs)),
564
+ ]);
565
+ if (timedOut) {
566
+ this.#pendingSessionRequestResolve = null;
567
+ return { ok: false, reason: "timeout" };
568
+ }
569
+ const frame = responseFrame;
570
+ if (frame["type"] === "session_request_error") {
571
+ return mapSessionRequestErrorFrame(frame);
572
+ }
573
+ if (frame["type"] === "session_assignment") {
574
+ const rawAssignment = frame["assignment"];
575
+ if (!rawAssignment)
576
+ return { ok: false, reason: "directory_unreachable" };
577
+ const assignment = parseSessionAssignment(rawAssignment);
578
+ if (!assignment)
579
+ return { ok: false, reason: "directory_unreachable" };
580
+ const result = await this.#ctx.receiveSessionAssignment(assignment, myPubkey);
581
+ if (!result.ok) {
582
+ return { ok: false, reason: "directory_unreachable" };
583
+ }
584
+ const sessionIdHex = Buffer.from(result.sessionId).toString("hex");
585
+ const record = this.#ctx.getSession(sessionIdHex);
586
+ if (!record)
587
+ return { ok: false, reason: "directory_unreachable" };
588
+ return {
589
+ ok: true,
590
+ sessionId: result.sessionId,
591
+ genesisPrevRoot: record.genesis_prev_root,
592
+ };
593
+ }
594
+ return { ok: false, reason: "directory_unreachable" };
595
+ }
596
+ }
597
+ //# sourceMappingURL=signaling-manager.js.map