/** * `runOnboardingLogin` — the 3-step onboarding login engine (T11724 · M3). * * The imperative front-door flow that takes a provider from a cold start to a * usable, validated profile binding. It ORCHESTRATES the already-merged * credential / catalog / config / resolver accessors — it does NOT reimplement * OAuth, construct a transport, or open an LLM client (Gate-13: chokepoint only). * * ## The four steps * * 1. **connect** — resolve the provider (alias → canonical via * {@link getProviderProfile}) and land a credential (Account) in the * multi-credential pool via {@link addCredential}. The OAuth token / API * key is supplied by the caller (`opts.token`): the owner-driven browser * step (PKCE / device-code) runs in `cleo llm login` BEFORE this engine, * so the engine itself is non-interactive and testable. OAuth-less * providers pass the same `opts.token` as an API key (AC4). * 2. **select** — choose a model from the catalog: `opts.model` when given * (validated against the catalog), else the latest by `release_date` via * {@link resolveProviderDefaultModel}. * 3. **bind** — write the `llm.default` (or `llm.roles[role]`) config * binding tying the account + model together via {@link setConfigValue}. * 4. **validate** — round-trip the binding through {@link resolveLLMForSystem} * and confirm the resolved provider/model are consistent. NOT a live LLM * call: no token is materialized; only resolution metadata is asserted, * and the secret stays behind the sealed handle. * * ## Security (AC2) * * The {@link OnboardingResult} envelope carries NO secret fields — never an * access token, API key, or refresh token. The plaintext is materialized only * at the wire via the sealed handle, never on this envelope. * * ## Testability (AC3) * * Every step depends only on the injectable {@link OnboardingDeps} seam, which * defaults to the real core accessors. Tests pass a {@link StubOnboardingDeps} * (stub catalog + in-memory pool/config + stub resolver) to exercise each step * in isolation without a live browser, network, or disk. * * @module llm/onboarding/login-engine * @task T11724 * @epic T11671 (E6-ONBOARDING-FRONT-DOOR) */ import type { ModelTransport, OnboardingAuthMode, OnboardingResult, ProviderProfile, RoleName } from '@cleocode/contracts'; import { type StoredAuthType } from '../credentials-store.js'; /** * Options for {@link runOnboardingLogin}. * * @task T11724 */ export interface OnboardingLoginOptions { /** * The credential secret to store — an OAuth access token (when * `authMode: 'oauth'`) or a raw API key (when `authMode: 'api_key'`). * * The interactive OAuth browser/device-code exchange runs in * `cleo llm login` BEFORE this engine; the resulting token is passed here so * the engine is non-interactive and testable. When omitted, the connect step * fails cleanly with `E_ONBOARDING_NO_CREDENTIAL` (no throw). */ token?: string; /** * Auth scheme for the supplied `token`. Defaults to `'oauth'` when the * resolved provider profile exposes an OAuth config, else `'api_key'` (AC4). */ authMode?: OnboardingAuthMode; /** Human-readable label stored alongside the credential. Defaults to `'oauth-login'`. */ label?: string; /** * Explicit model id to bind. When omitted, the latest catalog model for the * provider (by `release_date`) is selected. When given, it is validated * against the catalog and rejected with `E_MODEL_NOT_IN_CATALOG` on a miss. */ model?: string; /** * Bind the model to a specific role (`llm.roles[role]`) instead of the * global default (`llm.default`). The bound name surfaces as * {@link OnboardingResult.profileName}. */ role?: RoleName; /** Project root passed through to the resolver during the validate step. */ projectRoot?: string; /** * Skip the connect-step credential write because an interactive OAuth flow * (e.g. `cleo llm login`'s PKCE / device-code dance) has ALREADY landed the * credential in the pool. The connect step still resolves the provider and * records an `ok` trace, but does NOT call {@link addCredential} a second * time. Used by the shared front-door orchestrator (T11725) so the OAuth * token is stored exactly once. * * When `true`, {@link OnboardingLoginOptions.token} is NOT required. */ credentialAlreadyStored?: boolean; } /** * Minimal resolver result the validate step asserts against — a structural * subset of `ResolvedLLMForSystem` so a stub resolver need not construct a full * client/credential graph. * * @task T11724 */ export interface OnboardingResolution { /** Provider the binding resolved to. */ provider: ModelTransport; /** Model the binding resolved to. */ model: string; /** * Sealed credential handle (or `null`). The validate step reads only its * presence + `tokenPreview` — it NEVER calls `fetch()`, so no secret is * materialized during onboarding. */ sealedCredential: { tokenPreview: string; } | null; } /** * Injectable accessor seam. Defaults to the real `@cleocode/core` accessors; * tests override individual members to exercise each step in isolation. * * @task T11724 */ export interface OnboardingDeps { /** Resolve a provider profile by name/alias (alias → canonical). */ getProviderProfile: (name: string) => Promise; /** Persist a credential (Account) into the multi-credential pool. */ addCredential: (input: { provider: ModelTransport; label: string; authType: StoredAuthType; accessToken: string; source: string; }) => Promise; /** Map a CLEO provider name to its models.dev catalog key. */ catalogKeyForProvider: (providerName: string) => string; /** Resolve the latest catalog model for a provider, or `null`. */ resolveProviderDefaultModel: (catalogKey: string) => string | null; /** Validate a model id against the catalog for a provider. */ validateModelForProvider: (model: string, catalogKey: string) => { valid: boolean; reason: string; }; /** Write a global config value (the `llm.default` / `llm.roles[role]` binding). */ setConfigValue: (key: string, value: unknown) => Promise; /** * Round-trip resolve the binding for the validate step. * * `role` is the role the bind step just wrote (`llm.roles[role]`), so the * validate resolution MUST target that role — resolving a different role * would validate an unrelated binding (T11725 takeover review). */ resolve: (provider: ModelTransport, projectRoot?: string, role?: RoleName) => Promise; } /** * Run the 3-step onboarding login flow for a provider (T11724 · M3). * * Walks **connect → select → bind → validate**, reusing the merged credential / * catalog / config / resolver accessors. Returns a typed {@link OnboardingResult} * envelope with the per-step trace and the non-secret binding identifiers. * * Like the resolver chokepoint, this engine **never throws** for an expected * failure (missing credential, unknown model, config write error): it returns * an envelope whose failing step carries `status: 'failed'` and a stable `E_*` * `code`, with `validated: false`. Unexpected accessor errors are caught and * mapped to a failed step trace as well. * * @param provider - Provider name or alias (e.g. `'anthropic'`, `'codex'`). * @param opts - {@link OnboardingLoginOptions} (token, model, role, …). * @param deps - Injectable accessor seam (AC3); defaults to real core accessors. * @returns The typed onboarding result envelope; never throws on expected failure. * * @task T11724 */ export declare function runOnboardingLogin(provider: string, opts?: OnboardingLoginOptions, deps?: OnboardingDeps): Promise; //# sourceMappingURL=login-engine.d.ts.map