oomi-ai 0.2.5 → 0.2.7
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 +337 -11
- package/openclaw.plugin.json +1 -1
- 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,124 @@ 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
|
+
const incomingClientId = typeof client.id === 'string' ? client.id.trim().toLowerCase() : '';
|
|
775
|
+
const incomingClientMode = typeof client.mode === 'string' ? client.mode.trim().toLowerCase() : '';
|
|
776
|
+
const proxiedBrowserClient =
|
|
777
|
+
incomingClientMode === 'webchat' ||
|
|
778
|
+
incomingClientId === 'webchat-ui' ||
|
|
779
|
+
incomingClientId === 'webchat' ||
|
|
780
|
+
incomingClientId === 'clawdbot-control-ui';
|
|
781
|
+
|
|
782
|
+
// Frames relayed by this bridge originate from a local Node websocket, not a browser.
|
|
783
|
+
// Keep gateway auth/nonce flow, but normalize browser-mode connects to backend identity
|
|
784
|
+
// so Control UI/webchat Origin checks don't reject proxied sessions.
|
|
785
|
+
client.id = proxiedBrowserClient
|
|
786
|
+
? 'node-host'
|
|
787
|
+
: (typeof client.id === 'string' && client.id.trim() ? client.id.trim() : 'node-host');
|
|
788
|
+
client.version = typeof client.version === 'string' && client.version.trim() ? client.version.trim() : '0.1.0';
|
|
789
|
+
client.platform = typeof client.platform === 'string' && client.platform.trim() ? client.platform.trim() : process.platform;
|
|
790
|
+
client.mode = proxiedBrowserClient
|
|
791
|
+
? 'backend'
|
|
792
|
+
: (typeof client.mode === 'string' && client.mode.trim() ? client.mode.trim() : 'backend');
|
|
793
|
+
params.client = client;
|
|
794
|
+
|
|
795
|
+
params.role = typeof params.role === 'string' && params.role.trim() ? params.role.trim() : 'operator';
|
|
796
|
+
|
|
676
797
|
const existingScopes = Array.isArray(params.scopes)
|
|
677
798
|
? params.scopes.filter((value) => typeof value === 'string' && value.trim())
|
|
678
799
|
: [];
|
|
@@ -684,17 +805,63 @@ function injectGatewayAuth(frameText, gatewayAuth) {
|
|
|
684
805
|
}
|
|
685
806
|
params.scopes = existingScopes;
|
|
686
807
|
|
|
808
|
+
if (!Array.isArray(params.caps)) {
|
|
809
|
+
params.caps = [];
|
|
810
|
+
}
|
|
811
|
+
if (!Array.isArray(params.commands)) {
|
|
812
|
+
params.commands = [];
|
|
813
|
+
}
|
|
814
|
+
if (!params.permissions || typeof params.permissions !== 'object') {
|
|
815
|
+
params.permissions = {};
|
|
816
|
+
}
|
|
817
|
+
|
|
687
818
|
const auth = {};
|
|
688
|
-
if (gatewayAuth.token)
|
|
689
|
-
|
|
819
|
+
if (gatewayAuth.token) {
|
|
820
|
+
auth.token = gatewayAuth.token;
|
|
821
|
+
} else if (gatewayAuth.password) {
|
|
822
|
+
auth.password = gatewayAuth.password;
|
|
823
|
+
}
|
|
690
824
|
if (Object.keys(auth).length > 0) {
|
|
691
825
|
params.auth = auth;
|
|
692
|
-
frame.params = params;
|
|
693
|
-
return JSON.stringify(frame);
|
|
694
826
|
}
|
|
695
|
-
|
|
827
|
+
|
|
828
|
+
if (deviceIdentity) {
|
|
829
|
+
if (!connectNonce) {
|
|
830
|
+
return { frameText: null, waitForChallenge: true };
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const signedAtMs = Date.now();
|
|
834
|
+
const tokenForSignature =
|
|
835
|
+
typeof auth.token === 'string' && auth.token.trim()
|
|
836
|
+
? auth.token.trim()
|
|
837
|
+
: (typeof auth.deviceToken === 'string' && auth.deviceToken.trim() ? auth.deviceToken.trim() : '');
|
|
838
|
+
|
|
839
|
+
const payload = buildDeviceAuthPayloadV3({
|
|
840
|
+
deviceId: deviceIdentity.deviceId,
|
|
841
|
+
clientId: client.id,
|
|
842
|
+
clientMode: client.mode,
|
|
843
|
+
role: params.role,
|
|
844
|
+
scopes: existingScopes,
|
|
845
|
+
signedAtMs,
|
|
846
|
+
token: tokenForSignature,
|
|
847
|
+
nonce: connectNonce,
|
|
848
|
+
platform: client.platform,
|
|
849
|
+
deviceFamily: typeof client.deviceFamily === 'string' ? client.deviceFamily : '',
|
|
850
|
+
});
|
|
851
|
+
const signature = signDevicePayload(deviceIdentity.privateKeyPem, payload);
|
|
852
|
+
params.device = {
|
|
853
|
+
id: deviceIdentity.deviceId,
|
|
854
|
+
publicKey: publicKeyRawBase64UrlFromPem(deviceIdentity.publicKeyPem),
|
|
855
|
+
signature,
|
|
856
|
+
signedAt: signedAtMs,
|
|
857
|
+
nonce: connectNonce,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
frame.params = params;
|
|
862
|
+
return { frameText: JSON.stringify(frame), waitForChallenge: false };
|
|
696
863
|
} catch {
|
|
697
|
-
return frameText;
|
|
864
|
+
return { frameText, waitForChallenge: false };
|
|
698
865
|
}
|
|
699
866
|
}
|
|
700
867
|
|
|
@@ -952,6 +1119,12 @@ async function startOpenclawBridge(flags) {
|
|
|
952
1119
|
if (!gateway.token && !gateway.password) {
|
|
953
1120
|
throw new Error(`Gateway auth token/password not found in ${gateway.configPath}.`);
|
|
954
1121
|
}
|
|
1122
|
+
const gatewayDeviceIdentity = loadGatewayDeviceIdentity();
|
|
1123
|
+
if (!gatewayDeviceIdentity) {
|
|
1124
|
+
console.warn(
|
|
1125
|
+
`[bridge] OpenClaw device identity not found at ${DEVICE_IDENTITY_PATH}; device-signed connect may fail on newer gateways.`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
955
1128
|
|
|
956
1129
|
if (runtimeConfig.managedConfigUsed && runtimeConfig.appUrl) {
|
|
957
1130
|
console.log(`[bridge] refreshed broker URLs from ${runtimeConfig.appUrl}`);
|
|
@@ -1099,21 +1272,157 @@ async function startOpenclawBridge(flags) {
|
|
|
1099
1272
|
|
|
1100
1273
|
const brokerSocket = new WebSocket(wsUrl.toString());
|
|
1101
1274
|
let actionCableHeartbeat = null;
|
|
1275
|
+
|
|
1276
|
+
const clearChallengeTimer = (sessionBridge) => {
|
|
1277
|
+
if (sessionBridge && sessionBridge.connectChallengeTimer) {
|
|
1278
|
+
clearTimeout(sessionBridge.connectChallengeTimer);
|
|
1279
|
+
sessionBridge.connectChallengeTimer = null;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
const queueConnectUntilChallenge = (sessionId, sessionBridge, frame) => {
|
|
1284
|
+
if (!sessionBridge || typeof frame !== 'string' || !frame) return;
|
|
1285
|
+
if (!Array.isArray(sessionBridge.pendingConnectFrames)) {
|
|
1286
|
+
sessionBridge.pendingConnectFrames = [];
|
|
1287
|
+
}
|
|
1288
|
+
if (sessionBridge.pendingConnectFrames.includes(frame)) {
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
sessionBridge.pendingConnectFrames.push(frame);
|
|
1292
|
+
|
|
1293
|
+
if (sessionBridge.connectChallengeTimer) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
sessionBridge.connectChallengeTimer = setTimeout(() => {
|
|
1298
|
+
sessionBridge.connectChallengeTimer = null;
|
|
1299
|
+
const hasPending = Array.isArray(sessionBridge.pendingConnectFrames)
|
|
1300
|
+
? sessionBridge.pendingConnectFrames.length > 0
|
|
1301
|
+
: false;
|
|
1302
|
+
if (!hasPending || sessionBridge.connectNonce) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
console.error(
|
|
1306
|
+
`[bridge] gateway.connect_challenge_timeout ${sessionId} (${String(BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS)}ms)`
|
|
1307
|
+
);
|
|
1308
|
+
sendBrokerPayload(brokerSocket, {
|
|
1309
|
+
action: 'log',
|
|
1310
|
+
type: 'log',
|
|
1311
|
+
sessionId,
|
|
1312
|
+
level: 'error',
|
|
1313
|
+
message: `Gateway challenge timeout (${String(BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS)}ms) for session ${sessionId}`,
|
|
1314
|
+
});
|
|
1315
|
+
try {
|
|
1316
|
+
sessionBridge.socket?.close(4009, 'connect_challenge_timeout');
|
|
1317
|
+
} catch {
|
|
1318
|
+
// no-op
|
|
1319
|
+
}
|
|
1320
|
+
}, BRIDGE_CONNECT_CHALLENGE_TIMEOUT_MS);
|
|
1321
|
+
};
|
|
1322
|
+
|
|
1323
|
+
const flushPendingConnectFrames = (sessionId, sessionBridge) => {
|
|
1324
|
+
if (!sessionBridge || !sessionBridge.connectNonce) return;
|
|
1325
|
+
const pending = Array.isArray(sessionBridge.pendingConnectFrames)
|
|
1326
|
+
? sessionBridge.pendingConnectFrames.splice(0, sessionBridge.pendingConnectFrames.length)
|
|
1327
|
+
: [];
|
|
1328
|
+
if (pending.length === 0) {
|
|
1329
|
+
clearChallengeTimer(sessionBridge);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
clearChallengeTimer(sessionBridge);
|
|
1334
|
+
for (const pendingFrame of pending) {
|
|
1335
|
+
const prepared = prepareGatewayFrameForLocalGateway(pendingFrame, gateway, {
|
|
1336
|
+
connectNonce: sessionBridge.connectNonce,
|
|
1337
|
+
deviceIdentity: gatewayDeviceIdentity,
|
|
1338
|
+
});
|
|
1339
|
+
if (!prepared.frameText || prepared.waitForChallenge) {
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
const result = forwardFrameToSession(sessionBridge, prepared.frameText);
|
|
1343
|
+
if (result === 'queued') {
|
|
1344
|
+
console.log(`[bridge] client.frame queued after challenge ${sessionId}`);
|
|
1345
|
+
} else if (result === 'dropped') {
|
|
1346
|
+
console.log(`[bridge] client.frame dropped after challenge ${sessionId}`);
|
|
1347
|
+
} else {
|
|
1348
|
+
console.log(`[bridge] client.frame sent after challenge ${sessionId}`);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1102
1353
|
const setupGatewaySession = (sessionId, sessionBridge) => {
|
|
1103
1354
|
if (!sessionBridge || !sessionBridge.socket) return;
|
|
1104
1355
|
const gatewaySocket = sessionBridge.socket;
|
|
1356
|
+
if (typeof sessionBridge.connectNonce !== 'string') {
|
|
1357
|
+
sessionBridge.connectNonce = '';
|
|
1358
|
+
}
|
|
1359
|
+
if (!Array.isArray(sessionBridge.pendingConnectFrames)) {
|
|
1360
|
+
sessionBridge.pendingConnectFrames = [];
|
|
1361
|
+
}
|
|
1362
|
+
let connectTimeout = setTimeout(() => {
|
|
1363
|
+
if (gatewaySocket.readyState !== WebSocket.CONNECTING) return;
|
|
1364
|
+
console.error(
|
|
1365
|
+
`[bridge] gateway.connect_timeout ${sessionId} (${String(BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS)}ms)`
|
|
1366
|
+
);
|
|
1367
|
+
sendBrokerPayload(brokerSocket, {
|
|
1368
|
+
action: 'log',
|
|
1369
|
+
type: 'log',
|
|
1370
|
+
sessionId,
|
|
1371
|
+
level: 'error',
|
|
1372
|
+
message: `Gateway connect timeout (${String(BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS)}ms) for session ${sessionId}`,
|
|
1373
|
+
});
|
|
1374
|
+
try {
|
|
1375
|
+
gatewaySocket.close(4008, 'gateway_connect_timeout');
|
|
1376
|
+
} catch {
|
|
1377
|
+
// no-op
|
|
1378
|
+
}
|
|
1379
|
+
}, BRIDGE_GATEWAY_CONNECT_TIMEOUT_MS);
|
|
1105
1380
|
|
|
1106
1381
|
gatewaySocket.on('open', () => {
|
|
1382
|
+
if (connectTimeout) {
|
|
1383
|
+
clearTimeout(connectTimeout);
|
|
1384
|
+
connectTimeout = null;
|
|
1385
|
+
}
|
|
1107
1386
|
console.log(`[bridge] gateway.open ${sessionId}`);
|
|
1108
1387
|
flushSessionQueue(sessionBridge);
|
|
1109
1388
|
});
|
|
1110
1389
|
|
|
1111
1390
|
gatewaySocket.on('message', (gatewayRaw) => {
|
|
1112
1391
|
const frame = typeof gatewayRaw === 'string' ? gatewayRaw : gatewayRaw.toString();
|
|
1392
|
+
const gatewayPayload = parseJsonPayload(frame);
|
|
1393
|
+
if (gatewayPayload?.event === 'connect.challenge') {
|
|
1394
|
+
const nonce =
|
|
1395
|
+
gatewayPayload.payload && typeof gatewayPayload.payload.nonce === 'string'
|
|
1396
|
+
? gatewayPayload.payload.nonce.trim()
|
|
1397
|
+
: '';
|
|
1398
|
+
if (!nonce) {
|
|
1399
|
+
console.error(`[bridge] gateway.connect.challenge missing nonce for ${sessionId}`);
|
|
1400
|
+
sendBrokerPayload(brokerSocket, {
|
|
1401
|
+
action: 'log',
|
|
1402
|
+
type: 'log',
|
|
1403
|
+
sessionId,
|
|
1404
|
+
level: 'error',
|
|
1405
|
+
message: `Gateway connect challenge missing nonce for session ${sessionId}`,
|
|
1406
|
+
});
|
|
1407
|
+
try {
|
|
1408
|
+
gatewaySocket.close(1008, 'connect_challenge_missing_nonce');
|
|
1409
|
+
} catch {
|
|
1410
|
+
// no-op
|
|
1411
|
+
}
|
|
1412
|
+
} else {
|
|
1413
|
+
sessionBridge.connectNonce = nonce;
|
|
1414
|
+
flushPendingConnectFrames(sessionId, sessionBridge);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1113
1417
|
sendBrokerPayload(brokerSocket, { action: 'gateway_frame', type: 'gateway.frame', sessionId, frame });
|
|
1114
1418
|
});
|
|
1115
1419
|
|
|
1116
1420
|
gatewaySocket.on('close', (code, reason) => {
|
|
1421
|
+
if (connectTimeout) {
|
|
1422
|
+
clearTimeout(connectTimeout);
|
|
1423
|
+
connectTimeout = null;
|
|
1424
|
+
}
|
|
1425
|
+
clearChallengeTimer(sessionBridge);
|
|
1117
1426
|
const reasonText = reason ? reason.toString() : '';
|
|
1118
1427
|
console.log(
|
|
1119
1428
|
`[bridge] gateway.close ${sessionId} code=${String(code)}${reasonText ? ` reason=${reasonText}` : ''}`
|
|
@@ -1129,6 +1438,10 @@ async function startOpenclawBridge(flags) {
|
|
|
1129
1438
|
});
|
|
1130
1439
|
|
|
1131
1440
|
gatewaySocket.on('error', (err) => {
|
|
1441
|
+
if (connectTimeout && gatewaySocket.readyState !== WebSocket.CONNECTING) {
|
|
1442
|
+
clearTimeout(connectTimeout);
|
|
1443
|
+
connectTimeout = null;
|
|
1444
|
+
}
|
|
1132
1445
|
console.error(`[bridge] gateway.error ${sessionId}: ${String(err)}`);
|
|
1133
1446
|
sendBrokerPayload(brokerSocket, {
|
|
1134
1447
|
action: 'log',
|
|
@@ -1270,8 +1583,19 @@ async function startOpenclawBridge(flags) {
|
|
|
1270
1583
|
console.log(`[bridge] client.frame ${sessionId}`);
|
|
1271
1584
|
const sessionBridge = getOrCreateGatewaySession(sessionId);
|
|
1272
1585
|
if (!sessionBridge) return;
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1586
|
+
const prepared = prepareGatewayFrameForLocalGateway(frame, gateway, {
|
|
1587
|
+
connectNonce: sessionBridge.connectNonce,
|
|
1588
|
+
deviceIdentity: gatewayDeviceIdentity,
|
|
1589
|
+
});
|
|
1590
|
+
if (prepared.waitForChallenge) {
|
|
1591
|
+
queueConnectUntilChallenge(sessionId, sessionBridge, frame);
|
|
1592
|
+
console.log(`[bridge] client.frame waiting for challenge ${sessionId}`);
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
if (!prepared.frameText) {
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
const result = forwardFrameToSession(sessionBridge, prepared.frameText);
|
|
1275
1599
|
if (result === 'queued') {
|
|
1276
1600
|
console.log(`[bridge] client.frame queued ${sessionId}`);
|
|
1277
1601
|
} else if (result === 'dropped') {
|
|
@@ -1285,6 +1609,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1285
1609
|
console.log(`[bridge] client.close ${sessionId}`);
|
|
1286
1610
|
const sessionBridge = activeGatewaySockets.get(sessionId);
|
|
1287
1611
|
if (sessionBridge && sessionBridge.socket) {
|
|
1612
|
+
clearChallengeTimer(sessionBridge);
|
|
1288
1613
|
activeGatewaySockets.delete(sessionId);
|
|
1289
1614
|
sessionBridge.socket.close(1000, 'client_closed');
|
|
1290
1615
|
}
|
|
@@ -1300,6 +1625,7 @@ async function startOpenclawBridge(flags) {
|
|
|
1300
1625
|
const reasonText = reason ? reason.toString() : '';
|
|
1301
1626
|
console.log(`[bridge] Broker disconnected (${code}) ${reasonText}`);
|
|
1302
1627
|
for (const [sessionId, sessionBridge] of activeGatewaySockets.entries()) {
|
|
1628
|
+
clearChallengeTimer(sessionBridge);
|
|
1303
1629
|
activeGatewaySockets.delete(sessionId);
|
|
1304
1630
|
try {
|
|
1305
1631
|
sessionBridge.socket.close(1001, 'broker_disconnected');
|
package/openclaw.plugin.json
CHANGED