counterfact 2.12.0 → 2.14.1

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 CHANGED
@@ -1,61 +1,77 @@
1
1
  <div align="center" markdown="1">
2
2
 
3
- <h1><img src="./counterfact.svg" alt="Counterfact" border=0></h1>
3
+ <h1><img src="./counterfact.svg" alt="Counterfact" border="0"></h1>
4
4
 
5
- <br>
5
+ ![MIT License](https://img.shields.io/badge/license-MIT-blue) [![Coverage Status](https://coveralls.io/repos/github/counterfact/api-simulator/badge.svg)](https://coveralls.io/github/counterfact/api-simulator) ![Swagger 2.0](https://img.shields.io/badge/Swagger-2.0-85EA2D) ![OpenAPI 3.0–3.2](https://img.shields.io/badge/OpenAPI-3.x-6BA539) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/counterfact/api-simulator)
6
6
 
7
- ![MIT License](https://img.shields.io/badge/license-MIT-blue) [![Coverage Status](https://coveralls.io/repos/github/counterfact/api-simulator/badge.svg)](https://coveralls.io/github/pmcelhaney/counterfact) ![friction 0%](https://img.shields.io/badge/friction-0%25-brightgreen) ![Swagger 2.0](https://img.shields.io/badge/Swagger-2.0-85EA2D) ![OpenAPI 3.0-3.2](https://img.shields.io/badge/OpenAPI-3.x-6BA539)
7
+ ## Turn an OpenAPI document into an editable, stateful local API.
8
8
 
9
9
  </div>
10
10
 
11
- <div align="center" markdown="1">
12
- <h2>Mock servers work—until you need state, failures, or control mid-run.</h2>
13
- </div
11
+ Counterfact generates TypeScript handlers and request/response types for supported OpenAPI operations, then starts a local server. Begin with schema-derived sample responses; add shared state, failures, latency, middleware, or selective proxying as your workflow needs them. Route and context files hot-reload while their in-memory state stays available.
14
12
 
15
- Static responses aren’t enough. There’s no shared state. You can’t inject failures. You can’t test real workflows.<br>
16
- Mock servers make it easy to get started, but hard to keep going.<br>
17
- Counterfact is an API simulator without those limits.
13
+ Counterfact checks contract shape while giving you a path to shape the behavior. Start with schema-derived responses, then turn them into deterministic, stateful scenarios by editing the generated typed handlers and adding shared context. You can explore in seconds and keep the behavior you author as a repeatable local or CI fixture.
18
14
 
19
- Point it at an [OpenAPI](https://www.openapis.org) document and get a live, stateful API in seconds.
20
- Supports Swagger 2.0 and OpenAPI 3.0, 3.1, and 3.2.
21
- - Type-safe TypeScript handlers for every endpoint
22
- - Hot reloading as you edit
23
- - Shared state across routes
24
- - **A built-in REPL to control behavior at runtime**
25
- - Optional proxying to real backends
15
+ ## Run a live API in 60 seconds
26
16
 
27
- Flexbile for humans. Stable for [AI agents](https://github.com/counterfact/api-simulator/blob/main/docs/patterns/ai-assisted-implementation.md).
17
+ ```sh
18
+ npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
19
+ ```
28
20
 
29
- You’re in control—without restarting.
21
+ This generates editable files in `api/`, starts the server at `http://localhost:3100`, serves Swagger UI at `http://localhost:3100/counterfact/swagger/`, and opens the live REPL. Exit with `.exit`, <kbd>Ctrl</kbd>+<kbd>D</kbd>, or <kbd>Ctrl</kbd>+<kbd>C</kbd> twice.
30
22
 
31
- For a *frontend developer* waiting on a backend,<br>
32
- a *test engineer* who needs clean, reproducible state,<br>
33
- or an *AI agent* that needs a stable API
23
+ Requires Node.js 22 or newer. For a repeatable project or CI workflow, install Counterfact as a dev dependency and commit the lockfile:
34
24
 
35
- Real enough to be useful. Fake enough to be usable.
25
+ ```sh
26
+ npm install --save-dev counterfact
27
+ npx counterfact ./openapi.yaml api
28
+ ```
36
29
 
30
+ ## Start with the workflow you need
37
31
 
38
- ## Try it now
32
+ | When you need to… | You can… | Start here |
33
+ | -------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
34
+ | Build ahead of your backend | Create a record, read it later, and shape the failure paths | [Build a stateful flow](./docs/first-10-minutes.md#build-a-stateful-flow) |
35
+ | Make failures repeatable | Reset, fail, recover, and tear down the same way every run | [Automate a deterministic test](./docs/first-10-minutes.md#automate-a-deterministic-test) |
36
+ | Give a coding agent a reliable sandbox | Leave a resettable context and an HTTP test for review | [Give an agent a verifiable task](./docs/first-10-minutes.md#give-an-agent-a-verifiable-task) |
37
+ | Bring endpoints online gradually | Keep one client base URL while paths move from local to upstream | [Run the checked hybrid path](./examples/first-10-minutes/#hybrid-path-ownership) |
38
+ | Keep the contract in the loop | Regenerate types and catch drift in your normal type check | [See a contract change reach a client](./examples/first-10-minutes/#see-a-contract-change-reach-the-client) |
39
39
 
40
- ```sh
41
- npx counterfact@latest https://petstore3.swagger.io/api/v3/openapi.json api
42
- ```
40
+ The [first-10-minutes guide](./docs/first-10-minutes.md) includes the shared setup, reset and isolation rules, CI lifecycle, and the boundary between contract checks and behavior realism.
41
+
42
+ Prefer a complete artifact to copy? The [CI-checked first-10-minutes example](./examples/first-10-minutes/) contains a minimal OpenAPI contract, resettable context, typed handlers, real-HTTP test, lockfile, and type-check configuration.
43
+
44
+ ## Confidence you can build on
45
+
46
+ | Where Counterfact helps | What you get | What remains yours |
47
+ | ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
48
+ | Generated TypeScript types | Supported status codes, media types, declared headers, and request/response shapes when your project runs a type check | The business rules that make the API truly yours |
49
+ | Request validation | Required query, header, and cookie parameters plus supported JSON/form bodies; detected mismatches return `400` | Authentication, authorization, path-parameter schemas, and business rules |
50
+ | Response checks | Required response headers and their schema types, with advisory `response-type-error` details when something is off | Response-body enforcement and production correctness |
51
+ | Your handlers, context, and scenarios | The state, rules, failures, reset behavior, and deterministic fixtures your workflow needs | Targeted real-backend and end-to-end coverage for the real service |
52
+
53
+ Counterfact gives you a fast, contract-shaped world to build against. Keep targeted real-backend and end-to-end coverage for the parts only the real service can prove. See [how to make the workflow yours](./docs/first-10-minutes.md#make-it-yours-with-confidence) for the practical checklist.
54
+
55
+ ## Project lifecycle
43
56
 
44
- > Starts a local server with a live REPL to inspect and control API behavior
45
- > Requires Node 22.0.0
57
+ - Commit your OpenAPI document, editable `routes/`, scenarios, and usually the generated `types/` so collaborators share a working contract snapshot.
58
+ - Regeneration overwrites generated types and may append scaffolding for newly added operations; it preserves existing handler bodies unless you explicitly use a destructive option such as `--prune`.
59
+ - In-memory context survives hot reload. A new process creates fresh context, and its constructor or `startup` scenario defines the initial state.
60
+ - Counterfact executes TypeScript route files, but it does not run a TypeScript type-check step for you. Include generated files in your project and run your normal type check in CI.
61
+ - Use a user-defined `reset()` in test setup, and use separate server instances and ports for parallel workers.
46
62
 
47
- ## Go deeper
63
+ ## Documentation
48
64
 
49
- - [Getting started](./docs/getting-started.md) – Detailed walkthrough with state, REPL, and proxy
50
- - [Patterns](./docs/patterns/index.md) – How Counterfact transforms your workflow
51
- - [Example repo](https://github.com/counterfact/example-petstore) – Using Counterfact to implement the Swagger Petstore
52
- - [How it compares](./docs/comparison.md) – json-server, WireMock, Prism, Microcks, MSW
53
- - [Usage](./docs/usage.md) – Explore features and how to use them
54
- - [Reference](./docs/reference.md) – `$` API, CLI flags, architecture
55
- - [FAQ](./docs/faq.md) – State, types, regeneration
65
+ - [Getting started](./docs/getting-started.md) – Generated files, state, REPL, proxying, and spec changes
66
+ - [Usage](./docs/usage.md) – Feature map
67
+ - [Patterns](./docs/patterns/index.md) – Reusable development and testing workflows
68
+ - [Reference](./docs/reference.md) – `$` API, CLI flags, and architecture
69
+ - [FAQ](./docs/faq.md) – State, types, validation, and regeneration
70
+ - [How it compares](./docs/comparison.md) – json-server, WireMock, Prism, Microcks, and MSW
71
+ - [Example repository](https://github.com/counterfact/example-petstore) – A larger Petstore implementation
56
72
 
57
73
  <div align="center" markdown="1">
58
74
 
59
- [Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md)
75
+ [Changelog](./CHANGELOG.md) · [Contributing](./CONTRIBUTING.md) · [Security](./SECURITY.md)
60
76
 
61
77
  </div>
@@ -59,6 +59,11 @@ export class ApiRunner {
59
59
  openApiPath;
60
60
  /** URL prefix that this runner intercepts (default `""`). */
61
61
  prefix;
62
+ /**
63
+ * Ordered list of overlay file paths/URLs applied to the OpenAPI document
64
+ * after loading. Empty when no overlays are configured.
65
+ */
66
+ overlays;
62
67
  /**
63
68
  * Optional group name that places generated code in a subdirectory.
64
69
  * Defaults to `""` (no subdirectory).
@@ -89,11 +94,12 @@ export class ApiRunner {
89
94
  this.openApiDocument = openApiDocument;
90
95
  this.openApiPath = config.openApiPath;
91
96
  this.prefix = config.prefix;
97
+ this.overlays = config.overlays ?? [];
92
98
  this.registry = new Registry();
93
99
  this.contextRegistry = new ContextRegistry();
94
100
  this.scenarioRegistry = new ScenarioRegistry();
95
101
  this.scenarioFileGenerator = new ScenarioFileGenerator(modulesPath);
96
- this.codeGenerator = new CodeGenerator(this.openApiPath, config.basePath + this.subdirectory, config.generate, version);
102
+ this.codeGenerator = new CodeGenerator(this.openApiPath, config.basePath + this.subdirectory, config.generate, version, config.overlays ?? []);
97
103
  this.dispatcher = new Dispatcher(this.registry, this.contextRegistry, openApiDocument, config, version, versions);
98
104
  this.transpiler = new Transpiler(pathJoin(modulesPath, "routes"), compiledPathsDirectory, "commonjs");
99
105
  this.moduleLoader = new ModuleLoader(compiledPathsDirectory, this.registry, this.contextRegistry, pathJoin(modulesPath, "scenarios"), this.scenarioRegistry);
@@ -121,7 +127,7 @@ export class ApiRunner {
121
127
  }
122
128
  const openApiDocument = config.openApiPath === "_"
123
129
  ? undefined
124
- : await loadOpenApiDocument(config.openApiPath);
130
+ : await loadOpenApiDocument(config.openApiPath, config.overlays ?? []);
125
131
  return new ApiRunner(config, nativeTs, openApiDocument, group, version, versions);
126
132
  }
127
133
  /**
package/dist/app.js CHANGED
@@ -131,7 +131,14 @@ export async function counterfact(config, specs) {
131
131
  versionsByGroup.set(spec.group, [...existing, version]);
132
132
  }
133
133
  }
134
- const runners = await Promise.all(normalizedSpecs.map((spec) => ApiRunner.create({ ...config, openApiPath: spec.source, prefix: spec.prefix }, spec.group, spec.version ?? "", versionsByGroup.get(spec.group) ?? [])));
134
+ const runners = await Promise.all(normalizedSpecs.map((spec) => ApiRunner.create({
135
+ ...config,
136
+ openApiPath: spec.source,
137
+ // Per-spec overlays take precedence; fall back to config-level overlays
138
+ // so that the --overlay CLI flag works in single-spec mode.
139
+ overlays: spec.overlays ?? config.overlays ?? [],
140
+ prefix: spec.prefix,
141
+ }, spec.group, spec.version ?? "", versionsByGroup.get(spec.group) ?? [])));
135
142
  const koaApp = createKoaApp({
136
143
  runners,
137
144
  config,
package/dist/cli/run.js CHANGED
@@ -22,7 +22,7 @@ const DEFAULT_PORT = 3100;
22
22
  * CLI flag) into an array of {@link SpecConfig} objects, or `undefined` when
23
23
  * the option is a plain string (single OpenAPI document path).
24
24
  *
25
- * - **Array**: each entry is mapped to `{source, prefix, group, version}` with defaults.
25
+ * - **Array**: each entry is mapped to `{source, prefix, group, version, overlays}` with defaults.
26
26
  * - **Object**: wrapped in a single-element array.
27
27
  * - **String / undefined**: returns `undefined` — caller handles the string
28
28
  * case (it shifts the positional argument) and the `undefined` case
@@ -39,6 +39,7 @@ export function normalizeSpecOption(specOption) {
39
39
  prefix: entry.prefix,
40
40
  group: entry.group ?? "",
41
41
  version: entry.version,
42
+ overlays: entry.overlays,
42
43
  }));
43
44
  }
44
45
  if (typeof specOption === "object" &&
@@ -50,6 +51,7 @@ export function normalizeSpecOption(specOption) {
50
51
  prefix: specOption.prefix,
51
52
  group: specOption.group ?? "",
52
53
  version: specOption.version,
54
+ overlays: specOption.overlays,
53
55
  },
54
56
  ];
55
57
  }
@@ -104,11 +106,17 @@ function buildProgram(version, taglines) {
104
106
  const configFilePath = resolve(options.config ?? "counterfact.yaml");
105
107
  const fileConfig = await loadConfigFile(configFilePath, options.config !== undefined);
106
108
  debug("fileConfig: %o", fileConfig);
109
+ const knownOptionKeys = new Set(program.options.map((option) => option.attributeName()));
107
110
  // Apply config file values for any option that was not explicitly set on
108
111
  // the command line (i.e. its source is "default" or it was never defined).
109
112
  for (const [key, value] of Object.entries(fileConfig)) {
113
+ if (!knownOptionKeys.has(key)) {
114
+ debug("ignoring unknown config key %s", key);
115
+ continue;
116
+ }
110
117
  const optionSource = program.getOptionValueSource(key);
111
118
  if (optionSource !== "cli") {
119
+ // eslint-disable-next-line security/detect-object-injection -- key is validated against known Commander option names above.
112
120
  options[key] = value;
113
121
  }
114
122
  }
@@ -139,6 +147,7 @@ function buildProgram(version, taglines) {
139
147
  const actions = ["repl", "serve", "watch", "generate", "buildCache"];
140
148
  if (!Object.keys(options).some((argument) => actions.some((action) => argument.startsWith(action)))) {
141
149
  for (const action of actions) {
150
+ // eslint-disable-next-line security/detect-object-injection -- action names come from the local allowlist above.
142
151
  options[action] = true;
143
152
  }
144
153
  }
@@ -170,6 +179,7 @@ function buildProgram(version, taglines) {
170
179
  prune: Boolean(options.prune),
171
180
  },
172
181
  openApiPath: source,
182
+ overlays: options.overlay ?? [],
173
183
  port: options.port,
174
184
  proxyPaths: new Map([["", Boolean(options.proxyUrl)]]),
175
185
  proxyUrl: options.proxyUrl ?? "",
@@ -298,6 +308,7 @@ function buildProgram(version, taglines) {
298
308
  .option("--always-fake-optionals", "random responses will include optional fields")
299
309
  .option("--prune", "remove route files that no longer exist in the OpenAPI spec")
300
310
  .option("--spec <string>", "path or URL to OpenAPI document (alternative to the positional [openapi.yaml] argument)")
311
+ .option("--overlay <path>", "path or URL to an OpenAPI overlay file to apply (repeatable)", (value, previous) => [...previous, value], [])
301
312
  .option("--no-update-check", "disable the npm update check on startup")
302
313
  .option("--no-validate-request", "disable request validation against the OpenAPI spec")
303
314
  .option("--no-validate-response", "disable response validation against the OpenAPI spec")
@@ -165,6 +165,36 @@ export class Dispatcher {
165
165
  }
166
166
  return undefined;
167
167
  }
168
+ apiKeySecurityParameters() {
169
+ const schemes = this.openApiDocument?.components?.securitySchemes;
170
+ return Object.values(schemes ?? {})
171
+ .filter(({ in: location, name, type }) => type === "apiKey" &&
172
+ typeof name === "string" &&
173
+ (location === "header" ||
174
+ location === "query" ||
175
+ location === "cookie"))
176
+ .map(({ in: location, name }) => ({
177
+ in: location,
178
+ name: name,
179
+ required: false,
180
+ schema: { type: "string" },
181
+ }));
182
+ }
183
+ authWithApiKey(auth, cookie, headers, query) {
184
+ const apiKeyScheme = this.apiKeySecurityParameters().find((parameter) => ["cookie", "header", "query"].includes(parameter.in));
185
+ if (!apiKeyScheme) {
186
+ return auth;
187
+ }
188
+ const apiKey = apiKeyScheme.in === "query"
189
+ ? query[apiKeyScheme.name]
190
+ : apiKeyScheme.in === "cookie"
191
+ ? cookie[apiKeyScheme.name]
192
+ : Object.entries(headers).find(([key]) => key.toLowerCase() === apiKeyScheme.name.toLowerCase())?.[1];
193
+ const normalizedApiKey = Array.isArray(apiKey)
194
+ ? (apiKey[0] ?? "")
195
+ : (apiKey ?? "");
196
+ return { ...auth, apiKey: normalizedApiKey };
197
+ }
168
198
  /**
169
199
  * Resolves the OpenAPI operation for `path` and `method`, merging any
170
200
  * top-level `produces` array from the document root and any path-item-level
@@ -203,13 +233,20 @@ export class Dispatcher {
203
233
  const mergedOperation = mergedParameters !== undefined
204
234
  ? { ...operation, parameters: mergedParameters }
205
235
  : operation;
236
+ const apiKeyParameters = this.apiKeySecurityParameters();
237
+ const operationWithSecurity = apiKeyParameters.length > 0
238
+ ? {
239
+ ...mergedOperation,
240
+ parameters: mergeParameters(mergedOperation.parameters ?? [], apiKeyParameters),
241
+ }
242
+ : mergedOperation;
206
243
  if (this.openApiDocument?.produces) {
207
244
  return {
208
245
  produces: this.openApiDocument.produces,
209
- ...mergedOperation,
246
+ ...operationWithSecurity,
210
247
  };
211
248
  }
212
- return mergedOperation;
249
+ return operationWithSecurity;
213
250
  }
214
251
  normalizeResponse(response, acceptHeader) {
215
252
  if (response.content !== undefined) {
@@ -299,8 +336,14 @@ export class Dispatcher {
299
336
  };
300
337
  }
301
338
  const operation = this.operationForPathAndMethod(matchedPath, method);
339
+ const requestCookie = parseCookies(headers.cookie ?? headers.Cookie ?? "");
302
340
  if (this.config?.validateRequests !== false) {
303
- const validation = validateRequest(operation, { body, headers, query });
341
+ const validation = validateRequest(operation, {
342
+ body,
343
+ cookie: requestCookie,
344
+ headers,
345
+ query,
346
+ });
304
347
  if (!validation.valid) {
305
348
  return {
306
349
  body: `Request validation failed:\n${validation.errors.join("\n")}`,
@@ -316,7 +359,7 @@ export class Dispatcher {
316
359
  return min + Math.random() * (max - min);
317
360
  };
318
361
  const response = await this.registry.endpoint(method, path, this.parameterTypes(operation?.parameters))({
319
- auth,
362
+ auth: this.authWithApiKey(auth, requestCookie, headers, query),
320
363
  body,
321
364
  context: this.contextRegistry.find(matchedPath),
322
365
  async delay(milliseconds = 0, maxMilliseconds = 0) {
@@ -325,7 +368,7 @@ export class Dispatcher {
325
368
  : continuousDistribution(milliseconds, maxMilliseconds);
326
369
  return new Promise((resolve) => setTimeout(resolve, delayInMs));
327
370
  },
328
- cookie: parseCookies(headers.cookie ?? headers.Cookie ?? ""),
371
+ cookie: requestCookie,
329
372
  headers,
330
373
  proxy: async (url) => {
331
374
  delete headers.host;
@@ -1,6 +1,6 @@
1
1
  import { OpenApiDocument } from "./openapi-document.js";
2
- export async function loadOpenApiDocument(source) {
3
- const document = new OpenApiDocument(source);
2
+ export async function loadOpenApiDocument(source, overlays = []) {
3
+ const document = new OpenApiDocument(source, overlays);
4
4
  await document.load();
5
5
  return document;
6
6
  }
@@ -1,6 +1,7 @@
1
1
  import { watch } from "chokidar";
2
2
  import createDebug from "debug";
3
3
  import { dereference } from "@apidevtools/json-schema-ref-parser";
4
+ import { applyOverlays } from "../util/apply-overlay.js";
4
5
  import { waitForEvent } from "../util/wait-for-event.js";
5
6
  import { sendTelemetry } from "../cli/telemetry.js";
6
7
  import { CHOKIDAR_OPTIONS } from "./constants.js";
@@ -14,13 +15,20 @@ const debug = createDebug("counterfact:server:openapi-document");
14
15
  export class OpenApiDocument extends EventTarget {
15
16
  /** The path or URL of the OpenAPI source file. */
16
17
  source;
18
+ /**
19
+ * Optional ordered list of overlay file paths/URLs to apply after each
20
+ * load of the document.
21
+ */
22
+ overlays;
17
23
  basePath;
24
+ components;
18
25
  paths = {};
19
26
  produces;
20
27
  watcher;
21
- constructor(source) {
28
+ constructor(source, overlays = []) {
22
29
  super();
23
30
  this.source = source;
31
+ this.overlays = overlays;
24
32
  }
25
33
  /**
26
34
  * Reads the source file and populates the document's properties.
@@ -29,7 +37,11 @@ export class OpenApiDocument extends EventTarget {
29
37
  async load() {
30
38
  try {
31
39
  const data = (await dereference(this.source));
40
+ if (this.overlays.length > 0) {
41
+ await applyOverlays(data, this.overlays);
42
+ }
32
43
  this.basePath = data.basePath;
44
+ this.components = data.components;
33
45
  this.paths = data.paths;
34
46
  this.produces = data.produces;
35
47
  }
@@ -62,6 +62,7 @@ export function validateRequest(operation, request) {
62
62
  // by the registry before the route handler is called.
63
63
  errors.push(...findMissingRequired(parameters, "query", request.query));
64
64
  errors.push(...findMissingRequired(parameters, "header", request.headers));
65
+ errors.push(...findMissingRequired(parameters, "cookie", request.cookie));
65
66
  // Validate request body (OpenAPI 3.x requestBody)
66
67
  if (operation.requestBody?.content !== undefined) {
67
68
  const schema = operation.requestBody.content["application/json"]?.schema ??
@@ -1,4 +1,5 @@
1
1
  import { generate } from "json-schema-faker";
2
+ /* eslint-disable security/detect-object-injection -- OpenAPI response/content maps are spec-defined dictionaries accessed by status code, media type, and example name. */
2
3
  import { jsonToXml } from "./json-to-xml.js";
3
4
  import { STREAMING_CONTENT_TYPES } from "../typescript-generator/streaming-content-types.js";
4
5
  const DEFAULT_GENERATE_OPTIONS = {
@@ -29,6 +29,7 @@ export function createKoaApp({ runners, config, }) {
29
29
  app.use(openapiMiddleware(`/counterfact/openapi${runner.subdirectory}`, {
30
30
  path: runner.openApiPath,
31
31
  baseUrl: `//localhost:${config.port}${runner.prefix}`,
32
+ overlays: runner.overlays,
32
33
  }));
33
34
  app.use(koaSwagger({
34
35
  routePrefix: `/counterfact/swagger${runner.subdirectory}`,
@@ -1,5 +1,6 @@
1
1
  import { bundle } from "@apidevtools/json-schema-ref-parser";
2
2
  import { dump } from "js-yaml";
3
+ import { applyOverlays } from "../../util/apply-overlay.js";
3
4
  /**
4
5
  * Returns a Koa middleware that serves a bundled OpenAPI document as YAML at
5
6
  * the given `pathPrefix`.
@@ -22,6 +23,9 @@ export function openapiMiddleware(pathPrefix, document) {
22
23
  return await next();
23
24
  }
24
25
  const openApiDocument = (await bundle(document.path));
26
+ if (document.overlays && document.overlays.length > 0) {
27
+ await applyOverlays(openApiDocument, document.overlays);
28
+ }
25
29
  openApiDocument.servers ??= [];
26
30
  openApiDocument.servers.unshift({
27
31
  name: "Counterfact",
@@ -24,10 +24,10 @@ const HEADERS_TO_DROP = new Set([
24
24
  * SSE/JSONL/JSON-seq formatter map. Each entry maps a content-type to the
25
25
  * function that serialises a single stream item into the wire format.
26
26
  */
27
- const STREAMING_FORMATTERS = {
28
- "text/event-stream": (item) => `data: ${JSON.stringify(item)}\n\n`,
29
- "application/json-seq": (item) => `\x1e${JSON.stringify(item)}\n`,
30
- };
27
+ const STREAMING_FORMATTERS = new Map([
28
+ ["text/event-stream", (item) => `data: ${JSON.stringify(item)}\n\n`],
29
+ ["application/json-seq", (item) => `\x1e${JSON.stringify(item)}\n`],
30
+ ]);
31
31
  function defaultStreamFormatter(item) {
32
32
  return `${JSON.stringify(item)}\n`;
33
33
  }
@@ -42,7 +42,7 @@ function isAsyncIterable(value) {
42
42
  * each item according to the given content type.
43
43
  */
44
44
  function asyncIterableToReadable(iterable, contentType) {
45
- const formatter = STREAMING_FORMATTERS[contentType] ?? defaultStreamFormatter;
45
+ const formatter = STREAMING_FORMATTERS.get(contentType) ?? defaultStreamFormatter;
46
46
  async function* generate() {
47
47
  for await (const item of iterable) {
48
48
  yield formatter(item);
@@ -23,13 +23,15 @@ export class CodeGenerator extends EventTarget {
23
23
  openapiPath;
24
24
  destination;
25
25
  version;
26
+ overlays;
26
27
  generateOptions;
27
28
  watcher;
28
- constructor(openApiPath, destination, generateOptions, version = "") {
29
+ constructor(openApiPath, destination, generateOptions, version = "", overlays = []) {
29
30
  super();
30
31
  this.openapiPath = openApiPath;
31
32
  this.destination = destination;
32
33
  this.version = version;
34
+ this.overlays = overlays;
33
35
  this.generateOptions = generateOptions;
34
36
  }
35
37
  /**
@@ -87,7 +89,7 @@ export class CodeGenerator extends EventTarget {
87
89
  await this.buildCacheDirectory(destination);
88
90
  debug("done initializing the .cache directory");
89
91
  debug("creating specification from %s", this.openapiPath);
90
- const specification = await Specification.fromFile(this.openapiPath);
92
+ const specification = await Specification.fromFile(this.openapiPath, this.overlays);
91
93
  debug("created specification: $o", specification);
92
94
  debug("reading the #/paths from the specification");
93
95
  const paths = await this.getPathsFromSpecification(specification);
@@ -135,16 +137,20 @@ export class CodeGenerator extends EventTarget {
135
137
  debug("finished writing the files");
136
138
  }
137
139
  /**
138
- * Starts watching the OpenAPI document for changes.
140
+ * Starts watching the OpenAPI document and any overlay files for changes.
139
141
  *
140
- * Has no effect when `openApiPath` is a URL (HTTP sources are not watched).
142
+ * Has no effect when neither source is watchable (for example, an HTTP
143
+ * OpenAPI source with no local overlays).
141
144
  * Resolves once the watcher is ready.
142
145
  */
143
146
  async watch() {
144
- if (this.openapiPath.startsWith("http")) {
147
+ const watchablePaths = this.openapiPath.startsWith("http")
148
+ ? [...this.overlays]
149
+ : [this.openapiPath, ...this.overlays];
150
+ if (watchablePaths.length === 0) {
145
151
  return;
146
152
  }
147
- this.watcher = watch(this.openapiPath, CHOKIDAR_OPTIONS).on("change", () => {
153
+ this.watcher = watch(watchablePaths, CHOKIDAR_OPTIONS).on("change", () => {
148
154
  void this.generate().then(() => {
149
155
  this.dispatchEvent(new Event("generate"));
150
156
  return true;
@@ -163,6 +163,23 @@ export class OperationTypeCoder extends TypeCoder {
163
163
  }
164
164
  return "never";
165
165
  }
166
+ /**
167
+ * Returns the TypeScript type for the `auth` argument.
168
+ *
169
+ * Includes basic-auth credentials when present and `apiKey` when at least one
170
+ * apiKey security scheme is configured.
171
+ */
172
+ authType() {
173
+ const fields = new Set();
174
+ if (this.securitySchemes.some(({ scheme, type }) => type === "http" && scheme === "basic")) {
175
+ fields.add("username?: string");
176
+ fields.add("password?: string");
177
+ }
178
+ if (this.securitySchemes.some(({ type }) => type === "apiKey")) {
179
+ fields.add("apiKey: string");
180
+ }
181
+ return fields.size === 0 ? "never" : `{${[...fields].join(", ")}}`;
182
+ }
166
183
  /**
167
184
  * Returns the effective parameters for this operation by merging path-item-level
168
185
  * parameters with operation-level parameters. Per the OpenAPI specification,
@@ -178,16 +195,32 @@ export class OperationTypeCoder extends TypeCoder {
178
195
  getEffectiveParameters() {
179
196
  const operationParams = this.requirement.get("parameters");
180
197
  const pathItemParams = this.requirement.parent?.get("parameters");
181
- if (!pathItemParams) {
198
+ const apiKeyParameters = this.securitySchemes
199
+ .filter(({ in: location, name, type }) => type === "apiKey" &&
200
+ typeof name === "string" &&
201
+ (location === "header" ||
202
+ location === "query" ||
203
+ location === "cookie"))
204
+ .map(({ in: location, name }) => ({
205
+ in: location,
206
+ name,
207
+ required: true,
208
+ schema: { type: "string" },
209
+ }));
210
+ if (!pathItemParams && !operationParams && apiKeyParameters.length === 0) {
211
+ return undefined;
212
+ }
213
+ if (!pathItemParams && apiKeyParameters.length === 0) {
182
214
  return operationParams;
183
215
  }
184
- if (!operationParams) {
216
+ if (!operationParams && apiKeyParameters.length === 0) {
185
217
  return pathItemParams;
186
218
  }
187
219
  // Merge using a Map keyed on `${in}:${name}`.
188
- // Path-level params are added first; operation-level overrides them.
189
- const pathData = pathItemParams.data;
190
- const opData = operationParams.data;
220
+ // Path-level params are added first; operation-level and security-level
221
+ // params override them.
222
+ const pathData = pathItemParams?.data ?? [];
223
+ const opData = operationParams?.data ?? [];
191
224
  const map = new Map();
192
225
  for (const p of pathData) {
193
226
  map.set(`${p.in}:${p.name}`, p);
@@ -195,6 +228,9 @@ export class OperationTypeCoder extends TypeCoder {
195
228
  for (const p of opData) {
196
229
  map.set(`${p.in}:${p.name}`, p);
197
230
  }
231
+ for (const p of apiKeyParameters) {
232
+ map.set(`${p.in}:${p.name}`, p);
233
+ }
198
234
  return new Requirement([...map.values()], this.requirement.url, this.requirement.specification);
199
235
  }
200
236
  /**
@@ -245,7 +281,7 @@ export class OperationTypeCoder extends TypeCoder {
245
281
  : "never";
246
282
  const querystringTypeName = this.exportParameterType(script, "querystring", querystringType, baseName, modulePath);
247
283
  const versionLiteralType = this.version !== "" ? `"${this.version}"` : "never";
248
- return `OmitValueWhenNever<{ query: ${queryTypeName}, querystring: ${querystringTypeName}, path: ${pathTypeName}, headers: ${headersTypeName}, cookie: ${cookieTypeName}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, x: ${xType}, proxy: ${proxyType}, user: ${this.userType()}, delay: ${delayType}, version: ${versionLiteralType} }>`;
284
+ return `OmitValueWhenNever<{ query: ${queryTypeName}, querystring: ${querystringTypeName}, path: ${pathTypeName}, headers: ${headersTypeName}, cookie: ${cookieTypeName}, body: ${bodyType}, context: ${contextTypeImportName}, response: ${responseType}, x: ${xType}, proxy: ${proxyType}, auth: ${this.authType()}, user: ${this.userType()}, delay: ${delayType}, version: ${versionLiteralType} }>`;
249
285
  }
250
286
  writeCode(script) {
251
287
  script.comments = READ_ONLY_COMMENTS;
@@ -70,9 +70,11 @@ export class Repository {
70
70
  /**
71
71
  * Waits for all scripts to finish, then writes each one to disk.
72
72
  *
73
- * Route files (`routes/…`) are never overwritten if they already exist on
74
- * disk, preserving user edits. Type files (`types/…`) are always
75
- * overwritten.
73
+ * Route files (`routes/…`) are never fully overwritten if they already exist
74
+ * on disk, preserving user edits. However, if the generated script contains
75
+ * HTTP-method handler exports that are absent from the existing file, those
76
+ * new exports (and their `import type` statements) are appended to the file.
77
+ * Type files (`types/…`) are always overwritten.
76
78
  *
77
79
  * @param destination - Absolute path to the output root directory.
78
80
  * @param options - Controls which artefacts are written.
@@ -87,13 +89,16 @@ export class Repository {
87
89
  await ensureDirectoryExists(fullPath);
88
90
  const shouldWriteRoutes = routes && path.startsWith("routes");
89
91
  const shouldWriteTypes = types && !path.startsWith("routes");
90
- if (shouldWriteRoutes &&
91
- (await fs
92
+ if (shouldWriteRoutes) {
93
+ const fileExists = await fs
92
94
  .stat(fullPath)
93
95
  .then((stat) => stat.isFile())
94
- .catch(() => false))) {
95
- debug(`not overwriting ${fullPath}\n`);
96
- return;
96
+ .catch(() => false);
97
+ if (fileExists) {
98
+ debug(`route file exists, checking for new handlers: ${fullPath}`);
99
+ await this.appendNewHandlers(fullPath, contents.replaceAll(CONTEXT_FILE_TOKEN, this.findContextPath(destination, path)));
100
+ return;
101
+ }
97
102
  }
98
103
  if (shouldWriteRoutes || shouldWriteTypes) {
99
104
  debug("about to write", fullPath);
@@ -139,6 +144,90 @@ export class Context {
139
144
  }
140
145
  `);
141
146
  }
147
+ /**
148
+ * Appends any HTTP-method handler exports that appear in `generatedContent`
149
+ * but are absent from the existing file at `fullPath`.
150
+ *
151
+ * For each new export the corresponding `import type` statement is inserted
152
+ * after the last existing import line (or prepended when no imports exist),
153
+ * and the export block is appended at the end of the file.
154
+ *
155
+ * @param fullPath - Absolute path of the route file to update.
156
+ * @param generatedContent - The fully-generated file content (used as the
157
+ * source of new import and export statements).
158
+ */
159
+ async appendNewHandlers(fullPath, generatedContent) {
160
+ const existingContent = await fs.readFile(fullPath, "utf8");
161
+ // Names already exported by the existing file (e.g. GET, POST).
162
+ // RegExp match groups are typed as optional strings, so narrow defensively.
163
+ const existingExportNames = new Set(Array.from(existingContent.matchAll(/^export\s+const\s+(\w+)/gmu), (m) => m[1]).filter((name) => name !== undefined));
164
+ // All named exports in the generated content together with their type names.
165
+ const generatedExports = Array.from(generatedContent.matchAll(/^export\s+const\s+(\w+)\s*:\s*(\w+)/gmu), (m) => ({ methodName: m[1], typeName: m[2] })).filter((value) => value.methodName !== undefined && value.typeName !== undefined);
166
+ const newExports = generatedExports.filter(({ methodName }) => !existingExportNames.has(methodName));
167
+ if (newExports.length === 0) {
168
+ debug(`no new handlers to append to ${fullPath}`);
169
+ return;
170
+ }
171
+ debug(`appending ${newExports.length} new handler(s) to ${fullPath}: %o`, newExports.map(({ methodName }) => methodName));
172
+ const newImportLines = [];
173
+ const newExportBlocks = [];
174
+ for (const { methodName, typeName } of newExports) {
175
+ // Both names come from \w+ captures so they are safe identifiers, but
176
+ // guard explicitly to satisfy static analysis and avoid RegExp injection.
177
+ if (!/^\w+$/u.test(typeName) || !/^\w+$/u.test(methodName)) {
178
+ debug(`skipping handler with unsafe name – methodName: %s, typeName: %s`, methodName, typeName);
179
+ continue;
180
+ }
181
+ // Find the `import type { TypeName } from "..."` line for this type.
182
+ const importMatch = generatedContent.match(new RegExp(`^import\\s+type\\s+\\{[^}]*\\b${typeName}\\b[^}]*\\}\\s+from\\s+["'][^"']+["'];`, "mu"));
183
+ if (importMatch?.[0] && !existingContent.includes(importMatch[0])) {
184
+ newImportLines.push(importMatch[0]);
185
+ }
186
+ // Find the export block: from `export const METHOD` to the closing `};`.
187
+ // The generated code is always Prettier-formatted, so the closing brace
188
+ // and semicolon of every top-level arrow-function export appear on their
189
+ // own line as `\n};`.
190
+ const startMatch = new RegExp(`^export\\s+const\\s+${methodName}\\b`, "mu").exec(generatedContent);
191
+ if (startMatch) {
192
+ const fromExport = generatedContent.slice(startMatch.index);
193
+ const closingIndex = fromExport.indexOf("\n};");
194
+ if (closingIndex !== -1) {
195
+ // Include the closing `};` (3 chars: \n, }, ;)
196
+ newExportBlocks.push(fromExport.slice(0, closingIndex + 3));
197
+ }
198
+ }
199
+ }
200
+ let updatedContent = existingContent;
201
+ // Insert new import lines right after the last existing import statement.
202
+ if (newImportLines.length > 0) {
203
+ const importMatches = [...existingContent.matchAll(/^import\s[^\n]*/gmu)];
204
+ if (importMatches.length > 0) {
205
+ const lastImport = importMatches[importMatches.length - 1];
206
+ const importIndex = lastImport?.index;
207
+ const insertPos = importIndex === undefined
208
+ ? 0
209
+ : (() => {
210
+ const lineEnd = existingContent.indexOf("\n", importIndex);
211
+ return lineEnd === -1 ? existingContent.length : lineEnd + 1;
212
+ })();
213
+ updatedContent =
214
+ existingContent.slice(0, insertPos) +
215
+ newImportLines.join("\n") +
216
+ "\n" +
217
+ existingContent.slice(insertPos);
218
+ }
219
+ else {
220
+ updatedContent = newImportLines.join("\n") + "\n" + existingContent;
221
+ }
222
+ }
223
+ // Append new export blocks at the end of the file.
224
+ if (newExportBlocks.length > 0) {
225
+ const separator = updatedContent.endsWith("\n") ? "\n" : "\n\n";
226
+ updatedContent += separator + newExportBlocks.join("\n\n") + "\n";
227
+ }
228
+ await fs.writeFile(fullPath, updatedContent);
229
+ debug(`appended new handlers to ${fullPath}`);
230
+ }
142
231
  /**
143
232
  * Returns the path of the `_.context.ts` file that is nearest to `path` in
144
233
  * the directory hierarchy, relative to the script's output directory.
@@ -1,5 +1,6 @@
1
1
  import { bundle } from "@apidevtools/json-schema-ref-parser";
2
2
  import createDebug from "debug";
3
+ import { applyOverlays } from "../util/apply-overlay.js";
3
4
  import { Requirement } from "./requirement.js";
4
5
  const debug = createDebug("counterfact:typescript-generator:specification");
5
6
  /**
@@ -23,12 +24,14 @@ export class Specification {
23
24
  * Loads the OpenAPI document at `urlOrPath`, bundles all external `$ref`
24
25
  * references, and returns a fully initialised {@link Specification}.
25
26
  *
26
- * @param urlOrPath - A local file path or HTTP(S) URL.
27
+ * @param urlOrPath - A local file path or HTTP(S) URL.
28
+ * @param overlays - Optional ordered list of overlay file paths/URLs to
29
+ * apply after loading the document.
27
30
  * @throws When the document cannot be found or parsed.
28
31
  */
29
- static async fromFile(urlOrPath) {
32
+ static async fromFile(urlOrPath, overlays = []) {
30
33
  const specification = new Specification();
31
- await specification.load(urlOrPath);
34
+ await specification.load(urlOrPath, overlays);
32
35
  return specification;
33
36
  }
34
37
  /**
@@ -42,16 +45,22 @@ export class Specification {
42
45
  return this.rootRequirement.select(url.slice(2));
43
46
  }
44
47
  /**
45
- * Loads (or reloads) the specification from `urlOrPath`.
48
+ * Loads (or reloads) the specification from `urlOrPath`, then applies any
49
+ * overlay files listed in `overlays` in order.
46
50
  *
47
51
  * @param urlOrPath - A local file path or HTTP(S) URL.
52
+ * @param overlays - Optional ordered list of overlay file paths/URLs.
48
53
  * @throws When the document cannot be found or parsed.
49
54
  */
50
- async load(urlOrPath) {
55
+ async load(urlOrPath, overlays = []) {
51
56
  try {
52
- this.rootRequirement = new Requirement((await bundle(urlOrPath, {
57
+ const document = (await bundle(urlOrPath, {
53
58
  resolve: { http: { safeUrlResolver: false } },
54
- })), urlOrPath, this);
59
+ }));
60
+ if (overlays.length > 0) {
61
+ await applyOverlays(document, overlays);
62
+ }
63
+ this.rootRequirement = new Requirement(document, urlOrPath, this);
55
64
  }
56
65
  catch (error) {
57
66
  const details = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,119 @@
1
+ import { load as loadYaml } from "js-yaml";
2
+ import { JSONPath } from "jsonpath-plus";
3
+ import { readFile } from "./read-file.js";
4
+ /**
5
+ * Deeply merges `source` into `target`, overwriting scalar values and
6
+ * recursively merging plain objects. Arrays and non-plain-object values in
7
+ * `source` always overwrite the corresponding entry in `target`.
8
+ */
9
+ function deepMerge(target, source) {
10
+ for (const [key, value] of Object.entries(source)) {
11
+ // Guard against prototype pollution attacks.
12
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
13
+ continue;
14
+ }
15
+ if (typeof value === "object" &&
16
+ value !== null &&
17
+ !Array.isArray(value) &&
18
+ typeof target[key] === "object" &&
19
+ target[key] !== null &&
20
+ !Array.isArray(target[key])) {
21
+ deepMerge(target[key], value);
22
+ }
23
+ else {
24
+ target[key] = value;
25
+ }
26
+ }
27
+ }
28
+ /**
29
+ * Applies a list of overlay actions to `document` in place.
30
+ *
31
+ * Each action may either:
32
+ * - **update**: deep-merge the `action.update` object into every node matched
33
+ * by the JSONPath `action.target`.
34
+ * - **remove**: delete every node matched by `action.target` from its parent.
35
+ *
36
+ * @param document - The OpenAPI document object to mutate.
37
+ * @param actions - The ordered list of overlay actions to apply.
38
+ */
39
+ export function applyOverlayActions(document, actions) {
40
+ for (const action of actions) {
41
+ const results = JSONPath({
42
+ path: action.target,
43
+ json: document,
44
+ resultType: "all",
45
+ });
46
+ if (action.remove === true) {
47
+ // Iterate in reverse so that removing by numeric index doesn't shift
48
+ // subsequent items in the same parent array.
49
+ for (const result of [...results].reverse()) {
50
+ const { parent, parentProperty } = result;
51
+ if (Array.isArray(parent)) {
52
+ parent.splice(Number(parentProperty), 1);
53
+ }
54
+ else {
55
+ delete parent[String(parentProperty)];
56
+ }
57
+ }
58
+ }
59
+ else if (action.update !== undefined) {
60
+ for (const result of results) {
61
+ if (typeof result.value === "object" &&
62
+ result.value !== null &&
63
+ !Array.isArray(result.value)) {
64
+ deepMerge(result.value, action.update);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Loads and parses an overlay file (YAML or JSON), validates that it looks
72
+ * like a valid OpenAPI overlay document, and returns the parsed object.
73
+ *
74
+ * @param overlayPath - Path or URL to the overlay file.
75
+ * @throws When the file cannot be read, parsed, or does not contain an
76
+ * `overlay` version field and an `actions` array.
77
+ */
78
+ export async function loadOverlay(overlayPath) {
79
+ let content;
80
+ try {
81
+ content = await readFile(overlayPath);
82
+ }
83
+ catch (error) {
84
+ const details = error instanceof Error ? error.message : String(error);
85
+ throw new Error(`Could not read overlay file "${overlayPath}".\n${details}`, { cause: error });
86
+ }
87
+ let parsed;
88
+ try {
89
+ parsed = loadYaml(content);
90
+ }
91
+ catch (error) {
92
+ const details = error instanceof Error ? error.message : String(error);
93
+ throw new Error(`Could not parse overlay file "${overlayPath}".\n${details}`, { cause: error });
94
+ }
95
+ if (typeof parsed !== "object" ||
96
+ parsed === null ||
97
+ !("overlay" in parsed) ||
98
+ !("actions" in parsed) ||
99
+ !Array.isArray(parsed.actions)) {
100
+ throw new Error(`"${overlayPath}" does not appear to be a valid OpenAPI overlay file. ` +
101
+ `Expected an object with "overlay" and "actions" fields.`);
102
+ }
103
+ return parsed;
104
+ }
105
+ /**
106
+ * Applies all overlays listed in `overlayPaths` to `document` in order.
107
+ *
108
+ * Each overlay is loaded from disk (or a URL), parsed, and its actions are
109
+ * applied sequentially. The document is mutated in place.
110
+ *
111
+ * @param document - The OpenAPI document object to mutate.
112
+ * @param overlayPaths - Ordered list of paths/URLs to overlay files.
113
+ */
114
+ export async function applyOverlays(document, overlayPaths) {
115
+ for (const overlayPath of overlayPaths) {
116
+ const overlay = await loadOverlay(overlayPath);
117
+ applyOverlayActions(document, overlay.actions);
118
+ }
119
+ }
@@ -33,6 +33,9 @@ export async function loadConfigFile(configPath, required = false) {
33
33
  }
34
34
  throw error;
35
35
  }
36
+ if (!content.trim()) {
37
+ return {};
38
+ }
36
39
  const parsed = loadYaml(content);
37
40
  if (parsed === null || parsed === undefined) {
38
41
  return {};
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "counterfact",
3
- "version": "2.12.0",
3
+ "version": "2.14.1",
4
4
  "description": "Generate a TypeScript-based mock server from an OpenAPI spec in seconds — with stateful routes, hot reload, and REPL support.",
5
5
  "type": "module",
6
6
  "main": "./dist/app.js",
7
7
  "exports": {
8
8
  ".": {
9
- "import": "./dist/app.js"
9
+ "import": "./dist/app.js",
10
+ "types": "./dist/server/types.d.ts"
10
11
  }
11
12
  },
12
13
  "types": "./dist/server/types.d.ts",
@@ -64,7 +65,7 @@
64
65
  ],
65
66
  "sideEffects": false,
66
67
  "scripts": {
67
- "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box",
68
+ "test": "yarn node --experimental-vm-modules ./node_modules/jest-cli/bin/jest --testPathIgnorePatterns=black-box --testPathIgnorePatterns=examples",
68
69
  "test:black-box": "rimraf dist && yarn build && python3 -m pytest test-black-box/ -v",
69
70
  "test:tsd": "tsd --typings ./dist/server/counterfact-types/index.ts --files ./test/**/*.test-d.ts",
70
71
  "build": "rm -rf dist && tsc && copyfiles -f \"src/counterfact-types/*.ts\" dist/server/counterfact-types && copyfiles -f \"src/server/*.cjs\" dist/server",
@@ -81,76 +82,76 @@
81
82
  "counterfact": "./bin/counterfact.js"
82
83
  },
83
84
  "devDependencies": {
84
- "@changesets/cli": "2.31.0",
85
+ "@changesets/cli": "2.31.1",
85
86
  "@eslint/js": "10.0.1",
86
87
  "@jest/globals": "30.4.1",
87
- "@swc/core": "1.15.33",
88
+ "@swc/core": "1.15.46",
88
89
  "@swc/jest": "0.2.39",
89
90
  "@testing-library/dom": "10.4.1",
90
91
  "@types/debug": "4.1.13",
91
92
  "@types/jest": "30.0.0",
92
93
  "@types/js-yaml": "4.0.9",
93
- "@types/koa": "3.0.2",
94
+ "@types/koa": "3.0.3",
94
95
  "@types/koa-bodyparser": "4.3.13",
95
96
  "@types/koa-proxy": "1.0.8",
96
97
  "@types/koa-static": "4.0.4",
97
98
  "@types/node": "22",
98
- "@typescript-eslint/eslint-plugin": "8.59.3",
99
- "@typescript-eslint/parser": "8.59.3",
99
+ "@typescript-eslint/eslint-plugin": "8.65.0",
100
+ "@typescript-eslint/parser": "8.65.0",
100
101
  "copyfiles": "2.4.1",
101
- "eslint": "10.3.0",
102
+ "eslint": "10.8.0",
102
103
  "eslint-formatter-github-annotations": "0.1.0",
103
- "eslint-import-resolver-typescript": "4.4.4",
104
+ "eslint-import-resolver-typescript": "4.4.5",
104
105
  "eslint-plugin-etc": "2.0.3",
105
106
  "eslint-plugin-file-progress": "4.0.0",
106
107
  "eslint-plugin-import": "2.32.0",
107
- "eslint-plugin-jest": "29.15.2",
108
- "eslint-plugin-jest-dom": "5.5.0",
109
- "eslint-plugin-n": "18.0.1",
108
+ "eslint-plugin-jest": "29.16.0",
109
+ "eslint-plugin-jest-dom": "5.6.0",
110
+ "eslint-plugin-n": "18.2.2",
110
111
  "eslint-plugin-no-explicit-type-exports": "0.12.1",
111
- "eslint-plugin-prettier": "5.5.5",
112
+ "eslint-plugin-prettier": "5.5.6",
112
113
  "eslint-plugin-promise": "7.3.0",
113
- "eslint-plugin-regexp": "3.1.0",
114
- "eslint-plugin-security": "4.0.0",
114
+ "eslint-plugin-regexp": "3.1.1",
115
+ "eslint-plugin-security": "4.0.1",
115
116
  "eslint-plugin-unused-imports": "4.4.1",
116
117
  "husky": "9.1.7",
117
118
  "jest": "30.4.2",
118
119
  "jest-retries": "1.0.1",
119
- "node-mocks-http": "1.17.2",
120
+ "node-mocks-http": "1.18.1",
120
121
  "rimraf": "6.1.3",
121
122
  "supertest": "7.2.2",
122
123
  "tsd": "0.33.0",
123
124
  "using-temporary-files": "2.2.1"
124
125
  },
125
126
  "dependencies": {
126
- "@apidevtools/json-schema-ref-parser": "15.3.5",
127
+ "@apidevtools/json-schema-ref-parser": "15.5.0",
127
128
  "@hapi/accept": "6.0.3",
128
129
  "@types/json-schema": "7.0.15",
129
130
  "ajv": "8.20.0",
130
131
  "chokidar": "5.0.0",
131
- "commander": "14.0.3",
132
+ "commander": "15.0.0",
132
133
  "debug": "4.4.3",
133
- "fs-extra": "11.3.5",
134
+ "fs-extra": "11.4.0",
134
135
  "http-terminator": "3.2.0",
135
- "js-yaml": "4.1.1",
136
- "json-schema-faker": "0.6.1",
136
+ "js-yaml": "5.2.2",
137
+ "json-schema-faker": "0.6.2",
138
+ "jsonpath-plus": "10.4.0",
137
139
  "jsonwebtoken": "9.0.3",
138
- "koa": "3.2.0",
140
+ "koa": "3.2.1",
139
141
  "koa-bodyparser": "4.4.1",
140
142
  "koa-proxies": "0.12.4",
141
143
  "koa2-swagger-ui": "5.12.0",
142
144
  "node-fetch": "3.3.2",
143
145
  "open": "11.0.0",
144
- "posthog-node": "5.34.1",
145
- "precinct": "12.3.2",
146
- "prettier": "3.8.3",
147
- "recast": "0.23.11",
148
- "tsx": "4.21.0",
146
+ "posthog-node": "5.46.0",
147
+ "precinct": "13.0.1",
148
+ "prettier": "3.8.5",
149
+ "recast": "0.23.12",
150
+ "tsx": "4.23.1",
149
151
  "typescript": "6.0.3"
150
152
  },
151
153
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
152
154
  "resolutions": {
153
- "js-yaml": "4.1.1",
154
155
  "@typescript-eslint/utils": "^8.58.0"
155
156
  }
156
157
  }