diff --git a/src/web/sensitive-path.ts b/src/web/sensitive-path.ts index bb20c37f..29caede8 100644 --- a/src/web/sensitive-path.ts +++ b/src/web/sensitive-path.ts @@ -80,12 +80,23 @@ const SENSITIVE_PATTERNS: RegExp[] = [ /\/\.claude\/\.credentials\.json$/, /\/\.codeman[^/]*\/hook-secret$/, /\/\.codeman[^/]*\/users\.json$/, - // Codeman's own state file. Named once `.json` became previewable outside the - // workspace: `SessionState.envOverrides` persists whatever the user set for a - // session, and the env allowlist admits key-shaped names (`GEMINI_API_KEY`, - // `CLAUDE_CODE_*`), so this file can hold a live credential. Same reasoning - // as the two entries above, and it leaves the rest of ~/.codeman attachable. - /\/\.codeman[^/]*\/state\.json$/, + // Codeman's own state files. Named once `.json` became previewable outside + // the workspace: `SessionState.envOverrides` persists whatever the user set + // for a session, and the env allowlist admits key-shaped names + // (`GEMINI_API_KEY`, `CLAUDE_CODE_*`), so state can hold a live credential. + // `state[^/]*` rather than `state`: siblings like state-inner.json carry the + // same payload. Same reasoning as the two entries above, and it leaves the + // rest of ~/.codeman attachable. + /\/\.codeman[^/]*\/state[^/]*\.json$/, + // settings.json holds a credential BY SCHEMA (`voiceSettings.apiKey`, the + // Deepgram key); push-keys.json holds the VAPID PRIVATE key (enough to forge + // push notifications to every subscribed device); intents.json is written + // 0600 precisely because captured prompts can contain secrets, and is + // deliberately kept out of /api/search — it must not be readable through a + // different route instead. + /\/\.codeman[^/]*\/settings\.json$/, + /\/\.codeman[^/]*\/push-keys\.json$/, + /\/\.codeman[^/]*\/intents\.json$/, ]; /** diff --git a/test/response-viewer-file-links.test.ts b/test/response-viewer-file-links.test.ts index 07835e45..6caababf 100644 --- a/test/response-viewer-file-links.test.ts +++ b/test/response-viewer-file-links.test.ts @@ -100,11 +100,14 @@ describe('response viewer file-path linkifier', () => { it('never re-cuts text already inside an anchor', () => { // marked autolinks URLs; a path-looking tail inside one must stay whole, and // a nested is invalid markup that would swallow the outer link's click. - const root = linkify('

https://example.com/x/y.png

'); + // ⚠️ The URL's tail MUST be a string the pattern matches on its own + // (`/tmp/...` here): with an unmatchable tail this test passes with the + // inside-anchor guard deleted, i.e. it pins nothing. + const root = linkify('

https://example.com/tmp/shot.png

'); expect(paths(root)).toHaveLength(0); expect(root.querySelectorAll('a')).toHaveLength(1); - expect(root.querySelector('a')!.getAttribute('href')).toBe('https://example.com/x/y.png'); + expect(root.querySelector('a')!.getAttribute('href')).toBe('https://example.com/tmp/shot.png'); }); it('leaves text with no path untouched', () => { diff --git a/test/sensitive-path.test.ts b/test/sensitive-path.test.ts index 9b5013cb..79329925 100644 --- a/test/sensitive-path.test.ts +++ b/test/sensitive-path.test.ts @@ -79,6 +79,14 @@ describe('isSensitivePath', () => { // workspace. ['codeman state file', `${HOME}/.codeman/state.json`], ['codeman state file on a named instance', `${HOME}/.codeman-beta/state.json`], + ['codeman state sibling (same payload)', `${HOME}/.codeman/state-inner.json`], + // settings.json holds voiceSettings.apiKey by schema; push-keys.json holds + // the VAPID PRIVATE key; intents.json is 0600 because captured prompts can + // contain secrets and is deliberately kept out of /api/search. + ['codeman settings (Deepgram key)', `${HOME}/.codeman/settings.json`], + ['codeman push keys (VAPID private)', `${HOME}/.codeman/push-keys.json`], + ['codeman intent profiles', `${HOME}/.codeman/intents.json`], + ['codeman intents on a named instance', `${HOME}/.codeman-beta/intents.json`], ]; it.each(blocked)('blocks the %s', (_label, path) => {