echoclaw-relay-agent 0.19.4 → 0.20.0
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 +1 -0
- package/dist/RelayAgent.js +32 -6
- package/dist/RelayClient.js +27 -9
- package/dist/chat/ChatHandler.js +2 -0
- package/dist/relay/PairingProtocol.js +21 -11
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/RelayAgent.d.ts
CHANGED
package/dist/RelayAgent.js
CHANGED
|
@@ -81,6 +81,12 @@ export class RelayAgent extends EventEmitter {
|
|
|
81
81
|
writable: true,
|
|
82
82
|
value: null
|
|
83
83
|
});
|
|
84
|
+
Object.defineProperty(this, "_dataHandlerInner", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: null
|
|
89
|
+
});
|
|
84
90
|
Object.defineProperty(this, "_stopped", {
|
|
85
91
|
enumerable: true,
|
|
86
92
|
configurable: true,
|
|
@@ -286,9 +292,11 @@ export class RelayAgent extends EventEmitter {
|
|
|
286
292
|
this.sessionKey = null;
|
|
287
293
|
this.frameCrypto = null;
|
|
288
294
|
this._dataHandler = null;
|
|
295
|
+
this._dataHandlerInner = null;
|
|
289
296
|
this._rekeyListener = null;
|
|
290
297
|
this._proactiveKeyPair = null;
|
|
291
298
|
this._rekeyInFlight = false;
|
|
299
|
+
this._identityExchanged = false;
|
|
292
300
|
this.setStatus('disconnected');
|
|
293
301
|
}
|
|
294
302
|
// ── Private ────────────────────────────────────────────────
|
|
@@ -552,7 +560,16 @@ export class RelayAgent extends EventEmitter {
|
|
|
552
560
|
this.emit('error', ixErr);
|
|
553
561
|
}
|
|
554
562
|
}
|
|
555
|
-
// Persist session
|
|
563
|
+
// Persist session — merge with existing to preserve identity data on re-key.
|
|
564
|
+
// Fresh pairing writes clean; re-key only updates sessionKey/sessionId.
|
|
565
|
+
if (!freshPairing) {
|
|
566
|
+
const existing = await this.sessionStore.load();
|
|
567
|
+
if (existing) {
|
|
568
|
+
sessionData.identityDeviceId = sessionData.identityDeviceId || existing.identityDeviceId;
|
|
569
|
+
sessionData.peerPublicKeyRaw = sessionData.peerPublicKeyRaw || existing.peerPublicKeyRaw;
|
|
570
|
+
sessionData.bindingSecret = sessionData.bindingSecret || existing.bindingSecret;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
556
573
|
await this.sessionStore.save(sessionData);
|
|
557
574
|
// Re-install re-key listener so future desktop HELLOs are handled.
|
|
558
575
|
// Without this, after a re-key, the listener references the old transport
|
|
@@ -651,11 +668,16 @@ export class RelayAgent extends EventEmitter {
|
|
|
651
668
|
setupDataHandler() {
|
|
652
669
|
if (!this.transport)
|
|
653
670
|
return;
|
|
654
|
-
//
|
|
655
|
-
|
|
656
|
-
|
|
671
|
+
// Single permanent listener pattern: install once, swap inner handler on re-key.
|
|
672
|
+
// Prevents listener accumulation from multiple onPaired() calls.
|
|
673
|
+
if (!this._dataHandler) {
|
|
674
|
+
this._dataHandler = (msg) => {
|
|
675
|
+
if (this._dataHandlerInner)
|
|
676
|
+
this._dataHandlerInner(msg);
|
|
677
|
+
};
|
|
678
|
+
this.transport.on('message', this._dataHandler);
|
|
657
679
|
}
|
|
658
|
-
this.
|
|
680
|
+
this._dataHandlerInner = async (msg) => {
|
|
659
681
|
if (msg.type !== 'DATA' || !msg.iv || !msg.payload || !this.frameCrypto)
|
|
660
682
|
return;
|
|
661
683
|
try {
|
|
@@ -694,7 +716,6 @@ export class RelayAgent extends EventEmitter {
|
|
|
694
716
|
this.emit('error', err);
|
|
695
717
|
}
|
|
696
718
|
};
|
|
697
|
-
this.transport.on('message', this._dataHandler);
|
|
698
719
|
}
|
|
699
720
|
setupTransportEvents() {
|
|
700
721
|
if (!this.transport)
|
|
@@ -703,6 +724,11 @@ export class RelayAgent extends EventEmitter {
|
|
|
703
724
|
if (!this._stopped) {
|
|
704
725
|
this.setStatus('reconnecting');
|
|
705
726
|
this.emit('disconnected', { code: 0, reason: 'transport closed' });
|
|
727
|
+
// Reset re-key state on transport close — new connection = fresh re-key.
|
|
728
|
+
// Prevents deadlock where old _rekeyInFlight blocks new connection's re-key.
|
|
729
|
+
this._rekeyInFlight = false;
|
|
730
|
+
this._activePairing?.abort();
|
|
731
|
+
this._activePairing = null;
|
|
706
732
|
// V2: Notify gateway of disconnect (starts grace period)
|
|
707
733
|
this.gatewayManager?.onDisconnected();
|
|
708
734
|
}
|
package/dist/RelayClient.js
CHANGED
|
@@ -685,21 +685,39 @@ export class RelayClient extends EventEmitter {
|
|
|
685
685
|
this.transport.on('connect_timeout', (timeoutMs) => {
|
|
686
686
|
this.emit('error', Object.assign(new Error(`Connection timed out after ${timeoutMs / 1000}s`), { code: 'CONNECT_TIMEOUT' }));
|
|
687
687
|
});
|
|
688
|
-
// Listen for server CLOSE messages
|
|
688
|
+
// Listen for server CLOSE/STATUS messages — whitelist dispatch.
|
|
689
|
+
// Only specific payloads trigger re-key. Others dispatch to handlers.
|
|
690
|
+
const REKEY_TRIGGERS = new Set(['AGENT_RESTARTED', 'FORCE_REKEY']);
|
|
691
|
+
const PEER_STATUS = new Set(['AGENT_WARNING', 'AGENT_ONLINE', 'DESKTOP_DISCONNECTED']);
|
|
692
|
+
const SESSION_FATAL = new Set(['SESSION_NOT_FOUND', 'INVALID_SESSION']);
|
|
689
693
|
this.transport.on('message', (msg) => {
|
|
690
|
-
if (msg.type === 'CLOSE' && msg.sender_role === 'server') {
|
|
691
|
-
const payload = msg.payload;
|
|
692
|
-
if (payload
|
|
693
|
-
|
|
694
|
+
if ((msg.type === 'CLOSE' || msg.type === 'STATUS') && msg.sender_role === 'server') {
|
|
695
|
+
const payload = typeof msg.payload === 'string' ? msg.payload : '';
|
|
696
|
+
if (REKEY_TRIGGERS.has(payload)) {
|
|
697
|
+
this._needsRekey = true;
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (PEER_STATUS.has(payload)) {
|
|
694
701
|
this.emit('peer_status', {
|
|
695
|
-
status: payload === '
|
|
702
|
+
status: payload === 'AGENT_ONLINE' ? 'online' : 'warning',
|
|
696
703
|
timestamp: msg.timestamp,
|
|
697
704
|
});
|
|
705
|
+
return;
|
|
698
706
|
}
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
this.
|
|
707
|
+
if (payload === 'UNPAIRED') {
|
|
708
|
+
// Permanent disconnection — clear session, stop reconnecting
|
|
709
|
+
this.sessionStore.clear().catch(() => { });
|
|
710
|
+
this.emit('unpaired');
|
|
711
|
+
this.transport?.disconnect();
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (SESSION_FATAL.has(payload)) {
|
|
715
|
+
// Session gone — clear and let reconnect do fresh pairing
|
|
716
|
+
this.sessionStore.clear().catch(() => { });
|
|
717
|
+
return;
|
|
702
718
|
}
|
|
719
|
+
// Unknown payload — log but don't trigger re-key
|
|
720
|
+
// (prevents infinite re-key loops from future server messages)
|
|
703
721
|
}
|
|
704
722
|
});
|
|
705
723
|
this.transport.on('open', () => {
|
package/dist/chat/ChatHandler.js
CHANGED
|
@@ -101,10 +101,15 @@ export class PairingProtocol extends EventEmitter {
|
|
|
101
101
|
reject(err);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
//
|
|
105
|
-
if (msg.type === 'CLOSE') {
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
// Server messages — only fatal errors reject pairing
|
|
105
|
+
if (msg.type === 'CLOSE' || msg.type === 'STATUS') {
|
|
106
|
+
const payload = typeof msg.payload === 'string' ? msg.payload : '';
|
|
107
|
+
const FATAL = ['SESSION_NOT_FOUND', 'INVALID_CODE', 'MISSING_CODE', 'NO_PAIRING', 'UNPAIRED'];
|
|
108
|
+
if (FATAL.includes(payload)) {
|
|
109
|
+
cleanup();
|
|
110
|
+
reject(new Error(`Relay server closed: ${payload}`));
|
|
111
|
+
}
|
|
112
|
+
// Non-fatal (AGENT_WARNING, AGENT_ONLINE, DESKTOP_DISCONNECTED, STATUS) — ignore
|
|
108
113
|
}
|
|
109
114
|
};
|
|
110
115
|
this.transport.on('message', onMessage);
|
|
@@ -202,13 +207,18 @@ export class PairingProtocol extends EventEmitter {
|
|
|
202
207
|
}
|
|
203
208
|
// PAIRED notification from server — agent has joined (initiator mode)
|
|
204
209
|
// No action needed, just wait for agent's HELLO with pubkey
|
|
205
|
-
//
|
|
206
|
-
if (msg.type === 'CLOSE') {
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
210
|
+
// Server messages — only fatal errors reject pairing
|
|
211
|
+
if (msg.type === 'CLOSE' || msg.type === 'STATUS') {
|
|
212
|
+
const payload = typeof msg.payload === 'string' ? msg.payload : '';
|
|
213
|
+
const FATAL = ['SESSION_NOT_FOUND', 'INVALID_CODE', 'MISSING_CODE', 'NO_PAIRING', 'UNPAIRED'];
|
|
214
|
+
if (FATAL.includes(payload)) {
|
|
215
|
+
cleanup();
|
|
216
|
+
const reason = payload === 'INVALID_CODE'
|
|
217
|
+
? `Invalid pairing code: ****`
|
|
218
|
+
: `Relay server closed: ${payload}`;
|
|
219
|
+
reject(new Error(reason));
|
|
220
|
+
}
|
|
221
|
+
// Non-fatal — ignore
|
|
212
222
|
}
|
|
213
223
|
};
|
|
214
224
|
this.transport.on('message', onMessage);
|
package/dist/types.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface RelayMessage {
|
|
|
12
12
|
version: typeof RELAY_PROTOCOL_VERSION;
|
|
13
13
|
session_id: string;
|
|
14
14
|
msg_id: string;
|
|
15
|
-
type: 'HELLO' | 'DATA' | 'PING' | 'CLOSE';
|
|
15
|
+
type: 'HELLO' | 'DATA' | 'PING' | 'CLOSE' | 'STATUS' | 'PAIRED';
|
|
16
16
|
pubkey?: string;
|
|
17
17
|
iv?: string;
|
|
18
18
|
payload?: string;
|
package/package.json
CHANGED