@yefengr/remote-pi 0.7.3 → 0.7.5

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 (171) hide show
  1. package/README.md +81 -586
  2. package/dist/actions/handlers.d.ts +1 -1
  3. package/dist/actions/handlers.js +2 -1
  4. package/dist/actions/handlers.js.map +1 -1
  5. package/dist/bin/supervisord.js +4 -5
  6. package/dist/bin/supervisord.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.js +2 -2
  9. package/dist/daemon/cli-links.d.ts +30 -0
  10. package/dist/daemon/cli-links.js +134 -0
  11. package/dist/daemon/cli-links.js.map +1 -0
  12. package/dist/daemon/commands.d.ts +27 -0
  13. package/dist/daemon/commands.js +295 -0
  14. package/dist/daemon/commands.js.map +1 -0
  15. package/dist/daemon/control_protocol.d.ts +53 -31
  16. package/dist/daemon/control_protocol.js +8 -31
  17. package/dist/daemon/control_protocol.js.map +1 -1
  18. package/dist/daemon/cron_log.d.ts +2 -23
  19. package/dist/daemon/cron_log.js +10 -25
  20. package/dist/daemon/cron_log.js.map +1 -1
  21. package/dist/daemon/cron_registry.d.ts +3 -40
  22. package/dist/daemon/cron_registry.js +58 -103
  23. package/dist/daemon/cron_registry.js.map +1 -1
  24. package/dist/daemon/install-runtime.d.ts +10 -0
  25. package/dist/daemon/install-runtime.js +81 -0
  26. package/dist/daemon/install-runtime.js.map +1 -0
  27. package/dist/daemon/install.d.ts +3 -63
  28. package/dist/daemon/install.js +5 -289
  29. package/dist/daemon/install.js.map +1 -1
  30. package/dist/daemon/preflight.d.ts +51 -0
  31. package/dist/daemon/preflight.js +136 -0
  32. package/dist/daemon/preflight.js.map +1 -0
  33. package/dist/daemon/registry.d.ts +33 -45
  34. package/dist/daemon/registry.js +171 -117
  35. package/dist/daemon/registry.js.map +1 -1
  36. package/dist/daemon/rpc_child.d.ts +92 -146
  37. package/dist/daemon/rpc_child.js +336 -263
  38. package/dist/daemon/rpc_child.js.map +1 -1
  39. package/dist/daemon/status.d.ts +45 -0
  40. package/dist/daemon/status.js +87 -0
  41. package/dist/daemon/status.js.map +1 -0
  42. package/dist/daemon/supervisor-ipc.d.ts +2 -0
  43. package/dist/daemon/supervisor-ipc.js +20 -0
  44. package/dist/daemon/supervisor-ipc.js.map +1 -0
  45. package/dist/daemon/supervisor.d.ts +48 -71
  46. package/dist/daemon/supervisor.js +430 -420
  47. package/dist/daemon/supervisor.js.map +1 -1
  48. package/dist/extension_ui_bridge.d.ts +4 -3
  49. package/dist/extension_ui_bridge.js +10 -0
  50. package/dist/extension_ui_bridge.js.map +1 -1
  51. package/dist/index.d.ts +25 -217
  52. package/dist/index.js +527 -5504
  53. package/dist/index.js.map +1 -1
  54. package/dist/pairing/crypto.d.ts +8 -1
  55. package/dist/pairing/crypto.js +56 -6
  56. package/dist/pairing/crypto.js.map +1 -1
  57. package/dist/pairing/owner_storage.d.ts +42 -0
  58. package/dist/pairing/owner_storage.js +207 -0
  59. package/dist/pairing/owner_storage.js.map +1 -0
  60. package/dist/pairing/qr.d.ts +3 -42
  61. package/dist/pairing/qr.js +9 -59
  62. package/dist/pairing/qr.js.map +1 -1
  63. package/dist/pairing/storage.d.ts +3 -45
  64. package/dist/pairing/storage.js +10 -223
  65. package/dist/pairing/storage.js.map +1 -1
  66. package/dist/protocol/types.d.ts +1 -1
  67. package/dist/protocol/v2/schemas.d.ts +1 -1
  68. package/dist/protocol/v2/schemas.js +1 -1
  69. package/dist/protocol/v2/schemas.js.map +1 -1
  70. package/dist/runtime/owner_router.d.ts +16 -0
  71. package/dist/runtime/owner_router.js +37 -0
  72. package/dist/runtime/owner_router.js.map +1 -0
  73. package/dist/session/ipc.d.ts +1 -22
  74. package/dist/session/ipc.js +4 -11
  75. package/dist/session/ipc.js.map +1 -1
  76. package/dist/session/local_config.d.ts +4 -47
  77. package/dist/session/local_config.js +19 -101
  78. package/dist/session/local_config.js.map +1 -1
  79. package/dist/timeline/v2_service.d.ts +6 -0
  80. package/dist/timeline/v2_service.js +20 -2
  81. package/dist/timeline/v2_service.js.map +1 -1
  82. package/dist/transport/peer_channel.d.ts +25 -39
  83. package/dist/transport/peer_channel.js +74 -131
  84. package/dist/transport/peer_channel.js.map +1 -1
  85. package/dist/transport/relay_client.d.ts +26 -70
  86. package/dist/transport/relay_client.js +37 -106
  87. package/dist/transport/relay_client.js.map +1 -1
  88. package/docs/daemon.md +91 -207
  89. package/package.json +21 -17
  90. package/dist/daemon/id.d.ts +0 -24
  91. package/dist/daemon/id.js +0 -36
  92. package/dist/daemon/id.js.map +0 -1
  93. package/dist/mcp/mesh_result.d.ts +0 -10
  94. package/dist/mcp/mesh_result.js +0 -42
  95. package/dist/mcp/mesh_result.js.map +0 -1
  96. package/dist/mcp/mesh_server.d.ts +0 -16
  97. package/dist/mcp/mesh_server.js +0 -327
  98. package/dist/mcp/mesh_server.js.map +0 -1
  99. package/dist/mesh/canonical.d.ts +0 -30
  100. package/dist/mesh/canonical.js +0 -61
  101. package/dist/mesh/canonical.js.map +0 -1
  102. package/dist/mesh/client.d.ts +0 -18
  103. package/dist/mesh/client.js +0 -125
  104. package/dist/mesh/client.js.map +0 -1
  105. package/dist/mesh/encoding.d.ts +0 -38
  106. package/dist/mesh/encoding.js +0 -216
  107. package/dist/mesh/encoding.js.map +0 -1
  108. package/dist/mesh/self_revoke.d.ts +0 -73
  109. package/dist/mesh/self_revoke.js +0 -401
  110. package/dist/mesh/self_revoke.js.map +0 -1
  111. package/dist/mesh/siblings.d.ts +0 -49
  112. package/dist/mesh/siblings.js +0 -159
  113. package/dist/mesh/siblings.js.map +0 -1
  114. package/dist/mesh/types.d.ts +0 -34
  115. package/dist/mesh/types.js +0 -11
  116. package/dist/mesh/types.js.map +0 -1
  117. package/dist/mesh/verify.d.ts +0 -17
  118. package/dist/mesh/verify.js +0 -86
  119. package/dist/mesh/verify.js.map +0 -1
  120. package/dist/rooms.d.ts +0 -29
  121. package/dist/rooms.js +0 -57
  122. package/dist/rooms.js.map +0 -1
  123. package/dist/session/bridge.d.ts +0 -37
  124. package/dist/session/bridge.js +0 -130
  125. package/dist/session/bridge.js.map +0 -1
  126. package/dist/session/broker.d.ts +0 -189
  127. package/dist/session/broker.js +0 -507
  128. package/dist/session/broker.js.map +0 -1
  129. package/dist/session/broker_remote.d.ts +0 -74
  130. package/dist/session/broker_remote.js +0 -583
  131. package/dist/session/broker_remote.js.map +0 -1
  132. package/dist/session/cwd_lock.d.ts +0 -33
  133. package/dist/session/cwd_lock.js +0 -126
  134. package/dist/session/cwd_lock.js.map +0 -1
  135. package/dist/session/envelope.d.ts +0 -33
  136. package/dist/session/envelope.js +0 -117
  137. package/dist/session/envelope.js.map +0 -1
  138. package/dist/session/global_config.d.ts +0 -33
  139. package/dist/session/global_config.js +0 -73
  140. package/dist/session/global_config.js.map +0 -1
  141. package/dist/session/leader_election.d.ts +0 -32
  142. package/dist/session/leader_election.js +0 -106
  143. package/dist/session/leader_election.js.map +0 -1
  144. package/dist/session/mesh_node.d.ts +0 -116
  145. package/dist/session/mesh_node.js +0 -573
  146. package/dist/session/mesh_node.js.map +0 -1
  147. package/dist/session/peer.d.ts +0 -139
  148. package/dist/session/peer.js +0 -451
  149. package/dist/session/peer.js.map +0 -1
  150. package/dist/session/peer_inventory.d.ts +0 -13
  151. package/dist/session/peer_inventory.js +0 -48
  152. package/dist/session/peer_inventory.js.map +0 -1
  153. package/dist/session/peer_limits.d.ts +0 -9
  154. package/dist/session/peer_limits.js +0 -21
  155. package/dist/session/peer_limits.js.map +0 -1
  156. package/dist/session/setup_wizard.d.ts +0 -39
  157. package/dist/session/setup_wizard.js +0 -63
  158. package/dist/session/setup_wizard.js.map +0 -1
  159. package/dist/session/tools.d.ts +0 -24
  160. package/dist/session/tools.js +0 -252
  161. package/dist/session/tools.js.map +0 -1
  162. package/dist/session/wizard.d.ts +0 -13
  163. package/dist/session/wizard.js +0 -20
  164. package/dist/session/wizard.js.map +0 -1
  165. package/dist/transport/pi_forward_client.d.ts +0 -29
  166. package/dist/transport/pi_forward_client.js +0 -62
  167. package/dist/transport/pi_forward_client.js.map +0 -1
  168. package/dist/ui/footer.d.ts +0 -32
  169. package/dist/ui/footer.js +0 -34
  170. package/dist/ui/footer.js.map +0 -1
  171. package/skills/agent-network/SKILL.md +0 -310
@@ -1,171 +1,114 @@
1
1
  import { decodeClientFrameV2, encodeServerFrameV2, } from "../protocol/v2/index.js";
2
2
  /**
3
- * Plaintext PeerChannel backed by a RelayClient WebSocket.
4
- *
5
- * Usage (after pair_request handshake completes):
6
- * const channel = new PlainPeerChannel(relay, appPeerId, myRoomId, onMsg)
7
- * channel.send(serverMessage) // base64-encodes JSON, routes via relay
8
- * // incoming relay messages destined for appPeerId are auto-decoded
9
- * // and delivered via onMessage callback
10
- *
11
- * `myRoomId` is the *local* Pi's room id — sent on every outbound envelope
12
- * so the app can correlate which Pi sent it (multi-pi support, plano 17).
3
+ * A Host-side protocol-v2 channel for one authorized Owner. The relay
4
+ * authenticates and injects `source_owner_id` for Owner-to-Host routes; this
5
+ * channel only accepts routes for its immutable endpoint identity and owner.
13
6
  */
14
7
  export class V2PeerChannel {
15
8
  relay;
16
- remotePeerId;
9
+ ownerId;
10
+ host;
17
11
  onMessage;
18
12
  unsubscribe;
19
- constructor(relay, remotePeerId, onMessage) {
13
+ constructor(relay, ownerId, host, onMessage) {
20
14
  this.relay = relay;
21
- this.remotePeerId = remotePeerId;
15
+ this.ownerId = ownerId;
16
+ this.host = host;
22
17
  this.onMessage = onMessage;
23
18
  const listener = (line) => this.onLine(line);
24
19
  relay.on("message", listener);
25
20
  this.unsubscribe = () => relay.off("message", listener);
26
21
  }
27
22
  getPeerId() {
28
- return this.remotePeerId;
23
+ return this.ownerId;
29
24
  }
30
25
  sendV2(msg) {
31
- let ct;
32
26
  try {
33
- ct = Buffer.from(encodeServerFrameV2(msg)).toString("base64");
34
- this.relay.send(JSON.stringify({ peer: this.remotePeerId, ct }));
27
+ const ct = Buffer.from(encodeServerFrameV2(msg)).toString("base64");
28
+ this.relay.send(JSON.stringify({
29
+ type: "route",
30
+ purpose: msg.type === "pair_ok" || msg.type === "pair_error" ? "pairing" : "session",
31
+ device_id: this.host.deviceId,
32
+ endpoint_id: this.host.endpointId,
33
+ runtime_instance_id: this.host.runtimeInstanceId,
34
+ target_owner_id: this.ownerId,
35
+ ct,
36
+ }));
35
37
  }
36
38
  catch {
37
- // Reconnect plus authoritative history recovers dropped formal events.
39
+ // Formal history recovers messages lost while the relay reconnects.
38
40
  }
39
41
  }
40
42
  detach() {
41
43
  this.unsubscribe();
42
44
  }
43
45
  onLine(line) {
44
- let outer;
45
- try {
46
- outer = JSON.parse(line);
47
- }
48
- catch {
49
- return;
50
- }
51
- if (outer.peer !== this.remotePeerId || !outer.ct)
46
+ const outer = parseIncomingRoute(line, this.host, this.ownerId);
47
+ if (!outer)
52
48
  return;
53
49
  try {
54
- const plaintext = Buffer.from(outer.ct, "base64").toString("utf8");
55
- this.onMessage(decodeClientFrameV2(plaintext));
50
+ const frame = decodeClientFrameV2(Buffer.from(outer.ct, "base64").toString("utf8"));
51
+ const expectedPurpose = frame.type === "pair_request" ? "pairing" : "session";
52
+ if (outer.purpose !== expectedPurpose)
53
+ return;
54
+ this.onMessage(frame);
56
55
  }
57
56
  catch {
58
- // Strict v2 boundary. A parseable legacy/invalid request receives an upgrade
59
- // error; malformed bytes without an id cannot be safely targeted.
60
- try {
61
- const raw = JSON.parse(Buffer.from(outer.ct, "base64").toString("utf8"));
62
- const id = typeof raw.id === "string" ? raw.id : undefined;
63
- const channelId = typeof raw.channel_id === "string" ? raw.channel_id : undefined;
64
- if (id) {
65
- this.sendV2({
66
- protocol_version: 2,
67
- type: "protocol_error",
68
- ...(channelId ? { target_channel_id: channelId } : {}),
69
- in_reply_to: id,
70
- code: "protocol_upgrade_required",
71
- message: "Protocol v2 is required",
72
- });
73
- }
74
- }
75
- catch {
76
- // Ignore malformed outer payloads.
77
- }
57
+ this.sendUpgradeRequired(outer.ct);
78
58
  }
79
59
  }
80
- }
81
- /** @deprecated Protocol v1 test helper. Production uses V2PeerChannel. */
82
- export class PlainPeerChannel {
83
- relay;
84
- remotePeerId;
85
- onMessage;
86
- _unsubscribe;
87
- constructor(relay, remotePeerId,
88
- /**
89
- * This Pi's room id. Currently NOT injected in the outer envelope
90
- * (defensive — relay/app not yet ready). Kept in the constructor for
91
- * forward-compat so callers don't need to change again when we re-enable.
92
- */
93
- myRoomId, onMessage,
94
- /** Called when this specific peer connection is considered lost. */
95
- _onDisconnect) {
96
- this.relay = relay;
97
- this.remotePeerId = remotePeerId;
98
- this.onMessage = onMessage;
99
- const listener = (line) => this._onLine(line);
100
- relay.on("message", listener);
101
- this._unsubscribe = () => relay.off("message", listener);
102
- void _onDisconnect;
103
- void myRoomId; // intentionally unused — see send() comment
104
- }
105
- // ── PeerChannel interface ──────────────────────────────────────────────────
106
- /** Authenticated Owner peer id used as a server-derived sender reference. */
107
- getPeerId() {
108
- return this.remotePeerId;
109
- }
110
- send(msg) {
111
- const ct = Buffer.from(JSON.stringify(msg)).toString("base64");
112
- // NOTE: `room` removed from the outer envelope until relay (W1.A) + app
113
- // (W1.C) accept the field. Multi-Pi multiplexing already works via
114
- // `room_id`/`room_meta` in the WS-level `hello` — outer routing stays by
115
- // `peer` alone. Re-add the field once downstream is ready.
116
- const outer = { peer: this.remotePeerId, ct };
117
- // Best-effort delivery. The relay WS can be mid-reconnect (idle/NAT drop, or
118
- // a session_new/session-replacement teardown) when we push a server→app frame
119
- // — notably the action_ok/action_error ack a handler emits right after
120
- // newSession. `relay.send` throws "relay: not connected" in that window; since
121
- // this runs inside an async SDK event callback, letting it propagate becomes an
122
- // uncaughtException that kills the whole pi process. The relay auto-reconnects
123
- // and the app re-syncs via session_sync, so a dropped frame is recoverable — a
124
- // crash is not. Mirrors RelayClient.sendControl's no-op-when-closed policy.
60
+ sendUpgradeRequired(ct) {
125
61
  try {
126
- this.relay.send(JSON.stringify(outer));
62
+ const raw = JSON.parse(Buffer.from(ct, "base64").toString("utf8"));
63
+ const id = typeof raw.id === "string" ? raw.id : undefined;
64
+ const channelId = typeof raw.channel_id === "string" ? raw.channel_id : undefined;
65
+ if (!id)
66
+ return;
67
+ this.sendV2({
68
+ protocol_version: 2,
69
+ type: "protocol_error",
70
+ ...(channelId ? { target_channel_id: channelId } : {}),
71
+ in_reply_to: id,
72
+ code: "protocol_upgrade_required",
73
+ message: "Protocol v2 is required",
74
+ });
127
75
  }
128
76
  catch {
129
- /* relay down — drop this frame; reconnect + session_sync will recover */
77
+ // Invalid bytes without a request id are not targetable.
130
78
  }
131
79
  }
132
- /** Detaches from relay (does not close the relay itself). */
133
- detach() {
134
- this._unsubscribe();
80
+ }
81
+ export function parseIncomingRoute(line, host, ownerId) {
82
+ let route;
83
+ try {
84
+ route = JSON.parse(line);
135
85
  }
136
- // ── Incoming line from relay ────────────────────────────────────────────────
137
- _onLine(line) {
138
- let outer;
139
- try {
140
- outer = JSON.parse(line);
141
- }
142
- catch {
143
- return; // malformed line
144
- }
145
- if (outer.peer !== this.remotePeerId)
146
- return;
147
- if (!outer.ct)
148
- return;
149
- let plaintext;
150
- try {
151
- plaintext = Buffer.from(outer.ct, "base64").toString("utf8");
152
- }
153
- catch {
154
- return;
155
- }
156
- let msg;
157
- try {
158
- msg = JSON.parse(plaintext);
159
- }
160
- catch {
161
- return;
162
- }
163
- if (!msg ||
164
- typeof msg !== "object" ||
165
- typeof msg.type !== "string") {
166
- return;
167
- }
168
- this.onMessage(msg);
86
+ catch {
87
+ return null;
169
88
  }
89
+ if (route.type !== "route" ||
90
+ (route.purpose !== "pairing" && route.purpose !== "session") ||
91
+ typeof route.ct !== "string" ||
92
+ route.device_id !== host.deviceId ||
93
+ route.endpoint_id !== host.endpointId ||
94
+ route.runtime_instance_id !== host.runtimeInstanceId ||
95
+ typeof route.source_owner_id !== "string" ||
96
+ route.target_owner_id !== undefined) {
97
+ return null;
98
+ }
99
+ if (ownerId !== undefined && route.source_owner_id !== ownerId)
100
+ return null;
101
+ return route;
102
+ }
103
+ export function sendRoute(relay, host, ownerId, purpose, ct) {
104
+ relay.send(JSON.stringify({
105
+ type: "route",
106
+ purpose,
107
+ device_id: host.deviceId,
108
+ endpoint_id: host.endpointId,
109
+ runtime_instance_id: host.runtimeInstanceId,
110
+ target_owner_id: ownerId,
111
+ ct,
112
+ }));
170
113
  }
171
114
  //# sourceMappingURL=peer_channel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"peer_channel.js","sourceRoot":"","sources":["../../src/transport/peer_channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"peer_channel.js","sourceRoot":"","sources":["../../src/transport/peer_channel.ts"],"names":[],"mappings":"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"}
@@ -1,91 +1,47 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import type { Ed25519Keypair } from "../pairing/crypto.js";
3
- export interface RoomMeta {
4
- name: string;
5
- cwd: string;
6
- /** Friendly model name (e.g. "claude-sonnet-4.5"). Optional — pi-ext sends
7
- * when `ExtensionContext.model` is available; relay/app tolerate absence. */
3
+ export type EndpointMetadata = {
4
+ kind: "daemon" | "interactive";
5
+ name?: string;
6
+ cwd?: string;
7
+ pid?: number;
8
+ started_at?: number;
8
9
  model?: string;
9
- }
10
- /** Control frame sent to relay (not routed to app peer). */
11
- export interface RoomMetaUpdateFrame {
12
- type: "room_meta_update";
13
- room_id: string;
14
- meta: {
15
- model?: string;
16
- };
17
- }
18
- export interface ConnectOptions {
19
- roomId?: string;
20
- roomMeta?: RoomMeta;
21
- }
22
- /** Relay rejected hello because another peer already holds (pubkey, room_id). */
23
- export declare class RoomAlreadyOpenError extends Error {
24
- readonly roomId: string | undefined;
25
- constructor(roomId: string | undefined);
26
- }
10
+ thinking?: string;
11
+ working?: boolean;
12
+ };
13
+ export type HostConnectOptions = {
14
+ role: "host";
15
+ endpointId: string;
16
+ runtimeInstanceId: string;
17
+ metadata: EndpointMetadata;
18
+ authorizedOwnerIds: readonly string[];
19
+ };
20
+ export type OwnerConnectOptions = {
21
+ role: "owner";
22
+ };
23
+ export type ConnectOptions = HostConnectOptions | OwnerConnectOptions;
27
24
  export interface RelayClientEvents {
28
- /** A single JSONL line delivered by the relay (outer envelope). */
29
25
  message: [line: string];
30
26
  close: [];
31
27
  error: [err: Error];
32
28
  }
33
- /**
34
- * Thin WebSocket client for the Remote Pi relay.
35
- *
36
- * Lifecycle:
37
- * const relay = new RelayClient(url, ed25519Keypair)
38
- * await relay.connect() // opens WS + runs Ed25519 challenge-response
39
- * relay.on("message", line => …) // outer envelopes: { peer, ct }
40
- * relay.send(jsonLine) // write to relay
41
- * relay.close()
42
- *
43
- * Auth sequence (pairing.md §Challenge-response):
44
- * → { type:"hello", pubkey: "<Ed25519 pubkey base64>" }
45
- * ← { type:"challenge", nonce: "<32 bytes base64>" }
46
- * → { type:"auth", sig: "<Ed25519 sig base64>" }
47
- */
29
+ /** Authenticated WebSocket client for the endpoint relay. */
48
30
  export declare class RelayClient extends EventEmitter {
49
31
  private readonly url;
50
32
  private readonly keypair;
51
33
  private ws;
52
- /** Epoch ms of the last inbound frame (message / relay ping / pong). */
53
34
  private lastActivityAt;
54
35
  private livenessTimer;
55
36
  constructor(url: string, keypair: Ed25519Keypair);
56
- /**
57
- * Connects and completes Ed25519 auth. Resolves when relay is ready.
58
- *
59
- * `options.roomId` (12-char id derived from cwd, see `src/rooms.ts`) is
60
- * included in the hello so the relay can multiplex N concurrent peers
61
- * with the same Ed25519 pubkey but different rooms (one pi-ext per cwd).
62
- * Omitting `roomId` is backward-compat with old relays (treated as the
63
- * default "main" room server-side).
64
- *
65
- * Throws `RoomAlreadyOpenError` if the relay rejects the hello because
66
- * another peer already holds the (pubkey, room_id) tuple. Caller (e.g.
67
- * `_cmdStart`) maps that to a clearer UI message.
68
- */
69
37
  connect(options?: ConnectOptions): Promise<void>;
70
- /** True only while the authenticated Relay WebSocket is currently open. */
71
38
  isOpen(): boolean;
72
- /** Sends a raw line to the relay (caller is responsible for framing). */
73
39
  send(line: string): void;
74
- /**
75
- * Sends a control frame to the relay (not routed to app peer). Used for
76
- * out-of-band metadata updates like `room_meta_update`. Silently no-ops if
77
- * the WS isn't open (best-effort: control frames are observational; we
78
- * don't want them throwing inside SDK event callbacks).
79
- */
80
40
  sendControl(frame: object): void;
81
41
  close(): void;
82
- /** Force-close the WS when no inbound frame has arrived for
83
- * `LIVENESS_TIMEOUT_MS` — see the constant's doc for why. `terminate()`
84
- * fires `close`, which the owner turns into a reconnect. */
85
- private _startLiveness;
86
- private _stopLiveness;
87
- private _authenticate;
88
- /** Waits for the next single WS message with a timeout. */
89
- private _nextMsg;
90
- private _rawSend;
42
+ private startLiveness;
43
+ private stopLiveness;
44
+ private authenticate;
45
+ private nextMessage;
46
+ private rawSend;
91
47
  }
@@ -2,48 +2,13 @@ import { EventEmitter } from "node:events";
2
2
  import WebSocket from "ws";
3
3
  import { ed25519Sign } from "../pairing/crypto.js";
4
4
  const AUTH_TIMEOUT_MS = 5_000;
5
- /**
6
- * Liveness watchdog. The relay sends a WS Ping every ~25s (relay `peer.rs`),
7
- * so a healthy connection sees inbound frames at least that often. If NOTHING
8
- * arrives for this long the socket is silently dead — NAT/router idle drop,
9
- * laptop sleep, or the relay/Cloudflare reaping the connection WITHOUT a clean
10
- * close frame. That half-open case never fires `close`, so reconnect never
11
- * triggers and a background daemon sits "online" but dead after a few idle
12
- * hours. We force-close on timeout so `close` drives the caller's reconnect.
13
- */
14
- const LIVENESS_TIMEOUT_MS = 70_000; // ~2.8 missed relay pings → confidently dead
5
+ const LIVENESS_TIMEOUT_MS = 70_000;
15
6
  const LIVENESS_CHECK_MS = 20_000;
16
- /** Relay rejected hello because another peer already holds (pubkey, room_id). */
17
- export class RoomAlreadyOpenError extends Error {
18
- roomId;
19
- constructor(roomId) {
20
- super(roomId
21
- ? `relay rejected hello: room ${roomId} already open for this peer`
22
- : "relay rejected hello: peer already connected");
23
- this.roomId = roomId;
24
- this.name = "RoomAlreadyOpenError";
25
- }
26
- }
27
- /**
28
- * Thin WebSocket client for the Remote Pi relay.
29
- *
30
- * Lifecycle:
31
- * const relay = new RelayClient(url, ed25519Keypair)
32
- * await relay.connect() // opens WS + runs Ed25519 challenge-response
33
- * relay.on("message", line => …) // outer envelopes: { peer, ct }
34
- * relay.send(jsonLine) // write to relay
35
- * relay.close()
36
- *
37
- * Auth sequence (pairing.md §Challenge-response):
38
- * → { type:"hello", pubkey: "<Ed25519 pubkey base64>" }
39
- * ← { type:"challenge", nonce: "<32 bytes base64>" }
40
- * → { type:"auth", sig: "<Ed25519 sig base64>" }
41
- */
7
+ /** Authenticated WebSocket client for the endpoint relay. */
42
8
  export class RelayClient extends EventEmitter {
43
9
  url;
44
10
  keypair;
45
11
  ws = null;
46
- /** Epoch ms of the last inbound frame (message / relay ping / pong). */
47
12
  lastActivityAt = 0;
48
13
  livenessTimer = null;
49
14
  constructor(url, keypair) {
@@ -51,31 +16,14 @@ export class RelayClient extends EventEmitter {
51
16
  this.url = url;
52
17
  this.keypair = keypair;
53
18
  }
54
- // ── Public API ──────────────────────────────────────────────────────────────
55
- /**
56
- * Connects and completes Ed25519 auth. Resolves when relay is ready.
57
- *
58
- * `options.roomId` (12-char id derived from cwd, see `src/rooms.ts`) is
59
- * included in the hello so the relay can multiplex N concurrent peers
60
- * with the same Ed25519 pubkey but different rooms (one pi-ext per cwd).
61
- * Omitting `roomId` is backward-compat with old relays (treated as the
62
- * default "main" room server-side).
63
- *
64
- * Throws `RoomAlreadyOpenError` if the relay rejects the hello because
65
- * another peer already holds the (pubkey, room_id) tuple. Caller (e.g.
66
- * `_cmdStart`) maps that to a clearer UI message.
67
- */
68
- async connect(options = {}) {
19
+ async connect(options = { role: "owner" }) {
69
20
  return new Promise((resolve, reject) => {
70
21
  const ws = new WebSocket(this.url);
71
22
  this.ws = ws;
72
23
  ws.on("error", (err) => reject(err));
73
24
  ws.on("open", async () => {
74
25
  try {
75
- await this._authenticate(ws, options);
76
- // Auth done — wire persistent message handler. Every inbound frame
77
- // (data, plus the relay's keepalive ping/pong below) refreshes the
78
- // liveness clock.
26
+ await this.authenticate(ws, options);
79
27
  this.lastActivityAt = Date.now();
80
28
  ws.on("message", (raw) => {
81
29
  this.lastActivityAt = Date.now();
@@ -86,17 +34,13 @@ export class RelayClient extends EventEmitter {
86
34
  this.emit("message", trimmed);
87
35
  }
88
36
  });
89
- // The relay pings every ~25s; `ws` auto-replies Pong (keeping the
90
- // relay's view of us alive). The relay ignores client pings rather
91
- // than ponging, so these inbound pings — not a ping/pong we initiate
92
- // — are our liveness signal.
93
37
  ws.on("ping", () => { this.lastActivityAt = Date.now(); });
94
38
  ws.on("pong", () => { this.lastActivityAt = Date.now(); });
95
39
  ws.on("close", () => {
96
- this._stopLiveness();
40
+ this.stopLiveness();
97
41
  this.emit("close");
98
42
  });
99
- this._startLiveness(ws);
43
+ this.startLiveness(ws);
100
44
  resolve();
101
45
  }
102
46
  catch (err) {
@@ -106,62 +50,56 @@ export class RelayClient extends EventEmitter {
106
50
  });
107
51
  });
108
52
  }
109
- /** True only while the authenticated Relay WebSocket is currently open. */
110
53
  isOpen() {
111
54
  return this.ws?.readyState === WebSocket.OPEN;
112
55
  }
113
- /** Sends a raw line to the relay (caller is responsible for framing). */
114
56
  send(line) {
115
57
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
116
58
  throw new Error("relay: not connected");
117
59
  }
118
60
  this.ws.send(line);
119
61
  }
120
- /**
121
- * Sends a control frame to the relay (not routed to app peer). Used for
122
- * out-of-band metadata updates like `room_meta_update`. Silently no-ops if
123
- * the WS isn't open (best-effort: control frames are observational; we
124
- * don't want them throwing inside SDK event callbacks).
125
- */
126
62
  sendControl(frame) {
127
63
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
128
64
  return;
129
65
  this.ws.send(JSON.stringify(frame));
130
66
  }
131
67
  close() {
132
- this._stopLiveness();
68
+ this.stopLiveness();
133
69
  this.ws?.close();
134
70
  this.ws = null;
135
71
  }
136
- // ── Liveness watchdog ─────────────────────────────────────────────────────
137
- /** Force-close the WS when no inbound frame has arrived for
138
- * `LIVENESS_TIMEOUT_MS` — see the constant's doc for why. `terminate()`
139
- * fires `close`, which the owner turns into a reconnect. */
140
- _startLiveness(ws) {
141
- this._stopLiveness();
72
+ startLiveness(ws) {
73
+ this.stopLiveness();
142
74
  this.livenessTimer = setInterval(() => {
143
75
  if (Date.now() - this.lastActivityAt > LIVENESS_TIMEOUT_MS) {
144
- this._stopLiveness();
76
+ this.stopLiveness();
145
77
  ws.terminate();
146
78
  }
147
79
  }, LIVENESS_CHECK_MS);
148
80
  }
149
- _stopLiveness() {
150
- if (this.livenessTimer) {
151
- clearInterval(this.livenessTimer);
152
- this.livenessTimer = null;
153
- }
81
+ stopLiveness() {
82
+ if (!this.livenessTimer)
83
+ return;
84
+ clearInterval(this.livenessTimer);
85
+ this.livenessTimer = null;
154
86
  }
155
- // ── Auth ────────────────────────────────────────────────────────────────────
156
- async _authenticate(ws, opts) {
157
- const pubkeyB64 = Buffer.from(this.keypair.publicKey).toString("base64");
158
- const hello = { type: "hello", pubkey: pubkeyB64 };
159
- if (opts.roomId)
160
- hello.room_id = opts.roomId;
161
- if (opts.roomMeta)
162
- hello.room_meta = opts.roomMeta;
163
- this._rawSend(ws, JSON.stringify(hello));
164
- const challengeRaw = await this._nextMsg(ws);
87
+ async authenticate(ws, options) {
88
+ const pubkey = Buffer.from(this.keypair.publicKey).toString("base64");
89
+ const hello = options.role === "host"
90
+ ? {
91
+ type: "hello",
92
+ protocol_version: 2,
93
+ role: "host",
94
+ pubkey,
95
+ endpoint_id: options.endpointId,
96
+ runtime_instance_id: options.runtimeInstanceId,
97
+ metadata: options.metadata,
98
+ authorized_owner_ids: options.authorizedOwnerIds,
99
+ }
100
+ : { type: "hello", protocol_version: 2, role: "owner", pubkey };
101
+ this.rawSend(ws, JSON.stringify(hello));
102
+ const challengeRaw = await this.nextMessage(ws);
165
103
  let challenge;
166
104
  try {
167
105
  challenge = JSON.parse(challengeRaw);
@@ -170,27 +108,20 @@ export class RelayClient extends EventEmitter {
170
108
  throw new Error(`relay auth_failed: not JSON: ${challengeRaw}`);
171
109
  }
172
110
  if (challenge.type === "error") {
173
- const code = challenge.code ?? "";
174
- if (code === "room_already_open") {
175
- throw new RoomAlreadyOpenError(opts.roomId);
176
- }
177
- throw new Error(`relay rejected hello: ${code || challenge.message || "unknown"}`);
111
+ const detail = challenge.code || challenge.message || "unknown";
112
+ throw new Error(`relay rejected hello: ${detail}`);
178
113
  }
179
114
  if (challenge.type !== "challenge" || !challenge.nonce) {
180
115
  throw new Error(`relay auth_failed: expected challenge, got ${challengeRaw}`);
181
116
  }
182
117
  const nonce = Buffer.from(challenge.nonce, "base64");
183
- const sig = ed25519Sign(this.keypair.secretKey, nonce);
184
118
  const auth = {
185
119
  type: "auth",
186
- sig: Buffer.from(sig).toString("base64"),
120
+ sig: Buffer.from(ed25519Sign(this.keypair.secretKey, nonce)).toString("base64"),
187
121
  };
188
- this._rawSend(ws, JSON.stringify(auth));
189
- // Relay does not send an explicit "ok" — it simply starts routing.
190
- // Proceed immediately after sending auth.
122
+ this.rawSend(ws, JSON.stringify(auth));
191
123
  }
192
- /** Waits for the next single WS message with a timeout. */
193
- _nextMsg(ws) {
124
+ nextMessage(ws) {
194
125
  return new Promise((resolve, reject) => {
195
126
  const timer = setTimeout(() => reject(new Error("relay auth timeout")), AUTH_TIMEOUT_MS);
196
127
  ws.once("message", (raw) => {
@@ -199,7 +130,7 @@ export class RelayClient extends EventEmitter {
199
130
  });
200
131
  });
201
132
  }
202
- _rawSend(ws, data) {
133
+ rawSend(ws, data) {
203
134
  ws.send(data);
204
135
  }
205
136
  }