@yefengr/remote-pi 0.7.10 → 0.7.12

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.
@@ -35,7 +35,7 @@ export class QRSession {
35
35
  }
36
36
  export const qrSession = new QRSession();
37
37
  /** Builds the endpoint-aware pairing URI. */
38
- export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId) {
38
+ export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl) {
39
39
  const params = new URLSearchParams({
40
40
  t: token,
41
41
  epk: Buffer.from(longtermEdPk).toString("base64url"),
@@ -43,6 +43,14 @@ export function buildQRUri(token, longtermEdPk, sessionName, endpointId, runtime
43
43
  ep: endpointId,
44
44
  rt: runtimeInstanceId,
45
45
  });
46
+ if (relayUrl) {
47
+ try {
48
+ const protocol = new URL(relayUrl).protocol;
49
+ if (protocol === "http:" || protocol === "https:")
50
+ params.set("r", relayUrl);
51
+ }
52
+ catch { /* invalid optional hint */ }
53
+ }
46
54
  return `remotepi://pair?${params.toString()}`;
47
55
  }
48
56
  export function renderQRAscii(uri) {
@@ -53,14 +61,14 @@ export function renderQRAscii(uri) {
53
61
  export function displayQR(uri) {
54
62
  process.stderr.write(`\nScan to pair:\n\n${renderQRAscii(uri)}\n`);
55
63
  }
56
- export function startQRRotation(longtermEdPk, sessionName, endpointId, runtimeInstanceId) {
64
+ export function startQRRotation(longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl) {
57
65
  let timer = null;
58
66
  let stopped = false;
59
67
  const rotate = () => {
60
68
  if (stopped)
61
69
  return;
62
70
  const { token, expiresAt } = qrSession.issueToken();
63
- displayQR(buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId));
71
+ displayQR(buildQRUri(token, longtermEdPk, sessionName, endpointId, runtimeInstanceId, relayUrl));
64
72
  console.log(`Renews at ${new Date(expiresAt).toLocaleTimeString()} — waiting for scan…`);
65
73
  timer = setTimeout(rotate, TOKEN_TTL_MS);
66
74
  };
@@ -1 +1 @@
1
- {"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"qr.js","sourceRoot":"","sources":["../../src/pairing/qr.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { RelayResolution } from "../config.js";
3
+ import type { Ed25519Keypair } from "../pairing/crypto.js";
4
+ import type { HostConnectOptions, RelayClient } from "../transport/relay_client.js";
5
+ export type RemoteState = "idle" | "started";
6
+ export type RelayConnectivity = "connected" | "reconnecting" | "disconnected";
7
+ export type InitialRelayResult = "completed" | "cancelled";
8
+ export type RelayStartContext = Pick<ExtensionContext, "ui" | "cwd">;
9
+ export interface RelayLifecycleDependencies {
10
+ loadIdentity(): Promise<Ed25519Keypair>;
11
+ resolveRelayUrl(): RelayResolution;
12
+ createClient(relayUrl: string, identity: Ed25519Keypair): RelayClient;
13
+ buildConnectOptions(cwd?: string): Promise<HostConnectOptions>;
14
+ handleIdentityError(error: unknown, ctx: RelayStartContext): boolean;
15
+ describeConnection(resolution: RelayResolution): string;
16
+ onStateChange(): void;
17
+ onConnected(client: RelayClient, ctx?: RelayStartContext): void;
18
+ onDisconnected(): void;
19
+ }
20
+ export declare class RelayLifecycle {
21
+ private readonly deps;
22
+ private activeRelay;
23
+ private pendingCandidate;
24
+ private currentRelayUrl;
25
+ private identity;
26
+ private initialAttempt;
27
+ private cancelInitialAttempt;
28
+ private reconnectTimer;
29
+ private reconnectAttempt;
30
+ private generation;
31
+ private isDisposed;
32
+ private currentState;
33
+ private readonly closedCandidates;
34
+ constructor(deps: RelayLifecycleDependencies);
35
+ get state(): RemoteState;
36
+ get relay(): RelayClient | null;
37
+ get relayUrl(): string | null;
38
+ get keypair(): Ed25519Keypair | null;
39
+ get disposed(): boolean;
40
+ get hasPendingReconnect(): boolean;
41
+ get status(): RelayConnectivity;
42
+ isCurrent(candidate: RelayClient): boolean;
43
+ setDisposed(value: boolean): void;
44
+ start(ctx: RelayStartContext): Promise<InitialRelayResult>;
45
+ waitForInitial(): Promise<InitialRelayResult>;
46
+ stop(): void;
47
+ private startInitial;
48
+ private scheduleReconnect;
49
+ private reconnect;
50
+ private onRelayClose;
51
+ private handleConnectionFailure;
52
+ private clearCandidate;
53
+ private closeCandidateOnce;
54
+ private clearInitialAttempt;
55
+ private isOperationCurrent;
56
+ private isReconnectCurrent;
57
+ private notify;
58
+ }
@@ -0,0 +1,241 @@
1
+ const RECONNECT_BACKOFFS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
2
+ export class RelayLifecycle {
3
+ deps;
4
+ activeRelay = null;
5
+ pendingCandidate = null;
6
+ currentRelayUrl = null;
7
+ identity = null;
8
+ initialAttempt = null;
9
+ cancelInitialAttempt = null;
10
+ reconnectTimer = null;
11
+ reconnectAttempt = 0;
12
+ generation = 0;
13
+ isDisposed = false;
14
+ currentState = "idle";
15
+ closedCandidates = new WeakSet();
16
+ constructor(deps) {
17
+ this.deps = deps;
18
+ }
19
+ get state() { return this.currentState; }
20
+ get relay() { return this.activeRelay; }
21
+ get relayUrl() { return this.currentRelayUrl; }
22
+ get keypair() { return this.identity; }
23
+ get disposed() { return this.isDisposed; }
24
+ get hasPendingReconnect() { return this.reconnectTimer !== null; }
25
+ get status() {
26
+ if (this.currentState === "idle")
27
+ return "disconnected";
28
+ return this.activeRelay?.isOpen() ? "connected" : "reconnecting";
29
+ }
30
+ isCurrent(candidate) {
31
+ return !this.isDisposed && this.currentState === "started" && this.activeRelay === candidate;
32
+ }
33
+ setDisposed(value) {
34
+ this.isDisposed = value;
35
+ }
36
+ start(ctx) {
37
+ const existing = this.initialAttempt;
38
+ if (existing)
39
+ return existing;
40
+ if (this.currentState !== "idle") {
41
+ const message = this.status === "reconnecting"
42
+ ? "[remote-pi] Relay is reconnecting in background."
43
+ : "[remote-pi] Already connected.";
44
+ this.notify(ctx, message, "warning");
45
+ return Promise.resolve("completed");
46
+ }
47
+ const operation = this.startInitial(ctx);
48
+ let cancel;
49
+ const cancellation = new Promise((resolve) => {
50
+ cancel = () => resolve("cancelled");
51
+ });
52
+ const attempt = Promise.race([operation.then(() => "completed"), cancellation]);
53
+ this.initialAttempt = attempt;
54
+ this.cancelInitialAttempt = cancel;
55
+ void operation.catch(() => undefined);
56
+ void attempt.then(() => this.clearInitialAttempt(attempt, cancel), () => this.clearInitialAttempt(attempt, cancel));
57
+ return attempt;
58
+ }
59
+ waitForInitial() {
60
+ return this.initialAttempt ?? Promise.resolve("completed");
61
+ }
62
+ stop() {
63
+ this.generation += 1;
64
+ if (this.reconnectTimer) {
65
+ clearTimeout(this.reconnectTimer);
66
+ this.reconnectTimer = null;
67
+ }
68
+ this.reconnectAttempt = 0;
69
+ const active = this.activeRelay;
70
+ const pending = this.pendingCandidate;
71
+ const cancel = this.cancelInitialAttempt;
72
+ this.activeRelay = null;
73
+ this.pendingCandidate = null;
74
+ this.currentRelayUrl = null;
75
+ this.initialAttempt = null;
76
+ this.cancelInitialAttempt = null;
77
+ this.currentState = "idle";
78
+ cancel?.();
79
+ if (active)
80
+ this.closeCandidateOnce(active);
81
+ if (pending && pending !== active)
82
+ this.closeCandidateOnce(pending);
83
+ this.deps.onDisconnected();
84
+ this.deps.onStateChange();
85
+ }
86
+ async startInitial(ctx) {
87
+ const expectedGeneration = ++this.generation;
88
+ let identity;
89
+ try {
90
+ identity = await this.deps.loadIdentity();
91
+ }
92
+ catch (error) {
93
+ if (!this.isOperationCurrent(expectedGeneration))
94
+ return;
95
+ if (this.deps.handleIdentityError(error, ctx))
96
+ return;
97
+ throw error;
98
+ }
99
+ if (!this.isOperationCurrent(expectedGeneration))
100
+ return;
101
+ this.identity = identity;
102
+ const resolution = this.deps.resolveRelayUrl();
103
+ this.currentRelayUrl = resolution.url;
104
+ this.currentState = "started";
105
+ this.deps.onStateChange();
106
+ this.notify(ctx, this.deps.describeConnection(resolution));
107
+ let options;
108
+ try {
109
+ options = await this.deps.buildConnectOptions(ctx.cwd);
110
+ }
111
+ catch (error) {
112
+ this.handleConnectionFailure(expectedGeneration, resolution.url, ctx, error);
113
+ return;
114
+ }
115
+ if (!this.isReconnectCurrent(expectedGeneration))
116
+ return;
117
+ const candidate = this.deps.createClient(resolution.url, identity);
118
+ this.pendingCandidate = candidate;
119
+ try {
120
+ await candidate.connect(options);
121
+ }
122
+ catch (error) {
123
+ this.clearCandidate(candidate, true);
124
+ this.handleConnectionFailure(expectedGeneration, resolution.url, ctx, error);
125
+ return;
126
+ }
127
+ if (!this.isReconnectCurrent(expectedGeneration) || this.pendingCandidate !== candidate) {
128
+ this.closeCandidateOnce(candidate);
129
+ return;
130
+ }
131
+ this.pendingCandidate = null;
132
+ this.activeRelay = candidate;
133
+ this.reconnectAttempt = 0;
134
+ candidate.on("close", () => this.onRelayClose(candidate));
135
+ this.deps.onConnected(candidate, ctx);
136
+ this.deps.onStateChange();
137
+ }
138
+ scheduleReconnect(url) {
139
+ if (!this.identity || this.isDisposed || this.currentState !== "started")
140
+ return;
141
+ if (this.reconnectTimer)
142
+ clearTimeout(this.reconnectTimer);
143
+ const expectedGeneration = ++this.generation;
144
+ const delay = RECONNECT_BACKOFFS_MS[Math.min(this.reconnectAttempt++, RECONNECT_BACKOFFS_MS.length - 1)];
145
+ this.reconnectTimer = setTimeout(() => {
146
+ this.reconnectTimer = null;
147
+ if (!this.isReconnectCurrent(expectedGeneration))
148
+ return;
149
+ void this.reconnect(url, expectedGeneration);
150
+ }, delay);
151
+ }
152
+ async reconnect(url, expectedGeneration) {
153
+ const identity = this.identity;
154
+ if (!identity || !this.isReconnectCurrent(expectedGeneration))
155
+ return;
156
+ let options;
157
+ try {
158
+ options = await this.deps.buildConnectOptions();
159
+ }
160
+ catch {
161
+ if (this.isReconnectCurrent(expectedGeneration)) {
162
+ this.deps.onStateChange();
163
+ this.scheduleReconnect(url);
164
+ }
165
+ return;
166
+ }
167
+ if (!this.isReconnectCurrent(expectedGeneration) || this.identity !== identity)
168
+ return;
169
+ const candidate = this.deps.createClient(url, identity);
170
+ this.pendingCandidate = candidate;
171
+ try {
172
+ await candidate.connect(options);
173
+ }
174
+ catch {
175
+ this.clearCandidate(candidate, true);
176
+ if (this.isReconnectCurrent(expectedGeneration)) {
177
+ this.deps.onStateChange();
178
+ this.scheduleReconnect(url);
179
+ }
180
+ return;
181
+ }
182
+ if (!this.isReconnectCurrent(expectedGeneration) || this.pendingCandidate !== candidate) {
183
+ this.closeCandidateOnce(candidate);
184
+ return;
185
+ }
186
+ this.pendingCandidate = null;
187
+ this.activeRelay = candidate;
188
+ this.reconnectAttempt = 0;
189
+ candidate.on("close", () => this.onRelayClose(candidate));
190
+ this.deps.onConnected(candidate);
191
+ this.deps.onStateChange();
192
+ }
193
+ onRelayClose(closed) {
194
+ if (closed !== this.activeRelay || this.currentState === "idle")
195
+ return;
196
+ this.activeRelay = null;
197
+ this.deps.onDisconnected();
198
+ this.deps.onStateChange();
199
+ if (this.currentRelayUrl)
200
+ this.scheduleReconnect(this.currentRelayUrl);
201
+ }
202
+ handleConnectionFailure(expectedGeneration, url, ctx, error) {
203
+ if (!this.isReconnectCurrent(expectedGeneration))
204
+ return;
205
+ this.notify(ctx, `[remote-pi] Relay unavailable; reconnecting in background: ${String(error)}`, "warning");
206
+ this.deps.onStateChange();
207
+ this.scheduleReconnect(url);
208
+ }
209
+ clearCandidate(candidate, close) {
210
+ if (this.pendingCandidate !== candidate)
211
+ return;
212
+ this.pendingCandidate = null;
213
+ if (close)
214
+ this.closeCandidateOnce(candidate);
215
+ }
216
+ closeCandidateOnce(candidate) {
217
+ if (this.closedCandidates.has(candidate))
218
+ return;
219
+ this.closedCandidates.add(candidate);
220
+ candidate.close();
221
+ }
222
+ clearInitialAttempt(attempt, cancel) {
223
+ if (this.initialAttempt === attempt)
224
+ this.initialAttempt = null;
225
+ if (this.cancelInitialAttempt === cancel)
226
+ this.cancelInitialAttempt = null;
227
+ }
228
+ isOperationCurrent(expected) {
229
+ return !this.isDisposed && this.generation === expected;
230
+ }
231
+ isReconnectCurrent(expected) {
232
+ return this.currentState === "started" && this.isOperationCurrent(expected);
233
+ }
234
+ notify(ctx, message, kind = "info") {
235
+ try {
236
+ ctx.ui.notify(message, kind);
237
+ }
238
+ catch { /* stale context */ }
239
+ }
240
+ }
241
+ //# sourceMappingURL=relay_lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relay_lifecycle.js","sourceRoot":"","sources":["../../src/runtime/relay_lifecycle.ts"],"names":[],"mappings":"AAsBA,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAE7E,MAAM,OAAO,cAAc;IAcI;IAbrB,WAAW,GAAuB,IAAI,CAAC;IACvC,gBAAgB,GAAuB,IAAI,CAAC;IAC5C,eAAe,GAAkB,IAAI,CAAC;IACtC,QAAQ,GAA0B,IAAI,CAAC;IACvC,cAAc,GAAuC,IAAI,CAAC;IAC1D,oBAAoB,GAAwB,IAAI,CAAC;IACjD,cAAc,GAAyC,IAAI,CAAC;IAC5D,gBAAgB,GAAG,CAAC,CAAC;IACrB,UAAU,GAAG,CAAC,CAAC;IACf,UAAU,GAAG,KAAK,CAAC;IACnB,YAAY,GAAgB,MAAM,CAAC;IAC1B,gBAAgB,GAAG,IAAI,OAAO,EAAe,CAAC;IAE/D,YAA6B,IAAgC;QAAhC,SAAI,GAAJ,IAAI,CAA4B;IAAG,CAAC;IAEjE,IAAI,KAAK,KAAkB,OAAO,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC;IACtD,IAAI,KAAK,KAAyB,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5D,IAAI,QAAQ,KAAoB,OAAO,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAC9D,IAAI,OAAO,KAA4B,OAAO,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC9D,IAAI,QAAQ,KAAc,OAAO,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;IACnD,IAAI,mBAAmB,KAAc,OAAO,IAAI,CAAC,cAAc,KAAK,IAAI,CAAC,CAAC,CAAC;IAC3E,IAAI,MAAM;QACR,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM;YAAE,OAAO,cAAc,CAAC;QACxD,OAAO,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,cAAc,CAAC;IACnE,CAAC;IAED,SAAS,CAAC,SAAsB;QAC9B,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,CAAC;IAC/F,CAAC;IAED,WAAW,CAAC,KAAc;QACxB,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAsB;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC;QACrC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM,EAAE,CAAC;YACjC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,KAAK,cAAc;gBAC5C,CAAC,CAAC,kDAAkD;gBACpD,CAAC,CAAC,gCAAgC,CAAC;YACrC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;YACrC,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,MAAmB,CAAC;QACxB,MAAM,YAAY,GAAG,IAAI,OAAO,CAAqB,CAAC,OAAO,EAAE,EAAE;YAC/D,MAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,WAAoB,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC;QACzF,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC;QAC9B,IAAI,CAAC,oBAAoB,GAAG,MAAM,CAAC;QACnC,KAAK,SAAS,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACtC,KAAK,OAAO,CAAC,IAAI,CACf,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,EAC/C,GAAG,EAAE,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,EAAE,MAAM,CAAC,CAChD,CAAC;QACF,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7D,CAAC;IAED,IAAI;QACF,IAAI,CAAC,UAAU,IAAI,CAAC,CAAC;QACrB,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YAClC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAE1B,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC;QACtC,MAAM,MAAM,GAAG,IAAI,CAAC,oBAAoB,CAAC;QACzC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;QAE3B,MAAM,EAAE,EAAE,CAAC;QACX,IAAI,MAAM;YAAE,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,OAAO,IAAI,OAAO,KAAK,MAAM;YAAE,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC;QACpE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,GAAsB;QAC/C,MAAM,kBAAkB,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;QAC7C,IAAI,QAAwB,CAAC;QAC7B,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAC5C,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;gBAAE,OAAO;YACzD,IAAI,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,GAAG,CAAC;gBAAE,OAAO;YACtD,MAAM,KAAK,CAAC;QACd,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEzD,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;QAC/C,IAAI,CAAC,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC;QACtC,IAAI,CAAC,YAAY,GAAG,SAAS,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;QAE3D,IAAI,OAA2B,CAAC;QAChC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACzD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEzD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACnE,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,uBAAuB,CAAC,kBAAkB,EAAE,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC7E,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxF,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC1B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,iBAAiB,CAAC,GAAW;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,YAAY,KAAK,SAAS;YAAE,OAAO;QACjF,IAAI,IAAI,CAAC,cAAc;YAAE,YAAY,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3D,MAAM,kBAAkB,GAAG,EAAE,IAAI,CAAC,UAAU,CAAC;QAC7C,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,qBAAqB,CAAC,MAAM,GAAG,CAAC,CAAC,CAAE,CAAC;QAC1G,IAAI,CAAC,cAAc,GAAG,UAAU,CAAC,GAAG,EAAE;YACpC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;gBAAE,OAAO;YACzD,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;QAC/C,CAAC,EAAE,KAAK,CAAC,CAAC;IACZ,CAAC;IAEO,KAAK,CAAC,SAAS,CAAC,GAAW,EAAE,kBAA0B;QAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC/B,IAAI,CAAC,QAAQ,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QAEtE,IAAI,OAA2B,CAAC;QAChC,IAAI,CAAC;YACH,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO;QAEvF,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACxD,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;QAClC,IAAI,CAAC;YACH,MAAM,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QACnC,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,cAAc,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;YACrC,IAAI,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,EAAE,CAAC;gBAChD,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxF,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;YACnC,OAAO;QACT,CAAC;QAED,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,SAAS,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;QAC1B,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1D,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;IAC5B,CAAC;IAEO,YAAY,CAAC,MAAmB;QACtC,IAAI,MAAM,KAAK,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,YAAY,KAAK,MAAM;YAAE,OAAO;QACxE,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,IAAI,CAAC,eAAe;YAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;IACzE,CAAC;IAEO,uBAAuB,CAAC,kBAA0B,EAAE,GAAW,EAAE,GAAsB,EAAE,KAAc;QAC7G,IAAI,CAAC,IAAI,CAAC,kBAAkB,CAAC,kBAAkB,CAAC;YAAE,OAAO;QACzD,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,8DAA8D,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QAC3G,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;QAC1B,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;IAC9B,CAAC;IAEO,cAAc,CAAC,SAAsB,EAAE,KAAc;QAC3D,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS;YAAE,OAAO;QAChD,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QAC7B,IAAI,KAAK;YAAE,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC;IAEO,kBAAkB,CAAC,SAAsB;QAC/C,IAAI,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC;YAAE,OAAO;QACjD,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrC,SAAS,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;IAEO,mBAAmB,CAAC,OAAoC,EAAE,MAAkB;QAClF,IAAI,IAAI,CAAC,cAAc,KAAK,OAAO;YAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAChE,IAAI,IAAI,CAAC,oBAAoB,KAAK,MAAM;YAAE,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC7E,CAAC;IAEO,kBAAkB,CAAC,QAAgB;QACzC,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC;IAC1D,CAAC;IAEO,kBAAkB,CAAC,QAAgB;QACzC,OAAO,IAAI,CAAC,YAAY,KAAK,SAAS,IAAI,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC9E,CAAC;IAEO,MAAM,CAAC,GAAsB,EAAE,OAAe,EAAE,OAA2B,MAAM;QACvF,IAAI,CAAC;YAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,mBAAmB,CAAC,CAAC;IACrE,CAAC;CACF"}
@@ -33,15 +33,28 @@ export declare class RelayClient extends EventEmitter {
33
33
  private ws;
34
34
  private lastActivityAt;
35
35
  private livenessTimer;
36
+ private livenessSocket;
37
+ private pendingConnection;
38
+ private pendingMessage;
36
39
  constructor(url: string, keypair: Ed25519Keypair);
37
40
  connect(options?: ConnectOptions): Promise<void>;
38
41
  isOpen(): boolean;
39
42
  send(line: string): void;
40
43
  sendControl(frame: object): void;
41
44
  close(): void;
45
+ private finishConnection;
42
46
  private startLiveness;
43
47
  private stopLiveness;
44
48
  private authenticate;
45
49
  private nextMessage;
50
+ private isPendingConnection;
51
+ private assertPendingConnection;
52
+ private resolvePendingConnection;
53
+ private abortPendingConnection;
54
+ private clearConnectTimer;
55
+ private clearPendingConnectionListeners;
56
+ private cancelPendingMessage;
57
+ private reportError;
58
+ private asError;
46
59
  private rawSend;
47
60
  }
@@ -1,6 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import WebSocket from "ws";
3
3
  import { ed25519Sign } from "../pairing/crypto.js";
4
+ const CONNECT_TIMEOUT_MS = 10_000;
4
5
  const AUTH_TIMEOUT_MS = 5_000;
5
6
  const LIVENESS_TIMEOUT_MS = 70_000;
6
7
  const LIVENESS_CHECK_MS = 20_000;
@@ -11,43 +12,36 @@ export class RelayClient extends EventEmitter {
11
12
  ws = null;
12
13
  lastActivityAt = 0;
13
14
  livenessTimer = null;
15
+ livenessSocket = null;
16
+ pendingConnection = null;
17
+ pendingMessage = null;
14
18
  constructor(url, keypair) {
15
19
  super();
16
20
  this.url = url;
17
21
  this.keypair = keypair;
18
22
  }
19
- async connect(options = { role: "owner" }) {
23
+ connect(options = { role: "owner" }) {
24
+ this.close();
20
25
  return new Promise((resolve, reject) => {
21
26
  const ws = new WebSocket(this.url);
27
+ const onEarlyClose = () => this.abortPendingConnection(ws, new Error("relay connection closed"), false);
28
+ const onOpen = () => {
29
+ this.clearConnectTimer(ws);
30
+ void this.finishConnection(ws, options);
31
+ };
22
32
  this.ws = ws;
23
- ws.on("error", (err) => reject(err));
24
- ws.on("open", async () => {
25
- try {
26
- await this.authenticate(ws, options);
27
- this.lastActivityAt = Date.now();
28
- ws.on("message", (raw) => {
29
- this.lastActivityAt = Date.now();
30
- const text = Buffer.isBuffer(raw) ? raw.toString() : String(raw);
31
- for (const line of text.split("\n")) {
32
- const trimmed = line.trim();
33
- if (trimmed)
34
- this.emit("message", trimmed);
35
- }
36
- });
37
- ws.on("ping", () => { this.lastActivityAt = Date.now(); });
38
- ws.on("pong", () => { this.lastActivityAt = Date.now(); });
39
- ws.on("close", () => {
40
- this.stopLiveness();
41
- this.emit("close");
42
- });
43
- this.startLiveness(ws);
44
- resolve();
45
- }
46
- catch (err) {
47
- ws.terminate();
48
- reject(err);
49
- }
33
+ const pending = { ws, resolve, reject, onEarlyClose, onOpen, connectTimer: null };
34
+ this.pendingConnection = pending;
35
+ pending.connectTimer = setTimeout(() => this.abortPendingConnection(ws, new Error("relay connection timeout"), true), CONNECT_TIMEOUT_MS);
36
+ ws.on("error", (error) => {
37
+ if (this.ws !== ws)
38
+ return;
39
+ this.reportError(error);
40
+ if (this.isPendingConnection(ws))
41
+ this.abortPendingConnection(ws, error, true);
50
42
  });
43
+ ws.once("close", onEarlyClose);
44
+ ws.once("open", onOpen);
51
45
  });
52
46
  }
53
47
  isOpen() {
@@ -65,26 +59,77 @@ export class RelayClient extends EventEmitter {
65
59
  this.ws.send(JSON.stringify(frame));
66
60
  }
67
61
  close() {
68
- this.stopLiveness();
69
- this.ws?.close();
62
+ const ws = this.ws;
63
+ if (!ws)
64
+ return;
65
+ this.stopLiveness(ws);
66
+ if (this.isPendingConnection(ws)) {
67
+ this.abortPendingConnection(ws, new Error("relay connection closed"), true);
68
+ return;
69
+ }
70
70
  this.ws = null;
71
+ ws.close();
72
+ }
73
+ async finishConnection(ws, options) {
74
+ if (!this.isPendingConnection(ws))
75
+ return;
76
+ try {
77
+ await this.authenticate(ws, options);
78
+ if (!this.isPendingConnection(ws))
79
+ return;
80
+ this.lastActivityAt = Date.now();
81
+ const pending = this.pendingConnection;
82
+ if (!pending || pending.ws !== ws)
83
+ return;
84
+ ws.removeListener("close", pending.onEarlyClose);
85
+ ws.on("message", (raw) => {
86
+ if (this.ws !== ws)
87
+ return;
88
+ this.lastActivityAt = Date.now();
89
+ const text = Buffer.isBuffer(raw) ? raw.toString() : String(raw);
90
+ for (const line of text.split("\n")) {
91
+ const trimmed = line.trim();
92
+ if (trimmed)
93
+ this.emit("message", trimmed);
94
+ }
95
+ });
96
+ ws.on("ping", () => { if (this.ws === ws)
97
+ this.lastActivityAt = Date.now(); });
98
+ ws.on("pong", () => { if (this.ws === ws)
99
+ this.lastActivityAt = Date.now(); });
100
+ ws.on("close", () => {
101
+ if (this.ws !== ws)
102
+ return;
103
+ this.stopLiveness(ws);
104
+ this.ws = null;
105
+ this.emit("close");
106
+ });
107
+ this.startLiveness(ws);
108
+ this.resolvePendingConnection(ws);
109
+ }
110
+ catch (error) {
111
+ this.abortPendingConnection(ws, this.asError(error), true);
112
+ }
71
113
  }
72
114
  startLiveness(ws) {
73
115
  this.stopLiveness();
116
+ this.livenessSocket = ws;
74
117
  this.livenessTimer = setInterval(() => {
75
118
  if (Date.now() - this.lastActivityAt > LIVENESS_TIMEOUT_MS) {
76
- this.stopLiveness();
119
+ this.stopLiveness(ws);
77
120
  ws.terminate();
78
121
  }
79
122
  }, LIVENESS_CHECK_MS);
80
123
  }
81
- stopLiveness() {
82
- if (!this.livenessTimer)
124
+ stopLiveness(ws) {
125
+ if (!this.livenessTimer || (ws && this.livenessSocket !== ws))
83
126
  return;
84
127
  clearInterval(this.livenessTimer);
85
128
  this.livenessTimer = null;
129
+ this.livenessSocket = null;
86
130
  }
87
131
  async authenticate(ws, options) {
132
+ this.assertPendingConnection(ws);
88
133
  const pubkey = Buffer.from(this.keypair.publicKey).toString("base64");
89
134
  const hello = options.role === "host"
90
135
  ? {
@@ -100,6 +145,7 @@ export class RelayClient extends EventEmitter {
100
145
  : { type: "hello", protocol_version: 2, role: "owner", pubkey };
101
146
  this.rawSend(ws, JSON.stringify(hello));
102
147
  const challengeRaw = await this.nextMessage(ws);
148
+ this.assertPendingConnection(ws);
103
149
  let challenge;
104
150
  try {
105
151
  challenge = JSON.parse(challengeRaw);
@@ -119,17 +165,96 @@ export class RelayClient extends EventEmitter {
119
165
  type: "auth",
120
166
  sig: Buffer.from(ed25519Sign(this.keypair.secretKey, nonce)).toString("base64"),
121
167
  };
168
+ this.assertPendingConnection(ws);
122
169
  this.rawSend(ws, JSON.stringify(auth));
123
170
  }
124
171
  nextMessage(ws) {
125
172
  return new Promise((resolve, reject) => {
126
- const timer = setTimeout(() => reject(new Error("relay auth timeout")), AUTH_TIMEOUT_MS);
127
- ws.once("message", (raw) => {
128
- clearTimeout(timer);
129
- resolve(Buffer.isBuffer(raw) ? raw.toString() : String(raw));
130
- });
173
+ let timer = null;
174
+ let settled = false;
175
+ const cleanup = () => {
176
+ if (timer)
177
+ clearTimeout(timer);
178
+ ws.removeListener("message", onMessage);
179
+ ws.removeListener("error", onError);
180
+ ws.removeListener("close", onClose);
181
+ if (this.pendingMessage?.ws === ws)
182
+ this.pendingMessage = null;
183
+ };
184
+ const finish = (settle) => {
185
+ if (settled)
186
+ return;
187
+ settled = true;
188
+ cleanup();
189
+ settle();
190
+ };
191
+ const onMessage = (raw) => finish(() => resolve(Buffer.isBuffer(raw) ? raw.toString() : String(raw)));
192
+ const onError = (error) => finish(() => reject(error));
193
+ const onClose = () => finish(() => reject(new Error("relay connection closed")));
194
+ timer = setTimeout(() => finish(() => reject(new Error("relay auth timeout"))), AUTH_TIMEOUT_MS);
195
+ ws.once("message", onMessage);
196
+ ws.once("error", onError);
197
+ ws.once("close", onClose);
198
+ this.pendingMessage = { ws, cancel: (error) => finish(() => reject(error)) };
131
199
  });
132
200
  }
201
+ isPendingConnection(ws) {
202
+ return this.ws === ws && this.pendingConnection?.ws === ws;
203
+ }
204
+ assertPendingConnection(ws) {
205
+ if (!this.isPendingConnection(ws))
206
+ throw new Error("relay connection closed");
207
+ }
208
+ resolvePendingConnection(ws) {
209
+ const pending = this.pendingConnection;
210
+ if (!pending || pending.ws !== ws)
211
+ return;
212
+ this.pendingConnection = null;
213
+ this.clearPendingConnectionListeners(pending);
214
+ pending.resolve();
215
+ }
216
+ abortPendingConnection(ws, error, terminate) {
217
+ const pending = this.pendingConnection;
218
+ if (!pending || pending.ws !== ws)
219
+ return;
220
+ this.pendingConnection = null;
221
+ this.clearPendingConnectionListeners(pending);
222
+ this.cancelPendingMessage(ws, error);
223
+ this.stopLiveness(ws);
224
+ if (this.ws === ws)
225
+ this.ws = null;
226
+ pending.reject(error);
227
+ if (terminate)
228
+ ws.terminate();
229
+ }
230
+ clearConnectTimer(ws) {
231
+ const pending = this.pendingConnection;
232
+ if (!pending || pending.ws !== ws || !pending.connectTimer)
233
+ return;
234
+ clearTimeout(pending.connectTimer);
235
+ pending.connectTimer = null;
236
+ }
237
+ clearPendingConnectionListeners(pending) {
238
+ if (pending.connectTimer)
239
+ clearTimeout(pending.connectTimer);
240
+ pending.connectTimer = null;
241
+ pending.ws.removeListener("close", pending.onEarlyClose);
242
+ pending.ws.removeListener("open", pending.onOpen);
243
+ }
244
+ cancelPendingMessage(ws, error) {
245
+ const pending = this.pendingMessage;
246
+ if (!pending || pending.ws !== ws)
247
+ return;
248
+ this.pendingMessage = null;
249
+ pending.cancel(error);
250
+ }
251
+ reportError(error) {
252
+ if (this.listenerCount("error") > 0)
253
+ this.emit("error", error);
254
+ }
255
+ asError(error) {
256
+ return error instanceof Error ? error : new Error(String(error));
257
+ }
133
258
  rawSend(ws, data) {
134
259
  ws.send(data);
135
260
  }