diff --git a/apps/api/src/cutover-orchestrator.ts b/apps/api/src/cutover-orchestrator.ts index ea08ec7..aed88a2 100644 --- a/apps/api/src/cutover-orchestrator.ts +++ b/apps/api/src/cutover-orchestrator.ts @@ -14,7 +14,7 @@ import { } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { request } from 'node:http'; -import { isAbsolute, join, resolve } from 'node:path'; +import { basename, isAbsolute, join, resolve } from 'node:path'; import { GATEWAY_AUTH_HEADER } from './runtime-config.js'; import { PRODUCTION_CUTOVER_ACK } from './canary-runtime.js'; @@ -185,6 +185,15 @@ function exactKeys( throw new Error(`${label} has an invalid schema`); } +function commandMatchesArgv(command: string, commandArgv: readonly string[]): boolean { + const rendered = commandArgv.join(' '); + if (rendered === command) return true; + const executable = commandArgv[0]; + if (executable === undefined) return false; + const args = commandArgv.slice(1); + return [basename(executable), ...args].join(' ') === command; +} + function safeString(value: unknown, label: string): string { if (typeof value !== 'string' || value.length === 0 || value.includes('\0')) throw new Error(`${label} must be a non-empty string`); @@ -246,7 +255,7 @@ export function parseCutoverPlan(value: unknown): CutoverPlan { throw new Error('legacyPid must be a safe PID greater than 1'); if (!DIGEST.test(plan.backupEvidenceSha256) || !DIGEST.test(plan.drillEvidenceSha256)) throw new Error('Evidence digests must be lowercase SHA-256'); - if (plan.legacyStart.join(' ') !== plan.legacyCommand) + if (!commandMatchesArgv(plan.legacyCommand, plan.legacyStart)) throw new Error('legacyStart must exactly reproduce legacyCommand'); return Object.freeze(plan); }