* fix(#2997): mask SECRET_CONFIG_KEYS in SDK config-set/get and init responses The CJS→TS port at sdk/src/query/config-mutation.ts:240,243 and config-query.ts:122,128,132 dropped the masking layer that secrets.cjs spec defines for brave_search/firecrawl/exa_search. Result: the SDK echoed plaintext API keys into machine-readable JSON output (stdout, transcripts, CI logs). Adjacent leak in init.ts:673-675 / init.cjs:728-730: the init bundle passed config.brave_search through raw, leaking the API key whenever the user had stored one. Fix: - New sdk/src/query/secrets.ts ports SECRET_CONFIG_KEYS, isSecretKey, maskSecret, maskIfSecret. Exact CJS parity (verified by 17 tests in secrets.test.ts that import secrets.cjs and compare). - config-set masks value + previousValue in response; on-disk plaintext intact (key stays usable). - config-get masks read response. --default flows through unmasked (user's own input, not stored secret). - init.ts/init.cjs mask string values only; booleans (availability flags) pass through unchanged so the typed contract is preserved. Tests: 17 in secrets.test.ts (including CJS parity), 5 in config-mutation.test.ts (#2997 block — covers on-disk-preserved, previousValue masking, short-value, unset, non-secret pass-through), 4 in config-query.test.ts. Closes #2997 * chore(#2997): add changeset fragment for PR #2999 * chore(#2997): add changeset fragment for PR #2999 * chore(#2999): drop direct CHANGELOG.md edit; release entry now lives in .changeset/ The changeset-fragment workflow (#2975) renders fragments into CHANGELOG.md at release time. Direct edits to [Unreleased] on each PR caused merge conflicts on every concurrent PR. This commit restores CHANGELOG.md to match origin/main; the release entry for this fix is preserved in the .changeset/*.md fragment(s) on this branch, which the release workflow consolidates.
44 lines
1.5 KiB
TypeScript
44 lines
1.5 KiB
TypeScript
/**
|
|
* Secrets handling — TypeScript mirror of `get-shit-done/bin/lib/secrets.cjs`.
|
|
*
|
|
* Keys considered sensitive (`SECRET_CONFIG_KEYS`) are masked in any
|
|
* machine-readable response from `config-set` / `config-get` so plaintext
|
|
* credentials don't end up in workflow output, session transcripts, or
|
|
* shell histories. The on-disk value is unchanged; only the response is masked.
|
|
*
|
|
* Behavior must match `secrets.cjs` exactly. A parity test asserts the
|
|
* two modules expose the same set of secret keys and produce identical
|
|
* masked output for representative inputs.
|
|
*
|
|
* Tracked in #2997 (security: SDK port lost masking behavior).
|
|
*/
|
|
|
|
export const SECRET_CONFIG_KEYS: ReadonlySet<string> = new Set([
|
|
'brave_search',
|
|
'firecrawl',
|
|
'exa_search',
|
|
]);
|
|
|
|
export function isSecretKey(keyPath: string): boolean {
|
|
return SECRET_CONFIG_KEYS.has(keyPath);
|
|
}
|
|
|
|
/**
|
|
* Convention: ≥8 chars → `****<last-4>`; <8 chars → `****`; null/empty/undefined → `(unset)`.
|
|
* Identical to `secrets.cjs` `maskSecret`.
|
|
*/
|
|
export function maskSecret(value: unknown): string {
|
|
if (value === null || value === undefined || value === '') return '(unset)';
|
|
const s = String(value);
|
|
if (s.length < 8) return '****';
|
|
return '****' + s.slice(-4);
|
|
}
|
|
|
|
/**
|
|
* Helper: returns the value masked if `keyPath` is a secret, else the value
|
|
* unchanged. Use at response-construction boundaries in query handlers.
|
|
*/
|
|
export function maskIfSecret<T>(keyPath: string, value: T): T | string {
|
|
return isSecretKey(keyPath) ? maskSecret(value) : value;
|
|
}
|