@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
package/README.md CHANGED
@@ -1,247 +1,49 @@
1
- # @useairfoil/effect-vcr
1
+ # Effect VCR
2
2
 
3
- An `HttpClient` for Effect that records HTTP interactions to cassette files and replays them for future test runs.
3
+ This package records Effect HTTP requests into cassette files. Tests can replay
4
+ the same requests later.
4
5
 
5
- The first time the following test is run, it sends the HTTP request to `httpbin.org` and stores the request/response pair to a cassette. All subsequent test runs replay the stored response and make no live HTTP requests.
6
+ ## Install
6
7
 
7
- ```ts
8
- // my-program.ts
9
- import { Effect } from "effect";
10
- import { HttpClient } from "effect/unstable/http";
11
-
12
- export const program = Effect.gen(function* () {
13
- const client = yield* HttpClient.HttpClient;
14
- const response = yield* client.get("https://httpbin.org/robots.txt");
15
- return yield* response.text;
16
- });
17
-
18
- // my-program.test.ts
19
- import { NodeServices } from "@effect/platform-node";
20
- import { FileSystemCassetteStore, VcrHttpClient } from "@useairfoil/effect-vcr";
21
- import { describe, it } from "@effect/vitest";
22
- import { Effect, Layer } from "effect";
23
- import { FetchHttpClient } from "effect/unstable/http";
24
-
25
- const cassetteStoreLayer = FileSystemCassetteStore.layer().pipe(Layer.provide(NodeServices.layer));
26
-
27
- const vcrRuntimeLayer = Layer.mergeAll(
28
- FetchHttpClient.layer,
29
- NodeServices.layer,
30
- cassetteStoreLayer,
31
- );
32
-
33
- const vcrLayer = VcrHttpClient.layer({
34
- vcrName: "httpbin",
35
- mode: "auto",
36
- }).pipe(Layer.provide(vcrRuntimeLayer));
37
-
38
- describe("my awesome program", () => {
39
- it.effect("works", () => program.pipe(Effect.provide(vcrLayer)));
40
- });
8
+ ```bash
9
+ pnpm add -D @useairfoil/effect-vcr @effect/platform-node@^4 effect@^4
41
10
  ```
42
11
 
43
- The main use case for the VCR is to let open source projects run tests against a real upstream API without exposing secrets to forks.
44
-
45
- - project owners generate cassettes locally by talking to the real API
46
- - external contributors run tests against committed cassettes
47
- - CI runs deterministically from cassettes
48
- - scheduled or manual runs can still hit the live API to catch upstream drift
49
-
50
- ## Package shape
51
-
52
- Root exports:
53
-
54
- - `CassetteStore`
55
- - `FileSystemCassetteStore`
56
- - `VcrHttpClient`
57
- - `VcrConfig`
58
- - `VcrMode`
59
- - `VcrRequest`
60
- - `VcrResponse`
61
- - `VcrEntry`
62
- - `Cassette`
63
- - `CassetteFile`
64
-
65
- Focused subpath exports are also available:
66
-
67
- - `@useairfoil/effect-vcr/cassette-store`
68
- - `@useairfoil/effect-vcr/file-system-cassette-store`
69
- - `@useairfoil/effect-vcr/types`
70
- - `@useairfoil/effect-vcr/vcr-http-client`
71
-
72
12
  ## Usage
73
13
 
74
- Use the VCR by providing `VcrHttpClient.layer(...)` to the Effect being tested.
75
-
76
- - `VcrHttpClient` wraps an existing `HttpClient` and adds record/replay behavior
77
- - `FileSystemCassetteStore` persists cassette files to disk
78
-
79
- `VcrHttpClient.layer(...)` depends on:
80
-
81
- - a live `HttpClient` implementation such as `FetchHttpClient.layer`
82
- - `Path.Path` for cassette name resolution
83
- - `CassetteStore.CassetteStore` for persistence
84
-
85
- `FileSystemCassetteStore.layer(...)` depends on:
86
-
87
- - `FileSystem.FileSystem`
88
- - `Path.Path`
89
-
90
- In Node tests, `NodeServices.layer` satisfies those platform services.
91
-
92
- ## Configuration and defaults
93
-
94
- ### VcrHttpClient
95
-
96
- ```ts
97
- type VcrConfig = {
98
- readonly vcrName?: string;
99
- readonly cassetteName?: string;
100
- readonly mode?: "record" | "replay" | "auto";
101
- readonly redact?: {
102
- readonly requestHeaders?: ReadonlyArray<string>;
103
- readonly responseHeaders?: ReadonlyArray<string>;
104
- readonly requestBodyKeys?: ReadonlyArray<string>;
105
- readonly responseBodyKeys?: ReadonlyArray<string>;
106
- readonly requestBodyReplacements?: Readonly<Record<string, string | number | boolean | null>>;
107
- readonly responseBodyReplacements?: Readonly<Record<string, string | number | boolean | null>>;
108
- };
109
- readonly matchIgnore?: {
110
- readonly requestHeaders?: ReadonlyArray<string>;
111
- readonly requestBodyKeys?: ReadonlyArray<string>;
112
- };
113
- readonly match?: (request: VcrRequest, entry: VcrEntry) => boolean;
114
- };
115
- ```
116
-
117
- - `vcrName?: string`
118
- - logical VCR name used by `ACK_DISABLE_VCR`
119
- - `cassetteName?: string`
120
- - cassette file basename or full file name
121
- - `users` resolves to `users.cassette`
122
- - `users.cassette` is preserved as-is
123
- - when omitted in Vitest, the cassette file defaults to `<test-file>.cassette` and the current test name becomes the export key inside that file
124
- - `mode?: "record" | "replay" | "auto"`
125
- - `record`: always call the live client and write a cassette
126
- - `replay`: only serve from cassette; missing entries fail
127
- - `auto`: replay if the cassette exists, otherwise record; when `CI=true`, missing cassettes fail instead of recording
128
- - `redact`
129
- - remove sensitive headers or JSON and URL-encoded body keys before writing to disk
130
- - replace body values with safe placeholders when the field must remain available during replay
131
- - request fields redacted here are also ignored when building cassette keys
132
- - `matchIgnore`
133
- - ignore request headers or JSON and URL-encoded body keys when computing the request lookup key
134
- - `match`
135
- - custom request matcher for advanced lookup behavior
136
-
137
- Defaults:
138
-
139
- - `mode` defaults to `"auto"`
140
- - `authorization` is ignored for matching by default
141
- - `authorization` is redacted from recorded request headers by default
142
-
143
- ### FileSystemCassetteStore
144
-
145
- - `cassetteDir?: string`
146
- - changes the cassette root directory
147
- - when omitted in Vitest, cassettes are written to the `__cassettes__` directory beside the test file
148
- - outside Vitest, provide `cassetteDir` explicitly
149
-
150
- ## Matching and redaction
151
-
152
- `matchIgnore` and `redact` solve different problems.
153
-
154
- - `matchIgnore` changes how requests are matched to cassette entries
155
- - `redact` changes what is persisted to disk
156
-
157
- Typical usage:
158
-
159
- ```ts
160
- const vcrLayer = VcrHttpClient.layer({
161
- vcrName: "shopify-products",
162
- mode: "auto",
163
- redact: {
164
- requestHeaders: ["x-shopify-access-token", "authorization"],
165
- requestBodyReplacements: {
166
- client_secret: "client-secret-placeholder",
167
- },
168
- responseBodyReplacements: {
169
- access_token: "access-token-placeholder",
170
- },
171
- },
172
- });
173
- ```
174
-
175
- Use `matchIgnore` when a request field should not affect cassette identity.
176
-
177
- Use `redact` when a field should never be written to disk.
178
- Replacement values are useful for fields such as access tokens that a replayed
179
- response must still contain. If body redaction changes the encoded length, the VCR
180
- also removes and ignores the stale `content-length` header.
181
-
182
- ## Runtime configuration
183
-
184
- The VCR is controlled by the following environment variables:
185
-
186
- - `CI=true`
187
- - if the VCR mode is `auto`, tests with a missing cassette fail instead of recording
188
- - `ACK_DISABLE_VCR=<...>`
189
- - disables the VCR and falls back to the wrapped live client
190
- - `*` disables all configured VCRs
191
- - a comma-separated list disables only matching `vcrName` values
192
-
193
- When the VCR is disabled, cassette files are not updated.
194
-
195
- ## Common patterns
196
-
197
- ### Build the VCR runtime once
14
+ Use `VcrHttpClient.layer(...)` around the live Effect HTTP client and
15
+ `FileSystemCassetteStore.layer(...)` for cassette files.
198
16
 
199
17
  ```ts
200
- const cassetteStoreLayer = FileSystemCassetteStore.layer().pipe(Layer.provide(NodeServices.layer));
18
+ import { NodeServices } from "@effect/platform-node";
19
+ import { FileSystemCassetteStore, VcrHttpClient } from "@useairfoil/effect-vcr";
20
+ import { Layer } from "effect";
21
+ import { FetchHttpClient } from "effect/http";
201
22
 
202
- const vcrRuntimeLayer = Layer.mergeAll(
203
- FetchHttpClient.layer,
204
- NodeServices.layer,
205
- cassetteStoreLayer,
206
- );
23
+ const store = FileSystemCassetteStore.layer().pipe(Layer.provide(NodeServices.layer));
207
24
 
208
- const vcrLayer = VcrHttpClient.layer({
209
- vcrName: "example",
25
+ const VcrLive = VcrHttpClient.layer({
26
+ vcrName: "provider-api",
210
27
  mode: "auto",
211
- }).pipe(Layer.provide(vcrRuntimeLayer));
28
+ }).pipe(Layer.provide(Layer.mergeAll(FetchHttpClient.layer, NodeServices.layer, store)));
212
29
  ```
213
30
 
214
- This keeps the dependency graph explicit:
31
+ Auto mode replays a cassette if it is present. It records one if it is missing.
32
+ On CI, a missing cassette fails. `ACK_DISABLE_VCR=*` skips VCR and uses live
33
+ HTTP.
215
34
 
216
- - `FileSystemCassetteStore.layer(...)` gets its platform services from `NodeServices.layer`
217
- - `VcrHttpClient.layer(...)` gets its live client, `Path.Path`, and cassette store from `vcrRuntimeLayer`
35
+ Requests match on method, URL with its query params, headers, and body.
36
+ Authorization headers are ignored while matching and removed before a cassette
37
+ is written. Add other secrets to `redact`. Query params listed in
38
+ `redact.requestQueryParams` are removed from the cassette and ignored while
39
+ matching, which also helps when a param comes from local config.
218
40
 
219
- ### Override config in tests
220
-
221
- ```ts
222
- import { ConfigProvider, Layer } from "effect";
41
+ Requests can record at the same time. Cassette writes take turns, so no entry
42
+ is lost.
223
43
 
224
- const vcrTestRuntimeLayer = Layer.mergeAll(
225
- vcrRuntimeLayer,
226
- ConfigProvider.layer(
227
- ConfigProvider.fromUnknown({
228
- CI: false,
229
- ACK_DISABLE_VCR: "",
230
- }),
231
- ),
232
- );
233
-
234
- const vcrLayer = VcrHttpClient.layer({
235
- vcrName: "example",
236
- mode: "auto",
237
- }).pipe(Layer.provide(vcrTestRuntimeLayer));
238
- ```
239
-
240
- ### Use focused subpath imports
241
-
242
- ```ts
243
- import * as VcrHttpClient from "@useairfoil/effect-vcr/vcr-http-client";
244
- import * as FileSystemCassetteStore from "@useairfoil/effect-vcr/file-system-cassette-store";
245
- ```
44
+ ## Configuration
246
45
 
247
- Use root imports for normal package consumption and subpath imports when you want a narrower surface.
46
+ | Variable | Required | Default |
47
+ | ----------------- | -------- | ------- |
48
+ | `CI` | no | `false` |
49
+ | `ACK_DISABLE_VCR` | no | empty |
@@ -1,18 +1,14 @@
1
- import { n as CassetteFile } from "./types-XZehMu_v.js";
1
+ import { n as CassetteFile } from "./types-v6cSTgDp.mjs";
2
2
  import { Context, Effect } from "effect";
3
- import * as effect_Types0 from "effect/Types";
4
- import * as effect_Cause0 from "effect/Cause";
5
-
6
- //#region src/cassette-store.d.ts
7
3
  declare namespace cassette_store_d_exports {
8
4
  export { CassetteStore, CassetteStoreError, CassetteStoreOperation, CassetteStoreService, createEmptyCassette, createEmptyCassetteFile, make, parseCassette, toStoreError };
9
5
  }
10
- type CassetteStoreOperation = "exists" | "load" | "save" | "loadOrInit" | "resolveRoot";
6
+ export type CassetteStoreOperation = "exists" | "load" | "save" | "loadOrInit" | "resolveRoot";
11
7
  /**
12
8
  * Storage contract for VCR cassettes.
13
9
  *
14
10
  */
15
- interface CassetteStoreService {
11
+ export interface CassetteStoreService {
16
12
  /** Returns true if a cassette with the given name exists. */
17
13
  readonly exists: (name: string) => Effect.Effect<boolean, CassetteStoreError>;
18
14
  /** Loads a cassette by name, returning a `CassetteFile` if found. */
@@ -26,11 +22,11 @@ declare const CassetteStore_base: Context.ServiceClass<CassetteStore, "@useairfo
26
22
  /**
27
23
  * Effect service tag for the cassette store.
28
24
  */
29
- declare class CassetteStore extends CassetteStore_base {}
25
+ export declare class CassetteStore extends CassetteStore_base {}
30
26
  /**
31
27
  * Creates a new empty cassette with a timestamp and format version.
32
28
  */
33
- declare const createEmptyCassette: () => Effect.Effect<{
29
+ export declare const createEmptyCassette: () => Effect.Effect<{
34
30
  readonly meta: {
35
31
  readonly createdAt: string;
36
32
  readonly version: string;
@@ -55,8 +51,8 @@ declare const createEmptyCassette: () => Effect.Effect<{
55
51
  };
56
52
  };
57
53
  }, never, never>;
58
- declare const createEmptyCassetteFile: () => Effect.Effect<CassetteFile>;
59
- declare const CassetteStoreError_base: new <A extends Record<string, any> = {}>(args: effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => effect_Cause0.YieldableError & {
54
+ export declare const createEmptyCassetteFile: () => Effect.Effect<CassetteFile>;
55
+ declare const CassetteStoreError_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 & {
60
56
  readonly _tag: "CassetteStoreError";
61
57
  } & Readonly<A>;
62
58
  /**
@@ -65,19 +61,19 @@ declare const CassetteStoreError_base: new <A extends Record<string, any> = {}>(
65
61
  * `operation` indicates which store function failed.
66
62
  * `path` is the file or directory path that was targeted.
67
63
  */
68
- declare class CassetteStoreError extends CassetteStoreError_base<{
64
+ export declare class CassetteStoreError extends CassetteStoreError_base<{
69
65
  readonly operation: CassetteStoreOperation;
70
66
  readonly path: string;
71
67
  readonly message?: string;
72
68
  readonly cause?: unknown;
73
69
  }> {}
74
- declare const toStoreError: (operation: CassetteStoreOperation, path: string, cause?: unknown, message?: string) => CassetteStoreError;
70
+ export declare const toStoreError: (operation: CassetteStoreOperation, path: string, cause?: unknown, message?: string) => CassetteStoreError;
75
71
  /**
76
72
  * Parses cassette JSON and maps invalid JSON or an invalid cassette shape to a store error.
77
73
  */
78
- declare const parseCassette: (content: string, path: string) => Effect.Effect<CassetteFile, CassetteStoreError>;
74
+ export declare const parseCassette: (content: string, path: string) => Effect.Effect<CassetteFile, CassetteStoreError>;
79
75
  /** Creates a `CassetteStore` implementation that uses the given `impl` object. */
80
- declare const make: (impl: CassetteStoreService) => CassetteStoreService;
76
+ export declare const make: (impl: CassetteStoreService) => CassetteStoreService;
81
77
  //#endregion
82
- export { cassette_store_d_exports as a, make as c, CassetteStoreService as i, parseCassette as l, CassetteStoreError as n, createEmptyCassette as o, CassetteStoreOperation as r, createEmptyCassetteFile as s, CassetteStore as t, toStoreError as u };
83
- //# sourceMappingURL=cassette-store-BMtcjh5h.d.ts.map
78
+ export { cassette_store_d_exports as t };
79
+ //# sourceMappingURL=cassette-store.d.mts.map
@@ -1,9 +1,8 @@
1
- import { t as __export } from "./chunk-Bp6m_JJh.js";
2
- import { t as CassetteFileSchema } from "./types-DMuz_JGd.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { CassetteFileSchema } from "./types.mjs";
3
3
  import { Context, Data, DateTime, Effect, Schema } from "effect";
4
-
5
4
  //#region src/cassette-store.ts
6
- var cassette_store_exports = /* @__PURE__ */ __export({
5
+ var cassette_store_exports = /* @__PURE__ */ __exportAll({
7
6
  CassetteStore: () => CassetteStore,
8
7
  CassetteStoreError: () => CassetteStoreError,
9
8
  createEmptyCassette: () => createEmptyCassette,
@@ -52,7 +51,7 @@ const parseCassette = (content, path) => Effect.try({
52
51
  }).pipe(Effect.flatMap((parsed) => Schema.decodeUnknownEffect(CassetteFileSchema)(parsed).pipe(Effect.mapError((error) => toStoreError("load", path, error, "Invalid cassette file format")))));
53
52
  /** Creates a `CassetteStore` implementation that uses the given `impl` object. */
54
53
  const make = (impl) => CassetteStore.of({ ...impl });
55
-
56
54
  //#endregion
57
- export { createEmptyCassetteFile as a, toStoreError as c, createEmptyCassette as i, CassetteStoreError as n, make as o, cassette_store_exports as r, parseCassette as s, CassetteStore as t };
58
- //# sourceMappingURL=cassette-store-BF9e0rz3.js.map
55
+ export { CassetteStore, CassetteStoreError, createEmptyCassette, createEmptyCassetteFile, make, parseCassette, cassette_store_exports as t, toStoreError };
56
+
57
+ //# sourceMappingURL=cassette-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cassette-store.mjs","names":[],"sources":["../src/cassette-store.ts"],"sourcesContent":["import { Context, Data, DateTime, Effect, Schema } from \"effect\";\n\nimport { CassetteFileSchema, type Cassette, type CassetteFile } from \"./types\";\n\nexport type CassetteStoreOperation = \"exists\" | \"load\" | \"save\" | \"loadOrInit\" | \"resolveRoot\";\n\n/**\n * Storage contract for VCR cassettes.\n *\n */\nexport interface CassetteStoreService {\n /** Returns true if a cassette with the given name exists. */\n readonly exists: (name: string) => Effect.Effect<boolean, CassetteStoreError>;\n\n /** Loads a cassette by name, returning a `CassetteFile` if found. */\n readonly load: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n\n /** Saves a cassette by name, overwriting any existing cassette with the same name. */\n readonly save: (name: string, content: CassetteFile) => Effect.Effect<void, CassetteStoreError>;\n\n /** Loads a cassette by name, initializing a new empty cassette if not found. */\n readonly loadOrInit: (name: string) => Effect.Effect<CassetteFile, CassetteStoreError>;\n}\n\n/**\n * Effect service tag for the cassette store.\n */\nexport class CassetteStore extends Context.Service<CassetteStore, CassetteStoreService>()(\n \"@useairfoil/effect-vcr/CassetteStore\",\n) {}\n\n/**\n * Creates a new empty cassette with a timestamp and format version.\n */\nexport const createEmptyCassette = Effect.fnUntraced(function* (): Effect.fn.Return<Cassette> {\n const now = yield* DateTime.now;\n return {\n meta: {\n createdAt: DateTime.formatIso(now),\n version: \"1\",\n },\n entries: {},\n };\n});\n\nexport const createEmptyCassetteFile = (): Effect.Effect<CassetteFile> =>\n Effect.map(createEmptyCassette(), (cassette) => ({\n exports: { default: cassette },\n }));\n\n/**\n * Structured error for cassette persistence operations.\n *\n * `operation` indicates which store function failed.\n * `path` is the file or directory path that was targeted.\n */\nexport class CassetteStoreError extends Data.TaggedError(\"CassetteStoreError\")<{\n readonly operation: CassetteStoreOperation;\n readonly path: string;\n readonly message?: string;\n readonly cause?: unknown;\n}> {}\n\nexport const toStoreError = (\n operation: CassetteStoreOperation,\n path: string,\n cause?: unknown,\n message?: string,\n) =>\n new CassetteStoreError({\n operation,\n path,\n cause,\n message,\n });\n\n/**\n * Parses cassette JSON and maps invalid JSON or an invalid cassette shape to a store error.\n */\nexport const parseCassette = (\n content: string,\n path: string,\n): Effect.Effect<CassetteFile, CassetteStoreError> =>\n Effect.try({\n try: () => JSON.parse(content) as unknown,\n catch: (error) => toStoreError(\"load\", path, error, \"Invalid JSON\"),\n }).pipe(\n Effect.flatMap((parsed) =>\n Schema.decodeUnknownEffect(CassetteFileSchema)(parsed).pipe(\n Effect.mapError((error) =>\n toStoreError(\"load\", path, error, \"Invalid cassette file format\"),\n ),\n ),\n ),\n );\n\n/** Creates a `CassetteStore` implementation that uses the given `impl` object. */\nexport const make = (impl: CassetteStoreService): CassetteStoreService =>\n CassetteStore.of({ ...impl });\n"],"mappings":";;;;;;;;;;;;;;;;AA2BA,IAAa,gBAAb,cAAmC,QAAQ,QAA6C,CAAC,CACvF,sCACF,CAAC,CAAC,CAAC;;;;AAKH,MAAa,sBAAsB,OAAO,WAAW,aAAyC;CAC5F,MAAM,MAAM,OAAO,SAAS;CAC5B,OAAO;EACL,MAAM;GACJ,WAAW,SAAS,UAAU,GAAG;GACjC,SAAS;EACX;EACA,SAAS,CAAC;CACZ;AACF,CAAC;AAED,MAAa,gCACX,OAAO,IAAI,oBAAoB,IAAI,cAAc,EAC/C,SAAS,EAAE,SAAS,SAAS,EAC/B,EAAE;;;;;;;AAQJ,IAAa,qBAAb,cAAwC,KAAK,YAAY,oBAAoB,CAAC,CAK3E,CAAC;AAEJ,MAAa,gBACX,WACA,MACA,OACA,YAEA,IAAI,mBAAmB;CACrB;CACA;CACA;CACA;AACF,CAAC;;;;AAKH,MAAa,iBACX,SACA,SAEA,OAAO,IAAI;CACT,WAAW,KAAK,MAAM,OAAO;CAC7B,QAAQ,UAAU,aAAa,QAAQ,MAAM,OAAO,cAAc;AACpE,CAAC,CAAC,CAAC,KACD,OAAO,SAAS,WACd,OAAO,oBAAoB,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,KACrD,OAAO,UAAU,UACf,aAAa,QAAQ,MAAM,OAAO,8BAA8B,CAClE,CACF,CACF,CACF;;AAGF,MAAa,QAAQ,SACnB,cAAc,GAAG,EAAE,GAAG,KAAK,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { CassetteStore, CassetteStoreError, t as cassette_store_d_exports } from "./cassette-store.mjs";
2
+ import { FileSystem, Layer, Path } from "effect";
3
+ declare namespace file_system_cassette_store_d_exports {
4
+ export { FileSystemCassetteStoreConfig, layer };
5
+ }
6
+ export type FileSystemCassetteStoreConfig = {
7
+ readonly cassetteDir?: string;
8
+ };
9
+ /**
10
+ * FileSystem-backed cassette store.
11
+ *
12
+ * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.
13
+ */
14
+ export declare const layer: (config?: FileSystemCassetteStoreConfig) => Layer.Layer<CassetteStore, CassetteStoreError, FileSystem.FileSystem | Path.Path>;
15
+ //#endregion
16
+ export { file_system_cassette_store_d_exports as t };
17
+ //# sourceMappingURL=file-system-cassette-store.d.mts.map
@@ -1,10 +1,9 @@
1
- import { t as __export } from "./chunk-Bp6m_JJh.js";
2
- import { a as createEmptyCassetteFile, c as toStoreError, o as make, s as parseCassette, t as CassetteStore } from "./cassette-store-BF9e0rz3.js";
3
- import { t as getVitestState } from "./vitest-state-CiW5dzXZ.js";
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { CassetteStore, createEmptyCassetteFile, make, parseCassette, toStoreError } from "./cassette-store.mjs";
3
+ import { t as getVitestState } from "./vitest-state-rX5XIlH6.mjs";
4
4
  import { Effect, FileSystem, Layer, Path } from "effect";
5
-
6
5
  //#region src/file-system-cassette-store.ts
7
- var file_system_cassette_store_exports = /* @__PURE__ */ __export({ layer: () => layer });
6
+ var file_system_cassette_store_exports = /* @__PURE__ */ __exportAll({ layer: () => layer });
8
7
  /**
9
8
  * FileSystem-backed cassette store.
10
9
  *
@@ -36,7 +35,7 @@ const layer = (config = {}) => Layer.effect(CassetteStore)(Effect.gen(function*
36
35
  loadOrInit
37
36
  });
38
37
  }));
39
-
40
38
  //#endregion
41
- export { layer as n, file_system_cassette_store_exports as t };
42
- //# sourceMappingURL=file-system-cassette-store-CyOsu3z5.js.map
39
+ export { layer, file_system_cassette_store_exports as t };
40
+
41
+ //# sourceMappingURL=file-system-cassette-store.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-system-cassette-store.mjs","names":["CassetteStore.CassetteStore","CassetteStore.toStoreError","CassetteStore.parseCassette","CassetteStore.createEmptyCassetteFile","CassetteStore.make"],"sources":["../src/file-system-cassette-store.ts"],"sourcesContent":["import { Effect, Layer, FileSystem, Path } from \"effect\";\n\nimport type { CassetteFile } from \"./types\";\n\nimport * as CassetteStore from \"./cassette-store\";\nimport { getVitestState } from \"./vitest-state\";\n\nexport type FileSystemCassetteStoreConfig = {\n readonly cassetteDir?: string;\n};\n\n/**\n * FileSystem-backed cassette store.\n *\n * Requires a platform FileSystem layer (Node/Bun) to be provided by the user.\n */\nexport const layer = (config: FileSystemCassetteStoreConfig = {}) =>\n Layer.effect(CassetteStore.CassetteStore)(\n Effect.gen(function* () {\n const fs = yield* FileSystem.FileSystem;\n const path = yield* Path.Path;\n\n let rootDir: string;\n if (config.cassetteDir) {\n rootDir = config.cassetteDir;\n } else {\n const { testPath } = getVitestState();\n if (!testPath) {\n return yield* Effect.fail(\n CassetteStore.toStoreError(\n \"resolveRoot\",\n \"__cassettes__\",\n undefined,\n \"VCR cassette directory could not be inferred. Provide cassetteDir when not running in Vitest.\",\n ),\n );\n }\n const testFolder = path.dirname(testPath);\n rootDir = path.join(testFolder, \"__cassettes__\");\n }\n\n const fullPath = (name: string) => path.join(rootDir, name);\n\n const load = (name: string) =>\n fs.readFileString(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"load\", name, error)),\n Effect.flatMap((content) => CassetteStore.parseCassette(content, name)),\n );\n\n const save = (name: string, cassette: CassetteFile) => {\n const filePath = fullPath(name);\n const dir = path.dirname(filePath);\n const ensureDir =\n dir === \"\"\n ? Effect.void\n : fs\n .makeDirectory(dir, { recursive: true })\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", dir, error)));\n return ensureDir.pipe(\n Effect.andThen(\n fs\n .writeFileString(filePath, JSON.stringify(cassette, null, 2))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"save\", name, error))),\n ),\n );\n };\n\n const loadOrInit = (name: string) =>\n fs.exists(fullPath(name)).pipe(\n Effect.mapError((error) => CassetteStore.toStoreError(\"loadOrInit\", name, error)),\n Effect.flatMap((exists) =>\n exists\n ? load(name)\n : CassetteStore.createEmptyCassetteFile().pipe(\n Effect.tap((cassette) => save(name, cassette)),\n ),\n ),\n );\n\n return CassetteStore.make({\n exists: (name: string) =>\n fs\n .exists(fullPath(name))\n .pipe(Effect.mapError((error) => CassetteStore.toStoreError(\"exists\", name, error))),\n load,\n save,\n loadOrInit,\n });\n }),\n );\n"],"mappings":";;;;;;;;;;;AAgBA,MAAa,SAAS,SAAwC,CAAC,MAC7D,MAAM,OAAOA,aAA2B,CAAC,CACvC,OAAO,IAAI,aAAa;CACtB,MAAM,KAAK,OAAO,WAAW;CAC7B,MAAM,OAAO,OAAO,KAAK;CAEzB,IAAI;CACJ,IAAI,OAAO,aACT,UAAU,OAAO;MACZ;EACL,MAAM,EAAE,aAAa,eAAe;EACpC,IAAI,CAAC,UACH,OAAO,OAAO,OAAO,KACnBC,aACE,eACA,iBACA,KAAA,GACA,+FACF,CACF;EAEF,MAAM,aAAa,KAAK,QAAQ,QAAQ;EACxC,UAAU,KAAK,KAAK,YAAY,eAAe;CACjD;CAEA,MAAM,YAAY,SAAiB,KAAK,KAAK,SAAS,IAAI;CAE1D,MAAM,QAAQ,SACZ,GAAG,eAAe,SAAS,IAAI,CAAC,CAAC,CAAC,KAChC,OAAO,UAAU,UAAUA,aAA2B,QAAQ,MAAM,KAAK,CAAC,GAC1E,OAAO,SAAS,YAAYC,cAA4B,SAAS,IAAI,CAAC,CACxE;CAEF,MAAM,QAAQ,MAAc,aAA2B;EACrD,MAAM,WAAW,SAAS,IAAI;EAC9B,MAAM,MAAM,KAAK,QAAQ,QAAQ;EAOjC,QALE,QAAQ,KACJ,OAAO,OACP,GACG,cAAc,KAAK,EAAE,WAAW,KAAK,CAAC,CAAC,CACvC,KAAK,OAAO,UAAU,UAAUD,aAA2B,QAAQ,KAAK,KAAK,CAAC,CAAC,EAAA,CACvE,KACf,OAAO,QACL,GACG,gBAAgB,UAAU,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC,CAAC,CAC5D,KAAK,OAAO,UAAU,UAAUA,aAA2B,QAAQ,MAAM,KAAK,CAAC,CAAC,CACrF,CACF;CACF;CAEA,MAAM,cAAc,SAClB,GAAG,OAAO,SAAS,IAAI,CAAC,CAAC,CAAC,KACxB,OAAO,UAAU,UAAUA,aAA2B,cAAc,MAAM,KAAK,CAAC,GAChF,OAAO,SAAS,WACd,SACI,KAAK,IAAI,IACTE,wBAAsC,CAAC,CAAC,KACtC,OAAO,KAAK,aAAa,KAAK,MAAM,QAAQ,CAAC,CAC/C,CACN,CACF;CAEF,OAAOC,KAAmB;EACxB,SAAS,SACP,GACG,OAAO,SAAS,IAAI,CAAC,CAAC,CACtB,KAAK,OAAO,UAAU,UAAUH,aAA2B,UAAU,MAAM,KAAK,CAAC,CAAC;EACvF;EACA;EACA;CACF,CAAC;AACH,CAAC,CACH"}
@@ -1,5 +1,5 @@
1
- import { a as VcrConfig, c as VcrMode, d as VcrRequestSchema, f as VcrResponse, i as CassetteSchema, l as VcrRedactedValue, n as CassetteFile, o as VcrEntry, p as VcrResponseSchema, r as CassetteFileSchema, s as VcrEntrySchema, t as Cassette, u as VcrRequest } from "./types-XZehMu_v.js";
2
- import { a as cassette_store_d_exports } from "./cassette-store-BMtcjh5h.js";
3
- import { n as file_system_cassette_store_d_exports } from "./file-system-cassette-store-CYmQT7qj.js";
4
- import { r as vcr_http_client_d_exports } from "./vcr-http-client-zQZTK-ru.js";
1
+ import { a as VcrConfig, c as VcrMode, d as VcrRequestSchema, f as VcrResponse, i as CassetteSchema, l as VcrRedactedValue, n as CassetteFile, o as VcrEntry, p as VcrResponseSchema, r as CassetteFileSchema, s as VcrEntrySchema, t as Cassette, u as VcrRequest } from "./types-v6cSTgDp.mjs";
2
+ import { t as cassette_store_d_exports } from "./cassette-store.mjs";
3
+ import { t as file_system_cassette_store_d_exports } from "./file-system-cassette-store.mjs";
4
+ import { t as vcr_http_client_d_exports } from "./vcr-http-client.mjs";
5
5
  export { Cassette, CassetteFile, CassetteFileSchema, CassetteSchema, cassette_store_d_exports as CassetteStore, file_system_cassette_store_d_exports as FileSystemCassetteStore, VcrConfig, VcrEntry, VcrEntrySchema, vcr_http_client_d_exports as VcrHttpClient, VcrMode, VcrRedactedValue, VcrRequest, VcrRequestSchema, VcrResponse, VcrResponseSchema };
package/dist/index.mjs ADDED
@@ -0,0 +1,5 @@
1
+ import { CassetteFileSchema, CassetteSchema, VcrEntrySchema, VcrRequestSchema, VcrResponseSchema } from "./types.mjs";
2
+ import { t as cassette_store_exports } from "./cassette-store.mjs";
3
+ import { t as file_system_cassette_store_exports } from "./file-system-cassette-store.mjs";
4
+ import { r as vcr_http_client_exports } from "./vcr-http-client-5K20k6US.mjs";
5
+ export { CassetteFileSchema, CassetteSchema, cassette_store_exports as CassetteStore, file_system_cassette_store_exports as FileSystemCassetteStore, VcrEntrySchema, vcr_http_client_exports as VcrHttpClient, VcrRequestSchema, VcrResponseSchema };
@@ -1,13 +1,13 @@
1
- //#region rolldown:runtime
1
+ //#region \0rolldown/runtime.js
2
2
  var __defProp = Object.defineProperty;
3
- var __export = (all) => {
3
+ var __exportAll = (all, no_symbols) => {
4
4
  let target = {};
5
5
  for (var name in all) __defProp(target, name, {
6
6
  get: all[name],
7
7
  enumerable: true
8
8
  });
9
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
9
10
  return target;
10
11
  };
11
-
12
12
  //#endregion
13
- export { __export as t };
13
+ export { __exportAll as t };
@@ -1,7 +1,5 @@
1
1
  import { Schema } from "effect";
2
-
3
2
  //#region src/types.d.ts
4
-
5
3
  /**
6
4
  * VCR operating mode.
7
5
  * - record: always call the live client and persist.
@@ -114,6 +112,8 @@ type VcrConfig = {
114
112
  * Remove or replace sensitive data before writing to disk.
115
113
  */
116
114
  readonly redact?: {
115
+ /** Removed from the recorded URL and ignored when matching. */
116
+ readonly requestQueryParams?: ReadonlyArray<string>;
117
117
  readonly requestHeaders?: ReadonlyArray<string>;
118
118
  readonly responseHeaders?: ReadonlyArray<string>;
119
119
  readonly requestBodyKeys?: ReadonlyArray<string>;
@@ -135,4 +135,4 @@ type VcrConfig = {
135
135
  };
136
136
  //#endregion
137
137
  export { VcrConfig as a, VcrMode as c, VcrRequestSchema as d, VcrResponse as f, CassetteSchema as i, VcrRedactedValue as l, CassetteFile as n, VcrEntry as o, VcrResponseSchema as p, CassetteFileSchema as r, VcrEntrySchema as s, Cassette as t, VcrRequest as u };
138
- //# sourceMappingURL=types-XZehMu_v.d.ts.map
138
+ //# sourceMappingURL=types-v6cSTgDp.d.mts.map
@@ -1,2 +1,2 @@
1
- import { a as VcrConfig, c as VcrMode, d as VcrRequestSchema, f as VcrResponse, i as CassetteSchema, l as VcrRedactedValue, n as CassetteFile, o as VcrEntry, p as VcrResponseSchema, r as CassetteFileSchema, s as VcrEntrySchema, t as Cassette, u as VcrRequest } from "./types-XZehMu_v.js";
1
+ import { a as VcrConfig, c as VcrMode, d as VcrRequestSchema, f as VcrResponse, i as CassetteSchema, l as VcrRedactedValue, n as CassetteFile, o as VcrEntry, p as VcrResponseSchema, r as CassetteFileSchema, s as VcrEntrySchema, t as Cassette, u as VcrRequest } from "./types-v6cSTgDp.mjs";
2
2
  export { Cassette, CassetteFile, CassetteFileSchema, CassetteSchema, VcrConfig, VcrEntry, VcrEntrySchema, VcrMode, VcrRedactedValue, VcrRequest, VcrRequestSchema, VcrResponse, VcrResponseSchema };
@@ -1,5 +1,4 @@
1
1
  import { Schema } from "effect";
2
-
3
2
  //#region src/types.ts
4
3
  /**
5
4
  * Serialized request shape stored in a cassette.
@@ -39,7 +38,7 @@ const CassetteSchema = Schema.Struct({
39
38
  * Cassette file format with multiple exports.
40
39
  */
41
40
  const CassetteFileSchema = Schema.Struct({ exports: Schema.Record(Schema.String, CassetteSchema) });
42
-
43
41
  //#endregion
44
- export { VcrResponseSchema as a, VcrRequestSchema as i, CassetteSchema as n, VcrEntrySchema as r, CassetteFileSchema as t };
45
- //# sourceMappingURL=types-DMuz_JGd.js.map
42
+ export { CassetteFileSchema, CassetteSchema, VcrEntrySchema, VcrRequestSchema, VcrResponseSchema };
43
+
44
+ //# sourceMappingURL=types.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.mjs","names":[],"sources":["../src/types.ts"],"sourcesContent":["import { Schema } from \"effect\";\n\n/**\n * VCR operating mode.\n * - record: always call the live client and persist.\n * - replay: serve only from cassette, fail if missing.\n * - auto: replay if cassette exists, otherwise record (fail in CI).\n */\nexport type VcrMode = \"record\" | \"replay\" | \"auto\";\n\n/**\n * Safe scalar value written in place of a sensitive body field.\n */\nexport type VcrRedactedValue = string | number | boolean | null;\n\n/**\n * Serialized request shape stored in a cassette.\n */\nexport const VcrRequestSchema = Schema.Struct({\n method: Schema.String,\n url: Schema.String,\n headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),\n body: Schema.optional(Schema.String),\n});\nexport type VcrRequest = Schema.Schema.Type<typeof VcrRequestSchema>;\n\n/**\n * Serialized response shape stored in a cassette.\n */\nexport const VcrResponseSchema = Schema.Struct({\n status: Schema.Number,\n body: Schema.String,\n headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),\n});\nexport type VcrResponse = Schema.Schema.Type<typeof VcrResponseSchema>;\n\n/**\n * A single request/response pair keyed in the cassette.\n */\nexport const VcrEntrySchema = Schema.Struct({\n request: VcrRequestSchema,\n response: VcrResponseSchema,\n});\nexport type VcrEntry = Schema.Schema.Type<typeof VcrEntrySchema>;\n\n/**\n * Cassette file format.\n */\nexport const CassetteSchema = Schema.Struct({\n meta: Schema.Struct({\n createdAt: Schema.String,\n version: Schema.String,\n }),\n entries: Schema.Record(Schema.String, VcrEntrySchema),\n});\nexport type Cassette = Schema.Schema.Type<typeof CassetteSchema>;\n\n/**\n * Cassette file format with multiple exports.\n */\nexport const CassetteFileSchema = Schema.Struct({\n exports: Schema.Record(Schema.String, CassetteSchema),\n});\nexport type CassetteFile = Schema.Schema.Type<typeof CassetteFileSchema>;\n\n/**\n * VCR configuration.\n */\nexport type VcrConfig = {\n readonly vcrName?: string;\n /**\n * Cassette file name or basename.\n *\n * `users` resolves to `users.cassette`.\n * `users.cassette` is preserved as-is.\n */\n readonly cassetteName?: string;\n readonly mode?: VcrMode;\n /**\n * Remove or replace sensitive data before writing to disk.\n */\n readonly redact?: {\n /** Removed from the recorded URL and ignored when matching. */\n readonly requestQueryParams?: ReadonlyArray<string>;\n readonly requestHeaders?: ReadonlyArray<string>;\n readonly responseHeaders?: ReadonlyArray<string>;\n readonly requestBodyKeys?: ReadonlyArray<string>;\n readonly responseBodyKeys?: ReadonlyArray<string>;\n readonly requestBodyReplacements?: Readonly<Record<string, VcrRedactedValue>>;\n readonly responseBodyReplacements?: Readonly<Record<string, VcrRedactedValue>>;\n };\n /**\n * Ignore fields when computing the request key for replay matching.\n */\n readonly matchIgnore?: {\n readonly requestHeaders?: ReadonlyArray<string>;\n readonly requestBodyKeys?: ReadonlyArray<string>;\n };\n /**\n * Custom matcher to override key-based lookup.\n */\n readonly match?: (request: VcrRequest, entry: VcrEntry) => boolean;\n};\n"],"mappings":";;;;;AAkBA,MAAa,mBAAmB,OAAO,OAAO;CAC5C,QAAQ,OAAO;CACf,KAAK,OAAO;CACZ,SAAS,OAAO,SAAS,OAAO,OAAO,OAAO,QAAQ,OAAO,MAAM,CAAC;CACpE,MAAM,OAAO,SAAS,OAAO,MAAM;AACrC,CAAC;;;;AAMD,MAAa,oBAAoB,OAAO,OAAO;CAC7C,QAAQ,OAAO;CACf,MAAM,OAAO;CACb,SAAS,OAAO,SAAS,OAAO,OAAO,OAAO,QAAQ,OAAO,MAAM,CAAC;AACtE,CAAC;;;;AAMD,MAAa,iBAAiB,OAAO,OAAO;CAC1C,SAAS;CACT,UAAU;AACZ,CAAC;;;;AAMD,MAAa,iBAAiB,OAAO,OAAO;CAC1C,MAAM,OAAO,OAAO;EAClB,WAAW,OAAO;EAClB,SAAS,OAAO;CAClB,CAAC;CACD,SAAS,OAAO,OAAO,OAAO,QAAQ,cAAc;AACtD,CAAC;;;;AAMD,MAAa,qBAAqB,OAAO,OAAO,EAC9C,SAAS,OAAO,OAAO,OAAO,QAAQ,cAAc,EACtD,CAAC"}