/**
 * CredentialSwapExecutor — Step 5 of live credential re-pointing (spec §2.3).
 *
 * The staged exchange that MOVES a credential between two config-home slots WITHOUT restarting
 * the sessions reading those slots (E3: the `claude` client re-reads its store on the next API
 * call). `swap(slotA, slotB)` EXCHANGES the two slots' credentials — never copies — so the §0.d
 * invariant ("one lineage per readable config home") holds by construction for the swap itself.
 *
 * Ships DARK + dry-run for EVERYONE (incl. dev). `subscriptionPool.credentialRepointing` is a
 * `DARK_GATE_EXCLUSIONS` `destructive` entry (`enabled:false` + `dryRun:true`); with the feature
 * off the executor performs ZERO real keychain/config writes (a strict no-op verdict), and with
 * `dryRun` on it runs the full decision loop and AUDITS what it WOULD do without writing. Going
 * live needs a deliberate `enabled:true` AND `dryRun:false` flip (the two-flag gate).
 *
 * ── The staged-exchange mechanism (spec §2.3 steps, each audited) ──
 *  1.  Preconditions: BOTH slots resolve by EXACT membership in the ledger's enumerated slot set
 *      BEFORE any path expansion (a value not `===` a known slot is rejected — `../`/`~`/absolute
 *      traversal can never reach a keychain service, since the value is validated against the
 *      ledger, not the filesystem); neither tenant quarantined; no swap in flight (single-mover);
 *      both blobs re-read fresh, parse, and carry refresh tokens.
 *  1a. Source-slot CAS: immediately before staging/overwriting each slot, RE-READ its on-disk
 *      blob and compare to the step-1 read. If it changed and the new blob parses + identity-
 *      matches the SAME tenant → ADOPT the newer blob (the client's rotated copy); never carry a
 *      blob older than what is on disk. A different-tenant blob is a clobber-race → ABORT.
 *  2.  Staging escrow: COPY (never move) blob A (the 1a re-read, freshest) into a DISJOINT
 *      `instar-credential-swap-staging-<swapId>` keychain entry; journal `begin` BEFORE the first
 *      destructive write. The staging namespace is guaranteed disjoint from every
 *      `claudeCredentialService(home)` output, so no `claude` client / poller ever reads a staged
 *      copy. Staging is RETAINED until step 6's delayed re-verify passes.
 *  3.  The exchange: write blob B → slot A, write blob A (from staging) → slot B — KEYCHAIN FIRST,
 *      then config (`oauthAccount` blocks; default slot config = `~/.claude.json`, home-root).
 *  4.  Verify on ACCOUNT IDENTITY via the oracle (NOT token bytes, NOT auth-status). match→commit;
 *      identity-MISMATCH→repair-from-staging then re-verify, still-wrong→quarantine;
 *      oracle-UNAVAILABLE→QUARANTINE-NEVER-REPAIR (an outage must never trigger a destructive
 *      repair). The system NEVER writes a credential into a slot it cannot identity-verify.
 *  5.  Commit: journal `committed`, ledger assignments updated — staging RETAINED (a client
 *      write-back between commit and step-6 could clobber slot B's only copy; staging is the heal
 *      source for that window).
 *  6.  Delayed re-verify ~90s after commit (oracle identity of both slots); only on pass →
 *      DELETE staging + journal `done`. (90s covers the sub-2-min in-flight refresh; the at-expiry
 *      write-back is the always-on §2.4 audit's job.)
 *
 * ── Crash-safety ── A crash at ANY boundary is decidable from the journal + the two slots' on-disk
 * reads: before step 3's first write nothing destructive happened (staging is a COPY, slot A
 * untouched) → unwind is a no-op; after `begin` and before `done`, staging is the escrow and
 * recovery applies the adopt-on-newer rule (never a blind staging overwrite). `recover()` acquires
 * the single-mover mutex (recovery WRITES race the boot balancer otherwise) and resolves every
 * in-flight journal row; the balancer's first pass is gated on `recoveryBarrier` WITH a bounded
 * hang-timeout (a wedged recovery write must not freeze the balancer forever — on timeout the
 * unresolved slots are quarantined BEFORE the barrier lifts, so the balancer structurally cannot
 * select them).
 *
 * ── Concurrency ── Every swap runs inside `funnel.withSingleMover(() => funnel.withSlotLocks([A,B],
 * …))` so two swaps never overlap and a swap can never interleave with a refresh on the same slot
 * (the Step-4b funnel guards both). All `security` keychain calls go through the async
 * execFile-based `KeychainCredentialExec` with a 10s timeout — NEVER `execFileSync` (a locked
 * keychain ACL prompt would wedge the event loop). The credential WRITE path routes through the
 * funnel; the executor never calls `defaultCredentialStore.write` / `provider.writeCredentials`,
 * so the §2.2 unfunneled-write lint stays clean.
 *
 * ── No token material on any surface ── audit records, journal details, attention items, and the
 * swap-result reference accounts by id / slot only; every free-text error string is scrubbed
 * through `redactToken` before it reaches any persisted/served/notified surface.
 */
import type { CredentialWriteFunnel } from './CredentialWriteFunnel.js';
import type { CredentialLocationLedger } from './CredentialLocationLedger.js';
/**
 * The async keychain access surface. A slot's credential lives at the keychain service
 * `claudeCredentialService(slot)`; a staged escrow copy lives at `instar-credential-swap-staging-*`.
 * Reads/writes/deletes ALL go through async `execFile` with a 10s timeout — never the SYNC
 * `defaultCredentialStore` (a locked keychain could wedge the event loop) and never
 * `provider.writeCredentials` (the §2.2 lint forbids both outside the funnel; this surface is the
 * funnel-routed Step-5 owner). The default impl uses macOS `security`; tests inject an in-memory map.
 */
export interface KeychainCredentialExec {
    /** Read the raw JSON blob at a keychain SERVICE name (slot service or a staging service). null = absent. */
    readService(service: string): Promise<string | null>;
    /** Write (upsert) the raw JSON blob at a keychain SERVICE name. */
    writeService(service: string, rawJson: string): Promise<void>;
    /** Delete a keychain entry at a SERVICE name (used to remove staging after step 6). */
    deleteService(service: string): Promise<void>;
}
/** Default macOS keychain exec — async `execFile`, 10s timeout. Non-darwin: a per-service file. */
export declare const defaultKeychainExec: KeychainCredentialExec;
/**
 * The `oauthAccount` config metadata exchange. The credential keychain blob is the record of
 * truth; config follows (keychain-first/config-second). The DEFAULT slot's canonical config file
 * is `~/.claude.json` (home-root). Injectable so tests assert the ordering; a config-write failure
 * after a successful keychain exchange is a REPAIRABLE metadata condition (never a quarantine).
 */
export interface ConfigBlockExchange {
    /** Move/exchange the two slots' `oauthAccount` config blocks. Resolves on success. */
    exchange(slotA: string, slotB: string): Promise<void>;
}
/** No-op config exchange (the metadata follow is wired in Step 6's census re-routing). */
export declare const noopConfigBlockExchange: ConfigBlockExchange;
/** Identity verdict for a slot: the confirmed accountId, or unavailable (never a guess). */
export type SlotIdentity = {
    accountId: string;
} | {
    unavailable: true;
    reason: string;
};
/**
 * Resolve which pool account currently tenants a slot, by reading its blob via the oracle and
 * mapping email→accountId through the pool. Composed by the host from `CredentialIdentityOracle`
 * + the pool; injected so the executor's verify/adopt/repair/quarantine boundary is unit-testable
 * without a live oracle. CONTRACT: an unreachable/slow/5xx/429/ambiguous oracle → `unavailable`
 * (quarantine-never-repair upstream), NEVER a wrong accountId.
 */
export type ResolveSlotIdentity = (slot: string) => Promise<SlotIdentity>;
export type SwapOutcome = 'swapped' | 'dry-run' | 'disabled' | 'skipped' | 'precondition-failed' | 'clobber-race' | 'quarantined';
export interface SwapResult {
    outcome: SwapOutcome;
    /** Credential-free human reason (scrubbed). */
    reason: string;
    /** The swap id (present once a swap was attempted past preconditions). */
    swapId?: string;
    /** Slot(s) quarantined by this attempt, if any. */
    quarantinedSlots?: string[];
}
/** One audited step (no token material — scrubbed at emit). */
export interface SwapAuditRecord {
    swapId: string;
    step: string;
    slotA: string;
    slotB: string;
    detail?: string;
    at: string;
}
export interface CredentialSwapExecutorDeps {
    funnel: CredentialWriteFunnel;
    ledger: CredentialLocationLedger;
    keychain?: KeychainCredentialExec;
    configExchange?: ConfigBlockExchange;
    /** Compose oracle + pool: slot → confirmed accountId | unavailable. */
    resolveIdentity: ResolveSlotIdentity;
    /** Feature gate (DARK_GATE_EXCLUSIONS destructive). */
    config?: {
        enabled?: boolean;
        dryRun?: boolean;
    };
    /** Single audit chokepoint — every step routes here (scrubbed). */
    emitAudit?: (record: SwapAuditRecord) => void;
    /** HIGH attention on a quarantine (the blast-radius surface). */
    emitAttention?: (item: {
        id: string;
        title: string;
        summary: string;
        category: string;
        priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
        sourceContext?: string;
    }) => void | Promise<void>;
    /**
     * Census #8 cache-bust hook. Invoked at commit with the slots whose tenant just changed. The
     * server wires this to `InUseAccountResolver.bustCache()` so a default-slot (`~/.claude`) swap
     * invalidates the stale in-use badge immediately — the keychain-first/config-second window is
     * exactly when a re-probe would re-cache the wrong tenant. Best-effort; never throws into commit.
     */
    onSlotsChanged?: (slots: string[]) => void;
    now?: () => number;
    nowIso?: () => string;
    /** Delay before the step-6 re-verify (ms; default 90_000). Tests inject a small value. */
    reverifyDelayMs?: number;
    /** Recovery-barrier hang-timeout (ms; default 60_000). */
    recoveryBarrierTimeoutMs?: number;
    /** Random/sequence swap-id source (derives from NO token bytes). */
    swapIdFactory?: () => string;
}
export declare class CredentialSwapExecutor {
    private readonly funnel;
    private readonly ledger;
    private readonly keychain;
    private readonly configExchange;
    private readonly resolveIdentity;
    private readonly config;
    private readonly emitAudit?;
    private readonly emitAttention?;
    private readonly onSlotsChanged?;
    private readonly now;
    private readonly nowIso;
    private readonly reverifyDelayMs;
    private readonly recoveryBarrierTimeoutMs;
    private readonly swapIdFactory;
    /** Resolves once boot-recovery has resolved every in-flight journal row (or the barrier timed out). */
    private recoveryResolve;
    private readonly recoveryBarrier;
    private recoveryDone;
    private seq;
    constructor(deps: CredentialSwapExecutorDeps);
    /**
     * The balancer's first pass awaits THIS before selecting any pair — so a recovery WRITE can't
     * race a fresh swap on a different slot pair (round-2 unmutexed write-write find). Bounded by a
     * hang-timeout: a wedged recovery write must not freeze the balancer forever.
     */
    awaitRecoveryComplete(): Promise<void>;
    isRecoveryComplete(): boolean;
    /** SINGLE emit chokepoint — every record is scrubbed of token material here (§2.9). */
    private audit;
    /** Census #8: fire the cache-bust hook (best-effort — a consumer error never breaks commit). */
    private notifySlotsChanged;
    private stagingService;
    /**
     * EXCHANGE the credentials of two slots. Returns a verdict; never throws into the caller for an
     * expected condition (precondition fail / clobber-race / quarantine / busy lock are all results).
     */
    swap(slotA: string, slotB: string): Promise<SwapResult>;
    /** Runs under the single-mover mutex + both slot locks. */
    private swapLocked;
    private casReread;
    private verifyAndHeal;
    private scheduleReverify;
    /** Run the step-6 re-verify immediately (exposed for deterministic tests). */
    reverifyNow(slotA: string, slotB: string, wantA: string, wantB: string, staging: string, swapId: string): Promise<void>;
    /**
     * Resolve every in-flight swap journal row left by a crash. Acquires the single-mover mutex (a
     * recovery WRITE must not race a boot balancer pass on the ledger). When it finishes (or the
     * hang-timeout fires) the recovery barrier lifts so the balancer's first pass can run. A wedged
     * recovery write quarantines its slots BEFORE the barrier lifts, so the post-lift balancer
     * structurally cannot select a wedged slot.
     */
    recover(): Promise<void>;
    /** In-flight swap journal rows = a `begin`/`exchanged`/`verified` row whose swapId has no later `done`/`aborted`. */
    private inFlightSwaps;
    /**
     * Resolve one in-flight entry. Per §2.3 recovery decidability: re-read each slot, apply
     * adopt-on-newer (never a blind staging overwrite), and verify identity; an unverifiable slot is
     * quarantined (never repaired blindly). Staging is deleted + the row closed `done` ONLY when
     * both slots verify; otherwise the row stays in-flight and its staging is retained (the sweep
     * predicate protects any non-`done` row).
     */
    private recoverEntry;
    /**
     * A staging entry is an ORPHAN iff its swapId has NO journal row, OR its row is `done`. Any
     * non-`done` row (begin/exchanged/verified) PROTECTS its staging (the heal source through step
     * 6 — a literal "in-flight = begin only" reading would delete a committed row's heal source).
     * Caller supplies the live staging-service list (the executor never enumerates the keychain).
     */
    sweepOrphanStaging(stagingServices: string[]): Promise<string[]>;
    /** Read a slot's blob via the async keychain exec; require it parse + carry a refresh token. */
    private readBlob;
    private raiseQuarantineAttention;
}
//# sourceMappingURL=CredentialSwapExecutor.d.ts.map