echoclaw-relay-agent 0.8.1 → 0.8.2
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.
- package/dist/RelayAgent.d.ts +2 -0
- package/dist/RelayAgent.js +44 -12
- package/dist/RelayClient.d.ts +3 -0
- package/dist/RelayClient.js +51 -1
- package/dist/relay/PairingProtocol.d.ts +3 -0
- package/dist/relay/PairingProtocol.js +17 -0
- package/package.json +1 -1
package/dist/RelayAgent.d.ts
CHANGED
package/dist/RelayAgent.js
CHANGED
|
@@ -91,6 +91,18 @@ export class RelayAgent extends EventEmitter {
|
|
|
91
91
|
writable: true,
|
|
92
92
|
value: false
|
|
93
93
|
});
|
|
94
|
+
Object.defineProperty(this, "_paired", {
|
|
95
|
+
enumerable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
writable: true,
|
|
98
|
+
value: false
|
|
99
|
+
});
|
|
100
|
+
Object.defineProperty(this, "_activePairing", {
|
|
101
|
+
enumerable: true,
|
|
102
|
+
configurable: true,
|
|
103
|
+
writable: true,
|
|
104
|
+
value: null
|
|
105
|
+
});
|
|
94
106
|
Object.defineProperty(this, "config", {
|
|
95
107
|
enumerable: true,
|
|
96
108
|
configurable: true,
|
|
@@ -194,6 +206,9 @@ export class RelayAgent extends EventEmitter {
|
|
|
194
206
|
/** Gracefully stop the agent. */
|
|
195
207
|
async stop() {
|
|
196
208
|
this._stopped = true;
|
|
209
|
+
this._paired = false;
|
|
210
|
+
this._activePairing?.abort();
|
|
211
|
+
this._activePairing = null;
|
|
197
212
|
this.gatewayManager?.stop();
|
|
198
213
|
this.transport?.disconnect();
|
|
199
214
|
this.transport = null;
|
|
@@ -268,6 +283,8 @@ export class RelayAgent extends EventEmitter {
|
|
|
268
283
|
this.sessionId = result.sessionId;
|
|
269
284
|
this.pairingCode = result.pairingCode;
|
|
270
285
|
this.frameCrypto = new FrameCrypto(result.sessionKey);
|
|
286
|
+
this._paired = true;
|
|
287
|
+
this._activePairing = null;
|
|
271
288
|
// Persist session for future auto-resume
|
|
272
289
|
await this.sessionStore.save({
|
|
273
290
|
pairingCode: result.pairingCode,
|
|
@@ -355,18 +372,33 @@ export class RelayAgent extends EventEmitter {
|
|
|
355
372
|
this.emit('error', err);
|
|
356
373
|
});
|
|
357
374
|
this.transport.on('open', () => {
|
|
358
|
-
if (this.
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
375
|
+
if (this._paired && this.transport) {
|
|
376
|
+
// Transport reconnected after prior successful pairing —
|
|
377
|
+
// redo ECDH because desktop may have new ephemeral keys.
|
|
378
|
+
// Cancel any in-flight pairing and invalidate old crypto state.
|
|
379
|
+
this._activePairing?.abort();
|
|
380
|
+
this.sessionKey = null;
|
|
381
|
+
this.frameCrypto = null;
|
|
382
|
+
this.setStatus('connecting');
|
|
383
|
+
const pairing = new PairingProtocol(this.transport);
|
|
384
|
+
this._activePairing = pairing;
|
|
385
|
+
pairing.pairAsAgent()
|
|
386
|
+
.then(result => {
|
|
387
|
+
this.onPaired(result);
|
|
388
|
+
// V2: Reattach gateway callbacks after reconnect
|
|
389
|
+
if (this.gatewayManager) {
|
|
390
|
+
this.gatewayManager.onReconnected(async (r) => {
|
|
391
|
+
try {
|
|
392
|
+
await this.send(r);
|
|
393
|
+
}
|
|
394
|
+
catch { /* send may fail */ }
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
.catch(err => {
|
|
399
|
+
this._activePairing = null;
|
|
400
|
+
this.emit('error', err);
|
|
401
|
+
});
|
|
370
402
|
}
|
|
371
403
|
});
|
|
372
404
|
}
|
package/dist/RelayClient.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export declare class RelayClient extends EventEmitter {
|
|
|
34
34
|
private _status;
|
|
35
35
|
private _connecting;
|
|
36
36
|
private _stopped;
|
|
37
|
+
private _paired;
|
|
38
|
+
private _needsRekey;
|
|
39
|
+
private _activePairing;
|
|
37
40
|
private readonly config;
|
|
38
41
|
private readonly sessionStore;
|
|
39
42
|
private readonly subscribers;
|
package/dist/RelayClient.js
CHANGED
|
@@ -79,6 +79,24 @@ export class RelayClient extends EventEmitter {
|
|
|
79
79
|
writable: true,
|
|
80
80
|
value: false
|
|
81
81
|
});
|
|
82
|
+
Object.defineProperty(this, "_paired", {
|
|
83
|
+
enumerable: true,
|
|
84
|
+
configurable: true,
|
|
85
|
+
writable: true,
|
|
86
|
+
value: false
|
|
87
|
+
});
|
|
88
|
+
Object.defineProperty(this, "_needsRekey", {
|
|
89
|
+
enumerable: true,
|
|
90
|
+
configurable: true,
|
|
91
|
+
writable: true,
|
|
92
|
+
value: false
|
|
93
|
+
});
|
|
94
|
+
Object.defineProperty(this, "_activePairing", {
|
|
95
|
+
enumerable: true,
|
|
96
|
+
configurable: true,
|
|
97
|
+
writable: true,
|
|
98
|
+
value: null
|
|
99
|
+
});
|
|
82
100
|
Object.defineProperty(this, "config", {
|
|
83
101
|
enumerable: true,
|
|
84
102
|
configurable: true,
|
|
@@ -178,6 +196,10 @@ export class RelayClient extends EventEmitter {
|
|
|
178
196
|
/** Disconnect from the relay. */
|
|
179
197
|
async disconnect() {
|
|
180
198
|
this._stopped = true;
|
|
199
|
+
this._paired = false;
|
|
200
|
+
this._needsRekey = false;
|
|
201
|
+
this._activePairing?.abort();
|
|
202
|
+
this._activePairing = null;
|
|
181
203
|
this.transport?.disconnect();
|
|
182
204
|
this.transport = null;
|
|
183
205
|
this.sessionKey = null;
|
|
@@ -250,6 +272,8 @@ export class RelayClient extends EventEmitter {
|
|
|
250
272
|
this.sessionId = result.sessionId;
|
|
251
273
|
this.pairingCode = result.pairingCode;
|
|
252
274
|
this.frameCrypto = new FrameCrypto(result.sessionKey);
|
|
275
|
+
this._paired = true;
|
|
276
|
+
this._activePairing = null;
|
|
253
277
|
// Update transport URL so auto-reconnect uses ?resume=sessionId
|
|
254
278
|
// instead of the original /client/connect (which would create a new session)
|
|
255
279
|
if (this.transport) {
|
|
@@ -319,8 +343,34 @@ export class RelayClient extends EventEmitter {
|
|
|
319
343
|
this.transport.on('error', (err) => {
|
|
320
344
|
this.emit('error', err);
|
|
321
345
|
});
|
|
346
|
+
// Listen for server CLOSE messages (e.g. AGENT_RESTARTED) to know re-ECDH is needed.
|
|
347
|
+
// Without this, normal network drops would unnecessarily trigger re-ECDH.
|
|
348
|
+
this.transport.on('message', (msg) => {
|
|
349
|
+
if (msg.type === 'CLOSE' && msg.sender_role === 'server') {
|
|
350
|
+
this._needsRekey = true;
|
|
351
|
+
}
|
|
352
|
+
});
|
|
322
353
|
this.transport.on('open', () => {
|
|
323
|
-
if (this.
|
|
354
|
+
if (this._paired && this._needsRekey && this.pairingCode && this.transport) {
|
|
355
|
+
// Server requested re-ECDH (e.g. AGENT_RESTARTED) —
|
|
356
|
+
// cancel any in-flight pairing and start fresh handshake.
|
|
357
|
+
// Invalidate old crypto state to prevent sending with stale keys.
|
|
358
|
+
this._needsRekey = false;
|
|
359
|
+
this._activePairing?.abort();
|
|
360
|
+
this.sessionKey = null;
|
|
361
|
+
this.frameCrypto = null;
|
|
362
|
+
this.setStatus('connecting');
|
|
363
|
+
const pairing = new PairingProtocol(this.transport);
|
|
364
|
+
this._activePairing = pairing;
|
|
365
|
+
pairing.pairAsClient(this.pairingCode)
|
|
366
|
+
.then(result => this.onPaired(result, 'resumed'))
|
|
367
|
+
.catch(err => {
|
|
368
|
+
this._activePairing = null;
|
|
369
|
+
this.emit('error', err);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else if (this.sessionKey) {
|
|
373
|
+
// Normal reconnect (network drop) — old session key still valid
|
|
324
374
|
this.setStatus('connected');
|
|
325
375
|
this.emit('connected');
|
|
326
376
|
}
|
|
@@ -24,7 +24,10 @@ export interface PairingResult {
|
|
|
24
24
|
export declare class PairingProtocol extends EventEmitter {
|
|
25
25
|
private readonly transport;
|
|
26
26
|
private keyPair;
|
|
27
|
+
private _cleanup;
|
|
27
28
|
constructor(transport: RelayTransport);
|
|
29
|
+
/** Abort an in-flight pairing attempt. Safe to call multiple times. */
|
|
30
|
+
abort(): void;
|
|
28
31
|
/**
|
|
29
32
|
* Agent-side pairing: register with relay, wait for client to pair.
|
|
30
33
|
* Returns the E2E session key once pairing is complete.
|
|
@@ -31,6 +31,17 @@ export class PairingProtocol extends EventEmitter {
|
|
|
31
31
|
writable: true,
|
|
32
32
|
value: null
|
|
33
33
|
});
|
|
34
|
+
Object.defineProperty(this, "_cleanup", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: null
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Abort an in-flight pairing attempt. Safe to call multiple times. */
|
|
42
|
+
abort() {
|
|
43
|
+
this._cleanup?.();
|
|
44
|
+
this._cleanup = null;
|
|
34
45
|
}
|
|
35
46
|
/**
|
|
36
47
|
* Agent-side pairing: register with relay, wait for client to pair.
|
|
@@ -52,7 +63,10 @@ export class PairingProtocol extends EventEmitter {
|
|
|
52
63
|
resolved = true;
|
|
53
64
|
clearTimeout(timeout);
|
|
54
65
|
this.transport.removeListener('message', onMessage);
|
|
66
|
+
this._cleanup = null;
|
|
55
67
|
};
|
|
68
|
+
// Expose cleanup for external abort()
|
|
69
|
+
this._cleanup = cleanup;
|
|
56
70
|
const onMessage = async (msg) => {
|
|
57
71
|
if (resolved)
|
|
58
72
|
return;
|
|
@@ -124,7 +138,10 @@ export class PairingProtocol extends EventEmitter {
|
|
|
124
138
|
clearTimeout(timeout);
|
|
125
139
|
this.transport.removeListener('message', onMessage);
|
|
126
140
|
this.transport.removeListener('open', onOpen);
|
|
141
|
+
this._cleanup = null;
|
|
127
142
|
};
|
|
143
|
+
// Expose cleanup for external abort()
|
|
144
|
+
this._cleanup = cleanup;
|
|
128
145
|
const sendPubkey = () => {
|
|
129
146
|
if (sentHello)
|
|
130
147
|
return;
|
package/package.json
CHANGED