Ctrl K
ComunidadGuías…

scripts/smoke-phone-relay-auth.cjs

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

Archivo original ↗Preguntar a KiBot
// pnpm build:server && pnpm build:companion
// pnpm exec electron scripts/smoke-phone-relay-auth.cjs
// Real Electron children and pairing registry, disposable home, fake engine.
const { app, utilityProcess } = require("electron");
const assert = require("node:assert/strict");
const { randomBytes } = require("node:crypto");
const { mkdtempSync, writeFileSync, rmSync } = require("node:fs");
const { tmpdir } = require("node:os");
const { join, resolve } = require("node:path");
const { createServer } = require("node:net");
const { once } = require("node:events");
const { setTimeout: delay } = require("node:timers/promises");

const root = resolve(__dirname, "..");
const home = mkdtempSync(join(tmpdir(), "omb-phone-auth-smoke-"));
app.setPath("userData", join(home, "electron"));
const children = [];
let logs = "";
const owner = randomBytes(32).toString("base64url");
const relay = randomBytes(32).toString("base64url");
const control = randomBytes(32).toString("base64url");
const controlHeaders = { "x-openmausbot-control-token": control };
async function freePort() {
  const listener = createServer();
  listener.listen(0, "127.0.0.1");
  await once(listener, "listening");
  const port = listener.address().port;
  await new Promise((done) => listener.close(done));
  return port;
}
async function until(check) {
  const deadline = Date.now() + 40_000;
  do {
    const result = await check();
    if (result) return result;
    await delay(100);
  } while (Date.now() < deadline);
  throw new Error("Fixture timed out");
}
async function api(port, path, method = "GET", body, headers = {}) {
  const response = await fetch(`http://127.0.0.1:${port}${path}`, {
    method, headers: { "content-type": "application/json", ...headers },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
    signal: AbortSignal.timeout(5000),
  });
  return { status: response.status, body: await response.json() };
}
function fork(entry, env) {
  const child = utilityProcess.fork(join(root, entry), [], {
    cwd: root, env, stdio: "pipe",
  });
  children.push(child);
  child.stdout.on("data", (chunk) => { logs += chunk; });
  child.stderr.on("data", (chunk) => { logs += chunk; });
  return child;
}
app.whenReady().then(async () => {
  const ports = new Set();
  while (ports.size < 4) ports.add(await freePort());
  const [harnessPort, webhookPort, phonePort, controlPort] = [...ports];
  writeFileSync(join(home, "config.json"), JSON.stringify({ instances: {
    claude: { driver: "claudeAgent", config: { cli: join(root, "server/testing/fake-claude-cli.ts") } },
  } }));
  const env = {
    HOME: home, USERPROFILE: home, OMB_DATA_DIR: home, PATH: "",
    ...(process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {}),
    OMB_PORT: String(harnessPort), OMB_WEBHOOK_PORT: String(webhookPort),
    OMB_COMPANION_PORT: String(phonePort), OMB_CONTROL_PORT: String(controlPort),
    FAKE_CLAUDE_MODE: "happy",
  };
  const harness = fork("dist-server/index.js", { ...env, OMB_DESKTOP_PARENT: "1" });
  await until(() => api(harnessPort, "/api/health").catch(() => null));
  assert.equal((await api(harnessPort, "/api/bots", "POST", {})).status, 403);
  harness.postMessage({ type: "openmausbot:desktop-mutation-token", token: owner, companionToken: relay });
  const sidecar = fork("dist-companion/index.js", env);
  // The app hands over the control token at spawn, as Electron main does; the relay credential comes later.
  sidecar.once("spawn", () => sidecar.postMessage({ type: "openmausbot:companion-mutation-token", controlToken: control }));
  await until(() => api(controlPort, "/state", "GET", undefined, controlHeaders).catch(() => null));
  assert.equal((await api(controlPort, "/pairing", "POST")).status, 403, "control needs the app's token");
  const pairing = await api(controlPort, "/pairing", "POST", undefined, controlHeaders);
  assert.equal(pairing.status, 201);
  const paired = await api(phonePort, "/api/pair", "POST", { credential: pairing.body.token, deviceName: "Fixture phone" });
  assert.equal(paired.status, 201);
  const phoneHeaders = { authorization: `Bearer ${paired.body.token}` };
  const phone = (path, method = "GET", body, extra = {}) => api(phonePort, path, method, body, { ...phoneHeaders, ...extra });
  assert.equal((await phone("/api/bots", "POST", {})).status, 503, "bootstrap must fail closed");
  // Reproduce the old relay: marker/device alone cannot authorize a mutation.
  assert.equal((await api(harnessPort, "/api/bots", "POST", {}, {
    "x-openmausbot-companion": "1", "x-openmausbot-companion-device": paired.body.device.id,
  })).status, 403);
  sidecar.postMessage({ type: "openmausbot:companion-mutation-token", token: relay });
  await until(async () => (await phone("/api/bots")).status === 200);
  const created = await phone("/api/bots", "POST", { modelSelection: { instanceId: "claude", model: "claude-sonnet-5" } }, {
    "x-openmausbot-companion-auth": "forged", "x-openmausbot-desktop-owner": "forged",
    "x-openmausbot-companion-device": "forged-device",
  });
  assert.equal(created.status, 201);
  const id = created.body.bot.id;
  assert.equal((await phone(`/api/bots/${id}/read`, "POST", {})).status, 200);
  assert.equal((await phone(`/api/bots/${id}/messages`, "POST", { text: "Hello from the paired phone" })).status, 202);
  const bot = await until(async () => {
    const result = await phone("/api/bots");
    const current = result.body.bots.find((b) => b.id === id);
    return current && !current.busy && current.messages.some((m) => m.text?.includes("hello from fake claude")) && current;
  });
  assert.ok(bot.messages.some((m) => m.text?.includes("Hello from the paired phone")));
  // An app restart must not make an existing phone pair again. The registry
  // persists the phone identity while the private relay credential rotates.
  const sidecarExited = once(sidecar, "exit");
  sidecar.kill();
  await sidecarExited;
  const nextRelay = randomBytes(32).toString("base64url");
  harness.postMessage({ type: "openmausbot:desktop-mutation-token", token: owner, companionToken: nextRelay });
  const restarted = fork("dist-companion/index.js", env);
  restarted.once("spawn", () => restarted.postMessage({ type: "openmausbot:companion-mutation-token", token: nextRelay, controlToken: control }));
  await until(() => phone("/api/bots").then((r) => r.status === 200).catch(() => false));
  assert.equal((await phone(`/api/bots/${id}/read`, "POST", {})).status, 200);
  assert.equal((await api(phonePort, `/api/bots/${id}/read`, "POST", {})).status, 401);
  assert.equal((await phone("/api/config", "PUT", {})).status, 403);
  assert.equal((await phone("/api/internal/anything", "POST", {})).status, 404);
  assert.equal((await api(controlPort, `/devices/${paired.body.device.id}`, "DELETE", undefined, controlHeaders)).status, 200);
  assert.equal((await phone(`/api/bots/${id}/read`, "POST", {})).status, 401);
  console.log(JSON.stringify({ passed: true, realPairing: true, old403Reproduced: true,
    botCreated: true, readMarked: true, replyVerified: true, forgedHeadersIgnored: true,
    unpairedAndRevokedDenied: true, bootstrapFailsClosed: true, existingPairingAfterRestart: true, controlTokenRequired: true }));
}).catch((error) => {
  console.error(error);
  process.exitCode = 1;
}).finally(async () => {
  const evidence = join(tmpdir(), `omb-phone-auth-${Date.now()}.log`);
  writeFileSync(evidence, logs.replaceAll(owner, "[redacted]").replaceAll(relay, "[redacted]").replaceAll(control, "[redacted]"));
  console.log(`Fixture log: ${evidence}`);
  for (const child of children.reverse()) {
    if (!child.pid) continue;
    const exited = once(child, "exit");
    child.kill();
    await Promise.race([exited, delay(5000)]);
  }
  rmSync(home, { recursive: true, force: true });
  app.exit(process.exitCode || 0);
});