@copilotkit/aimock 1.14.6 → 1.14.8
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 +20 -0
- package/README.md +11 -0
- package/dist/cli.cjs +70 -24
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +70 -24
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts.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/dist/vector-types.d.ts.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @copilotkit/aimock
|
|
2
2
|
|
|
3
|
+
## 1.14.8
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `--proxy-only` mode now accepts URL-only `--fixtures` sources without requiring a local
|
|
8
|
+
filesystem path. Previously the first `--fixtures` value was always checked as a
|
|
9
|
+
record-destination base path, which rejected all-URL invocations even though proxy-only
|
|
10
|
+
mode doesn't write recordings to disk. The check now fires only for `--record` mode
|
|
11
|
+
where a writable destination is actually required. Same fix applied to the parallel
|
|
12
|
+
`--agui-proxy-only` CLI path. Unblocks the showcase-aimock Railway service which runs
|
|
13
|
+
aimock in proxy-only mode with remote GitHub raw fixture URLs and no local fallback.
|
|
14
|
+
|
|
15
|
+
## 1.14.7
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- `--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.
|
|
20
|
+
- 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`).
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
3
23
|
## 1.14.6
|
|
4
24
|
|
|
5
25
|
### 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,15 @@ 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
|
+
const recordBaseIsUrl = /^https?:\/\//i.test(recordBase);
|
|
236
|
+
if (values.record && recordBaseIsUrl) {
|
|
237
|
+
console.error(`Error: --record requires a local --fixtures path for the recording destination; got URL ${recordBase}`);
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
231
240
|
record = {
|
|
232
241
|
providers,
|
|
233
|
-
fixturePath: (0, node_path.resolve)(
|
|
242
|
+
fixturePath: recordBaseIsUrl ? void 0 : (0, node_path.resolve)(recordBase, "recorded"),
|
|
234
243
|
proxyOnly: values["proxy-only"]
|
|
235
244
|
};
|
|
236
245
|
}
|
|
@@ -240,10 +249,16 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
240
249
|
console.error("Error: --agui-record/--agui-proxy-only requires --agui-upstream");
|
|
241
250
|
process.exit(1);
|
|
242
251
|
}
|
|
252
|
+
const aguiBase = fixtureValues[0];
|
|
253
|
+
const aguiBaseIsUrl = /^https?:\/\//i.test(aguiBase);
|
|
254
|
+
if (values["agui-record"] && aguiBaseIsUrl) {
|
|
255
|
+
console.error(`Error: --agui-record requires a local --fixtures path for the recording destination; got URL ${aguiBase}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
243
258
|
const agui = new require_agui_mock.AGUIMock();
|
|
244
259
|
agui.enableRecording({
|
|
245
260
|
upstream: values["agui-upstream"],
|
|
246
|
-
fixturePath: (0, node_path.resolve)(
|
|
261
|
+
fixturePath: aguiBaseIsUrl ? void 0 : (0, node_path.resolve)(aguiBase, "agui-recorded"),
|
|
247
262
|
proxyOnly: values["agui-proxy-only"]
|
|
248
263
|
});
|
|
249
264
|
aguiMount = {
|
|
@@ -251,21 +266,46 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
251
266
|
handler: agui
|
|
252
267
|
};
|
|
253
268
|
}
|
|
254
|
-
async function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
269
|
+
async function resolveAllFixtureSources() {
|
|
270
|
+
const resolved = [];
|
|
271
|
+
for (const value of fixtureValues) {
|
|
272
|
+
let local;
|
|
273
|
+
try {
|
|
274
|
+
local = await require_fixtures_remote.resolveFixturesValue(value, {
|
|
275
|
+
validateOnLoad,
|
|
276
|
+
logger
|
|
277
|
+
});
|
|
278
|
+
} catch (err) {
|
|
264
279
|
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
-
console.error(`Failed to
|
|
280
|
+
console.error(`Failed to resolve --fixtures value "${value}": ${msg}`);
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
if (!local.path) continue;
|
|
284
|
+
try {
|
|
285
|
+
const stat = (0, node_fs.statSync)(local.path);
|
|
286
|
+
resolved.push({
|
|
287
|
+
source: local.source,
|
|
288
|
+
path: local.path,
|
|
289
|
+
isDir: stat.isDirectory()
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
if (err.code === "ENOENT") console.error(`Fixtures path not found: ${local.path}`);
|
|
293
|
+
else {
|
|
294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
295
|
+
console.error(`Failed to load fixtures from ${local.path}: ${msg}`);
|
|
296
|
+
}
|
|
297
|
+
process.exit(1);
|
|
266
298
|
}
|
|
267
|
-
process.exit(1);
|
|
268
299
|
}
|
|
300
|
+
return resolved;
|
|
301
|
+
}
|
|
302
|
+
function loadSource(source) {
|
|
303
|
+
return source.isDir ? require_fixture_loader.loadFixturesFromDir(source.path, logger) : require_fixture_loader.loadFixtureFile(source.path, logger);
|
|
304
|
+
}
|
|
305
|
+
async function main() {
|
|
306
|
+
const sources = await resolveAllFixtureSources();
|
|
307
|
+
const fixtures = [];
|
|
308
|
+
for (const src of sources) fixtures.push(...loadSource(src));
|
|
269
309
|
if (fixtures.length === 0) {
|
|
270
310
|
if (validateOnLoad || values.strict) {
|
|
271
311
|
console.error("Error: No fixtures loaded and validation/strict mode is enabled — aborting.");
|
|
@@ -273,7 +313,8 @@ async function main() {
|
|
|
273
313
|
}
|
|
274
314
|
console.warn("Warning: No fixtures loaded. The server will return 404 for all requests.");
|
|
275
315
|
}
|
|
276
|
-
|
|
316
|
+
const sourceLabel = sources.map((s) => s.source).join(", ") || "<none>";
|
|
317
|
+
logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);
|
|
277
318
|
if (validateOnLoad) {
|
|
278
319
|
const results = require_fixture_loader.validateFixtures(fixtures);
|
|
279
320
|
const errors = results.filter((r) => r.severity === "error");
|
|
@@ -302,12 +343,17 @@ async function main() {
|
|
|
302
343
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
303
344
|
let watcher = null;
|
|
304
345
|
if (watchMode) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
346
|
+
const primary = sources[0];
|
|
347
|
+
if (!primary) logger.warn("--watch requested but no resolvable fixture sources; skipping watcher");
|
|
348
|
+
else {
|
|
349
|
+
const loadFn = () => loadSource(primary);
|
|
350
|
+
watcher = require_watcher.watchFixtures(primary.path, fixtures, loadFn, {
|
|
351
|
+
logger,
|
|
352
|
+
validate: validateOnLoad,
|
|
353
|
+
validateFn: require_fixture_loader.validateFixtures
|
|
354
|
+
});
|
|
355
|
+
logger.info(`Watching ${primary.path} for changes`);
|
|
356
|
+
}
|
|
311
357
|
}
|
|
312
358
|
function shutdown() {
|
|
313
359
|
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, the first --fixtures value is the base path for the recording\n // destination and must be a local filesystem path — writing to a URL is not supported.\n // For --proxy-only, unmatched requests are forwarded without saving, so no writable\n // destination is required; URL-only --fixtures is valid in that mode.\n const recordBase = fixtureValues[0];\n const recordBaseIsUrl = /^https?:\\/\\//i.test(recordBase);\n if (values.record && recordBaseIsUrl) {\n console.error(\n `Error: --record requires a local --fixtures path for the recording destination; got URL ${recordBase}`,\n );\n process.exit(1);\n }\n record = {\n providers,\n // In proxy-only mode with only URL sources, fixturePath is never consumed\n // (recorder.ts skips disk writes when proxyOnly is set). Leave it undefined\n // rather than resolving a URL string as a filesystem path.\n fixturePath: recordBaseIsUrl ? undefined : 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 // --agui-record writes recorded AG-UI fixtures to disk, so a URL source is unsupported.\n // --agui-proxy-only forwards without saving, so URL-only --fixtures is valid.\n const aguiBase = fixtureValues[0];\n const aguiBaseIsUrl = /^https?:\\/\\//i.test(aguiBase);\n if (values[\"agui-record\"] && aguiBaseIsUrl) {\n console.error(\n `Error: --agui-record 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 // In proxy-only mode with a URL-only --fixtures, the AG-UI recorder never\n // writes to disk (see agui-recorder.ts). Leave fixturePath undefined rather\n // than resolving a URL as a filesystem path.\n fixturePath: aguiBaseIsUrl ? undefined : 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,15 @@ 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
|
+
const recordBaseIsUrl = /^https?:\/\//i.test(recordBase);
|
|
235
|
+
if (values.record && recordBaseIsUrl) {
|
|
236
|
+
console.error(`Error: --record requires a local --fixtures path for the recording destination; got URL ${recordBase}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
230
239
|
record = {
|
|
231
240
|
providers,
|
|
232
|
-
fixturePath: resolve(
|
|
241
|
+
fixturePath: recordBaseIsUrl ? void 0 : resolve(recordBase, "recorded"),
|
|
233
242
|
proxyOnly: values["proxy-only"]
|
|
234
243
|
};
|
|
235
244
|
}
|
|
@@ -239,10 +248,16 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
239
248
|
console.error("Error: --agui-record/--agui-proxy-only requires --agui-upstream");
|
|
240
249
|
process.exit(1);
|
|
241
250
|
}
|
|
251
|
+
const aguiBase = fixtureValues[0];
|
|
252
|
+
const aguiBaseIsUrl = /^https?:\/\//i.test(aguiBase);
|
|
253
|
+
if (values["agui-record"] && aguiBaseIsUrl) {
|
|
254
|
+
console.error(`Error: --agui-record requires a local --fixtures path for the recording destination; got URL ${aguiBase}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
242
257
|
const agui = new AGUIMock();
|
|
243
258
|
agui.enableRecording({
|
|
244
259
|
upstream: values["agui-upstream"],
|
|
245
|
-
fixturePath: resolve(
|
|
260
|
+
fixturePath: aguiBaseIsUrl ? void 0 : resolve(aguiBase, "agui-recorded"),
|
|
246
261
|
proxyOnly: values["agui-proxy-only"]
|
|
247
262
|
});
|
|
248
263
|
aguiMount = {
|
|
@@ -250,21 +265,46 @@ if (values["agui-record"] || values["agui-proxy-only"]) {
|
|
|
250
265
|
handler: agui
|
|
251
266
|
};
|
|
252
267
|
}
|
|
253
|
-
async function
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
268
|
+
async function resolveAllFixtureSources() {
|
|
269
|
+
const resolved = [];
|
|
270
|
+
for (const value of fixtureValues) {
|
|
271
|
+
let local;
|
|
272
|
+
try {
|
|
273
|
+
local = await resolveFixturesValue(value, {
|
|
274
|
+
validateOnLoad,
|
|
275
|
+
logger
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
263
278
|
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
-
console.error(`Failed to
|
|
279
|
+
console.error(`Failed to resolve --fixtures value "${value}": ${msg}`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
if (!local.path) continue;
|
|
283
|
+
try {
|
|
284
|
+
const stat = statSync(local.path);
|
|
285
|
+
resolved.push({
|
|
286
|
+
source: local.source,
|
|
287
|
+
path: local.path,
|
|
288
|
+
isDir: stat.isDirectory()
|
|
289
|
+
});
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (err.code === "ENOENT") console.error(`Fixtures path not found: ${local.path}`);
|
|
292
|
+
else {
|
|
293
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
294
|
+
console.error(`Failed to load fixtures from ${local.path}: ${msg}`);
|
|
295
|
+
}
|
|
296
|
+
process.exit(1);
|
|
265
297
|
}
|
|
266
|
-
process.exit(1);
|
|
267
298
|
}
|
|
299
|
+
return resolved;
|
|
300
|
+
}
|
|
301
|
+
function loadSource(source) {
|
|
302
|
+
return source.isDir ? loadFixturesFromDir(source.path, logger) : loadFixtureFile(source.path, logger);
|
|
303
|
+
}
|
|
304
|
+
async function main() {
|
|
305
|
+
const sources = await resolveAllFixtureSources();
|
|
306
|
+
const fixtures = [];
|
|
307
|
+
for (const src of sources) fixtures.push(...loadSource(src));
|
|
268
308
|
if (fixtures.length === 0) {
|
|
269
309
|
if (validateOnLoad || values.strict) {
|
|
270
310
|
console.error("Error: No fixtures loaded and validation/strict mode is enabled — aborting.");
|
|
@@ -272,7 +312,8 @@ async function main() {
|
|
|
272
312
|
}
|
|
273
313
|
console.warn("Warning: No fixtures loaded. The server will return 404 for all requests.");
|
|
274
314
|
}
|
|
275
|
-
|
|
315
|
+
const sourceLabel = sources.map((s) => s.source).join(", ") || "<none>";
|
|
316
|
+
logger.info(`Loaded ${fixtures.length} fixture(s) from ${sourceLabel}`);
|
|
276
317
|
if (validateOnLoad) {
|
|
277
318
|
const results = validateFixtures(fixtures);
|
|
278
319
|
const errors = results.filter((r) => r.severity === "error");
|
|
@@ -301,12 +342,17 @@ async function main() {
|
|
|
301
342
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
302
343
|
let watcher = null;
|
|
303
344
|
if (watchMode) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
345
|
+
const primary = sources[0];
|
|
346
|
+
if (!primary) logger.warn("--watch requested but no resolvable fixture sources; skipping watcher");
|
|
347
|
+
else {
|
|
348
|
+
const loadFn = () => loadSource(primary);
|
|
349
|
+
watcher = watchFixtures(primary.path, fixtures, loadFn, {
|
|
350
|
+
logger,
|
|
351
|
+
validate: validateOnLoad,
|
|
352
|
+
validateFn: validateFixtures
|
|
353
|
+
});
|
|
354
|
+
logger.info(`Watching ${primary.path} for changes`);
|
|
355
|
+
}
|
|
310
356
|
}
|
|
311
357
|
function shutdown() {
|
|
312
358
|
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":"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"}
|
|
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, the first --fixtures value is the base path for the recording\n // destination and must be a local filesystem path — writing to a URL is not supported.\n // For --proxy-only, unmatched requests are forwarded without saving, so no writable\n // destination is required; URL-only --fixtures is valid in that mode.\n const recordBase = fixtureValues[0];\n const recordBaseIsUrl = /^https?:\\/\\//i.test(recordBase);\n if (values.record && recordBaseIsUrl) {\n console.error(\n `Error: --record requires a local --fixtures path for the recording destination; got URL ${recordBase}`,\n );\n process.exit(1);\n }\n record = {\n providers,\n // In proxy-only mode with only URL sources, fixturePath is never consumed\n // (recorder.ts skips disk writes when proxyOnly is set). Leave it undefined\n // rather than resolving a URL string as a filesystem path.\n fixturePath: recordBaseIsUrl ? undefined : 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 // --agui-record writes recorded AG-UI fixtures to disk, so a URL source is unsupported.\n // --agui-proxy-only forwards without saving, so URL-only --fixtures is valid.\n const aguiBase = fixtureValues[0];\n const aguiBaseIsUrl = /^https?:\\/\\//i.test(aguiBase);\n if (values[\"agui-record\"] && aguiBaseIsUrl) {\n console.error(\n `Error: --agui-record 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 // In proxy-only mode with a URL-only --fixtures, the AG-UI recorder never\n // writes to disk (see agui-recorder.ts). Leave fixturePath undefined rather\n // than resolving a URL as a filesystem path.\n fixturePath: aguiBaseIsUrl ? undefined : 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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config-loader.d.ts","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.ts","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,EAAA;EAKT,SAAA,CAAA,EAvBH,iBAuBoB,EAAA;EAOjB,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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vector-types.d.ts","names":[],"sources":["../src/vector-types.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,IAAA,CAAA,EAAA,MAAA;EAKA,IAAA,CAAA,EAAA,MAAA;;AAGM,UAHN,gBAAA,CAGM;MAAZ,EAAA,MAAA;EAAG,SAAA,EAAA,MAAA;EAGG,OAAA,EAHN,GAGM,CAAA,MAAW,EAHL,
|
|
1
|
+
{"version":3,"file":"vector-types.d.ts","names":[],"sources":["../src/vector-types.ts"],"sourcesContent":[],"mappings":";UAAiB,iBAAA;EAAA,IAAA,CAAA,EAAA,MAAA;EAKA,IAAA,CAAA,EAAA,MAAA;;AAGM,UAHN,gBAAA,CAGM;MAAZ,EAAA,MAAA;EAAG,SAAA,EAAA,MAAA;EAGG,OAAA,EAHN,GAGM,CAAA,MAAW,EAHL,WAMV,CAAM;AAGnB;AAOiB,UAbA,WAAA,CAaW;EAOhB,EAAA,EAAA,MAAA;EAAY,MAAA,EAAA,MAAA,EAAA;UAAG,CAAA,EAjBd,MAiBc,CAAA,MAAA,EAAA,OAAA,CAAA;;AAAyC,UAdnD,WAAA,CAcmD;EAAW,EAAA,EAAA,MAAA;;aAXlE;;;UAII,WAAA;;;;;;KAOL,YAAA,GAAe,yBAAyB,gBAAgB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@copilotkit/aimock",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.8",
|
|
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": [
|