/** * Console session token storage and verification (#1780). * * Manages the file at `~/.dollhouse/run/console-token.auth.json` which * holds the Bearer tokens that authenticate requests to the web console. * The file is created on first leader election and persists across * restarts — tokens only rotate when explicitly requested. * * The `.auth` filename suffix keeps this state isolated from any legacy * no-authentication DollhouseMCP installation running on the same * machine. The port, lock file, and token file all share the same * isolation strategy — see `src/config/env.ts` for the port, and * `LeaderElection.ts` for the lock file. * * Schema is forward-compatible with multi-device, multi-tenant, and * scope-restricted tokens for Phase 2+. Phase 1 uses a single "console" * kind token and stubs the scope/boundary checks. * * File format (version 1): * ```json * { * "version": 1, * "tokens": [ * { * "id": "018e1a2b-...", * "name": "Kermit on mick-MacBook-Pro", * "kind": "console", * "token": "<64-hex>", * "scopes": ["admin"], * "elementBoundaries": null, * "tenant": null, * "platform": "local", * "labels": {}, * "createdAt": "2026-04-04T20:00:00.000Z", * "lastUsedAt": null, * "createdVia": "initial-setup" * } * ], * "totp": { "enrolled": false, "secret": null, "backupCodes": [] } * } * ``` * * @since v2.1.0 — Issue #1780 */ /** * Default path to the authenticated console's token file. * * The `.auth` suffix isolates this from any legacy no-authentication * DollhouseMCP installation that may also be running on the same * machine. Legacy installs did not write a token file at all; the * authenticated console writes `console-token.auth.json`. Combined with * the port and lock-file separation, this gives the two generations of * the console fully independent state trees under `~/.dollhouse/run/`. * * Callers can override via `DOLLHOUSE_CONSOLE_TOKEN_FILE` in the env. */ declare const DEFAULT_TOKEN_FILE: string; /** Current token file schema version. */ declare const TOKEN_FILE_VERSION: 1; /** * Element visibility boundary — Phase 3 enterprise feature. * Phase 1 always stores `null`; the field exists so the schema is stable. */ export interface ElementBoundary { allowCategories?: string[]; denyCategories?: string[]; allowTypes?: string[]; denyTypes?: string[]; } /** * A single token entry in the console token file. * * Enterprise-ready fields (`scopes`, `elementBoundaries`, `tenant`, `platform`, * `labels`) are present from Phase 1 but not enforced — the middleware treats * every valid token as admin-scoped, single-tenant, all-elements for now. * Phase 2+ flips enforcement on without requiring a schema migration. */ export interface ConsoleTokenEntry { /** Stable unique identifier for this token (UUID v4). */ id: string; /** Human-readable name shown in the Security tab. */ name: string; /** What kind of client this token is for. */ kind: 'console' | 'device' | 'automation'; /** The secret Bearer value. 64 hex chars (256 bits of entropy). */ token: string; /** Scopes granted to this token. Phase 1 = always `["admin"]`. */ scopes: string[]; /** Element visibility restriction. Phase 1 = always `null`. */ elementBoundaries: ElementBoundary | null; /** Tenant identifier for multi-tenant deployments. Phase 1 = always `null`. */ tenant: string | null; /** Where this token is used. Phase 1 = always `"local"`. */ platform: string; /** Opaque metadata for enterprise tooling. Phase 1 = always `{}`. */ labels: Record; /** ISO timestamp of token creation. */ createdAt: string; /** ISO timestamp of most recent use, or null if never used. */ lastUsedAt: string | null; /** How this token was created — "initial-setup", "pairing", "rotation", etc. */ createdVia: string; } /** * TOTP enrollment state. * * Populated by Phase 2 (#1794). The `secret` field holds the base32-encoded * RFC 6238 secret in plaintext — the surrounding file is 0600, which matches * standard file-based TOTP storage practice (SSH keys, age identity files, * 1Password vault backups). Encrypting the secret at rest with an OS keychain * is a future enhancement. * * Backup codes are stored as **sha256 hex hashes**, never plaintext. The * plaintext codes are shown to the user exactly once at enrollment confirm * time. Each backup code is single-use: consuming one removes its hash from * this array. * * `enrolledAt` is optional for backward compatibility with Phase 1 files, * which were written with only `{enrolled, secret, backupCodes}`. */ export interface TotpState { enrolled: boolean; secret: string | null; backupCodes: string[]; enrolledAt?: string | null; } /** * Public-safe view of TOTP state — never leaks secret material. * Returned from the status endpoint and from store methods that need to * report enrollment state to callers. */ export interface TotpStatus { enrolled: boolean; enrolledAt: string | null; backupCodesRemaining: number; } /** * Result of `beginTotpEnrollment` — the caller needs all of these to show a * QR code and manual-entry fallback in the UI. None of this is persisted; * the pending state only lives in-memory until confirmed. */ export interface TotpEnrollmentBegin { pendingId: string; /** Base32-encoded TOTP secret for manual entry (grouped display is caller's job). */ secret: string; /** Full `otpauth://` URI for authenticator apps to import. */ otpauthUri: string; /** Timestamp (ms since epoch) when this pending enrollment expires. */ expiresAt: number; } /** * Result of `confirmTotpEnrollment` — the plaintext backup codes are * returned to the caller exactly once, at this point, and then only their * hashes are retained. The caller must display them to the user immediately * and never log them. */ export interface TotpEnrollmentConfirm { backupCodes: string[]; enrolledAt: string; } /** * Result of `rotatePrimary` — returned to the caller so they can update * the browser token in-place and display the grace-window deadline. */ export interface RotationResult { /** The new 64-hex token value. The caller must treat this as a secret. */ token: string; /** ISO timestamp of the rotation. */ rotatedAt: string; /** ms-since-epoch deadline after which the old token stops verifying. */ graceUntil: number; } /** * Typed error for TOTP store operations. Carries a machine-readable * `code` field so the HTTP layer can map failures to consistent response * codes without parsing free-form error messages. Callers (CLI, UI) can * branch on `code` instead of the human-readable `message`. */ export declare class TotpError extends Error { readonly code: TotpErrorCode; constructor(message: string, code: TotpErrorCode); } /** Discriminated set of TOTP failure reasons surfaced by the store. */ export type TotpErrorCode = 'ALREADY_ENROLLED' | 'NOT_ENROLLED' | 'PENDING_NOT_FOUND' | 'INVALID_TOTP_CODE' | 'STORE_NOT_INITIALIZED' | 'TOO_MANY_PENDING' | 'TOTP_REQUIRED'; /** * The full on-disk token file structure. */ export interface ConsoleTokenFile { version: typeof TOKEN_FILE_VERSION; tokens: ConsoleTokenEntry[]; totp: TotpState; } /** * A safe-to-log view of a token entry — the secret `token` field is * replaced with a masked preview so it never appears in logs or API responses. */ export interface MaskedTokenEntry extends Omit { tokenPreview: string; } /** * Stateful store that owns the console token file and verifies presented tokens. * * Designed to live on the leader process. Followers should not construct this — * they read the file directly via `readTokenFileRaw()` for their own HTTP calls. */ export declare class ConsoleTokenStore { private readonly filePath; private data; /** * Pre-converted Buffer cache keyed by entry id. Populated whenever `this.data` * is assigned (load, create, future rotation). Verify() reuses the stored * buffers so the hot path doesn't re-allocate per-token on every request. * Negligible win with 1 token today; meaningful with Phase 2 multi-token * lookups. Not serialized — buffers are never written to disk. */ private readonly tokenBuffers; /** * In-memory pending TOTP enrollments, keyed by opaque pendingId. Nothing * lives on disk until confirmTotpEnrollment() succeeds, which limits the * window in which a half-completed enrollment leaks a secret via file read. * Entries expire after TOTP_PENDING_TTL_MS (#1794). */ private readonly pendingEnrollments; /** * In-memory grace buffer for recently-rotated tokens (#1795). After a * rotation, the old token value is stashed here with a per-rotation expiry * so in-flight requests that were sent before the rotation response arrived * still authenticate. Entries are per-rotation (concurrent rotations each * get their own grace slot) and never persisted to disk. */ private readonly graceEntries; constructor(filePath?: string); /** * Rebuild the token buffer cache after a data load, create, or mutation. * Keeps the hot verify() path allocation-free for the stored side. */ private rebuildTokenBuffers; /** * Read the existing token file, or create a new one with a single initial * token if none exists. Idempotent — safe to call on every leader election. * * @param puppetName - A puppet name picked by the caller (e.g. from SessionNames) * used to build the default display name on first run. * @returns The primary (first) token entry — convenient for server startup * to inject into HTML and stamp on followers. */ ensureInitialized(puppetName: string): Promise; /** * Verify a presented Bearer token against the stored entries. * Uses timing-safe comparison to prevent side-channel attacks. * * Updates `lastUsedAt` on the matched entry (in memory only; disk write * is debounced to avoid disk thrash on every request — Phase 2 feature). * * @returns The matching entry, or null if no match. */ verify(presented: string): ConsoleTokenEntry | null; /** * Get the primary token value for injection into HTML or forwarder config. * Returns the first entry's token string, or null if uninitialized. */ getPrimaryTokenValue(): string | null; /** * Return all tokens with the secret value masked — safe to serialize for * the Security tab UI or `GET /api/console/token/info` responses. */ listMasked(): MaskedTokenEntry[]; /** * Get the path to the token file on disk. */ getFilePath(): string; /** * Returns a safe-to-serialize view of TOTP enrollment state. Never leaks * the secret or any backup code material. */ getTotpStatus(): TotpStatus; /** Convenience: true if the user has a confirmed TOTP secret. */ isTotpEnrolled(): boolean; /** * Begin a TOTP enrollment. Generates a fresh secret, holds it in the * in-memory pending map, and returns the data the UI needs to render a * QR code and manual-entry fallback. Nothing is persisted until * `confirmTotpEnrollment` succeeds. * * Callers may call this multiple times; each call produces a new pendingId * and secret. Old pending entries expire after TOTP_PENDING_TTL_MS. * * @throws Error if TOTP is already enrolled — callers must disable first. */ beginTotpEnrollment(label?: string): TotpEnrollmentBegin; /** * Confirm a pending TOTP enrollment. Verifies the code against the pending * secret; on success, generates 10 plaintext backup codes, hashes them for * storage, and persists the enrollment. Returns the plaintext backup codes * exactly once — the caller is responsible for showing them to the user * and then discarding them. * * Wrong codes do NOT consume or invalidate the pending enrollment — the * user can retry until it expires. This matches user expectations for * "oops, typed the wrong code" and limits the damage from a fat-fingered * first attempt. * * @throws Error if pendingId is unknown, expired, or code invalid. */ confirmTotpEnrollment(pendingId: string, code: string): Promise; /** * Verify a user-presented code. Accepts either a live TOTP code or a * single-use backup code. On successful backup-code match, the consumed * code's hash is removed from storage and the file is re-written. * * Returns a discriminated result so the caller can distinguish "consumed * a backup code" (which the UI should surface with a warning about * remaining count) from "valid TOTP code" (normal case). Returns * `{ ok: false }` on any failure — the caller should not retry within * the same request lifecycle. */ verifyTotp(code: string): Promise<{ ok: true; method: 'totp' | 'backup'; backupCodesRemaining: number; } | { ok: false; }>; /** * Disable TOTP. Requires a valid code (TOTP or backup) as confirmation so * an attacker who momentarily has access to a live session can't silently * strip the second factor. * * @throws Error if not enrolled or code invalid. */ disableTotp(code: string): Promise; /** Remove any pending enrollments whose TTL has passed. */ private sweepExpiredEnrollments; /** * Rotate the primary console token. Requires TOTP confirmation (Pattern B). * * Flow: * 1. Verify the confirmation code via `verifyTotp()` (live TOTP or backup code). * 2. Stash the old token value in the in-memory grace buffer so in-flight * requests from the rotating tab (and any other local process that read * the file before the rotation) still authenticate for ROTATION_GRACE_MS. * 3. Generate a new 32-byte token, mutate the primary entry in place, write * the file atomically, and rebuild the buffer cache. * 4. Return the new token inline so the caller can update `DollhouseAuth.token` * without a page reload. * * Pattern A (OS dialog fallback for users without TOTP) is deferred — the * caller should gate the UI so the rotate action is only available when * TOTP is enrolled. * * @throws TotpError STORE_NOT_INITIALIZED — store never loaded * @throws TotpError TOTP_REQUIRED — TOTP not enrolled, rotation requires second-factor confirmation * @throws TotpError INVALID_TOTP_CODE — wrong code */ rotatePrimary(confirmationCode: string): Promise; /** Remove expired grace entries so they stop matching in verify(). */ private sweepExpiredGraceEntries; /** * Read the token file and distinguish missing from corrupt. * * Returning a tagged union lets `ensureInitialized()` back up corrupt files * before overwriting them — users who hand-edited their tokens with custom * names or labels deserve a recovery path instead of a silent destroy. */ private readWithStatus; /** * Copy the current (presumed corrupt) token file to a timestamped backup * alongside it so the user can recover hand-edited data after an accidental * syntax error. Best-effort — failure to back up does not block creating * a fresh file, since the primary goal is keeping the console usable. */ private backupCorruptFile; /** * Atomically write the token file with owner-only permissions. * Uses temp+rename to avoid partial writes on crash. * * On Windows, `chmod(0o600)` is effectively a no-op because the file * system uses ACLs instead of POSIX modes. We log a one-time warning so * users on Windows know the token file does not have OS-enforced access * control and can decide whether to use additional tooling (icacls, NTFS * permissions, or a different storage location). */ private write; /** One-shot flag so the Windows permissions warning is logged at most once. */ private windowsWarningLogged; private warnIfWindowsPermissions; } /** * Read the raw token file from disk without constructing a store. * Intended for follower processes that need the primary token to attach * to their ingest POSTs. Returns null if the file does not exist or is invalid. * * @param filePath - Optional override for the token file location */ export declare function readTokenFileRaw(filePath?: string): Promise; /** * Get the primary token value from the token file on disk. * Convenience helper for followers and external consumers. */ export declare function getPrimaryTokenFromFile(filePath?: string): Promise; /** Export the default file path so callers can reference it in logs/docs. */ export { DEFAULT_TOKEN_FILE }; //# sourceMappingURL=consoleToken.d.ts.map