| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- import { Option } from "effect"
- import { decodeJson } from "./matching.js"
- import { REDACTED, redactHeaders, redactUrl } from "./redaction.js"
- import type { RedactOptions, RequestSnapshot, ResponseSnapshot } from "./types.js"
- export type { RedactOptions } from "./types.js"
- export const DEFAULT_REQUEST_HEADERS: ReadonlyArray<string> = ["content-type", "accept", "openai-beta"]
- export const DEFAULT_RESPONSE_HEADERS: ReadonlyArray<string> = ["content-type"]
- const identity = <T>(value: T) => value
- export interface Redactor {
- readonly request: (snapshot: RequestSnapshot) => RequestSnapshot
- readonly response: (snapshot: ResponseSnapshot) => ResponseSnapshot
- }
- export const compose = (...redactors: ReadonlyArray<Partial<Redactor>>): Redactor => {
- const requests = redactors.map((r) => r.request).filter((fn): fn is Redactor["request"] => fn !== undefined)
- const responses = redactors.map((r) => r.response).filter((fn): fn is Redactor["response"] => fn !== undefined)
- return {
- request: requests.length === 0 ? identity : (snapshot) => requests.reduce((acc, fn) => fn(acc), snapshot),
- response: responses.length === 0 ? identity : (snapshot) => responses.reduce((acc, fn) => fn(acc), snapshot),
- }
- }
- export interface HeaderOptions {
- readonly allow?: ReadonlyArray<string>
- readonly redact?: ReadonlyArray<string>
- }
- export const requestHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
- request: (snapshot) => ({
- ...snapshot,
- headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_REQUEST_HEADERS, options.redact),
- }),
- })
- export const responseHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
- response: (snapshot) => ({
- ...snapshot,
- headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_RESPONSE_HEADERS, options.redact),
- }),
- })
- export interface UrlOptions {
- readonly query?: ReadonlyArray<string>
- readonly transform?: (url: string) => string
- }
- export const url = (options: UrlOptions = {}): Partial<Redactor> => ({
- request: (snapshot) => ({ ...snapshot, url: redactUrl(snapshot.url, options.query, options.transform) }),
- })
- export const body = (transform: (parsed: unknown) => unknown): Partial<Redactor> => ({
- request: (snapshot) => ({
- ...snapshot,
- body: Option.match(decodeJson(snapshot.body), {
- onNone: () => snapshot.body,
- onSome: (parsed) => JSON.stringify(transform(parsed)),
- }),
- }),
- })
- export interface DefaultRedactorOverrides {
- readonly requestHeaders?: HeaderOptions
- readonly responseHeaders?: HeaderOptions
- readonly url?: UrlOptions
- readonly body?: (parsed: unknown) => unknown
- }
- const DEFAULT_REDACT_JSON_FIELDS = [
- "access_token",
- "api_key",
- "apikey",
- "client_secret",
- "password",
- "refresh_token",
- "secret",
- "token",
- ]
- const normalizeField = (field: string) => field.replace(/[^a-z0-9]/gi, "").toLowerCase()
- const redactJsonFields = (value: unknown, fields: ReadonlySet<string>): unknown => {
- if (Array.isArray(value)) return value.map((item) => redactJsonFields(item, fields))
- if (!value || typeof value !== "object") return value
- return Object.fromEntries(
- Object.entries(value).map(([key, child]) => [
- key,
- fields.has(normalizeField(key)) ? REDACTED : redactJsonFields(child, fields),
- ]),
- )
- }
- const redactBody = (value: string, fields: ReadonlySet<string>, transform: ((body: string) => string) | undefined) => {
- const redacted = Option.match(decodeJson(value), {
- onNone: () => value,
- onSome: (parsed) => JSON.stringify(redactJsonFields(parsed, fields)),
- })
- return transform?.(redacted) ?? redacted
- }
- export const make = (options: RedactOptions = {}): Redactor => {
- const fields = new Set([...DEFAULT_REDACT_JSON_FIELDS, ...(options.jsonFields ?? [])].map(normalizeField))
- return compose(
- requestHeaders({
- allow: [...DEFAULT_REQUEST_HEADERS, ...(options.allowRequestHeaders ?? []), ...(options.headers ?? [])],
- redact: options.headers,
- }),
- responseHeaders({
- allow: [...DEFAULT_RESPONSE_HEADERS, ...(options.allowResponseHeaders ?? []), ...(options.headers ?? [])],
- redact: options.headers,
- }),
- url({ query: options.queryParameters, transform: options.url }),
- {
- request: (snapshot) => ({
- ...snapshot,
- body: redactBody(snapshot.body, fields, options.body),
- }),
- response: (snapshot) => ({
- ...snapshot,
- body: redactBody(snapshot.body, fields, options.body),
- }),
- },
- )
- }
- export const defaults = (overrides: DefaultRedactorOverrides = {}): Redactor =>
- compose(
- requestHeaders(overrides.requestHeaders),
- responseHeaders(overrides.responseHeaders),
- url(overrides.url),
- ...(overrides.body ? [body(overrides.body)] : []),
- )
|