/** * Cleo Nexus session-token store. * * Stored at `/nexus-credentials.json` (resolved through * `@cleocode/paths`, arch gate 2) with 0600 permissions, one session per API * origin. Modelled on `../decide/credentials.ts`: * * - Writes go through `withLock` and `writeJsonFileAtomic({ mode: 0o600 })`, * so neither the live file, the temp file nor a rotated backup is ever * looser than owner-only. Deleting a session also purges the rotated * backups, which would otherwise keep a copy of the token. * - Reads return a {@link SealedNexusSession}: the token sits in a private * field, so `JSON.stringify`, `util.inspect` and template strings of the * handle show a masked preview only. {@link SealedNexusSession.bearer} is the * one accessor, called where the HTTP request is built. * * Everything goes through the {@link NexusTokenStore} interface, so an OS * keychain store can replace the file later without touching the callers. * * @task T12712 * @epic T12322 */ import { inspect } from 'node:util'; import type { NexusAccountOrganization, NexusAccountUser } from '@cleocode/contracts'; import { z } from 'zod'; /** File name of the store, directly under the CLEO home. */ export declare const NEXUS_CREDENTIALS_FILE = "nexus-credentials.json"; declare const sessionSchema: z.ZodObject<{ token: z.ZodString; tokenType: z.ZodString; expiresAt: z.ZodNullable; user: z.ZodNullable>; }, z.core.$strip>>; organization: z.ZodNullable>; role: z.ZodOptional; personal: z.ZodOptional; }, z.core.$strip>>; createdAt: z.ZodString; }, z.core.$loose>; /** A session to store. The token is the only secret. */ export interface NexusSessionInput { /** Bearer session token issued by the device-code grant. */ readonly token: string; /** Token type the server reported (`Bearer`). */ readonly tokenType: string; /** ISO expiry, when the server reported one. */ readonly expiresAt: string | null; /** The signed-in user, when known. */ readonly user: NexusAccountUser | null; /** Primary organization, when known. */ readonly organization: NexusAccountOrganization | null; } /** * Pluggable storage for Nexus session tokens, keyed by API origin. * * The file store below is the default. A keychain-backed store implements the * same four methods. */ export interface NexusTokenStore { /** Absolute location shown to the user (a path, or a keychain service name). */ readonly location: string; /** The session for `apiUrl`'s origin, or `null`. */ get(apiUrl: string): Promise; /** Store (replace) the session for `apiUrl`'s origin. */ put(apiUrl: string, session: NexusSessionInput): Promise; /** * Delete the session for `apiUrl`'s origin. With `expected`, delete only if * the stored token is still that session's token (checked under the store's * lock), so a login that finished meanwhile is not wiped. Returns `true` when * a session was deleted. */ delete(apiUrl: string, expected?: SealedNexusSession): Promise; /** Every stored session. */ list(): Promise; } /** * Mask a token down to a short, non-reversible preview. * * @param token - Plaintext token (read transiently, not retained). * @returns `…` plus at most the last 4 characters. */ export declare function maskNexusToken(token: string): string; /** * Opaque handle to a stored session. The token is held in a private field; * serialising or inspecting the handle yields the masked preview only. */ export declare class SealedNexusSession { #private; /** API origin the session belongs to. */ readonly apiUrl: string; /** Token type (`Bearer`). */ readonly tokenType: string; /** ISO expiry, when known. */ readonly expiresAt: string | null; /** Signed-in user, when known. */ readonly user: NexusAccountUser | null; /** Primary organization, when known. */ readonly organization: NexusAccountOrganization | null; /** ISO time the session was stored. */ readonly createdAt: string; /** Masked token preview. */ readonly tokenPreview: string; /** * @param apiUrl - API origin. * @param record - Stored record; the token is captured privately. */ constructor(apiUrl: string, record: z.infer); /** * The plaintext token. Call only where the `Authorization` header is built. * * @returns The bearer token. */ bearer(): string; /** Secret-free JSON form. */ toJSON(): Record; /** Secret-free string form. */ toString(): string; /** Secret-free `util.inspect` form. */ [inspect.custom](): string; } /** Thrown when the store cannot be written safely. */ export declare class NexusCredentialsError extends Error { constructor(message: string); } /** * The storage key for an API URL: its origin (scheme, host and port). * * @param apiUrl - Any URL on the API host. * @returns The origin, e.g. `https://api.cleocode.dev`. */ export declare function nexusOriginKey(apiUrl: string): string; /** * Absolute path of the default file store. * * @returns `/nexus-credentials.json`. */ export declare function nexusCredentialsPath(): string; /** * File-backed {@link NexusTokenStore}: `/nexus-credentials.json`, * mode 0600, one entry per API origin. */ export declare class FileNexusTokenStore implements NexusTokenStore { /** Absolute path of the store file. */ readonly location: string; /** @param path - Store path; defaults to {@link nexusCredentialsPath}. */ constructor(path?: string); get(apiUrl: string): Promise; put(apiUrl: string, session: NexusSessionInput): Promise; delete(apiUrl: string, expected?: SealedNexusSession): Promise; list(): Promise; /** * Read and validate the file; a missing or empty file reads as empty. A * malformed or newer-format file is refused and left as is (T12880). A * file readable or writable by group/others, or owned by another user, is * refused: its token may already be exposed or planted. */ private read; /** Locked, atomic, owner-only read-modify-write. */ private write; /** * Create the store file owner-only if it does not exist. `O_EXCL` plus * `O_NOFOLLOW` close the window between the symlink check and the create: a * symlink planted meanwhile makes the open fail instead of being followed. * On `EEXIST` the existing entry is re-opened with `O_NOFOLLOW`, which fails * (`ELOOP`) on a symlink, so a planted link is refused before the locked * write can touch it. */ private seed; /** * Throw when the store exists with a mode wider than 0600 or another owner. * Skipped on Windows, where Node reports every file as 0o666 and uids do not * apply; the file lives in the user's profile, protected by its ACL. */ private assertPrivate; /** Refuse to write a token through a symlink (it could point anywhere). */ private assertNoSymlinks; /** Rotated backups of the store file. */ private backupFiles; /** Delete every rotated backup (best effort). */ private purgeBackups; } export {}; //# sourceMappingURL=nexus-credentials.d.ts.map