forked from MCERQUA/OpenVoiceUI
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinject-device-identity.js
More file actions
189 lines (164 loc) · 7.43 KB
/
Copy pathinject-device-identity.js
File metadata and controls
189 lines (164 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env node
// inject-device-identity.js — Sync device identity between OpenVoiceUI and OpenClaw.
//
// Called by start.js AFTER containers are up but BEFORE the user opens the browser.
//
// Handles two scenarios:
// 1. Fresh install (no identity in volume): inject pre-paired identity
// 2. Reinstall (old identity in volume): read it, update paired.json to match
//
// Windows compatibility:
// - MSYS_NO_PATHCONV=1 prevents Git Bash from mangling /container/paths
// - docker cp avoids single-quote issues with cmd.exe
// - No sh -c with single quotes (Windows doesn't support them)
const { execSync } = require("child_process");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const COMPOSE = "docker compose -f docker-compose.yml -f docker-compose.local.yml";
const IDENTITY_FILE = "openclaw-data/pre-paired-device.json";
const PAIRED_FILE = "openclaw-data/devices/paired.json";
const CONTAINER_PATH = "/app/runtime/uploads/.device-identity.json";
// MSYS_NO_PATHCONV=1 prevents Git Bash on Windows from converting
// /app/runtime/... to E:/pinokio/bin/.../app/runtime/...
const EXEC_ENV = Object.assign({}, process.env, { MSYS_NO_PATHCONV: "1" });
function exec(cmd, opts) {
return execSync(cmd, Object.assign(
{ encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"], env: EXEC_ENV },
opts || {}
)).trim();
}
function writePairedJson(deviceId, publicKeyPem) {
// Derive base64url public key from PEM
var pubPemLines = publicKeyPem.split("\n").filter(function(l) {
return !l.startsWith("---") && l.trim();
});
var derBuf = Buffer.from(pubPemLines.join(""), "base64");
// Ed25519 SPKI DER = 12 byte header + 32 byte raw key
var rawPub = derBuf.slice(-32);
var pubB64url = rawPub.toString("base64url");
var nowMs = Date.now();
var pairingToken = crypto.randomBytes(32).toString("hex");
// Read existing paired.json so we don't clobber other paired devices
var paired = {};
try {
var existingPaired = fs.readFileSync(PAIRED_FILE, "utf8");
paired = JSON.parse(existingPaired);
} catch (e) { /* no existing file, start fresh */ }
// Fix scopes on ALL existing paired devices (repairs installs from before this fix)
for (var did in paired) {
var dev = paired[did];
if (dev.clientId === "openclaw-probe") continue;
var scopes = dev.scopes || [];
if (scopes.indexOf("operator.admin") === -1) {
var fullScopes = ["operator.admin", "operator.read", "operator.write"];
dev.scopes = fullScopes;
dev.approvedScopes = fullScopes;
if (dev.tokens && dev.tokens.operator) {
dev.tokens.operator.scopes = fullScopes;
}
}
}
paired[deviceId] = {
deviceId: deviceId,
publicKey: pubB64url,
displayName: "pinokio-openvoiceui",
platform: "linux",
clientId: "cli",
clientMode: "cli",
role: "operator",
roles: ["operator"],
scopes: ["operator.admin", "operator.read", "operator.write"],
approvedScopes: ["operator.admin", "operator.read", "operator.write"],
tokens: {
operator: {
token: pairingToken,
role: "operator",
scopes: ["operator.admin", "operator.read", "operator.write"],
createdAtMs: nowMs,
},
},
createdAtMs: nowMs,
approvedAtMs: nowMs,
};
var pairedContent = JSON.stringify(paired, null, 2) + "\n";
// Try direct write first (works when current user owns openclaw-data/devices/)
try {
fs.mkdirSync("openclaw-data/devices", { recursive: true });
fs.writeFileSync(PAIRED_FILE, pairedContent);
fs.writeFileSync("openclaw-data/devices/pending.json", "{}\n");
return;
} catch (e) {
if (e.code !== "EACCES") throw e;
}
// Fallback: openclaw container created devices/ as root via bind mount.
// Write via docker cp into the container — bind mount reflects it on host too.
console.log(" devices/ is root-owned — writing via docker cp");
var os = require("os");
var tmpPaired = path.join(os.tmpdir(), "ovu-paired.json");
var tmpPending = path.join(os.tmpdir(), "ovu-pending.json");
fs.writeFileSync(tmpPaired, pairedContent);
fs.writeFileSync(tmpPending, "{}\n");
var cid = exec(COMPOSE + " ps -q openclaw");
if (!cid) throw new Error("openclaw container not found for docker cp fallback");
exec("docker cp " + JSON.stringify(tmpPaired) + " " + cid + ":/root/.openclaw/devices/paired.json");
exec("docker cp " + JSON.stringify(tmpPending) + " " + cid + ":/root/.openclaw/devices/pending.json");
try { fs.unlinkSync(tmpPaired); } catch (e2) { /* ignore */ }
try { fs.unlinkSync(tmpPending); } catch (e2) { /* ignore */ }
}
// Step 1: Check if the container already has a device identity (from a previous run)
var containerIdentity = null;
try {
var out = exec(COMPOSE + " exec -T openvoiceui cat " + CONTAINER_PATH);
if (out && out.startsWith("{")) {
containerIdentity = JSON.parse(out);
if (!containerIdentity.deviceId) containerIdentity = null;
}
} catch (e) {
// File doesn't exist — fresh volume
}
if (containerIdentity) {
// Scenario 2: Container already has an identity (Docker volume persisted it).
// Update paired.json to match whatever the container has.
console.log(" Found existing device identity: " + containerIdentity.deviceId.slice(0, 16) + "...");
try {
writePairedJson(containerIdentity.deviceId, containerIdentity.publicKeyPem);
console.log(" Updated paired.json to match container identity");
// Save locally so they stay in sync
fs.writeFileSync(IDENTITY_FILE, JSON.stringify(containerIdentity, null, 2) + "\n");
// No restart needed — openclaw-data/ is a bind mount so paired.json
// changes are immediately visible inside the container. OpenClaw reads
// paired.json on each WS connection attempt (getPairedDevice()), not
// just at startup. DO NOT restart openclaw here — openvoiceui uses
// network_mode: "service:openclaw" so restarting openclaw kills both.
console.log(" Device is now paired (no restart needed — bind mount is live)");
} catch (e) {
console.log(" Warning: could not sync identity: " + e.message);
}
} else {
// Scenario 1: Fresh install — no identity in container yet.
// Inject the pre-paired identity that setup-config.js generated.
if (!fs.existsSync(IDENTITY_FILE)) {
console.log(" No pre-paired device identity found — skipping");
process.exit(0);
}
var identity = fs.readFileSync(IDENTITY_FILE, "utf8");
try {
// Get the container ID for docker cp (avoids shell quoting issues on Windows)
var containerId = exec(COMPOSE + " ps -q openvoiceui");
if (!containerId) throw new Error("openvoiceui container not found");
// Ensure uploads dir exists (use double quotes, not single quotes — Windows compat)
exec(COMPOSE + ' exec -T openvoiceui mkdir -p /app/runtime/uploads');
// Write identity to a temp file and docker cp it in
// docker cp avoids all shell quoting and stdin piping issues
var tmpFile = path.join("openclaw-data", ".tmp-device-identity.json");
fs.writeFileSync(tmpFile, identity);
exec("docker cp " + JSON.stringify(tmpFile) + " " + containerId + ":" + CONTAINER_PATH);
// Clean up temp file
try { fs.unlinkSync(tmpFile); } catch (e) { /* ignore */ }
var deviceId = JSON.parse(identity).deviceId || "unknown";
console.log(" Injected pre-paired device identity: " + deviceId.slice(0, 16) + "...");
} catch (e) {
console.log(" Warning: Could not inject device identity: " + e.message.split("\n")[0]);
}
}