echoclaw-relay-agent 0.19.2 → 0.19.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.
- package/dist/RelayAgent.js +7 -6
- package/dist/RelayClient.js +18 -8
- package/dist/cli.js +19 -0
- package/dist/relay/PairingProtocol.js +5 -5
- package/package.json +1 -1
package/dist/RelayAgent.js
CHANGED
|
@@ -602,18 +602,19 @@ export class RelayAgent extends EventEmitter {
|
|
|
602
602
|
this._identityExchanged = true;
|
|
603
603
|
return;
|
|
604
604
|
}
|
|
605
|
-
//
|
|
606
|
-
//
|
|
607
|
-
this.removeListener('message', earlyHandler);
|
|
608
|
-
// Use early-captured message if available, otherwise wait with timeout
|
|
609
|
-
const peerIdentity = earlyIdentity ?? await this._waitForIdentityExchange(10000);
|
|
610
|
-
// Respond with our own identity
|
|
605
|
+
// SYMMETRIC EXCHANGE: Send our identity FIRST, then wait for peer's.
|
|
606
|
+
// Both sides send immediately upon pairing — eliminates "who sends first" race.
|
|
611
607
|
const myPubKeyBase64 = toBase64(ed.publicKeyRaw);
|
|
612
608
|
await this.send({
|
|
613
609
|
type: 'identity_exchange',
|
|
614
610
|
ed25519_pubkey: myPubKeyBase64,
|
|
615
611
|
device_id: identity.deviceId,
|
|
616
612
|
});
|
|
613
|
+
// Remove early handler before entering wait — prevents both handlers from
|
|
614
|
+
// processing the same message simultaneously.
|
|
615
|
+
this.removeListener('message', earlyHandler);
|
|
616
|
+
// Use early-captured message if available, otherwise wait with timeout
|
|
617
|
+
const peerIdentity = earlyIdentity ?? await this._waitForIdentityExchange(15000);
|
|
617
618
|
// Persist peer's public key
|
|
618
619
|
await this.identityStore.updatePeer(peerIdentity.ed25519_pubkey);
|
|
619
620
|
sessionData.peerPublicKeyRaw = peerIdentity.ed25519_pubkey;
|
package/dist/RelayClient.js
CHANGED
|
@@ -286,11 +286,20 @@ export class RelayClient extends EventEmitter {
|
|
|
286
286
|
await this.onPaired(result, 'paired');
|
|
287
287
|
}
|
|
288
288
|
catch (err) {
|
|
289
|
-
// Identity exchange failed
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
// Identity exchange failed — non-fatal for fresh pairing.
|
|
290
|
+
// ECDH succeeded, encrypted channel works. Identity exchange only enables
|
|
291
|
+
// V2 identity-based reconnect. Keep connection alive, emit warning.
|
|
292
|
+
const isIdentityErr = err?.message?.includes('Identity exchange');
|
|
293
|
+
if (isIdentityErr) {
|
|
294
|
+
console.warn('[RelayClient] Identity exchange failed (non-fatal):', err?.message);
|
|
295
|
+
this.emit('error', Object.assign(new Error(`Identity exchange failed: ${err?.message}`), { name: 'IdentityExchangeError' }));
|
|
296
|
+
// Connection is usable — don't tear down
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// Non-identity errors are still fatal
|
|
300
|
+
this.transport?.disconnect();
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
294
303
|
}
|
|
295
304
|
}
|
|
296
305
|
async resumeSession(session) {
|
|
@@ -577,7 +586,8 @@ export class RelayClient extends EventEmitter {
|
|
|
577
586
|
this._identityExchanged = true;
|
|
578
587
|
return; // finally will clean up earlyHandler
|
|
579
588
|
}
|
|
580
|
-
// Send our
|
|
589
|
+
// SYMMETRIC EXCHANGE: Send our identity FIRST, then wait for peer's.
|
|
590
|
+
// Both sides send immediately upon pairing — eliminates "who sends first" race.
|
|
581
591
|
const myPubKeyBase64 = toBase64(ed.publicKeyRaw);
|
|
582
592
|
await this.send({
|
|
583
593
|
type: 'identity_exchange',
|
|
@@ -586,8 +596,8 @@ export class RelayClient extends EventEmitter {
|
|
|
586
596
|
});
|
|
587
597
|
// Remove early handler before entering wait — prevents dual listeners
|
|
588
598
|
this.removeListener('message', earlyHandler);
|
|
589
|
-
// Use early-captured message if available, otherwise wait with timeout
|
|
590
|
-
const peerIdentity = earlyIdentity ?? await this._waitForIdentityExchange(
|
|
599
|
+
// Use early-captured message if available, otherwise wait with timeout (15s for setup)
|
|
600
|
+
const peerIdentity = earlyIdentity ?? await this._waitForIdentityExchange(15000);
|
|
591
601
|
// Persist peer's public key
|
|
592
602
|
await this.identityStore.updatePeer(peerIdentity.ed25519_pubkey);
|
|
593
603
|
sessionData.peerPublicKeyRaw = peerIdentity.ed25519_pubkey;
|
package/dist/cli.js
CHANGED
|
@@ -326,6 +326,25 @@ async function runSetup(code, relay, bridgePort) {
|
|
|
326
326
|
const pairInfo = await pairPromise;
|
|
327
327
|
printOk(`Paired with desktop client`);
|
|
328
328
|
console.log(` ${DIM}Session: ${pairInfo.sessionId.slice(0, 8)}...${RESET}`);
|
|
329
|
+
// Wait for identity exchange to complete (or timeout gracefully)
|
|
330
|
+
// Identity exchange starts inside onPaired() — give it time before stopping.
|
|
331
|
+
await new Promise((resolve) => {
|
|
332
|
+
const onIdentity = () => { cleanup(); resolve(); };
|
|
333
|
+
const onError = (err) => {
|
|
334
|
+
if (err.constructor?.name === 'IdentityExchangeError') {
|
|
335
|
+
cleanup();
|
|
336
|
+
resolve();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const timer = setTimeout(() => { cleanup(); resolve(); }, 12000); // 12s > 10s identity timeout
|
|
340
|
+
const cleanup = () => {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
agent.removeListener('identity_exchanged', onIdentity);
|
|
343
|
+
agent.removeListener('error', onError);
|
|
344
|
+
};
|
|
345
|
+
agent.on('identity_exchanged', onIdentity);
|
|
346
|
+
agent.on('error', onError);
|
|
347
|
+
});
|
|
329
348
|
console.log();
|
|
330
349
|
// Step 2: Stop the foreground agent (service will take over)
|
|
331
350
|
await agent.stop();
|
|
@@ -174,18 +174,18 @@ export class PairingProtocol extends EventEmitter {
|
|
|
174
174
|
code = msg.payload;
|
|
175
175
|
this.emit('pairing_code', code);
|
|
176
176
|
}
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
sendPubkey();
|
|
180
|
-
}
|
|
177
|
+
// Do NOT send pubkey here in initiator mode — agent hasn't connected yet.
|
|
178
|
+
// Wait for agent's HELLO with pubkey first (see below).
|
|
181
179
|
}
|
|
182
180
|
// Track session_id from any message
|
|
183
181
|
if (msg.session_id && !sessionId) {
|
|
184
182
|
sessionId = msg.session_id;
|
|
185
183
|
}
|
|
186
|
-
// Agent sends their public key — complete ECDH
|
|
184
|
+
// Agent sends their public key — send ours back, then complete ECDH
|
|
187
185
|
if (msg.type === 'HELLO' && msg.pubkey && msg.sender_role === 'agent') {
|
|
188
186
|
try {
|
|
187
|
+
// Send our pubkey AFTER receiving agent's (so agent WS is guaranteed connected)
|
|
188
|
+
sendPubkey();
|
|
189
189
|
const theirPub = fromBase64(msg.pubkey);
|
|
190
190
|
const sessionKey = await completeHandshake(this.keyPair.privateKey, theirPub, code || undefined);
|
|
191
191
|
cleanup();
|
package/package.json
CHANGED