/** * credentials.set / credentials.delete, writing a credential through the * daemon instead of into a client's own secret store. * * ── The gap this closes ──────────────────────────────────────────────────── * * `credentials.get` has existed since the config-sharing work: a surface can * ask the daemon, over the wire, which credentials are configured and usable. * There has never been a way to SET one. Every product wrote secrets through an * in-process SecretsManager against its own disk, which works exactly as long * as the client and the daemon share a filesystem, and produces the platform's * oldest recurring failure the moment they do not: a credential pasted into a * settings modal reports success, lands in a store the daemon never reads, and * the capability it configures stays dead with no error anywhere. * * ── What a write actually does, in order ─────────────────────────────────── * * The same four steps the plaintext sweep uses, for the same reason: * * 1. Derive the secret-store name from the config path * (`daemonSecretKeyFor`, one derivation, platform-wide). * 2. Write the value into the secret store at the scope the ownership rules * resolve (`resolveSecretWriteScope`); a daemon-needed credential goes to * the daemon tier no matter who asked. * 3. Read it BACK out of the store and compare. * 4. Only then replace the config value with its * `goodvibes://secrets/goodvibes/` reference. * * If step 3 does not match, the config value is left exactly as it was and the * call fails. A config key pointing at a reference that resolves to nothing is * worse than a key that was never written: every reader treats it as a * configured-but-broken credential, and the surface that wrote it was told the * write succeeded. * * ── What never comes back ────────────────────────────────────────────────── * * The value. Not on success, not in an error, not in a log line. The response * names the config key, the secret-store key, the scope it landed in and the * reference the config now holds, everything an operator needs to verify the * write, and nothing that repeats the credential. `credentials.get` remains the * only read, and it is secret-free by construction. * * ── Auth posture ─────────────────────────────────────────────────────────── * * `access: 'admin'` and `write:config`, matching `config.set` and * `credentials.get`, a credential write is a config write whose value happens * to be secret, and it must not be reachable by a scoped-down token that was * only granted session access. Step-up rides the platform's existing rule * rather than a per-verb flag: these are MUTATING calls, so when * `relay.requireStepUpForMutations` is on, a call arriving over the relay is * refused without a fresh WebAuthn assertion by the same dispatch gate that * covers `config.set` (relay/daemon-wiring.ts). A second, verb-local step-up * mechanism would be a different control with different failure modes guarding * the same class of act. */ import type { GatewayMethodCatalog } from '../method-catalog.js'; import type { GatewayMethodHandler } from '../method-catalog-shared.js'; import { type SecretScope } from '../../config/secrets.js'; /** The narrow config surface a credential write needs. Structural, so it is testable. */ export interface CredentialWriteConfig { get(key: string): unknown; setDynamic(key: string, value: unknown): void; } /** The narrow secret surface a credential write needs. */ export interface CredentialWriteSecrets { set(key: string, value: string, options?: { scope?: SecretScope | undefined; }): Promise; get(key: string): Promise; delete(key: string, options?: { scope?: SecretScope | undefined; }): Promise; } export interface CredentialWriteDeps { readonly config: CredentialWriteConfig; readonly secrets: CredentialWriteSecrets; /** * Extra config paths this product treats as credential-bearing, beyond the * platform's declared set. Same escape hatch the sweep offers. */ readonly additionalSecretKeys?: readonly string[] | undefined; /** * Where the audit line goes. Defaults to the platform logger at info. Never * receives a value, only key names, scope and outcome. */ readonly audit?: ((entry: CredentialWriteAuditEntry) => void) | undefined; } /** One credential write or clear, as recorded. Contains no secret material. */ export interface CredentialWriteAuditEntry { readonly action: 'set' | 'delete'; readonly configKey: string; readonly secretKey: string; readonly scope: SecretScope; readonly outcome: 'stored' | 'cleared' | 'refused'; /** The authenticated principal the daemon observed, when there was one. */ readonly principalId?: string | undefined; readonly principalKind?: string | undefined; readonly surface?: string | undefined; readonly detail?: string | undefined; } /** * Store a credential for a secret-bearing config key. * * Returns key names, the resolved scope and the reference now in config, never * the value, and never a value-derived fingerprint either, which is a hash of a * short secret and therefore a way to confirm a guess. */ export declare function createCredentialSetHandler(deps: CredentialWriteDeps): GatewayMethodHandler; /** * Remove a credential: the stored secret AND the config reference pointing at * it, in that order. * * Order matters and is the reverse of the write. Clearing the config first * would leave an orphaned secret in the store that nothing points at and * nothing reaps; clearing the secret first leaves, for an instant, a reference * that resolves to nothing, which every reader already treats as "configured * but broken", the honest state for a credential mid-removal. * * `cleared` is false when there was nothing to remove. That is a miss, not an * error: asking for a credential to be gone when it already is has succeeded. */ export declare function createCredentialDeleteHandler(deps: CredentialWriteDeps): GatewayMethodHandler; /** Attach the credentials.set/.delete handlers to their descriptors. Missing descriptor is a silent no-op. */ export declare function registerCredentialWriteGatewayMethods(catalog: GatewayMethodCatalog, deps: CredentialWriteDeps): void; //# sourceMappingURL=credentials-write.d.ts.map