inject-device-identity.js
Rama main · réplica de lectura del repositorio original.
#!/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]);
}
}