/**
 * WS5.2 Account Follow-Me — the `account-credential-share` mesh verb (Mechanism A).
 *
 * STRUCTURALLY SEPARATE from `secret-share` (SecretSync.ts) by design — spec R3a / §6.5.
 * The legacy `secret-share` verb/`SecretShareHandler`/`decryptFromSync` path is PERMISSIVE
 * (any registered peer; no AAD; no mandate). A credential MUST NOT ride it. This file is the
 * distinct, operator-mandate-gated path:
 *
 *   - the credential is sealed with the AAD-bound `encryptAccountCredential` (NOT
 *     `encryptForSync`), so the recipient/account/mandate/grant/epoch are cryptographically
 *     bound (SecretStore.ts);
 *   - the receiver runs an RBAC gate BEFORE decrypt — verify the authorizing mandate, match
 *     the addressed recipient, and CONSUME a single-use grant — and only then attempts the
 *     fail-closed decrypt against THIS machine's live fingerprint + key-rotation epoch.
 *
 * PR1 SCOPE (no live credential): the verb, the builder, and the RBAC-gated handler ship as
 * primitives with injected seams (mandate verifier, grant consumer, credential writer). The
 * real config-home credential write is a LATER PR — here `storeCredential` is optional and,
 * when absent, the handler authenticates+decrypts but reports the write as unwired rather than
 * touching any live credential store. Everything stays dark behind `multiMachine.accountFollowMe`.
 */
import { encryptAccountCredential, decryptAccountCredential, } from './SecretStore.js';
/** Build an `account-credential-share` command sealed + AAD-bound to one recipient. */
export function buildAccountCredentialShareCommand(args) {
    const aad = {
        recipientFingerprint: args.recipient.fingerprint,
        accountId: args.accountId,
        mandateId: args.mandateId,
        grantId: args.grantId,
        pairingEpoch: args.pairingEpoch,
    };
    const payload = encryptAccountCredential(args.secrets, args.recipient.encryptionPublicKey, aad);
    return {
        type: 'account-credential-share',
        encrypted: JSON.stringify(payload),
        mandateId: args.mandateId,
        grantId: args.grantId,
        targetFingerprint: args.recipient.fingerprint,
        accountId: args.accountId,
        pairingEpoch: args.pairingEpoch,
        expiresAt: args.expiresAt,
    };
}
/**
 * Receive-side handler. Runs the RBAC gate BEFORE decrypt (spec §5.4: "verifies ... AT THE
 * GATE (before decrypt)"), then a fail-closed AAD-bound decrypt against THIS machine's live
 * fingerprint + epoch. Order is load-bearing: a bad sender/recipient/grant is rejected without
 * ever doing the cryptographic work, and a stale/forged seal cannot decrypt regardless.
 */
export class AccountCredentialShareHandler {
    deps;
    constructor(deps) {
        this.deps = deps;
    }
    handle(command, sender) {
        // 0. Type guard — this handler accepts ONLY the credential verb (never secret-share).
        if (!command || command.type !== 'account-credential-share') {
            return { accepted: false, reason: 'wrong-verb' };
        }
        const now = (this.deps.now ?? Date.now)();
        // 1. Expiry — reject before any work.
        if (typeof command.expiresAt !== 'number' || now > command.expiresAt) {
            return { accepted: false, reason: 'expired' };
        }
        // 2. Recipient binding — this share must be addressed to THIS machine.
        const myFp = this.deps.currentRecipientFingerprint();
        if (command.targetFingerprint !== myFp) {
            return { accepted: false, reason: 'recipient-mismatch' };
        }
        // 3. Mandate RBAC — deny-by-default; operator-mandate-gated (R1/R4a).
        const m = this.deps.verifyMandate({
            mandateId: command.mandateId,
            sender,
            accountId: command.accountId,
            targetFingerprint: command.targetFingerprint,
        });
        if (!m.ok) {
            return { accepted: false, reason: `mandate-denied:${m.reason ?? 'unknown'}` };
        }
        // 4. Single-use grant — consume it AT THE GATE (replay of a consumed grant is rejected, R3).
        const g = this.deps.consumeGrant(command.grantId, command.mandateId);
        if (!g.ok) {
            return { accepted: false, reason: `grant-rejected:${g.reason ?? 'unknown'}` };
        }
        // 5. Fail-closed decrypt — expectedAAD is built from THIS machine's LIVE state, so a seal to
        //    a stale fingerprint or pre-rotation epoch (R4b) cannot decrypt.
        const expectedAAD = {
            recipientFingerprint: myFp,
            accountId: command.accountId,
            mandateId: command.mandateId,
            grantId: command.grantId,
            pairingEpoch: this.deps.currentPairingEpoch(),
        };
        let secrets;
        try {
            const payload = JSON.parse(command.encrypted);
            secrets = decryptAccountCredential(payload, this.deps.ownEncryptionPrivateKey(), expectedAAD);
        }
        catch (err) {
            return { accepted: false, reason: `decrypt-failed:${err instanceof Error ? err.message : String(err)}` };
        }
        // 6. Persist — the real config-home write is a LATER PR. PR1 ships the secure path proven
        //    end-to-end without touching a live credential store.
        if (!this.deps.storeCredential) {
            this.deps.log?.(`[account-cred] decrypted credential for ${command.accountId} from ${sender} (writer unwired — PR1)`);
            return { accepted: true, stored: false, storeReason: 'no-credential-writer-wired (PR1)', accountId: command.accountId };
        }
        this.deps.storeCredential(command.accountId, secrets);
        this.deps.log?.(`[account-cred] stored credential for ${command.accountId} from ${sender}`);
        return { accepted: true, stored: true, accountId: command.accountId };
    }
}
//# sourceMappingURL=AccountCredentialShare.js.map