import { MachineCredentialLock } from "./credential-lock.js"; import { type MachineCredentials, type MachineCredentialStore, type MachineTokenFamily } from "./machine-credentials.js"; import type { TokenExchangeClient } from "./token-exchange.js"; /** * Login-surface commit plumbing (unified-machine-auth 03 F1/F2/F6/F7/F10, 04 §4) — the shared * machinery every TypeScript login surface (the three CLIs, the App) uses to turn a successful * mint into machine-store state. Owns three invariants no surface may re-implement: * * - **The two-lock-hold commit sequence (F1.3/F1.4):** commit the agent family under a FIRST * lock hold, perform the RFC 8693 token exchange with the lock RELEASED, then write the * derived plugin family (+ v1 mirror) under a SECOND hold. Two holds by design: a failed * exchange leaves a valid, committed agent family and the surface reports * "partially authorized, retrying" ({@link CommitAgentLoginResult} `partial`) — it retries * the derivation alone via {@link derivePluginFamily}. * - **The account-switch guard (D6/F7):** before ANY write, the new mint's `subject` is compared * with the store's. A mismatch requires explicit confirmation; **decline revokes the * just-minted family (best effort, RFC 7009) and aborts with the store untouched** — no orphan * device row. Confirm revokes the old account's families (best effort) and REPLACES the store * (single-account store, D6). Missing subjects (pre-O5 servers) compare as "proceed" — F7.3. * - **Tools-only mode (O10/F10, `--tools-only`):** {@link commitToolsOnlyLogin} commits a * `scope=mcp:plugin` mint as a plugin family ONLY — the store then holds no agent family, so * App pickup is impossible by design and the runner appears as its own revocable device group. * * Plus the machine-wide sign-out ({@link signOutMachineWide}, F6/D5) and the RFC 7009 revocation * seam ({@link revokeTokenBestEffort}). Nothing here logs token material. */ /** Path (relative to the AS root) of the RFC 7009 token-revocation endpoint. */ export declare const OAUTH_REVOCATION_PATH = "/oauth/revoke"; /** Absolute revocation URL for an AS root. */ export declare function revocationUrl(serverBaseUrl: string): string; /** Options for {@link revokeTokenBestEffort}. */ export interface RevokeTokenOptions { /** The credential's server target (AS root or `/mcp` hub URL — normalized before use). */ serverTarget: string; /** The token to revoke (revoking a refresh token revokes its whole family server-side). */ token: string; /** The client id to present — the family's stored id, or the component default (F6.2). */ clientId: string; /** RFC 7009 §2.1 hint. Defaults to `refresh_token`. */ tokenTypeHint?: "refresh_token" | "access_token"; /** Injectable for tests; defaults to the global `fetch`. */ fetchImpl?: typeof fetch; /** Per-request network timeout (ms). Default 15s. */ timeoutMs?: number; } /** * RFC 7009 revocation, best effort: POSTs `token` + `token_type_hint` + `client_id` to * `{AS root}/oauth/revoke`. Returns true on an HTTP 2xx (the RFC returns 200 even for invalid * tokens) and false on ANY failure — it never throws (offline sign-out must proceed, F6.4). */ export declare function revokeTokenBestEffort(options: RevokeTokenOptions): Promise; /** The guard's verdict: write may proceed, or the surface must confirm the account switch. */ export type AccountSwitchDecision = { kind: "proceed"; } | { kind: "confirm-required"; storedSubject: string; newSubject: string; }; /** * The D6/F7 account-switch guard primitive: compare the new mint's `subject` against the stored * document's BEFORE any write. Only a genuine mismatch of two KNOWN subjects requires * confirmation — when either side is unknown (subject-less v1 / pre-O5 enroll credentials, or an * unreadable/missing store) the guard cannot judge and the write proceeds (F7.3: the guard * becomes fully effective once every mint path carries `sub`). */ export declare function evaluateAccountSwitch(stored: MachineCredentials | null, newSubject: string | undefined): AccountSwitchDecision; /** Best-effort revocation seam used by the commit/sign-out flows (injectable for tests/surfaces). */ export type RevokeTokenFn = (token: string, clientId: string, tokenTypeHint: "refresh_token" | "access_token") => boolean | Promise; /** Options for {@link runAccountSwitchGuard} — the shared D6/F7 guard runner for login surfaces. */ export interface AccountSwitchGuardOptions { store: MachineCredentialStore; /** The client id presented when revoking the JUST-MINTED family on decline. */ clientId: string; /** The freshly minted credentials being committed (subject compared; tokens revoked on decline). */ credentials: MachineCredentials; confirmAccountSwitch?: ((info: { storedSubject: string; newSubject: string; }) => boolean | Promise) | undefined; revokeToken?: RevokeTokenFn | undefined; fetchImpl?: typeof fetch | undefined; onWarning?: ((message: string) => void) | undefined; } /** * The guard's outcome. `premiseSubject` is the stored subject the decision was based on — * {@link commitFamilyUnderHold} RE-VERIFIES it under the lock before writing (review fix B2b: * the guard evaluates on a lock-free read and the confirm dialog can wait unbounded, so the * premise must still hold at write time). */ export type AccountSwitchGuardOutcome = { kind: "proceed"; confirmedSwitch: boolean; premiseSubject: string | undefined; } | { kind: "declined"; storedSubject: string; newSubject: string; }; /** * Run the D6/F7 guard including the decline path (revoke just-minted, abort, store untouched) * and — on a CONFIRMED switch — the best-effort revocation of the old account's families. * Shared by every login surface commit path (agent login, tools-only login, enroll). */ export declare function runAccountSwitchGuard(options: AccountSwitchGuardOptions): Promise; /** * Why a commit path aborted without writing (review fix B2): * - `guard-premise-changed` — the store's subject changed between the lock-free guard * evaluation (or the confirm dialog) and the write hold; re-run the commit so the guard * re-evaluates against the CURRENT store. * - `subject-changed` — a confirmed account switch landed between the two commit holds; the * derived family belongs to the OLD account and was not written. * - `store-missing` — a machine-wide sign-out (F6) landed between the holds; the store is * deliberately NOT recreated (a signed-out machine stays signed out). * - `store-unreadable` — the store turned unreadable at the write hold; it is never overwritten * by this path (04 §1) and the condition surfaces as a result, not a throw. */ export type CommitAbortReason = "guard-premise-changed" | "subject-changed" | "store-missing" | "store-unreadable"; /** Parameters of {@link commitFamilyUnderHold} — one guarded family write under ONE lock hold. */ export interface FamilyCommitParams { store: MachineCredentialStore; lock: MachineCredentialLock; name: "agent" | "plugin"; family: MachineTokenFamily; serverTarget?: string | undefined; subject?: string | undefined; /** The {@link runAccountSwitchGuard} proceed outcome whose premise is re-verified under the hold. */ guard: { confirmedSwitch: boolean; premiseSubject: string | undefined; }; } /** Outcome of {@link commitFamilyUnderHold}: written, or the guard premise no longer held. */ export type FamilyCommitOutcome = { committed: true; document: MachineCredentials; } | { committed: false; reason: "guard-premise-changed"; }; /** * Write one family under ONE lock hold, RE-VERIFYING the account-switch guard's premise inside * the hold before touching the store (review fix B2b — the guard evaluated on a lock-free read, * and a confirm dialog may have waited unbounded): * * - confirmed switch: the store's subject must still equal the premise the user confirmed * replacing; anything else (a concurrent login as a third account, a sign-out, an unreadable * store) aborts — the confirmation described a state that no longer exists. * - plain path: the guard is re-evaluated against the CURRENT store; a mismatch that appeared * since the lock-free read aborts (the confirm dialog cannot run under the lock). The * explicit-login `replaceUnreadable` semantic is preserved (04 §1). * * On `committed: false` the caller returns `guard-premise-changed` and the surface re-runs its * commit — the guard then re-evaluates against the new store state. */ export declare function commitFamilyUnderHold(params: FamilyCommitParams): Promise; export interface CommitAgentLoginOptions { store: MachineCredentialStore; /** The 04 §2 lock; defaults to one on the store's own directory. */ lock?: MachineCredentialLock; /** The RFC 8693 exchange client used to derive the plugin family (04 §4). */ exchangeClient: TokenExchangeClient; /** The login surface's OWN OAuth client id — stamped onto BOTH families (D8/O2). */ clientId: string; /** * The agent-plane mint result from `deviceLogin`/`authCodeLogin` (scope `mcp:agent`): access + * refresh tokens, expiry, `serverTarget`, and `subject` (from the JWT `sub`). */ credentials: MachineCredentials; /** D6/F7 confirmation callback. ABSENT ⇒ a subject mismatch is DECLINED (fail closed). */ confirmAccountSwitch?: (info: { storedSubject: string; newSubject: string; }) => boolean | Promise; /** Injectable best-effort revoker; defaults to RFC 7009 against the credential's serverTarget. */ revokeToken?: RevokeTokenFn; /** Injectable `fetch` for the default revoker (tests). */ fetchImpl?: typeof fetch; onWarning?: (message: string) => void; signal?: AbortSignal; } export type CommitAgentLoginResult = /** Agent family committed AND plugin family derived + mirrored — the machine is fully signed in. */ { status: "committed"; document: MachineCredentials; } /** * F1 failure path: the agent family IS committed, but the exchange failed — the surface shows * "partially authorized, retrying" and retries {@link derivePluginFamily} with backoff. */ | { status: "partial"; document: MachineCredentials; exchangeFailure: string; } /** D6/F7 decline: the just-minted family was revoked (best effort); the store is untouched. */ | { status: "switch-declined"; storedSubject: string; newSubject: string; } /** Review fix B2: a concurrent store change voided the commit — see {@link CommitAbortReason}. */ | { status: "aborted"; reason: CommitAbortReason; detail?: string; }; /** * Commit an agent-plane login (03 F1.3–F1.4 / F2.2): D6 guard → agent family under the FIRST * lock hold → RFC 8693 exchange (lock released) → plugin family + v1 mirror under the SECOND * hold. On a confirmed account switch the store is REPLACED (old families revoked best-effort); * otherwise the agent family is merged in, `replaceUnreadable` — an explicit login owns the * store (04 §1). */ export declare function commitAgentLogin(options: CommitAgentLoginOptions): Promise; export interface DerivePluginFamilyOptions { store: MachineCredentialStore; lock?: MachineCredentialLock; exchangeClient: TokenExchangeClient; /** The exchanging client's OWN id (stamped onto the derived family — O2). */ clientId: string; /** A FRESH agent-plane access token (the exchange `subject_token`). */ agentAccessToken: string; /** * The subject the agent credential belongs to (review fix B2a). When the store's subject at * the write hold is a DIFFERENT known subject — a confirmed account switch landed between the * holds — the write is refused (`aborted`/`subject-changed`): the derived family belongs to * the old account and must not enter the new account's store. Omit only when the mint carried * no `sub` (pre-O5 servers — nothing to compare, F7.3). */ expectedSubject?: string; serverTarget?: string; /** Injectable best-effort revoker for a derived family orphaned by an abort. */ revokeToken?: RevokeTokenFn; fetchImpl?: typeof fetch; onWarning?: (message: string) => void; signal?: AbortSignal; } export type DerivePluginFamilyResult = { status: "derived"; document: MachineCredentials; } | { status: "exchange-failed"; reason: string; } /** Review fix B2a: the hold-2 re-verification refused the write; nothing was written. */ | { status: "aborted"; reason: Exclude; detail?: string; }; /** * The F1.4 derivation leg alone: RFC 8693 exchange → plugin family + v1 mirror under ONE lock * hold. Also the retry entry point for the `partial` state. On failure NOTHING is written. * * **Hold-2 re-verification (review fix B2a).** The exchange runs with the lock released, so the * world may have changed before the write hold. Under the hold, the store must still match the * commit's expectations, or the write is refused as a result (never a throw): * - subject changed to a different known account (confirmed switch between holds) ⇒ * `aborted`/`subject-changed`; * - store gone (machine-wide sign-out, F6) ⇒ `aborted`/`store-missing` — a signed-out machine * is NEVER signed back in by recreating its store; * - store unreadable ⇒ `aborted`/`store-unreadable` — never overwritten by this path (04 §1). * On any abort the just-derived (now orphaned) family is revoked best-effort — no orphan device * row. */ export declare function derivePluginFamily(options: DerivePluginFamilyOptions): Promise; export interface CommitToolsOnlyLoginOptions { store: MachineCredentialStore; lock?: MachineCredentialLock; /** The minting surface's OWN OAuth client id (stamped onto the plugin family). */ clientId: string; /** The `scope=mcp:plugin` mint result (device flow with `--tools-only`, or enroll). */ credentials: MachineCredentials; confirmAccountSwitch?: (info: { storedSubject: string; newSubject: string; }) => boolean | Promise; revokeToken?: RevokeTokenFn; fetchImpl?: typeof fetch; onWarning?: (message: string) => void; } export type CommitToolsOnlyLoginResult = { status: "committed"; document: MachineCredentials; } | { status: "switch-declined"; storedSubject: string; newSubject: string; } /** Review fix B2b: the guard's premise changed before the write hold; nothing was written. */ | { status: "aborted"; reason: "guard-premise-changed"; }; /** * Commit a `--tools-only` (O10) mint: ONE lock hold writing the plugin family only (F10 — the * store then holds no agent family; App pickup is impossible by design). The same D6/F7 guard * applies (premise re-verified under the hold — B2b); a confirmed switch replaces the store. */ export declare function commitToolsOnlyLogin(options: CommitToolsOnlyLoginOptions): Promise; export interface SignOutMachineWideOptions { store: MachineCredentialStore; lock?: MachineCredentialLock; /** * The component's own client id — presented when revoking a family that stores none * (`families.legacy`, F6.2; RFC 7009 returns 200 even for a wrong id/invalid token). */ defaultClientId?: string; /** Injectable best-effort revoker; defaults to RFC 7009 against the document's serverTarget. */ revokeToken?: RevokeTokenFn; fetchImpl?: typeof fetch; onWarning?: (message: string) => void; } export interface SignOutMachineWideResult { /** Per-family best-effort revocation outcomes (empty when nothing was revocable). */ revoked: Array<{ family: string; ok: boolean; }>; /** True when the store file was deleted (the F6 delete path ran). */ deleted: boolean; } /** * Machine-wide sign-out (F6, D5 — "signs out all tools on this machine"; the caller shows the * confirmation): best-effort RFC 7009 revocation of EVERY family's refresh token (stored * `clientId`, falling back to `defaultClientId` for legacy — F6.2), then the 04 §2 delete path — * acquire lock → unlink store → release → unlink lock. Failed/offline revokes never block the * local delete (F6.4: families die naturally at ≤30 d). Throws {@link CredentialLockBusyError} * when the lock stays contended — sign-out must not delete the store mid-rotation; retry. */ export declare function signOutMachineWide(options: SignOutMachineWideOptions): Promise; //# sourceMappingURL=login-commit.d.ts.map