/** * Decision-provider credential store — the provider kind, the two settings a * user supplies (an API base URL and an API key) and the default model. * * The file stays at schema version 1. T12713 added `provider` (`layahost` | * `jev`) as an OPTIONAL, additive field instead of bumping the version, so a * CLEO that predates provider kinds (whose zod object is non-strict and * accepts only `version: 1`) still parses the file after a downgrade. A file * without `provider` infers the kind from its base URL * ({@link inferDecisionProviderKind}). * * Stored at `/decide-credentials.json` (resolved through * `@cleocode/paths`, arch gate 2) with 0600 permissions: * * - Writes go through `withLock` (cross-process lock) and * `writeJsonFileAtomic({ mode: 0o600 })`, so neither the live file, the * temp file nor a rotated backup ever exists at a looser mode. Modelled on * `../llm/credentials-store.ts`. * - The file lives at the TOP level of the CLEO home so its rotated backups * land in the owner-only `/.backups/`, and the portable-bundle * scanner classifies it as a secret (see `store/portable-bundle-scan.ts`). * - Reads return a {@link SealedDecideConnection}: the key sits in a private * class field, so `JSON.stringify`, `util.inspect` and template strings of * the handle only ever show a masked last-4 preview. The plaintext is * materialised solely by {@link SealedDecideConnection.connection}, which the * decision client calls at the wire. * * The key never lands in `config.json`, logs, LAFS envelopes or the decisions * audit: nothing here logs, and every summary this module returns carries the * masked preview only. * * ## Profiles (T12733) * * The store holds any number of profiles, several per provider (accounts, * each with its own key), addressed as `/` (e.g. * `layahost/work`, `layahost/personal`, `jev/team`), and exactly one ACTIVE * profile, which everyday decisions use. The format stays additive on * version 1: * * ```json * { "version": 1, * "provider": "layahost", "baseUrl": "https://layahost.com", "apiKey": "…", * "model": "laya-auto", "updatedAt": "…", * "active": "layahost/work", * "profiles": { * "layahost/work": { "provider": "layahost", "name": "work", "baseUrl": "default", "apiKey": "…", "model": "laya-auto" }, * "jev/team": { "provider": "jev", "name": "team", "baseUrl": "https://jev.internal", "apiKey": "…", "model": "…" } } } * ``` * * - A profile's `baseUrl` is the literal `default` (its provider's preset * URL, resolved at call time, so a changed preset flows through) or an * override URL. * - The top-level single-config fields ARE the active profile with its URL * resolved, so a CLEO that predates profiles (9.23) reads the active profile * as its one config. Every write keeps them in sync. * - A file without `profiles` (written before T12733) is one active profile, * `/default`. * - An older CLEO that rewrites the file drops `profiles` and `active` (its * zod object strips unknown keys and its save replaces the file). When the * top-level settings then differ from the active profile, the TOP-LEVEL * settings are authoritative: they replace the active profile (or, when the * provider changed, become `/`), and * {@link listDecideProfiles} reports `reconciled: true`. Profiles dropped * by such a rewrite cannot be recovered and must be added again. Empty * top-level settings beside a stored active profile mean the older CLEO * cleared them: no profile is active. * * @task T12491 * @task T12713 * @task T12733 * @epic T12486 */ import { inspect } from 'node:util'; import type { DecideProfileConnection, DecideProfileListResult, DecideProfileUrlSource, DecisionProviderKind } from '@cleocode/contracts'; import type { DecisionProviderConnection } from './provider.js'; /** File name of the store, directly under the CLEO home. */ export declare const DECIDE_CREDENTIALS_FILE = "decide-credentials.json"; /** * On-disk schema version. Deliberately still 1: `provider` is an additive * optional field, and released CLEO parses `z.literal(1)` — bumping this would * silently disable System One after a downgrade. */ export declare const DECIDE_CREDENTIALS_VERSION = 1; /** * Whether `name` is a valid profile name: 1-32 characters of `a-z`, `0-9` * and `-`, starting and ending with a letter or digit. * * @param name - Candidate name (the part after `/`). * @returns True when the name may be stored. */ export declare function isValidDecideProfileName(name: string): boolean; /** A parsed profile reference. */ export interface DecideProfileRef { /** Provider kind. */ readonly provider: DecisionProviderKind; /** Profile name within the provider. */ readonly name: string; /** `/`. */ readonly id: string; } /** * Build a profile id. * * @param provider - Provider kind. * @param name - Profile name. * @returns `/`. */ export declare function decideProfileId(provider: DecisionProviderKind, name: string): string; /** * Parse a profile reference: `/` (e.g. `layahost/work`), or a * bare provider kind meaning `/default`. * * @param ref - Reference as typed by the user. * @returns The parsed reference, or `null` when it names no valid profile. */ export declare function parseDecideProfileRef(ref: string): DecideProfileRef | null; /** Settings accepted by {@link saveDecideCredentials}. */ export interface DecideCredentialsInput { /** * Provider kind. Omitted → the provider of a `/` profile, * else inferred from the URL (the layahost origin → `layahost`, any other → * `jev`), else `layahost` for a `default` URL. */ readonly provider?: DecisionProviderKind; /** * `default` (the provider's preset URL, resolved at call time) or an * absolute override URL, e.g. `https://provider.example`. A URL equal to * the preset is stored as `default`. */ readonly baseUrl: string; /** API key. Leading/trailing whitespace (e.g. a piped newline) is trimmed. */ readonly apiKey: string; /** Optional default model; omitted → the request carries no model. */ readonly model?: string; /** * Profile to add or update (T12733): `/` or a bare name. * Omitted → `/default`. Other profiles are kept. */ readonly profile?: string; /** * Make the profile the active one. Omitted → true when no profile is active * yet (the first profile) or the profile is already active; else false. */ readonly activate?: boolean; } /** Secret-free description of the stored settings. Safe to log and to emit in envelopes. */ export interface DecideCredentialsSummary { /** True when a valid base URL and a non-blank key are stored. */ readonly configured: boolean; /** Absolute path of the store file. */ readonly path: string; /** Stored provider kind, when present. */ readonly provider?: DecisionProviderKind; /** Stored base URL, when present. */ readonly baseUrl?: string; /** Stored default model, when present. */ readonly model?: string; /** Masked key preview (`…abcd`), when a key is stored. */ readonly keyPreview?: string; /** ISO timestamp of the last write, when known. */ readonly updatedAt?: string; /** Id (`/`) of the profile described (T12733), when one is stored. */ readonly profile?: string; /** Whether the profile's URL is its provider's preset or an override (T12733). */ readonly urlSource?: DecideProfileUrlSource; /** Id of the active profile (T12733), when one is active. */ readonly activeProfile?: string; } /** * Absolute path of the decision-credential store. * * @returns `/decide-credentials.json`. */ export declare function decideCredentialsPath(): string; /** * Mask an API key down to its last four characters. * * @param apiKey - Plaintext key (read transiently, not retained). * @returns `…` plus at most the last 4 characters; `…` alone for keys of 4 or fewer characters. */ export declare function maskApiKey(apiKey: string): string; /** * Opaque handle to a stored connection. The key is held in a private field; * serialising or inspecting the handle yields the masked preview only. */ export declare class SealedDecideConnection { #private; /** Provider kind (not secret). */ readonly provider: DecisionProviderKind; /** Provider base URL (not secret). */ readonly baseUrl: string; /** Default model, when configured. */ readonly model?: string; /** Masked key preview (`…abcd`). */ readonly keyPreview: string; /** Profile id (`/`, T12733), when resolved from a profile. */ readonly profile?: string; /** * @param baseUrl - Provider base URL. * @param apiKey - Plaintext key; captured privately. * @param model - Optional default model. * @param provider - Provider kind. Default `jev`. * @param profile - Profile id, when known. */ constructor(baseUrl: string, apiKey: string, model?: string, provider?: DecisionProviderKind, profile?: string); /** * Materialise the wire connection. Call only where the request is built. * * @returns Base URL, plaintext key, optional model and the profile id when known. */ connection(): DecisionProviderConnection; /** @returns The secret-free JSON form. */ toJSON(): { provider: DecisionProviderKind; baseUrl: string; model?: string; keyPreview: string; profile?: string; }; /** @returns A secret-free string form. */ toString(): string; /** Secret-free `util.inspect` / `console.log` form. */ [inspect.custom](): string; } /** * Whether `baseUrl` is an acceptable provider URL: absolute `https://`, or * plain `http://` to a loopback host (`localhost`, `127.0.0.1`, `::1`). A * remote plain-http URL is rejected so the bearer key never crosses the * network in clear text. * * @param baseUrl - Candidate base URL. * @returns True when the URL may be stored and used. */ export declare function isAllowedDecideBaseUrl(baseUrl: string): boolean; /** * Whether two base URLs address the same host (hostname + port). * * @param a - First URL. * @param b - Second URL. * @returns True when both parse and share `host`; false otherwise. */ export declare function sameDecideHost(a: string, b: string): boolean; /** * Load the ACTIVE profile's connection: what everyday decisions use. Reads * the top-level settings, which are the active profile with its URL * resolved. Synchronous and total: missing, malformed or incomplete * settings return `null` (the decision client then falls back to heuristics). * * @returns The sealed connection, or `null` when unconfigured. */ export declare function loadDecideConnection(): SealedDecideConnection | null; /** * Load a profile's connection without changing the active profile. Total: * an invalid or unknown reference, or unusable settings, return `null`. * * @param ref - `/`, or a bare provider for `/default`. * @returns The sealed connection, or `null`. */ export declare function loadDecideProfile(ref: string): SealedDecideConnection | null; /** * Resolve a profile to its connection, for callers (the System One * benchmark) that address a profile by name without switching the active * one. A `default` URL is resolved to the provider's current preset. The * result CONTAINS THE PLAINTEXT KEY: pass it to the wire, never log or emit * it. For a secret-safe handle use {@link loadDecideProfile}. * * @param ref - `/` (e.g. `layahost/work`), or a bare provider for `/default`. * @returns `{ name, profile, provider, baseUrl, apiKey, model }`, `name` and `profile` being the id. * @throws {DecideCredentialsError} When the reference is invalid or unknown, * or the profile's settings are unusable. The message lists the stored ids. */ export declare function resolveDecideProfile(ref: string): DecideProfileConnection; /** * List every stored profile, the active one marked, keys masked. The * benchmark uses it to enumerate the profiles it can call by id. * * @returns The profiles sorted by id, the active id, and whether the * top-level settings overrode a stale active entry (an older CLEO rewrote the file). */ export declare function listDecideProfiles(): DecideProfileListResult; /** * Describe the ACTIVE profile's settings without the key. * * @returns A secret-free summary. */ export declare function describeDecideCredentials(): DecideCredentialsSummary; /** * Describe one profile's settings without the key. * * @param ref - `/`, or a bare provider for `/default`. * @returns A secret-free summary; `configured: false` when the profile is unknown. */ export declare function describeDecideProfile(ref: string): DecideCredentialsSummary; /** Error thrown by {@link saveDecideCredentials} for invalid input. The message never contains the key. */ export declare class DecideCredentialsError extends Error { /** @param message - Secret-free description. */ constructor(message: string); } /** Reset the once-only loose-home warning. Tests only. @internal */ export declare function _resetDecideHomeWarningForTest(): void; /** * Store a profile: provider kind, base URL (`default` or an override), API * key and optional model (0600, locked, atomic). Adds the profile or * replaces that profile's settings; every other profile is kept. A URL equal * to the provider's preset is stored as `default`, so a changed preset flows * through. Writes schema version 1 with the active profile mirrored at the * top level, URL resolved. * * @param input - Settings to store, the profile and whether to activate it. * @returns Secret-free summary of the profile now stored. * @throws {DecideCredentialsError} When the URL is not https (or loopback http), `default` * names a provider without a preset URL, the key is blank, the model or profile is * invalid, or the store or its backups are symlinks. */ export declare function saveDecideCredentials(input: DecideCredentialsInput): Promise; /** * Make a profile the active one: everyday decisions use it from now on. * * @param ref - `/`, or a bare provider for `/default`. * @returns The profile list after the switch. * @throws {DecideCredentialsError} When the reference is invalid or unknown. */ export declare function useDecideProfile(ref: string): Promise; /** * Remove a profile, then delete every rotated backup of the store (a backup * may hold the removed key). The active profile is removed only when `use` * names another profile to activate in its place. * * @param ref - Profile to remove. * @param use - Profile to activate instead; required when `ref` is active. * @returns The profile list after the removal. * @throws {DecideCredentialsError} When a reference is invalid or unknown, * or `ref` is active and `use` is missing or names it. */ export declare function removeDecideProfile(ref: string, use?: string): Promise; /** * Remove every profile: the store is overwritten with empty settings and * every rotated backup of it (which may hold an old key) is deleted. * * @returns True when settings were present before the call. */ export declare function clearDecideCredentials(): Promise; //# sourceMappingURL=credentials.d.ts.map