@copilotkit/aimock 1.14.5 → 1.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +384 -0
- package/README.md +35 -14
- package/dist/_virtual/_rolldown/runtime.cjs +0 -2
- package/dist/cli.cjs +68 -24
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +68 -24
- package/dist/cli.js.map +1 -1
- package/dist/config-loader.d.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/vitest.cjs +4 -4
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.js +1 -1
- package/package.json +31 -12
- package/dist/_virtual/_rolldown/runtime.js +0 -29
- package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.cjs +0 -934
- package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.js +0 -934
- package/dist/node_modules/.pnpm/@vitest_pretty-format@3.2.4/node_modules/@vitest/pretty-format/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs +0 -1051
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js +0 -1042
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/index.cjs +0 -1
- package/dist/node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/index.js +0 -3
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.cjs +0 -96
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js +0 -93
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/chunk-_commonjsHelpers.js.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.cjs +0 -49
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.js +0 -43
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/helpers.js.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.cjs +0 -456
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.js +0 -456
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.cjs +0 -170
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.cjs.map +0 -1
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.js +0 -169
- package/dist/node_modules/.pnpm/@vitest_utils@3.2.4/node_modules/@vitest/utils/dist/source-map.js.map +0 -1
- package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.cjs +0 -388
- package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.js +0 -385
- package/dist/node_modules/.pnpm/js-tokens@9.0.1/node_modules/js-tokens/index.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.cjs +0 -12
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js +0 -12
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/arguments.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.cjs +0 -17
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js +0 -17
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/array.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.cjs +0 -12
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js +0 -12
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/bigint.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.cjs +0 -16
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js +0 -16
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/class.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.cjs +0 -14
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js +0 -14
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/date.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.cjs +0 -35
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js +0 -35
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/error.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.cjs +0 -13
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js +0 -13
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/function.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.cjs +0 -128
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js +0 -123
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/helpers.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.cjs +0 -41
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js +0 -40
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/html.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.cjs +0 -100
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js +0 -100
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/index.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.cjs +0 -26
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js +0 -26
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/map.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.cjs +0 -15
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js +0 -15
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/number.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.cjs +0 -22
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js +0 -22
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/object.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.cjs +0 -7
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js +0 -6
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/promise.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.cjs +0 -13
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js +0 -13
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/regexp.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.cjs +0 -19
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js +0 -19
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/set.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.cjs +0 -26
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js +0 -26
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/string.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.cjs +0 -10
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js +0 -9
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/symbol.js.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.cjs +0 -31
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.cjs.map +0 -1
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js +0 -31
- package/dist/node_modules/.pnpm/loupe@3.2.1/node_modules/loupe/lib/typedarray.js.map +0 -1
- package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.cjs +0 -52
- package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.cjs.map +0 -1
- package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.js +0 -52
- package/dist/node_modules/.pnpm/strip-literal@3.1.0/node_modules/strip-literal/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.cjs +0 -83
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.cjs.map +0 -1
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js +0 -82
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/chunk-BVHSVHOK.js.map +0 -1
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.cjs +0 -10
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.cjs.map +0 -1
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.js +0 -10
- package/dist/node_modules/.pnpm/tinyrainbow@2.0.0/node_modules/tinyrainbow/dist/node.js.map +0 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
2
|
+
let node_crypto = require("node:crypto");
|
|
3
|
+
let node_path = require("node:path");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
let node_dns_promises = require("node:dns/promises");
|
|
6
|
+
let node_net = require("node:net");
|
|
7
|
+
let node_os = require("node:os");
|
|
8
|
+
|
|
9
|
+
//#region src/fixtures-remote.ts
|
|
10
|
+
const REMOTE_FETCH_TIMEOUT_MS = 1e4;
|
|
11
|
+
const REMOTE_MAX_BYTES = 50 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Private / reserved address ranges blocked by default to prevent SSRF.
|
|
14
|
+
*
|
|
15
|
+
* The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /
|
|
16
|
+
* ULA / multicast / unspecified — any destination that could let an attacker
|
|
17
|
+
* pivot a fetch into the local network or cloud control plane via a hostile
|
|
18
|
+
* `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required
|
|
19
|
+
* for local dev / tests that target 127.0.0.1).
|
|
20
|
+
*/
|
|
21
|
+
const PRIVATE_V4_RANGES = [
|
|
22
|
+
["0.0.0.0", 8],
|
|
23
|
+
["10.0.0.0", 8],
|
|
24
|
+
["100.64.0.0", 10],
|
|
25
|
+
["127.0.0.0", 8],
|
|
26
|
+
["169.254.0.0", 16],
|
|
27
|
+
["172.16.0.0", 12],
|
|
28
|
+
["192.0.0.0", 24],
|
|
29
|
+
["192.0.2.0", 24],
|
|
30
|
+
["192.88.99.0", 24],
|
|
31
|
+
["192.168.0.0", 16],
|
|
32
|
+
["198.18.0.0", 15],
|
|
33
|
+
["198.51.100.0", 24],
|
|
34
|
+
["203.0.113.0", 24],
|
|
35
|
+
["224.0.0.0", 4],
|
|
36
|
+
["240.0.0.0", 4],
|
|
37
|
+
["255.255.255.255", 32]
|
|
38
|
+
];
|
|
39
|
+
const PRIVATE_V6_RANGES = [
|
|
40
|
+
["::", 128],
|
|
41
|
+
["::1", 128],
|
|
42
|
+
["fc00::", 7],
|
|
43
|
+
["fe80::", 10]
|
|
44
|
+
];
|
|
45
|
+
function buildBlockList() {
|
|
46
|
+
const bl = new node_net.BlockList();
|
|
47
|
+
for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, "ipv4");
|
|
48
|
+
for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, "ipv6");
|
|
49
|
+
return bl;
|
|
50
|
+
}
|
|
51
|
+
const PRIVATE_BLOCKLIST = buildBlockList();
|
|
52
|
+
/**
|
|
53
|
+
* Returns true if `address` is a literal IP (v4 or v6) that falls in any
|
|
54
|
+
* blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,
|
|
55
|
+
* ULA, multicast, unspecified, reserved). Returns false for public IPs
|
|
56
|
+
* and for non-literal hostnames.
|
|
57
|
+
*/
|
|
58
|
+
function isPrivateAddress(address) {
|
|
59
|
+
const family = (0, node_net.isIP)(address);
|
|
60
|
+
if (family === 0) return false;
|
|
61
|
+
if (family === 6) {
|
|
62
|
+
const mapped = address.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
63
|
+
if (mapped) return isPrivateAddress(mapped[1]);
|
|
64
|
+
}
|
|
65
|
+
return PRIVATE_BLOCKLIST.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
66
|
+
}
|
|
67
|
+
function privateUrlsAllowed() {
|
|
68
|
+
const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;
|
|
69
|
+
return v === "1" || v === "true";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Throws if `hostname` resolves to (or literally is) a private / reserved
|
|
73
|
+
* address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is
|
|
74
|
+
* not a literal IP, all resolved addresses are checked — any blocked
|
|
75
|
+
* address in the set rejects the host.
|
|
76
|
+
*/
|
|
77
|
+
async function assertAllowedHost(hostname) {
|
|
78
|
+
if (privateUrlsAllowed()) return;
|
|
79
|
+
if ((0, node_net.isIP)(hostname) !== 0) {
|
|
80
|
+
if (isPrivateAddress(hostname)) throw new Error(`Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let addresses;
|
|
84
|
+
try {
|
|
85
|
+
addresses = await (0, node_dns_promises.lookup)(hostname, { all: true });
|
|
86
|
+
} catch (err) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
for (const a of addresses) if (isPrivateAddress(a.address)) throw new Error(`Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Returns true if `value` looks like a URL (has a scheme followed by ://).
|
|
93
|
+
* Path inputs like ./fixtures or /tmp/x never start with a scheme.
|
|
94
|
+
*/
|
|
95
|
+
function looksLikeUrl(value) {
|
|
96
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Returns the default on-disk cache root for fetched fixtures.
|
|
100
|
+
* Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.
|
|
101
|
+
*/
|
|
102
|
+
function defaultCacheRoot() {
|
|
103
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
104
|
+
return (0, node_path.join)(xdg && xdg.length > 0 ? xdg : (0, node_path.join)((0, node_os.homedir)(), ".cache"), "aimock", "fixtures");
|
|
105
|
+
}
|
|
106
|
+
function sha256Hex(input) {
|
|
107
|
+
return (0, node_crypto.createHash)("sha256").update(input).digest("hex");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Resolve a single --fixtures value to a local filesystem path.
|
|
111
|
+
*
|
|
112
|
+
* Behavior:
|
|
113
|
+
* - Filesystem path → return as-is.
|
|
114
|
+
* - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.
|
|
115
|
+
* On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).
|
|
116
|
+
* If --validate-on-load is set and no cache is usable, throws.
|
|
117
|
+
* - Any other scheme (file://, ftp://, ...) → throws.
|
|
118
|
+
*/
|
|
119
|
+
async function resolveFixturesValue(value, opts) {
|
|
120
|
+
if (!looksLikeUrl(value)) return {
|
|
121
|
+
source: value,
|
|
122
|
+
path: (0, node_path.resolve)(value)
|
|
123
|
+
};
|
|
124
|
+
const lower = value.toLowerCase();
|
|
125
|
+
if (!lower.startsWith("https://") && !lower.startsWith("http://")) {
|
|
126
|
+
const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
127
|
+
const scheme = match ? match[1] : "unknown";
|
|
128
|
+
throw new Error(`Unsupported --fixtures URL scheme "${scheme}" in ${value} (only https:// and http:// are supported)`);
|
|
129
|
+
}
|
|
130
|
+
return await resolveHttpFixture(value, opts);
|
|
131
|
+
}
|
|
132
|
+
async function resolveHttpFixture(url, opts) {
|
|
133
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
134
|
+
const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();
|
|
135
|
+
const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;
|
|
136
|
+
const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;
|
|
137
|
+
const cacheDir = (0, node_path.join)(cacheRoot, sha256Hex(url));
|
|
138
|
+
const cacheFile = (0, node_path.join)(cacheDir, "fixtures.json");
|
|
139
|
+
try {
|
|
140
|
+
await assertAllowedHost(new URL(url).hostname);
|
|
141
|
+
const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);
|
|
142
|
+
JSON.parse(body);
|
|
143
|
+
(0, node_fs.mkdirSync)(cacheDir, { recursive: true });
|
|
144
|
+
(0, node_fs.writeFileSync)(cacheFile, body, "utf-8");
|
|
145
|
+
opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);
|
|
146
|
+
return {
|
|
147
|
+
source: url,
|
|
148
|
+
path: cacheFile
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
152
|
+
if (cacheFileExists(cacheFile)) {
|
|
153
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`);
|
|
154
|
+
return {
|
|
155
|
+
source: url,
|
|
156
|
+
path: cacheFile
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (opts.validateOnLoad) throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);
|
|
160
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`);
|
|
161
|
+
return {
|
|
162
|
+
source: url,
|
|
163
|
+
path: ""
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function cacheFileExists(file) {
|
|
168
|
+
try {
|
|
169
|
+
return (0, node_fs.statSync)(file).isFile();
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes) {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timeout")), timeoutMs);
|
|
177
|
+
try {
|
|
178
|
+
const res = await fetchImpl(url, {
|
|
179
|
+
signal: controller.signal,
|
|
180
|
+
redirect: "manual"
|
|
181
|
+
});
|
|
182
|
+
if (res.status >= 300 && res.status < 400) {
|
|
183
|
+
const location = res.headers.get("location") ?? "<none>";
|
|
184
|
+
throw new Error(`redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`);
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
187
|
+
const len = res.headers.get("content-length");
|
|
188
|
+
if (len) {
|
|
189
|
+
const n = Number(len);
|
|
190
|
+
if (Number.isFinite(n) && n > maxBytes) throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);
|
|
191
|
+
}
|
|
192
|
+
if (!res.body) {
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
if (Buffer.byteLength(text, "utf-8") > maxBytes) throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
const reader = res.body.getReader();
|
|
198
|
+
const chunks = [];
|
|
199
|
+
let total = 0;
|
|
200
|
+
for (;;) {
|
|
201
|
+
const { value, done } = await reader.read();
|
|
202
|
+
if (done) break;
|
|
203
|
+
if (value) {
|
|
204
|
+
total += value.byteLength;
|
|
205
|
+
if (total > maxBytes) {
|
|
206
|
+
try {
|
|
207
|
+
await reader.cancel();
|
|
208
|
+
} catch {}
|
|
209
|
+
throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
210
|
+
}
|
|
211
|
+
chunks.push(value);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "timeout")) throw new Error(`fetch timed out after ${timeoutMs}ms`);
|
|
217
|
+
throw err;
|
|
218
|
+
} finally {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
exports.resolveFixturesValue = resolveFixturesValue;
|
|
225
|
+
//# sourceMappingURL=fixtures-remote.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures-remote.cjs","names":["BlockList"],"sources":["../src/fixtures-remote.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, statSync } from \"node:fs\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join, resolve as pathResolve } from \"node:path\";\nimport type { Logger } from \"./logger.js\";\n\nexport const REMOTE_FETCH_TIMEOUT_MS = 10_000;\nexport const REMOTE_MAX_BYTES = 50 * 1024 * 1024; // 50 MB\n\n/**\n * Private / reserved address ranges blocked by default to prevent SSRF.\n *\n * The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /\n * ULA / multicast / unspecified — any destination that could let an attacker\n * pivot a fetch into the local network or cloud control plane via a hostile\n * `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required\n * for local dev / tests that target 127.0.0.1).\n */\nconst PRIVATE_V4_RANGES: Array<[string, number]> = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // RFC1918\n [\"100.64.0.0\", 10], // CGNAT\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local / cloud metadata\n [\"172.16.0.0\", 12], // RFC1918\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // TEST-NET-1\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // RFC1918\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // TEST-NET-2\n [\"203.0.113.0\", 24], // TEST-NET-3\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved\n [\"255.255.255.255\", 32], // broadcast\n];\n\nconst PRIVATE_V6_RANGES: Array<[string, number]> = [\n [\"::\", 128], // unspecified\n [\"::1\", 128], // loopback\n [\"fc00::\", 7], // ULA\n [\"fe80::\", 10], // link-local\n];\n\nfunction buildBlockList(): BlockList {\n const bl = new BlockList();\n for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, \"ipv4\");\n for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, \"ipv6\");\n return bl;\n}\n\nconst PRIVATE_BLOCKLIST: BlockList = buildBlockList();\n\n/**\n * Returns true if `address` is a literal IP (v4 or v6) that falls in any\n * blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,\n * ULA, multicast, unspecified, reserved). Returns false for public IPs\n * and for non-literal hostnames.\n */\nexport function isPrivateAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 0) return false; // not a literal IP\n // BlockList.check's \"ipv6\" bucket does not match v4-mapped ::ffff:a.b.c.d\n // automatically — unwrap to the underlying v4 address and recurse.\n if (family === 6) {\n const lower = address.toLowerCase();\n const mapped = lower.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mapped) return isPrivateAddress(mapped[1]);\n }\n return PRIVATE_BLOCKLIST.check(address, family === 4 ? \"ipv4\" : \"ipv6\");\n}\n\nfunction privateUrlsAllowed(): boolean {\n const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Throws if `hostname` resolves to (or literally is) a private / reserved\n * address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is\n * not a literal IP, all resolved addresses are checked — any blocked\n * address in the set rejects the host.\n */\nexport async function assertAllowedHost(hostname: string): Promise<void> {\n if (privateUrlsAllowed()) return;\n\n if (isIP(hostname) !== 0) {\n if (isPrivateAddress(hostname)) {\n throw new Error(\n `Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n addresses = await dnsLookup(hostname, { all: true });\n } catch (err) {\n // DNS failure is not an SSRF signal — let the fetch itself surface the\n // resolution error with its own (more detailed) message.\n void err;\n return;\n }\n for (const a of addresses) {\n if (isPrivateAddress(a.address)) {\n throw new Error(\n `Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n }\n}\n\nexport interface RemoteResolveOptions {\n validateOnLoad: boolean;\n logger: Logger;\n /** Override fetch implementation (tests). */\n fetchImpl?: typeof fetch;\n /** Override cache root (tests). */\n cacheRoot?: string;\n /** Override timeout (tests). */\n timeoutMs?: number;\n /** Override max response size (tests). */\n maxBytes?: number;\n}\n\nexport interface ResolvedLocalFixture {\n /** Original value as passed on the CLI (for logging). */\n source: string;\n /** Filesystem path — downstream code treats this identically to a --fixtures path. */\n path: string;\n}\n\n/**\n * Returns true if `value` looks like a URL (has a scheme followed by ://).\n * Path inputs like ./fixtures or /tmp/x never start with a scheme.\n */\nexport function looksLikeUrl(value: string): boolean {\n return /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value);\n}\n\n/**\n * Returns the default on-disk cache root for fetched fixtures.\n * Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.\n */\nexport function defaultCacheRoot(): string {\n const xdg = process.env.XDG_CACHE_HOME;\n const base = xdg && xdg.length > 0 ? xdg : join(homedir(), \".cache\");\n return join(base, \"aimock\", \"fixtures\");\n}\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Resolve a single --fixtures value to a local filesystem path.\n *\n * Behavior:\n * - Filesystem path → return as-is.\n * - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.\n * On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).\n * If --validate-on-load is set and no cache is usable, throws.\n * - Any other scheme (file://, ftp://, ...) → throws.\n */\nexport async function resolveFixturesValue(\n value: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n if (!looksLikeUrl(value)) {\n return { source: value, path: pathResolve(value) };\n }\n\n const lower = value.toLowerCase();\n if (!lower.startsWith(\"https://\") && !lower.startsWith(\"http://\")) {\n // Extract the scheme for a clearer error\n const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//);\n const scheme = match ? match[1] : \"unknown\";\n throw new Error(\n `Unsupported --fixtures URL scheme \"${scheme}\" in ${value} (only https:// and http:// are supported)`,\n );\n }\n\n return await resolveHttpFixture(value, opts);\n}\n\nasync function resolveHttpFixture(\n url: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n const fetchImpl = opts.fetchImpl ?? fetch;\n const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();\n const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;\n const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;\n\n const digest = sha256Hex(url);\n const cacheDir = join(cacheRoot, digest);\n const cacheFile = join(cacheDir, \"fixtures.json\");\n\n try {\n // SSRF defense: reject private / reserved destinations before any network\n // I/O, unless explicitly opted in via AIMOCK_ALLOW_PRIVATE_URLS=1.\n const parsed = new URL(url);\n await assertAllowedHost(parsed.hostname);\n\n const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);\n // Parse to verify it is valid JSON before caching — fail loud if not.\n JSON.parse(body);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cacheFile, body, \"utf-8\");\n opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);\n return { source: url, path: cacheFile };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const cacheExists = cacheFileExists(cacheFile);\n if (cacheExists) {\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`,\n );\n return { source: url, path: cacheFile };\n }\n if (opts.validateOnLoad) {\n throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);\n }\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`,\n );\n // Signal \"no path\" by returning a sentinel with empty path — callers detect and skip.\n return { source: url, path: \"\" };\n }\n}\n\nfunction cacheFileExists(file: string): boolean {\n try {\n return statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nasync function fetchWithLimits(\n url: string,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n maxBytes: number,\n): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new Error(\"timeout\")), timeoutMs);\n try {\n // Redirects are disabled: following a 3xx into a different scheme or host\n // would bypass the scheme check and SSRF denylist. Upstream services\n // should serve the final URL directly (e.g. GitHub raw content URLs).\n const res = await fetchImpl(url, {\n signal: controller.signal,\n redirect: \"manual\",\n });\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get(\"location\") ?? \"<none>\";\n throw new Error(\n `redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`,\n );\n }\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n // Early reject on over-large Content-Length when the server reports it.\n const len = res.headers.get(\"content-length\");\n if (len) {\n const n = Number(len);\n if (Number.isFinite(n) && n > maxBytes) {\n throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);\n }\n }\n\n // Stream and enforce the limit incrementally in case Content-Length is absent/lying.\n if (!res.body) {\n const text = await res.text();\n if (Buffer.byteLength(text, \"utf-8\") > maxBytes) {\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n return text;\n }\n\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > maxBytes) {\n try {\n await reader.cancel();\n } catch {\n // ignore cancel errors\n }\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n } catch (err) {\n if (err instanceof Error && (err.name === \"AbortError\" || err.message === \"timeout\")) {\n throw new Error(`fetch timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { mkdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { lookup } from "node:dns/promises";
|
|
5
|
+
import { BlockList, isIP } from "node:net";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
|
|
8
|
+
//#region src/fixtures-remote.ts
|
|
9
|
+
const REMOTE_FETCH_TIMEOUT_MS = 1e4;
|
|
10
|
+
const REMOTE_MAX_BYTES = 50 * 1024 * 1024;
|
|
11
|
+
/**
|
|
12
|
+
* Private / reserved address ranges blocked by default to prevent SSRF.
|
|
13
|
+
*
|
|
14
|
+
* The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /
|
|
15
|
+
* ULA / multicast / unspecified — any destination that could let an attacker
|
|
16
|
+
* pivot a fetch into the local network or cloud control plane via a hostile
|
|
17
|
+
* `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required
|
|
18
|
+
* for local dev / tests that target 127.0.0.1).
|
|
19
|
+
*/
|
|
20
|
+
const PRIVATE_V4_RANGES = [
|
|
21
|
+
["0.0.0.0", 8],
|
|
22
|
+
["10.0.0.0", 8],
|
|
23
|
+
["100.64.0.0", 10],
|
|
24
|
+
["127.0.0.0", 8],
|
|
25
|
+
["169.254.0.0", 16],
|
|
26
|
+
["172.16.0.0", 12],
|
|
27
|
+
["192.0.0.0", 24],
|
|
28
|
+
["192.0.2.0", 24],
|
|
29
|
+
["192.88.99.0", 24],
|
|
30
|
+
["192.168.0.0", 16],
|
|
31
|
+
["198.18.0.0", 15],
|
|
32
|
+
["198.51.100.0", 24],
|
|
33
|
+
["203.0.113.0", 24],
|
|
34
|
+
["224.0.0.0", 4],
|
|
35
|
+
["240.0.0.0", 4],
|
|
36
|
+
["255.255.255.255", 32]
|
|
37
|
+
];
|
|
38
|
+
const PRIVATE_V6_RANGES = [
|
|
39
|
+
["::", 128],
|
|
40
|
+
["::1", 128],
|
|
41
|
+
["fc00::", 7],
|
|
42
|
+
["fe80::", 10]
|
|
43
|
+
];
|
|
44
|
+
function buildBlockList() {
|
|
45
|
+
const bl = new BlockList();
|
|
46
|
+
for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, "ipv4");
|
|
47
|
+
for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, "ipv6");
|
|
48
|
+
return bl;
|
|
49
|
+
}
|
|
50
|
+
const PRIVATE_BLOCKLIST = buildBlockList();
|
|
51
|
+
/**
|
|
52
|
+
* Returns true if `address` is a literal IP (v4 or v6) that falls in any
|
|
53
|
+
* blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,
|
|
54
|
+
* ULA, multicast, unspecified, reserved). Returns false for public IPs
|
|
55
|
+
* and for non-literal hostnames.
|
|
56
|
+
*/
|
|
57
|
+
function isPrivateAddress(address) {
|
|
58
|
+
const family = isIP(address);
|
|
59
|
+
if (family === 0) return false;
|
|
60
|
+
if (family === 6) {
|
|
61
|
+
const mapped = address.toLowerCase().match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
62
|
+
if (mapped) return isPrivateAddress(mapped[1]);
|
|
63
|
+
}
|
|
64
|
+
return PRIVATE_BLOCKLIST.check(address, family === 4 ? "ipv4" : "ipv6");
|
|
65
|
+
}
|
|
66
|
+
function privateUrlsAllowed() {
|
|
67
|
+
const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;
|
|
68
|
+
return v === "1" || v === "true";
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Throws if `hostname` resolves to (or literally is) a private / reserved
|
|
72
|
+
* address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is
|
|
73
|
+
* not a literal IP, all resolved addresses are checked — any blocked
|
|
74
|
+
* address in the set rejects the host.
|
|
75
|
+
*/
|
|
76
|
+
async function assertAllowedHost(hostname) {
|
|
77
|
+
if (privateUrlsAllowed()) return;
|
|
78
|
+
if (isIP(hostname) !== 0) {
|
|
79
|
+
if (isPrivateAddress(hostname)) throw new Error(`Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
let addresses;
|
|
83
|
+
try {
|
|
84
|
+
addresses = await lookup(hostname, { all: true });
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for (const a of addresses) if (isPrivateAddress(a.address)) throw new Error(`Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if `value` looks like a URL (has a scheme followed by ://).
|
|
92
|
+
* Path inputs like ./fixtures or /tmp/x never start with a scheme.
|
|
93
|
+
*/
|
|
94
|
+
function looksLikeUrl(value) {
|
|
95
|
+
return /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(value);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Returns the default on-disk cache root for fetched fixtures.
|
|
99
|
+
* Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.
|
|
100
|
+
*/
|
|
101
|
+
function defaultCacheRoot() {
|
|
102
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
103
|
+
return join(xdg && xdg.length > 0 ? xdg : join(homedir(), ".cache"), "aimock", "fixtures");
|
|
104
|
+
}
|
|
105
|
+
function sha256Hex(input) {
|
|
106
|
+
return createHash("sha256").update(input).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a single --fixtures value to a local filesystem path.
|
|
110
|
+
*
|
|
111
|
+
* Behavior:
|
|
112
|
+
* - Filesystem path → return as-is.
|
|
113
|
+
* - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.
|
|
114
|
+
* On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).
|
|
115
|
+
* If --validate-on-load is set and no cache is usable, throws.
|
|
116
|
+
* - Any other scheme (file://, ftp://, ...) → throws.
|
|
117
|
+
*/
|
|
118
|
+
async function resolveFixturesValue(value, opts) {
|
|
119
|
+
if (!looksLikeUrl(value)) return {
|
|
120
|
+
source: value,
|
|
121
|
+
path: resolve(value)
|
|
122
|
+
};
|
|
123
|
+
const lower = value.toLowerCase();
|
|
124
|
+
if (!lower.startsWith("https://") && !lower.startsWith("http://")) {
|
|
125
|
+
const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\/\//);
|
|
126
|
+
const scheme = match ? match[1] : "unknown";
|
|
127
|
+
throw new Error(`Unsupported --fixtures URL scheme "${scheme}" in ${value} (only https:// and http:// are supported)`);
|
|
128
|
+
}
|
|
129
|
+
return await resolveHttpFixture(value, opts);
|
|
130
|
+
}
|
|
131
|
+
async function resolveHttpFixture(url, opts) {
|
|
132
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
133
|
+
const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();
|
|
134
|
+
const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;
|
|
135
|
+
const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;
|
|
136
|
+
const cacheDir = join(cacheRoot, sha256Hex(url));
|
|
137
|
+
const cacheFile = join(cacheDir, "fixtures.json");
|
|
138
|
+
try {
|
|
139
|
+
await assertAllowedHost(new URL(url).hostname);
|
|
140
|
+
const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);
|
|
141
|
+
JSON.parse(body);
|
|
142
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
143
|
+
writeFileSync(cacheFile, body, "utf-8");
|
|
144
|
+
opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);
|
|
145
|
+
return {
|
|
146
|
+
source: url,
|
|
147
|
+
path: cacheFile
|
|
148
|
+
};
|
|
149
|
+
} catch (err) {
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
if (cacheFileExists(cacheFile)) {
|
|
152
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`);
|
|
153
|
+
return {
|
|
154
|
+
source: url,
|
|
155
|
+
path: cacheFile
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (opts.validateOnLoad) throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);
|
|
159
|
+
opts.logger.warn(`upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`);
|
|
160
|
+
return {
|
|
161
|
+
source: url,
|
|
162
|
+
path: ""
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function cacheFileExists(file) {
|
|
167
|
+
try {
|
|
168
|
+
return statSync(file).isFile();
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async function fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes) {
|
|
174
|
+
const controller = new AbortController();
|
|
175
|
+
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error("timeout")), timeoutMs);
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetchImpl(url, {
|
|
178
|
+
signal: controller.signal,
|
|
179
|
+
redirect: "manual"
|
|
180
|
+
});
|
|
181
|
+
if (res.status >= 300 && res.status < 400) {
|
|
182
|
+
const location = res.headers.get("location") ?? "<none>";
|
|
183
|
+
throw new Error(`redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`);
|
|
184
|
+
}
|
|
185
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
186
|
+
const len = res.headers.get("content-length");
|
|
187
|
+
if (len) {
|
|
188
|
+
const n = Number(len);
|
|
189
|
+
if (Number.isFinite(n) && n > maxBytes) throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);
|
|
190
|
+
}
|
|
191
|
+
if (!res.body) {
|
|
192
|
+
const text = await res.text();
|
|
193
|
+
if (Buffer.byteLength(text, "utf-8") > maxBytes) throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
194
|
+
return text;
|
|
195
|
+
}
|
|
196
|
+
const reader = res.body.getReader();
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let total = 0;
|
|
199
|
+
for (;;) {
|
|
200
|
+
const { value, done } = await reader.read();
|
|
201
|
+
if (done) break;
|
|
202
|
+
if (value) {
|
|
203
|
+
total += value.byteLength;
|
|
204
|
+
if (total > maxBytes) {
|
|
205
|
+
try {
|
|
206
|
+
await reader.cancel();
|
|
207
|
+
} catch {}
|
|
208
|
+
throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);
|
|
209
|
+
}
|
|
210
|
+
chunks.push(value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
214
|
+
} catch (err) {
|
|
215
|
+
if (err instanceof Error && (err.name === "AbortError" || err.message === "timeout")) throw new Error(`fetch timed out after ${timeoutMs}ms`);
|
|
216
|
+
throw err;
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//#endregion
|
|
223
|
+
export { resolveFixturesValue };
|
|
224
|
+
//# sourceMappingURL=fixtures-remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixtures-remote.js","names":["dnsLookup","pathResolve"],"sources":["../src/fixtures-remote.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { mkdirSync, writeFileSync, statSync } from \"node:fs\";\nimport { lookup as dnsLookup } from \"node:dns/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport { homedir } from \"node:os\";\nimport { join, resolve as pathResolve } from \"node:path\";\nimport type { Logger } from \"./logger.js\";\n\nexport const REMOTE_FETCH_TIMEOUT_MS = 10_000;\nexport const REMOTE_MAX_BYTES = 50 * 1024 * 1024; // 50 MB\n\n/**\n * Private / reserved address ranges blocked by default to prevent SSRF.\n *\n * The list covers RFC1918 / CGNAT / loopback / link-local / cloud-metadata /\n * ULA / multicast / unspecified — any destination that could let an attacker\n * pivot a fetch into the local network or cloud control plane via a hostile\n * `--fixtures` URL. Set `AIMOCK_ALLOW_PRIVATE_URLS=1` to opt out (required\n * for local dev / tests that target 127.0.0.1).\n */\nconst PRIVATE_V4_RANGES: Array<[string, number]> = [\n [\"0.0.0.0\", 8], // \"this network\"\n [\"10.0.0.0\", 8], // RFC1918\n [\"100.64.0.0\", 10], // CGNAT\n [\"127.0.0.0\", 8], // loopback\n [\"169.254.0.0\", 16], // link-local / cloud metadata\n [\"172.16.0.0\", 12], // RFC1918\n [\"192.0.0.0\", 24], // IETF protocol assignments\n [\"192.0.2.0\", 24], // TEST-NET-1\n [\"192.88.99.0\", 24], // 6to4 relay anycast (deprecated)\n [\"192.168.0.0\", 16], // RFC1918\n [\"198.18.0.0\", 15], // benchmarking\n [\"198.51.100.0\", 24], // TEST-NET-2\n [\"203.0.113.0\", 24], // TEST-NET-3\n [\"224.0.0.0\", 4], // multicast\n [\"240.0.0.0\", 4], // reserved\n [\"255.255.255.255\", 32], // broadcast\n];\n\nconst PRIVATE_V6_RANGES: Array<[string, number]> = [\n [\"::\", 128], // unspecified\n [\"::1\", 128], // loopback\n [\"fc00::\", 7], // ULA\n [\"fe80::\", 10], // link-local\n];\n\nfunction buildBlockList(): BlockList {\n const bl = new BlockList();\n for (const [addr, prefix] of PRIVATE_V4_RANGES) bl.addSubnet(addr, prefix, \"ipv4\");\n for (const [addr, prefix] of PRIVATE_V6_RANGES) bl.addSubnet(addr, prefix, \"ipv6\");\n return bl;\n}\n\nconst PRIVATE_BLOCKLIST: BlockList = buildBlockList();\n\n/**\n * Returns true if `address` is a literal IP (v4 or v6) that falls in any\n * blocked range (loopback, RFC1918, CGNAT, link-local, cloud-metadata,\n * ULA, multicast, unspecified, reserved). Returns false for public IPs\n * and for non-literal hostnames.\n */\nexport function isPrivateAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 0) return false; // not a literal IP\n // BlockList.check's \"ipv6\" bucket does not match v4-mapped ::ffff:a.b.c.d\n // automatically — unwrap to the underlying v4 address and recurse.\n if (family === 6) {\n const lower = address.toLowerCase();\n const mapped = lower.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (mapped) return isPrivateAddress(mapped[1]);\n }\n return PRIVATE_BLOCKLIST.check(address, family === 4 ? \"ipv4\" : \"ipv6\");\n}\n\nfunction privateUrlsAllowed(): boolean {\n const v = process.env.AIMOCK_ALLOW_PRIVATE_URLS;\n return v === \"1\" || v === \"true\";\n}\n\n/**\n * Throws if `hostname` resolves to (or literally is) a private / reserved\n * address, unless `AIMOCK_ALLOW_PRIVATE_URLS=1` is set. If the hostname is\n * not a literal IP, all resolved addresses are checked — any blocked\n * address in the set rejects the host.\n */\nexport async function assertAllowedHost(hostname: string): Promise<void> {\n if (privateUrlsAllowed()) return;\n\n if (isIP(hostname) !== 0) {\n if (isPrivateAddress(hostname)) {\n throw new Error(\n `Refusing to fetch from private address ${hostname}: not allowed by default (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n return;\n }\n\n let addresses: Array<{ address: string; family: number }>;\n try {\n addresses = await dnsLookup(hostname, { all: true });\n } catch (err) {\n // DNS failure is not an SSRF signal — let the fetch itself surface the\n // resolution error with its own (more detailed) message.\n void err;\n return;\n }\n for (const a of addresses) {\n if (isPrivateAddress(a.address)) {\n throw new Error(\n `Refusing to fetch from ${hostname}: resolves to private address ${a.address} (set AIMOCK_ALLOW_PRIVATE_URLS=1 to override)`,\n );\n }\n }\n}\n\nexport interface RemoteResolveOptions {\n validateOnLoad: boolean;\n logger: Logger;\n /** Override fetch implementation (tests). */\n fetchImpl?: typeof fetch;\n /** Override cache root (tests). */\n cacheRoot?: string;\n /** Override timeout (tests). */\n timeoutMs?: number;\n /** Override max response size (tests). */\n maxBytes?: number;\n}\n\nexport interface ResolvedLocalFixture {\n /** Original value as passed on the CLI (for logging). */\n source: string;\n /** Filesystem path — downstream code treats this identically to a --fixtures path. */\n path: string;\n}\n\n/**\n * Returns true if `value` looks like a URL (has a scheme followed by ://).\n * Path inputs like ./fixtures or /tmp/x never start with a scheme.\n */\nexport function looksLikeUrl(value: string): boolean {\n return /^[a-zA-Z][a-zA-Z0-9+.-]*:\\/\\//.test(value);\n}\n\n/**\n * Returns the default on-disk cache root for fetched fixtures.\n * Honors $XDG_CACHE_HOME when set, otherwise falls back to ~/.cache.\n */\nexport function defaultCacheRoot(): string {\n const xdg = process.env.XDG_CACHE_HOME;\n const base = xdg && xdg.length > 0 ? xdg : join(homedir(), \".cache\");\n return join(base, \"aimock\", \"fixtures\");\n}\n\nfunction sha256Hex(input: string): string {\n return createHash(\"sha256\").update(input).digest(\"hex\");\n}\n\n/**\n * Resolve a single --fixtures value to a local filesystem path.\n *\n * Behavior:\n * - Filesystem path → return as-is.\n * - https://, http:// URL → fetch JSON (once) to the on-disk cache; return the cached path.\n * On fetch failure, fall back to a pre-existing cached copy if present (warn + continue).\n * If --validate-on-load is set and no cache is usable, throws.\n * - Any other scheme (file://, ftp://, ...) → throws.\n */\nexport async function resolveFixturesValue(\n value: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n if (!looksLikeUrl(value)) {\n return { source: value, path: pathResolve(value) };\n }\n\n const lower = value.toLowerCase();\n if (!lower.startsWith(\"https://\") && !lower.startsWith(\"http://\")) {\n // Extract the scheme for a clearer error\n const match = value.match(/^([a-zA-Z][a-zA-Z0-9+.-]*):\\/\\//);\n const scheme = match ? match[1] : \"unknown\";\n throw new Error(\n `Unsupported --fixtures URL scheme \"${scheme}\" in ${value} (only https:// and http:// are supported)`,\n );\n }\n\n return await resolveHttpFixture(value, opts);\n}\n\nasync function resolveHttpFixture(\n url: string,\n opts: RemoteResolveOptions,\n): Promise<ResolvedLocalFixture> {\n const fetchImpl = opts.fetchImpl ?? fetch;\n const cacheRoot = opts.cacheRoot ?? defaultCacheRoot();\n const timeoutMs = opts.timeoutMs ?? REMOTE_FETCH_TIMEOUT_MS;\n const maxBytes = opts.maxBytes ?? REMOTE_MAX_BYTES;\n\n const digest = sha256Hex(url);\n const cacheDir = join(cacheRoot, digest);\n const cacheFile = join(cacheDir, \"fixtures.json\");\n\n try {\n // SSRF defense: reject private / reserved destinations before any network\n // I/O, unless explicitly opted in via AIMOCK_ALLOW_PRIVATE_URLS=1.\n const parsed = new URL(url);\n await assertAllowedHost(parsed.hostname);\n\n const body = await fetchWithLimits(url, fetchImpl, timeoutMs, maxBytes);\n // Parse to verify it is valid JSON before caching — fail loud if not.\n JSON.parse(body);\n mkdirSync(cacheDir, { recursive: true });\n writeFileSync(cacheFile, body, \"utf-8\");\n opts.logger.info(`Fetched ${url} (${body.length} bytes) → cached at ${cacheFile}`);\n return { source: url, path: cacheFile };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const cacheExists = cacheFileExists(cacheFile);\n if (cacheExists) {\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); using cached copy at ${cacheFile}`,\n );\n return { source: url, path: cacheFile };\n }\n if (opts.validateOnLoad) {\n throw new Error(`Failed to fetch ${url} and no cached copy available: ${msg}`);\n }\n opts.logger.warn(\n `upstream fetch failed for ${url} (${msg}); no cached copy available — skipping`,\n );\n // Signal \"no path\" by returning a sentinel with empty path — callers detect and skip.\n return { source: url, path: \"\" };\n }\n}\n\nfunction cacheFileExists(file: string): boolean {\n try {\n return statSync(file).isFile();\n } catch {\n return false;\n }\n}\n\nasync function fetchWithLimits(\n url: string,\n fetchImpl: typeof fetch,\n timeoutMs: number,\n maxBytes: number,\n): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(new Error(\"timeout\")), timeoutMs);\n try {\n // Redirects are disabled: following a 3xx into a different scheme or host\n // would bypass the scheme check and SSRF denylist. Upstream services\n // should serve the final URL directly (e.g. GitHub raw content URLs).\n const res = await fetchImpl(url, {\n signal: controller.signal,\n redirect: \"manual\",\n });\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get(\"location\") ?? \"<none>\";\n throw new Error(\n `redirect not allowed: upstream returned ${res.status} → ${location} (configure the upstream to serve the final URL directly; redirects are disabled to prevent scheme-bypass)`,\n );\n }\n if (!res.ok) {\n throw new Error(`HTTP ${res.status} ${res.statusText}`);\n }\n // Early reject on over-large Content-Length when the server reports it.\n const len = res.headers.get(\"content-length\");\n if (len) {\n const n = Number(len);\n if (Number.isFinite(n) && n > maxBytes) {\n throw new Error(`response too large: content-length ${n} exceeds limit ${maxBytes} bytes`);\n }\n }\n\n // Stream and enforce the limit incrementally in case Content-Length is absent/lying.\n if (!res.body) {\n const text = await res.text();\n if (Buffer.byteLength(text, \"utf-8\") > maxBytes) {\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n return text;\n }\n\n const reader = res.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n for (;;) {\n const { value, done } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > maxBytes) {\n try {\n await reader.cancel();\n } catch {\n // ignore cancel errors\n }\n throw new Error(`response too large: body exceeds limit ${maxBytes} bytes`);\n }\n chunks.push(value);\n }\n }\n return Buffer.concat(chunks).toString(\"utf-8\");\n } catch (err) {\n if (err instanceof Error && (err.name === \"AbortError\" || err.message === \"timeout\")) {\n throw new Error(`fetch timed out after ${timeoutMs}ms`);\n }\n throw err;\n } finally {\n clearTimeout(timer);\n }\n}\n"],"mappings":"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"}
|
package/dist/vitest.cjs
CHANGED
|
@@ -2,9 +2,9 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
|
|
3
3
|
const require_fixture_loader = require('./fixture-loader.cjs');
|
|
4
4
|
const require_llmock = require('./llmock.cjs');
|
|
5
|
-
const require_chunk_hooks = require('./node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.cjs');
|
|
6
5
|
let node_path = require("node:path");
|
|
7
6
|
let node_fs = require("node:fs");
|
|
7
|
+
let vitest = require("vitest");
|
|
8
8
|
|
|
9
9
|
//#region src/vitest.ts
|
|
10
10
|
/**
|
|
@@ -30,7 +30,7 @@ let node_fs = require("node:fs");
|
|
|
30
30
|
*/
|
|
31
31
|
function useAimock(options = {}) {
|
|
32
32
|
let handle = null;
|
|
33
|
-
|
|
33
|
+
(0, vitest.beforeAll)(async () => {
|
|
34
34
|
const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;
|
|
35
35
|
const llm = new require_llmock.LLMock(serverOpts);
|
|
36
36
|
if (fixturePath) {
|
|
@@ -47,10 +47,10 @@ function useAimock(options = {}) {
|
|
|
47
47
|
url
|
|
48
48
|
};
|
|
49
49
|
});
|
|
50
|
-
|
|
50
|
+
(0, vitest.beforeEach)(() => {
|
|
51
51
|
if (handle) handle.llm.resetMatchCounts();
|
|
52
52
|
});
|
|
53
|
-
|
|
53
|
+
(0, vitest.afterAll)(async () => {
|
|
54
54
|
if (handle) {
|
|
55
55
|
if (options.patchEnv !== false) {
|
|
56
56
|
delete process.env.OPENAI_BASE_URL;
|
package/dist/vitest.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vitest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\nimport { beforeAll, afterAll, beforeEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n delete process.env.OPENAI_BASE_URL;\n delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch {\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;AAElC
|
|
1
|
+
{"version":3,"file":"vitest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\nimport { beforeAll, afterAll, beforeEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n delete process.env.OPENAI_BASE_URL;\n delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch {\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;AAElC,uBAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,oCADE,YAAY,CACQ;AAC7C,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,8BAAiB;AACf,MAAI,OACF,QAAO,IAAI,kBAAkB;GAE/B;AAEF,sBAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,WAAO,QAAQ,IAAI;AACnB,WAAO,QAAQ,IAAI;;AAErB,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAgC;AACpD,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,QAAOC,2CAAoB,YAAY;AAEzC,SAAOC,uCAAgB,YAAY;SAC7B;AACN,SAAO,EAAE"}
|
package/dist/vitest.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { loadFixtureFile, loadFixturesFromDir } from "./fixture-loader.js";
|
|
2
2
|
import { LLMock } from "./llmock.js";
|
|
3
|
-
import { afterAll, beforeAll, beforeEach } from "./node_modules/.pnpm/@vitest_runner@3.2.4/node_modules/@vitest/runner/dist/chunk-hooks.js";
|
|
4
3
|
import { resolve } from "node:path";
|
|
5
4
|
import { statSync } from "node:fs";
|
|
5
|
+
import { afterAll, beforeAll, beforeEach } from "vitest";
|
|
6
6
|
|
|
7
7
|
//#region src/vitest.ts
|
|
8
8
|
/**
|