File size: 2,283 Bytes
34810d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * Canonical header redaction for debug proxy captures.
 *
 * Both capture writers — the patched-fetch runtime and the standalone proxy
 * server — must redact identically. A capture that leaks credentials is worse
 * than no capture, and the standalone path previously stored raw headers while
 * the runtime path redacted, so this policy lives in one leaf module that both
 * import rather than being duplicated per writer.
 */
import { isHeadersLike, type HeadersLike } from "../infra/fetch-headers.js";
import { redactRegisteredSecretValues } from "../logging/secret-redaction-registry.js";
import { isLikelySensitiveModelProviderHeaderName } from "../secrets/model-provider-header-policy.js";

export const REDACTED_CAPTURE_HEADER_VALUE = "[REDACTED]";

function isSensitiveCaptureHeaderName(name: string): boolean {
  const normalized = name.trim().toLowerCase();
  return (
    isLikelySensitiveModelProviderHeaderName(normalized) ||
    normalized === "cookie" ||
    normalized === "set-cookie" ||
    normalized.includes("session")
  );
}

export function redactedCaptureHeaders(
  headers: HeadersLike | Record<string, string | string[] | undefined> | undefined,
  additionalSensitiveNames?: Iterable<string>,
): Record<string, string> | undefined {
  if (!headers) {
    return undefined;
  }
  const additionalSensitive = new Set(
    [...(additionalSensitiveNames ?? [])].map((name) => name.trim().toLowerCase()),
  );
  const entries = isHeadersLike(headers) ? Array.from(headers.entries()) : Object.entries(headers);
  const redacted: Record<string, string> = {};
  for (const [name, value] of entries) {
    // Header names are matched exactly and by sensitive fragments because
    // providers use many token/key naming variants. Names that pass the check
    // still run through value redaction so a registered secret pasted into an
    // innocuous header does not survive.
    if (additionalSensitive.has(name.trim().toLowerCase()) || isSensitiveCaptureHeaderName(name)) {
      redacted[name] = REDACTED_CAPTURE_HEADER_VALUE;
      continue;
    }
    const flattened = Array.isArray(value) ? value.join(", ") : (value ?? "");
    redacted[name] = redactRegisteredSecretValues(flattened, () => REDACTED_CAPTURE_HEADER_VALUE);
  }
  return redacted;
}