/** * Signing in and out, shared by `skills login` / `skills logout` / `skills * whoami` and the interactive TUI's `/login`, `/logout` and `/whoami`. * * The flow follows the Codex CLI model the owner chose (2026-09-23): a browser * sign-in by default, a printed device code for headless machines, an API key * read from stdin, and a logout that revokes what the sign-in minted. * * Device authorization follows RFC 8628: poll no faster than the server's * `interval`, add five seconds on every `slow_down`, and stop on * `expired_token`, `invalid_device_code` or `access_denied`. A pending device * session is saved beside the credentials file, owner-only, so a headless * `skills login --device` can be finished later with `skills login --poll` * instead of starting a new session and discarding the code the user approved. * * Nothing here decides WHERE to sign in: callers pass the origin from * `resolveSkillsSignInOrigin()`, which never sends a legacy internal key to the * product default. */ import { type StoredCredentialOrigin, type StoredKeyIssuer } from "./auth-store.js"; import { type SkillsFleetOptions } from "./fleet-credentials.js"; type Env = Record; /** The CLI's device-authorization client label, recorded by the server. */ export declare const SKILLS_CLI_DEVICE_CLIENT = "skills-cli"; /** RFC 8628 §3.5: every slow_down adds five seconds to the polling interval. */ export declare const SLOW_DOWN_INCREMENT_MS = 5000; export interface DeviceAuthorizationStart { /** Bearer capability for the pending key. Never printed; stored owner-only. */ deviceCode: string; userCode: string; verificationUri: string; verificationUriComplete?: string; expiresIn: number; interval: number; } /** What a successful sign-in returns: the key (or a session to mint one) and who signed in. */ export interface SignInResult { apiKey?: string; token?: string; user?: { id?: string; email?: string; role?: string; }; organization?: { id?: string; slug?: string; name?: string; }; firstLogin?: boolean; } export type DevicePollOutcome = { status: "authorized"; result: SignInResult; } | { status: "expired"; } | { status: "invalid"; } | { status: "denied"; } | { status: "timeout"; } | { status: "cancelled"; }; /** Validate the server's device-start answer; a malformed one is refused, never guessed at. */ export declare function parseDeviceAuthorizationStart(body: unknown): DeviceAuthorizationStart; /** Ask the instance for a device code. Sends no credential. */ export declare function startDeviceAuthorization(origin: string, client?: string): Promise; /** One poll of the token endpoint. Sends only the device code. */ export declare function pollDeviceToken(origin: string, deviceCode: string): Promise; export interface PollDeviceAuthorizationInput { /** One token-endpoint request. Resolves with the body or throws HostedApiError. */ poll: () => Promise; intervalSeconds: number; /** Epoch milliseconds after which polling stops with `timeout`. */ deadline: number; signal?: AbortSignal; /** Called with the new interval each time the server asks to slow down. */ onSlowDown?: (intervalMs: number) => void; sleep?: (ms: number) => Promise; now?: () => number; } /** * Poll until the user approves, the code expires, or the deadline passes. * * Transport failures and unexpected server errors are thrown, never read as * "still pending": a sign-in that cannot reach its server must say so. */ export declare function pollDeviceAuthorization(input: PollDeviceAuthorizationInput): Promise; export interface PendingDeviceSignIn { origin: string; deviceCode: string; userCode: string; verificationUri: string; verificationUriComplete?: string; interval: number; /** Epoch milliseconds. */ expiresAt: number; } /** * Beside the credentials file, one per profile AND instance: * `device-login[-]-.json`. Keyed by origin so a sign-in * started on one server never overwrites a pending session for another. */ export declare function pendingDeviceSignInPath(origin: string, env?: Env): string; export declare function savePendingDeviceSignIn(origin: string, start: DeviceAuthorizationStart, env?: Env, now?: number): string; /** * The saved session for exactly this instance, if it has not expired. A session * saved for a different instance is never returned, so its device code is only * ever sent back to the server that issued it. */ export declare function loadPendingDeviceSignIn(origin: string, env?: Env, now?: number): PendingDeviceSignIn | null; /** The instances with an unexpired pending sign-in for this profile. */ export declare function pendingDeviceSignInOrigins(env?: Env, now?: number): string[]; /** Forget the pending session for one instance, or every pending session of this profile. */ export declare function clearPendingDeviceSignIn(env?: Env, origin?: string): void; /** * Store the key a sign-in produced, bound to the instance that issued it. * A session-only answer (a token, no key) mints a CLI key first. */ export declare function persistSignIn(result: SignInResult, origin: string, env?: Env, issuedBy?: StoredKeyIssuer): Promise; /** * What happened to server-side revocation. * * revoked — the server confirmed it revoked the key. * already_ended — the server no longer accepts the key (HTTP 401/403). * not_requested — a user-brought key deleted without --revoke (still works). * skipped — --no-revoke on a login-minted key (may still be live). * unsupported — cannot be revoked: the server has no revoke route * (404/405/501), the internal gateway has no login service, * or an older sign-in left no record of how it was issued. * not_revoked — the server answered without confirming revocation. * failed — the request did not complete (network, 5xx, bad answer). * none — no credential stored by `skills login` to revoke. */ export type RevocationOutcome = "revoked" | "already_ended" | "not_requested" | "skipped" | "unsupported" | "not_revoked" | "failed" | "none"; /** A stable reason code plus one plain sentence. Never contains a credential. */ export interface SignOutReason { code: "revocation_failed" | "revocation_not_confirmed" | "revocation_unsupported" | "revocation_skipped" | "credential_not_from_login" | "credential_still_active" | "local_delete_failed"; message: string; } export interface SignOutResult { /** Exit 0 when true: every reason list is empty. */ signedOut: boolean; /** What happened to the active profile's stored credential. */ stored: "deleted" | "none" | "left_alone" | "delete_failed"; storedBy?: StoredCredentialOrigin; /** The credentials file, when one holds a credential. */ file?: string; /** The instance the stored credential belongs to. */ origin?: string; revocation: RevocationOutcome; /** Sentences for a human: what was done, in order. */ notes: string[]; /** Why the command must exit non-zero. Empty when signed out. */ reasons: SignOutReason[]; } export interface SignOutOptions { /** * true: also revoke a user-brought or legacy key (`--revoke`). * false: revoke nothing (`--no-revoke`); a login-minted key left live exits non-zero. * undefined: the default — revoke only what login minted. */ revoke?: boolean; env?: Env; /** Resolution controls (a fake Keychain in tests). */ fleet?: SkillsFleetOptions; /** * A profile the ENVIRONMENT selects that differs from the active one, e.g. * `HASNA_PROFILE=b skills --profile a logout`. If it still holds a credential, * the next plain command uses it, so logout names it and exits non-zero. */ environmentProfile?: string; } /** Where a person revokes a key by hand. Never a composed URL. */ export declare function manualRevocationHint(origin: string): string; export declare function signOut(options?: SignOutOptions): Promise; export type SignedInAccount = { signedIn: false; reason: string; } | { signedIn: true; apiOrigin: string; source: string; email?: string; organization?: string; error?: string; }; /** The account in effect, for a compact display (the TUI's `/whoami`). */ export declare function readSignedInAccount(env?: Env): Promise; /** * The verification page to open, or null when it must only be printed. * * The URL comes from the server's answer, so it is opened only when it is an * https URL (http only on loopback) with no userinfo, on the sign-in origin * itself or on `auth.` — where rule global-product-auth-url-layout * will move the identity endpoints. Anything else is printed for the user to * judge and never handed to the operating system. */ export declare function verificationUrlToOpen(raw: string, signInOrigin: string): string | null; /** * The opener argv for a platform. Never a shell: on Windows `cmd /c start` * would re-parse `&`, `|` and `^` in a server-provided URL, so the URL goes to * the URL protocol handler as one argument instead. */ export declare function browserCommand(url: string, platform?: NodeJS.Platform): string[]; /** * Best effort: the URL and code are always printed too, so a refusal or a * failure here costs nothing. Returns whether a browser was asked to open it. */ export declare function openVerificationPage(url: string, signInOrigin: string): boolean; export {};