@copilotkit/aimock 1.14.6 → 1.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +11 -0
- package/dist/cli.cjs +68 -24
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +68 -24
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/fixtures-remote.cjs +225 -0
- package/dist/fixtures-remote.cjs.map +1 -0
- package/dist/fixtures-remote.js +224 -0
- package/dist/fixtures-remote.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @copilotkit/aimock
|
|
2
2
|
|
|
3
|
+
## 1.14.7
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `--fixtures` now accepts `https://` and `http://` URLs to JSON fixture files in addition to filesystem paths. Fetches at boot, parses, and registers the remote fixture as if loaded from disk. On-disk cache at `~/.cache/aimock/fixtures/<sha256-of-url>/` (honoring `$XDG_CACHE_HOME`) provides resilience against transient upstream failures: with `--validate-on-load`, a fetch failure with a valid cached copy logs a warning and continues; without a cache, the process exits non-zero. HTTP fetch has a hard-coded 10s timeout and a 50 MB body size cap (enforced incrementally so a lying `Content-Length` cannot bypass it). Only `https://` and `http://` schemes are accepted — `file://`, `ftp://`, etc. are rejected with a clear error. The flag is now repeatable; multiple sources are loaded and concatenated. Tarball (`.tar.gz`) and zip URL support intentionally deferred to a future release.
|
|
8
|
+
- Private-address denylist for remote `--fixtures` URLs: fetches to loopback (`127.0.0.0/8`, `::1`), link-local (`169.254.0.0/16`, `fe80::/10`), RFC1918 (`10/8`, `172.16/12`, `192.168/16`), CGNAT (`100.64/10`), cloud-metadata (`169.254.169.254`), ULA (`fc00::/7`), multicast, and other reserved ranges are rejected with a clear fail-loud error. Hostnames are resolved and every returned address is checked. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required for local dev / tests that target `127.0.0.1`).
|
|
9
|
+
- HTTP redirects are rejected (fail-loud) for remote `--fixtures` URLs to prevent scheme-bypass (a 3xx `Location:` pointing at `file://` or `javascript:` would otherwise sidestep the scheme gate and SSRF denylist). Configure the upstream to serve the final URL directly — GitHub raw content URLs already do this.
|
|
10
|
+
|
|
3
11
|
## 1.14.6
|
|
4
12
|
|
|
5
13
|
### Changed
|
package/README.md
CHANGED
|
@@ -82,6 +82,11 @@ See the [GitHub Action docs](https://aimock.copilotkit.dev/github-action) for al
|
|
|
82
82
|
# LLM mocking only
|
|
83
83
|
npx -p @copilotkit/aimock llmock -p 4010 -f ./fixtures
|
|
84
84
|
|
|
85
|
+
# Remote fixtures — load JSON from an HTTPS URL (repeatable)
|
|
86
|
+
npx -p @copilotkit/aimock llmock -p 4010 \
|
|
87
|
+
-f https://raw.githubusercontent.com/acme/mocks/main/openai.json \
|
|
88
|
+
-f ./fixtures/local-overrides.json
|
|
89
|
+
|
|
85
90
|
# Full suite from config
|
|
86
91
|
npx @copilotkit/aimock --config aimock.json
|
|
87
92
|
|
|
@@ -98,6 +103,12 @@ docker run -d -p 4010:4010 -v "$(pwd)/fixtures:/fixtures" ghcr.io/copilotkit/aim
|
|
|
98
103
|
|
|
99
104
|
> **Note on `llmock` vs `aimock` CLIs.** The `llmock` bin is retained as a compat alias for users of the pre-1.7.0 `@copilotkit/llmock` package. It runs a narrower flag-driven CLI without `--config` or the `convert` subcommand. New projects should use `aimock` (or `npx @copilotkit/aimock`) for full feature support.
|
|
100
105
|
|
|
106
|
+
### Remote fixture URLs
|
|
107
|
+
|
|
108
|
+
`--fixtures` accepts `https://` and `http://` URLs pointing at JSON fixture files in addition to filesystem paths, and the flag is repeatable so you can layer remote and local sources in argv order. Fetched fixtures are cached on disk at `~/.cache/aimock/fixtures/<sha256-of-url>/` (honors `$XDG_CACHE_HOME`); when paired with `--validate-on-load`, a fetch failure with a valid cached copy logs a warning and continues — without a cache, the process exits non-zero. HTTP fetches have a 10s timeout and a 50 MB body cap; redirects are rejected fail-loud, so configure your upstream to serve the final URL directly (GitHub raw content URLs already do).
|
|
109
|
+
|
|
110
|
+
Private and link-local addresses (loopback, RFC1918, CGNAT, cloud metadata, ULA, multicast) are rejected by default to prevent SSRF. For local development or tests that need to hit `127.0.0.1`, opt out with `AIMOCK_ALLOW_PRIVATE_URLS=1`. Tarball and zip URL support is intentionally deferred.
|
|
111
|
+
|
|
101
112
|
## Framework Guides
|
|
102
113
|
|
|
103
114
|
Test your AI agents with aimock — no API keys, no network calls: [LangChain](https://aimock.copilotkit.dev/integrate-langchain) · [CrewAI](https://aimock.copilotkit.dev/integrate-crewai) · [PydanticAI](https://aimock.copilotkit.dev/integrate-pydanticai) · [LlamaIndex](https://aimock.copilotkit.dev/integrate-llamaindex) · [Mastra](https://aimock.copilotkit.dev/integrate-mastra) · [Google ADK](https://aimock.copilotkit.dev/integrate-adk) · [Microsoft Agent Framework](https://aimock.copilotkit.dev/integrate-maf)
|
package/dist/cli.cjs
CHANGED
|
@@ -5,6 +5,7 @@ const require_logger = require('./logger.cjs');
|
|
|
5
5
|
const require_server = require('./server.cjs');
|
|
6
6
|
const require_agui_mock = require('./agui-mock.cjs');
|
|
7
7
|
const require_watcher = require('./watcher.cjs');
|
|
8
|
+
const require_fixtures_remote = require('./fixtures-remote.cjs');
|
|
8
9
|
let node_util = require("node:util");
|
|
9
10
|
let node_path = require("node:path");
|
|
10
11
|
let node_fs = require("node:fs");
|
|
@@ -16,7 +17,9 @@ Usage: aimock [options]
|
|
|
16
17
|
Options:
|
|
17
18
|
-p, --port <number> Port to listen on (default: 4010)
|
|
18
19
|
-h, --host <string> Host to bind to (default: 127.0.0.1)
|
|
19
|
-
-f, --fixtures <
|
|
20
|
+
-f, --fixtures <value> Fixture source (repeatable). Accepts:
|
|
21
|
+
- filesystem path to a directory or .json file (default: ./fixtures)
|
|
22
|
+
- https:// or http:// URL to a .json fixture file
|
|
20
23
|
-l, --latency <ms> Latency in ms between SSE chunks (default: 0)
|
|
21
24
|
-c, --chunk-size <chars> Chunk size in characters (default: 20)
|
|
22
25
|
-w, --watch Watch fixture path for changes and reload
|
|
@@ -59,7 +62,7 @@ const { values } = (0, node_util.parseArgs)({
|
|
|
59
62
|
fixtures: {
|
|
60
63
|
type: "string",
|
|
61
64
|
short: "f",
|
|
62
|
-
|
|
65
|
+
multiple: true
|
|
63
66
|
},
|
|
64
67
|
latency: {
|
|
65
68
|
type: "string",
|
|
@@ -143,7 +146,7 @@ const port = Number(values.port);
|
|
|
143
146
|
const host = values.host;
|
|
144
147
|
const latency = Number(values.latency);
|
|
145
148
|
const chunkSize = Number(values["chunk-size"]);
|
|
146
|
-
const
|
|
149
|
+
const fixtureValues = values.fixtures && values.fixtures.length > 0 ? values.fixtures : ["./fixtures"];
|
|
147
150
|
const watchMode = values.watch;
|
|
148
151
|
const validateOnLoad = values["validate-on-load"];
|
|
149
152
|
const logLevelStr = values["log-level"];
|
|
@@ -228,9 +231,14 @@ if (values.record || values["proxy-only"]) {
|
|
|
228
231
|
console.error(`Error: --${values["proxy-only"] ? "proxy-only" : "record"} requires at least one --provider-* flag`);
|
|
229
232
|
process.exit(1);
|
|
230
233
|
}
|
|
234
|
+
const recordBase = fixtureValues[0];
|
|
235
|
+
if (/^https?:\/\//i.test(recordBase)) {
|
|
236
|
+
console.error(`Error: --record/--proxy-only requires a local --fixtures path for the recording destination; got URL ${recordBase}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
231
239
|
record = {
|
|
232
240
|
providers,
|
|
233
|
-
fixturePath: (0, node_path.resolve)(
|
|
241
|
+
fixturePath: (0, node_path.resolve)(recordBase, "recorded"),
|
|
234
242
|
proxyOnly: values["proxy-only"]
|
|
235
243
|
};
|
|
236
244
|
}
|
|
@@ -240,10 +248,15 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
240
248
|
console.error("Error: --agui-record/--agui-proxy-only requires --agui-upstream");
|
|
241
249
|
process.exit(1);
|
|
242
250
|
}
|
|
251
|
+
const aguiBase = fixtureValues[0];
|
|
252
|
+
if (/^https?:\/\//i.test(aguiBase)) {
|
|
253
|
+
console.error(`Error: --agui-record/--agui-proxy-only requires a local --fixtures path for the recording destination; got URL ${aguiBase}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
243
256
|
const agui = new require_agui_mock.AGUIMock();
|
|
244
257
|
agui.enableRecording({
|
|
245
258
|
upstream: values["agui-upstream"],
|
|
246
|
-
fixturePath: (0, node_path.resolve)(
|
|
259
|
+
fixturePath: (0, node_path.resolve)(aguiBase, "agui-recorded"),
|
|
247
260
|
proxyOnly: values["agui-proxy-only"]
|
|
248
261
|
});
|
|
249
262
|
aguiMount = {
|
|
@@ -251,21 +264,46 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
251
264
|
handler: agui
|
|
252
265
|
};
|
|
253
266
|
}
|
|
254
|
-
async function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
267
|
+
async function resolveAllFixtureSources() {
|
|
268
|
+
const resolved = [];
|
|
269
|
+
for (const value of fixtureValues) {
|
|
270
|
+
let local;
|
|
271
|
+
try {
|
|
272
|
+
local = await require_fixtures_remote.resolveFixturesValue(value, {
|
|
273
|
+
validateOnLoad,
|
|
274
|
+
logger
|
|
275
|
+
});
|
|
276
|
+
} catch (err) {
|
|
264
277
|
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
-
console.error(`Failed to
|
|
278
|
+
console.error(`Failed to resolve --fixtures value "${value}": ${msg}`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
if (!local.path) continue;
|
|
282
|
+
try {
|
|
283
|
+
const stat = (0, node_fs.statSync)(local.path);
|
|
284
|
+
resolved.push({
|
|
285
|
+
source: local.source,
|
|
286
|
+
path: local.path,
|
|
287
|
+
isDir: stat.isDirectory()
|
|
288
|
+
});
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (err.code === "ENOENT") console.error(`Fixtures path not found: ${local.path}`);
|
|
291
|
+
else {
|
|
292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
console.error(`Failed to load fixtures from ${local.path}: ${msg}`);
|
|
294
|
+
}
|
|
295
|
+
process.exit(1);
|
|
266
296
|
}
|
|
267
|
-
process.exit(1);
|
|
268
297
|
}
|
|
298
|
+
return resolved;
|
|
299
|
+
}
|
|
300
|
+
function loadSource(source) {
|
|
301
|
+
return source.isDir ? require_fixture_loader.loadFixturesFromDir(source.path, logger) : require_fixture_loader.loadFixtureFile(source.path, logger);
|
|
302
|
+
}
|
|
303
|
+
async function main() {
|
|
304
|
+
const sources = await resolveAllFixtureSources();
|
|
305
|
+
const fixtures = [];
|
|
306
|
+
for (const src of sources) fixtures.push(...loadSource(src));
|
|
269
307
|
if (fixtures.length === 0) {
|
|
270
308
|
if (validateOnLoad || values.strict) {
|
|
271
309
|
console.error("Error: No fixtures loaded and validation/strict mode is enabled — aborting.");
|
|
@@ -273,7 +311,8 @@ async function main() {
|
|
|
273
311
|
}
|
|
274
312
|
console.warn("Warning: No fixtures loaded. The server will return 404 for all requests.");
|
|
275
313
|
}
|
|
276
|
-
|
|
314
|
+
const sourceLabel = sources.map((s) => s.source).join(", ") || "<none>";
|
|
315
|
+
logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);
|
|
277
316
|
if (validateOnLoad) {
|
|
278
317
|
const results = require_fixture_loader.validateFixtures(fixtures);
|
|
279
318
|
const errors = results.filter((r) => r.severity === "error");
|
|
@@ -302,12 +341,17 @@ async function main() {
|
|
|
302
341
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
303
342
|
let watcher = null;
|
|
304
343
|
if (watchMode) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
344
|
+
const primary = sources[0];
|
|
345
|
+
if (!primary) logger.warn("--watch requested but no resolvable fixture sources; skipping watcher");
|
|
346
|
+
else {
|
|
347
|
+
const loadFn = () => loadSource(primary);
|
|
348
|
+
watcher = require_watcher.watchFixtures(primary.path, fixtures, loadFn, {
|
|
349
|
+
logger,
|
|
350
|
+
validate: validateOnLoad,
|
|
351
|
+
validateFn: require_fixture_loader.validateFixtures
|
|
352
|
+
});
|
|
353
|
+
logger.info(`Watching ${primary.path} for changes`);
|
|
354
|
+
}
|
|
311
355
|
}
|
|
312
356
|
function shutdown() {
|
|
313
357
|
logger.info("Shutting down...");
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["Logger","AGUIMock","loadFixturesFromDir","loadFixtureFile","validateFixtures","createServer","watchFixtures"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["Logger","AGUIMock","resolveFixturesValue","loadFixturesFromDir","loadFixtureFile","validateFixtures","createServer","watchFixtures"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport { resolveFixturesValue } from \"./fixtures-remote.js\";\nimport type { Fixture, ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <value> Fixture source (repeatable). Accepts:\n - filesystem path to a directory or .json file (default: ./fixtures)\n - https:// or http:// URL to a .json fixture file\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", multiple: true },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixtureValues: string[] =\n values.fixtures && values.fixtures.length > 0 ? values.fixtures : [\"./fixtures\"];\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n // For record mode, use the first --fixtures value as the base path.\n // Remote URL sources are not supported as record destinations — bail out with a clear error.\n const recordBase = fixtureValues[0];\n if (/^https?:\\/\\//i.test(recordBase)) {\n console.error(\n `Error: --record/--proxy-only requires a local --fixtures path for the recording destination; got URL ${recordBase}`,\n );\n process.exit(1);\n }\n record = {\n providers,\n fixturePath: resolve(recordBase, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const aguiBase = fixtureValues[0];\n if (/^https?:\\/\\//i.test(aguiBase)) {\n console.error(\n `Error: --agui-record/--agui-proxy-only requires a local --fixtures path for the recording destination; got URL ${aguiBase}`,\n );\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(aguiBase, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\ninterface ResolvedFixtureSource {\n source: string;\n path: string;\n isDir: boolean;\n}\n\nasync function resolveAllFixtureSources(): Promise<ResolvedFixtureSource[]> {\n const resolved: ResolvedFixtureSource[] = [];\n for (const value of fixtureValues) {\n let local;\n try {\n local = await resolveFixturesValue(value, {\n validateOnLoad,\n logger,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to resolve --fixtures value \"${value}\": ${msg}`);\n process.exit(1);\n }\n if (!local.path) {\n // Remote fetch failed without validate-on-load and no cache — already warned; skip.\n continue;\n }\n try {\n const stat = statSync(local.path);\n resolved.push({ source: local.source, path: local.path, isDir: stat.isDirectory() });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${local.path}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${local.path}: ${msg}`);\n }\n process.exit(1);\n }\n }\n return resolved;\n}\n\nfunction loadSource(source: ResolvedFixtureSource): Fixture[] {\n return source.isDir\n ? loadFixturesFromDir(source.path, logger)\n : loadFixtureFile(source.path, logger);\n}\n\nasync function main() {\n const sources = await resolveAllFixtureSources();\n\n const fixtures: Fixture[] = [];\n for (const src of sources) {\n fixtures.push(...loadSource(src));\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n const sourceLabel = sources.map((s) => s.source).join(\", \") || \"<none>\";\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested. Only the first local source is watched —\n // remote URL sources are fetched once at boot and are not monitored.\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const primary = sources[0];\n if (!primary) {\n logger.warn(\"--watch requested but no resolvable fixture sources; skipping watcher\");\n } else {\n const loadFn = (): Fixture[] => loadSource(primary);\n watcher = watchFixtures(primary.path, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${primary.path} for changes`);\n }\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Logger } from "./logger.js";
|
|
|
4
4
|
import { createServer } from "./server.js";
|
|
5
5
|
import { AGUIMock } from "./agui-mock.js";
|
|
6
6
|
import { watchFixtures } from "./watcher.js";
|
|
7
|
+
import { resolveFixturesValue } from "./fixtures-remote.js";
|
|
7
8
|
import { parseArgs } from "node:util";
|
|
8
9
|
import { resolve } from "node:path";
|
|
9
10
|
import { statSync } from "node:fs";
|
|
@@ -15,7 +16,9 @@ Usage: aimock [options]
|
|
|
15
16
|
Options:
|
|
16
17
|
-p, --port <number> Port to listen on (default: 4010)
|
|
17
18
|
-h, --host <string> Host to bind to (default: 127.0.0.1)
|
|
18
|
-
-f, --fixtures <
|
|
19
|
+
-f, --fixtures <value> Fixture source (repeatable). Accepts:
|
|
20
|
+
- filesystem path to a directory or .json file (default: ./fixtures)
|
|
21
|
+
- https:// or http:// URL to a .json fixture file
|
|
19
22
|
-l, --latency <ms> Latency in ms between SSE chunks (default: 0)
|
|
20
23
|
-c, --chunk-size <chars> Chunk size in characters (default: 20)
|
|
21
24
|
-w, --watch Watch fixture path for changes and reload
|
|
@@ -58,7 +61,7 @@ const { values } = parseArgs({
|
|
|
58
61
|
fixtures: {
|
|
59
62
|
type: "string",
|
|
60
63
|
short: "f",
|
|
61
|
-
|
|
64
|
+
multiple: true
|
|
62
65
|
},
|
|
63
66
|
latency: {
|
|
64
67
|
type: "string",
|
|
@@ -142,7 +145,7 @@ const port = Number(values.port);
|
|
|
142
145
|
const host = values.host;
|
|
143
146
|
const latency = Number(values.latency);
|
|
144
147
|
const chunkSize = Number(values["chunk-size"]);
|
|
145
|
-
const
|
|
148
|
+
const fixtureValues = values.fixtures && values.fixtures.length > 0 ? values.fixtures : ["./fixtures"];
|
|
146
149
|
const watchMode = values.watch;
|
|
147
150
|
const validateOnLoad = values["validate-on-load"];
|
|
148
151
|
const logLevelStr = values["log-level"];
|
|
@@ -227,9 +230,14 @@ if (values.record || values["proxy-only"]) {
|
|
|
227
230
|
console.error(`Error: --${values["proxy-only"] ? "proxy-only" : "record"} requires at least one --provider-* flag`);
|
|
228
231
|
process.exit(1);
|
|
229
232
|
}
|
|
233
|
+
const recordBase = fixtureValues[0];
|
|
234
|
+
if (/^https?:\/\//i.test(recordBase)) {
|
|
235
|
+
console.error(`Error: --record/--proxy-only requires a local --fixtures path for the recording destination; got URL ${recordBase}`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
230
238
|
record = {
|
|
231
239
|
providers,
|
|
232
|
-
fixturePath: resolve(
|
|
240
|
+
fixturePath: resolve(recordBase, "recorded"),
|
|
233
241
|
proxyOnly: values["proxy-only"]
|
|
234
242
|
};
|
|
235
243
|
}
|
|
@@ -239,10 +247,15 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
239
247
|
console.error("Error: --agui-record/--agui-proxy-only requires --agui-upstream");
|
|
240
248
|
process.exit(1);
|
|
241
249
|
}
|
|
250
|
+
const aguiBase = fixtureValues[0];
|
|
251
|
+
if (/^https?:\/\//i.test(aguiBase)) {
|
|
252
|
+
console.error(`Error: --agui-record/--agui-proxy-only requires a local --fixtures path for the recording destination; got URL ${aguiBase}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
242
255
|
const agui = new AGUIMock();
|
|
243
256
|
agui.enableRecording({
|
|
244
257
|
upstream: values["agui-upstream"],
|
|
245
|
-
fixturePath: resolve(
|
|
258
|
+
fixturePath: resolve(aguiBase, "agui-recorded"),
|
|
246
259
|
proxyOnly: values["agui-proxy-only"]
|
|
247
260
|
});
|
|
248
261
|
aguiMount = {
|
|
@@ -250,21 +263,46 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
250
263
|
handler: agui
|
|
251
264
|
};
|
|
252
265
|
}
|
|
253
|
-
async function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
async function resolveAllFixtureSources() {
|
|
267
|
+
const resolved = [];
|
|
268
|
+
for (const value of fixtureValues) {
|
|
269
|
+
let local;
|
|
270
|
+
try {
|
|
271
|
+
local = await resolveFixturesValue(value, {
|
|
272
|
+
validateOnLoad,
|
|
273
|
+
logger
|
|
274
|
+
});
|
|
275
|
+
} catch (err) {
|
|
263
276
|
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
-
console.error(`Failed to
|
|
277
|
+
console.error(`Failed to resolve --fixtures value "${value}": ${msg}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
if (!local.path) continue;
|
|
281
|
+
try {
|
|
282
|
+
const stat = statSync(local.path);
|
|
283
|
+
resolved.push({
|
|
284
|
+
source: local.source,
|
|
285
|
+
path: local.path,
|
|
286
|
+
isDir: stat.isDirectory()
|
|
287
|
+
});
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (err.code === "ENOENT") console.error(`Fixtures path not found: ${local.path}`);
|
|
290
|
+
else {
|
|
291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
292
|
+
console.error(`Failed to load fixtures from ${local.path}: ${msg}`);
|
|
293
|
+
}
|
|
294
|
+
process.exit(1);
|
|
265
295
|
}
|
|
266
|
-
process.exit(1);
|
|
267
296
|
}
|
|
297
|
+
return resolved;
|
|
298
|
+
}
|
|
299
|
+
function loadSource(source) {
|
|
300
|
+
return source.isDir ? loadFixturesFromDir(source.path, logger) : loadFixtureFile(source.path, logger);
|
|
301
|
+
}
|
|
302
|
+
async function main() {
|
|
303
|
+
const sources = await resolveAllFixtureSources();
|
|
304
|
+
const fixtures = [];
|
|
305
|
+
for (const src of sources) fixtures.push(...loadSource(src));
|
|
268
306
|
if (fixtures.length === 0) {
|
|
269
307
|
if (validateOnLoad || values.strict) {
|
|
270
308
|
console.error("Error: No fixtures loaded and validation/strict mode is enabled — aborting.");
|
|
@@ -272,7 +310,8 @@ async function main() {
|
|
|
272
310
|
}
|
|
273
311
|
console.warn("Warning: No fixtures loaded. The server will return 404 for all requests.");
|
|
274
312
|
}
|
|
275
|
-
|
|
313
|
+
const sourceLabel = sources.map((s) => s.source).join(", ") || "<none>";
|
|
314
|
+
logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);
|
|
276
315
|
if (validateOnLoad) {
|
|
277
316
|
const results = validateFixtures(fixtures);
|
|
278
317
|
const errors = results.filter((r) => r.severity === "error");
|
|
@@ -301,12 +340,17 @@ async function main() {
|
|
|
301
340
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
302
341
|
let watcher = null;
|
|
303
342
|
if (watchMode) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
343
|
+
const primary = sources[0];
|
|
344
|
+
if (!primary) logger.warn("--watch requested but no resolvable fixture sources; skipping watcher");
|
|
345
|
+
else {
|
|
346
|
+
const loadFn = () => loadSource(primary);
|
|
347
|
+
watcher = watchFixtures(primary.path, fixtures, loadFn, {
|
|
348
|
+
logger,
|
|
349
|
+
validate: validateOnLoad,
|
|
350
|
+
validateFn: validateFixtures
|
|
351
|
+
});
|
|
352
|
+
logger.info(`Watching ${primary.path} for changes`);
|
|
353
|
+
}
|
|
310
354
|
}
|
|
311
355
|
function shutdown() {
|
|
312
356
|
logger.info("Shutting down...");
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":";;;;;;;;;;;AAWA,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiCX,MAAM;AAER,MAAM,EAAE,WAAW,UAAU;CAC3B,SAAS;EACP,MAAM;GAAE,MAAM;GAAU,OAAO;GAAK,SAAS;GAAQ;EACrD,MAAM;GAAE,MAAM;GAAU,OAAO;GAAK,SAAS;GAAa;EAC1D,UAAU;GAAE,MAAM;GAAU,OAAO;GAAK,SAAS;GAAc;EAC/D,SAAS;GAAE,MAAM;GAAU,OAAO;GAAK,SAAS;GAAK;EACrD,cAAc;GAAE,MAAM;GAAU,OAAO;GAAK,SAAS;GAAM;EAC3D,OAAO;GAAE,MAAM;GAAW,OAAO;GAAK,SAAS;GAAO;EACtD,aAAa;GAAE,MAAM;GAAU,SAAS;GAAQ;EAChD,oBAAoB;GAAE,MAAM;GAAW,SAAS;GAAO;EACvD,SAAS;GAAE,MAAM;GAAW,SAAS;GAAO;EAC5C,QAAQ;GAAE,MAAM;GAAW,SAAS;GAAO;EAC3C,cAAc;GAAE,MAAM;GAAW,SAAS;GAAO;EACjD,QAAQ;GAAE,MAAM;GAAW,SAAS;GAAO;EAC3C,mBAAmB,EAAE,MAAM,UAAU;EACrC,sBAAsB,EAAE,MAAM,UAAU;EACxC,mBAAmB,EAAE,MAAM,UAAU;EACrC,qBAAqB,EAAE,MAAM,UAAU;EACvC,oBAAoB,EAAE,MAAM,UAAU;EACtC,kBAAkB,EAAE,MAAM,UAAU;EACpC,mBAAmB,EAAE,MAAM,UAAU;EACrC,mBAAmB,EAAE,MAAM,UAAU;EACrC,eAAe;GAAE,MAAM;GAAW,SAAS;GAAO;EAClD,iBAAiB,EAAE,MAAM,UAAU;EACnC,mBAAmB;GAAE,MAAM;GAAW,SAAS;GAAO;EACtD,cAAc,EAAE,MAAM,UAAU;EAChC,mBAAmB,EAAE,MAAM,UAAU;EACrC,oBAAoB,EAAE,MAAM,UAAU;EACtC,eAAe;GAAE,MAAM;GAAU,SAAS;GAAQ;EAClD,sBAAsB;GAAE,MAAM;GAAU,SAAS;GAAO;EACxD,MAAM;GAAE,MAAM;GAAW,SAAS;GAAO;EAC1C;CACD,QAAQ;CACT,CAAC;AAEF,IAAI,OAAO,MAAM;AACf,SAAQ,IAAI,KAAK;AACjB,SAAQ,KAAK,EAAE;;AAGjB,MAAM,OAAO,OAAO,OAAO,KAAK;AAChC,MAAM,OAAO,OAAO;AACpB,MAAM,UAAU,OAAO,OAAO,QAAQ;AACtC,MAAM,YAAY,OAAO,OAAO,cAAc;AAC9C,MAAM,cAAc,QAAQ,OAAO,SAAU;AAC7C,MAAM,YAAY,OAAO;AACzB,MAAM,iBAAiB,OAAO;AAC9B,MAAM,cAAc,OAAO;AAE3B,IAAI,CAAC;CAAC;CAAU;CAAQ;CAAQ,CAAC,SAAS,YAAY,EAAE;AACtD,SAAQ,MAAM,sBAAsB,YAAY,mCAAmC;AACnF,SAAQ,KAAK,EAAE;;AAEjB,MAAM,WAAW;AAEjB,IAAI,OAAO,MAAM,KAAK,IAAI,OAAO,KAAK,OAAO,OAAO;AAClD,SAAQ,MAAM,iBAAiB,OAAO,OAAO;AAC7C,SAAQ,KAAK,EAAE;;AAGjB,IAAI,OAAO,MAAM,QAAQ,IAAI,UAAU,GAAG;AACxC,SAAQ,MAAM,oBAAoB,OAAO,UAAU;AACnD,SAAQ,KAAK,EAAE;;AAGjB,IAAI,OAAO,MAAM,UAAU,IAAI,YAAY,GAAG;AAC5C,SAAQ,MAAM,uBAAuB,OAAO,gBAAgB;AAC5D,SAAQ,KAAK,EAAE;;AAGjB,MAAM,aAAa,OAAO,OAAO,eAAe;AAChD,IAAI,OAAO,MAAM,WAAW,IAAI,CAAC,OAAO,UAAU,WAAW,IAAI,aAAa,GAAG;AAC/E,SAAQ,MACN,wBAAwB,OAAO,eAAe,6DAC/C;AACD,SAAQ,KAAK,EAAE;;AAGjB,MAAM,sBAAsB,OAAO;AACnC,MAAM,mBAAmB,OAAO,oBAAoB;AACpD,IAAI,OAAO,MAAM,iBAAiB,IAAI,CAAC,OAAO,UAAU,iBAAiB,IAAI,mBAAmB,GAAG;AACjG,SAAQ,MACN,+BAA+B,oBAAoB,kDACpD;AACD,SAAQ,KAAK,EAAE;;AAGjB,MAAM,SAAS,IAAI,OAAO,SAAS;AAGnC,IAAI;AACJ;CACE,MAAM,UAAU,OAAO;CACvB,MAAM,eAAe,OAAO;CAC5B,MAAM,gBAAgB,OAAO;AAE7B,KAAI,YAAY,UAAa,iBAAiB,UAAa,kBAAkB,QAAW;AACtF,UAAQ,EAAE;AACV,MAAI,YAAY,QAAW;GACzB,MAAM,MAAM,WAAW,QAAQ;AAC/B,OAAI,MAAM,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG;AACpC,YAAQ,MAAM,uBAAuB,QAAQ,gBAAgB;AAC7D,YAAQ,KAAK,EAAE;;AAEjB,SAAM,WAAW;;AAEnB,MAAI,iBAAiB,QAAW;GAC9B,MAAM,MAAM,WAAW,aAAa;AACpC,OAAI,MAAM,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG;AACpC,YAAQ,MAAM,4BAA4B,aAAa,gBAAgB;AACvE,YAAQ,KAAK,EAAE;;AAEjB,SAAM,gBAAgB;;AAExB,MAAI,kBAAkB,QAAW;GAC/B,MAAM,MAAM,WAAW,cAAc;AACrC,OAAI,MAAM,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG;AACpC,YAAQ,MAAM,6BAA6B,cAAc,gBAAgB;AACzE,YAAQ,KAAK,EAAE;;AAEjB,SAAM,iBAAiB;;;;AAM7B,IAAI;AACJ,IAAI,OAAO,UAAU,OAAO,eAAe;CACzC,MAAM,YAAuC,EAAE;AAC/C,KAAI,OAAO,mBAAoB,WAAU,SAAS,OAAO;AACzD,KAAI,OAAO,sBAAuB,WAAU,YAAY,OAAO;AAC/D,KAAI,OAAO,mBAAoB,WAAU,SAAS,OAAO;AACzD,KAAI,OAAO,qBAAsB,WAAU,WAAW,OAAO;AAC7D,KAAI,OAAO,oBAAqB,WAAU,UAAU,OAAO;AAC3D,KAAI,OAAO,kBAAmB,WAAU,QAAQ,OAAO;AACvD,KAAI,OAAO,mBAAoB,WAAU,SAAS,OAAO;AACzD,KAAI,OAAO,mBAAoB,WAAU,SAAS,OAAO;AAEzD,KAAI,OAAO,KAAK,UAAU,CAAC,WAAW,GAAG;AACvC,UAAQ,MACN,YAAY,OAAO,gBAAgB,eAAe,SAAS,0CAC5D;AACD,UAAQ,KAAK,EAAE;;AAGjB,UAAS;EACP;EACA,aAAa,QAAQ,aAAa,WAAW;EAC7C,WAAW,OAAO;EACnB;;AAIH,IAAI;AACJ,IAAI,OAAO,kBAAkB,OAAO,oBAAoB;AACtD,KAAI,CAAC,OAAO,kBAAkB;AAC5B,UAAQ,MAAM,kEAAkE;AAChF,UAAQ,KAAK,EAAE;;CAEjB,MAAM,OAAO,IAAI,UAAU;AAC3B,MAAK,gBAAgB;EACnB,UAAU,OAAO;EACjB,aAAa,QAAQ,aAAa,gBAAgB;EAClD,WAAW,OAAO;EACnB,CAAC;AACF,aAAY;EAAE,MAAM;EAAS,SAAS;EAAM;;AAG9C,eAAe,OAAO;CAEpB,IAAI;CACJ,IAAI;AACJ,KAAI;AAEF,UADa,SAAS,YAAY,CACrB,aAAa;AAC1B,MAAI,MACF,YAAW,oBAAoB,aAAa,OAAO;MAEnD,YAAW,gBAAgB,aAAa,OAAO;UAE1C,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,SAAQ,MAAM,4BAA4B,cAAc;OACnD;GACL,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAQ,MAAM,gCAAgC,YAAY,IAAI,MAAM;;AAEtE,UAAQ,KAAK,EAAE;;AAGjB,KAAI,SAAS,WAAW,GAAG;AACzB,MAAI,kBAAkB,OAAO,QAAQ;AACnC,WAAQ,MAAM,8EAA8E;AAC5F,WAAQ,KAAK,EAAE;;AAEjB,UAAQ,KAAK,4EAA4E;;AAG3F,QAAO,KAAK,UAAU,SAAS,OAAO,mBAAmB,cAAc;AAGvE,KAAI,gBAAgB;EAClB,MAAM,UAAU,iBAAiB,SAAS;EAC1C,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;EAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,OAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAExD,OAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGzD,MAAI,OAAO,SAAS,GAAG;AACrB,WAAQ,MAAM,sBAAsB,OAAO,OAAO,aAAa,SAAS,OAAO,aAAa;AAC5F,WAAQ,KAAK,EAAE;;;CAInB,MAAM,SAAS,YAAY,CAAC,UAAU,GAAG;CAEzC,MAAM,WAAW,MAAM,aACrB,UACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA,SAAS,OAAO;EAChB;EACA,QAAQ,OAAO;EACf,mBAAmB;EACnB,yBAAyB;EAC1B,EACD,OACD;AAED,QAAO,KAAK,8BAA8B,SAAS,MAAM;CAGzD,IAAI,UAAwC;AAC5C,KAAI,WAAW;AAKb,YAAU,cAAc,aAAa,UAJtB,cACL,oBAAoB,aAAa,OAAO,SACxC,gBAAgB,aAAa,OAAO,EAES;GACrD;GACA,UAAU;GACV,YAAY;GACb,CAAC;AACF,SAAO,KAAK,YAAY,YAAY,cAAc;;CAGpD,SAAS,WAAW;AAClB,SAAO,KAAK,mBAAmB;AAC/B,MAAI,QAAS,SAAQ,OAAO;AAC5B,WAAS,OAAO,YAAY;AAC1B,WAAQ,KAAK,EAAE;IACf;;AAGJ,SAAQ,GAAG,UAAU,SAAS;AAC9B,SAAQ,GAAG,WAAW,SAAS;;AAGjC,MAAM,CAAC,OAAO,QAAQ;AACpB,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport { resolveFixturesValue } from \"./fixtures-remote.js\";\nimport type { Fixture, ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <value> Fixture source (repeatable). Accepts:\n - filesystem path to a directory or .json file (default: ./fixtures)\n - https:// or http:// URL to a .json fixture file\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", multiple: true },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixtureValues: string[] =\n values.fixtures && values.fixtures.length > 0 ? values.fixtures : [\"./fixtures\"];\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n // For record mode, use the first --fixtures value as the base path.\n // Remote URL sources are not supported as record destinations — bail out with a clear error.\n const recordBase = fixtureValues[0];\n if (/^https?:\\/\\//i.test(recordBase)) {\n console.error(\n `Error: --record/--proxy-only requires a local --fixtures path for the recording destination; got URL ${recordBase}`,\n );\n process.exit(1);\n }\n record = {\n providers,\n fixturePath: resolve(recordBase, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const aguiBase = fixtureValues[0];\n if (/^https?:\\/\\//i.test(aguiBase)) {\n console.error(\n `Error: --agui-record/--agui-proxy-only requires a local --fixtures path for the recording destination; got URL ${aguiBase}`,\n );\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(aguiBase, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\ninterface ResolvedFixtureSource {\n source: string;\n path: string;\n isDir: boolean;\n}\n\nasync function resolveAllFixtureSources(): Promise<ResolvedFixtureSource[]> {\n const resolved: ResolvedFixtureSource[] = [];\n for (const value of fixtureValues) {\n let local;\n try {\n local = await resolveFixturesValue(value, {\n validateOnLoad,\n logger,\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to resolve --fixtures value \"${value}\": ${msg}`);\n process.exit(1);\n }\n if (!local.path) {\n // Remote fetch failed without validate-on-load and no cache — already warned; skip.\n continue;\n }\n try {\n const stat = statSync(local.path);\n resolved.push({ source: local.source, path: local.path, isDir: stat.isDirectory() });\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${local.path}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${local.path}: ${msg}`);\n }\n process.exit(1);\n }\n }\n return resolved;\n}\n\nfunction loadSource(source: ResolvedFixtureSource): Fixture[] {\n return source.isDir\n ? loadFixturesFromDir(source.path, logger)\n : loadFixtureFile(source.path, logger);\n}\n\nasync function main() {\n const sources = await resolveAllFixtureSources();\n\n const fixtures: Fixture[] = [];\n for (const src of sources) {\n fixtures.push(...loadSource(src));\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n const sourceLabel = sources.map((s) => s.source).join(\", \") || \"<none>\";\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested. Only the first local source is watched —\n // remote URL sources are fetched once at boot and are not monitored.\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const primary = sources[0];\n if (!primary) {\n logger.warn(\"--watch requested but no resolvable fixture sources; skipping watcher\");\n } else {\n const loadFn = (): Fixture[] => loadSource(primary);\n watcher = watchFixtures(primary.path, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${primary.path} for changes`);\n }\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.cts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,
|
|
1
|
+
{"version":3,"file":"config-loader.d.cts","names":[],"sources":["../src/config-loader.ts"],"sourcesContent":[],"mappings":";;;;;;;;UAciB,aAAA,SAAsB;;AAAvC;AAIiB,UAAA,iBAAA,CAAiB;EASjB,GAAA,EAAA,MAAA;EAAgB,IAAA,EAAA,MAAA;UAEnB,CAAA,EAAA,MAAA;aAF2B,CAAA,EAAA,MAAA;EAAmB,IAAA,CAAA,EAAA,MAAA;EAM3C,IAAA,CAAA,EAAA,MAAS;;AAGhB,UATO,eAAA,SAAwB,mBAS/B,CAAA;QACI,CAAA,EAAA;IACF,QAAA,EATE,KASF,CAAA;MAAe,IAAA,EAAA,MAAA;MAGV,OAAA,EAAA;QAAgB,IAAA,EAAA,MAAA;QAEvB,IAAA,EAAA,MAAA;MACI,CAAA;IACH,CAAA,CAAA;EAAc,CAAA;AAIzB;AAAgC,UAhBf,SAAA,CAgBe;MACnB,CAAA,EAAA,MAAA;YACH,CAAA,EAAA;IACS,IAAA,EAAA,MAAA;IAHqB,OAAA,EAAA,MAAA;EAAkB,CAAA;EAMzC,KAAA,CAAA,EAnBP,aAmBgB,EAEf;EAGM,SAAA,CAAA,EAvBH,iBAuBoB,EAGvB;EAIM,OAAA,CAAA,EA7BL,eA+BC,EAAA;AAGb;AAAuC,UA/BtB,gBAAA,CA+BsB;SAMxB,EAAA,MAAA;OAHH,CAAA,EAhCF,OAgCE,EAAA;WAKK,CAAA,EApCH,WAoCG,EAAA;EAAW,MAAA,CAAA,EAnCjB,cAmCiB,EAAA;EAGX,OAAA,CAAA,EAAA,MAAY;AAK7B;AAA6B,UAvCZ,cAAA,SAAuB,kBAuCX,CAAA;UAGjB,CAAA,EAzCC,gBAyCD,EAAA;OACC,CAAA,EAzCH,gBAyCG,EAAA;gBAEL,CAAA,EA1CW,gBA0CX,EAAA;;AAEC,UAzCQ,SAAA,CAyCR;MACE,CAAA,EAAA,MAAA;EAAY,MAAA,CAAA,EAxCZ,cAwCY,EAAA;AAQvB;AAKsB,UAlDL,iBAAA,CAkDoB;EAAA,KAAA,EAAA;IAC3B,OAAA,CAAA,EAAA,MAAA;IAEW,QAAA,CAAA,EAAA,MAAA;IAAlB,QAAA,CAAA,EAAA,MAAA;EAAO,CAAA;;WAlDC;;;UAIM,UAAA;;aAEJ;;UAGI,sBAAA;;;YAGL;;;eAGG;;iBAEE;;UAGA,YAAA;;gBAED;;UAGC,YAAA;;;YAGL;aACC;;QAEL;QACA;SACC;WACE;;;;;;;;;;;iBAQK,UAAA,sBAAgC;iBAK1B,eAAA,SACZ;;;IAEP;UAAkB"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
let node_dns_promises = require("node:dns/promises");
|
|
6
|
+
let node_net = require("node:net");
|
|
7
|
+
let node_os = require("node:os");
|
|
8
|
+
|
|
9
|
+
//#region src/fixtures-remote.ts
|
|
10
|
+
const REMOTE_FETCH_TIMEOUT_MS = 1e4;
|
|
11
|
+
const REMOTE_MAX_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Private / reserved address ranges blocked by default to prevent SSRF.
|
|
14
|
+
*
|
|
15
|
+
* The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /
|
|
16
|
+
* ULA / multicast / unspecified — any destination that could let an attacker
|
|
17
|
+
* pivot a fetch into the local network or cloud control plane via a hostile
|
|
18
|
+
* `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required
|
|
19
|
+
* for local dev / tests that target 127.0.0.1).
|
|
20
|
+
*/
|
|
21
|
+
const PRIVATE_V4_RANGES = [
|
|
22
|
+
["0.0.0.0", 8],
|
|
23
|
+
["10.0.0.0", 8],
|
|
24
|
+
["100.64.0.0", 10],
|
|
25
|
+
["127.0.0.0", 8],
|
|
26
|
+
["169.254.0.0", 16],
|
|
27
|
+
["172.16.0.0", 12],
|
|
28
|
+
["192.0.0.0", 24],
|
|
29
|
+
["192.0.2.0", 24],
|
|
30
|
+
["192.88.99.0", 24],
|
|
31
|
+
["192.168.0.0", 16],
|
|
32
|
+
["198.18.0.0", 15],
|
|
33
|
+
["198.51.100.0", 24],
|
|
34
|
+
["203.0.113.0", 24],
|
|
35
|
+
["224.0.0.0", 4],
|
|
36
|
+
["240.0.0.0", 4],
|
|
37
|
+
["255.255.255.255", 32]
|
|
38
|
+
];
|
|
39
|
+
const PRIVATE_V6_RANGES = [
|
|
40
|
+
["::", 128],
|
|
41
|
+
["::1", 128],
|
|
42
|
+
["fc00::", 7],
|
|
43
|
+
["fe80::", 10]
|
|
44
|
+
];
|
|
45
|
+
function buildBlockList() {
|
|
46
|
+
const bl = new node_net.BlockList();
|
|
47
|
+
for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, "ipv4");
|
|
48
|
+
for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, "ipv6");
|
|
49
|
+
return bl;
|
|
50
|
+
}
|
|
51
|
+
const PRIVATE_BLOCKLIST = buildBlockList();
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if `address` is a literal IP (v4 or v6) that falls in any
|
|
54
|
+
* blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,
|
|
55
|
+
* ULA, multicast, unspecified, reserved). Returns false for public IPs
|
|
56
|
+
* and for non-literal hostnames.
|
|
57
|
+
*/
|
|
58
|
+
function isPrivateAddress(address) {
|
|
59
|
+
const family = (0, node_net.isIP)(address);
|
|
60
|
+
if (family === 0) return false;
|
|
61
|
+
if (family === 6) {
|
|
62
|
+
const mapped = address.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
63
|
+
if (mapped) return isPrivateAddress(mapped[1]);
|
|
64
|
+
}
|
|
65
|
+
return PRIVATE_BLOCKLIST.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
66
|
+
}
|
|
67
|
+
function privateUrlsAllowed() {
|
|
68
|
+
const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;
|
|
69
|
+
return v === "1" || v === "true";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Throws if `hostname` resolves to (or literally is) a private / reserved
|
|
73
|
+
* address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is
|
|
74
|
+
* not a literal IP, all resolved addresses are checked — any blocked
|
|
75
|
+
* address in the set rejects the host.
|
|
76
|
+
*/
|
|
77
|
+
async function assertAllowedHost(hostname) {
|
|
78
|
+
if (privateUrlsAllowed()) return;
|
|
79
|
+
if ((0, node_net.isIP)(hostname) !== 0) {
|
|
80
|
+
if (isPrivateAddress(hostname)) throw new Error(`Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let addresses;
|
|
84
|
+
try {
|
|
85
|
+
addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const a of addresses) if (isPrivateAddress(a.address)) throw new Error(`Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns true if `value` looks like a URL (has a scheme followed by ://).
|
|
93
|
+
* Path inputs like ./fixtures or /tmp/x never start with a scheme.
|
|
94
|
+
*/
|
|
95
|
+
function looksLikeUrl(value) {
|
|
96
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the default on-disk cache root for fetched fixtures.
|
|
100
|
+
* Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.
|
|
101
|
+
*/
|
|
102
|
+
function defaultCacheRoot() {
|
|
103
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
104
|
+
return (0, node_path.join)(xdg && xdg.length > 0 ? xdg : (0, node_path.join)((0, node_os.homedir)(), ".cache"), "aimock", "fixtures");
|
|
105
|
+
}
|
|
106
|
+
function sha256Hex(input) {
|
|
107
|
+
return (0, node_crypto.createHash)("sha256").update(input).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a single --fixtures value to a local filesystem path.
|
|
111
|
+
*
|
|
112
|
+
* Behavior:
|
|
113
|
+
* - Filesystem path → return as-is.
|
|
114
|
+
* - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.
|
|
115
|
+
* On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).
|
|
116
|
+
* If --validate-on-load is set and no cache is usable, throws.
|
|
117
|
+
* - Any other scheme (file://, ftp://, ...) → throws.
|
|
118
|
+
*/
|
|
119
|
+
async function resolveFixturesValue(value, opts) {
|
|
120
|
+
if (!looksLikeUrl(value)) return {
|
|
121
|
+
source: value,
|
|
122
|
+
path: (0, node_path.resolve)(value)
|
|
123
|
+
};
|
|
124
|
+
const lower = value.toLowerCase();
|
|
125
|
+
if (!lower.startsWith("https://") && !lower.startsWith("http://")) {
|
|
126
|
+
const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
127
|
+
const scheme = match ? match[1] : "unknown";
|
|
128
|
+
throw new Error(`Unsupported --fixtures URL scheme "${scheme}" in ${value} (only https:// and http:// are supported)`);
|
|
129
|
+
}
|
|
130
|
+
return await resolveHttpFixture(value, opts);
|
|
131
|
+
}
|
|
132
|
+
async function resolveHttpFixture(url, opts) {
|
|
133
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
134
|
+
const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();
|
|
135
|
+
const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;
|
|
136
|
+
const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;
|
|
137
|
+
const cacheDir = (0, node_path.join)(cacheRoot, sha256Hex(url));
|
|
138
|
+
const cacheFile = (0, node_path.join)(cacheDir, "fixtures.json");
|
|
139
|
+
try {
|
|
140
|
+
await assertAllowedHost(new URL(url).hostname);
|
|
141
|
+
const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);
|
|
142
|
+
JSON.parse(body);
|
|
143
|
+
(0, node_fs.mkdirSync)(cacheDir, { recursive: true });
|
|
144
|
+
(0, node_fs.writeFileSync)(cacheFile, body, "utf-8");
|
|
145
|
+
opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);
|
|
146
|
+
return {
|
|
147
|
+
source: url,
|
|
148
|
+
path: cacheFile
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
if (cacheFileExists(cacheFile)) {
|
|
153
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`);
|
|
154
|
+
return {
|
|
155
|
+
source: url,
|
|
156
|
+
path: cacheFile
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (opts.validateOnLoad) throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);
|
|
160
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`);
|
|
161
|
+
return {
|
|
162
|
+
source: url,
|
|
163
|
+
path: ""
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function cacheFileExists(file) {
|
|
168
|
+
try {
|
|
169
|
+
return (0, node_fs.statSync)(file).isFile();
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes) {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timeout")), timeoutMs);
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetchImpl(url, {
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
redirect: "manual"
|
|
181
|
+
});
|
|
182
|
+
if (res.status >= 300 && res.status < 400) {
|
|
183
|
+
const location = res.headers.get("location") ?? "<none>";
|
|
184
|
+
throw new Error(`redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`);
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
187
|
+
const len = res.headers.get("content-length");
|
|
188
|
+
if (len) {
|
|
189
|
+
const n = Number(len);
|
|
190
|
+
if (Number.isFinite(n) && n > maxBytes) throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);
|
|
191
|
+
}
|
|
192
|
+
if (!res.body) {
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
if (Buffer.byteLength(text, "utf-8") > maxBytes) throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
const reader = res.body.getReader();
|
|
198
|
+
const chunks = [];
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const { value, done } = await reader.read();
|
|
202
|
+
if (done) break;
|
|
203
|
+
if (value) {
|
|
204
|
+
total += value.byteLength;
|
|
205
|
+
if (total > maxBytes) {
|
|
206
|
+
try {
|
|
207
|
+
await reader.cancel();
|
|
208
|
+
} catch {}
|
|
209
|
+
throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
210
|
+
}
|
|
211
|
+
chunks.push(value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "timeout")) throw new Error(`fetch timed out after ${timeoutMs}ms`);
|
|
217
|
+
throw err;
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
exports.resolveFixturesValue = resolveFixturesValue;
|
|
225
|
+
//# sourceMappingURL=fixtures-remote.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures-remote.cjs","names":["BlockList"],"sources":["../src/fixtures-remote.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, statSync } from \"node:fs\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join, resolve as pathResolve } from \"node:path\";\nimport type { Logger } from \"./logger.js\";\n\nexport const REMOTE_FETCH_TIMEOUT_MS = 10_000;\nexport const REMOTE_MAX_BYTES = 50 * 1024 * 1024; // 50 MB\n\n/**\n * Private / reserved address ranges blocked by default to prevent SSRF.\n *\n * The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /\n * ULA / multicast / unspecified — any destination that could let an attacker\n * pivot a fetch into the local network or cloud control plane via a hostile\n * `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required\n * for local dev / tests that target 127.0.0.1).\n */\nconst PRIVATE_V4_RANGES: Array<[string, number]> = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // RFC1918\n [\"100.64.0.0\", 10], // CGNAT\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local / cloud metadata\n [\"172.16.0.0\", 12], // RFC1918\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // TEST-NET-1\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // RFC1918\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // TEST-NET-2\n [\"203.0.113.0\", 24], // TEST-NET-3\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved\n [\"255.255.255.255\", 32], // broadcast\n];\n\nconst PRIVATE_V6_RANGES: Array<[string, number]> = [\n [\"::\", 128], // unspecified\n [\"::1\", 128], // loopback\n [\"fc00::\", 7], // ULA\n [\"fe80::\", 10], // link-local\n];\n\nfunction buildBlockList(): BlockList {\n const bl = new BlockList();\n for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, \"ipv4\");\n for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, \"ipv6\");\n return bl;\n}\n\nconst PRIVATE_BLOCKLIST: BlockList = buildBlockList();\n\n/**\n * Returns true if `address` is a literal IP (v4 or v6) that falls in any\n * blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,\n * ULA, multicast, unspecified, reserved). Returns false for public IPs\n * and for non-literal hostnames.\n */\nexport function isPrivateAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 0) return false; // not a literal IP\n // BlockList.check's \"ipv6\" bucket does not match v4-mapped ::ffff:a.b.c.d\n // automatically — unwrap to the underlying v4 address and recurse.\n if (family === 6) {\n const lower = address.toLowerCase();\n const mapped = lower.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mapped) return isPrivateAddress(mapped[1]);\n }\n return PRIVATE_BLOCKLIST.check(address, family === 4 ? \"ipv4\" : \"ipv6\");\n}\n\nfunction privateUrlsAllowed(): boolean {\n const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Throws if `hostname` resolves to (or literally is) a private / reserved\n * address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is\n * not a literal IP, all resolved addresses are checked — any blocked\n * address in the set rejects the host.\n */\nexport async function assertAllowedHost(hostname: string): Promise<void> {\n if (privateUrlsAllowed()) return;\n\n if (isIP(hostname) !== 0) {\n if (isPrivateAddress(hostname)) {\n throw new Error(\n `Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n addresses = await dnsLookup(hostname, { all: true });\n } catch (err) {\n // DNS failure is not an SSRF signal — let the fetch itself surface the\n // resolution error with its own (more detailed) message.\n void err;\n return;\n }\n for (const a of addresses) {\n if (isPrivateAddress(a.address)) {\n throw new Error(\n `Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n }\n}\n\nexport interface RemoteResolveOptions {\n validateOnLoad: boolean;\n logger: Logger;\n /** Override fetch implementation (tests). */\n fetchImpl?: typeof fetch;\n /** Override cache root (tests). */\n cacheRoot?: string;\n /** Override timeout (tests). */\n timeoutMs?: number;\n /** Override max response size (tests). */\n maxBytes?: number;\n}\n\nexport interface ResolvedLocalFixture {\n /** Original value as passed on the CLI (for logging). */\n source: string;\n /** Filesystem path — downstream code treats this identically to a --fixtures path. */\n path: string;\n}\n\n/**\n * Returns true if `value` looks like a URL (has a scheme followed by ://).\n * Path inputs like ./fixtures or /tmp/x never start with a scheme.\n */\nexport function looksLikeUrl(value: string): boolean {\n return /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value);\n}\n\n/**\n * Returns the default on-disk cache root for fetched fixtures.\n * Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.\n */\nexport function defaultCacheRoot(): string {\n const xdg = process.env.XDG_CACHE_HOME;\n const base = xdg && xdg.length > 0 ? xdg : join(homedir(), \".cache\");\n return join(base, \"aimock\", \"fixtures\");\n}\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Resolve a single --fixtures value to a local filesystem path.\n *\n * Behavior:\n * - Filesystem path → return as-is.\n * - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.\n * On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).\n * If --validate-on-load is set and no cache is usable, throws.\n * - Any other scheme (file://, ftp://, ...) → throws.\n */\nexport async function resolveFixturesValue(\n value: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n if (!looksLikeUrl(value)) {\n return { source: value, path: pathResolve(value) };\n }\n\n const lower = value.toLowerCase();\n if (!lower.startsWith(\"https://\") && !lower.startsWith(\"http://\")) {\n // Extract the scheme for a clearer error\n const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//);\n const scheme = match ? match[1] : \"unknown\";\n throw new Error(\n `Unsupported --fixtures URL scheme \"${scheme}\" in ${value} (only https:// and http:// are supported)`,\n );\n }\n\n return await resolveHttpFixture(value, opts);\n}\n\nasync function resolveHttpFixture(\n url: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n const fetchImpl = opts.fetchImpl ?? fetch;\n const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();\n const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;\n const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;\n\n const digest = sha256Hex(url);\n const cacheDir = join(cacheRoot, digest);\n const cacheFile = join(cacheDir, \"fixtures.json\");\n\n try {\n // SSRF defense: reject private / reserved destinations before any network\n // I/O, unless explicitly opted in via AIMOCK_ALLOW_PRIVATE_URLS=1.\n const parsed = new URL(url);\n await assertAllowedHost(parsed.hostname);\n\n const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);\n // Parse to verify it is valid JSON before caching — fail loud if not.\n JSON.parse(body);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cacheFile, body, \"utf-8\");\n opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);\n return { source: url, path: cacheFile };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const cacheExists = cacheFileExists(cacheFile);\n if (cacheExists) {\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`,\n );\n return { source: url, path: cacheFile };\n }\n if (opts.validateOnLoad) {\n throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);\n }\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`,\n );\n // Signal \"no path\" by returning a sentinel with empty path — callers detect and skip.\n return { source: url, path: \"\" };\n }\n}\n\nfunction cacheFileExists(file: string): boolean {\n try {\n return statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nasync function fetchWithLimits(\n url: string,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n maxBytes: number,\n): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new Error(\"timeout\")), timeoutMs);\n try {\n // Redirects are disabled: following a 3xx into a different scheme or host\n // would bypass the scheme check and SSRF denylist. Upstream services\n // should serve the final URL directly (e.g. GitHub raw content URLs).\n const res = await fetchImpl(url, {\n signal: controller.signal,\n redirect: \"manual\",\n });\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get(\"location\") ?? \"<none>\";\n throw new Error(\n `redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`,\n );\n }\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n // Early reject on over-large Content-Length when the server reports it.\n const len = res.headers.get(\"content-length\");\n if (len) {\n const n = Number(len);\n if (Number.isFinite(n) && n > maxBytes) {\n throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);\n }\n }\n\n // Stream and enforce the limit incrementally in case Content-Length is absent/lying.\n if (!res.body) {\n const text = await res.text();\n if (Buffer.byteLength(text, \"utf-8\") > maxBytes) {\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n return text;\n }\n\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > maxBytes) {\n try {\n await reader.cancel();\n } catch {\n // ignore cancel errors\n }\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n } catch (err) {\n if (err instanceof Error && (err.name === \"AbortError\" || err.message === \"timeout\")) {\n throw new Error(`fetch timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { lookup } from "node:dns/promises";
|
|
5
|
+
import { BlockList, isIP } from "node:net";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/fixtures-remote.ts
|
|
9
|
+
const REMOTE_FETCH_TIMEOUT_MS = 1e4;
|
|
10
|
+
const REMOTE_MAX_BYTES = 50 * 1024 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Private / reserved address ranges blocked by default to prevent SSRF.
|
|
13
|
+
*
|
|
14
|
+
* The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /
|
|
15
|
+
* ULA / multicast / unspecified — any destination that could let an attacker
|
|
16
|
+
* pivot a fetch into the local network or cloud control plane via a hostile
|
|
17
|
+
* `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required
|
|
18
|
+
* for local dev / tests that target 127.0.0.1).
|
|
19
|
+
*/
|
|
20
|
+
const PRIVATE_V4_RANGES = [
|
|
21
|
+
["0.0.0.0", 8],
|
|
22
|
+
["10.0.0.0", 8],
|
|
23
|
+
["100.64.0.0", 10],
|
|
24
|
+
["127.0.0.0", 8],
|
|
25
|
+
["169.254.0.0", 16],
|
|
26
|
+
["172.16.0.0", 12],
|
|
27
|
+
["192.0.0.0", 24],
|
|
28
|
+
["192.0.2.0", 24],
|
|
29
|
+
["192.88.99.0", 24],
|
|
30
|
+
["192.168.0.0", 16],
|
|
31
|
+
["198.18.0.0", 15],
|
|
32
|
+
["198.51.100.0", 24],
|
|
33
|
+
["203.0.113.0", 24],
|
|
34
|
+
["224.0.0.0", 4],
|
|
35
|
+
["240.0.0.0", 4],
|
|
36
|
+
["255.255.255.255", 32]
|
|
37
|
+
];
|
|
38
|
+
const PRIVATE_V6_RANGES = [
|
|
39
|
+
["::", 128],
|
|
40
|
+
["::1", 128],
|
|
41
|
+
["fc00::", 7],
|
|
42
|
+
["fe80::", 10]
|
|
43
|
+
];
|
|
44
|
+
function buildBlockList() {
|
|
45
|
+
const bl = new BlockList();
|
|
46
|
+
for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, "ipv4");
|
|
47
|
+
for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, "ipv6");
|
|
48
|
+
return bl;
|
|
49
|
+
}
|
|
50
|
+
const PRIVATE_BLOCKLIST = buildBlockList();
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if `address` is a literal IP (v4 or v6) that falls in any
|
|
53
|
+
* blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,
|
|
54
|
+
* ULA, multicast, unspecified, reserved). Returns false for public IPs
|
|
55
|
+
* and for non-literal hostnames.
|
|
56
|
+
*/
|
|
57
|
+
function isPrivateAddress(address) {
|
|
58
|
+
const family = isIP(address);
|
|
59
|
+
if (family === 0) return false;
|
|
60
|
+
if (family === 6) {
|
|
61
|
+
const mapped = address.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
62
|
+
if (mapped) return isPrivateAddress(mapped[1]);
|
|
63
|
+
}
|
|
64
|
+
return PRIVATE_BLOCKLIST.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
65
|
+
}
|
|
66
|
+
function privateUrlsAllowed() {
|
|
67
|
+
const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;
|
|
68
|
+
return v === "1" || v === "true";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Throws if `hostname` resolves to (or literally is) a private / reserved
|
|
72
|
+
* address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is
|
|
73
|
+
* not a literal IP, all resolved addresses are checked — any blocked
|
|
74
|
+
* address in the set rejects the host.
|
|
75
|
+
*/
|
|
76
|
+
async function assertAllowedHost(hostname) {
|
|
77
|
+
if (privateUrlsAllowed()) return;
|
|
78
|
+
if (isIP(hostname) !== 0) {
|
|
79
|
+
if (isPrivateAddress(hostname)) throw new Error(`Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let addresses;
|
|
83
|
+
try {
|
|
84
|
+
addresses = await lookup(hostname, { all: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const a of addresses) if (isPrivateAddress(a.address)) throw new Error(`Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if `value` looks like a URL (has a scheme followed by ://).
|
|
92
|
+
* Path inputs like ./fixtures or /tmp/x never start with a scheme.
|
|
93
|
+
*/
|
|
94
|
+
function looksLikeUrl(value) {
|
|
95
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns the default on-disk cache root for fetched fixtures.
|
|
99
|
+
* Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.
|
|
100
|
+
*/
|
|
101
|
+
function defaultCacheRoot() {
|
|
102
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
103
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".cache"), "aimock", "fixtures");
|
|
104
|
+
}
|
|
105
|
+
function sha256Hex(input) {
|
|
106
|
+
return createHash("sha256").update(input).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a single --fixtures value to a local filesystem path.
|
|
110
|
+
*
|
|
111
|
+
* Behavior:
|
|
112
|
+
* - Filesystem path → return as-is.
|
|
113
|
+
* - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.
|
|
114
|
+
* On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).
|
|
115
|
+
* If --validate-on-load is set and no cache is usable, throws.
|
|
116
|
+
* - Any other scheme (file://, ftp://, ...) → throws.
|
|
117
|
+
*/
|
|
118
|
+
async function resolveFixturesValue(value, opts) {
|
|
119
|
+
if (!looksLikeUrl(value)) return {
|
|
120
|
+
source: value,
|
|
121
|
+
path: resolve(value)
|
|
122
|
+
};
|
|
123
|
+
const lower = value.toLowerCase();
|
|
124
|
+
if (!lower.startsWith("https://") && !lower.startsWith("http://")) {
|
|
125
|
+
const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
126
|
+
const scheme = match ? match[1] : "unknown";
|
|
127
|
+
throw new Error(`Unsupported --fixtures URL scheme "${scheme}" in ${value} (only https:// and http:// are supported)`);
|
|
128
|
+
}
|
|
129
|
+
return await resolveHttpFixture(value, opts);
|
|
130
|
+
}
|
|
131
|
+
async function resolveHttpFixture(url, opts) {
|
|
132
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
133
|
+
const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();
|
|
134
|
+
const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;
|
|
135
|
+
const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;
|
|
136
|
+
const cacheDir = join(cacheRoot, sha256Hex(url));
|
|
137
|
+
const cacheFile = join(cacheDir, "fixtures.json");
|
|
138
|
+
try {
|
|
139
|
+
await assertAllowedHost(new URL(url).hostname);
|
|
140
|
+
const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);
|
|
141
|
+
JSON.parse(body);
|
|
142
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
143
|
+
writeFileSync(cacheFile, body, "utf-8");
|
|
144
|
+
opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);
|
|
145
|
+
return {
|
|
146
|
+
source: url,
|
|
147
|
+
path: cacheFile
|
|
148
|
+
};
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
if (cacheFileExists(cacheFile)) {
|
|
152
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`);
|
|
153
|
+
return {
|
|
154
|
+
source: url,
|
|
155
|
+
path: cacheFile
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (opts.validateOnLoad) throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);
|
|
159
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`);
|
|
160
|
+
return {
|
|
161
|
+
source: url,
|
|
162
|
+
path: ""
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function cacheFileExists(file) {
|
|
167
|
+
try {
|
|
168
|
+
return statSync(file).isFile();
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes) {
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timeout")), timeoutMs);
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetchImpl(url, {
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
redirect: "manual"
|
|
180
|
+
});
|
|
181
|
+
if (res.status >= 300 && res.status < 400) {
|
|
182
|
+
const location = res.headers.get("location") ?? "<none>";
|
|
183
|
+
throw new Error(`redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`);
|
|
184
|
+
}
|
|
185
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
186
|
+
const len = res.headers.get("content-length");
|
|
187
|
+
if (len) {
|
|
188
|
+
const n = Number(len);
|
|
189
|
+
if (Number.isFinite(n) && n > maxBytes) throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);
|
|
190
|
+
}
|
|
191
|
+
if (!res.body) {
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
if (Buffer.byteLength(text, "utf-8") > maxBytes) throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
194
|
+
return text;
|
|
195
|
+
}
|
|
196
|
+
const reader = res.body.getReader();
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let total = 0;
|
|
199
|
+
for (;;) {
|
|
200
|
+
const { value, done } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
if (value) {
|
|
203
|
+
total += value.byteLength;
|
|
204
|
+
if (total > maxBytes) {
|
|
205
|
+
try {
|
|
206
|
+
await reader.cancel();
|
|
207
|
+
} catch {}
|
|
208
|
+
throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
209
|
+
}
|
|
210
|
+
chunks.push(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "timeout")) throw new Error(`fetch timed out after ${timeoutMs}ms`);
|
|
216
|
+
throw err;
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
export { resolveFixturesValue };
|
|
224
|
+
//# sourceMappingURL=fixtures-remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures-remote.js","names":["dnsLookup","pathResolve"],"sources":["../src/fixtures-remote.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, statSync } from \"node:fs\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join, resolve as pathResolve } from \"node:path\";\nimport type { Logger } from \"./logger.js\";\n\nexport const REMOTE_FETCH_TIMEOUT_MS = 10_000;\nexport const REMOTE_MAX_BYTES = 50 * 1024 * 1024; // 50 MB\n\n/**\n * Private / reserved address ranges blocked by default to prevent SSRF.\n *\n * The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /\n * ULA / multicast / unspecified — any destination that could let an attacker\n * pivot a fetch into the local network or cloud control plane via a hostile\n * `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required\n * for local dev / tests that target 127.0.0.1).\n */\nconst PRIVATE_V4_RANGES: Array<[string, number]> = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // RFC1918\n [\"100.64.0.0\", 10], // CGNAT\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local / cloud metadata\n [\"172.16.0.0\", 12], // RFC1918\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // TEST-NET-1\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // RFC1918\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // TEST-NET-2\n [\"203.0.113.0\", 24], // TEST-NET-3\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved\n [\"255.255.255.255\", 32], // broadcast\n];\n\nconst PRIVATE_V6_RANGES: Array<[string, number]> = [\n [\"::\", 128], // unspecified\n [\"::1\", 128], // loopback\n [\"fc00::\", 7], // ULA\n [\"fe80::\", 10], // link-local\n];\n\nfunction buildBlockList(): BlockList {\n const bl = new BlockList();\n for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, \"ipv4\");\n for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, \"ipv6\");\n return bl;\n}\n\nconst PRIVATE_BLOCKLIST: BlockList = buildBlockList();\n\n/**\n * Returns true if `address` is a literal IP (v4 or v6) that falls in any\n * blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,\n * ULA, multicast, unspecified, reserved). Returns false for public IPs\n * and for non-literal hostnames.\n */\nexport function isPrivateAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 0) return false; // not a literal IP\n // BlockList.check's \"ipv6\" bucket does not match v4-mapped ::ffff:a.b.c.d\n // automatically — unwrap to the underlying v4 address and recurse.\n if (family === 6) {\n const lower = address.toLowerCase();\n const mapped = lower.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mapped) return isPrivateAddress(mapped[1]);\n }\n return PRIVATE_BLOCKLIST.check(address, family === 4 ? \"ipv4\" : \"ipv6\");\n}\n\nfunction privateUrlsAllowed(): boolean {\n const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Throws if `hostname` resolves to (or literally is) a private / reserved\n * address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is\n * not a literal IP, all resolved addresses are checked — any blocked\n * address in the set rejects the host.\n */\nexport async function assertAllowedHost(hostname: string): Promise<void> {\n if (privateUrlsAllowed()) return;\n\n if (isIP(hostname) !== 0) {\n if (isPrivateAddress(hostname)) {\n throw new Error(\n `Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n addresses = await dnsLookup(hostname, { all: true });\n } catch (err) {\n // DNS failure is not an SSRF signal — let the fetch itself surface the\n // resolution error with its own (more detailed) message.\n void err;\n return;\n }\n for (const a of addresses) {\n if (isPrivateAddress(a.address)) {\n throw new Error(\n `Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n }\n}\n\nexport interface RemoteResolveOptions {\n validateOnLoad: boolean;\n logger: Logger;\n /** Override fetch implementation (tests). */\n fetchImpl?: typeof fetch;\n /** Override cache root (tests). */\n cacheRoot?: string;\n /** Override timeout (tests). */\n timeoutMs?: number;\n /** Override max response size (tests). */\n maxBytes?: number;\n}\n\nexport interface ResolvedLocalFixture {\n /** Original value as passed on the CLI (for logging). */\n source: string;\n /** Filesystem path — downstream code treats this identically to a --fixtures path. */\n path: string;\n}\n\n/**\n * Returns true if `value` looks like a URL (has a scheme followed by ://).\n * Path inputs like ./fixtures or /tmp/x never start with a scheme.\n */\nexport function looksLikeUrl(value: string): boolean {\n return /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value);\n}\n\n/**\n * Returns the default on-disk cache root for fetched fixtures.\n * Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.\n */\nexport function defaultCacheRoot(): string {\n const xdg = process.env.XDG_CACHE_HOME;\n const base = xdg && xdg.length > 0 ? xdg : join(homedir(), \".cache\");\n return join(base, \"aimock\", \"fixtures\");\n}\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Resolve a single --fixtures value to a local filesystem path.\n *\n * Behavior:\n * - Filesystem path → return as-is.\n * - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.\n * On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).\n * If --validate-on-load is set and no cache is usable, throws.\n * - Any other scheme (file://, ftp://, ...) → throws.\n */\nexport async function resolveFixturesValue(\n value: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n if (!looksLikeUrl(value)) {\n return { source: value, path: pathResolve(value) };\n }\n\n const lower = value.toLowerCase();\n if (!lower.startsWith(\"https://\") && !lower.startsWith(\"http://\")) {\n // Extract the scheme for a clearer error\n const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//);\n const scheme = match ? match[1] : \"unknown\";\n throw new Error(\n `Unsupported --fixtures URL scheme \"${scheme}\" in ${value} (only https:// and http:// are supported)`,\n );\n }\n\n return await resolveHttpFixture(value, opts);\n}\n\nasync function resolveHttpFixture(\n url: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n const fetchImpl = opts.fetchImpl ?? fetch;\n const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();\n const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;\n const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;\n\n const digest = sha256Hex(url);\n const cacheDir = join(cacheRoot, digest);\n const cacheFile = join(cacheDir, \"fixtures.json\");\n\n try {\n // SSRF defense: reject private / reserved destinations before any network\n // I/O, unless explicitly opted in via AIMOCK_ALLOW_PRIVATE_URLS=1.\n const parsed = new URL(url);\n await assertAllowedHost(parsed.hostname);\n\n const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);\n // Parse to verify it is valid JSON before caching — fail loud if not.\n JSON.parse(body);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cacheFile, body, \"utf-8\");\n opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);\n return { source: url, path: cacheFile };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const cacheExists = cacheFileExists(cacheFile);\n if (cacheExists) {\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`,\n );\n return { source: url, path: cacheFile };\n }\n if (opts.validateOnLoad) {\n throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);\n }\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`,\n );\n // Signal \"no path\" by returning a sentinel with empty path — callers detect and skip.\n return { source: url, path: \"\" };\n }\n}\n\nfunction cacheFileExists(file: string): boolean {\n try {\n return statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nasync function fetchWithLimits(\n url: string,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n maxBytes: number,\n): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new Error(\"timeout\")), timeoutMs);\n try {\n // Redirects are disabled: following a 3xx into a different scheme or host\n // would bypass the scheme check and SSRF denylist. Upstream services\n // should serve the final URL directly (e.g. GitHub raw content URLs).\n const res = await fetchImpl(url, {\n signal: controller.signal,\n redirect: \"manual\",\n });\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get(\"location\") ?? \"<none>\";\n throw new Error(\n `redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`,\n );\n }\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n // Early reject on over-large Content-Length when the server reports it.\n const len = res.headers.get(\"content-length\");\n if (len) {\n const n = Number(len);\n if (Number.isFinite(n) && n > maxBytes) {\n throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);\n }\n }\n\n // Stream and enforce the limit incrementally in case Content-Length is absent/lying.\n if (!res.body) {\n const text = await res.text();\n if (Buffer.byteLength(text, \"utf-8\") > maxBytes) {\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n return text;\n }\n\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > maxBytes) {\n try {\n await reader.cancel();\n } catch {\n // ignore cancel errors\n }\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n } catch (err) {\n if (err instanceof Error && (err.name === \"AbortError\" || err.message === \"timeout\")) {\n throw new Error(`fetch timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/aimock",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.7",
|
|
4
4
|
"description": "Mock infrastructure for AI application testing — LLM APIs, image generation, text-to-speech, transcription, video generation, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation. One package, one port, zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|