oomi-ai 0.2.5 → 0.2.6
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/README.md +3 -0
- package/bin/oomi-ai.js +322 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Agent-intent mapping (recommended):
|
|
|
72
72
|
Important distinction:
|
|
73
73
|
- `pairCode` is one-time and used internally by the pair/bootstrap flow.
|
|
74
74
|
- Invite auth links are the required user flow.
|
|
75
|
+
- Managed chat connect now uses OpenClaw challenge auth (`connect.challenge` nonce + signed device payload) in the local bridge path.
|
|
75
76
|
|
|
76
77
|
Sync personas from the repo into the backend registry:
|
|
77
78
|
```
|
|
@@ -109,6 +110,8 @@ Restart OpenClaw after running `oomi init` or `oomi openclaw install`.
|
|
|
109
110
|
- `OOMI_SKIP_UPDATE_CHECK=1` disables checks
|
|
110
111
|
- `OOMI_UPDATE_CHECK_INTERVAL_MS=<ms>` changes check interval
|
|
111
112
|
- `OOMI_UPDATE_CHECK_TIMEOUT_MS=<ms>` changes network timeout
|
|
113
|
+
- `OOMI_BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS=<ms>` changes local gateway socket connect timeout
|
|
114
|
+
- `OOMI_BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS=<ms>` changes wait timeout for gateway `connect.challenge` nonce
|
|
112
115
|
|
|
113
116
|
## Package Audit + Publish (pnpm)
|
|
114
117
|
```
|
package/bin/oomi-ai.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
|
-
import { randomUUID } from 'crypto';
|
|
6
|
+
import { createPrivateKey, createPublicKey, randomUUID, sign as cryptoSign } from 'crypto';
|
|
7
7
|
import net from 'net';
|
|
8
8
|
import { lookup as dnsLookup } from 'dns/promises';
|
|
9
9
|
import WebSocket from 'ws';
|
|
@@ -18,6 +18,16 @@ const DEFAULT_UPDATE_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
|
18
18
|
const DEFAULT_UPDATE_CHECK_TIMEOUT_MS = 1200;
|
|
19
19
|
const BRIDGE_RECONNECT_BASE_MS = 2000;
|
|
20
20
|
const BRIDGE_RECONNECT_MAX_MS = 60000;
|
|
21
|
+
const BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS = parsePositiveInteger(
|
|
22
|
+
process.env.OOMI_BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS,
|
|
23
|
+
10000
|
|
24
|
+
);
|
|
25
|
+
const BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS = parsePositiveInteger(
|
|
26
|
+
process.env.OOMI_BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS,
|
|
27
|
+
3000
|
|
28
|
+
);
|
|
29
|
+
const DEVICE_IDENTITY_PATH = path.join(os.homedir(), '.openclaw', 'identity', 'device.json');
|
|
30
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
21
31
|
|
|
22
32
|
function parsePositiveInteger(value, fallback) {
|
|
23
33
|
const num = Number(value);
|
|
@@ -666,13 +676,109 @@ async function fetchManagedGatewayConfig({ appUrl }) {
|
|
|
666
676
|
return payload;
|
|
667
677
|
}
|
|
668
678
|
|
|
669
|
-
function
|
|
679
|
+
function base64UrlEncode(value) {
|
|
680
|
+
return Buffer.from(value)
|
|
681
|
+
.toString('base64')
|
|
682
|
+
.replaceAll('+', '-')
|
|
683
|
+
.replaceAll('/', '_')
|
|
684
|
+
.replace(/=+$/g, '');
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function normalizeDeviceMetadataForAuth(value) {
|
|
688
|
+
if (typeof value !== 'string') return '';
|
|
689
|
+
const trimmed = value.trim();
|
|
690
|
+
if (!trimmed) return '';
|
|
691
|
+
return trimmed.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
|
|
695
|
+
const der = createPublicKey(publicKeyPem).export({ type: 'spki', format: 'der' });
|
|
696
|
+
const raw =
|
|
697
|
+
der.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
698
|
+
der.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
699
|
+
? der.subarray(ED25519_SPKI_PREFIX.length)
|
|
700
|
+
: der;
|
|
701
|
+
return base64UrlEncode(raw);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function signDevicePayload(privateKeyPem, payload) {
|
|
705
|
+
const key = createPrivateKey(privateKeyPem);
|
|
706
|
+
return base64UrlEncode(cryptoSign(null, Buffer.from(payload, 'utf8'), key));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function buildDeviceAuthPayloadV3({
|
|
710
|
+
deviceId,
|
|
711
|
+
clientId,
|
|
712
|
+
clientMode,
|
|
713
|
+
role,
|
|
714
|
+
scopes,
|
|
715
|
+
signedAtMs,
|
|
716
|
+
token,
|
|
717
|
+
nonce,
|
|
718
|
+
platform,
|
|
719
|
+
deviceFamily,
|
|
720
|
+
}) {
|
|
721
|
+
return [
|
|
722
|
+
'v3',
|
|
723
|
+
deviceId,
|
|
724
|
+
clientId,
|
|
725
|
+
clientMode,
|
|
726
|
+
role,
|
|
727
|
+
scopes.join(','),
|
|
728
|
+
String(signedAtMs),
|
|
729
|
+
token || '',
|
|
730
|
+
nonce,
|
|
731
|
+
normalizeDeviceMetadataForAuth(platform),
|
|
732
|
+
normalizeDeviceMetadataForAuth(deviceFamily),
|
|
733
|
+
].join('|');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function loadGatewayDeviceIdentity() {
|
|
737
|
+
if (!fs.existsSync(DEVICE_IDENTITY_PATH)) {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
const parsed = JSON.parse(readFile(DEVICE_IDENTITY_PATH));
|
|
742
|
+
if (
|
|
743
|
+
parsed &&
|
|
744
|
+
parsed.version === 1 &&
|
|
745
|
+
typeof parsed.deviceId === 'string' &&
|
|
746
|
+
typeof parsed.publicKeyPem === 'string' &&
|
|
747
|
+
typeof parsed.privateKeyPem === 'string'
|
|
748
|
+
) {
|
|
749
|
+
return {
|
|
750
|
+
deviceId: parsed.deviceId.trim(),
|
|
751
|
+
publicKeyPem: parsed.publicKeyPem,
|
|
752
|
+
privateKeyPem: parsed.privateKeyPem,
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
} catch {
|
|
756
|
+
// no-op
|
|
757
|
+
}
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function prepareGatewayFrameForLocalGateway(frameText, gatewayAuth, options = {}) {
|
|
762
|
+
const connectNonce = typeof options.connectNonce === 'string' ? options.connectNonce.trim() : '';
|
|
763
|
+
const deviceIdentity = options.deviceIdentity || null;
|
|
764
|
+
|
|
670
765
|
try {
|
|
671
766
|
const frame = JSON.parse(frameText);
|
|
672
767
|
if (frame?.type !== 'req' || frame?.method !== 'connect') {
|
|
673
|
-
return frameText;
|
|
768
|
+
return { frameText, waitForChallenge: false };
|
|
674
769
|
}
|
|
770
|
+
|
|
675
771
|
const params = frame.params && typeof frame.params === 'object' ? frame.params : {};
|
|
772
|
+
|
|
773
|
+
const client = params.client && typeof params.client === 'object' ? params.client : {};
|
|
774
|
+
client.id = typeof client.id === 'string' && client.id.trim() ? client.id.trim() : 'webchat-ui';
|
|
775
|
+
client.version = typeof client.version === 'string' && client.version.trim() ? client.version.trim() : '0.1.0';
|
|
776
|
+
client.platform = typeof client.platform === 'string' && client.platform.trim() ? client.platform.trim() : process.platform;
|
|
777
|
+
client.mode = typeof client.mode === 'string' && client.mode.trim() ? client.mode.trim() : 'webchat';
|
|
778
|
+
params.client = client;
|
|
779
|
+
|
|
780
|
+
params.role = typeof params.role === 'string' && params.role.trim() ? params.role.trim() : 'operator';
|
|
781
|
+
|
|
676
782
|
const existingScopes = Array.isArray(params.scopes)
|
|
677
783
|
? params.scopes.filter((value) => typeof value === 'string' && value.trim())
|
|
678
784
|
: [];
|
|
@@ -684,17 +790,63 @@ function injectGatewayAuth(frameText, gatewayAuth) {
|
|
|
684
790
|
}
|
|
685
791
|
params.scopes = existingScopes;
|
|
686
792
|
|
|
793
|
+
if (!Array.isArray(params.caps)) {
|
|
794
|
+
params.caps = [];
|
|
795
|
+
}
|
|
796
|
+
if (!Array.isArray(params.commands)) {
|
|
797
|
+
params.commands = [];
|
|
798
|
+
}
|
|
799
|
+
if (!params.permissions || typeof params.permissions !== 'object') {
|
|
800
|
+
params.permissions = {};
|
|
801
|
+
}
|
|
802
|
+
|
|
687
803
|
const auth = {};
|
|
688
|
-
if (gatewayAuth.token)
|
|
689
|
-
|
|
804
|
+
if (gatewayAuth.token) {
|
|
805
|
+
auth.token = gatewayAuth.token;
|
|
806
|
+
} else if (gatewayAuth.password) {
|
|
807
|
+
auth.password = gatewayAuth.password;
|
|
808
|
+
}
|
|
690
809
|
if (Object.keys(auth).length > 0) {
|
|
691
810
|
params.auth = auth;
|
|
692
|
-
frame.params = params;
|
|
693
|
-
return JSON.stringify(frame);
|
|
694
811
|
}
|
|
695
|
-
|
|
812
|
+
|
|
813
|
+
if (deviceIdentity) {
|
|
814
|
+
if (!connectNonce) {
|
|
815
|
+
return { frameText: null, waitForChallenge: true };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const signedAtMs = Date.now();
|
|
819
|
+
const tokenForSignature =
|
|
820
|
+
typeof auth.token === 'string' && auth.token.trim()
|
|
821
|
+
? auth.token.trim()
|
|
822
|
+
: (typeof auth.deviceToken === 'string' && auth.deviceToken.trim() ? auth.deviceToken.trim() : '');
|
|
823
|
+
|
|
824
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
825
|
+
deviceId: deviceIdentity.deviceId,
|
|
826
|
+
clientId: client.id,
|
|
827
|
+
clientMode: client.mode,
|
|
828
|
+
role: params.role,
|
|
829
|
+
scopes: existingScopes,
|
|
830
|
+
signedAtMs,
|
|
831
|
+
token: tokenForSignature,
|
|
832
|
+
nonce: connectNonce,
|
|
833
|
+
platform: client.platform,
|
|
834
|
+
deviceFamily: typeof client.deviceFamily === 'string' ? client.deviceFamily : '',
|
|
835
|
+
});
|
|
836
|
+
const signature = signDevicePayload(deviceIdentity.privateKeyPem, payload);
|
|
837
|
+
params.device = {
|
|
838
|
+
id: deviceIdentity.deviceId,
|
|
839
|
+
publicKey: publicKeyRawBase64UrlFromPem(deviceIdentity.publicKeyPem),
|
|
840
|
+
signature,
|
|
841
|
+
signedAt: signedAtMs,
|
|
842
|
+
nonce: connectNonce,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
frame.params = params;
|
|
847
|
+
return { frameText: JSON.stringify(frame), waitForChallenge: false };
|
|
696
848
|
} catch {
|
|
697
|
-
return frameText;
|
|
849
|
+
return { frameText, waitForChallenge: false };
|
|
698
850
|
}
|
|
699
851
|
}
|
|
700
852
|
|
|
@@ -952,6 +1104,12 @@ async function startOpenclawBridge(flags) {
|
|
|
952
1104
|
if (!gateway.token && !gateway.password) {
|
|
953
1105
|
throw new Error(`Gateway auth token/password not found in ${gateway.configPath}.`);
|
|
954
1106
|
}
|
|
1107
|
+
const gatewayDeviceIdentity = loadGatewayDeviceIdentity();
|
|
1108
|
+
if (!gatewayDeviceIdentity) {
|
|
1109
|
+
console.warn(
|
|
1110
|
+
`[bridge] OpenClaw device identity not found at ${DEVICE_IDENTITY_PATH}; device-signed connect may fail on newer gateways.`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
955
1113
|
|
|
956
1114
|
if (runtimeConfig.managedConfigUsed && runtimeConfig.appUrl) {
|
|
957
1115
|
console.log(`[bridge] refreshed broker URLs from ${runtimeConfig.appUrl}`);
|
|
@@ -1099,21 +1257,157 @@ async function startOpenclawBridge(flags) {
|
|
|
1099
1257
|
|
|
1100
1258
|
const brokerSocket = new WebSocket(wsUrl.toString());
|
|
1101
1259
|
let actionCableHeartbeat = null;
|
|
1260
|
+
|
|
1261
|
+
const clearChallengeTimer = (sessionBridge) => {
|
|
1262
|
+
if (sessionBridge && sessionBridge.connectChallengeTimer) {
|
|
1263
|
+
clearTimeout(sessionBridge.connectChallengeTimer);
|
|
1264
|
+
sessionBridge.connectChallengeTimer = null;
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const queueConnectUntilChallenge = (sessionId, sessionBridge, frame) => {
|
|
1269
|
+
if (!sessionBridge || typeof frame !== 'string' || !frame) return;
|
|
1270
|
+
if (!Array.isArray(sessionBridge.pendingConnectFrames)) {
|
|
1271
|
+
sessionBridge.pendingConnectFrames = [];
|
|
1272
|
+
}
|
|
1273
|
+
if (sessionBridge.pendingConnectFrames.includes(frame)) {
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
sessionBridge.pendingConnectFrames.push(frame);
|
|
1277
|
+
|
|
1278
|
+
if (sessionBridge.connectChallengeTimer) {
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
sessionBridge.connectChallengeTimer = setTimeout(() => {
|
|
1283
|
+
sessionBridge.connectChallengeTimer = null;
|
|
1284
|
+
const hasPending = Array.isArray(sessionBridge.pendingConnectFrames)
|
|
1285
|
+
? sessionBridge.pendingConnectFrames.length > 0
|
|
1286
|
+
: false;
|
|
1287
|
+
if (!hasPending || sessionBridge.connectNonce) {
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
console.error(
|
|
1291
|
+
`[bridge] gateway.connect_challenge_timeout ${sessionId} (${String(BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS)}ms)`
|
|
1292
|
+
);
|
|
1293
|
+
sendBrokerPayload(brokerSocket, {
|
|
1294
|
+
action: 'log',
|
|
1295
|
+
type: 'log',
|
|
1296
|
+
sessionId,
|
|
1297
|
+
level: 'error',
|
|
1298
|
+
message: `Gateway challenge timeout (${String(BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS)}ms) for session ${sessionId}`,
|
|
1299
|
+
});
|
|
1300
|
+
try {
|
|
1301
|
+
sessionBridge.socket?.close(4009, 'connect_challenge_timeout');
|
|
1302
|
+
} catch {
|
|
1303
|
+
// no-op
|
|
1304
|
+
}
|
|
1305
|
+
}, BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS);
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
const flushPendingConnectFrames = (sessionId, sessionBridge) => {
|
|
1309
|
+
if (!sessionBridge || !sessionBridge.connectNonce) return;
|
|
1310
|
+
const pending = Array.isArray(sessionBridge.pendingConnectFrames)
|
|
1311
|
+
? sessionBridge.pendingConnectFrames.splice(0, sessionBridge.pendingConnectFrames.length)
|
|
1312
|
+
: [];
|
|
1313
|
+
if (pending.length === 0) {
|
|
1314
|
+
clearChallengeTimer(sessionBridge);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
clearChallengeTimer(sessionBridge);
|
|
1319
|
+
for (const pendingFrame of pending) {
|
|
1320
|
+
const prepared = prepareGatewayFrameForLocalGateway(pendingFrame, gateway, {
|
|
1321
|
+
connectNonce: sessionBridge.connectNonce,
|
|
1322
|
+
deviceIdentity: gatewayDeviceIdentity,
|
|
1323
|
+
});
|
|
1324
|
+
if (!prepared.frameText || prepared.waitForChallenge) {
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const result = forwardFrameToSession(sessionBridge, prepared.frameText);
|
|
1328
|
+
if (result === 'queued') {
|
|
1329
|
+
console.log(`[bridge] client.frame queued after challenge ${sessionId}`);
|
|
1330
|
+
} else if (result === 'dropped') {
|
|
1331
|
+
console.log(`[bridge] client.frame dropped after challenge ${sessionId}`);
|
|
1332
|
+
} else {
|
|
1333
|
+
console.log(`[bridge] client.frame sent after challenge ${sessionId}`);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1102
1338
|
const setupGatewaySession = (sessionId, sessionBridge) => {
|
|
1103
1339
|
if (!sessionBridge || !sessionBridge.socket) return;
|
|
1104
1340
|
const gatewaySocket = sessionBridge.socket;
|
|
1341
|
+
if (typeof sessionBridge.connectNonce !== 'string') {
|
|
1342
|
+
sessionBridge.connectNonce = '';
|
|
1343
|
+
}
|
|
1344
|
+
if (!Array.isArray(sessionBridge.pendingConnectFrames)) {
|
|
1345
|
+
sessionBridge.pendingConnectFrames = [];
|
|
1346
|
+
}
|
|
1347
|
+
let connectTimeout = setTimeout(() => {
|
|
1348
|
+
if (gatewaySocket.readyState !== WebSocket.CONNECTING) return;
|
|
1349
|
+
console.error(
|
|
1350
|
+
`[bridge] gateway.connect_timeout ${sessionId} (${String(BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS)}ms)`
|
|
1351
|
+
);
|
|
1352
|
+
sendBrokerPayload(brokerSocket, {
|
|
1353
|
+
action: 'log',
|
|
1354
|
+
type: 'log',
|
|
1355
|
+
sessionId,
|
|
1356
|
+
level: 'error',
|
|
1357
|
+
message: `Gateway connect timeout (${String(BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS)}ms) for session ${sessionId}`,
|
|
1358
|
+
});
|
|
1359
|
+
try {
|
|
1360
|
+
gatewaySocket.close(4008, 'gateway_connect_timeout');
|
|
1361
|
+
} catch {
|
|
1362
|
+
// no-op
|
|
1363
|
+
}
|
|
1364
|
+
}, BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS);
|
|
1105
1365
|
|
|
1106
1366
|
gatewaySocket.on('open', () => {
|
|
1367
|
+
if (connectTimeout) {
|
|
1368
|
+
clearTimeout(connectTimeout);
|
|
1369
|
+
connectTimeout = null;
|
|
1370
|
+
}
|
|
1107
1371
|
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
1108
1372
|
flushSessionQueue(sessionBridge);
|
|
1109
1373
|
});
|
|
1110
1374
|
|
|
1111
1375
|
gatewaySocket.on('message', (gatewayRaw) => {
|
|
1112
1376
|
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
1377
|
+
const gatewayPayload = parseJsonPayload(frame);
|
|
1378
|
+
if (gatewayPayload?.event === 'connect.challenge') {
|
|
1379
|
+
const nonce =
|
|
1380
|
+
gatewayPayload.payload && typeof gatewayPayload.payload.nonce === 'string'
|
|
1381
|
+
? gatewayPayload.payload.nonce.trim()
|
|
1382
|
+
: '';
|
|
1383
|
+
if (!nonce) {
|
|
1384
|
+
console.error(`[bridge] gateway.connect.challenge missing nonce for ${sessionId}`);
|
|
1385
|
+
sendBrokerPayload(brokerSocket, {
|
|
1386
|
+
action: 'log',
|
|
1387
|
+
type: 'log',
|
|
1388
|
+
sessionId,
|
|
1389
|
+
level: 'error',
|
|
1390
|
+
message: `Gateway connect challenge missing nonce for session ${sessionId}`,
|
|
1391
|
+
});
|
|
1392
|
+
try {
|
|
1393
|
+
gatewaySocket.close(1008, 'connect_challenge_missing_nonce');
|
|
1394
|
+
} catch {
|
|
1395
|
+
// no-op
|
|
1396
|
+
}
|
|
1397
|
+
} else {
|
|
1398
|
+
sessionBridge.connectNonce = nonce;
|
|
1399
|
+
flushPendingConnectFrames(sessionId, sessionBridge);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1113
1402
|
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
1114
1403
|
});
|
|
1115
1404
|
|
|
1116
1405
|
gatewaySocket.on('close', (code, reason) => {
|
|
1406
|
+
if (connectTimeout) {
|
|
1407
|
+
clearTimeout(connectTimeout);
|
|
1408
|
+
connectTimeout = null;
|
|
1409
|
+
}
|
|
1410
|
+
clearChallengeTimer(sessionBridge);
|
|
1117
1411
|
const reasonText = reason ? reason.toString() : '';
|
|
1118
1412
|
console.log(
|
|
1119
1413
|
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
@@ -1129,6 +1423,10 @@ async function startOpenclawBridge(flags) {
|
|
|
1129
1423
|
});
|
|
1130
1424
|
|
|
1131
1425
|
gatewaySocket.on('error', (err) => {
|
|
1426
|
+
if (connectTimeout && gatewaySocket.readyState !== WebSocket.CONNECTING) {
|
|
1427
|
+
clearTimeout(connectTimeout);
|
|
1428
|
+
connectTimeout = null;
|
|
1429
|
+
}
|
|
1132
1430
|
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
1133
1431
|
sendBrokerPayload(brokerSocket, {
|
|
1134
1432
|
action: 'log',
|
|
@@ -1270,8 +1568,19 @@ async function startOpenclawBridge(flags) {
|
|
|
1270
1568
|
console.log(`[bridge] client.frame ${sessionId}`);
|
|
1271
1569
|
const sessionBridge = getOrCreateGatewaySession(sessionId);
|
|
1272
1570
|
if (!sessionBridge) return;
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1571
|
+
const prepared = prepareGatewayFrameForLocalGateway(frame, gateway, {
|
|
1572
|
+
connectNonce: sessionBridge.connectNonce,
|
|
1573
|
+
deviceIdentity: gatewayDeviceIdentity,
|
|
1574
|
+
});
|
|
1575
|
+
if (prepared.waitForChallenge) {
|
|
1576
|
+
queueConnectUntilChallenge(sessionId, sessionBridge, frame);
|
|
1577
|
+
console.log(`[bridge] client.frame waiting for challenge ${sessionId}`);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
if (!prepared.frameText) {
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
const result = forwardFrameToSession(sessionBridge, prepared.frameText);
|
|
1275
1584
|
if (result === 'queued') {
|
|
1276
1585
|
console.log(`[bridge] client.frame queued ${sessionId}`);
|
|
1277
1586
|
} else if (result === 'dropped') {
|
|
@@ -1285,6 +1594,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1285
1594
|
console.log(`[bridge] client.close ${sessionId}`);
|
|
1286
1595
|
const sessionBridge = activeGatewaySockets.get(sessionId);
|
|
1287
1596
|
if (sessionBridge && sessionBridge.socket) {
|
|
1597
|
+
clearChallengeTimer(sessionBridge);
|
|
1288
1598
|
activeGatewaySockets.delete(sessionId);
|
|
1289
1599
|
sessionBridge.socket.close(1000, 'client_closed');
|
|
1290
1600
|
}
|
|
@@ -1300,6 +1610,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1300
1610
|
const reasonText = reason ? reason.toString() : '';
|
|
1301
1611
|
console.log(`[bridge] Broker disconnected (${code}) ${reasonText}`);
|
|
1302
1612
|
for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
|
|
1613
|
+
clearChallengeTimer(sessionBridge);
|
|
1303
1614
|
activeGatewaySockets.delete(sessionId);
|
|
1304
1615
|
try {
|
|
1305
1616
|
sessionBridge.socket.close(1001, 'broker_disconnected');
|