"use strict";
// admins-write — single chokepoint for the dual-file admin write
// (users.json device-level PIN auth + account.json account-level role).
//
// background: earlier the two writers — `admin-add`
// (platform/plugins/admin/mcp/src/index.ts) and `set-pin` POST
// (platform/ui/server/routes/onboarding.ts) — each performed the dual write
// inline. They drifted: admin-add returned `is_error: true` with a per-leg
// `[admin-auth-store]` line on partial failure; set-pin logged on stderr but
// otherwise rolled forward. This module collapses both into one routed write
// so the static check `grep -rnE 'admins\.push|config\.admins\s*=' platform/`
// returns 0 outside this file (excluding tests, scripts, and the reader paths
// which never push).
//
// Atomicity contract — single file: write-temp + rename, durable.
// Atomicity contract — across files: NOT atomic. POSIX has no two-file
// transaction. The helper writes users.json first, then account.json. On
// account.json failure with users.json already written, the result names the
// partial state. Callers MUST surface partial state to the operator (admin-add
// returns is_error: true with the [admins-write] line; set-pin returns 500
// with the same). Same trust model as the [admin-auth-store] pair.
Object.defineProperty(exports, "__esModule", { value: true });
exports.writeAdminEntry = writeAdminEntry;
exports.removeAdminFromAccount = removeAdminFromAccount;
exports.checkAdminAuthInvariant = checkAdminAuthInvariant;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
function logLine(input, result) {
    const userIdShort = input.userId.slice(0, 8);
    console.error(`[admins-write] caller=${input.caller} userId=${userIdShort} ` +
        `usersJsonResult=${result.usersJsonResult} accountJsonResult=${result.accountJsonResult}` +
        (result.usersError ? ` usersError=${result.usersError}` : "") +
        (result.accountError ? ` accountError=${result.accountError}` : ""));
}
function writeFileAtomic(filePath, contents, mode) {
    (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(filePath), { recursive: true });
    const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
    (0, node_fs_1.writeFileSync)(tempPath, contents, mode !== undefined ? { mode } : undefined);
    (0, node_fs_1.renameSync)(tempPath, filePath);
}
/**
 * Write a single admin entry across both auth stores (users.json + account.json admins[]).
 *
 * Idempotent on userId: if the userId already exists in users.json, the PIN
 * field is updated in place rather than duplicated; admins[] is checked for
 * presence before pushing.
 *
 * Always emits exactly one `[admins-write] caller=… userId=<prefix> usersJsonResult=… accountJsonResult=…`
 * line — success and failure paths both fire it. Operators grep for this single
 * predicate when reconstructing a partial-write incident.
 */
function writeAdminEntry(input) {
    const result = { usersJsonResult: "noop", accountJsonResult: "noop" };
    // 1. users.json — read-modify-write atomically.
    try {
        let users = [];
        if ((0, node_fs_1.existsSync)(input.usersFile)) {
            const raw = (0, node_fs_1.readFileSync)(input.usersFile, "utf-8").trim();
            if (raw) {
                const parsed = JSON.parse(raw);
                if (!Array.isArray(parsed)) {
                    throw new Error("users.json is not an array");
                }
                users = parsed;
            }
        }
        const existing = users.findIndex(u => u.userId === input.userId);
        if (existing === -1) {
            users.push({ userId: input.userId, pin: input.pin });
        }
        else {
            // Preserve sibling fields other writers may have set; overwrite pin.
            users[existing] = { ...users[existing], pin: input.pin };
        }
        writeFileAtomic(input.usersFile, JSON.stringify(users, null, 2) + "\n", 0o600);
        result.usersJsonResult = "ok";
    }
    catch (err) {
        result.usersJsonResult = "fail";
        result.usersError = err instanceof Error ? err.message : String(err);
        logLine(input, result);
        return result;
    }
    // 2. account.json — read-modify-write atomically.
    try {
        const accountJsonPath = (0, node_path_1.join)(input.accountDir, "account.json");
        if (!(0, node_fs_1.existsSync)(accountJsonPath)) {
            throw new Error(`account.json not found at ${accountJsonPath}`);
        }
        const config = JSON.parse((0, node_fs_1.readFileSync)(accountJsonPath, "utf-8"));
        const admins = (config.admins ?? []);
        if (admins.some(a => a.userId === input.userId)) {
            result.accountJsonResult = "noop";
        }
        else {
            admins.push({ userId: input.userId, role: input.role });
            config.admins = admins;
            writeFileAtomic(accountJsonPath, JSON.stringify(config, null, 2) + "\n");
            result.accountJsonResult = "ok";
        }
    }
    catch (err) {
        result.accountJsonResult = "fail";
        result.accountError = err instanceof Error ? err.message : String(err);
    }
    logLine(input, result);
    return result;
}
/**
 * Remove a single admin entry from account.json admins[] — does NOT touch
 * users.json (the device-level entry is preserved because the user may admin
 * other accounts). Single-file operation; the [admins-write] line records
 * `usersJsonResult=skip` so the grep-by-helper-call still picks it up.
 */
function removeAdminFromAccount(input) {
    const result = { accountJsonResult: "noop" };
    const userIdShort = input.userId.slice(0, 8);
    try {
        const accountJsonPath = (0, node_path_1.join)(input.accountDir, "account.json");
        if (!(0, node_fs_1.existsSync)(accountJsonPath)) {
            throw new Error(`account.json not found at ${accountJsonPath}`);
        }
        const config = JSON.parse((0, node_fs_1.readFileSync)(accountJsonPath, "utf-8"));
        const admins = (config.admins ?? []);
        const targetIndex = admins.findIndex(a => a.userId === input.userId);
        if (targetIndex === -1) {
            result.accountJsonResult = "noop";
        }
        else {
            admins.splice(targetIndex, 1);
            config.admins = admins;
            writeFileAtomic(accountJsonPath, JSON.stringify(config, null, 2) + "\n");
            result.accountJsonResult = "ok";
        }
    }
    catch (err) {
        result.accountJsonResult = "fail";
        result.accountError = err instanceof Error ? err.message : String(err);
    }
    console.error(`[admins-write] caller=${input.caller} userId=${userIdShort} ` +
        `usersJsonResult=skip accountJsonResult=${result.accountJsonResult}` +
        (result.accountError ? ` accountError=${result.accountError}` : ""));
    return result;
}
/**
 * Walk every account.json under accountsDir and compare its admins[] to
 * users.json. Emits one log line per divergence and one summary line.
 *
 * Behaviour: log-only — does NOT throw, does NOT refuse boot or install.
 * Pre-Task-904 installs that already diverged would otherwise brick on the
 * first boot of a Task-904 device. The summary line `divergences=<n>` is
 * always emitted (wired-up assertion); per-divergence lines fire only when
 * divergences > 0. Operators grep `[install-invariant]` or `[admin-invariant]`
 * to detect the regression class without reading every install log.
 *
 * Promotion to refuse-or-degrade is a future sprint; capture as a TODO once
 * the deployed fleet has been audited clean.
 */
function checkAdminAuthInvariant(input) {
    const result = {
        divergences: 0,
        accountWithoutUsers: [],
        usersWithoutAccount: [],
    };
    // Read users.json userIds (persistent file). Absent file == empty set; do
    // not abort — first-boot before set-pin has run is a legitimate state.
    const usersUserIds = new Set();
    if ((0, node_fs_1.existsSync)(input.usersFile)) {
        try {
            const raw = (0, node_fs_1.readFileSync)(input.usersFile, "utf-8").trim();
            if (raw) {
                const users = JSON.parse(raw);
                for (const u of users) {
                    if (typeof u.userId === "string")
                        usersUserIds.add(u.userId);
                }
            }
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            console.error(`[${input.tag}] users.json unreadable usersFile=${input.usersFile} error=${msg}`);
        }
    }
    // Walk account dirs. Skip dotfiles (.trash/) and missing account.json.
    const accountUserIds = new Set();
    if ((0, node_fs_1.existsSync)(input.accountsDir)) {
        let entries;
        try {
            entries = (0, node_fs_1.readdirSync)(input.accountsDir);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            console.error(`[${input.tag}] accounts-dir unreadable accountsDir=${input.accountsDir} error=${msg}`);
            console.error(`[${input.tag}] check complete divergences=0 (accounts-dir unreadable)`);
            return result;
        }
        for (const entry of entries) {
            if (entry.startsWith("."))
                continue;
            const accountDir = (0, node_path_1.join)(input.accountsDir, entry);
            try {
                if (!(0, node_fs_1.statSync)(accountDir).isDirectory())
                    continue;
            }
            catch {
                continue;
            }
            const accountJsonPath = (0, node_path_1.join)(accountDir, "account.json");
            if (!(0, node_fs_1.existsSync)(accountJsonPath))
                continue;
            let admins = [];
            try {
                const config = JSON.parse((0, node_fs_1.readFileSync)(accountJsonPath, "utf-8"));
                admins = (config.admins ?? []);
            }
            catch (err) {
                const msg = err instanceof Error ? err.message : String(err);
                console.error(`[${input.tag}] account.json unreadable source=${accountJsonPath} error=${msg}`);
                continue;
            }
            for (const a of admins) {
                if (typeof a.userId !== "string")
                    continue;
                accountUserIds.add(a.userId);
                if (!usersUserIds.has(a.userId)) {
                    const userIdShort = a.userId.slice(0, 8);
                    console.error(`[${input.tag}] direction=account-without-users userId=${userIdShort} source=${accountJsonPath}`);
                    result.accountWithoutUsers.push({ userId: a.userId, source: accountJsonPath });
                    result.divergences += 1;
                }
            }
        }
    }
    for (const uid of usersUserIds) {
        if (!accountUserIds.has(uid)) {
            const userIdShort = uid.slice(0, 8);
            console.error(`[${input.tag}] direction=users-without-account userId=${userIdShort} source=${input.usersFile}`);
            result.usersWithoutAccount.push({ userId: uid });
            result.divergences += 1;
        }
    }
    console.error(`[${input.tag}] check complete divergences=${result.divergences}`);
    return result;
}
//# sourceMappingURL=index.js.map