@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.
- package/README.md +31 -229
- package/dist/{cassette-store-BMtcjh5h.d.ts → cassette-store.d.mts} +13 -17
- package/dist/{cassette-store-BF9e0rz3.js → cassette-store.mjs} +6 -7
- package/dist/cassette-store.mjs.map +1 -0
- package/dist/file-system-cassette-store.d.mts +17 -0
- package/dist/{file-system-cassette-store-CyOsu3z5.js → file-system-cassette-store.mjs} +7 -8
- package/dist/file-system-cassette-store.mjs.map +1 -0
- package/dist/{index.d.ts → index.d.mts} +4 -4
- package/dist/index.mjs +5 -0
- package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
- package/dist/{types-XZehMu_v.d.ts → types-v6cSTgDp.d.mts} +3 -3
- package/dist/{types.d.ts → types.d.mts} +1 -1
- package/dist/{types-DMuz_JGd.js → types.mjs} +3 -4
- package/dist/types.mjs.map +1 -0
- package/dist/{vcr-http-client-Ba0WYPV9.js → vcr-http-client-5K20k6US.mjs} +67 -45
- package/dist/vcr-http-client-5K20k6US.mjs.map +1 -0
- package/dist/vcr-http-client.d.mts +21 -0
- package/dist/vcr-http-client.mjs +3 -0
- package/dist/{vitest-state-CiW5dzXZ.js → vitest-state-rX5XIlH6.mjs} +2 -2
- package/dist/vitest-state-rX5XIlH6.mjs.map +1 -0
- package/package.json +25 -25
- package/src/sanitize.ts +21 -2
- package/src/types.ts +2 -0
- package/src/vcr-http-client.ts +81 -33
- package/dist/cassette-store-BF9e0rz3.js.map +0 -1
- package/dist/cassette-store.d.ts +0 -3
- package/dist/cassette-store.js +0 -4
- package/dist/file-system-cassette-store-CYmQT7qj.d.ts +0 -19
- package/dist/file-system-cassette-store-CyOsu3z5.js.map +0 -1
- package/dist/file-system-cassette-store.d.ts +0 -4
- package/dist/file-system-cassette-store.js +0 -6
- package/dist/index.js +0 -7
- package/dist/types-DMuz_JGd.js.map +0 -1
- package/dist/types.js +0 -3
- package/dist/vcr-http-client-Ba0WYPV9.js.map +0 -1
- package/dist/vcr-http-client-zQZTK-ru.d.ts +0 -26
- package/dist/vcr-http-client.d.ts +0 -4
- package/dist/vcr-http-client.js +0 -6
- package/dist/vitest-state-CiW5dzXZ.js.map +0 -1
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
3
|
-
import { t as getVitestState } from "./vitest-state-
|
|
4
|
-
import { Array as Array$1, Config, Data, Effect, Option, Path, Record } from "effect";
|
|
5
|
-
import { HttpClient, HttpClientError, HttpClientResponse, UrlParams } from "effect/
|
|
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
|
|
49
|
+
const record = value;
|
|
51
50
|
const next = {};
|
|
52
51
|
let redacted = false;
|
|
53
|
-
for (const [key, entry] of Object.entries(record
|
|
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__ */
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
...cassette
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 @@
|
|
|
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.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"keywords": [
|
|
5
5
|
"effect"
|
|
6
6
|
],
|
|
7
|
-
"repository": "useairfoil/
|
|
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.
|
|
15
|
-
"types": "./dist/index.d.
|
|
14
|
+
"main": "./dist/index.mjs",
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"types": "./dist/index.d.
|
|
19
|
-
"import": "./dist/index.
|
|
20
|
-
"default": "./dist/index.
|
|
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.
|
|
24
|
-
"import": "./dist/cassette-store.
|
|
25
|
-
"default": "./dist/cassette-store.
|
|
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.
|
|
29
|
-
"import": "./dist/file-system-cassette-store.
|
|
30
|
-
"default": "./dist/file-system-cassette-store.
|
|
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.
|
|
34
|
-
"import": "./dist/types.
|
|
35
|
-
"default": "./dist/types.
|
|
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.
|
|
39
|
-
"import": "./dist/vcr-http-client.
|
|
40
|
-
"default": "./dist/vcr-http-client.
|
|
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.
|
|
54
|
-
"@effect/vitest": "^4.0.
|
|
53
|
+
"@effect/platform-node": "^4.0.7",
|
|
54
|
+
"@effect/vitest": "^4.0.7",
|
|
55
55
|
"@types/node": "^24.2.0",
|
|
56
|
-
"effect": "^4.0.
|
|
57
|
-
"tsdown": "^0.
|
|
58
|
-
"vitest": "^
|
|
56
|
+
"effect": "^4.0.7",
|
|
57
|
+
"tsdown": "^0.23.0",
|
|
58
|
+
"vitest": "^5.0.3"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"effect": "^4.0.
|
|
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/
|
|
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>;
|