diff --git a/scripts/check/check-install-upgrade.mjs b/scripts/check/check-install-upgrade.mjs index a71ccdb1fe..11cdcf9816 100644 --- a/scripts/check/check-install-upgrade.mjs +++ b/scripts/check/check-install-upgrade.mjs @@ -76,7 +76,9 @@ const INTERNAL_SERVICE_HEADER = "x-omniroute-internal-service-token"; * packaged CLI. Sent alongside the internal-service token so a build that only honours one * of the two still answers with the full payload. */ -function derivePackagedCliToken(prefix) { +// The CLI token salt lives under (#13679/#13909): derive against the DATA_DIR the +// probed server boots on, or the token never matches. +function derivePackagedCliToken(prefix, dataDir) { const cliModuleUrl = pathToFileURL( path.join(packageRootFor(prefix), "bin", "cli", "utils", "cliToken.mjs") ).href; @@ -89,7 +91,7 @@ function derivePackagedCliToken(prefix) { "import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))", cliModuleUrl, ], - { encoding: "utf8", env: { ...process.env } } + { encoding: "utf8", env: { ...process.env, DATA_DIR: dataDir } } ).trim(); } catch { // A truncated/broken install cannot derive a token. Returning null keeps the boot @@ -176,7 +178,7 @@ async function bootAndProbe({ prefix, dataDir, port, expectVersion, label }) { if (!fs.existsSync(binPath)) { return { ok: false, failures: [`${label}: bin not found at ${binPath}`], tail: [] }; } - const cliToken = derivePackagedCliToken(prefix); + const cliToken = derivePackagedCliToken(prefix, dataDir); const probeHeaders = { [INTERNAL_SERVICE_HEADER]: INTERNAL_SERVICE_TOKEN, ...(cliToken ? { "x-omniroute-cli-token": cliToken } : {}), diff --git a/scripts/check/check-pack-boot.mjs b/scripts/check/check-pack-boot.mjs index 2ca4097a0a..33f85b6ef4 100644 --- a/scripts/check/check-pack-boot.mjs +++ b/scripts/check/check-pack-boot.mjs @@ -311,7 +311,13 @@ function spawnServer(binPath, port, dataDir) { return { child, tail }; } -function derivePackagedCliToken(packageRoot) { +/** + * Derive the loopback machine token exactly as the installed CLI would for this install. + * Since #13679/#13909 the token salt is per-install, persisted under , so the + * derivation MUST see the same DATA_DIR the server boots on — otherwise the CLI resolves + * a different salt, the server rejects the token and health serves the anonymous view. + */ +export function derivePackagedCliToken(packageRoot, dataDir) { const cliModuleUrl = pathToFileURL( path.join(packageRoot, "bin", "cli", "utils", "cliToken.mjs") ).href; @@ -323,7 +329,7 @@ function derivePackagedCliToken(packageRoot) { "import(process.argv[1]).then(async m => process.stdout.write(await m.getCliToken()))", cliModuleUrl, ], - { encoding: "utf8", env: { ...process.env } } + { encoding: "utf8", env: { ...process.env, DATA_DIR: dataDir } } ).trim(); } @@ -454,7 +460,7 @@ async function main() { const dataDir = path.join(tmp, "data"); fs.mkdirSync(dataDir, { recursive: true }); const binPath = path.join(prefix, "bin", "omniroute"); - const packagedCliToken = derivePackagedCliToken(packageRoot); + const packagedCliToken = derivePackagedCliToken(packageRoot, dataDir); // BOOT #1 — boot, prove the forced sql.js tier, PATCH a setting, then shut down cleanly // so the sql.js adapter's graceful persist actually lands on disk. The in-flow stopChild diff --git a/tests/unit/check-pack-boot.test.ts b/tests/unit/check-pack-boot.test.ts index 2a7e0edf07..8c8bce2617 100644 --- a/tests/unit/check-pack-boot.test.ts +++ b/tests/unit/check-pack-boot.test.ts @@ -1,7 +1,8 @@ import { test } from "node:test"; import assert from "node:assert/strict"; import { createHmac } from "node:crypto"; -import { readFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { @@ -15,6 +16,7 @@ import { evaluateMachineTokenAuth, evaluateSqlJsRoundTrip, evaluateRestartPersistence, + derivePackagedCliToken, } from "../../scripts/check/check-pack-boot.mjs"; // WS1.2 (T1, v3.8.49 quality plan) — pure-function guards for the tarball boot-smoke @@ -244,3 +246,33 @@ test("source guard: final shutdown only deletes the workspace after a CONFIRMED "stopChild/waitForHealthy must read authoritative exit state, not a stale boolean" ); }); + +// #13679/#13909 made the CLI token salt per-install, persisted under . The +// smoke boots the server on an isolated DATA_DIR, so the token it sends must be derived +// against that SAME DATA_DIR — derived without it, the CLI resolves a different salt, +// the server rejects the token, and /api/monitoring/health answers the anonymous view +// ("version undefined", release PR #11442 Package Artifact). +test("derivePackagedCliToken derives the token against the server's DATA_DIR salt", () => { + const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + const dataDir = mkdtempSync(path.join(os.tmpdir(), "pack-boot-cli-token-")); + const otherDataDir = mkdtempSync(path.join(os.tmpdir(), "pack-boot-cli-token-other-")); + const savedCliSalt = process.env.OMNIROUTE_CLI_SALT; + delete process.env.OMNIROUTE_CLI_SALT; + try { + const token = derivePackagedCliToken(repoRoot, dataDir); + assert.match(token, /^[0-9a-f]{64}$/); + assert.equal( + existsSync(path.join(dataDir, "cli-token-salt.json")), + true, + "the salt must be established inside the smoke's DATA_DIR" + ); + // Stable for the same DATA_DIR, different for another install's DATA_DIR. + assert.equal(derivePackagedCliToken(repoRoot, dataDir), token); + assert.notEqual(derivePackagedCliToken(repoRoot, otherDataDir), token); + } finally { + if (savedCliSalt === undefined) delete process.env.OMNIROUTE_CLI_SALT; + else process.env.OMNIROUTE_CLI_SALT = savedCliSalt; + rmSync(dataDir, { recursive: true, force: true }); + rmSync(otherDataDir, { recursive: true, force: true }); + } +});