redactor.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  1. import { Option } from "effect"
  2. import { decodeJson } from "./matching"
  3. import { redactHeaders, redactUrl } from "./redaction"
  4. import type { RequestSnapshot, ResponseSnapshot } from "./schema"
  5. export const DEFAULT_REQUEST_HEADERS: ReadonlyArray<string> = ["content-type", "accept", "openai-beta"]
  6. export const DEFAULT_RESPONSE_HEADERS: ReadonlyArray<string> = ["content-type"]
  7. const identity = <T>(value: T) => value
  8. export interface Redactor {
  9. readonly request: (snapshot: RequestSnapshot) => RequestSnapshot
  10. readonly response: (snapshot: ResponseSnapshot) => ResponseSnapshot
  11. }
  12. export const compose = (...redactors: ReadonlyArray<Partial<Redactor>>): Redactor => {
  13. const requests = redactors.map((r) => r.request).filter((fn): fn is Redactor["request"] => fn !== undefined)
  14. const responses = redactors.map((r) => r.response).filter((fn): fn is Redactor["response"] => fn !== undefined)
  15. return {
  16. request: requests.length === 0 ? identity : (snapshot) => requests.reduce((acc, fn) => fn(acc), snapshot),
  17. response: responses.length === 0 ? identity : (snapshot) => responses.reduce((acc, fn) => fn(acc), snapshot),
  18. }
  19. }
  20. export interface HeaderOptions {
  21. readonly allow?: ReadonlyArray<string>
  22. readonly redact?: ReadonlyArray<string>
  23. }
  24. export const requestHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
  25. request: (snapshot) => ({
  26. ...snapshot,
  27. headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_REQUEST_HEADERS, options.redact),
  28. }),
  29. })
  30. export const responseHeaders = (options: HeaderOptions = {}): Partial<Redactor> => ({
  31. response: (snapshot) => ({
  32. ...snapshot,
  33. headers: redactHeaders(snapshot.headers, options.allow ?? DEFAULT_RESPONSE_HEADERS, options.redact),
  34. }),
  35. })
  36. export interface UrlOptions {
  37. readonly query?: ReadonlyArray<string>
  38. readonly transform?: (url: string) => string
  39. }
  40. export const url = (options: UrlOptions = {}): Partial<Redactor> => ({
  41. request: (snapshot) => ({ ...snapshot, url: redactUrl(snapshot.url, options.query, options.transform) }),
  42. })
  43. export const body = (transform: (parsed: unknown) => unknown): Partial<Redactor> => ({
  44. request: (snapshot) => ({
  45. ...snapshot,
  46. body: Option.match(decodeJson(snapshot.body), {
  47. onNone: () => snapshot.body,
  48. onSome: (parsed) => JSON.stringify(transform(parsed)),
  49. }),
  50. }),
  51. })
  52. export interface DefaultRedactorOverrides {
  53. readonly requestHeaders?: HeaderOptions
  54. readonly responseHeaders?: HeaderOptions
  55. readonly url?: UrlOptions
  56. readonly body?: (parsed: unknown) => unknown
  57. }
  58. export const defaults = (overrides: DefaultRedactorOverrides = {}): Redactor =>
  59. compose(
  60. requestHeaders(overrides.requestHeaders),
  61. responseHeaders(overrides.responseHeaders),
  62. url(overrides.url),
  63. ...(overrides.body ? [body(overrides.body)] : []),
  64. )