detectedLanguage: pii?.detectedLanguage,
maskedContent,
secretsDetected: secrets?.detected,
+ secretsMasked: secrets?.masked,
secretsTypes: secrets?.types,
statusCode,
errorMessage,
--- /dev/null
+import { describe, expect, test } from "bun:test";
+import { shouldLogMaskedContent } from "./log-content";
+
+describe("shouldLogMaskedContent", () => {
+ // With action "mask", maskedContent has both PII and secrets replaced by
+ // placeholders, e.g. "My key is [API_KEY_SK_1] and email [[EMAIL_ADDRESS_1]]".
+ // Storing it is safe even when secrets were detected (issue #91).
+ const maskedWithSecret = "My key is [API_KEY_SK_1] and email [[EMAIL_ADDRESS_1]]";
+ const maskedPiiOnly = "Email [[EMAIL_ADDRESS_1]]";
+
+ test("logs masked content when secrets were detected and masked", () => {
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: maskedWithSecret,
+ logMaskedContent: true,
+ secretsDetected: true,
+ secretsMasked: true,
+ }),
+ ).toBe(true);
+ });
+
+ test("logs masked content when only PII was detected", () => {
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: maskedPiiOnly,
+ logMaskedContent: true,
+ secretsDetected: false,
+ }),
+ ).toBe(true);
+ });
+
+ test("does not log when log_masked_content is false", () => {
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: maskedWithSecret,
+ logMaskedContent: false,
+ secretsDetected: true,
+ secretsMasked: true,
+ }),
+ ).toBe(false);
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: maskedPiiOnly,
+ logMaskedContent: false,
+ }),
+ ).toBe(false);
+ });
+
+ test("does not log when secrets were detected but not masked (route_local)", () => {
+ // Route mode with action "route_local" leaves secrets raw for the trusted
+ // local provider, so the content may contain actual secret material.
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: "My key is sk-live-actual-secret and email [[EMAIL_ADDRESS_1]]",
+ logMaskedContent: true,
+ secretsDetected: true,
+ secretsMasked: false,
+ }),
+ ).toBe(false);
+ });
+
+ test("does not log when there is no masked content", () => {
+ expect(
+ shouldLogMaskedContent({
+ maskedContent: undefined,
+ logMaskedContent: true,
+ }),
+ ).toBe(false);
+ });
+});
--- /dev/null
+export interface LogContentDecision {
+ maskedContent?: string;
+ logMaskedContent: boolean;
+ secretsDetected?: boolean;
+ secretsMasked?: boolean;
+}
+
+/**
+ * Decide whether masked content should be persisted to the request log.
+ *
+ * When secrets_detection.action is "mask" (the default), maskedContent has both
+ * PII and secrets replaced by placeholders (e.g. "[API_KEY_SK_1]",
+ * "[[EMAIL_ADDRESS_1]]") by the time it reaches the logger, so it is safe to
+ * store even when secrets were detected — gating follows log_masked_content.
+ *
+ * The exception is route mode with action "route_local": secrets are detected
+ * but intentionally left unmasked for the trusted local provider, so the
+ * content may contain raw secret material and must never be persisted.
+ */
+export function shouldLogMaskedContent(decision: LogContentDecision): boolean {
+ const { maskedContent, logMaskedContent, secretsDetected, secretsMasked } = decision;
+ if (!maskedContent || !logMaskedContent) return false;
+ // Detected but unmasked secrets (action: route_local) are still raw in the content
+ if (secretsDetected && !secretsMasked) return false;
+ return true;
+}
import { Database } from "bun:sqlite";
import { mkdirSync } from "node:fs";
import { getConfig } from "../config";
+import { shouldLogMaskedContent } from "./log-content";
export interface RequestLog {
id?: number;
detectedLanguage?: string;
maskedContent?: string;
secretsDetected?: boolean;
+ secretsMasked?: boolean;
secretsTypes?: string[];
statusCode?: number;
errorMessage?: string;
const config = getConfig();
const logger = getLogger();
- // Safety: Never log content if secrets were detected
- // Even if log_content is true, secrets are never logged
- const shouldLogContent = data.maskedContent && !data.secretsDetected;
+ const shouldLogContent = shouldLogMaskedContent({
+ maskedContent: data.maskedContent,
+ logMaskedContent: config.logging.log_masked_content,
+ secretsDetected: data.secretsDetected,
+ secretsMasked: data.secretsMasked,
+ });
// Only log secret types if configured to do so
const shouldLogSecretTypes =