Reference solution
The findings below are ordered by priority.
Findings
1. Common sensitive keys and case variants are emitted
Location: src/audit-redaction.ts:1-8
The list omits cookie, set-cookie, and secret, and comparison is case-sensitive. Values under keys such as Authorization, PASSWORD, or Cookie therefore pass directly into the audit line even though the contract classifies them as credentials.
Canonicalize each complete key for comparison and use the full policy set. Preserve exact-key matching so permitted names such as tokenCount are not over-redacted.
2. Redaction stops at the first object level
Location: src/audit-redaction.ts:3-12
Only entries directly on metadata are inspected. Credentials inside nested request objects or objects stored in arrays remain unchanged and are serialized by the writer.
Walk arrays and plain objects recursively, applying the same complete-key policy at every object depth while preserving non-sensitive structure.
3. Error messages and stacks can disclose request data
Location: src/audit-writer.ts:26-37
errorDetails explicitly adds message and stack. Validation messages, downstream URLs, and stack-attached context commonly include user input or credentials, so a failed administrative action can leak data even if metadata redaction were complete.
Emit only the safe error name and an allowlisted application code. Exclude message, stack, cause, and arbitrary custom properties.
4. Sanitizing an event changes caller-owned metadata
Location: src/audit-redaction.ts:3-12 and src/audit-writer.ts:11-12
redactMetadata replaces fields on the original object and returns that same reference. Code that reuses the metadata after audit emission sees redaction placeholders instead of its original values, while nested mutation would make the problem broader once recursion is added.
Build a sanitized copy during traversal. Use reference tracking to preserve repeated references safely without assigning into caller-owned objects or arrays.
5. Circular metadata prevents the audit event from being written
Location: src/audit-writer.ts:14-23
The payload retains nested object references and is passed directly to JSON.stringify. A framework object that points back to itself makes serialization throw, dropping the audit record and potentially replacing the operation's actual outcome with a logging failure.
Track objects by identity during the non-mutating traversal and replace only back-edges with [Circular] before serialization. Keep other branches intact and still let genuine sink transport failures follow the existing contract.
Reasonable non-findings
- Approved actor and target identifiers are intentionally present at the top level; they make the audit record attributable.
- Complete-key matching should not redact
tokenCount or secretaryName merely because they contain sensitive words.
- Awaiting the sink is acceptable because the scenario explicitly excludes transport reliability and latency policy.