counterfact 2.12.0 → 2.14.0
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/dist/api-runner.js +8 -2
- package/dist/app.js +8 -1
- package/dist/cli/run.js +12 -1
- package/dist/server/dispatcher.js +48 -5
- package/dist/server/load-openapi-document.js +2 -2
- package/dist/server/openapi-document.js +13 -1
- package/dist/server/request-validator.js +1 -0
- package/dist/server/response-builder.js +1 -0
- package/dist/server/web-server/create-koa-app.js +1 -0
- package/dist/server/web-server/openapi-middleware.js +4 -0
- package/dist/server/web-server/routes-middleware.js +5 -5
- package/dist/typescript-generator/code-generator.js +12 -6
- package/dist/typescript-generator/operation-type-coder.js +42 -6
- package/dist/typescript-generator/repository.js +97 -8
- package/dist/typescript-generator/specification.js +16 -7
- package/dist/util/apply-overlay.js +119 -0
- package/package.json +14 -12
package/dist/api-runner.js
CHANGED
|
@@ -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({
|
|
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
|
-
...
|
|
246
|
+
...operationWithSecurity,
|
|
210
247
|
};
|
|
211
248
|
}
|
|
212
|
-
return
|
|
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, {
|
|
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:
|
|
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"
|
|
29
|
-
"application/json-seq"
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
189
|
-
|
|
190
|
-
const
|
|
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
|
|
74
|
-
* disk, preserving user edits.
|
|
75
|
-
*
|
|
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
|
-
|
|
92
|
+
if (shouldWriteRoutes) {
|
|
93
|
+
const fileExists = await fs
|
|
92
94
|
.stat(fullPath)
|
|
93
95
|
.then((stat) => stat.isFile())
|
|
94
|
-
.catch(() => false)
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
57
|
+
const document = (await bundle(urlOrPath, {
|
|
53
58
|
resolve: { http: { safeUrlResolver: false } },
|
|
54
|
-
}))
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "counterfact",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
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",
|
|
@@ -84,21 +85,21 @@
|
|
|
84
85
|
"@changesets/cli": "2.31.0",
|
|
85
86
|
"@eslint/js": "10.0.1",
|
|
86
87
|
"@jest/globals": "30.4.1",
|
|
87
|
-
"@swc/core": "1.15.
|
|
88
|
+
"@swc/core": "1.15.40",
|
|
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.
|
|
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.
|
|
99
|
-
"@typescript-eslint/parser": "8.
|
|
99
|
+
"@typescript-eslint/eslint-plugin": "8.60.0",
|
|
100
|
+
"@typescript-eslint/parser": "8.60.0",
|
|
100
101
|
"copyfiles": "2.4.1",
|
|
101
|
-
"eslint": "10.
|
|
102
|
+
"eslint": "10.4.0",
|
|
102
103
|
"eslint-formatter-github-annotations": "0.1.0",
|
|
103
104
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
104
105
|
"eslint-plugin-etc": "2.0.3",
|
|
@@ -133,19 +134,20 @@
|
|
|
133
134
|
"fs-extra": "11.3.5",
|
|
134
135
|
"http-terminator": "3.2.0",
|
|
135
136
|
"js-yaml": "4.1.1",
|
|
136
|
-
"json-schema-faker": "0.6.
|
|
137
|
+
"json-schema-faker": "0.6.2",
|
|
138
|
+
"jsonpath-plus": "10.4.0",
|
|
137
139
|
"jsonwebtoken": "9.0.3",
|
|
138
|
-
"koa": "3.2.
|
|
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.
|
|
145
|
-
"precinct": "
|
|
146
|
+
"posthog-node": "5.35.5",
|
|
147
|
+
"precinct": "13.0.0",
|
|
146
148
|
"prettier": "3.8.3",
|
|
147
149
|
"recast": "0.23.11",
|
|
148
|
-
"tsx": "4.
|
|
150
|
+
"tsx": "4.22.3",
|
|
149
151
|
"typescript": "6.0.3"
|
|
150
152
|
},
|
|
151
153
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|