@useairfoil/effect-vcr 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +31 -229
  2. package/dist/{cassette-store-BMtcjh5h.d.ts → cassette-store.d.mts} +13 -17
  3. package/dist/{cassette-store-BF9e0rz3.js → cassette-store.mjs} +6 -7
  4. package/dist/cassette-store.mjs.map +1 -0
  5. package/dist/file-system-cassette-store.d.mts +17 -0
  6. package/dist/{file-system-cassette-store-CyOsu3z5.js → file-system-cassette-store.mjs} +7 -8
  7. package/dist/file-system-cassette-store.mjs.map +1 -0
  8. package/dist/{index.d.ts → index.d.mts} +4 -4
  9. package/dist/index.mjs +5 -0
  10. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  11. package/dist/{types-XZehMu_v.d.ts → types-v6cSTgDp.d.mts} +3 -3
  12. package/dist/{types.d.ts → types.d.mts} +1 -1
  13. package/dist/{types-DMuz_JGd.js → types.mjs} +3 -4
  14. package/dist/types.mjs.map +1 -0
  15. package/dist/{vcr-http-client-Ba0WYPV9.js → vcr-http-client-5K20k6US.mjs} +67 -45
  16. package/dist/vcr-http-client-5K20k6US.mjs.map +1 -0
  17. package/dist/vcr-http-client.d.mts +21 -0
  18. package/dist/vcr-http-client.mjs +3 -0
  19. package/dist/{vitest-state-CiW5dzXZ.js → vitest-state-rX5XIlH6.mjs} +2 -2
  20. package/dist/vitest-state-rX5XIlH6.mjs.map +1 -0
  21. package/package.json +25 -25
  22. package/src/sanitize.ts +21 -2
  23. package/src/types.ts +2 -0
  24. package/src/vcr-http-client.ts +81 -33
  25. package/dist/cassette-store-BF9e0rz3.js.map +0 -1
  26. package/dist/cassette-store.d.ts +0 -3
  27. package/dist/cassette-store.js +0 -4
  28. package/dist/file-system-cassette-store-CYmQT7qj.d.ts +0 -19
  29. package/dist/file-system-cassette-store-CyOsu3z5.js.map +0 -1
  30. package/dist/file-system-cassette-store.d.ts +0 -4
  31. package/dist/file-system-cassette-store.js +0 -6
  32. package/dist/index.js +0 -7
  33. package/dist/types-DMuz_JGd.js.map +0 -1
  34. package/dist/types.js +0 -3
  35. package/dist/vcr-http-client-Ba0WYPV9.js.map +0 -1
  36. package/dist/vcr-http-client-zQZTK-ru.d.ts +0 -26
  37. package/dist/vcr-http-client.d.ts +0 -4
  38. package/dist/vcr-http-client.js +0 -6
  39. package/dist/vitest-state-CiW5dzXZ.js.map +0 -1
@@ -1,10 +1,9 @@
1
- import { t as __export } from "./chunk-Bp6m_JJh.js";
2
- import { i as createEmptyCassette, t as CassetteStore } from "./cassette-store-BF9e0rz3.js";
3
- import { t as getVitestState } from "./vitest-state-CiW5dzXZ.js";
4
- import { Array as Array$1, Config, Data, Effect, Option, Path, Record } from "effect";
5
- import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/unstable/http";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { CassetteStore, createEmptyCassette } from "./cassette-store.mjs";
3
+ import { t as getVitestState } from "./vitest-state-rX5XIlH6.mjs";
4
+ import { Array as Array$1, Config, Data, Effect, Option, Path, Record, Result, Semaphore } from "effect";
5
+ import { HttpClient, HttpClientError, HttpClientResponse, Url, UrlParams } from "effect/http";
6
6
  import stableStringify from "json-stable-stringify";
7
-
8
7
  //#region src/sanitize.ts
9
8
  /**
10
9
  * Normalize header names to match HttpClient's lowercase behavior.
@@ -47,10 +46,10 @@ const transformJsonKeys = (value, omit, replacements) => {
47
46
  };
48
47
  }
49
48
  if (value && typeof value === "object") {
50
- const record$1 = value;
49
+ const record = value;
51
50
  const next = {};
52
51
  let redacted = false;
53
- for (const [key, entry] of Object.entries(record$1)) {
52
+ for (const [key, entry] of Object.entries(record)) {
54
53
  if (omit.has(key)) {
55
54
  redacted = true;
56
55
  continue;
@@ -125,6 +124,10 @@ const transformBody = (body, headers, options) => {
125
124
  }
126
125
  })));
127
126
  };
127
+ const omitQueryParams = (url, names) => !names || names.length === 0 ? url : Result.match(Url.fromString(url), {
128
+ onFailure: () => url,
129
+ onSuccess: (parsed) => Url.modifyUrlParams(parsed, UrlParams.transform(Array$1.filter(([key]) => !names.includes(key)))).toString()
130
+ });
128
131
  /**
129
132
  * Normalize request for matching: header canonicalization and structured body filtering.
130
133
  */
@@ -134,6 +137,7 @@ const sanitizeRequest = (request, options) => {
134
137
  const filteredHeaders = omitHeaderKeys(request.headers, transformedBody.pipe(Option.exists((transformed) => transformed.redacted)) ? [...options.ignoreHeaders ?? [], "content-length"] : options.ignoreHeaders);
135
138
  return {
136
139
  ...request,
140
+ url: omitQueryParams(request.url, options.ignoreQueryParams),
137
141
  headers: toHeaderRecord(Option.getOrUndefined(filteredHeaders)),
138
142
  body
139
143
  };
@@ -144,7 +148,8 @@ const sanitizeRequest = (request, options) => {
144
148
  const buildRequestKey = (request, options) => Effect.sync(() => {
145
149
  const sanitized = sanitizeRequest(request, {
146
150
  ignoreHeaders: options.ignoreHeaders,
147
- ignoreBodyKeys: options.ignoreBodyKeys
151
+ ignoreBodyKeys: options.ignoreBodyKeys,
152
+ ignoreQueryParams: options.ignoreQueryParams
148
153
  });
149
154
  return stableStringify({
150
155
  method: sanitized.method.toUpperCase(),
@@ -169,6 +174,7 @@ const redactRequest = (request, options) => {
169
174
  const body = Option.getOrUndefined(transformedBody.pipe(Option.map((transformed) => transformed.value)));
170
175
  return {
171
176
  ...request,
177
+ url: omitQueryParams(request.url, options.redactQueryParams),
172
178
  headers: Option.getOrUndefined(omitHeaderKeys(request.headers, transformedBody.pipe(Option.exists((transformed) => transformed.redacted)) ? [...options.redactHeaders ?? [], "content-length"] : options.redactHeaders)),
173
179
  body
174
180
  };
@@ -188,10 +194,9 @@ const redactResponse = (response, options) => {
188
194
  body
189
195
  };
190
196
  };
191
-
192
197
  //#endregion
193
198
  //#region src/vcr-http-client.ts
194
- var vcr_http_client_exports = /* @__PURE__ */ __export({
199
+ var vcr_http_client_exports = /* @__PURE__ */ __exportAll({
195
200
  VcrHttpClientError: () => VcrHttpClientError,
196
201
  layer: () => layer
197
202
  });
@@ -243,7 +248,7 @@ const normalizeConfig = (config) => {
243
248
  match: config.match
244
249
  };
245
250
  };
246
- const AckDisableVcrConfig = Config.option(Config.string("ACK_DISABLE_VCR")).pipe(Config.map((value) => Option.match(value, {
251
+ const AckDisableVcrConfig = Config.option(Config.String("ACK_DISABLE_VCR")).pipe(Config.map((value) => Option.match(value, {
247
252
  onNone: () => /* @__PURE__ */ new Set(),
248
253
  onSome: (raw) => raw === "*" ? "*" : new Set(raw.split(",").map((segment) => segment.trim().toLowerCase()).filter((segment) => segment.length > 0))
249
254
  })));
@@ -290,12 +295,20 @@ const toVcrRequest = (request) => {
290
295
  })();
291
296
  return {
292
297
  method: request.method,
293
- url: request.url,
298
+ url: requestUrl(request),
294
299
  headers,
295
300
  body
296
301
  };
297
302
  };
298
303
  /**
304
+ * The URL with its query params, built like `HttpClient` builds it.
305
+ * `request.url` alone leaves out params set with `setUrlParams`.
306
+ */
307
+ const requestUrl = (request) => Result.match(Url.make(request.url, request.urlParams, Option.getOrUndefined(request.hash)), {
308
+ onFailure: () => request.url,
309
+ onSuccess: (url) => url.toString()
310
+ });
311
+ /**
299
312
  * Serialize an Effect HttpClientResponse into a cassette response shape.
300
313
  */
301
314
  const toVcrResponse = (response, body) => ({
@@ -330,6 +343,7 @@ const findEntry = (request, cassette, config) => {
330
343
  if (config.match) return Effect.succeed(Object.values(cassette.entries).find((entry) => config.match?.(request, entry)));
331
344
  return buildRequestKey(request, {
332
345
  ignoreHeaders: config.matchIgnore?.requestHeaders,
346
+ ignoreQueryParams: config.redact?.requestQueryParams,
333
347
  ignoreBodyKeys: config.matchIgnore?.requestBodyKeys
334
348
  }).pipe(Effect.map((key) => cassette.entries[key]));
335
349
  };
@@ -346,17 +360,19 @@ const replayResponse = (request, entry) => {
346
360
  const replay = (store, request, vcrRequest, config, name, exportKey) => readCassetteExport(store, name, exportKey, request).pipe(Effect.flatMap((cassette) => findEntry(vcrRequest, cassette, config).pipe(Effect.flatMap((entry) => {
347
361
  if (!entry) return Effect.fail(new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({
348
362
  request,
349
- description: `VCR replay missing entry for ${request.method} ${request.url}`
363
+ description: `VCR replay missing entry for ${request.method} ${requestUrl(request)}`
350
364
  }) }));
351
365
  return Effect.succeed(replayResponse(request, entry));
352
366
  }))));
353
367
  /**
354
368
  * Record a live response into the cassette and return the original response.
355
369
  */
356
- const record = Effect.fnUntraced(function* (store, request, vcrRequest, effect, config, name, exportKey) {
370
+ const record = Effect.fnUntraced(function* (writeLock, store, request, vcrRequest, effect, config, name, exportKey) {
357
371
  const response = yield* effect;
358
- const vcrResponse = toVcrResponse(response, yield* response.text);
372
+ const body = yield* response.text;
373
+ const vcrResponse = toVcrResponse(response, body);
359
374
  const sanitizedRequest = config.redact ? redactRequest(vcrRequest, {
375
+ redactQueryParams: config.redact.requestQueryParams,
360
376
  redactHeaders: config.redact.requestHeaders,
361
377
  redactBodyKeys: config.redact.requestBodyKeys,
362
378
  bodyReplacements: config.redact.requestBodyReplacements
@@ -366,29 +382,33 @@ const record = Effect.fnUntraced(function* (store, request, vcrRequest, effect,
366
382
  redactBodyKeys: config.redact.responseBodyKeys,
367
383
  bodyReplacements: config.redact.responseBodyReplacements
368
384
  }) : vcrResponse;
369
- const file = yield* loadOrInitCassetteFile(store, name, request);
370
- const cassette = file.exports[exportKey] ?? (yield* createEmptyCassette());
371
- const key = yield* buildRequestKey(vcrRequest, {
372
- ignoreHeaders: config.matchIgnore?.requestHeaders,
373
- ignoreBodyKeys: config.matchIgnore?.requestBodyKeys
374
- });
375
- const next = {
376
- ...cassette,
377
- entries: {
378
- ...cassette.entries,
379
- [key]: {
380
- request: sanitizedRequest,
381
- response: sanitizedResponse
385
+ yield* writeLock.withPermits(1)(Effect.gen(function* () {
386
+ const file = yield* loadOrInitCassetteFile(store, name, request);
387
+ const cassette = file.exports[exportKey] ?? (yield* createEmptyCassette());
388
+ const key = yield* buildRequestKey(vcrRequest, {
389
+ ignoreHeaders: config.matchIgnore?.requestHeaders,
390
+ ignoreQueryParams: config.redact?.requestQueryParams,
391
+ ignoreBodyKeys: config.matchIgnore?.requestBodyKeys
392
+ });
393
+ const next = {
394
+ ...cassette,
395
+ entries: {
396
+ ...cassette.entries,
397
+ [key]: {
398
+ request: sanitizedRequest,
399
+ response: sanitizedResponse
400
+ }
382
401
  }
383
- }
384
- };
385
- yield* saveCassetteFile(store, name, {
386
- ...file,
387
- exports: {
388
- ...file.exports,
389
- [exportKey]: next
390
- }
391
- }, request);
402
+ };
403
+ const nextFile = {
404
+ ...file,
405
+ exports: {
406
+ ...file.exports,
407
+ [exportKey]: next
408
+ }
409
+ };
410
+ yield* saveCassetteFile(store, name, nextFile, request);
411
+ }));
392
412
  return response;
393
413
  });
394
414
  /**
@@ -397,32 +417,34 @@ const record = Effect.fnUntraced(function* (store, request, vcrRequest, effect,
397
417
  const makeVcrHttpClient = Effect.fnUntraced(function* (config = {}) {
398
418
  const live = yield* HttpClient.HttpClient;
399
419
  const normalized = normalizeConfig(config);
400
- const isCi = yield* Config.boolean("CI").pipe(Config.withDefault(false));
420
+ const isCi = yield* Config.Boolean("CI").pipe(Config.withDefault(false));
401
421
  const disabledVcrs = yield* AckDisableVcrConfig;
402
422
  if (shouldDisableVcr(normalized.vcrName, disabledVcrs)) return live;
403
423
  const store = yield* CassetteStore;
424
+ const writeLock = yield* Semaphore.make(1);
404
425
  const { name, exportKey } = yield* resolveCassetteLocation(normalized);
405
426
  return live.pipe(HttpClient.transform(Effect.fnUntraced(function* (effect, request) {
406
427
  const vcrRequest = toVcrRequest(request);
407
428
  if (normalized.mode === "replay") return yield* replay(store, request, vcrRequest, normalized, name, exportKey);
408
- if (normalized.mode === "record") return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
429
+ if (normalized.mode === "record") return yield* record(writeLock, store, request, vcrRequest, effect, normalized, name, exportKey);
409
430
  if (!(yield* store.exists(name).pipe(Effect.mapError((error) => toRequestError(request, error))))) {
410
431
  if (isCi) return yield* Effect.fail(new HttpClientError.HttpClientError({ reason: new HttpClientError.TransportError({
411
432
  request,
412
433
  description: "VCR cassette missing in CI for auto mode"
413
434
  }) }));
414
- return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
435
+ return yield* record(writeLock, store, request, vcrRequest, effect, normalized, name, exportKey);
415
436
  }
416
- const entry = yield* findEntry(vcrRequest, yield* readCassetteExport(store, name, exportKey, request), normalized);
437
+ const cassette = yield* readCassetteExport(store, name, exportKey, request);
438
+ const entry = yield* findEntry(vcrRequest, cassette, normalized);
417
439
  if (entry) return replayResponse(request, entry);
418
- return yield* record(store, request, vcrRequest, effect, normalized, name, exportKey);
440
+ return yield* record(writeLock, store, request, vcrRequest, effect, normalized, name, exportKey);
419
441
  })));
420
442
  });
421
443
  /**
422
444
  * Layer that provides a VCR-wrapped HttpClient.
423
445
  */
424
446
  const layer = (config = {}) => HttpClient.layerMergedContext(makeVcrHttpClient(config));
425
-
426
447
  //#endregion
427
448
  export { layer as n, vcr_http_client_exports as r, VcrHttpClientError as t };
428
- //# sourceMappingURL=vcr-http-client-Ba0WYPV9.js.map
449
+
450
+ //# sourceMappingURL=vcr-http-client-5K20k6US.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vcr-http-client-5K20k6US.mjs","names":["Arr","Arr"],"sources":["../src/sanitize.ts","../src/vcr-http-client.ts"],"sourcesContent":["import { Array as Arr, Effect, Option, Record, Result } from \"effect\";\nimport { Url, UrlParams } from \"effect/http\";\nimport stableStringify from \"json-stable-stringify\";\n\nimport type { VcrRedactedValue, VcrRequest, VcrResponse } from \"./types\";\n\n/**\n * Normalize header names to match HttpClient's lowercase behavior.\n */\nconst normalizeHeaderKey = (key: string) => key.toLowerCase();\n\n/**\n * Canonicalize headers: lowercase keys and stable ordering.\n */\nconst toHeaderRecord = (headers?: Record<string, string>) => {\n if (!headers) return undefined;\n const lowered = Record.mapKeys(headers, normalizeHeaderKey);\n return Record.fromEntries(Record.toEntries(lowered).sort(([a], [b]) => a.localeCompare(b)));\n};\n\n/**\n * Remove specified header names (case-insensitive).\n */\nconst omitHeaderKeys = (\n headers: Record<string, string> | undefined,\n ignore: ReadonlyArray<string> | undefined,\n): Option.Option<Record<string, string>> => {\n if (!headers) return Option.none();\n if (!ignore || ignore.length === 0) return Option.some(headers);\n const ignoreSet = new Set(ignore.map(normalizeHeaderKey));\n return Option.some(Record.filter(headers, (_, key) => !ignoreSet.has(normalizeHeaderKey(key))));\n};\n\n/**\n * Best-effort JSON parse; returns undefined for non-JSON bodies.\n */\nconst tryParseJson = Option.liftThrowable((input: string) => JSON.parse(input));\n\ntype BodyOptions = {\n readonly omit?: ReadonlyArray<string>;\n readonly replacements?: Readonly<Record<string, VcrRedactedValue>>;\n};\n\ntype BodyTransformation<A> = {\n readonly value: A;\n readonly redacted: boolean;\n};\n\n/**\n * Recursively remove or replace keys in JSON values.\n */\nconst transformJsonKeys = (\n value: unknown,\n omit: ReadonlySet<string>,\n replacements: Readonly<Record<string, VcrRedactedValue>>,\n): BodyTransformation<unknown> => {\n if (Array.isArray(value)) {\n let redacted = false;\n const values = value.map((item) => {\n const transformed = transformJsonKeys(item, omit, replacements);\n redacted ||= transformed.redacted;\n return transformed.value;\n });\n return { value: values, redacted };\n }\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n const next: Record<string, unknown> = {};\n let redacted = false;\n for (const [key, entry] of Object.entries(record)) {\n if (omit.has(key)) {\n redacted = true;\n continue;\n }\n if (Record.has(replacements, key)) {\n next[key] = replacements[key];\n redacted = true;\n continue;\n }\n const transformed = transformJsonKeys(entry, omit, replacements);\n next[key] = transformed.value;\n redacted ||= transformed.redacted;\n }\n return { value: next, redacted };\n }\n return { value, redacted: false };\n};\n\nconst headerValue = (\n headers: Readonly<Record<string, string>> | undefined,\n name: string,\n): string | undefined => {\n if (!headers) return undefined;\n const normalizedName = normalizeHeaderKey(name);\n return Object.entries(headers).find(([key]) => normalizeHeaderKey(key) === normalizedName)?.[1];\n};\n\nconst isUrlEncoded = (headers: Readonly<Record<string, string>> | undefined): boolean =>\n headerValue(headers, \"content-type\")?.split(\";\", 1)[0]?.trim().toLowerCase() ===\n \"application/x-www-form-urlencoded\";\n\nconst transformUrlParams = (\n body: string,\n omit: ReadonlySet<string>,\n replacements: Readonly<Record<string, VcrRedactedValue>>,\n): BodyTransformation<string> => {\n let redacted = false;\n const value = UrlParams.fromInput(new URLSearchParams(body)).pipe(\n UrlParams.transform((params) =>\n params.flatMap(([key, value]) => {\n if (omit.has(key)) {\n redacted = true;\n return [];\n }\n if (Record.has(replacements, key)) {\n redacted = true;\n return [[key, String(replacements[key])] as const];\n }\n return [[key, value] as const];\n }),\n ),\n UrlParams.toString,\n );\n return { value, redacted };\n};\n\n/**\n * Remove or replace JSON and URL-encoded body fields when possible.\n */\nconst transformBody = (\n body: string | undefined,\n headers: Readonly<Record<string, string>> | undefined,\n options: BodyOptions,\n): Option.Option<BodyTransformation<string>> => {\n if (!body) return Option.none();\n const omit = new Set(options.omit);\n const replacements = options.replacements ?? {};\n if (isUrlEncoded(headers)) {\n const hasMatch = Array.from(new URLSearchParams(body).keys()).some(\n (key) => omit.has(key) || Record.has(replacements, key),\n );\n if (!hasMatch) {\n return Option.some({ value: body, redacted: false });\n }\n return Option.some(transformUrlParams(body, omit, replacements));\n }\n return Option.some(\n tryParseJson(body).pipe(\n Option.match({\n onNone: () => ({ value: body, redacted: false }),\n onSome: (parsed) => {\n const transformed = transformJsonKeys(parsed, omit, replacements);\n return {\n // stableStringify's return type is `string | undefined` for cyclic input;\n // parsed JSON is never cyclic, but the fallback keeps the types honest.\n value: stableStringify(transformed.value) ?? body,\n redacted: transformed.redacted,\n };\n },\n }),\n ),\n );\n};\n\n// A URL that does not parse is kept as is.\nconst omitQueryParams = (url: string, names: ReadonlyArray<string> | undefined): string =>\n !names || names.length === 0\n ? url\n : Result.match(Url.fromString(url), {\n onFailure: () => url,\n onSuccess: (parsed) =>\n Url.modifyUrlParams(\n parsed,\n UrlParams.transform(Arr.filter(([key]) => !names.includes(key))),\n ).toString(),\n });\n\n/**\n * Normalize request for matching: header canonicalization and structured body filtering.\n */\nexport const sanitizeRequest = (\n request: VcrRequest,\n options: {\n readonly ignoreHeaders?: ReadonlyArray<string>;\n readonly ignoreBodyKeys?: ReadonlyArray<string>;\n readonly ignoreQueryParams?: ReadonlyArray<string>;\n },\n): VcrRequest => {\n const transformedBody = transformBody(request.body, request.headers, {\n omit: options.ignoreBodyKeys,\n });\n const body = Option.getOrUndefined(\n transformedBody.pipe(Option.map((transformed) => transformed.value)),\n );\n const filteredHeaders = omitHeaderKeys(\n request.headers,\n transformedBody.pipe(Option.exists((transformed) => transformed.redacted))\n ? [...(options.ignoreHeaders ?? []), \"content-length\"]\n : options.ignoreHeaders,\n );\n return {\n ...request,\n url: omitQueryParams(request.url, options.ignoreQueryParams),\n headers: toHeaderRecord(Option.getOrUndefined(filteredHeaders)),\n body,\n };\n};\n\n/**\n * Compute the stable request key used for cassette lookups.\n */\nexport const buildRequestKey = (\n request: VcrRequest,\n options: {\n readonly ignoreHeaders?: ReadonlyArray<string>;\n readonly ignoreBodyKeys?: ReadonlyArray<string>;\n readonly ignoreQueryParams?: ReadonlyArray<string>;\n },\n): Effect.Effect<string> =>\n Effect.sync(() => {\n const sanitized = sanitizeRequest(request, {\n ignoreHeaders: options.ignoreHeaders,\n ignoreBodyKeys: options.ignoreBodyKeys,\n ignoreQueryParams: options.ignoreQueryParams,\n });\n // stableStringify only returns undefined for cyclic input; this literal never is,\n // but its declared type is `string | undefined`, so keep the fallback for TS.\n return (\n stableStringify({\n method: sanitized.method.toUpperCase(),\n url: sanitized.url,\n headers: sanitized.headers ?? {},\n body: sanitized.body ?? \"\",\n }) ??\n JSON.stringify({\n method: sanitized.method.toUpperCase(),\n url: sanitized.url,\n headers: sanitized.headers ?? {},\n body: sanitized.body ?? \"\",\n })\n );\n });\n\n/**\n * Remove sensitive request data before persisting to a cassette.\n */\nexport const redactRequest = (\n request: VcrRequest,\n options: {\n readonly redactQueryParams?: ReadonlyArray<string>;\n readonly redactHeaders?: ReadonlyArray<string>;\n readonly redactBodyKeys?: ReadonlyArray<string>;\n readonly bodyReplacements?: Readonly<Record<string, VcrRedactedValue>>;\n },\n): VcrRequest => {\n const transformedBody = transformBody(request.body, request.headers, {\n omit: options.redactBodyKeys,\n replacements: options.bodyReplacements,\n });\n const body = Option.getOrUndefined(\n transformedBody.pipe(Option.map((transformed) => transformed.value)),\n );\n return {\n ...request,\n url: omitQueryParams(request.url, options.redactQueryParams),\n headers: Option.getOrUndefined(\n omitHeaderKeys(\n request.headers,\n transformedBody.pipe(Option.exists((transformed) => transformed.redacted))\n ? [...(options.redactHeaders ?? []), \"content-length\"]\n : options.redactHeaders,\n ),\n ),\n body,\n };\n};\n\n/**\n * Remove sensitive response data before persisting to a cassette.\n */\nexport const redactResponse = (\n response: VcrResponse,\n options: {\n readonly redactHeaders?: ReadonlyArray<string>;\n readonly redactBodyKeys?: ReadonlyArray<string>;\n readonly bodyReplacements?: Readonly<Record<string, VcrRedactedValue>>;\n },\n): VcrResponse => {\n const transformedBody = transformBody(response.body, response.headers, {\n omit: options.redactBodyKeys,\n replacements: options.bodyReplacements,\n });\n const body = Option.getOrElse(\n transformedBody.pipe(Option.map((transformed) => transformed.value)),\n () => response.body,\n );\n return {\n ...response,\n headers: Option.getOrUndefined(\n omitHeaderKeys(\n response.headers,\n transformedBody.pipe(Option.exists((transformed) => transformed.redacted))\n ? [...(options.redactHeaders ?? []), \"content-length\"]\n : options.redactHeaders,\n ),\n ),\n body,\n };\n};\n","import { Array as Arr, Config, Data, Effect, Option, Path, Result, Semaphore } from \"effect\";\nimport {\n HttpClient,\n HttpClientError,\n type HttpClientRequest,\n HttpClientResponse,\n Url,\n} from \"effect/http\";\n\nimport type { Cassette, CassetteFile, VcrConfig, VcrEntry, VcrRequest, VcrResponse } from \"./types\";\n\nimport { CassetteStore, type CassetteStoreService, createEmptyCassette } from \"./cassette-store\";\nimport { buildRequestKey, redactRequest, redactResponse } from \"./sanitize\";\nimport { getVitestState } from \"./vitest-state\";\n\n/**\n * Decoder for Uint8Array request bodies when building cassette keys.\n */\nconst decoder = new TextDecoder();\n\nexport class VcrHttpClientError extends Data.TaggedError(\"VcrHttpClientError\")<{\n readonly message: string;\n readonly cause?: unknown;\n}> {}\n\nconst toCassetteFileName = (name: string): string =>\n name.endsWith(\".cassette\") ? name : `${name}.cassette`;\n\n/**\n * Resolve the cassette file name and export key.\n *\n * If explicit cassetteName are provided, use them and default to\n * the \"default\" export. Otherwise infer from Vitest state:\n * - <file-name>.cassette file name\n * - export key = current test name (describe > test)\n */\nconst resolveCassetteLocation = Effect.fnUntraced(function* (config: VcrConfig) {\n const path = yield* Path.Path;\n if (config.cassetteName) {\n return {\n name: toCassetteFileName(config.cassetteName),\n exportKey: \"default\",\n };\n }\n\n const { testPath, currentTestName } = getVitestState();\n if (!testPath || !currentTestName) {\n return yield* Effect.fail(\n new VcrHttpClientError({\n message:\n \"VCR cassette path could not be inferred. Provide cassetteName when not running in Vitest.\",\n }),\n );\n }\n\n const fileName = path.basename(testPath, \".ts\");\n const cassetteName = `${fileName}.cassette`;\n\n return {\n name: cassetteName,\n exportKey: currentTestName,\n };\n});\n\n/**\n * Apply defaults for common VCR behavior while preserving explicit overrides.\n */\nconst normalizeConfig = (config: VcrConfig): VcrConfig => {\n const redactedRequestBodyKeys = Arr.union(\n config.redact?.requestBodyKeys ?? [],\n Object.keys(config.redact?.requestBodyReplacements ?? {}),\n );\n const redactedRequestHeaders = Arr.union([\"authorization\"], config.redact?.requestHeaders ?? []);\n\n return {\n vcrName: config.vcrName,\n cassetteName: config.cassetteName,\n mode: config.mode ?? \"auto\",\n matchIgnore: {\n requestHeaders: Arr.union(redactedRequestHeaders, config.matchIgnore?.requestHeaders ?? []),\n requestBodyKeys: Arr.union(\n redactedRequestBodyKeys,\n config.matchIgnore?.requestBodyKeys ?? [],\n ),\n },\n redact: {\n ...config.redact,\n requestHeaders: redactedRequestHeaders,\n },\n match: config.match,\n };\n};\n\nconst AckDisableVcrConfig = Config.option(Config.String(\"ACK_DISABLE_VCR\")).pipe(\n Config.map((value) =>\n Option.match(value, {\n onNone: () => new Set<string>(),\n onSome: (raw) =>\n raw === \"*\"\n ? \"*\"\n : new Set(\n raw\n .split(\",\")\n .map((segment) => segment.trim().toLowerCase())\n .filter((segment) => segment.length > 0),\n ),\n }),\n ),\n);\n\nconst shouldDisableVcr = (name: string | undefined, disabled: \"*\" | Set<string>): boolean => {\n if (disabled === \"*\") return true;\n if (!name) return false;\n const normalized = name.trim().toLowerCase();\n return disabled.has(normalized);\n};\n\n/**\n * Convert store or replay failures into HttpClient transport errors.\n */\nconst toRequestError = (\n request: HttpClientRequest.HttpClientRequest,\n cause: unknown,\n description?: string,\n) =>\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n cause,\n description,\n }),\n });\n\n/**\n * Best-effort JSON serialization for opaque raw bodies.\n */\nconst tryStringify = Option.liftThrowable((value: unknown) => JSON.stringify(value));\n\n/**\n * Serialize an Effect HttpClientRequest into a cassette request shape.\n *\n * Note: some body variants (Stream, FormData) are not safely serializable\n * without consuming the body, so we store placeholders to avoid breaking\n * live requests.\n */\nconst toVcrRequest = (request: HttpClientRequest.HttpClientRequest): VcrRequest => {\n const headers: Record<string, string> = { ...request.headers };\n const body = (() => {\n switch (request.body._tag) {\n case \"Empty\":\n return \"\";\n case \"Uint8Array\":\n return decoder.decode(request.body.body);\n case \"Raw\": {\n const raw = request.body.body as unknown;\n if (typeof raw === \"string\") return raw;\n if (raw instanceof Uint8Array) return decoder.decode(raw);\n return tryStringify(raw).pipe(Option.getOrElse(() => \"[raw]\"));\n }\n case \"FormData\":\n return \"[form-data]\";\n case \"Stream\":\n return \"[stream]\";\n }\n })();\n\n return {\n method: request.method,\n url: requestUrl(request),\n headers,\n body,\n };\n};\n\n/**\n * The URL with its query params, built like `HttpClient` builds it.\n * `request.url` alone leaves out params set with `setUrlParams`.\n */\nconst requestUrl = (request: HttpClientRequest.HttpClientRequest): string =>\n Result.match(Url.make(request.url, request.urlParams, Option.getOrUndefined(request.hash)), {\n onFailure: () => request.url,\n onSuccess: (url) => url.toString(),\n });\n\n/**\n * Serialize an Effect HttpClientResponse into a cassette response shape.\n */\nconst toVcrResponse = (\n response: HttpClientResponse.HttpClientResponse,\n body: string,\n): VcrResponse => ({\n status: response.status,\n body,\n headers: { ...response.headers },\n});\n\n/**\n * Load a cassette and map store errors into HttpClient errors.\n */\nconst readCassetteFile = (\n store: CassetteStoreService,\n name: string,\n request: HttpClientRequest.HttpClientRequest,\n) => store.load(name).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Load a cassette or initialize a new one if missing.\n */\nconst loadOrInitCassetteFile = (\n store: CassetteStoreService,\n name: string,\n request: HttpClientRequest.HttpClientRequest,\n) => store.loadOrInit(name).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Persist the updated cassette.\n */\nconst saveCassetteFile = (\n store: CassetteStoreService,\n name: string,\n cassette: CassetteFile,\n request: HttpClientRequest.HttpClientRequest,\n) => store.save(name, cassette).pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n/**\n * Load a named cassette export from the file, or return a fresh empty cassette.\n */\nconst readCassetteExport = (\n store: CassetteStoreService,\n name: string,\n exportKey: string,\n request: HttpClientRequest.HttpClientRequest,\n) =>\n readCassetteFile(store, name, request).pipe(\n Effect.flatMap((file) => {\n const found = file.exports[exportKey];\n if (found) return Effect.succeed(found);\n return createEmptyCassette();\n }),\n );\n\n/**\n * Locate a cassette entry by custom matcher or stable request key.\n */\nconst findEntry = (\n request: VcrRequest,\n cassette: Cassette,\n config: VcrConfig,\n): Effect.Effect<VcrEntry | undefined> => {\n if (config.match) {\n return Effect.succeed(\n Object.values(cassette.entries).find((entry) => config.match?.(request, entry)),\n );\n }\n return buildRequestKey(request, {\n ignoreHeaders: config.matchIgnore?.requestHeaders,\n ignoreQueryParams: config.redact?.requestQueryParams,\n ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,\n }).pipe(Effect.map((key) => cassette.entries[key]));\n};\n\nconst replayResponse = (\n request: HttpClientRequest.HttpClientRequest,\n entry: VcrEntry,\n): HttpClientResponse.HttpClientResponse => {\n const web = new Response(entry.response.body, {\n status: entry.response.status,\n headers: entry.response.headers,\n });\n return HttpClientResponse.fromWeb(request, web);\n};\n\n/**\n * Replay a response from cassette data using HttpClientResponse.fromWeb.\n */\nconst replay = (\n store: CassetteStoreService,\n request: HttpClientRequest.HttpClientRequest,\n vcrRequest: VcrRequest,\n config: VcrConfig,\n name: string,\n exportKey: string,\n) =>\n readCassetteExport(store, name, exportKey, request).pipe(\n Effect.flatMap((cassette) =>\n findEntry(vcrRequest, cassette, config).pipe(\n Effect.flatMap((entry) => {\n if (!entry) {\n return Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: `VCR replay missing entry for ${request.method} ${requestUrl(request)}`,\n }),\n }),\n );\n }\n\n return Effect.succeed(replayResponse(request, entry));\n }),\n ),\n ),\n );\n\n/**\n * Record a live response into the cassette and return the original response.\n */\nconst record = Effect.fnUntraced(function* (\n writeLock: Semaphore.Semaphore,\n store: CassetteStoreService,\n request: HttpClientRequest.HttpClientRequest,\n vcrRequest: VcrRequest,\n effect: Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>,\n config: VcrConfig,\n name: string,\n exportKey: string,\n): Effect.fn.Return<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError> {\n const response = yield* effect;\n const body = yield* response.text;\n const vcrResponse = toVcrResponse(response, body);\n\n const sanitizedRequest = config.redact\n ? redactRequest(vcrRequest, {\n redactQueryParams: config.redact.requestQueryParams,\n redactHeaders: config.redact.requestHeaders,\n redactBodyKeys: config.redact.requestBodyKeys,\n bodyReplacements: config.redact.requestBodyReplacements,\n })\n : vcrRequest;\n const sanitizedResponse = config.redact\n ? redactResponse(vcrResponse, {\n redactHeaders: config.redact.responseHeaders,\n redactBodyKeys: config.redact.responseBodyKeys,\n bodyReplacements: config.redact.responseBodyReplacements,\n })\n : vcrResponse;\n\n // Concurrent requests record into the same file, so writes take turns.\n yield* writeLock.withPermits(1)(\n Effect.gen(function* () {\n const file = yield* loadOrInitCassetteFile(store, name, request);\n const cassette = file.exports[exportKey] ?? (yield* createEmptyCassette());\n const key = yield* buildRequestKey(vcrRequest, {\n ignoreHeaders: config.matchIgnore?.requestHeaders,\n ignoreQueryParams: config.redact?.requestQueryParams,\n ignoreBodyKeys: config.matchIgnore?.requestBodyKeys,\n });\n const next: Cassette = {\n ...cassette,\n entries: {\n ...cassette.entries,\n [key]: {\n request: sanitizedRequest,\n response: sanitizedResponse,\n },\n },\n };\n const nextFile: CassetteFile = {\n ...file,\n exports: {\n ...file.exports,\n [exportKey]: next,\n },\n };\n yield* saveCassetteFile(store, name, nextFile, request);\n }),\n );\n return response;\n});\n\n/**\n * Build a VCR-aware HttpClient that replays or records per config.\n */\nconst makeVcrHttpClient = Effect.fnUntraced(function* (config: VcrConfig = {}) {\n const live = yield* HttpClient.HttpClient;\n const normalized = normalizeConfig(config);\n\n const isCi = yield* Config.Boolean(\"CI\").pipe(Config.withDefault(false));\n\n const disabledVcrs = yield* AckDisableVcrConfig;\n if (shouldDisableVcr(normalized.vcrName, disabledVcrs)) {\n return live;\n }\n\n const store = yield* CassetteStore;\n const writeLock = yield* Semaphore.make(1);\n\n const { name, exportKey } = yield* resolveCassetteLocation(normalized);\n\n return live.pipe(\n HttpClient.transform(\n Effect.fnUntraced(function* (effect, request) {\n const vcrRequest = toVcrRequest(request);\n if (normalized.mode === \"replay\") {\n return yield* replay(store, request, vcrRequest, normalized, name, exportKey);\n }\n\n if (normalized.mode === \"record\") {\n return yield* record(\n writeLock,\n store,\n request,\n vcrRequest,\n effect,\n normalized,\n name,\n exportKey,\n );\n }\n\n const available = yield* store\n .exists(name)\n .pipe(Effect.mapError((error) => toRequestError(request, error)));\n\n if (!available) {\n if (isCi) {\n return yield* Effect.fail(\n new HttpClientError.HttpClientError({\n reason: new HttpClientError.TransportError({\n request,\n description: \"VCR cassette missing in CI for auto mode\",\n }),\n }),\n );\n }\n\n return yield* record(\n writeLock,\n store,\n request,\n vcrRequest,\n effect,\n normalized,\n name,\n exportKey,\n );\n }\n\n const cassette = yield* readCassetteExport(store, name, exportKey, request);\n const entry = yield* findEntry(vcrRequest, cassette, normalized);\n\n if (entry) {\n return replayResponse(request, entry);\n }\n\n return yield* record(\n writeLock,\n store,\n request,\n vcrRequest,\n effect,\n normalized,\n name,\n exportKey,\n );\n }),\n ),\n );\n});\n\n/**\n * Layer that provides a VCR-wrapped HttpClient.\n */\nexport const layer = (config: VcrConfig = {}) =>\n HttpClient.layerMergedContext(makeVcrHttpClient(config));\n"],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { a as VcrConfig } from "./types-v6cSTgDp.mjs";
2
+ import { CassetteStore } from "./cassette-store.mjs";
3
+ import { Config, Path } from "effect";
4
+ import { HttpClient } from "effect/http";
5
+ declare namespace vcr_http_client_d_exports {
6
+ export { VcrHttpClientError, layer };
7
+ }
8
+ declare const VcrHttpClientError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
9
+ readonly _tag: "VcrHttpClientError";
10
+ } & Readonly<A>;
11
+ export declare class VcrHttpClientError extends VcrHttpClientError_base<{
12
+ readonly message: string;
13
+ readonly cause?: unknown;
14
+ }> {}
15
+ /**
16
+ * Layer that provides a VCR-wrapped HttpClient.
17
+ */
18
+ export declare const layer: (config?: VcrConfig) => import("effect/Layer").Layer<HttpClient.HttpClient, VcrHttpClientError | Config.ConfigError, CassetteStore | Path.Path | HttpClient.HttpClient>;
19
+ //#endregion
20
+ export { vcr_http_client_d_exports as t };
21
+ //# sourceMappingURL=vcr-http-client.d.mts.map
@@ -0,0 +1,3 @@
1
+ import "./cassette-store.mjs";
2
+ import { n as layer, t as VcrHttpClientError } from "./vcr-http-client-5K20k6US.mjs";
3
+ export { VcrHttpClientError, layer };
@@ -8,7 +8,7 @@ const getVitestState = () => {
8
8
  currentTestName: state?.currentTestName
9
9
  };
10
10
  };
11
-
12
11
  //#endregion
13
12
  export { getVitestState as t };
14
- //# sourceMappingURL=vitest-state-CiW5dzXZ.js.map
13
+
14
+ //# sourceMappingURL=vitest-state-rX5XIlH6.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vitest-state-rX5XIlH6.mjs","names":[],"sources":["../src/vitest-state.ts"],"sourcesContent":["export type VitestState = {\n readonly testPath?: string;\n readonly currentTestName?: string;\n};\n\nexport const getVitestState = (): VitestState => {\n const expectGlobal =\n // biome-ignore lint/suspicious/noExplicitAny: --\n (globalThis as { expect?: { getState?: () => any } }).expect;\n const symbolExpect =\n // biome-ignore lint/suspicious/noExplicitAny: --\n (globalThis as { [key: symbol]: { getState?: () => any } })[Symbol.for(\"expect-global\")];\n const state = expectGlobal?.getState?.() ?? symbolExpect?.getState?.();\n return {\n testPath: state?.testPath as string | undefined,\n currentTestName: state?.currentTestName as string | undefined,\n };\n};\n"],"mappings":";AAKA,MAAa,uBAAoC;CAC/C,MAAM,eAEH,WAAqD;CACxD,MAAM,eAEH,WAA2D,OAAO,IAAI,eAAe;CACxF,MAAM,QAAQ,cAAc,WAAW,KAAK,cAAc,WAAW;CACrE,OAAO;EACL,UAAU,OAAO;EACjB,iBAAiB,OAAO;CAC1B;AACF"}
package/package.json CHANGED
@@ -1,43 +1,43 @@
1
1
  {
2
2
  "name": "@useairfoil/effect-vcr",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "keywords": [
5
5
  "effect"
6
6
  ],
7
- "repository": "useairfoil/typescript-sdk",
7
+ "repository": "useairfoil/connector-kit",
8
8
  "files": [
9
9
  "dist",
10
10
  "src",
11
11
  "README.md"
12
12
  ],
13
13
  "type": "module",
14
- "main": "./dist/index.js",
15
- "types": "./dist/index.d.ts",
14
+ "main": "./dist/index.mjs",
15
+ "types": "./dist/index.d.mts",
16
16
  "exports": {
17
17
  ".": {
18
- "types": "./dist/index.d.ts",
19
- "import": "./dist/index.js",
20
- "default": "./dist/index.js"
18
+ "types": "./dist/index.d.mts",
19
+ "import": "./dist/index.mjs",
20
+ "default": "./dist/index.mjs"
21
21
  },
22
22
  "./cassette-store": {
23
- "types": "./dist/cassette-store.d.ts",
24
- "import": "./dist/cassette-store.js",
25
- "default": "./dist/cassette-store.js"
23
+ "types": "./dist/cassette-store.d.mts",
24
+ "import": "./dist/cassette-store.mjs",
25
+ "default": "./dist/cassette-store.mjs"
26
26
  },
27
27
  "./file-system-cassette-store": {
28
- "types": "./dist/file-system-cassette-store.d.ts",
29
- "import": "./dist/file-system-cassette-store.js",
30
- "default": "./dist/file-system-cassette-store.js"
28
+ "types": "./dist/file-system-cassette-store.d.mts",
29
+ "import": "./dist/file-system-cassette-store.mjs",
30
+ "default": "./dist/file-system-cassette-store.mjs"
31
31
  },
32
32
  "./types": {
33
- "types": "./dist/types.d.ts",
34
- "import": "./dist/types.js",
35
- "default": "./dist/types.js"
33
+ "types": "./dist/types.d.mts",
34
+ "import": "./dist/types.mjs",
35
+ "default": "./dist/types.mjs"
36
36
  },
37
37
  "./vcr-http-client": {
38
- "types": "./dist/vcr-http-client.d.ts",
39
- "import": "./dist/vcr-http-client.js",
40
- "default": "./dist/vcr-http-client.js"
38
+ "types": "./dist/vcr-http-client.d.mts",
39
+ "import": "./dist/vcr-http-client.mjs",
40
+ "default": "./dist/vcr-http-client.mjs"
41
41
  }
42
42
  },
43
43
  "scripts": {
@@ -50,15 +50,15 @@
50
50
  "json-stable-stringify": "^1.3.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@effect/platform-node": "^4.0.0-rc.108",
54
- "@effect/vitest": "^4.0.0-rc.108",
53
+ "@effect/platform-node": "^4.0.7",
54
+ "@effect/vitest": "^4.0.7",
55
55
  "@types/node": "^24.2.0",
56
- "effect": "^4.0.0-rc.108",
57
- "tsdown": "^0.15.6",
58
- "vitest": "^4.1.0"
56
+ "effect": "^4.0.7",
57
+ "tsdown": "^0.23.0",
58
+ "vitest": "^5.0.3"
59
59
  },
60
60
  "peerDependencies": {
61
- "effect": "^4.0.0-rc.108"
61
+ "effect": "^4.0.7"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "effect": {
package/src/sanitize.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Effect, Option, Record } from "effect";
2
- import { UrlParams } from "effect/unstable/http";
1
+ import { Array as Arr, Effect, Option, Record, Result } from "effect";
2
+ import { Url, UrlParams } from "effect/http";
3
3
  import stableStringify from "json-stable-stringify";
4
4
 
5
5
  import type { VcrRedactedValue, VcrRequest, VcrResponse } from "./types";
@@ -162,6 +162,19 @@ const transformBody = (
162
162
  );
163
163
  };
164
164
 
165
+ // A URL that does not parse is kept as is.
166
+ const omitQueryParams = (url: string, names: ReadonlyArray<string> | undefined): string =>
167
+ !names || names.length === 0
168
+ ? url
169
+ : Result.match(Url.fromString(url), {
170
+ onFailure: () => url,
171
+ onSuccess: (parsed) =>
172
+ Url.modifyUrlParams(
173
+ parsed,
174
+ UrlParams.transform(Arr.filter(([key]) => !names.includes(key))),
175
+ ).toString(),
176
+ });
177
+
165
178
  /**
166
179
  * Normalize request for matching: header canonicalization and structured body filtering.
167
180
  */
@@ -170,6 +183,7 @@ export const sanitizeRequest = (
170
183
  options: {
171
184
  readonly ignoreHeaders?: ReadonlyArray<string>;
172
185
  readonly ignoreBodyKeys?: ReadonlyArray<string>;
186
+ readonly ignoreQueryParams?: ReadonlyArray<string>;
173
187
  },
174
188
  ): VcrRequest => {
175
189
  const transformedBody = transformBody(request.body, request.headers, {
@@ -186,6 +200,7 @@ export const sanitizeRequest = (
186
200
  );
187
201
  return {
188
202
  ...request,
203
+ url: omitQueryParams(request.url, options.ignoreQueryParams),
189
204
  headers: toHeaderRecord(Option.getOrUndefined(filteredHeaders)),
190
205
  body,
191
206
  };
@@ -199,12 +214,14 @@ export const buildRequestKey = (
199
214
  options: {
200
215
  readonly ignoreHeaders?: ReadonlyArray<string>;
201
216
  readonly ignoreBodyKeys?: ReadonlyArray<string>;
217
+ readonly ignoreQueryParams?: ReadonlyArray<string>;
202
218
  },
203
219
  ): Effect.Effect<string> =>
204
220
  Effect.sync(() => {
205
221
  const sanitized = sanitizeRequest(request, {
206
222
  ignoreHeaders: options.ignoreHeaders,
207
223
  ignoreBodyKeys: options.ignoreBodyKeys,
224
+ ignoreQueryParams: options.ignoreQueryParams,
208
225
  });
209
226
  // stableStringify only returns undefined for cyclic input; this literal never is,
210
227
  // but its declared type is `string | undefined`, so keep the fallback for TS.
@@ -230,6 +247,7 @@ export const buildRequestKey = (
230
247
  export const redactRequest = (
231
248
  request: VcrRequest,
232
249
  options: {
250
+ readonly redactQueryParams?: ReadonlyArray<string>;
233
251
  readonly redactHeaders?: ReadonlyArray<string>;
234
252
  readonly redactBodyKeys?: ReadonlyArray<string>;
235
253
  readonly bodyReplacements?: Readonly<Record<string, VcrRedactedValue>>;
@@ -244,6 +262,7 @@ export const redactRequest = (
244
262
  );
245
263
  return {
246
264
  ...request,
265
+ url: omitQueryParams(request.url, options.redactQueryParams),
247
266
  headers: Option.getOrUndefined(
248
267
  omitHeaderKeys(
249
268
  request.headers,
package/src/types.ts CHANGED
@@ -80,6 +80,8 @@ export type VcrConfig = {
80
80
  * Remove or replace sensitive data before writing to disk.
81
81
  */
82
82
  readonly redact?: {
83
+ /** Removed from the recorded URL and ignored when matching. */
84
+ readonly requestQueryParams?: ReadonlyArray<string>;
83
85
  readonly requestHeaders?: ReadonlyArray<string>;
84
86
  readonly responseHeaders?: ReadonlyArray<string>;
85
87
  readonly requestBodyKeys?: ReadonlyArray<string>;