/** * Claude token migration — retire the Claude sign-in tokens older Crewly * versions saved, without locking anyone out. * * Anthropic's Claude Code legal page: developers may not collect, store or * intermediate Claude.ai credentials or session tokens; sign-in must finish * through Anthropic's own flow. Older Crewly ran `claude setup-token`, read * the token off the screen and saved it in `harness-credentials.json` * (`claude.oauthToken` for the default login, `claudeAccounts..oauthToken` * for named accounts). Crewly now signs in with Claude Code's own * `claude auth login` and stores nothing (login-broker.service.ts). * * Rules of the transition: * * - A saved token keeps being handed to its account's Claude Code processes * until that account is signed in natively (store.legacyClaudeTokenEnv). * Nothing stops working on upgrade. * - A token is deleted only after the account's native login is verified by * a real Claude turn run WITHOUT any token or key in the env * (claude-native-login.ts). A login that only looks signed in is not enough. * - A named account's own config dir can only have been signed in by Crewly's * flow or by the owner, so a verified native login there retires its token * at once (the periodic run notices a login done by hand too). The default * login is different: the machine's own Claude login may be another * account than the one the saved token belongs to, so its token is retired * only by a sign-in done through Crewly (`login claude`), never guessed. * - The owner is told once per machine which accounts need one re-sign-in * (state file `claude-token-migration.json`, no secrets in it). * - When no saved token remains, the leftover Claude keys are removed from * `harness-credentials.json`; other keys are untouched, the file is * rewritten atomically with mode 0600, and no backup is kept. * * @module services/harness/claude-token-migration.service */ import { type HarnessCredentialsStore } from './harness-credentials.store.js'; import { type HarnessLogger, type LoginState } from './harness.types.js'; /** A login that still depends on a token an older Crewly saved. */ export interface LegacyTokenEntry { /** Named account; null = the default login */ account: string | null; } /** What one run did. */ export interface MigrationResult { /** Logins whose token was deleted this run */ retired: Array; /** Logins that still run on a saved token (need a native sign-in) */ pending: Array; /** The owner notice was sent this run */ notified: boolean; } /** Owner DM sender. */ export interface MigrationNotifier { sendToOwner(text: string): Promise; } /** Injectable dependencies. */ export interface ClaudeTokenMigrationDeps { credentials?: Pick; /** Real Claude turn on Claude Code's own login, no token in the env */ probe?: (configDir: string | null) => Promise; /** `claude auth status` on Claude Code's own login */ readStatus?: (configDir: string | null) => Promise; /** Config dir of a named account */ accountConfigDir?: (account: string) => string; notifier?: () => MigrationNotifier | null; /** Directory of the state file (default CREWLY_HOME) */ crewlyHome?: string; logger?: HarnessLogger; now?: () => number; } /** Retires saved Claude tokens once the account is signed in natively. */ export declare class ClaudeTokenMigrationService { private readonly credentials; private readonly probe; private readonly readStatus; private readonly accountConfigDir; private notifier; private readonly crewlyHome; private readonly logger; private readonly now; private running; private timer; /** * @param deps - Injectable dependencies (all optional) */ constructor(deps?: ClaudeTokenMigrationDeps); /** * Where the owner notice is sent. * * @param notifier - Lazy owner DM sender */ setNotifier(notifier: () => MigrationNotifier | null): void; /** * Logins that still depend on a saved token. * * @returns Entries (default login first) */ listLegacy(): LegacyTokenEntry[]; /** * Check every login that still has a saved token; retire what can be * retired and tell the owner about the rest, once. Safe to call at any * time and as often as you like (idempotent); concurrent calls share one run. * * @returns What happened; never rejects */ run(): Promise; /** * A sign-in done through Crewly just completed and was verified: retire * the saved token of that login (default or named). * * @param account - Named account; null = the default login * @returns Resolves when done; never rejects */ afterLogin(account: string | null): Promise; /** * Run now (after a short delay) and then on an interval. * * @param startDelayMs - Delay before the first run */ start(startDelayMs?: number): void; /** Stop the timers. */ stop(): void; private hasLegacy; private configDirOf; /** * Delete the saved token of a login when its native login passes a real * Claude turn with no token in the env. * * @param account - Named account; null = the default login * @returns True when the token was deleted */ private retireIfVerified; /** Remove leftover Claude keys once no saved token remains. */ private finishCleanup; private runOnce; private statePath; private readState; private writeState; /** * Tell the owner, once per machine, which logins need one re-sign-in. * * @param pending - Logins that still run on a saved token * @returns True when the notice was delivered now */ private notifyOnce; } /** * The backend's token migration, bound to the stored credentials. * * @returns The singleton */ export declare function getClaudeTokenMigrationService(): ClaudeTokenMigrationService; /** * Replace or clear the singleton (tests). * * @param service - Service, or null */ export declare function setClaudeTokenMigrationServiceForTesting(service: ClaudeTokenMigrationService | null): void; //# sourceMappingURL=claude-token-migration.service.d.ts.map