/** * plaintext-credential-sweep.ts, getting credentials out of config files. * * The sibling migration moves a credential that is in the wrong STORE. This one * handles the credential that is not in a store at all: a literal password * sitting in a settings JSON file, in the clear, because the path that wrote it * had no idea the key was a credential. * * Three routes produced these, all now closed at the write end: * * - the settings modal, for any key missing from the secret-key set, * `surfaces.email.password` and `surfaces.calendar.caldavPassword` were * both missing, and both have schema descriptions reading "Stored in the * daemon secret tier, never in config"; * - the generic `/config set `, which had no detection at all; * - the web UI's settings editor, which wrote every value through one * untyped `config.set`. * * Closing the write end leaves every value already written exactly where it is. * So this sweeps them: the literal moves into the encrypted store and the * config file keeps a `goodvibes://secrets/…` reference, which is what every * reader already knows how to resolve. * * ── The ordering, same as the credential migration ────────────────────────── * * 1. Read the literal out of config. * 2. Write it to the secret store, at whatever scope the ownership rules say. * 3. Read it BACK from the store and compare. * 4. Only then replace the config value with the reference. * * If step 3 does not match, the config value is left exactly as it was. A * credential that is readable in the clear still works; a credential replaced * by a reference that resolves to nothing does not, and that would be this * sweep breaking the very thing it exists to protect. * * Values never appear in a result, a log line or an error. */ import type { SecretScope, SecretStorageMedium } from './secrets.js'; /** What happened to one config key. */ export type PlaintextSweepOutcome = /** The literal moved into the store and the config now holds a reference. */ 'moved' /** The store already held it; the config now holds a reference. */ | 'already-stored' /** The store write, or its read-back, failed. The literal was left in place. */ | 'left-in-place'; export interface PlaintextSweepEntry { readonly configKey: string; readonly secretKey: string; readonly outcome: PlaintextSweepOutcome; readonly detail?: string | undefined; } export interface PlaintextSweepReport { readonly entries: readonly PlaintextSweepEntry[]; readonly moved: number; readonly failed: number; readonly noop: boolean; } /** The narrow config surface this needs. Structural, so it is testable. */ export interface SweepableConfig { get(key: string): unknown; set(key: string, value: unknown): void; } /** The narrow secret surface this needs. */ export interface SweepableSecrets { set(key: string, value: string, options?: { scope?: SecretScope; medium?: SecretStorageMedium; }): Promise; getFromScope(key: string, scope: SecretScope): Promise; get(key: string): Promise; } /** * The reference a config key holds once its value lives in the store. * * The provider segment is NOT decoration. `goodvibes://secrets/` does not * parse, the parser reads the first path segment as the provider name, so a * key there resolves to no known provider and `normalizeSecretRef` returns * null. Combined with the old passthrough in `resolveSecretInput`, this sweep * would have replaced a working plaintext password with a reference that * resolved to its own text, and put that text on the wire as the credential. * * The canonical form is the one channel account setup already emits * (channels/builtin/account-actions.ts): provider segment, then the key, * percent-encoded so a key containing a slash cannot invent a path segment. */ export declare function secretReferenceFor(secretKey: string): string; /** * Move every credential still sitting literally in a config file into the * secret store. * * Safe on every start. After the first run every declared key holds a reference * or nothing, and the sweep is a handful of config reads. */ export declare function sweepPlaintextCredentials(config: SweepableConfig, secrets: SweepableSecrets, /** Extra keys a product knows about that the platform set does not name. */ additionalKeys?: readonly string[], /** * Record the minimum reader version this rewrite requires, in the settings * file the rewrite landed in. * * This sweep rewrites SHARED state, `~/.goodvibes/daemon/settings.json` is * read by every component on the machine, and they are not all the same * version at the same moment. A `goodvibes://secrets/…` reference written * onto `calendar.google.clientSecretRef` is a form an older reader could not * walk, and the older reader failed on the KEY rather than reporting the * version gap that actually caused it. With a floor recorded, that reader * says the one true thing instead: the file was migrated by something newer * than it is. * * Injected rather than done here so the sweep keeps its narrow, testable * config surface and no knowledge of where the file lives. */ recordReaderFloor?: ((minReaderVersion: string, setBy: string) => void) | undefined): Promise; /** A one-line, safe-to-display summary. Never contains a value. */ export declare function describePlaintextSweep(report: PlaintextSweepReport): string; //# sourceMappingURL=plaintext-credential-sweep.d.ts.map