|
1 | | -# Issue #51 — docs(ci): close docs-drift gap with PR-wide build, version-pin guard, and src-citation verifier |
| 1 | +# Implement — issue #52: pino logger redact paths + err scrubbing |
| 2 | + |
| 3 | +Closes #52. |
2 | 4 |
|
3 | 5 | ## Summary |
4 | 6 |
|
5 | | -Closes the silent docs-drift gap in `.github/workflows/docs.yml` by removing |
6 | | -the `pull_request:` `paths:` filter (so every PR runs `mkdocs build --strict`, |
7 | | -not just doc-touching PRs), and adds two project-specific CI gates that |
8 | | -`mkdocs --strict` cannot do on its own — a Bun-version pin verifier and a |
9 | | -`src/<path>:<line>` citation verifier. Also refreshes the surface-to-page |
10 | | -map in `CLAUDE.md` for the post-reorg `docs/` layout and documents the new |
11 | | -gates. Closes #51. |
| 7 | +Adds path-based redaction and a composed `err` serializer to the root |
| 8 | +pino instance at `src/logger.ts:174` so every log line — across every |
| 9 | +child logger and every call site — passes through a single chokepoint |
| 10 | +that scrubs: |
| 11 | + |
| 12 | +- Named credential fields (`authorization`, `x-hub-signature-256`, |
| 13 | + `privateKey`, `webhookSecret`, `installationToken`, |
| 14 | + `claudeCodeOauthToken`, `daemonAuthToken`, `awsBearerTokenBedrock`, |
| 15 | + `awsSecretAccessKey`, `awsSessionToken`, `anthropicApiKey`, `token`, |
| 16 | + `*.password`, plus the `headers.*` / `*.headers.*` / |
| 17 | + `req.headers.*` / `request.headers.*` / `response.data.token` shapes |
| 18 | + for the same fields). |
| 19 | +- Free-text leakage in error `message`, `stack`, `request.headers.*`, |
| 20 | + and `response.data.*`. The serializer reuses the already-tested |
| 21 | + `redactGitHubTokens` regex (`src/utils/sanitize.ts:77-89`) and adds |
| 22 | + an inline `scheme://user:pass@host` scrubber that mirrors |
| 23 | + `redactValkeyUrl` (`src/orchestrator/valkey.ts:62-73`). |
| 24 | + |
| 25 | +The serializer operates on a copy, so the original `Error` instance is |
| 26 | +never mutated. No new npm dependencies, no env var changes, no edits |
| 27 | +to the ~27 callers of `logger.*`. |
12 | 28 |
|
13 | 29 | ## Files changed (path · one-line rationale) |
14 | 30 |
|
15 | | -- `.github/workflows/docs.yml` · drop `paths:` from `pull_request:` so |
16 | | - code-side changes that invalidate doc facts still trip the docs job; |
17 | | - add `oven-sh/setup-bun@v2` plus two `Verify…` steps before |
18 | | - `mkdocs build --strict`. |
19 | | -- `scripts/check-docs-versions.ts` · new Bun script. Reads canonical Bun |
20 | | - version from `.tool-versions`, asserts `package.json` `engines.bun` / |
21 | | - `packageManager` and `Dockerfile.{orchestrator,daemon}` |
22 | | - `FROM oven/bun:<ver>` lines agree, then scans every `docs/**/*.md` for |
23 | | - `oven/bun:<ver>` and for loose Bun semvers (only inside lines that |
24 | | - mention the word `bun`, to avoid false matches on Node / openssl pins) |
25 | | - and fails on disagreement. |
26 | | -- `scripts/check-docs-citations.ts` · new Bun script. Walks |
27 | | - `docs/**/*.md`, extracts every `src/<path>.<ext>:<line>` (or |
28 | | - `:<start>-<end>`) citation, and verifies the file exists and the cited |
29 | | - line / range is in bounds. Bare `src/foo.ts` references without a |
30 | | - `:line` suffix are intentionally out of scope — they don't claim a |
31 | | - line and can't go stale on a shift. |
32 | | -- `package.json` · adds `check:docs-versions` and `check:docs-citations` |
33 | | - scripts and chains both into the unified `check` script. |
34 | | -- `CLAUDE.md` · refreshes the `Documentation` surface-to-page map for |
35 | | - the audience-first `docs/` layout (`operate/`, `build/`, `use/`) and |
36 | | - documents the two new CI-enforced gates. |
37 | | -- `IMPLEMENT.md` · this artifact (replaces the prior run's tracking |
38 | | - comment body). |
| 31 | +- `src/logger.ts` · Adds `REDACT_PATHS` + composed `errSerializer`, |
| 32 | + wires both into the root pino instance, exports them so tests can |
| 33 | + rebuild the same config against a captured destination. |
| 34 | +- `test/utils/logger.test.ts` · New file — eight unit tests covering |
| 35 | + every leak vector from the issue: App JWT in |
| 36 | + `err.request.headers.authorization`, `ghs_…` in `err.message` and |
| 37 | + `err.stack`, top-level `privateKey` field, `x-hub-signature-256` |
| 38 | + header, Valkey URL credentials in `err.message`, non-mutation of |
| 39 | + the original Error, `response.data.token` redaction, and the |
| 40 | + non-error pass-through branch. |
| 41 | +- `docs/operate/observability.md` · New "Log redaction" section |
| 42 | + documenting the two-layer approach (paths + err serializer), |
| 43 | + citing `src/logger.ts:17` and `src/logger.ts:113`, and noting that |
| 44 | + `redactGitHubTokens` / `redactValkeyUrl` remain in place for their |
| 45 | + non-log call sites. |
39 | 46 |
|
40 | 47 | ## Commits (sha · subject) |
41 | 48 |
|
42 | | -- `<sha>` · ci(docs): run docs build on every PR + add version-pin and |
43 | | - src-citation guards |
| 49 | +- `36a23bf` · fix(logger): redact paths and scrub err.\* before pino emits |
| 50 | +- `854c8df` · test(logger): cover redact paths and err serializer scrubbing |
| 51 | +- `8e13042` · docs(observability): document the logger as canonical redaction chokepoint |
44 | 52 |
|
45 | 53 | ## Tests run (command · result) |
46 | 54 |
|
47 | | -- `bun run typecheck` · pass (clean). |
48 | | -- `bun run lint` · pass — 0 errors, 261 pre-existing warnings unchanged |
49 | | - (none in the two new scripts). |
50 | | -- `bun run format` · pass after `bun run format:fix` normalised |
51 | | - `scripts/check-docs-citations.ts`. |
52 | | -- `bun run check:no-destructive` · pass. |
53 | | -- `bun run check:docs-versions` · pass on clean tree |
54 | | - (`OK: every Bun version reference matches .tool-versions canonical 1.3.13`). |
55 | | -- `bun run check:docs-citations` · pass on clean tree |
56 | | - (`OK: every src/<path>:<line> citation in docs/ points at an in-range location`). |
57 | | -- Negative-path for `check:docs-versions`: edited `docs/operate/setup.md` |
58 | | - to say `1.3.8`; rerun exited 1 with |
59 | | - `docs/operate/setup.md:9 [...] found '1.3.8', expected '1.3.13'`. Reverted. |
60 | | -- Negative-path for `check:docs-citations`: appended |
61 | | - `src/k8s/ephemeral-daemon-spawner.ts:99999` to `docs/operate/deployment.md`; |
62 | | - rerun exited 1 with `start line 99999 out of range (file has 226 lines)`. |
63 | | - Reverted. |
| 55 | +- `bun run typecheck` · clean (0 errors) |
| 56 | +- `bun run lint` · 0 errors / 277 warnings (identical to pre-change baseline of 277) |
| 57 | +- `bun run format` · `All matched files use Prettier code style!` |
| 58 | +- `bun test test/utils/logger.test.ts` · 8 pass / 0 fail / 25 expect() calls |
| 59 | +- `bun test test/utils/sanitize.test.ts` · 30 pass / 0 fail (regression check) |
| 60 | +- `bun run scripts/check-docs-citations.ts` · OK (every `src/<path>:<line>` citation in docs is in-range) |
| 61 | +- `bun run scripts/check-docs-versions.ts` · OK (Bun version pins agree) |
| 62 | +- `mkdocs build --strict` · clean |
64 | 63 |
|
65 | | -`bun run test` and `bun run docs:build` were not run locally — the test |
66 | | -suite goes through `scripts/test-isolated.sh` and depends on |
67 | | -Postgres/Valkey docker containers, and `mkdocs build --strict` needs |
68 | | -the Python deps from `docs/requirements.txt`. Both run as part of CI on |
69 | | -this PR. |
| 64 | +The wider `bun test` suite has 186 fail / 24 errors that are |
| 65 | +infrastructure-dependent (require real Postgres + Valkey) and were |
| 66 | +present on `main` before this PR (verified by `git stash && bun test` |
| 67 | +baseline: 187 fail / 25 errors — this PR actually removes one failure |
| 68 | +and one error). |
70 | 69 |
|
71 | 70 | ## Verification |
72 | 71 |
|
73 | | -Acceptance criteria from the plan: |
74 | | - |
75 | | -1. **Docs build runs on non-doc PRs.** `pull_request:` no longer carries |
76 | | - a `paths:` filter (`.github/workflows/docs.yml`); the |
77 | | - `Deploy to GitHub Pages` step keeps its |
78 | | - `if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'` |
79 | | - guard so PR runs validate without publishing. |
80 | | -2. **Version-pin check is wired.** `scripts/check-docs-versions.ts` exits |
81 | | - 0 on a clean tree and 1 on any disagreement with `.tool-versions`. |
82 | | - Wired into `bun run check` and as a `Verify docs version pins` step |
83 | | - in `.github/workflows/docs.yml`. |
84 | | -3. **Citation check is wired.** `scripts/check-docs-citations.ts` exits |
85 | | - 0 on a clean tree and 1 on any out-of-range citation. Wired into |
86 | | - `bun run check` and as a `Verify docs src citations` step in |
87 | | - `.github/workflows/docs.yml`. |
88 | | -4. **`bun run check` includes both gates.** Updated in `package.json`. |
89 | | -5. **Workflow gate fails the job.** Both `Verify…` steps use |
90 | | - `bun run …` without `continue-on-error`, so a non-zero exit fails |
91 | | - the `Docs / build` check. |
92 | | -6. **Strict build still green.** No content changes to docs that the |
93 | | - verifier would flag; `oven-sh/setup-bun@v2` is added before the |
94 | | - Python setup so `bun` is on `PATH` for the new steps. |
| 72 | +1. **T1 — redact paths:** `src/logger.ts:17` enumerates every path the |
| 73 | + issue asked for. The list lives next to the logger so a new |
| 74 | + secret-bearing config field added in `src/config.ts` is one place |
| 75 | + to update. |
| 76 | +2. **T2 — composed err serializer:** `src/logger.ts:131` defers to |
| 77 | + `pino.stdSerializers.err` and only then runs string-scrubbers, |
| 78 | + preserving downstream tooling compatibility while catching the |
| 79 | + four-segment-deep `err.request.headers.authorization` path that |
| 80 | + pino's wildcard syntax cannot reach (pino's `*.foo.bar` only |
| 81 | + matches 3-segment paths). Verified by the |
| 82 | + `redacts request.headers.authorization carrying an App JWT` test. |
| 83 | +3. **T3 — Valkey URL credential scrubbing folded into logger:** |
| 84 | + `redactCredentialUrls` in `src/logger.ts:54` is invoked by |
| 85 | + `scrubString`, which the err serializer applies to `message`, |
| 86 | + `stack`, and string values inside `request.headers` / |
| 87 | + `response.data`. The point helper at `src/orchestrator/valkey.ts:64` |
| 88 | + stays in place for the info-log call site at |
| 89 | + `src/orchestrator/valkey.ts:33`. Verified by |
| 90 | + `scrubs Valkey URL credentials embedded in err.message`. |
| 91 | +4. **T4 — unit tests:** `test/utils/logger.test.ts` covers all five |
| 92 | + plan-mandated assertions plus the response.data.token / non-error |
| 93 | + branches. Coverage on `src/logger.ts` is 100% functions / 98.97% |
| 94 | + lines (above the 90 % per-file gate). |
| 95 | +5. **T5 — docs:** `docs/operate/observability.md` gains a "Log |
| 96 | + redaction" section that describes both layers and cross-links to |
| 97 | + the point helpers; `mkdocs build --strict` passes, citation guard |
| 98 | + passes. |
| 99 | +6. **Non-mutation:** The serializer uses object spread to produce |
| 100 | + fresh `request` / `response` objects, leaving the original `Error` |
| 101 | + instance untouched. Verified by the |
| 102 | + `does not mutate the original Error instance` test. |
95 | 103 |
|
96 | | -The four originally-cited stale facts (`SETUP.md:11` `>= 1.3.8`, |
97 | | -`DEPLOYMENT.md:30` `oven/bun:1.3.12`, `DEPLOYMENT.md:167` and |
98 | | -`DEPLOYMENT.md:215` `src/app.ts:<line>` pointers) were already re-synced |
99 | | -during the audience-first docs reorg in commit `a9c919d`, so no |
100 | | -doc-prose edits were needed in this PR — only the structural CI gates |
101 | | -that prevent them from rotting again. |
| 104 | +The optional plan item ("fold `redactValkeyUrl` into the logger |
| 105 | +config so the remaining ad-hoc call at `src/orchestrator/valkey.ts:33` |
| 106 | +stops diverging from the global policy") was implemented via the |
| 107 | +inline regex scrubber inside the err serializer rather than by |
| 108 | +importing the existing helper, to avoid introducing a |
| 109 | +`logger -> valkey -> logger` import cycle. The existing |
| 110 | +`redactValkeyUrl` call at line 33 (info path) is still wanted: it |
| 111 | +runs at startup _before_ the logger emits, on a value (`config.valkeyUrl`) |
| 112 | +that is otherwise safe to log structurally. |
0 commit comments