The changes-tab op previews replay read/write/edit payloads into the DOM (diff rows, read rows, markdown reading mode, error text) — when the model reads a credentials file, that plaintext must not land on screen or in inspectable DOM. New display-only redaction layer, on by default with a one-click pane toggle (persisted in localStorage): - path layer: credential-shaped filenames (.env / *secret* / *credential* / *token* / *api-key* / private keys...) mask whole files line-wise - content layer: ordinary files mask only secret-shaped spans — secret field assignments keep the field name (api_key: [REDACTED]), plus bare token prefixes (sk- / AKIA / ghp_ / xox / ya29.), Bearer headers, and PEM private-key block headers - redacted payloads flow through the same viewOp shape into diff rows, read rows, markdown reading mode, and error text Known edge (documented in README): an unquoted mermaid node label containing a redacted secret breaks the diagram and falls back to source; workaround is quoting the label. Session data is untouched — this is display-only and not a boundary against same-context malicious plugins. Locales: zh/en dictionaries + all 19 shipped third-language overrides (key-set-equal gate green). 9 new unit tests; typecheck clean; targeted suites 40/40 green. The fs-operations failure is pre-existing on the base branch (environment-dependent, verified via stash) and unrelated.
60 lines
2.6 KiB
TypeScript
60 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { isSensitivePath, redactContentText, redactText, REDACTED } from '../src/client/redact.js'
|
|
|
|
describe('isSensitivePath', () => {
|
|
it('marks credential-shaped filenames', () => {
|
|
expect(isSensitivePath('C:/proj/.env')).toBe(true)
|
|
expect(isSensitivePath('config/secrets.yaml')).toBe(true)
|
|
expect(isSensitivePath('~/auth/credentials.json')).toBe(true)
|
|
expect(isSensitivePath('deploy/api-key.prod.yml')).toBe(true)
|
|
expect(isSensitivePath('/home/u/.ssh/id_rsa')).toBe(true)
|
|
expect(isSensitivePath('cert/server.pem')).toBe(true)
|
|
})
|
|
it('passes ordinary filenames', () => {
|
|
expect(isSensitivePath('src/index.ts')).toBe(false)
|
|
expect(isSensitivePath('docs/readme.md')).toBe(false)
|
|
expect(isSensitivePath('package.json')).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('redactContentText', () => {
|
|
it('masks yaml secret assignments keeping the key name', () => {
|
|
const out = redactContentText('name: demo\napi_key: sk-1234567890abcdef\nport: 8080')
|
|
expect(out).toContain('name: demo')
|
|
expect(out).toContain('api_key: ' + REDACTED)
|
|
expect(out).toContain('port: 8080')
|
|
expect(out).not.toContain('sk-1234567890abcdef')
|
|
})
|
|
it('masks quoted json fields and equals-style env lines', () => {
|
|
expect(redactContentText('"password": "hunter2"')).toBe('"password": ' + REDACTED)
|
|
expect(redactContentText('API_TOKEN=abcdef1234567890')).toBe('API_TOKEN=' + REDACTED)
|
|
})
|
|
it('masks bare credential tokens in free text', () => {
|
|
const out = redactContentText('see sk-abcdefghijklmnop123456 and AKIAIOSFODNN7EXAMPLE')
|
|
expect(out).not.toContain('sk-abcdefghijklmnop123456')
|
|
expect(out).not.toContain('AKIAIOSFODNN7EXAMPLE')
|
|
expect(out).toContain(REDACTED)
|
|
})
|
|
it('masks bearer headers and PEM key headers', () => {
|
|
expect(redactContentText('Authorization: Bearer abcdef1234567890abcd')).toContain(REDACTED)
|
|
expect(redactContentText('-----BEGIN RSA PRIVATE KEY-----')).toBe(REDACTED)
|
|
})
|
|
it('leaves ordinary code untouched', () => {
|
|
const code = 'const x = 42\nconsole.log("count", x)'
|
|
expect(redactContentText(code)).toBe(code)
|
|
})
|
|
})
|
|
|
|
describe('redactText', () => {
|
|
it('masks a whole sensitive file line-wise', () => {
|
|
const out = redactText('/app/.env', 'USER=me\nTOKEN=abc\n\n# comment')
|
|
expect(out.text).toBe(REDACTED + '\n' + REDACTED + '\n\n' + REDACTED)
|
|
expect(out.hit).toBe(true)
|
|
})
|
|
it('reports hit=false for clean ordinary content', () => {
|
|
const out = redactText('src/main.ts', 'export function main() {}')
|
|
expect(out.hit).toBe(false)
|
|
expect(out.text).toBe('export function main() {}')
|
|
})
|
|
})
|