{"version":3,"file":"redact.mjs","names":[],"sources":["../../../../../../../ai/src/security/redact.ts"],"sourcesContent":["/**\n * Default set of sensitive key fragments (matched case-insensitively as\n * substrings of an object key). Covers the secrets that leak through\n * recorded requests, error causes, and trace payloads: auth headers, API\n * keys, cookies, tokens, passwords, and private keys.\n */\nexport const DEFAULT_SENSITIVE_KEYS: readonly string[] = [\n  \"authorization\",\n  \"x-api-key\",\n  \"api-key\",\n  \"apikey\",\n  \"cookie\",\n  \"set-cookie\",\n  \"password\",\n  \"passwd\",\n  \"secret\",\n  \"token\",\n  \"access_token\",\n  \"refresh_token\",\n  \"client_secret\",\n  \"private_key\",\n  \"session\",\n];\n\n/** HTTP header names always stripped from a serialized error/cause. */\nexport const SENSITIVE_HEADERS: readonly string[] = [\n  \"authorization\",\n  \"x-api-key\",\n  \"api-key\",\n  \"cookie\",\n  \"set-cookie\",\n  \"proxy-authorization\",\n];\n\nexport type RedactOptions = {\n  /** Extra key fragments to redact, merged with {@link DEFAULT_SENSITIVE_KEYS}. */\n  keys?: string[];\n  /** Replacement for a redacted value. Default `\"[redacted]\"`. */\n  placeholder?: string;\n  /** Maximum recursion depth before bailing out. Default `8`. */\n  maxDepth?: number;\n};\n\nconst DEFAULT_PLACEHOLDER = \"[redacted]\";\nconst DEFAULT_MAX_DEPTH = 8;\n\nfunction keyIsSensitive(key: string, fragments: string[]): boolean {\n  const lower = key.toLowerCase();\n  return fragments.some(fragment => lower.includes(fragment));\n}\n\n/**\n * Deep-copy `value` with any property whose KEY matches a sensitive\n * fragment replaced by the placeholder. Arrays are walked element-wise;\n * circular references and over-deep trees collapse to the placeholder.\n * Primitives pass through untouched (redaction is key-driven, not\n * value-driven — it never guesses at a bare string being a secret).\n *\n * Shared by VCR cassettes (S2), Panoptic content capture, and the error /\n * cause serializer (S4) so there is ONE redaction policy, not three.\n */\nexport function redact<T>(value: T, options: RedactOptions = {}): T {\n  const fragments = [...DEFAULT_SENSITIVE_KEYS, ...(options.keys ?? [])].map(k =>\n    k.toLowerCase(),\n  );\n  const placeholder = options.placeholder ?? DEFAULT_PLACEHOLDER;\n  const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;\n  const seen = new WeakSet<object>();\n\n  const walk = (input: unknown, depth: number): unknown => {\n    if (input === null || typeof input !== \"object\") {\n      return input;\n    }\n    if (depth >= maxDepth || seen.has(input)) {\n      return placeholder;\n    }\n    seen.add(input as object);\n\n    if (Array.isArray(input)) {\n      return input.map(item => walk(item, depth + 1));\n    }\n\n    // `name` / `message` / `stack` sit on Error's prototype chain (or as\n    // non-enumerable own properties), so a plain `Object.entries()` walk\n    // sees none of them — a raw Error `cause` would otherwise collapse to\n    // `{}`. Project them explicitly; own enumerable extras (`code`,\n    // `cause`, custom AIError fields) still merge in below and recurse\n    // normally, so a chained `cause` that is itself an Error is unwrapped\n    // the same way.\n    const source: Record<string, unknown> =\n      input instanceof Error\n        ? { ...input, name: input.name, message: input.message, stack: input.stack }\n        : (input as Record<string, unknown>);\n\n    const out: Record<string, unknown> = {};\n    for (const [key, val] of Object.entries(source)) {\n      out[key] = keyIsSensitive(key, fragments)\n        ? placeholder\n        : walk(val, depth + 1);\n    }\n    return out;\n  };\n\n  return walk(value, 0) as T;\n}\n\n/**\n * Strip sensitive HTTP headers from a `Headers` instance or a plain\n * header record, returning a redacted plain object. Header names are\n * matched case-insensitively against {@link SENSITIVE_HEADERS}.\n */\nexport function redactHeaders(\n  headers: Headers | Record<string, unknown> | undefined,\n  placeholder: string = DEFAULT_PLACEHOLDER,\n): Record<string, unknown> {\n  if (!headers) return {};\n\n  const entries: Array<[string, unknown]> =\n    headers instanceof Headers\n      ? [...headers.entries()]\n      : Object.entries(headers);\n\n  const out: Record<string, unknown> = {};\n  for (const [key, val] of entries) {\n    out[key] = SENSITIVE_HEADERS.includes(key.toLowerCase()) ? placeholder : val;\n  }\n  return out;\n}\n\n/**\n * Patterns for secrets that hide in FREE TEXT (error messages, stack\n * traces, log lines) where the key-based {@link redact} can't reach them.\n * Each entry replaces the secret with `[redacted]` while keeping\n * surrounding context.\n */\nconst SECRET_PATTERNS: ReadonlyArray<readonly [RegExp, string]> = [\n  [/\\bBearer\\s+[A-Za-z0-9._~+/-]+=*/gi, \"Bearer [redacted]\"],\n  [/\\b(authorization|x-api-key|api[_-]?key|cookie)(\"?\\s*[:=]\\s*\"?)[^\\s\",}]+/gi, \"$1$2[redacted]\"],\n  [/\\bsk-[A-Za-z0-9]{16,}\\b/g, \"[redacted]\"], // OpenAI-style keys\n  [/\\bxox[baprs]-[A-Za-z0-9-]{8,}\\b/g, \"[redacted]\"], // Slack tokens\n  [/\\bgh[pousr]_[A-Za-z0-9]{20,}\\b/g, \"[redacted]\"], // GitHub tokens\n  [/\\bAKIA[0-9A-Z]{16}\\b/g, \"[redacted]\"], // AWS access key ids\n];\n\n/**\n * Scrub secrets that appear in free-form text — error messages, stack\n * traces, exported log lines. Complements {@link redact} (which is key-\n * driven and can't see a token embedded in a string). Used by the trace /\n * error serializer (S4) before a message or stack is stored or exported.\n */\nexport function scrubSecrets(text: string): string {\n  let out = text;\n  for (const [pattern, replacement] of SECRET_PATTERNS) {\n    out = out.replace(pattern, replacement);\n  }\n  return out;\n}\n\n/**\n * Serialized, secret-free view of an error. `stack` is omitted by default\n * (it can embed local paths, endpoints, and tokens); pass\n * `includeStack: true` only for a trusted local sink. The retained\n * `cause` is deep-redacted via {@link redact}, so a raw provider SDK error\n * carrying `Authorization` / `x-api-key` on `cause.headers` is sanitized.\n */\nexport type RedactedError = {\n  name: string;\n  message: string;\n  code?: string;\n  cause?: unknown;\n  stack?: string;\n};\n\nexport function redactError(\n  error: unknown,\n  options: { includeStack?: boolean } & RedactOptions = {},\n): RedactedError {\n  const { includeStack, ...redactOptions } = options;\n\n  if (error === null || typeof error !== \"object\") {\n    return { name: \"Error\", message: String(error) };\n  }\n\n  const err = error as {\n    name?: unknown;\n    message?: unknown;\n    code?: unknown;\n    cause?: unknown;\n    stack?: unknown;\n  };\n\n  const result: RedactedError = {\n    name: typeof err.name === \"string\" ? err.name : \"Error\",\n    message: typeof err.message === \"string\" ? err.message : String(error),\n  };\n\n  if (typeof err.code === \"string\") {\n    result.code = err.code;\n  }\n  if (err.cause !== undefined) {\n    result.cause =\n      err.cause !== null && typeof err.cause === \"object\"\n        ? redact(err.cause, redactOptions)\n        : err.cause;\n  }\n  if (includeStack && typeof err.stack === \"string\") {\n    result.stack = err.stack;\n  }\n\n  return result;\n}\n"],"mappings":"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"}