@gtc6244/immediacy-daemon 0.2.2 → 0.2.4

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 (57) hide show
  1. package/dist/src/crypto.d.ts +18 -39
  2. package/dist/src/crypto.js +33 -152
  3. package/dist/src/crypto.js.map +1 -1
  4. package/dist/src/devices.d.ts +46 -0
  5. package/dist/src/devices.js +114 -0
  6. package/dist/src/devices.js.map +1 -0
  7. package/dist/src/envelope.d.ts +3 -3
  8. package/dist/src/envelope.js +3 -3
  9. package/dist/src/forward.d.ts +4 -4
  10. package/dist/src/forward.js +1 -1
  11. package/dist/src/forward.js.map +1 -1
  12. package/dist/src/framing.d.ts +76 -0
  13. package/dist/src/framing.js +133 -0
  14. package/dist/src/framing.js.map +1 -0
  15. package/dist/src/hub.d.ts +21 -9
  16. package/dist/src/hub.js +6 -5
  17. package/dist/src/hub.js.map +1 -1
  18. package/dist/src/index.d.ts +3 -3
  19. package/dist/src/index.js +79 -17
  20. package/dist/src/index.js.map +1 -1
  21. package/dist/src/lan.d.ts +103 -0
  22. package/dist/src/lan.js +182 -0
  23. package/dist/src/lan.js.map +1 -0
  24. package/dist/src/pairing.d.ts +69 -0
  25. package/dist/src/pairing.js +125 -0
  26. package/dist/src/pairing.js.map +1 -0
  27. package/dist/src/protocol.d.ts +75 -21
  28. package/dist/src/relay-transport.d.ts +62 -0
  29. package/dist/src/relay-transport.js +180 -0
  30. package/dist/src/relay-transport.js.map +1 -0
  31. package/dist/src/relay-wire.d.ts +25 -0
  32. package/dist/src/relay-wire.js +37 -0
  33. package/dist/src/relay-wire.js.map +1 -0
  34. package/dist/src/rendezvous.d.ts +57 -0
  35. package/dist/src/rendezvous.js +152 -0
  36. package/dist/src/rendezvous.js.map +1 -0
  37. package/dist/src/server.d.ts +19 -7
  38. package/dist/src/server.js +181 -61
  39. package/dist/src/server.js.map +1 -1
  40. package/dist/src/session-socket.d.ts +34 -0
  41. package/dist/src/session-socket.js +84 -0
  42. package/dist/src/session-socket.js.map +1 -0
  43. package/dist/src/session.d.ts +151 -0
  44. package/dist/src/session.js +241 -0
  45. package/dist/src/session.js.map +1 -0
  46. package/dist/src/tcp-transport.d.ts +37 -0
  47. package/dist/src/tcp-transport.js +106 -0
  48. package/dist/src/tcp-transport.js.map +1 -0
  49. package/dist/src/telemetry.d.ts +69 -0
  50. package/dist/src/telemetry.js +64 -0
  51. package/dist/src/telemetry.js.map +1 -0
  52. package/dist/src/transport.d.ts +75 -0
  53. package/dist/src/transport.js +136 -0
  54. package/dist/src/transport.js.map +1 -0
  55. package/dist/src/workspace.d.ts +4 -4
  56. package/dist/src/workspace.js.map +1 -1
  57. package/package.json +16 -4
@@ -0,0 +1,182 @@
1
+ /**
2
+ * LAN direct transport (docs/WEBRTC_MIGRATION.md Phase 3).
3
+ *
4
+ * Priority-2 path: when a client and daemon share a network segment they talk
5
+ * directly over TCP — no Cloudflare, no relay, works with the router's WAN link
6
+ * unplugged. The daemon:
7
+ * - advertises `_immediacy._tcp` over mDNS (TXT `rid=<rendezvous id>`) so a
8
+ * client can discover it without any typed address;
9
+ * - listens on TCP and, per connection, runs the §5.3 session handshake over
10
+ * the frame `Mux` (tcp-transport.ts + transport.ts), then hands the caller a
11
+ * `SecureChannel` for that peer.
12
+ *
13
+ * The handshake and frame codec are the SAME modules the loopback and (future)
14
+ * relay/WebRTC paths use — only the byte transport differs.
15
+ */
16
+ import { createServer, connect as tcpConnect } from "node:net";
17
+ import { Bonjour } from "bonjour-service";
18
+ import { Mux } from "./transport.js";
19
+ import { TcpTransport } from "./tcp-transport.js";
20
+ import { Channel } from "./framing.js";
21
+ import { acceptHandshake, startHandshake, } from "./session.js";
22
+ /** bonjour "type" (it prepends `_` and appends `._tcp`) ⇒ `_immediacy._tcp`. */
23
+ export const LAN_SERVICE_TYPE = "immediacy";
24
+ /**
25
+ * Run the responder (daemon) side of the handshake over `mux`'s control channel:
26
+ * receive msg1, verify the device against `isPaired`, reply msg2, and resolve
27
+ * with the established `SecureChannel`. Rejects on an unknown/revoked device or a
28
+ * malformed/failed handshake.
29
+ */
30
+ export function respondHandshakeOverMux(mux, opts, timeoutMs = 10_000) {
31
+ return new Promise((resolve, reject) => {
32
+ const timer = setTimeout(() => reject(new Error("handshake timeout")), timeoutMs);
33
+ let done = false;
34
+ mux.onMessage((channelId, message) => {
35
+ if (done || channelId !== Channel.control)
36
+ return;
37
+ done = true;
38
+ clearTimeout(timer);
39
+ try {
40
+ const msg1 = JSON.parse(message.toString("utf8"));
41
+ const { msg2, channel, devicePub, paired, label } = acceptHandshake(msg1, opts);
42
+ void mux.send(Channel.control, Buffer.from(JSON.stringify(msg2), "utf8"));
43
+ resolve({ channel, devicePub, paired, label });
44
+ }
45
+ catch (err) {
46
+ reject(err instanceof Error ? err : new Error(String(err)));
47
+ }
48
+ });
49
+ });
50
+ }
51
+ /**
52
+ * Run the initiator (client) side over `mux`'s control channel: send msg1, wait
53
+ * for msg2, and resolve with the `SecureChannel`. Used by the LAN test and
54
+ * mirrored by the Flutter/macOS LAN clients.
55
+ */
56
+ export function initiateHandshakeOverMux(mux, opts, timeoutMs = 10_000) {
57
+ return new Promise((resolve, reject) => {
58
+ const { msg1, finish } = startHandshake(opts);
59
+ const timer = setTimeout(() => reject(new Error("handshake timeout")), timeoutMs);
60
+ mux.onMessage((channelId, message) => {
61
+ if (channelId !== Channel.control)
62
+ return;
63
+ clearTimeout(timer);
64
+ try {
65
+ const msg2 = JSON.parse(message.toString("utf8"));
66
+ resolve(finish(msg2));
67
+ }
68
+ catch (err) {
69
+ reject(err instanceof Error ? err : new Error(String(err)));
70
+ }
71
+ });
72
+ void mux.send(Channel.control, Buffer.from(JSON.stringify(msg1), "utf8"));
73
+ });
74
+ }
75
+ export class LanServer {
76
+ rendezvous;
77
+ server;
78
+ bonjour;
79
+ service;
80
+ constructor(rendezvous) {
81
+ this.rendezvous = rendezvous;
82
+ }
83
+ /** Called for each peer that completes the handshake over LAN. */
84
+ onSession;
85
+ /**
86
+ * Start the TCP listener and advertise it over mDNS. Returns the bound port.
87
+ * `advertise: false` skips mDNS (useful in tests / restricted networks).
88
+ */
89
+ async start(port = 0, opts) {
90
+ const server = createServer((socket) => this.onConnection(socket));
91
+ this.server = server;
92
+ const boundPort = await new Promise((resolve, reject) => {
93
+ server.once("error", reject);
94
+ server.listen(port, opts?.host, () => {
95
+ const addr = server.address();
96
+ resolve(typeof addr === "object" && addr ? addr.port : port);
97
+ });
98
+ });
99
+ if (opts?.advertise !== false)
100
+ this.advertise(boundPort);
101
+ return boundPort;
102
+ }
103
+ advertise(port) {
104
+ try {
105
+ this.bonjour = new Bonjour();
106
+ this.service = this.bonjour.publish({
107
+ name: `immediacy-${this.rendezvous.identity.rendezvousId().slice(0, 8)}`,
108
+ type: LAN_SERVICE_TYPE,
109
+ port,
110
+ txt: { rid: this.rendezvous.identity.rendezvousId() },
111
+ });
112
+ }
113
+ catch {
114
+ // mDNS can fail on locked-down networks; LAN still works if the client
115
+ // reaches the port another way. Best-effort.
116
+ }
117
+ }
118
+ onConnection(socket) {
119
+ const transport = new TcpTransport(socket);
120
+ const mux = new Mux(transport);
121
+ const pairing = this.rendezvous.pairing;
122
+ respondHandshakeOverMux(mux, {
123
+ daemonSignSecret: this.rendezvous.identity.signSecretKey(),
124
+ rendezvousId: this.rendezvous.identity.rendezvousId(),
125
+ isPaired: (dev) => this.rendezvous.devices.has(dev),
126
+ redeemCode: pairing ? (_dev, code) => pairing.consume(code) : undefined,
127
+ })
128
+ .then(async ({ channel, devicePub, paired, label }) => {
129
+ // A first-pairing handshake authorized via a valid code — record the
130
+ // device now (the code is already consumed) before serving the session.
131
+ if (paired)
132
+ await this.rendezvous.devices.add(devicePub, label ?? "device");
133
+ void this.rendezvous.devices.touch(devicePub).catch(() => { });
134
+ this.onSession?.({
135
+ mux,
136
+ channel,
137
+ devicePub,
138
+ close: () => transport.close(),
139
+ onClose: (cb) => transport.onClose(cb),
140
+ });
141
+ })
142
+ .catch(() => transport.close());
143
+ }
144
+ async stop() {
145
+ this.service?.stop?.();
146
+ this.bonjour?.destroy();
147
+ await new Promise((resolve) => {
148
+ if (!this.server)
149
+ return resolve();
150
+ this.server.close(() => resolve());
151
+ });
152
+ }
153
+ }
154
+ /**
155
+ * Browse the LAN for advertised daemons. Calls `onPeer` for each service that
156
+ * carries a rendezvous-id TXT record. Returns a stop function.
157
+ */
158
+ export function browseLan(onPeer) {
159
+ const bonjour = new Bonjour();
160
+ const browser = bonjour.find({ type: LAN_SERVICE_TYPE }, (service) => {
161
+ const rid = service.txt?.rid;
162
+ const host = service.referer?.address ?? service.host;
163
+ if (rid && host) {
164
+ onPeer({ rendezvousId: rid, host, port: service.port, name: service.name });
165
+ }
166
+ });
167
+ return () => {
168
+ browser.stop();
169
+ bonjour.destroy();
170
+ };
171
+ }
172
+ /** Open a LAN TCP connection to `host:port` and wrap it in a `Mux`. */
173
+ export function lanConnect(host, port) {
174
+ return new Promise((resolve, reject) => {
175
+ const socket = tcpConnect({ host, port }, () => {
176
+ const transport = new TcpTransport(socket);
177
+ resolve({ mux: new Mux(transport), close: () => transport.close() });
178
+ });
179
+ socket.once("error", reject);
180
+ });
181
+ }
182
+ //# sourceMappingURL=lan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lan.js","sourceRoot":"","sources":["../../src/lan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pairing — single-use pairing codes + the QR payload a client scans (§3.4).
3
+ *
4
+ * A pairing code is a 128-bit secret, **in-memory only**, with a 60 s TTL and
5
+ * single use: it is discarded the moment it is consumed or expires. It gates a
6
+ * *new* device joining the daemon's device table; once paired, a device
7
+ * reconnects with its own key and never needs a code again.
8
+ *
9
+ * The QR payload is small JSON: `{ v, r, k, c, s }` — protocol version,
10
+ * rendezvous id, daemon Ed25519 pubkey (base64), the pairing code (base64), and
11
+ * the signal-server URL. The client scans it, dials the rendezvous, presents the
12
+ * code + a fresh device key, and proceeds straight into the session handshake.
13
+ */
14
+ /** Default signal server (§8: ownership/deploy pipeline still to be confirmed). */
15
+ export declare const DEFAULT_SIGNAL_URL = "wss://signal.ixiomsoftware.com";
16
+ /** Pairing-code lifetime. Short enough that a leaked QR photo goes stale fast. */
17
+ export declare const PAIRING_CODE_TTL_MS = 60000;
18
+ /** The QR payload a daemon renders and a client scans. Keep it tiny. */
19
+ export interface PairingPayload {
20
+ v: 1;
21
+ r: string;
22
+ k: string;
23
+ c: string;
24
+ s: string;
25
+ }
26
+ /**
27
+ * Issues and validates single-use pairing codes. All state is in memory, so a
28
+ * daemon restart invalidates every outstanding code — intentional.
29
+ */
30
+ export declare class PairingCodes {
31
+ private pending;
32
+ private readonly ttlMs;
33
+ /** Injectable clock so tests don't sleep. */
34
+ private readonly now;
35
+ constructor(opts?: {
36
+ ttlMs?: number;
37
+ now?: () => number;
38
+ });
39
+ /** Mint a fresh 128-bit code, remember it until its TTL, and return it. */
40
+ issue(): {
41
+ code: string;
42
+ expiresAt: number;
43
+ };
44
+ /**
45
+ * Consume a code. Returns true exactly once for a live code, then discards it
46
+ * (single use). Returns false for an unknown, already-used, or expired code.
47
+ * Comparison is constant-time to avoid leaking which prefix matched.
48
+ */
49
+ consume(code: string): boolean;
50
+ /** Number of live (unexpired) codes — for tests/telemetry. */
51
+ size(): number;
52
+ /** Drop expired codes so they never accumulate. */
53
+ private sweep;
54
+ }
55
+ /** Build the QR payload object a client scans. */
56
+ export declare function buildPairingPayload(opts: {
57
+ rendezvousId: string;
58
+ daemonPublicKeyB64: string;
59
+ code: string;
60
+ signalUrl?: string;
61
+ }): PairingPayload;
62
+ /** Serialize a pairing payload to the compact JSON string that goes in the QR. */
63
+ export declare function encodePairingPayload(payload: PairingPayload): string;
64
+ /**
65
+ * Render a pairing payload as a terminal QR code, with a plaintext fallback so
66
+ * the user can still pair on a terminal that can't draw the QR (or via copy /
67
+ * paste). Resolves to the string that was printed's source payload.
68
+ */
69
+ export declare function renderPairingQr(payload: PairingPayload, out?: (line: string) => void): Promise<string>;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Pairing — single-use pairing codes + the QR payload a client scans (§3.4).
3
+ *
4
+ * A pairing code is a 128-bit secret, **in-memory only**, with a 60 s TTL and
5
+ * single use: it is discarded the moment it is consumed or expires. It gates a
6
+ * *new* device joining the daemon's device table; once paired, a device
7
+ * reconnects with its own key and never needs a code again.
8
+ *
9
+ * The QR payload is small JSON: `{ v, r, k, c, s }` — protocol version,
10
+ * rendezvous id, daemon Ed25519 pubkey (base64), the pairing code (base64), and
11
+ * the signal-server URL. The client scans it, dials the rendezvous, presents the
12
+ * code + a fresh device key, and proceeds straight into the session handshake.
13
+ */
14
+ import sodium from "sodium-native";
15
+ import qrcode from "qrcode-terminal";
16
+ /** Default signal server (§8: ownership/deploy pipeline still to be confirmed). */
17
+ export const DEFAULT_SIGNAL_URL = "wss://signal.ixiomsoftware.com";
18
+ /** Pairing-code lifetime. Short enough that a leaked QR photo goes stale fast. */
19
+ export const PAIRING_CODE_TTL_MS = 60_000;
20
+ /** Pairing-code entropy: 128 bits, matching the rendezvous id. */
21
+ const PAIRING_CODE_BYTES = 16;
22
+ /**
23
+ * Issues and validates single-use pairing codes. All state is in memory, so a
24
+ * daemon restart invalidates every outstanding code — intentional.
25
+ */
26
+ export class PairingCodes {
27
+ pending = new Map();
28
+ ttlMs;
29
+ /** Injectable clock so tests don't sleep. */
30
+ now;
31
+ constructor(opts) {
32
+ this.ttlMs = opts?.ttlMs ?? PAIRING_CODE_TTL_MS;
33
+ this.now = opts?.now ?? Date.now;
34
+ }
35
+ /** Mint a fresh 128-bit code, remember it until its TTL, and return it. */
36
+ issue() {
37
+ this.sweep();
38
+ const raw = Buffer.alloc(PAIRING_CODE_BYTES);
39
+ sodium.randombytes_buf(raw);
40
+ const code = raw.toString("base64");
41
+ const expiresAt = this.now() + this.ttlMs;
42
+ this.pending.set(code, { code, expiresAt });
43
+ return { code, expiresAt };
44
+ }
45
+ /**
46
+ * Consume a code. Returns true exactly once for a live code, then discards it
47
+ * (single use). Returns false for an unknown, already-used, or expired code.
48
+ * Comparison is constant-time to avoid leaking which prefix matched.
49
+ */
50
+ consume(code) {
51
+ this.sweep();
52
+ if (typeof code !== "string")
53
+ return false;
54
+ let candidate;
55
+ try {
56
+ candidate = Buffer.from(code, "base64");
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ if (candidate.length !== PAIRING_CODE_BYTES)
62
+ return false;
63
+ for (const [key, entry] of this.pending) {
64
+ const known = Buffer.from(entry.code, "base64");
65
+ if (known.length === candidate.length && sodium.sodium_memcmp(known, candidate)) {
66
+ this.pending.delete(key); // single use — gone even on the winning path
67
+ return entry.expiresAt > this.now();
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+ /** Number of live (unexpired) codes — for tests/telemetry. */
73
+ size() {
74
+ this.sweep();
75
+ return this.pending.size;
76
+ }
77
+ /** Drop expired codes so they never accumulate. */
78
+ sweep() {
79
+ const t = this.now();
80
+ for (const [key, entry] of this.pending) {
81
+ if (entry.expiresAt <= t)
82
+ this.pending.delete(key);
83
+ }
84
+ }
85
+ }
86
+ /** Build the QR payload object a client scans. */
87
+ export function buildPairingPayload(opts) {
88
+ return {
89
+ v: 1,
90
+ r: opts.rendezvousId,
91
+ k: opts.daemonPublicKeyB64,
92
+ c: opts.code,
93
+ s: opts.signalUrl ?? DEFAULT_SIGNAL_URL,
94
+ };
95
+ }
96
+ /** Serialize a pairing payload to the compact JSON string that goes in the QR. */
97
+ export function encodePairingPayload(payload) {
98
+ return JSON.stringify(payload);
99
+ }
100
+ /**
101
+ * Render a pairing payload as a terminal QR code, with a plaintext fallback so
102
+ * the user can still pair on a terminal that can't draw the QR (or via copy /
103
+ * paste). Resolves to the string that was printed's source payload.
104
+ */
105
+ export async function renderPairingQr(payload, out = (l) => console.log(l)) {
106
+ const encoded = encodePairingPayload(payload);
107
+ try {
108
+ const art = await new Promise((resolve, reject) => {
109
+ try {
110
+ qrcode.generate(encoded, { small: true }, (o) => resolve(o));
111
+ }
112
+ catch (err) {
113
+ reject(err);
114
+ }
115
+ });
116
+ out(art);
117
+ }
118
+ catch {
119
+ // Fallback: some terminals/CI can't render — print the payload to scan/paste.
120
+ out("[pairing] QR unavailable — paste this into the app to pair:");
121
+ }
122
+ out(encoded);
123
+ return encoded;
124
+ }
125
+ //# sourceMappingURL=pairing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing.js","sourceRoot":"","sources":["../../src/pairing.ts"],"names":[],"mappings":"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"}
@@ -25,9 +25,9 @@ export interface Envelope<T extends MessageType = MessageType> {
25
25
  payload: PayloadFor<T>;
26
26
  }
27
27
  /** Client → daemon. */
28
- export type ClientMessageType = "project.list" | "project.create" | "project.delete" | "project.rename" | "project.reorder" | "project.branches" | "project.pulls" | "project.issues" | "issue.create" | "workspace.list" | "workspace.create" | "workspace.delete" | "workspace.archive" | "workspace.rename" | "workspace.action" | "workspace.setModel" | "delete.batch" | "prompt.send" | "agent.stop" | "permission.respond" | "question.respond" | "allowlist.list" | "allowlist.revoke" | "allowlist.clear" | "terminal.input" | "terminal.resize" | "terminal.snapshot" | "forward.list" | "forward.open" | "forward.data" | "forward.close" | "git.status" | "git.commit" | "git.push" | "git.resolve" | "git.merge" | "pr.create" | "pr.merge" | "workspace.open" | "diff.get" | "tree.get" | "file.get" | "file.set" | "media.get" | "chat.history" | "github.status" | "github.repos" | "models.list" | "job.list" | "job.create" | "job.update" | "job.delete" | "job.run" | "job.runs" | "settings.get" | "settings.set" | "config.set" | "log.tail" | "client.hello" | "hello" | "pair.info" | "daemon.get" | "daemon.rename" | "daemon.update.check" | "daemon.update" | "push.register" | "push.unregister" | "ping" | "echo";
28
+ export type ClientMessageType = "project.list" | "project.create" | "project.delete" | "project.rename" | "project.reorder" | "project.branches" | "project.pulls" | "project.issues" | "issue.create" | "workspace.list" | "workspace.create" | "workspace.delete" | "workspace.archive" | "workspace.rename" | "workspace.action" | "workspace.setModel" | "delete.batch" | "prompt.send" | "agent.stop" | "permission.respond" | "question.respond" | "allowlist.list" | "allowlist.revoke" | "allowlist.clear" | "terminal.input" | "terminal.resize" | "terminal.snapshot" | "forward.list" | "forward.open" | "forward.data" | "forward.close" | "git.status" | "git.commit" | "git.push" | "git.resolve" | "git.merge" | "pr.create" | "pr.merge" | "workspace.open" | "diff.get" | "tree.get" | "file.get" | "file.set" | "media.get" | "chat.history" | "github.status" | "github.repos" | "models.list" | "job.list" | "job.create" | "job.update" | "job.delete" | "job.run" | "job.runs" | "settings.get" | "settings.set" | "config.set" | "log.tail" | "client.hello" | "pair.begin" | "session.begin" | "pairing.issue" | "daemon.get" | "daemon.rename" | "daemon.update.check" | "daemon.update" | "push.register" | "push.unregister" | "ping" | "echo";
29
29
  /** Daemon → client. */
30
- export type DaemonMessageType = "project.list.result" | "project.branches.result" | "project.pulls.result" | "project.issues.result" | "workspace.list.result" | "workspace.created" | "workspace.renamed" | "delete.batch.result" | "event.assistant_message" | "event.assistant_image" | "event.tool_use" | "event.permission_request" | "event.permission_resolved" | "event.ask_question" | "event.question_resolved" | "event.session" | "event.agent_done" | "event.session_limit" | "event.system_notice" | "event.auth_failure" | "terminal.data" | "terminal.snapshot.result" | "forward.available" | "git.status.result" | "git.result" | "diff.result" | "tree.result" | "file.result" | "file.saved" | "media.result" | "chat.history.result" | "github.status.result" | "github.repos.result" | "models.list.result" | "job.list.result" | "job.runs.result" | "event.job_run" | "settings.result" | "allowlist.result" | "log.tail.result" | "log.event" | "hello.ok" | "pair.info.result" | "daemon.info" | "daemon.update.result" | "pong" | "error" | "echo.result";
30
+ export type DaemonMessageType = "project.list.result" | "project.branches.result" | "project.pulls.result" | "project.issues.result" | "workspace.list.result" | "workspace.created" | "workspace.renamed" | "delete.batch.result" | "event.assistant_message" | "event.assistant_image" | "event.tool_use" | "event.permission_request" | "event.permission_resolved" | "event.ask_question" | "event.question_resolved" | "event.session" | "event.agent_done" | "event.session_limit" | "event.system_notice" | "event.auth_failure" | "terminal.data" | "terminal.snapshot.result" | "forward.available" | "git.status.result" | "git.result" | "diff.result" | "tree.result" | "file.result" | "file.saved" | "media.result" | "chat.history.result" | "github.status.result" | "github.repos.result" | "models.list.result" | "job.list.result" | "job.runs.result" | "event.job_run" | "settings.result" | "allowlist.result" | "log.tail.result" | "log.event" | "pair.begin.result" | "session.begin.result" | "pairing.issue.result" | "daemon.info" | "daemon.update.result" | "pong" | "error" | "echo.result";
31
31
  export type MessageType = ClientMessageType | DaemonMessageType;
32
32
  export interface ProjectListPayload {
33
33
  }
@@ -1154,26 +1154,78 @@ export interface GithubReposResultPayload {
1154
1154
  }
1155
1155
  export interface PongPayload {
1156
1156
  }
1157
- /** Client → daemon: announce this device's public key to establish E2E. */
1158
- export interface HelloPayload {
1159
- /** base64 of the device's X25519 public key. */
1160
- pub: string;
1157
+ export type { HandshakeMsg1, HandshakeMsg2 } from "./session.js";
1158
+ import type { HandshakeMsg1, HandshakeMsg2 } from "./session.js";
1159
+ /**
1160
+ * Client → daemon: redeem a single-use pairing code (from the scanned QR) and
1161
+ * register this device's long-term Ed25519 public key. Sent cleartext (the code
1162
+ * is the pairing secret); the daemon adds the device to its table and replies
1163
+ * with its pinned identity so the client can persist a `KnownDaemon`.
1164
+ */
1165
+ export interface PairBeginPayload {
1166
+ /** The single-use pairing code from the QR (`c` field), base64. */
1167
+ code: string;
1168
+ /** This device's long-term Ed25519 public key, base64. */
1169
+ devicePub: string;
1161
1170
  /** Optional human label for the paired-devices record. */
1162
1171
  label?: string;
1163
1172
  }
1164
- /** Daemon → client: handshake ack carrying the daemon's public key. */
1165
- export interface HelloOkPayload {
1166
- /** base64 of the daemon's public key (empty when E2E is off). */
1167
- pub: string;
1168
- /** Whether payloads on this socket are now sealed. */
1169
- sealed: boolean;
1173
+ /** Daemon → client: the outcome of `pair.begin`. */
1174
+ export interface PairBeginResultPayload {
1175
+ /** True iff the code redeemed and the device was registered. */
1176
+ ok: boolean;
1177
+ /** The daemon's rendezvous id (base32) — present on success. */
1178
+ rendezvousId?: string;
1179
+ /** The daemon's long-term Ed25519 public key, base64 — present on success. */
1180
+ daemonPub?: string;
1181
+ /** The daemon's human name at pair time (for the client's `KnownDaemon`). */
1182
+ label?: string;
1183
+ /** Machine-readable failure reason (e.g. "bad_code", "bad_key") on !ok. */
1184
+ error?: string;
1170
1185
  }
1171
- /** Client → daemon: ask for the daemon's public key (to build a pairing QR). */
1172
- export interface PairInfoPayload {
1186
+ /**
1187
+ * Client → daemon: mint a fresh pairing payload to show as a QR (`pairing.issue`).
1188
+ * No arguments — the daemon already knows its own rendezvous id, public key, and
1189
+ * signal URL. Must arrive on an authenticated (sealed) session; the reply carries
1190
+ * a live single-use code.
1191
+ */
1192
+ export interface PairingIssuePayload {
1173
1193
  }
1174
- /** Daemon → client: the daemon's public key, or empty when E2E is off. */
1175
- export interface PairInfoResultPayload {
1176
- pub: string;
1194
+ /** Daemon → client: the outcome of `pairing.issue`. */
1195
+ export interface PairingIssueResultPayload {
1196
+ /** True iff a fresh payload was minted (requester was authenticated). */
1197
+ ok: boolean;
1198
+ /**
1199
+ * The pairing payload to encode as a QR, on success. Same `{v,r,k,c,s}` shape a
1200
+ * scanning device redeems via `pair.begin`. `c` is a single-use code with a 60s
1201
+ * TTL, so the client should re-request before it lapses.
1202
+ */
1203
+ payload?: {
1204
+ v: 1;
1205
+ r: string;
1206
+ k: string;
1207
+ c: string;
1208
+ s: string;
1209
+ };
1210
+ /** Machine-readable failure reason (e.g. "not_authenticated", "not_supported"). */
1211
+ error?: string;
1212
+ }
1213
+ /**
1214
+ * Client → daemon: begin the §5.3 session handshake. Carries the signed
1215
+ * ephemeral-key message (msg1). Sent cleartext; the reply (msg2) is also
1216
+ * cleartext, after which BOTH sides seal every frame with the derived keys.
1217
+ */
1218
+ export interface SessionBeginPayload {
1219
+ msg1: HandshakeMsg1;
1220
+ }
1221
+ /** Daemon → client: the handshake reply, or a failure. */
1222
+ export interface SessionBeginResultPayload {
1223
+ /** True iff the daemon verified the device and produced its reply. */
1224
+ ok: boolean;
1225
+ /** The daemon's signed ephemeral-key message (msg2) — present on success. */
1226
+ msg2?: HandshakeMsg2;
1227
+ /** Machine-readable failure reason (e.g. "unknown_device", "bad_signature"). */
1228
+ error?: string;
1177
1229
  }
1178
1230
  /** Client → daemon: ask for this daemon's identity (name, id, host, version). */
1179
1231
  export interface DaemonGetPayload {
@@ -1475,8 +1527,9 @@ export interface PayloadMap {
1475
1527
  "config.set": ConfigSetPayload;
1476
1528
  "log.tail": LogTailPayload;
1477
1529
  "client.hello": ClientHelloPayload;
1478
- hello: HelloPayload;
1479
- "pair.info": PairInfoPayload;
1530
+ "pair.begin": PairBeginPayload;
1531
+ "pairing.issue": PairingIssuePayload;
1532
+ "session.begin": SessionBeginPayload;
1480
1533
  "daemon.get": DaemonGetPayload;
1481
1534
  "daemon.rename": DaemonRenamePayload;
1482
1535
  "daemon.update.check": DaemonUpdateCheckPayload;
@@ -1526,8 +1579,9 @@ export interface PayloadMap {
1526
1579
  "allowlist.result": AllowlistResultPayload;
1527
1580
  "log.tail.result": LogTailResultPayload;
1528
1581
  "log.event": LogEventPayload;
1529
- "hello.ok": HelloOkPayload;
1530
- "pair.info.result": PairInfoResultPayload;
1582
+ "pair.begin.result": PairBeginResultPayload;
1583
+ "pairing.issue.result": PairingIssueResultPayload;
1584
+ "session.begin.result": SessionBeginResultPayload;
1531
1585
  "daemon.info": DaemonInfoPayload;
1532
1586
  "daemon.update.result": DaemonUpdateResultPayload;
1533
1587
  pong: PongPayload;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Relay transport (daemon side) — docs/WEBRTC_MIGRATION.md Phase 4.
3
+ *
4
+ * The daemon keeps ONE persistent WebSocket to its Durable Object (`/daemon/
5
+ * <rid>`) whenever it's running. The DO multiplexes every client over that single
6
+ * socket, tagged by client id. `RelayDaemon` demuxes: each client id gets its own
7
+ * `Transport` (a `SubTransport` over the shared ws), which feeds a `Mux` + the
8
+ * §5.3 responder handshake — exactly the same session stack the loopback and LAN
9
+ * paths use. The relay only ever sees sealed frames.
10
+ */
11
+ import { Mux } from "./transport.js";
12
+ import type { SecureChannel } from "./session.js";
13
+ import type { RendezvousIdentity } from "./rendezvous.js";
14
+ import type { DeviceTable } from "./devices.js";
15
+ export interface RelaySession {
16
+ clientId: number;
17
+ mux: Mux;
18
+ channel: SecureChannel;
19
+ devicePub: string;
20
+ close(): void;
21
+ /** Register a callback for when this client's relay socket drops. */
22
+ onClose(cb: () => void): void;
23
+ }
24
+ export interface RelayDaemonDeps {
25
+ identity: RendezvousIdentity;
26
+ devices: DeviceTable;
27
+ /**
28
+ * Single-use pairing codes. When present, a brand-new device can pair inline
29
+ * during the handshake (msg1 carries a code). Omit to disable relay pairing.
30
+ */
31
+ pairing?: {
32
+ consume(code: string): boolean;
33
+ };
34
+ }
35
+ /**
36
+ * Maintains the daemon's persistent relay socket and per-client sessions. Call
37
+ * `connect()` to dial; it reconnects with backoff on drop so the daemon is
38
+ * reachable over the relay whenever it's running (the Phase-4 acceptance:
39
+ * "daemon restart auto-reconnects with no user action").
40
+ */
41
+ export declare class RelayDaemon {
42
+ private readonly deps;
43
+ private readonly relayUrl;
44
+ private readonly opts?;
45
+ private ws;
46
+ private readonly subs;
47
+ private closed;
48
+ private attempt;
49
+ private reconnectTimer;
50
+ /** Called for each client that completes the handshake over the relay. */
51
+ onSession?: (session: RelaySession) => void;
52
+ constructor(deps: RelayDaemonDeps, relayUrl: string, opts?: {
53
+ license?: string;
54
+ } | undefined);
55
+ /** The daemon endpoint for this daemon's rendezvous id. */
56
+ private url;
57
+ connect(): void;
58
+ private onMessage;
59
+ private openClient;
60
+ private onClose;
61
+ close(): void;
62
+ }