import { randomBytes } from "node:crypto";
import { mkdtemp, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { LocalSqliteStore } from "../persistence/LocalSqliteStore.js";
import { DEBUG_REVEAL_SURFACES } from "../security/debugRevealContract.js";
import { assertNoPublicPayloadLeaks, redactPublicDiagnosticText, } from "../security/redactionBoundary.js";
import { startBridge } from "./opencodeRuntimeSmoke.js";
import { delay } from "./smokeTiming.js";
const DEFAULT_TIMEOUT_MS = 30_000;
const WORKSPACE_ID = "debug-reveal-smoke-workspace";
const LOCAL_UI_ORIGIN = "http://127.0.0.1:41732";
const DEBUG_DEVICE_ID = "debug-reveal-smoke-device";
const DEBUG_USER_SESSION_ID = "debug-reveal-smoke-user-session";
const WRONG_SCOPE_DEVICE_ID = "debug-reveal-smoke-wrong-scope-device";
const WRONG_SCOPE_USER_SESSION_ID = "debug-reveal-smoke-wrong-scope-user";
const REQUIRED_DEBUG_SCOPES = ["debug:reveal"];
const WRONG_SCOPES = ["bridge:read"];
const PROOFED_DEBUG_REVEAL_ROUTES = [
    "GET /workspaces/:workspaceId/debug-reveal/status",
    "POST /workspaces/:workspaceId/debug-reveal/sessions",
    "DELETE /workspaces/:workspaceId/debug-reveal/sessions/:reveal",
];
const HOSTILE_SECRET = "sk-dreveal1234567890";
const HOSTILE_COMMAND = "rm -rf /Users/a/drev";
const HOSTILE_HANDOFF_FRAGMENT = "patchTrailLocalHandoff=drevs-secret";
const HOSTILE_RUNTIME_PAYLOAD = "runtime-forbidden";
const HOSTILE_PROVIDER_PAYLOAD = "provider-forbidden";
const HOSTILE_INVALID_SURFACE = "pt_lui_sess_drevs_invalid_surface123456";
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
const LOCAL_UI_TOKEN_PATTERNS = [
    {
        label: "local UI capability token",
        pattern: /\bpt_lui_(?!sess_)[A-Za-z0-9_-]{16,}\b/u,
    },
    {
        label: "local UI session token",
        pattern: /\bpt_lui_sess_[A-Za-z0-9_-]{16,}\b/u,
    },
    {
        label: "local UI handoff fragment",
        pattern: /patchTrailLocalHandoff=[^\s"']+/u,
    },
];
export async function main(argv = process.argv.slice(2)) {
    const wantsJson = argv.includes("--json");
    try {
        const options = parseDebugRevealSmokeArgs(argv);
        if (options.help) {
            process.stdout.write(helpText());
            return;
        }
        const summary = await runDebugRevealSmoke(argv, process.env);
        if (options.json) {
            process.stdout.write(`${JSON.stringify(summary)}\n`);
            return;
        }
        process.stdout.write(`Debug reveal smoke passed: states=${summary.states.join(",")} markers=${summary.durability.markerCounts.afterRevoke} outboxDebugRevealEvents=${summary.outbox.debugRevealEvents}\n`);
    }
    catch (error) {
        const message = sanitizeDebugRevealSmokeError(error, [], []);
        process.stderr.write(wantsJson
            ? `${JSON.stringify({ status: "failed", error: message })}\n`
            : `${message}\n`);
        process.exitCode = 1;
    }
}
export async function runDebugRevealSmoke(argv = [], env = process.env) {
    const options = parseDebugRevealSmokeArgs(argv);
    if (options.help) {
        throw new Error("--help is only supported by the debug reveal smoke CLI.");
    }
    return executeDebugRevealSmoke(options, env);
}
export function parseDebugRevealSmokeArgs(argv) {
    const options = {
        keepTemp: false,
        timeoutMs: DEFAULT_TIMEOUT_MS,
        json: false,
        help: false,
    };
    for (let index = 0; index < argv.length; index += 1) {
        const arg = argv[index];
        if (!arg || arg === "--")
            continue;
        if (arg === "--help" || arg === "-h") {
            options.help = true;
            continue;
        }
        if (arg === "--keep-temp") {
            options.keepTemp = true;
            continue;
        }
        if (arg === "--json") {
            options.json = true;
            continue;
        }
        if (arg === "--timeout-ms") {
            options.timeoutMs = parseTimeout(requireArgValue(argv, ++index, arg));
            continue;
        }
        throw new Error(`Unknown debug reveal smoke argument: ${arg}`);
    }
    return options;
}
export function createDebugRevealSmokeEnvironment(input) {
    const smokeEnv = { ...input.env };
    for (const key of [
        "OPENAI_API_KEY",
        "ANTHROPIC_API_KEY",
        "GITHUB_TOKEN",
        "DATABASE_URL",
        "PATCHTRAIL_SMOKE_OPENAI_API_KEY",
        "PATCHTRAIL_CREDENTIAL_FALLBACK_KEY",
        "PATCHTRAIL_OPENCODE_METADATA_DIR",
        "PATCHTRAIL_OPENCODE_ARTIFACT_ROOT",
        "PATCHTRAIL_OPENCODE_TRUSTED_ROOT_KEY_IDS",
    ]) {
        delete smokeEnv[key];
    }
    smokeEnv.NODE_ENV = "test";
    smokeEnv.PATCHTRAIL_CREDENTIAL_STORE = "in-memory";
    smokeEnv.PATCHTRAIL_BRIDGE_HOST = "127.0.0.1";
    smokeEnv.PATCHTRAIL_BRIDGE_PORT = "0";
    smokeEnv.PATCHTRAIL_BRIDGE_CAPABILITY_TOKEN = input.bridgeToken;
    smokeEnv.PATCHTRAIL_OPENCODE_SERVER_PASSWORD = input.opencodeServerPassword;
    smokeEnv.PATCHTRAIL_APP_DATA_DIR = input.appDataDir;
    smokeEnv.PATCHTRAIL_LOCAL_DB_PATH = input.localDbPath;
    return smokeEnv;
}
export function buildDebugRevealSmokeProofPacket(input) {
    return {
        status: "passed",
        smokeName: "debug-reveal-guardrails",
        routes: [...input.routes],
        scopes: [...input.scopes],
        states: [...input.states],
        surfaces: [...input.surfaces],
        rejections: { ...input.rejections },
        safeguards: {
            localOnly: true,
            cloudSync: "disabled",
            rawStorage: "disabled",
        },
        durability: {
            sqliteAuditMarkers: "persisted-after-restart",
            markerCounts: { ...input.markerCounts },
            restartPersistenceVerified: input.restartPersistenceVerified,
        },
        localUiAuthorityVerified: input.localUiAuthorityVerified,
        revokeVerified: input.revokeVerified,
        outbox: {
            cloudSync: "disabled",
            debugRevealEvents: input.outboxDebugRevealEvents,
        },
        noLeakCheckedSurfaces: unique([
            ...input.noLeakCheckedSurfaces,
            "proof output",
        ]),
    };
}
export async function assertDebugRevealSmokeNoLeaks(surface, value, forbidden, options = {}) {
    assertNoPublicPayloadLeaks(surface, value, options.publicPayload ?? {});
    assertNoForbiddenValues(surface, value, forbidden, options);
}
export function sanitizeDebugRevealSmokeError(error, forbidden, pathHints) {
    let message = error instanceof Error ? error.message : "Debug reveal smoke failed.";
    message = redactPublicDiagnosticText(message);
    for (const { pattern, label } of LOCAL_UI_TOKEN_PATTERNS) {
        message = message.replace(pattern, `[redacted:${label}]`);
    }
    for (const item of forbidden) {
        if (!item.value)
            continue;
        message = message.split(item.value).join(`[redacted:${item.label}]`);
    }
    for (const hint of [...pathHints, PACKAGE_ROOT, process.env.HOME]) {
        if (!hint)
            continue;
        message = message.split(hint).join("[redacted:path]");
    }
    message = message.replace(/(?:[A-Za-z]:)?\/(?:private\/)?(?:tmp|var\/folders|Users)\/[^\s"']+/g, "[redacted:path]");
    return message.slice(0, 2_000);
}
export function assertDebugRevealStatusPayload(body, options) {
    const payload = readRecord(body, `${options.label} payload`);
    assertAllowedKeys(payload, ["debugReveal"], `${options.label} payload`);
    const debugReveal = readRecord(payload.debugReveal, `${options.label} status`);
    const summary = assertDebugRevealStatusRecord(debugReveal, options.label);
    if (options.expectedState && summary.state !== options.expectedState) {
        throw new Error(`${options.label} returned state ${summary.state}; expected ${options.expectedState}.`);
    }
    assertNoPublicPayloadLeaks(options.label, body);
    return summary;
}
async function executeDebugRevealSmoke(options, env) {
    let tempRoot;
    let bridge;
    const forbidden = [
        { label: "hostile provider payload", value: HOSTILE_PROVIDER_PAYLOAD },
        { label: "hostile runtime payload", value: HOSTILE_RUNTIME_PAYLOAD },
        { label: "hostile invalid surface", value: HOSTILE_INVALID_SURFACE },
        { label: "hostile fake secret", value: HOSTILE_SECRET },
        { label: "hostile command", value: HOSTILE_COMMAND },
        { label: "hostile handoff fragment", value: HOSTILE_HANDOFF_FRAGMENT },
    ];
    const noLeakCheckedSurfaces = [];
    const checkNoLeaks = async (surface, value, leakOptions = {}) => {
        await assertDebugRevealSmokeNoLeaks(surface, value, forbidden, leakOptions);
        noLeakCheckedSurfaces.push(surface);
    };
    try {
        tempRoot = await mkdtemp(path.join(os.tmpdir(), "patchtrail-debug-reveal-smoke-"));
        const appDataDir = path.join(tempRoot, "app-data");
        const localDbPath = path.join(tempRoot, "state", "patchtrail.sqlite");
        const hostileLocalPath = path.join(tempRoot, "workspace", "private-debug-reveal", "provider-token.txt");
        const bridgeToken = `pt_debug_reveal_smoke_${randomBytes(24).toString("base64url")}`;
        const opencodeServerPassword = randomBytes(32).toString("base64url");
        const hostileReason = [
            `Need ${HOSTILE_SECRET}`,
            `operator command ${HOSTILE_COMMAND}`,
            HOSTILE_HANDOFF_FRAGMENT,
            HOSTILE_RUNTIME_PAYLOAD,
            HOSTILE_PROVIDER_PAYLOAD,
        ].join(" | ");
        forbidden.push({ label: "bridge token", value: bridgeToken }, { label: "OpenCode server password", value: opencodeServerPassword }, { label: "smoke temp root", value: tempRoot }, { label: "smoke app data dir", value: appDataDir }, { label: "smoke local database path", value: localDbPath }, { label: "hostile local path", value: hostileLocalPath }, { label: "hostile debug reveal reason", value: hostileReason });
        const smokeEnv = createDebugRevealSmokeEnvironment({
            env,
            bridgeToken,
            opencodeServerPassword,
            appDataDir,
            localDbPath,
        });
        bridge = await startBridge(smokeEnv, options.timeoutMs, forbidden);
        const initial = await runInitialDebugRevealHttpProof({
            bridge,
            bridgeToken,
            hostileReason,
            forbidden,
            checkNoLeaks,
        });
        await stopBridge(bridge, "initial bridge", checkNoLeaks);
        bridge = undefined;
        const afterActivation = inspectDebugRevealPersistence({
            localDbPath,
            forbidden,
            expectedKinds: ["activated"],
            label: "after activation SQLite markers",
        });
        bridge = await startBridge(smokeEnv, options.timeoutMs, forbidden);
        const restart = await runRestartDebugRevealHttpProof({
            bridge,
            bridgeToken,
            forbidden,
            checkNoLeaks,
        });
        await stopBridge(bridge, "restart bridge", checkNoLeaks);
        bridge = undefined;
        const afterRevoke = inspectDebugRevealPersistence({
            localDbPath,
            forbidden,
            expectedKinds: ["activated", "revoked"],
            label: "after revoke SQLite markers",
        });
        const summary = buildDebugRevealSmokeProofPacket({
            routes: [...PROOFED_DEBUG_REVEAL_ROUTES],
            scopes: [...REQUIRED_DEBUG_SCOPES],
            states: [
                initial.inactive.state,
                initial.active.state,
                "active-after-restart",
                restart.revoked.state,
            ],
            surfaces: restart.restartActive.surfaces,
            markerCounts: {
                afterActivation: afterActivation.markerCount,
                afterRestart: restart.restartActive.markerCount,
                afterRevoke: afterRevoke.markerCount,
            },
            rejections: initial.rejections,
            restartPersistenceVerified: restart.restartActive.state === "active",
            localUiAuthorityVerified: true,
            revokeVerified: restart.revoked.state === "revoked",
            outboxDebugRevealEvents: afterRevoke.outboxDebugRevealEvents,
            noLeakCheckedSurfaces,
        });
        await assertDebugRevealSmokeNoLeaks("proof output", summary, forbidden);
        return summary;
    }
    catch (error) {
        throw new Error(sanitizeDebugRevealSmokeError(error, forbidden, [tempRoot]));
    }
    finally {
        if (bridge) {
            await stopBridge(bridge, "failed bridge", async () => undefined).catch(() => undefined);
        }
        if (tempRoot && !options.keepTemp) {
            await rm(tempRoot, { recursive: true, force: true });
        }
    }
}
async function runInitialDebugRevealHttpProof(input) {
    const request = (method, requestPath, body, options = {}) => requestJson(input.bridge.baseUrl, method, requestPath, body, options);
    const statusPath = `/workspaces/${encodeURIComponent(WORKSPACE_ID)}/debug-reveal/status`;
    const sessionsPath = `/workspaces/${encodeURIComponent(WORKSPACE_ID)}/debug-reveal/sessions`;
    const unauthenticated = await request("GET", statusPath);
    assertAuthorityRejection(unauthenticated, {
        status: 401,
        guard: "auth",
        reason: "missing_authentication",
        label: "unauthenticated debug reveal status",
    });
    await input.checkNoLeaks("unauthenticated debug reveal status", unauthenticated.body);
    const bridgeCredential = await request("GET", statusPath, undefined, {
        bridgeToken: input.bridgeToken,
    });
    assertAuthorityRejection(bridgeCredential, {
        status: 403,
        guard: "local-ui-session-required",
        reason: "local_ui_session_required",
        label: "bridge credential debug reveal status",
    });
    await input.checkNoLeaks("bridge credential debug reveal status", bridgeCredential.body);
    const wrongScopeSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...WRONG_SCOPES],
        deviceId: WRONG_SCOPE_DEVICE_ID,
        userSessionId: WRONG_SCOPE_USER_SESSION_ID,
        label: "wrong-scope",
    });
    const wrongScope = await request("GET", statusPath, undefined, {
        bearerToken: wrongScopeSession.token,
        origin: wrongScopeSession.origin,
    });
    assertAuthorityRejection(wrongScope, {
        status: 403,
        guard: "scope",
        reason: "insufficient_scope",
        label: "wrong scope debug reveal status",
    });
    await input.checkNoLeaks("wrong scope debug reveal status", wrongScope.body);
    const debugSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...REQUIRED_DEBUG_SCOPES],
        deviceId: DEBUG_DEVICE_ID,
        userSessionId: DEBUG_USER_SESSION_ID,
        label: "debug",
    });
    const debugAuth = {
        bearerToken: debugSession.token,
        origin: debugSession.origin,
    };
    const inactive = await request("GET", statusPath, undefined, debugAuth);
    assertStatus(inactive, 200, "GET debug reveal status inactive");
    await input.checkNoLeaks("GET debug reveal status inactive", inactive.body);
    const inactiveStatus = assertDebugRevealStatusPayload(inactive.body, {
        label: "GET debug reveal status inactive",
        expectedState: "inactive",
    });
    const missingConfirmation = await request("POST", sessionsPath, { ttlSeconds: 60 }, debugAuth);
    assertStatus(missingConfirmation, 422, "POST debug reveal missing confirmation");
    assertNestedEqual(missingConfirmation.body, ["error"], "confirmation_required");
    await input.checkNoLeaks("POST debug reveal missing confirmation", missingConfirmation.body);
    const invalidSurface = await request("POST", sessionsPath, {
        breakGlassConfirmed: true,
        surfaces: ["runtime-diagnostics", HOSTILE_INVALID_SURFACE],
    }, debugAuth);
    assertStatus(invalidSurface, 422, "POST debug reveal invalid surface");
    assertNestedEqual(invalidSurface.body, ["error"], "invalid_surface");
    await input.checkNoLeaks("POST debug reveal invalid surface", invalidSurface.body);
    const activated = await request("POST", sessionsPath, {
        breakGlassConfirmed: true,
        ttlSeconds: 60,
        reason: input.hostileReason,
        surfaces: [
            "runtime-diagnostics",
            "provider-auth-status",
            "permission-audit",
            "run-timeline",
        ],
    }, debugAuth);
    assertStatus(activated, 201, "POST debug reveal activate");
    await input.checkNoLeaks("POST debug reveal activate", activated.body);
    const activeStatus = assertDebugRevealStatusPayload(activated.body, {
        label: "POST debug reveal activate",
        expectedState: "active",
    });
    assertNestedEqual(activated.body, ["debugReveal", "session", "reasonLabel"], "redacted-sensitive-reason");
    const duplicate = await request("POST", sessionsPath, { breakGlassConfirmed: true, ttlSeconds: 60 }, debugAuth);
    assertStatus(duplicate, 409, "POST duplicate debug reveal activate");
    assertNestedEqual(duplicate.body, ["error"], "active_session_conflict");
    assertNestedEqual(duplicate.body, ["debugReveal", "state"], "active");
    await input.checkNoLeaks("POST duplicate debug reveal activate", duplicate.body);
    const unknownRevoke = await request("DELETE", `${sessionsPath}/drevs_unknown`, {}, debugAuth);
    assertStatus(unknownRevoke, 404, "DELETE unknown debug reveal session");
    assertNestedEqual(unknownRevoke.body, ["error"], "session_not_found");
    await input.checkNoLeaks("DELETE unknown debug reveal session", unknownRevoke.body);
    return {
        inactive: inactiveStatus,
        active: activeStatus,
        rejections: {
            unauthenticated: true,
            bridgeCredential: true,
            wrongScope: true,
            missingConfirmation: true,
            invalidSurface: true,
            duplicateActive: true,
            unknownRevoke: true,
        },
    };
}
async function runRestartDebugRevealHttpProof(input) {
    const request = (method, requestPath, body, options = {}) => requestJson(input.bridge.baseUrl, method, requestPath, body, options);
    const statusPath = `/workspaces/${encodeURIComponent(WORKSPACE_ID)}/debug-reveal/status`;
    const sessionsPath = `/workspaces/${encodeURIComponent(WORKSPACE_ID)}/debug-reveal/sessions`;
    const debugSession = await mintScopedLocalUiSession({
        request,
        bridgeToken: input.bridgeToken,
        forbidden: input.forbidden,
        checkNoLeaks: input.checkNoLeaks,
        scopes: [...REQUIRED_DEBUG_SCOPES],
        deviceId: `${DEBUG_DEVICE_ID}-restart`,
        userSessionId: `${DEBUG_USER_SESSION_ID}-restart`,
        label: "restart-debug",
    });
    const debugAuth = {
        bearerToken: debugSession.token,
        origin: debugSession.origin,
    };
    const afterRestart = await request("GET", statusPath, undefined, debugAuth);
    assertStatus(afterRestart, 200, "GET debug reveal status after restart");
    await input.checkNoLeaks("GET debug reveal status after restart", afterRestart.body);
    const restartActive = assertDebugRevealStatusPayload(afterRestart.body, {
        label: "GET debug reveal status after restart",
        expectedState: "active",
    });
    if (!restartActive.sessionId) {
        throw new Error("Restart status did not include an active reveal session.");
    }
    const revoked = await request("DELETE", `${sessionsPath}/${encodeURIComponent(restartActive.sessionId)}`, { reason: `operator closed debug panel ${HOSTILE_SECRET}` }, debugAuth);
    assertStatus(revoked, 200, "DELETE active debug reveal session");
    await input.checkNoLeaks("DELETE active debug reveal session", revoked.body);
    const revokedStatus = assertDebugRevealStatusPayload(revoked.body, {
        label: "DELETE active debug reveal session",
        expectedState: "revoked",
    });
    return { restartActive, revoked: revokedStatus };
}
async function mintScopedLocalUiSession(input) {
    const capability = await input.request("POST", "/local-ui/capabilities", {
        origin: LOCAL_UI_ORIGIN,
        workspaceId: WORKSPACE_ID,
        deviceId: input.deviceId,
        userSessionId: input.userSessionId,
        scopes: input.scopes,
        ttlSeconds: 60,
        sessionTtlSeconds: 300,
    }, { bridgeToken: input.bridgeToken });
    assertStatus(capability, 201, `${input.label} POST /local-ui/capabilities`);
    await input.checkNoLeaks(`${input.label} POST /local-ui/capabilities`, capability.body, localUiCapabilityLeakOptions());
    const capabilityToken = readNestedString(capability.body, ["token"]);
    input.forbidden.push({
        label: `${input.label} local UI capability token`,
        value: capabilityToken,
    });
    const exchange = await input.request("POST", "/local-ui/session", {
        capabilityToken,
        workspaceId: WORKSPACE_ID,
        deviceId: input.deviceId,
        userSessionId: input.userSessionId,
    }, { origin: LOCAL_UI_ORIGIN });
    assertStatus(exchange, 201, `${input.label} POST /local-ui/session`);
    await input.checkNoLeaks(`${input.label} POST /local-ui/session`, exchange.body, localUiSessionLeakOptions());
    const sessionToken = readNestedString(exchange.body, ["sessionToken"]);
    input.forbidden.push({
        label: `${input.label} local UI session token`,
        value: sessionToken,
    });
    assertDeepEqual(readNested(exchange.body, ["session", "scopes"]), input.scopes, `${input.label} local UI scopes`);
    return { token: sessionToken, origin: LOCAL_UI_ORIGIN, scopes: input.scopes };
}
function inspectDebugRevealPersistence(input) {
    const store = LocalSqliteStore.openSync(input.localDbPath);
    try {
        const events = store.listDebugRevealEvents(WORKSPACE_ID);
        assertDeepEqual(events.map((event) => event.kind), input.expectedKinds, `${input.label} event kinds`);
        assertNoForbiddenValues(input.label, events, input.forbidden, {
            allowLocalUiTokenPatterns: false,
        });
        const debugRevealOutbox = store
            .listPendingOutboxEvents()
            .filter((event) => event.type.includes("debug_reveal") ||
            event.payloadJson.includes("debug_reveal"));
        if (debugRevealOutbox.length > 0) {
            throw new Error("debug reveal markers were queued for outbox sync.");
        }
        if (store.listPendingOutboxEvents().length > 0) {
            throw new Error("debug reveal smoke created pending outbox events.");
        }
        return {
            markerCount: events.length,
            eventKinds: events.map((event) => event.kind),
            outboxDebugRevealEvents: debugRevealOutbox.length,
        };
    }
    finally {
        store.close();
    }
}
async function requestJson(baseUrl, method, requestPath, body, options = {}) {
    const headers = { accept: "application/json" };
    if (options.bridgeToken)
        headers["x-patchtrail-bridge-token"] = options.bridgeToken;
    if (options.bearerToken)
        headers.authorization = `Bearer ${options.bearerToken}`;
    if (options.origin)
        headers.origin = options.origin;
    let requestBody;
    if (body !== undefined) {
        headers["content-type"] = "application/json";
        requestBody = JSON.stringify(body);
    }
    const response = await fetch(new URL(requestPath, baseUrl), {
        method,
        headers,
        ...(requestBody ? { body: requestBody } : {}),
        signal: AbortSignal.timeout(10_000),
    });
    const text = await response.text();
    let parsed = text;
    if (text) {
        try {
            parsed = JSON.parse(text);
        }
        catch {
            parsed = text;
        }
    }
    return { status: response.status, body: parsed, text };
}
async function stopBridge(bridge, label, checkNoLeaks) {
    await terminateChild(bridge.child);
    await checkNoLeaks(`${label} stdout`, bridge.stdout.text());
    await checkNoLeaks(`${label} stderr`, bridge.stderr.text());
}
async function terminateChild(child) {
    if (child.exitCode !== null || child.signalCode !== null)
        return;
    child.kill("SIGTERM");
    const deadline = Date.now() + 5_000;
    while (Date.now() < deadline) {
        if (child.exitCode !== null || child.signalCode !== null)
            return;
        await delay(50);
    }
    if (child.exitCode === null && child.signalCode === null) {
        child.kill("SIGKILL");
    }
}
function assertDebugRevealStatusRecord(debugReveal, label) {
    assertAllowedKeys(debugReveal, ["state", "active", "safeguards", "audit", "session"], `${label} debugReveal`);
    const state = readDebugRevealState(debugReveal.state, `${label} state`);
    const active = readBoolean(debugReveal.active, `${label} active`);
    const safeguards = readRecord(debugReveal.safeguards, `${label} safeguards`);
    assertAllowedKeys(safeguards, ["localOnly", "cloudSync", "rawStorage"], `${label} safeguards`);
    assertNestedEqual(safeguards, ["localOnly"], true);
    assertNestedEqual(safeguards, ["cloudSync"], "disabled");
    assertNestedEqual(safeguards, ["rawStorage"], "disabled");
    const audit = readRecord(debugReveal.audit, `${label} audit`);
    assertAllowedKeys(audit, ["markerCount", "lastMarker"], `${label} audit`);
    const markerCount = readNumber(audit.markerCount, `${label} markerCount`);
    if (audit.lastMarker !== undefined) {
        const lastMarker = readRecord(audit.lastMarker, `${label} lastMarker`);
        assertAllowedKeys(lastMarker, ["id", "kind", "at"], `${label} lastMarker`);
    }
    let sessionId;
    let surfaces = [];
    if (debugReveal.session !== undefined) {
        const session = readRecord(debugReveal.session, `${label} session`);
        assertAllowedKeys(session, [
            "id",
            "activatedAt",
            "expiresAt",
            "ttlSeconds",
            "ttlRemainingSeconds",
            "surfaces",
            "reasonLabel",
        ], `${label} session`);
        sessionId = readString(session.id, `${label} reveal action id`);
        readString(session.expiresAt, `${label} expiresAt`);
        readNumber(session.ttlSeconds, `${label} ttlSeconds`);
        readNumber(session.ttlRemainingSeconds, `${label} ttlRemainingSeconds`);
        if (session.activatedAt !== undefined) {
            readString(session.activatedAt, `${label} activatedAt`);
        }
        surfaces = readStringArray(session.surfaces, `${label} surfaces`);
        for (const surface of surfaces) {
            if (!DEBUG_REVEAL_SURFACES.includes(surface)) {
                throw new Error(`${label} included unsupported debug reveal surface.`);
            }
        }
        if (session.reasonLabel !== undefined) {
            readString(session.reasonLabel, `${label} reasonLabel`);
        }
    }
    return {
        state,
        active,
        markerCount,
        ...(sessionId ? { sessionId } : {}),
        surfaces,
    };
}
function assertAuthorityRejection(result, expected) {
    assertStatus(result, expected.status, expected.label);
    assertNestedEqual(result.body, ["error"], "bridge_authority_rejected");
    assertNestedEqual(result.body, ["status"], expected.status);
    assertNestedEqual(result.body, ["guard"], expected.guard);
    assertNestedEqual(result.body, ["reason"], expected.reason);
}
function assertStatus(result, expected, label) {
    if (result.status !== expected) {
        throw new Error(`${label} returned HTTP ${result.status}; expected ${expected}.`);
    }
}
function assertAllowedKeys(record, allowedKeys, label) {
    const allowed = new Set(allowedKeys);
    const extra = Object.keys(record).filter((key) => !allowed.has(key));
    if (extra.length > 0) {
        throw new Error(`${label} included unavailable raw fields: ${extra.join(", ")}.`);
    }
}
function assertNestedEqual(value, pathSegments, expected) {
    const actual = readNested(value, pathSegments);
    if (actual !== expected) {
        throw new Error(`Expected ${pathSegments.join(".")} to equal ${String(expected)}.`);
    }
}
function assertDeepEqual(actual, expected, label) {
    if (JSON.stringify(actual) !== JSON.stringify(expected)) {
        throw new Error(`Expected ${label} to match.`);
    }
}
function readNested(value, pathSegments) {
    let current = value;
    for (const segment of pathSegments) {
        if (!isRecord(current))
            return undefined;
        current = current[segment];
    }
    return current;
}
function readNestedString(value, pathSegments) {
    return readString(readNested(value, pathSegments), pathSegments.join("."));
}
function readString(value, label) {
    if (typeof value === "string" && value.length > 0)
        return value;
    throw new Error(`Expected ${label} to be a non-empty string.`);
}
function readBoolean(value, label) {
    if (typeof value === "boolean")
        return value;
    throw new Error(`Expected ${label} to be a boolean.`);
}
function readNumber(value, label) {
    if (typeof value === "number" && Number.isFinite(value))
        return value;
    throw new Error(`Expected ${label} to be a finite number.`);
}
function readStringArray(value, label) {
    if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
        return [...value];
    }
    throw new Error(`Expected ${label} to be an array of strings.`);
}
function readDebugRevealState(value, label) {
    if (value === "inactive" ||
        value === "active" ||
        value === "expired" ||
        value === "revoked") {
        return value;
    }
    throw new Error(`Expected ${label} to be a debug reveal state.`);
}
function readRecord(value, label) {
    if (isRecord(value))
        return value;
    throw new Error(`Expected ${label} to be an object.`);
}
function isRecord(value) {
    return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertNoForbiddenValues(surface, value, forbidden, options = {}) {
    const serialized = serializeForLeakCheck(value);
    if (options.allowLocalUiTokenPatterns !== true) {
        const tokenLabels = LOCAL_UI_TOKEN_PATTERNS.filter(({ pattern }) => pattern.test(serialized)).map(({ label }) => label);
        if (tokenLabels.length > 0) {
            throw new Error(`${unique(tokenLabels).join(", ")} leaked through ${surface}.`);
        }
    }
    for (const item of forbidden) {
        if (!item.value)
            continue;
        if (serialized.includes(item.value)) {
            if (item.allowedSurfacePattern?.test(surface))
                continue;
            throw new Error(`${item.label} leaked through ${surface}.`);
        }
    }
}
function serializeForLeakCheck(value) {
    if (typeof value === "string")
        return value;
    return JSON.stringify(value) ?? "";
}
function localUiCapabilityLeakOptions() {
    return {
        allowLocalUiTokenPatterns: true,
        publicPayload: {
            allowedPaths: [
                "$.token",
                "$.capabilityId",
                "$.workspaceId",
                "$.deviceId",
                "$.userSessionId",
            ],
            allowedFieldNames: [
                "token",
                "capabilityId",
                "workspaceId",
                "deviceId",
                "userSessionId",
            ],
            allowedPatternIds: ["local-ui-capability-token"],
        },
    };
}
function localUiSessionLeakOptions() {
    return {
        allowLocalUiTokenPatterns: true,
        publicPayload: {
            allowedPaths: [
                "$.sessionToken",
                "$.session.id",
                "$.session.workspaceId",
                "$.session.deviceId",
                "$.session.userSessionId",
            ],
            allowedFieldNames: [
                "sessionToken",
                "workspaceId",
                "deviceId",
                "userSessionId",
            ],
            allowedPatternIds: ["local-ui-session-token"],
        },
    };
}
function parseTimeout(value) {
    if (!/^\d+$/.test(value)) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    const parsed = Number(value);
    if (!Number.isInteger(parsed) || parsed < 1_000) {
        throw new Error("--timeout-ms must be an integer >= 1000.");
    }
    return parsed;
}
function requireArgValue(argv, index, flag) {
    const value = argv[index];
    if (!value)
        throw new Error(`Expected value after ${flag}.`);
    return value;
}
function helpText() {
    return `Usage: pnpm smoke:debug-reveal -- [options]\n\nStarts the compiled loopback bridge with an isolated SQLite database, mints scoped local UI sessions, proves debug reveal activation/status/restart/revoke guardrails, verifies no debug reveal outbox sync, scans HTTP bodies/logs/proof output with the public redaction guard, and prints only non-secret labels/counts.\n\nOptions:\n  --timeout-ms <ms>   Bridge readiness timeout. Default: ${DEFAULT_TIMEOUT_MS}.\n  --keep-temp         Keep the isolated temp SQLite/app-data directory; the path is intentionally not printed.\n  --json              Print a machine-readable non-secret proof packet.\n  --help              Show this help.\n`;
}
function unique(values) {
    return [...new Set(values)];
}
