opencami 1.8.2 → 1.8.3
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 +163 -5
- package/bin/opencami.js +15 -6
- package/dist/server/assets/{_sessionKey-Bq_fl7uv.js → _sessionKey-C9o7YfxA.js} +3 -3
- package/dist/server/assets/{index-C2hVqxBl.js → index-Bw-bA_2M.js} +3 -3
- package/dist/server/assets/{router-bN_iTo0B.js → router-DCjikH21.js} +221 -20
- package/dist/server/assets/{search-dialog-DReM5ZD2.js → search-dialog-BnwiXpdA.js} +4 -4
- package/dist/server/assets/{settings-dialog-BUOrQN3Z.js → settings-dialog-ClKFnZ1x.js} +4 -4
- package/dist/server/server.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,16 +15,28 @@ A beautiful web client for [OpenClaw](https://github.com/openclaw/openclaw).
|
|
|
15
15
|
curl -fsSL https://opencami.xyz/install.sh | bash
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Then open `http://localhost:3000` (or your configured host/port).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Install (curl)
|
|
23
|
+
|
|
24
|
+
The recommended install flow is:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
curl -fsSL https://opencami.xyz/install.sh | bash
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This installs OpenCami and prints next-step instructions for required environment variables.
|
|
31
|
+
|
|
32
|
+
Alternative install:
|
|
19
33
|
|
|
20
34
|
```bash
|
|
21
35
|
npm install -g opencami
|
|
22
36
|
opencami
|
|
23
37
|
```
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
### Options
|
|
39
|
+
CLI options:
|
|
28
40
|
|
|
29
41
|
| Flag | Description | Default |
|
|
30
42
|
|------|-------------|---------|
|
|
@@ -33,13 +45,159 @@ Opens your browser to the chat interface.
|
|
|
33
45
|
| `--host` | Bind address | `localhost` |
|
|
34
46
|
| `--no-open` | Don't open browser | — |
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
> Note: `--gateway` sets `OPENCLAW_GATEWAY` internally. For predictable deployments, set `CLAWDBOT_GATEWAY_URL` explicitly in environment.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Configuration (OpenCami environment)
|
|
53
|
+
|
|
54
|
+
Set these in your shell, service manager, or container environment.
|
|
55
|
+
|
|
56
|
+
### Required
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
CLAWDBOT_GATEWAY_URL=ws://127.0.0.1:18789
|
|
60
|
+
# pick ONE auth method:
|
|
61
|
+
CLAWDBOT_GATEWAY_TOKEN=...
|
|
62
|
+
# or
|
|
63
|
+
CLAWDBOT_GATEWAY_PASSWORD=...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Required for remote Tailnet / origin allowlist setups
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Must exactly match the browser origin used to access OpenCami
|
|
70
|
+
# Example: https://openclaw-server.tailXXXX.ts.net:3001
|
|
71
|
+
OPENCAMI_ORIGIN=https://openclaw-server.tailXXXX.ts.net:3001
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Optional
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Enables compatibility fallback if strict device-auth connect fails
|
|
78
|
+
# Default is strict mode (fallback disabled)
|
|
79
|
+
OPENCAMI_DEVICE_AUTH_FALLBACK=true
|
|
80
|
+
|
|
81
|
+
FILES_ROOT=/path/to/workspace
|
|
82
|
+
OPENAI_API_KEY=sk-...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Remote Tailnet setup (OpenClaw + OpenCami)
|
|
88
|
+
|
|
89
|
+
If OpenCami loads remotely but you see **"origin not allowed"** or gateway connect failures, configure both sides.
|
|
90
|
+
|
|
91
|
+
### 1) OpenClaw gateway config (`gateway.controlUi.allowedOrigins`)
|
|
92
|
+
|
|
93
|
+
In your OpenClaw config, allow the exact OpenCami browser origin:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"gateway": {
|
|
98
|
+
"controlUi": {
|
|
99
|
+
"allowedOrigins": [
|
|
100
|
+
"https://openclaw-server.tailXXXX.ts.net:3001"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 2) OpenCami server env (`OPENCAMI_ORIGIN`)
|
|
108
|
+
|
|
109
|
+
Set `OPENCAMI_ORIGIN` to the same exact value:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
OPENCAMI_ORIGIN=https://openclaw-server.tailXXXX.ts.net:3001
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 3) Restart gateway
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
openclaw gateway restart
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
> If you access OpenCami via multiple hostnames/ports, each distinct origin must be listed in `allowedOrigins`.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Device auth notes (strict vs fallback)
|
|
126
|
+
|
|
127
|
+
OpenCami uses strict device-auth-compatible connect params by default.
|
|
128
|
+
|
|
129
|
+
- **Strict mode (default):** `OPENCAMI_DEVICE_AUTH_FALLBACK` unset/false
|
|
130
|
+
- **Fallback mode (compatibility):** set `OPENCAMI_DEVICE_AUTH_FALLBACK=true`
|
|
131
|
+
|
|
132
|
+
Fallback mode retries connect without device identity metadata if strict handshake fails.
|
|
133
|
+
Use fallback only when needed for compatibility, and prefer strict mode long-term.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Security notes
|
|
138
|
+
|
|
139
|
+
- Prefer `wss://` for remote connections.
|
|
140
|
+
- Prefer token auth (`CLAWDBOT_GATEWAY_TOKEN`) over password.
|
|
141
|
+
- Keep `allowedOrigins` minimal (exact origins only, no wildcards).
|
|
142
|
+
- Treat `OPENCAMI_DEVICE_AUTH_FALLBACK=true` as temporary compatibility mode.
|
|
143
|
+
- Do **not** expose OpenCami directly to the public internet without TLS + access controls.
|
|
144
|
+
- For Tailnet deployments, limit Tailnet device/user access.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Troubleshooting
|
|
149
|
+
|
|
150
|
+
### "origin not allowed"
|
|
151
|
+
|
|
152
|
+
Cause: gateway rejected browser origin.
|
|
153
|
+
|
|
154
|
+
Fix:
|
|
155
|
+
1. Add origin to `gateway.controlUi.allowedOrigins`
|
|
156
|
+
2. Set identical `OPENCAMI_ORIGIN` in OpenCami env
|
|
157
|
+
3. Restart gateway (`openclaw gateway restart`)
|
|
158
|
+
|
|
159
|
+
### Missing scope `operator.read`
|
|
160
|
+
|
|
161
|
+
Cause: gateway auth succeeded but token/permissions did not include required operator scope.
|
|
162
|
+
|
|
163
|
+
Fix:
|
|
164
|
+
- Use a token/password with operator access
|
|
165
|
+
- Verify gateway auth/scopes in OpenClaw
|
|
166
|
+
- Reconnect after updating credentials
|
|
167
|
+
|
|
168
|
+
### Pairing required / device auth connect issues
|
|
169
|
+
|
|
170
|
+
If strict connect fails in your deployment:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
OPENCAMI_DEVICE_AUTH_FALLBACK=true
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Then restart OpenCami and retry. Keep this as a compatibility fallback, not the default.
|
|
177
|
+
|
|
178
|
+
### Can’t connect to gateway at all
|
|
179
|
+
|
|
180
|
+
Checks:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
openclaw gateway status
|
|
184
|
+
echo "$CLAWDBOT_GATEWAY_URL"
|
|
185
|
+
echo "$CLAWDBOT_GATEWAY_TOKEN"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Also verify URL scheme (`ws://` local, `wss://` remote).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Docker
|
|
37
193
|
|
|
38
194
|
```bash
|
|
39
195
|
docker build -t opencami .
|
|
40
196
|
docker run -p 3000:3000 opencami
|
|
41
197
|
```
|
|
42
198
|
|
|
199
|
+
---
|
|
200
|
+
|
|
43
201
|
## Features
|
|
44
202
|
|
|
45
203
|
### 💬 Chat & Communication
|
package/bin/opencami.js
CHANGED
|
@@ -18,6 +18,9 @@ function getArg(name, def) {
|
|
|
18
18
|
const port = parseInt(getArg('port', '3000'), 10);
|
|
19
19
|
const host = getArg('host', '127.0.0.1');
|
|
20
20
|
const gateway = getArg('gateway', 'ws://127.0.0.1:18789');
|
|
21
|
+
const token = getArg('token', '');
|
|
22
|
+
const password = getArg('password', '');
|
|
23
|
+
const origin = getArg('origin', '');
|
|
21
24
|
const noOpen = args.includes('--no-open');
|
|
22
25
|
|
|
23
26
|
if (args.includes('--help') || args.includes('-h')) {
|
|
@@ -27,17 +30,23 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
27
30
|
Usage: opencami [options]
|
|
28
31
|
|
|
29
32
|
Options:
|
|
30
|
-
--port <n>
|
|
31
|
-
--host <addr>
|
|
32
|
-
--gateway <url>
|
|
33
|
-
--
|
|
34
|
-
|
|
33
|
+
--port <n> Port to listen on (default: 3000)
|
|
34
|
+
--host <addr> Host to bind to (default: 127.0.0.1)
|
|
35
|
+
--gateway <url> OpenClaw gateway URL (default: ws://127.0.0.1:18789)
|
|
36
|
+
--token <token> Gateway token (sets CLAWDBOT_GATEWAY_TOKEN)
|
|
37
|
+
--password <pw> Gateway password (sets CLAWDBOT_GATEWAY_PASSWORD)
|
|
38
|
+
--origin <url> Origin to send in backend WS (sets OPENCAMI_ORIGIN)
|
|
39
|
+
--no-open Don't open browser on start
|
|
40
|
+
-h, --help Show this help
|
|
35
41
|
`);
|
|
36
42
|
process.exit(0);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
// Set gateway env for the app
|
|
40
|
-
process.env.
|
|
46
|
+
process.env.CLAWDBOT_GATEWAY_URL = gateway;
|
|
47
|
+
if (token) process.env.CLAWDBOT_GATEWAY_TOKEN = token;
|
|
48
|
+
if (password) process.env.CLAWDBOT_GATEWAY_PASSWORD = password;
|
|
49
|
+
if (origin) process.env.OPENCAMI_ORIGIN = origin;
|
|
41
50
|
|
|
42
51
|
const MIME_TYPES = {
|
|
43
52
|
'.html': 'text/html',
|
|
@@ -19,7 +19,7 @@ import { u as useChatSettings$1 } from "./index-Dl2BOKP7.js";
|
|
|
19
19
|
import { create } from "zustand";
|
|
20
20
|
import { persist } from "zustand/middleware";
|
|
21
21
|
import { createPortal } from "react-dom";
|
|
22
|
-
import { a as Route } from "./router-
|
|
22
|
+
import { a as Route } from "./router-DCjikH21.js";
|
|
23
23
|
function deriveFriendlyIdFromKey(key) {
|
|
24
24
|
if (!key) return "main";
|
|
25
25
|
const trimmed = key.trim();
|
|
@@ -1681,7 +1681,7 @@ function areSidebarSessionsEqual(prev, next) {
|
|
|
1681
1681
|
return true;
|
|
1682
1682
|
}
|
|
1683
1683
|
const SettingsDialog = lazy(
|
|
1684
|
-
() => import("./settings-dialog-
|
|
1684
|
+
() => import("./settings-dialog-ClKFnZ1x.js").then((m) => ({ default: m.SettingsDialog }))
|
|
1685
1685
|
);
|
|
1686
1686
|
const SessionExportDialog = lazy(
|
|
1687
1687
|
() => import("./session-export-dialog-C53RRAah.js").then((m) => ({
|
|
@@ -6596,7 +6596,7 @@ const KeyboardShortcutsDialog = lazy(
|
|
|
6596
6596
|
}))
|
|
6597
6597
|
);
|
|
6598
6598
|
const SearchDialog = lazy(
|
|
6599
|
-
() => import("./search-dialog-
|
|
6599
|
+
() => import("./search-dialog-BnwiXpdA.js").then((m) => ({
|
|
6600
6600
|
default: m.SearchDialog
|
|
6601
6601
|
}))
|
|
6602
6602
|
);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect } from "react";
|
|
3
|
-
import { R as Route } from "./router-
|
|
3
|
+
import { R as Route } from "./router-DCjikH21.js";
|
|
4
4
|
import "@tanstack/react-router";
|
|
5
5
|
import "@tanstack/react-query";
|
|
6
6
|
import "node:crypto";
|
|
7
|
-
import "ws";
|
|
8
7
|
import "node:fs";
|
|
9
|
-
import "node:path";
|
|
10
8
|
import "node:os";
|
|
9
|
+
import "node:path";
|
|
10
|
+
import "ws";
|
|
11
11
|
import "@tanstack/router-core/ssr/client";
|
|
12
12
|
import "node:stream";
|
|
13
13
|
import "node:child_process";
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createRootRoute, Outlet, HeadContent, Scripts, createFileRoute, lazyRouteComponent, redirect, createRouter } from "@tanstack/react-router";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
5
|
-
import
|
|
6
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
-
import path, { join, resolve, relative, extname } from "node:path";
|
|
4
|
+
import crypto, { randomUUID } from "node:crypto";
|
|
5
|
+
import fs, { readFileSync, existsSync } from "node:fs";
|
|
8
6
|
import os, { homedir } from "node:os";
|
|
7
|
+
import path, { join, resolve, relative, extname } from "node:path";
|
|
8
|
+
import WebSocket from "ws";
|
|
9
9
|
import { json } from "@tanstack/router-core/ssr/client";
|
|
10
10
|
import { PassThrough, Readable } from "node:stream";
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
@@ -369,11 +369,11 @@ const $$splitComponentImporter$2 = () => import("./agents-CmQ4vvXm.js");
|
|
|
369
369
|
const Route$t = createFileRoute("/agents")({
|
|
370
370
|
component: lazyRouteComponent($$splitComponentImporter$2, "component")
|
|
371
371
|
});
|
|
372
|
-
const $$splitComponentImporter$1 = () => import("./index-
|
|
372
|
+
const $$splitComponentImporter$1 = () => import("./index-Bw-bA_2M.js");
|
|
373
373
|
const Route$s = createFileRoute("/")({
|
|
374
374
|
component: lazyRouteComponent($$splitComponentImporter$1, "component")
|
|
375
375
|
});
|
|
376
|
-
const $$splitComponentImporter = () => import("./_sessionKey-
|
|
376
|
+
const $$splitComponentImporter = () => import("./_sessionKey-C9o7YfxA.js").then((n) => n.$);
|
|
377
377
|
const Route$r = createFileRoute("/chat/$sessionKey")({
|
|
378
378
|
component: lazyRouteComponent($$splitComponentImporter, "component")
|
|
379
379
|
});
|
|
@@ -388,24 +388,150 @@ function getGatewayConfig() {
|
|
|
388
388
|
}
|
|
389
389
|
return { url, token, password };
|
|
390
390
|
}
|
|
391
|
-
|
|
391
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
392
|
+
function base64UrlEncode(buf) {
|
|
393
|
+
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
394
|
+
}
|
|
395
|
+
function derivePublicKeyRaw(publicKeyPem) {
|
|
396
|
+
const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
397
|
+
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
398
|
+
return spki.subarray(ED25519_SPKI_PREFIX.length);
|
|
399
|
+
}
|
|
400
|
+
return spki;
|
|
401
|
+
}
|
|
402
|
+
function fingerprintPublicKey(publicKeyPem) {
|
|
403
|
+
const raw = derivePublicKeyRaw(publicKeyPem);
|
|
404
|
+
return crypto.createHash("sha256").update(raw).digest("hex");
|
|
405
|
+
}
|
|
406
|
+
function ensureDir(filePath) {
|
|
407
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
408
|
+
}
|
|
409
|
+
function resolveDeviceIdentityPath() {
|
|
410
|
+
return path.join(os.homedir(), ".opencami", "identity", "device.json");
|
|
411
|
+
}
|
|
412
|
+
function loadOrCreateDeviceIdentity(filePath = resolveDeviceIdentityPath()) {
|
|
413
|
+
try {
|
|
414
|
+
if (fs.existsSync(filePath)) {
|
|
415
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
416
|
+
const parsed = JSON.parse(raw);
|
|
417
|
+
if (parsed?.version === 1 && typeof parsed.deviceId === "string" && typeof parsed.publicKeyPem === "string" && typeof parsed.privateKeyPem === "string") {
|
|
418
|
+
const derivedId = fingerprintPublicKey(parsed.publicKeyPem);
|
|
419
|
+
if (derivedId && derivedId !== parsed.deviceId) {
|
|
420
|
+
const updated = { ...parsed, deviceId: derivedId };
|
|
421
|
+
fs.writeFileSync(filePath, `${JSON.stringify(updated, null, 2)}
|
|
422
|
+
`, { mode: 384 });
|
|
423
|
+
try {
|
|
424
|
+
fs.chmodSync(filePath, 384);
|
|
425
|
+
} catch {
|
|
426
|
+
}
|
|
427
|
+
return { deviceId: derivedId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
428
|
+
}
|
|
429
|
+
return { deviceId: parsed.deviceId, publicKeyPem: parsed.publicKeyPem, privateKeyPem: parsed.privateKeyPem };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
} catch {
|
|
433
|
+
}
|
|
434
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
435
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" }).toString();
|
|
436
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" }).toString();
|
|
437
|
+
const deviceId = fingerprintPublicKey(publicKeyPem);
|
|
438
|
+
ensureDir(filePath);
|
|
439
|
+
const stored = {
|
|
440
|
+
version: 1,
|
|
441
|
+
deviceId,
|
|
442
|
+
publicKeyPem,
|
|
443
|
+
privateKeyPem,
|
|
444
|
+
createdAtMs: Date.now()
|
|
445
|
+
};
|
|
446
|
+
fs.writeFileSync(filePath, `${JSON.stringify(stored, null, 2)}
|
|
447
|
+
`, { mode: 384 });
|
|
448
|
+
try {
|
|
449
|
+
fs.chmodSync(filePath, 384);
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
return { deviceId, publicKeyPem, privateKeyPem };
|
|
453
|
+
}
|
|
454
|
+
function signDevicePayload(privateKeyPem, payload) {
|
|
455
|
+
const key = crypto.createPrivateKey(privateKeyPem);
|
|
456
|
+
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
|
|
457
|
+
}
|
|
458
|
+
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
|
|
459
|
+
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
|
|
460
|
+
}
|
|
461
|
+
function buildDeviceAuthPayload(args) {
|
|
462
|
+
const scopes = args.scopes.join(",");
|
|
463
|
+
const token = args.token ?? "";
|
|
464
|
+
return ["v2", args.deviceId, args.clientId, args.clientMode, args.role, scopes, String(args.signedAtMs), token, args.nonce].join("|");
|
|
465
|
+
}
|
|
466
|
+
function loadOrCreateInstanceId() {
|
|
467
|
+
const filePath = path.join(os.homedir(), ".opencami", "identity", "instance-id.txt");
|
|
468
|
+
try {
|
|
469
|
+
if (fs.existsSync(filePath)) {
|
|
470
|
+
const v2 = fs.readFileSync(filePath, "utf8").trim();
|
|
471
|
+
if (v2) return v2;
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
const v = randomUUID();
|
|
476
|
+
ensureDir(filePath);
|
|
477
|
+
fs.writeFileSync(filePath, `${v}
|
|
478
|
+
`, { mode: 384 });
|
|
479
|
+
try {
|
|
480
|
+
fs.chmodSync(filePath, 384);
|
|
481
|
+
} catch {
|
|
482
|
+
}
|
|
483
|
+
return v;
|
|
484
|
+
}
|
|
485
|
+
function buildConnectParams(token, password, nonce) {
|
|
486
|
+
const clientId = "openclaw-control-ui";
|
|
487
|
+
const clientMode = "webchat";
|
|
488
|
+
const role = "operator";
|
|
489
|
+
const scopes = ["operator.read", "operator.write"];
|
|
490
|
+
if (!nonce) {
|
|
491
|
+
throw new Error(
|
|
492
|
+
"OpenClaw did not send connect.challenge nonce in time. If you are connecting cross-origin, ensure your origin is allowed (gateway.controlUi.allowedOrigins)."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const identity = loadOrCreateDeviceIdentity();
|
|
496
|
+
const signedAtMs = Date.now();
|
|
497
|
+
const payload = buildDeviceAuthPayload({
|
|
498
|
+
deviceId: identity.deviceId,
|
|
499
|
+
clientId,
|
|
500
|
+
clientMode,
|
|
501
|
+
role,
|
|
502
|
+
scopes,
|
|
503
|
+
signedAtMs,
|
|
504
|
+
token: token || null,
|
|
505
|
+
nonce
|
|
506
|
+
});
|
|
507
|
+
const signature = signDevicePayload(identity.privateKeyPem, payload);
|
|
392
508
|
return {
|
|
393
509
|
minProtocol: 3,
|
|
394
510
|
maxProtocol: 3,
|
|
395
511
|
client: {
|
|
396
|
-
id:
|
|
512
|
+
id: clientId,
|
|
397
513
|
displayName: "OpenCami",
|
|
398
514
|
version: "dev",
|
|
399
515
|
platform: process.platform,
|
|
400
|
-
mode:
|
|
401
|
-
instanceId:
|
|
516
|
+
mode: clientMode,
|
|
517
|
+
instanceId: loadOrCreateInstanceId()
|
|
402
518
|
},
|
|
519
|
+
caps: [],
|
|
403
520
|
auth: {
|
|
404
521
|
token: token || void 0,
|
|
405
522
|
password: password || void 0
|
|
406
523
|
},
|
|
407
524
|
role: "operator",
|
|
408
|
-
scopes
|
|
525
|
+
scopes,
|
|
526
|
+
device: {
|
|
527
|
+
id: identity.deviceId,
|
|
528
|
+
publicKey: publicKeyRawBase64UrlFromPem(identity.publicKeyPem),
|
|
529
|
+
signature,
|
|
530
|
+
signedAt: signedAtMs,
|
|
531
|
+
nonce
|
|
532
|
+
},
|
|
533
|
+
userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
|
|
534
|
+
locale: process.env.LANG || "en"
|
|
409
535
|
};
|
|
410
536
|
}
|
|
411
537
|
class PersistentGatewayConnection {
|
|
@@ -440,7 +566,8 @@ class PersistentGatewayConnection {
|
|
|
440
566
|
async _connect() {
|
|
441
567
|
if (this.destroyed) throw new Error("Connection destroyed");
|
|
442
568
|
const { url, token, password } = getGatewayConfig();
|
|
443
|
-
const
|
|
569
|
+
const origin = process.env.OPENCAMI_ORIGIN?.trim();
|
|
570
|
+
const ws = origin ? new WebSocket(url, { headers: { Origin: origin } }) : new WebSocket(url);
|
|
444
571
|
this.ws = ws;
|
|
445
572
|
await new Promise((resolve2, reject) => {
|
|
446
573
|
const onOpen = () => {
|
|
@@ -458,19 +585,93 @@ class PersistentGatewayConnection {
|
|
|
458
585
|
ws.on("open", onOpen);
|
|
459
586
|
ws.on("error", onError);
|
|
460
587
|
});
|
|
588
|
+
const nonce = await new Promise((resolve2) => {
|
|
589
|
+
let done = false;
|
|
590
|
+
const timer = setTimeout(() => {
|
|
591
|
+
if (done) return;
|
|
592
|
+
done = true;
|
|
593
|
+
resolve2("");
|
|
594
|
+
}, 3e3);
|
|
595
|
+
const onMessage = (data) => {
|
|
596
|
+
try {
|
|
597
|
+
const str = typeof data === "string" ? data : data.toString();
|
|
598
|
+
const parsed = JSON.parse(str);
|
|
599
|
+
if (parsed.type === "event" && parsed.event === "connect.challenge") {
|
|
600
|
+
const n = parsed.payload?.nonce;
|
|
601
|
+
if (typeof n === "string" && n.length > 0) {
|
|
602
|
+
clearTimeout(timer);
|
|
603
|
+
ws.off("message", onMessage);
|
|
604
|
+
if (done) return;
|
|
605
|
+
done = true;
|
|
606
|
+
resolve2(n);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
} catch {
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
ws.on("message", onMessage);
|
|
613
|
+
});
|
|
461
614
|
ws.on("message", (data) => this._onMessage(data));
|
|
462
615
|
ws.on("close", () => this._onClose());
|
|
463
616
|
ws.on("error", () => {
|
|
464
617
|
});
|
|
465
618
|
const connectId = randomUUID();
|
|
466
|
-
const
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
619
|
+
const shouldFallback = process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "1" || process.env.OPENCAMI_DEVICE_AUTH_FALLBACK === "true";
|
|
620
|
+
try {
|
|
621
|
+
const connectParams = buildConnectParams(token, password, nonce);
|
|
622
|
+
ws.send(
|
|
623
|
+
JSON.stringify({
|
|
624
|
+
type: "req",
|
|
625
|
+
id: connectId,
|
|
626
|
+
method: "connect",
|
|
627
|
+
params: connectParams
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
const hello = await this._waitForRes(connectId, 1e4);
|
|
631
|
+
const grantedScopes = hello?.auth?.scopes;
|
|
632
|
+
if (Array.isArray(grantedScopes) && !grantedScopes.includes("operator.read")) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Gateway connected but missing required scope: operator.read (granted: ${grantedScopes.join(", ")})`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
} catch (err) {
|
|
638
|
+
if (!shouldFallback) throw err;
|
|
639
|
+
console.warn(
|
|
640
|
+
"[gateway-ws] Device auth connect failed; retrying without device identity (fallback enabled):",
|
|
641
|
+
err instanceof Error ? err.message : err
|
|
642
|
+
);
|
|
643
|
+
const fallbackId = randomUUID();
|
|
644
|
+
const fallbackParams = {
|
|
645
|
+
minProtocol: 3,
|
|
646
|
+
maxProtocol: 3,
|
|
647
|
+
client: {
|
|
648
|
+
id: "openclaw-control-ui",
|
|
649
|
+
displayName: "OpenCami",
|
|
650
|
+
version: "dev",
|
|
651
|
+
platform: process.platform,
|
|
652
|
+
mode: "webchat",
|
|
653
|
+
instanceId: loadOrCreateInstanceId()
|
|
654
|
+
},
|
|
655
|
+
caps: [],
|
|
656
|
+
auth: {
|
|
657
|
+
token: token || void 0,
|
|
658
|
+
password: password || void 0
|
|
659
|
+
},
|
|
660
|
+
role: "operator",
|
|
661
|
+
scopes: ["operator.read", "operator.write"],
|
|
662
|
+
userAgent: `opencami/${process.env.npm_package_version ?? "dev"} (node ${process.version})`,
|
|
663
|
+
locale: process.env.LANG || "en"
|
|
664
|
+
};
|
|
665
|
+
ws.send(
|
|
666
|
+
JSON.stringify({
|
|
667
|
+
type: "req",
|
|
668
|
+
id: fallbackId,
|
|
669
|
+
method: "connect",
|
|
670
|
+
params: fallbackParams
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
await this._waitForRes(fallbackId, 1e4);
|
|
674
|
+
}
|
|
474
675
|
this.connected = true;
|
|
475
676
|
this.reconnectDelay = 1e3;
|
|
476
677
|
console.log("[gateway-ws] Persistent connection established");
|
|
@@ -5,7 +5,7 @@ import { HugeiconsIcon } from "@hugeicons/react";
|
|
|
5
5
|
import { Search01Icon, Cancel01Icon, Loading03Icon } from "@hugeicons/core-free-icons";
|
|
6
6
|
import { D as DialogRoot, a as DialogContent } from "./use-file-explorer-state-s7CS50ho.js";
|
|
7
7
|
import { useQueryClient } from "@tanstack/react-query";
|
|
8
|
-
import { c as chatQueryKeys } from "./_sessionKey-
|
|
8
|
+
import { c as chatQueryKeys } from "./_sessionKey-C9o7YfxA.js";
|
|
9
9
|
import { c as cn } from "./button-CwY2OHFj.js";
|
|
10
10
|
import "@base-ui/react/dialog";
|
|
11
11
|
import "zustand";
|
|
@@ -26,12 +26,12 @@ import "remark-gfm";
|
|
|
26
26
|
import "./index-Dl2BOKP7.js";
|
|
27
27
|
import "zustand/middleware";
|
|
28
28
|
import "react-dom";
|
|
29
|
-
import "./router-
|
|
29
|
+
import "./router-DCjikH21.js";
|
|
30
30
|
import "node:crypto";
|
|
31
|
-
import "ws";
|
|
32
31
|
import "node:fs";
|
|
33
|
-
import "node:path";
|
|
34
32
|
import "node:os";
|
|
33
|
+
import "node:path";
|
|
34
|
+
import "ws";
|
|
35
35
|
import "@tanstack/router-core/ssr/client";
|
|
36
36
|
import "node:stream";
|
|
37
37
|
import "node:child_process";
|
|
@@ -8,7 +8,7 @@ import { D as DialogRoot, a as DialogContent, b as DialogTitle, c as DialogDescr
|
|
|
8
8
|
import { S as Switch } from "./switch-BbkUeVDV.js";
|
|
9
9
|
import { T as Tabs, a as TabsList, b as TabsTab } from "./tabs-DDFZob0m.js";
|
|
10
10
|
import { u as useChatSettings } from "./index-Dl2BOKP7.js";
|
|
11
|
-
import { u as useLlmSettings, g as getLlmProviderDefaults } from "./_sessionKey-
|
|
11
|
+
import { u as useLlmSettings, g as getLlmProviderDefaults } from "./_sessionKey-C9o7YfxA.js";
|
|
12
12
|
import "@base-ui/react/merge-props";
|
|
13
13
|
import "@base-ui/react/use-render";
|
|
14
14
|
import "class-variance-authority";
|
|
@@ -35,12 +35,12 @@ import "react-markdown";
|
|
|
35
35
|
import "remark-breaks";
|
|
36
36
|
import "remark-gfm";
|
|
37
37
|
import "react-dom";
|
|
38
|
-
import "./router-
|
|
38
|
+
import "./router-DCjikH21.js";
|
|
39
39
|
import "node:crypto";
|
|
40
|
-
import "ws";
|
|
41
40
|
import "node:fs";
|
|
42
|
-
import "node:path";
|
|
43
41
|
import "node:os";
|
|
42
|
+
import "node:path";
|
|
43
|
+
import "ws";
|
|
44
44
|
import "@tanstack/router-core/ssr/client";
|
|
45
45
|
import "node:stream";
|
|
46
46
|
import "node:child_process";
|
package/dist/server/server.js
CHANGED
|
@@ -656,7 +656,7 @@ function getStartResponseHeaders(opts) {
|
|
|
656
656
|
let entriesPromise;
|
|
657
657
|
let manifestPromise;
|
|
658
658
|
async function loadEntries() {
|
|
659
|
-
const routerEntry = await import("./assets/router-
|
|
659
|
+
const routerEntry = await import("./assets/router-DCjikH21.js").then((n) => n.r);
|
|
660
660
|
const startEntry = await import("./assets/start-HYkvq4Ni.js");
|
|
661
661
|
return { startEntry, routerEntry };
|
|
662
662
|
}
|