Ctrl K
ComunidadGuías…

inject-device-identity.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/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]);
  }
}