Skip to content

Commit 641f138

Browse files
chrisleekr-bot[bot]chrisleekr-bot[bot]chrisleekr
authored
fix(logger): redact paths and scrub err.* before pino emits (closes #52) (#89)
Co-authored-by: chrisleekr-bot[bot] <2890262+chrisleekr-bot[bot]@users.noreply.github.com> Co-authored-by: Chris Lee <chrisleekr@users.noreply.github.com> Co-authored-by: chrisleekr-bot[bot] <chrisleekr-bot[bot]@users.noreply.github.com>
1 parent cc70949 commit 641f138

4 files changed

Lines changed: 569 additions & 86 deletions

File tree

IMPLEMENT.md

Lines changed: 95 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,112 @@
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.
24

35
## Summary
46

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.*`.
1228

1329
## Files changed (path · one-line rationale)
1430

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.
3946

4047
## Commits (sha · subject)
4148

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
4452

4553
## Tests run (command · result)
4654

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
6463

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).
7069

7170
## Verification
7271

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.
95103

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.

docs/operate/observability.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@
22

33
Structured JSON logs via [pino](https://getpino.io) are the primary signal. Every dispatch decision and every pipeline step carries a `deliveryId` so you can reconstruct a request end-to-end from a single log query. When `DATABASE_URL` is configured, the same information is persisted to `executions` and `triage_results` for aggregate reporting.
44

5+
## Log redaction
6+
7+
The exported `logger` in `src/logger.ts` is the canonical chokepoint for secret scrubbing — every child logger inherits its `redact.paths` list and its custom `err` serializer, so individual call sites do not need to remember to scrub. Two layers run on every emitted line:
8+
9+
1. **Path-based redaction** — the exported `REDACT_PATHS` constant in `src/logger.ts` lists every field pino should replace with `[Redacted]` before the JSON is serialised. Paths covered: `authorization` and its `*.authorization` / `headers.authorization` / `*.headers.authorization` / `req.headers.authorization` / `request.headers.authorization` variants; the webhook signature header `x-hub-signature-256` (also wildcard-prefixed); `response.data.token`; and the named credential fields `token`, `installationToken`, `privateKey`, `webhookSecret`, `anthropicApiKey`, `claudeCodeOauthToken`, `daemonAuthToken`, `awsSecretAccessKey`, `awsSessionToken`, `awsBearerTokenBedrock`, `*.password`. The list is `Object.freeze`d so an accidental `push` from another module cannot silently weaken the policy.
10+
11+
2. **`errSerializer` scrubbing** — the exported `errSerializer` in `src/logger.ts` defers to pino's `stdSerializers.err` and then runs the result's `message`, `stack`, `request.headers`, and `response.data` through `redactGitHubTokens` (`src/utils/sanitize.ts`) plus an inline credential-URL scrubber that mirrors `redactValkeyUrl` (`src/orchestrator/valkey.ts`). The walker recurses through nested objects/arrays and replaces any key matching the sensitive-field-name set wholesale, so `err.response.data.meta.token` and `err.request.headers.forwarded.authorization` are caught at any depth — this is necessary because pino's path-based rules cannot match four-or-more segments deep on `err.*`. It also catches `ghs_…` installation tokens and App JWTs echoed inside `err.message` / `err.stack`.
12+
13+
The serializer operates on a copy, so the original Error instance is never mutated.
14+
15+
If you add a new secret-bearing config field to `src/config.ts`, add its property name to `REDACT_PATHS` in the same PR. The point helpers `redactGitHubTokens` and `redactValkeyUrl` remain in place for their non-log call sites (prompt sanitisation and the Valkey startup info log respectively); the logger config is the system-wide default.
16+
517
## Common log fields
618

719
| Field | Meaning |

0 commit comments

Comments
 (0)