stubsmith 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/cli.js +33 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +49 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +49 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ await axios.get('https://api.example.com/users');
|
|
|
50
50
|
Or configure via environment variables and call `install()` with no arguments:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
export STUBSMITH_URL="
|
|
53
|
+
export STUBSMITH_URL="https://ingest.stubsmith.dev/v1/captures"
|
|
54
54
|
export STUBSMITH_API_KEY="sk-your-project-key"
|
|
55
55
|
```
|
|
56
56
|
|
|
@@ -116,6 +116,13 @@ Both are overridable (`url` and `backendUrl`) for a project pointed at a
|
|
|
116
116
|
different environment. The rules timer is `unref()`ed, so the SDK never keeps
|
|
117
117
|
your process alive.
|
|
118
118
|
|
|
119
|
+
The SDK sends only over https. A plain `http://` URL is accepted only for
|
|
120
|
+
`localhost`, `127.0.0.1` and `::1`, for a backend on your own machine. With any
|
|
121
|
+
other `http://` URL for `url` or `backendUrl`, `install()` logs a warning and
|
|
122
|
+
capture stays off, so nothing is sent; `stubsmith pull`, `fetchBundle()` and
|
|
123
|
+
`fixtures()` throw an error instead. The SDK never follows a redirect: a 3xx
|
|
124
|
+
answer is treated as an error, so the API key is never re-sent to another URL.
|
|
125
|
+
|
|
119
126
|
---
|
|
120
127
|
|
|
121
128
|
## Privacy model
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import { realpathSync } from "fs";
|
|
|
5
5
|
import { pathToFileURL } from "url";
|
|
6
6
|
|
|
7
7
|
// src/_version.ts
|
|
8
|
-
var VERSION = "0.2.
|
|
8
|
+
var VERSION = "0.2.1";
|
|
9
9
|
|
|
10
10
|
// src/pull.ts
|
|
11
11
|
import { randomBytes } from "crypto";
|
|
@@ -39,6 +39,31 @@ function compareCodePoints(a, b) {
|
|
|
39
39
|
return 0;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// src/endpoint.ts
|
|
43
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
44
|
+
var InsecureEndpointError = class extends Error {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "InsecureEndpointError";
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
function checkEndpoint(url) {
|
|
51
|
+
let u;
|
|
52
|
+
try {
|
|
53
|
+
u = new URL(url);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new InsecureEndpointError(
|
|
56
|
+
"stubsmith: the Stubsmith URL is not a valid URL; the SDK sends only to an https URL."
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (u.protocol === "https:" && u.hostname !== "") return;
|
|
60
|
+
if (u.protocol === "http:" && LOCAL_HOSTS.has(u.hostname)) return;
|
|
61
|
+
const shown = u.hostname ? `${u.protocol}//${u.hostname}` : u.protocol;
|
|
62
|
+
throw new InsecureEndpointError(
|
|
63
|
+
`stubsmith: refusing to send to ${shown}. The SDK sends only over https; plain http is allowed only for localhost, 127.0.0.1 and ::1.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
// src/replayState.ts
|
|
43
68
|
import { AsyncLocalStorage } from "async_hooks";
|
|
44
69
|
import http from "http";
|
|
@@ -89,6 +114,12 @@ function sendRequest(url, opts) {
|
|
|
89
114
|
fail(err2);
|
|
90
115
|
return;
|
|
91
116
|
}
|
|
117
|
+
try {
|
|
118
|
+
checkEndpoint(url);
|
|
119
|
+
} catch (err2) {
|
|
120
|
+
fail(err2);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
92
123
|
const request = u.protocol === "https:" ? ORIGINAL_REQUEST.https : u.protocol === "http:" ? ORIGINAL_REQUEST.http : null;
|
|
93
124
|
if (request === null) {
|
|
94
125
|
fail(new Error(`unsupported protocol ${u.protocol}`));
|
|
@@ -187,6 +218,7 @@ async function fetchOne(apiUrl, apiKey, method, p, samples) {
|
|
|
187
218
|
if (method) params.set("method", method);
|
|
188
219
|
if (p) params.set("path", p);
|
|
189
220
|
if (samples !== null) params.set("samples", samples);
|
|
221
|
+
checkEndpoint(apiUrl);
|
|
190
222
|
const qs = [...params].length > 0 ? `?${params.toString()}` : "";
|
|
191
223
|
const url = `${apiUrl.replace(/\/+$/, "")}/v1/replay/bundle${qs}`;
|
|
192
224
|
let res;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/_version.ts","../src/pull.ts","../src/env.ts","../src/internal/codepoint.ts","../src/replayState.ts","../src/transport.ts"],"sourcesContent":["import { realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport { VERSION } from \"./_version.js\";\nimport type { Bundle } from \"./replay/bundle.js\";\nimport { type PullBundleResult, parseEndpoint, pullBundle, writeBundle } from \"./pull.js\";\n\nconst DEFAULT_OUT = \".stubsmith/bundle.json\";\n\nconst USAGE = `usage: stubsmith [-h] [--version] <command> ...\n\nStubSmith command-line tools.\n\ncommands:\n pull Fetch the replay bundle and write it to disk.\n\nusage: stubsmith pull [-h] [--out PATH] [--endpoint \"METHOD /path/template\"]... [--samples N|all]\n\n --out PATH Destination file (default: ${DEFAULT_OUT})\n --endpoint \"METHOD /path\" Filter to an endpoint, e.g. \"GET /api/users/{id}\".\n Repeat to pull several into one bundle. Each endpoint\n is one request; the responses are merged.\n --samples N|all Recordings to keep per response (default: 1, the newest).\n Requires --endpoint.\n\nEnvironment: STUBSMITH_API_KEY (required), STUBSMITH_API_URL.\n`;\n\ninterface PullArgs {\n out: string;\n endpoints: string[];\n samples: string | null;\n}\n\nfunction out(s: string): void {\n process.stdout.write(`${s}\\n`);\n}\nfunction err(s: string): void {\n process.stderr.write(`${s}\\n`);\n}\n\n/** Parse `pull` flags; a string is an error message. Accepts `--flag value` and `--flag=value`. */\nfunction parsePull(argv: string[]): PullArgs | \"help\" | string {\n const args: PullArgs = { out: DEFAULT_OUT, endpoints: [], samples: null };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i] as string;\n if (a === \"-h\" || a === \"--help\") return \"help\";\n const m = /^(--(?:out|endpoint|samples))(?:=([\\s\\S]*))?$/.exec(a);\n if (!m) return `unrecognized arguments: ${a}`;\n const flag = m[1] as string;\n let value = m[2];\n if (value === undefined) {\n if (i + 1 >= argv.length) return `argument ${flag}: expected one argument`;\n value = argv[++i] as string;\n }\n if (flag === \"--out\") args.out = value;\n else if (flag === \"--endpoint\") args.endpoints.push(value);\n else args.samples = value;\n }\n return args;\n}\n\nfunction printSummary(bundle: Bundle, outPath: string, s: PullBundleResult[\"summary\"]): void {\n out(`Wrote ${outPath}`);\n out(` endpoints : ${s.endpoints}`);\n out(` stubs : ${s.stubs}`);\n out(` variants : ${s.variants}`);\n out(` cursor : ${bundle.cursor ?? \"\"}`);\n out(` generated : ${bundle.generated_at ?? \"\"}`);\n\n const t = bundle.truncated as Record<string, Record<string, unknown> | Array<Record<string, unknown>>> | null | undefined;\n if (t && typeof t === \"object\" && Object.keys(t).length > 0) {\n out(\"\");\n out(\"WARNING: bundle is truncated - not all data was included.\");\n const stubs = t.stubs as Record<string, unknown> | undefined;\n if (stubs && stubs.dropped) out(` stubs: ${stubs.dropped} stubs dropped (server limit: ${stubs.limit})`);\n const variants = t.variants as Array<Record<string, unknown>> | undefined;\n if (Array.isArray(variants) && variants.length > 0) {\n const dropped = variants.reduce((n, v) => n + (typeof v.dropped === \"number\" ? v.dropped : 0), 0);\n out(\n ` variants: ${dropped} variant(s) dropped across ${variants.length} fingerprint(s) ` +\n `(server limit per fingerprint: ${(variants[0] as Record<string, unknown>).limit})`,\n );\n }\n const samples = t.samples as Record<string, unknown> | undefined;\n if (samples && samples.limit) out(` samples: clamped to the server limit of ${samples.limit} recordings per response`);\n const bb = t.body_bytes as Record<string, unknown> | undefined;\n if (bb) {\n const limitKb = Math.floor((typeof bb.limit === \"number\" ? bb.limit : 0) / 1024);\n out(\n ` body_bytes: ${bb.capped} variant body/bodies omitted (exceeded ${limitKb} KB cap) - ` +\n `these stubs will replay with an empty body`,\n );\n }\n }\n if (s.degraded.length > 0) {\n out(\n `WARNING: ${s.degraded.length} stub(s) marked degraded - ` +\n \"no recorded captures are available; replay will fail for those stubs.\",\n );\n }\n if (s.bodyCapped > 0) {\n out(\n `WARNING: ${s.bodyCapped} variant(s) have body_capped=true - ` +\n \"body exceeded the server size cap and was omitted; \" +\n \"replay will return an empty body for those variants.\",\n );\n }\n}\n\nasync function cmdPull(args: PullArgs): Promise<number> {\n const apiKey = process.env.STUBSMITH_API_KEY ?? \"\";\n if (!apiKey) {\n err(\"Error: STUBSMITH_API_KEY is not set. Export your project API key before running stubsmith pull.\");\n return 1;\n }\n\n for (const raw of args.endpoints) {\n if (!parseEndpoint(raw)) {\n err(`Error: --endpoint must be \"METHOD /path\" (e.g. \"GET /api/users\"), got: ${JSON.stringify(raw)}`);\n return 1;\n }\n }\n\n if (args.samples !== null) {\n const raw = args.samples.trim().toLowerCase();\n if (raw !== \"all\" && !/^\\d+$/.test(raw)) {\n err(`Error: --samples must be a positive integer or 'all', got: ${JSON.stringify(args.samples)}`);\n return 1;\n }\n if (raw !== \"all\" && Number(raw) < 1) {\n err(\"Error: --samples must be at least 1.\");\n return 1;\n }\n if ((raw === \"all\" || Number(raw) > 1) && args.endpoints.length === 0) {\n err(\n \"Error: --samples greater than 1 requires --endpoint.\\n\" +\n \"Fetching every recording for a whole project is not supported; \" +\n 'scope it, e.g. --endpoint \"GET /api/orders\". Repeat --endpoint ' +\n \"to cover several in one bundle.\",\n );\n return 1;\n }\n }\n\n let result;\n try {\n result = await pullBundle({\n apiKey,\n endpoints: args.endpoints,\n ...(args.samples !== null ? { samples: args.samples } : {}),\n onFetch: (method, path) => err(`Fetching ${method} ${path} ...`),\n });\n } catch (e) {\n err(`Error: ${e instanceof Error ? e.message : String(e)}`);\n return 1;\n }\n\n try {\n await writeBundle(args.out, result.bundle);\n } catch (e) {\n err(`Error writing bundle: ${e instanceof Error ? e.message : String(e)}`);\n return 1;\n }\n printSummary(result.bundle, args.out, result.summary);\n return 0;\n}\n\n/** CLI entry point; `argv` excludes the node binary and script path. Returns the exit code. */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n const first = argv[0];\n if (first === \"--version\" || first === \"-V\") {\n out(`stubsmith ${VERSION}`);\n return 0;\n }\n if (first === \"-h\" || first === \"--help\") {\n out(USAGE);\n return 0;\n }\n if (first === undefined) {\n err(USAGE);\n err(\"stubsmith: error: the following arguments are required: <command>\");\n return 1;\n }\n if (first !== \"pull\") {\n err(USAGE);\n err(`stubsmith: error: unrecognized command or argument: ${first}`);\n return 1;\n }\n const parsed = parsePull(argv.slice(1));\n if (parsed === \"help\") {\n out(USAGE);\n return 0;\n }\n if (typeof parsed === \"string\") {\n err(`stubsmith pull: error: ${parsed}`);\n return 1;\n }\n return cmdPull(parsed);\n}\n\nfunction isEntryPoint(): boolean {\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return import.meta.url === pathToFileURL(realpathSync(argv1)).href;\n } catch {\n return false;\n }\n}\n\nif (isEntryPoint()) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (e: unknown) => {\n err(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exitCode = 1;\n },\n );\n}\n","export const VERSION = \"0.2.0\";\n","/**\n * Fetch the replay bundle (`GET /v1/replay/bundle`): `fetchBundle` returns the\n * raw server bundle for one request (as Python `fetch_bundle`), `pullBundle`\n * merges, sorts and summarises several the way the Python `stubsmith pull`\n * does, and `writeBundle` serialises it identically.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\nimport { VERSION } from \"./_version.js\";\nimport { DEFAULT_BACKEND_URL, resolveApiUrl } from \"./env.js\";\nimport { compareCodePoints } from \"./internal/codepoint.js\";\nimport type { Bundle } from \"./replay/bundle.js\";\nimport { sendRequest } from \"./transport.js\";\n\nconst TIMEOUT_MS = 30_000;\n/*\n * Bundle response cap. The backend bounds a bundle at 2000 stubs, 10 variants\n * per fingerprint and 100 KB per body, so a real one is far below this. The\n * peak memory for a response of N bytes is roughly 3N to 4N: the collected\n * chunks, the concatenated Buffer, the utf8 string and the parsed object tree\n * all coexist briefly. 128 MiB therefore bounds the process near 0.5 GiB.\n */\nconst MAX_BYTES = 128 * 1024 * 1024;\nconst MAX_MIB = MAX_BYTES / (1024 * 1024);\n\ntype Obj = Record<string, unknown>;\n\nexport interface FetchBundleOptions {\n /** Recordings per response; above one (or `\"all\"`) needs an endpoint. */\n samples?: number | \"all\" | string;\n /** Defaults to `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted service. */\n apiUrl?: string;\n /** Defaults to `STUBSMITH_API_KEY`. */\n apiKey?: string;\n}\n\nexport interface PullBundleOptions {\n /** Defaults to `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted service. */\n apiUrl?: string;\n /** Defaults to `STUBSMITH_API_KEY`. */\n apiKey?: string;\n /** `\"METHOD /path\"` filters, one request each; omitted means one unfiltered request. */\n endpoints?: string[];\n /** Recordings per response; above one (or `\"all\"`) needs `endpoints`. */\n samples?: number | \"all\" | string;\n /** Called before each filtered request (the CLI prints progress). */\n onFetch?: (method: string, path: string) => void;\n}\n\nexport interface BundleStubRef {\n domain: string;\n method: string;\n pathTemplate: string;\n fingerprint: string;\n}\n\nexport interface BundleSummary {\n endpoints: number;\n stubs: number;\n variants: number;\n /** Every stub the server marked degraded. */\n degraded: BundleStubRef[];\n /** Variants with `body_capped` set. */\n bodyCapped: number;\n}\n\nexport interface PullBundleResult {\n bundle: Bundle;\n summary: BundleSummary;\n}\n\nfunction isObj(v: unknown): v is Obj {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction arr(v: unknown): Obj[] {\n return Array.isArray(v) ? v.filter(isObj) : [];\n}\n\n/** Python `x or \"\"` for a string-ish field. */\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : \"\";\n}\n\n/** Split `\"METHOD /path\"` on the first space; null when malformed. */\nexport function parseEndpoint(raw: string): { method: string; path: string } | null {\n const i = raw.indexOf(\" \");\n if (i < 0) return null;\n const method = raw.slice(0, i).trim();\n const p = raw.slice(i + 1).trim();\n if (!method || !p) return null;\n return { method: method.toUpperCase(), path: p };\n}\n\n/** Validate a samples value; returns the normalised string or throws. `msg` builds the error from a reason. */\nexport function normaliseSamples(samples: number | string, endpointGiven: boolean): string {\n const raw = String(samples).trim().toLowerCase();\n if (raw !== \"all\" && !/^\\d+$/.test(raw)) {\n throw new Error(`samples must be a positive integer or 'all', got: ${JSON.stringify(samples)}`);\n }\n if (raw !== \"all\" && Number(raw) < 1) throw new Error(`samples must be at least 1, got: ${JSON.stringify(samples)}`);\n if ((raw === \"all\" || Number(raw) > 1) && !endpointGiven) {\n throw new Error(\n \"samples greater than 1 requires an endpoint. Fetching every recording for a whole project is not \" +\n 'supported; scope it, e.g. endpoints: [\"GET /api/orders\"].',\n );\n }\n return raw;\n}\n\nasync function fetchOne(\n apiUrl: string,\n apiKey: string,\n method: string | null,\n p: string | null,\n samples: string | null,\n): Promise<Bundle> {\n const params = new URLSearchParams();\n if (method) params.set(\"method\", method);\n if (p) params.set(\"path\", p);\n if (samples !== null) params.set(\"samples\", samples);\n const qs = [...params].length > 0 ? `?${params.toString()}` : \"\";\n const url = `${apiUrl.replace(/\\/+$/, \"\")}/v1/replay/bundle${qs}`;\n\n let res;\n try {\n res = await sendRequest(url, {\n method: \"GET\",\n headers: { authorization: `Bearer ${apiKey}`, \"user-agent\": `stubsmith-cli/${VERSION}` },\n timeoutMs: TIMEOUT_MS,\n holdProcess: true,\n maxBytes: MAX_BYTES,\n }).promise;\n } catch (err) {\n throw new Error(\n `Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}\\n` +\n `Check that the backend is reachable, or set STUBSMITH_API_URL to override (default: ${DEFAULT_BACKEND_URL}).`,\n );\n }\n if (res.truncated) throw new Error(`Bundle response exceeded ${MAX_MIB} MiB; narrow with --endpoint.`);\n const text = res.body.toString(\"utf8\");\n if (res.status === 404) {\n throw new Error(\n `HTTP 404 from ${url}\\n` +\n `The StubSmith backend was not found at that URL.\\n` +\n `For the hosted service the URL is ${DEFAULT_BACKEND_URL}.\\n` +\n `Override with STUBSMITH_API_URL, or unset it to use the default.`,\n );\n }\n if (res.status < 200 || res.status >= 300) throw new Error(`HTTP ${res.status}: ${text}`);\n if (!res.complete) throw new Error(`Connection closed before the response completed (${url}). Retry the pull.`);\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch (err) {\n throw new Error(`Server returned unparseable JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (!isObj(data)) throw new Error(\"Server returned an unexpected response (not a JSON object)\");\n if (!Array.isArray(data.endpoints)) throw new Error(\"Server returned an unexpected response (no endpoints array)\");\n return data as unknown as Bundle;\n}\n\n/** Python `int(x or 0)` for a cursor string; null when it does not parse. */\nfunction cursorValue(v: unknown): bigint | null {\n if (v === undefined || v === null || v === \"\" || v === 0 || v === false) return 0n;\n if (typeof v === \"number\") return Number.isInteger(v) ? BigInt(v) : BigInt(Math.trunc(v));\n if (typeof v === \"string\" && /^\\s*[+-]?\\d+(?:_\\d+)*\\s*$/.test(v)) return BigInt(v.trim().replace(/_/g, \"\"));\n return null;\n}\n\n/** Merge per-endpoint bundles as if the server had returned one (cli.py `_merge_bundles`). */\nexport function mergeBundles(typed: Bundle[]): Bundle {\n // The merge works on loose records: servers add fields, and generated_at may be null.\n const parts = typed as unknown as Obj[];\n if (parts.length === 0) return { ok: true, version: 1, cursor: \"0\", endpoints: [] };\n if (parts.length === 1) return typed[0] as Bundle;\n\n const merged = new Map<string, Obj & { stubs: Obj[] }>();\n const seen = new Map<string, Set<string>>();\n const truncated = new Map<string, unknown>();\n let cursor = 0n;\n\n for (const part of parts) {\n const c = cursorValue(part.cursor);\n if (c !== null && c > cursor) cursor = c;\n if (isObj(part.truncated)) {\n for (const [k, v] of Object.entries(part.truncated)) if (!truncated.has(k)) truncated.set(k, v);\n }\n for (const ep of arr(part.endpoints)) {\n // JSON array of the key parts: unambiguous whatever the strings contain.\n const key = JSON.stringify([str(ep.domain), str(ep.method).toUpperCase(), str(ep.path_template)]);\n let m = merged.get(key);\n if (!m) {\n m = { ...ep, stubs: [] };\n merged.set(key, m);\n seen.set(key, new Set());\n }\n const fps = seen.get(key) as Set<string>;\n for (const stub of arr(ep.stubs)) {\n const fp = str(stub.fingerprint);\n if (fps.has(fp)) continue;\n fps.add(fp);\n m.stubs.push(stub);\n }\n }\n }\n\n const out: Obj = {\n ok: true,\n version: (parts[0] as Obj).version ?? 1,\n cursor: cursor.toString(),\n generated_at: (parts[parts.length - 1] as Obj).generated_at ?? null,\n endpoints: [...merged.values()],\n };\n // The server sends the same project_settings in every part, except that a\n // part served while it could not read them has none or an empty one. Replay\n // reads them, so keep the last part that holds the setting.\n for (let i = parts.length - 1; i >= 0; i--) {\n const ps = (parts[i] as Obj).project_settings;\n if (isObj(ps) && typeof ps.group_ids_in_paths === \"boolean\") {\n out.project_settings = ps;\n break;\n }\n }\n if (truncated.size > 0) out.truncated = Object.fromEntries(truncated);\n return out as unknown as Bundle;\n}\n\n/** Python `v.get(\"status\") or 0` sort key. */\nfunction statusKey(v: Obj): number {\n return typeof v.status === \"number\" ? v.status : 0;\n}\n\n/** Copy of `data` with endpoints by (domain, method, path_template), stubs by fingerprint, variants by status. */\nexport function sortBundle(typed: Bundle): Bundle {\n const data = typed as unknown as Obj;\n const endpoints = [...arr(data.endpoints)].sort(\n (a, b) =>\n compareCodePoints(str(a.domain), str(b.domain)) ||\n compareCodePoints(str(a.method), str(b.method)) ||\n compareCodePoints(str(a.path_template), str(b.path_template)),\n );\n return {\n ...data,\n endpoints: endpoints.map((ep) => ({\n ...ep,\n stubs: [...arr(ep.stubs)]\n .sort((a, b) => compareCodePoints(str(a.fingerprint), str(b.fingerprint)))\n .map((stub) => ({ ...stub, variants: [...arr(stub.variants)].sort((a, b) => statusKey(a) - statusKey(b)) })),\n })),\n } as unknown as Bundle;\n}\n\nexport function summarizeBundle(typed: Bundle): BundleSummary {\n const data = typed as unknown as Obj;\n const eps = arr(data.endpoints);\n const summary: BundleSummary = { endpoints: eps.length, stubs: 0, variants: 0, degraded: [], bodyCapped: 0 };\n for (const ep of eps) {\n for (const stub of arr(ep.stubs)) {\n summary.stubs++;\n if (stub.degraded) {\n summary.degraded.push({\n domain: str(ep.domain),\n method: str(ep.method),\n pathTemplate: str(ep.path_template),\n fingerprint: str(stub.fingerprint),\n });\n }\n for (const v of arr(stub.variants)) {\n summary.variants++;\n if (v.body_capped) summary.bodyCapped++;\n }\n }\n }\n return summary;\n}\n\nfunction resolveAuth(opts: { apiKey?: string; apiUrl?: string }): { apiKey: string; apiUrl: string } {\n const apiKey = opts.apiKey || process.env.STUBSMITH_API_KEY || \"\";\n if (!apiKey) throw new Error(\"No API key: pass apiKey or set $STUBSMITH_API_KEY.\");\n return { apiKey, apiUrl: opts.apiUrl || resolveApiUrl() };\n}\n\n/**\n * Fetch the raw replay bundle for one request: the whole project, or the\n * `\"METHOD /path\"` endpoint. Returned as the server sent it, unmerged and\n * unsorted, so `replay(await fetchBundle())` works. Does not touch disk.\n */\nexport async function fetchBundle(endpoint?: string, opts: FetchBundleOptions = {}): Promise<Bundle> {\n const { apiKey, apiUrl } = resolveAuth(opts);\n let method: string | null = null;\n let p: string | null = null;\n if (endpoint !== undefined) {\n const ep = parseEndpoint(endpoint);\n if (!ep) throw new Error(`endpoint must be \"METHOD /path\" (e.g. \"GET /api/orders\"), got: ${JSON.stringify(endpoint)}`);\n method = ep.method;\n p = ep.path;\n }\n const samples = opts.samples === undefined ? null : normaliseSamples(opts.samples, endpoint !== undefined);\n return fetchOne(apiUrl, apiKey, method, p, samples);\n}\n\n/**\n * Fetch the replay bundle, one request per endpoint (or one unfiltered\n * request), merged and sorted. Does not touch disk.\n */\nexport async function pullBundle(opts: PullBundleOptions = {}): Promise<PullBundleResult> {\n const { apiKey, apiUrl } = resolveAuth(opts);\n\n const targets: Array<{ method: string | null; path: string | null }> = [];\n for (const raw of opts.endpoints ?? []) {\n const ep = parseEndpoint(raw);\n if (!ep) throw new Error(`endpoint must be \"METHOD /path\" (e.g. \"GET /api/orders\"), got: ${JSON.stringify(raw)}`);\n targets.push({ method: ep.method, path: ep.path });\n }\n const samples = opts.samples === undefined ? null : normaliseSamples(opts.samples, targets.length > 0);\n if (targets.length === 0) targets.push({ method: null, path: null });\n\n const fetched: Bundle[] = [];\n for (const t of targets) {\n if (t.method && t.path) opts.onFetch?.(t.method, t.path);\n fetched.push(await fetchOne(apiUrl, apiKey, t.method, t.path, samples));\n }\n const bundle = sortBundle(mergeBundles(fetched));\n return { bundle, summary: summarizeBundle(bundle) };\n}\n\nfunction compareKeys(a: string, b: string): number {\n return compareCodePoints(a, b);\n}\n\n/*\n * Numbers go through JSON.stringify, so a float would format differently from\n * Python (`12.0` vs `12`, `1e+16` vs `1e16`). The backend emits only integers\n * and strings today, so output matches; revisit if a float field is added.\n */\n/** JSON as Python `json.dumps(sort_keys=True, indent=2, ensure_ascii=False)`: keys sorted by code point, non-ASCII kept. */\nexport function stringifyBundle(value: unknown, indent = \"\"): string {\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n const inner = `${indent} `;\n return `[\\n${value.map((v) => inner + stringifyBundle(v === undefined ? null : v, inner)).join(\",\\n\")}\\n${indent}]`;\n }\n if (isObj(value)) {\n const keys = Object.keys(value)\n .filter((k) => value[k] !== undefined)\n .sort(compareKeys);\n if (keys.length === 0) return \"{}\";\n const inner = `${indent} `;\n return `{\\n${keys.map((k) => `${inner}${JSON.stringify(k)}: ${stringifyBundle(value[k], inner)}`).join(\",\\n\")}\\n${indent}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n\n/** Write atomically: temp file in the same directory, chmod 0644, rename into place. */\nexport async function writeBundle(outPath: string, data: Bundle): Promise<void> {\n const dest = path.resolve(outPath);\n const dir = path.dirname(dest);\n await fsp.mkdir(dir, { recursive: true });\n const serialised = `${stringifyBundle(data)}\\n`;\n const tmp = path.join(dir, `.stubsmith-bundle-${randomBytes(6).toString(\"hex\")}`);\n try {\n await fsp.writeFile(tmp, serialised, { encoding: \"utf8\", flag: \"wx\", mode: 0o600 });\n await fsp.chmod(tmp, 0o644);\n await fsp.rename(tmp, dest);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","/**\n * Environment configuration, debug logging and once-per-process warnings.\n * Names and defaults follow SPEC.md section 4 (Python `client.py`).\n */\n\nexport const DEFAULT_URL = \"https://ingest.stubsmith.dev/v1/captures\";\nexport const DEFAULT_BACKEND_URL = \"https://app.stubsmith.dev/api\";\n/** Exit-flush budget in seconds when `STUBSMITH_FLUSH_TIMEOUT` is unset or malformed. */\nexport const DEFAULT_FLUSH_TIMEOUT_S = 1.0;\n\nexport interface EnvConfig {\n url: string;\n apiKey: string;\n backendUrl: string;\n /** `STUBSMITH_API_URL`; empty when unset (the CLI falls back to the backend URL). */\n apiUrl: string;\n debug: boolean;\n /** Seconds, never negative. */\n flushTimeoutS: number;\n maskSalt: string;\n}\n\nfunction env(name: string): string | undefined {\n return process.env[name];\n}\n\n/** `1`, `true`, `yes` (case-insensitive, trimmed) are truthy; anything else is not. */\nexport function parseBool(raw: string | undefined): boolean {\n return [\"1\", \"true\", \"yes\"].includes((raw ?? \"\").trim().toLowerCase());\n}\n\n/** Python `float()` decimal grammar: digits with single underscores between them, optional fraction and exponent. */\nconst PY_FLOAT_RE = /^[+-]?(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[eE][+-]?\\d+(?:_\\d+)*)?$/;\n\n/**\n * Seconds from `raw`, accepting what Python `float()` accepts for decimal\n * numbers (`1_000`, `.5`, `1e2`; not `0x10` or `0b11`, which `Number()` would).\n * A malformed value is ignored (the SDK must never break the host). Python also\n * accepts `inf` and `nan`; those are rejected here (fallback 1.0) because an\n * unbounded exit wait would let a dead endpoint hang the application forever,\n * and a value that overflows to Infinity (`1e999`) is treated the same way.\n */\nexport function parseFlushTimeout(raw: string | undefined): number {\n const s = (raw ?? \"\").trim();\n if (s && PY_FLOAT_RE.test(s)) {\n const n = Number(s.replace(/_/g, \"\"));\n if (Number.isFinite(n)) return Math.max(0, n);\n }\n return DEFAULT_FLUSH_TIMEOUT_S;\n}\n\n/** Read every `STUBSMITH_*` variable with the SPEC defaults. */\nexport function readEnv(): EnvConfig {\n return {\n url: env(\"STUBSMITH_URL\") || DEFAULT_URL,\n apiKey: env(\"STUBSMITH_API_KEY\") ?? \"\",\n backendUrl: env(\"STUBSMITH_BACKEND_URL\") || DEFAULT_BACKEND_URL,\n apiUrl: env(\"STUBSMITH_API_URL\") ?? \"\",\n debug: parseBool(env(\"STUBSMITH_DEBUG\")),\n flushTimeoutS: parseFlushTimeout(env(\"STUBSMITH_FLUSH_TIMEOUT\")),\n maskSalt: env(\"STUBSMITH_MASK_SALT\") ?? \"\",\n };\n}\n\nlet debugOverride: boolean | undefined;\n\n/** Pin the debug flag (a client constructed with an explicit `debug`); `undefined` goes back to the env var. */\nexport function setDebug(on: boolean | undefined): void {\n debugOverride = on;\n}\n\nexport function isDebug(): boolean {\n return debugOverride ?? parseBool(env(\"STUBSMITH_DEBUG\"));\n}\n\n/** Print to stderr only when debug is on. Never throws. */\nexport function debugLog(msg: string): void {\n if (!isDebug()) return;\n try {\n process.stderr.write(`stubsmith: ${msg}\\n`);\n } catch {\n // stderr may be closed; logging must never break the host.\n }\n}\n\n/** Cap on distinct keys remembered by {@link warnOnce} (client.py `_INGEST_MAX_WARNED_CODES`). */\nexport const WARN_ONCE_CAP = 64;\n\nconst warned = new Set<string>();\n\n/**\n * `console.warn(msg)` once per process per `key`. Past {@link WARN_ONCE_CAP}\n * distinct keys new keys stay quiet, so a varying key cannot flood the logs.\n * Returns true when the message was printed.\n */\nexport function warnOnce(key: string, msg: string): boolean {\n if (warned.has(key) || warned.size >= WARN_ONCE_CAP) return false;\n warned.add(key);\n try {\n console.warn(msg);\n } catch {\n // never break the host\n }\n return true;\n}\n\n/** Test hook: forget every warned key. */\nexport function resetWarnOnceForTests(): void {\n warned.clear();\n}\n\n/** Backend base URL for `pull` / `fetchBundle`: `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted default. */\nexport function resolveApiUrl(): string {\n return env(\"STUBSMITH_API_URL\") || env(\"STUBSMITH_BACKEND_URL\") || DEFAULT_BACKEND_URL;\n}\n\n/** Base URL for the fixtures API: `STUBSMITH_API_URL` only (SPEC 1.4), then the hosted default. */\nexport function resolveFixturesApiUrl(): string {\n return env(\"STUBSMITH_API_URL\") || DEFAULT_BACKEND_URL;\n}\n","/**\n * Compare two strings by Unicode code point, the order Python `sorted()` /\n * `str.__lt__` uses (CPython Objects/unicodeobject.c `unicode_compare`, reached\n * via `PyUnicode_RichCompare`; the file is not vendored here so no line is\n * cited). JS default comparison is by UTF-16 code unit, which places astral\n * characters (surrogates D800-DFFF) before U+E000-U+FFFF.\n *\n * Both strings are walked one code point at a time with `codePointAt`. A\n * valid surrogate pair is one astral code point; a lone surrogate is its own\n * code point (0xD800..0xDFFF), as in a Python str, and is compared\n * numerically against whatever the other string holds at that position.\n */\nexport function compareCodePoints(a: string, b: string): number {\n const la = a.length;\n const lb = b.length;\n let i = 0;\n let j = 0;\n while (i < la && j < lb) {\n const pa = a.codePointAt(i) as number;\n const pb = b.codePointAt(j) as number;\n if (pa !== pb) return pa < pb ? -1 : 1;\n i += pa > 0xffff ? 2 : 1;\n j += pb > 0xffff ? 2 : 1;\n }\n if (i < la) return 1;\n if (j < lb) return -1;\n return 0;\n}\n\n/**\n * Return a new array sorted by Unicode code point (Python `sorted(list)`).\n * The sort is stable. Does not mutate the input.\n */\nexport function sortCodePoints(arr: readonly string[]): string[] {\n return [...arr].sort(compareCodePoints);\n}\n","/**\n * Process-global shared state. Everything lives on `globalThis` under\n * `Symbol.for(...)` keys so duplicate copies of the package (ESM and CJS both\n * loaded, or two versions) see the same replay depth, capture guard, install\n * singleton and original `http.request`.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport http from \"node:http\";\nimport https from \"node:https\";\n\nconst DEPTH_KEY = Symbol.for(\"stubsmith.replayDepth\");\nconst GUARD_KEY = Symbol.for(\"stubsmith.captureGuard\");\nconst REQUEST_KEY = Symbol.for(\"stubsmith.originalRequest\");\n\ntype Shared = Record<symbol, unknown>;\nconst g = globalThis as unknown as Shared;\n\n/** Shared slot with a lazily created value. */\nexport function sharedSlot<T>(key: symbol, init: () => T): T {\n if (g[key] === undefined) g[key] = init();\n return g[key] as T;\n}\n\ninterface Depth {\n n: number;\n}\nconst depth = sharedSlot<Depth>(DEPTH_KEY, () => ({ n: 0 }));\n\n/** True while at least one replay context is active (capture is suppressed). */\nexport function replayActive(): boolean {\n return depth.n > 0;\n}\n\nexport function enterReplay(): void {\n depth.n += 1;\n}\n\nexport function exitReplay(): void {\n if (depth.n > 0) depth.n -= 1;\n}\n\n/**\n * Outermost-frame guard. A hook that finds a store set does not capture; the\n * fetch hook and every SDK-internal request run inside `captureGuard.run(true, ...)`.\n */\nexport const captureGuard = sharedSlot<AsyncLocalStorage<true>>(GUARD_KEY, () => new AsyncLocalStorage<true>());\n\n/**\n * The `http.request` / `https.request` that existed when the first copy of\n * the SDK loaded. The sender and the rules poller use these (not `fetch`), so\n * their sockets can be unref'd and they bypass the capture hooks.\n */\nexport const ORIGINAL_REQUEST = sharedSlot<{ http: typeof http.request; https: typeof https.request }>(REQUEST_KEY, () => ({\n http: http.request,\n https: https.request,\n}));\n","/**\n * Minimal HTTP client for the SDK's own traffic (ingest POSTs and rules sync).\n *\n * It uses the ORIGINAL `http.request` / `https.request` captured at module\n * load, not `fetch`: fetch sockets are ref'd, so a stalled endpoint would hold\n * the process open at exit. Here the socket is unref'd and the timeouts are\n * explicit, so nothing of the SDK's keeps a finished application alive. The\n * request is created inside `captureGuard.run`, so the capture hooks skip it.\n *\n * Callers whose request IS the work (the `pull` CLI, `fixtures()`) set\n * `holdProcess`: the socket and the deadline timer stay ref'd so the process\n * lives until the response arrives.\n */\nimport type { ClientRequest, IncomingHttpHeaders } from \"node:http\";\nimport { ORIGINAL_REQUEST, captureGuard } from \"./replayState.js\";\n\nexport interface TransportOptions {\n method: string;\n /** Sent as given (lowercase names keep the wire form of the old fetch-based sender). */\n headers: Record<string, string>;\n body?: Buffer;\n /**\n * Overall deadline for the exchange, also applied as the socket idle timeout.\n * With `holdProcess` it is only the idle timeout (no bytes for this long);\n * the overall deadline is then {@link HOLD_TOTAL_MS} unless `totalTimeoutMs` says otherwise.\n */\n timeoutMs: number;\n /**\n * Keep the process alive until the exchange finishes (no `unref()` on the\n * socket or the deadline timer). Default false: the SDK's background sender\n * and poller must never delay exit. Only standalone callers (CLI, `fixtures`,\n * `fetchBundle`) set it.\n */\n holdProcess?: boolean;\n /** Overall deadline override; defaults to `timeoutMs`, or {@link HOLD_TOTAL_MS} with `holdProcess`. */\n totalTimeoutMs?: number;\n /** Response bytes kept; the rest of the body is dropped and the connection destroyed. */\n maxBytes: number;\n}\n\n/**\n * Total deadline for `holdProcess` callers. A large bundle on a slow link can\n * legitimately take longer than the 30 s idle timeout; Python's per-operation\n * timeout does not cap the whole transfer either.\n */\nexport const HOLD_TOTAL_MS = 120_000;\n\nexport interface TransportResult {\n status: number;\n headers: IncomingHttpHeaders;\n body: Buffer;\n /** The body was cut at `maxBytes`; `body` is a prefix. */\n truncated: boolean;\n /** The response ended normally. False when the connection closed early. */\n complete: boolean;\n}\n\nexport interface Transport {\n /** Rejects on a network error, a timeout or {@link Transport.abort}. */\n promise: Promise<TransportResult>;\n /** Destroy the connection; `promise` rejects. */\n abort(): void;\n}\n\nexport function sendRequest(url: string, opts: TransportOptions): Transport {\n let req: ClientRequest | null = null;\n let aborted = false;\n const abort = (): void => {\n aborted = true;\n req?.destroy(new Error(\"aborted\"));\n };\n\n const promise = new Promise<TransportResult>((resolve, reject) => {\n let settled = false;\n let total: NodeJS.Timeout | null = null;\n const fail = (err: unknown): void => {\n if (settled) return;\n settled = true;\n if (total) clearTimeout(total);\n reject(err instanceof Error ? err : new Error(String(err)));\n };\n const succeed = (r: TransportResult): void => {\n if (settled) return;\n settled = true;\n if (total) clearTimeout(total);\n resolve(r);\n };\n\n let u: URL;\n try {\n u = new URL(url);\n } catch (err) {\n fail(err);\n return;\n }\n const request = u.protocol === \"https:\" ? ORIGINAL_REQUEST.https : u.protocol === \"http:\" ? ORIGINAL_REQUEST.http : null;\n if (request === null) {\n fail(new Error(`unsupported protocol ${u.protocol}`));\n return;\n }\n const headers = { ...opts.headers };\n if (opts.body !== undefined) headers[\"content-length\"] = String(opts.body.length);\n\n try {\n captureGuard.run(true, () => {\n req = request(u, { method: opts.method, headers }, (res) => {\n const chunks: Buffer[] = [];\n let got = 0;\n let truncated = false;\n const finish = (): void => {\n if (aborted) fail(new Error(\"aborted\"));\n else {\n succeed({\n status: res.statusCode ?? 0,\n headers: res.headers,\n body: Buffer.concat(chunks).subarray(0, opts.maxBytes),\n truncated,\n complete: !truncated && res.complete,\n });\n }\n };\n res.on(\"data\", (c: Buffer) => {\n if (got < opts.maxBytes) chunks.push(c);\n got += c.length;\n if (got > opts.maxBytes) {\n truncated = true;\n finish();\n res.destroy();\n }\n });\n res.on(\"end\", finish);\n // A body cut short still reports the status and what was read; `complete` says it was cut.\n res.on(\"error\", finish);\n res.on(\"close\", finish);\n });\n });\n } catch (err) {\n fail(err);\n return;\n }\n const r = req as unknown as ClientRequest;\n const hold = opts.holdProcess === true;\n const totalMs = opts.totalTimeoutMs ?? (hold ? HOLD_TOTAL_MS : opts.timeoutMs);\n total = setTimeout(() => r.destroy(new Error(\"timeout\")), totalMs);\n if (!hold) {\n total.unref();\n r.on(\"socket\", (s) => s.unref());\n }\n r.setTimeout(opts.timeoutMs, () => r.destroy(new Error(\"timeout\")));\n r.on(\"error\", fail);\n r.end(opts.body);\n if (aborted) r.destroy(new Error(\"aborted\"));\n });\n return { promise, abort };\n}\n"],"mappings":"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","names":["err","r","err","out","err","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/_version.ts","../src/pull.ts","../src/env.ts","../src/internal/codepoint.ts","../src/endpoint.ts","../src/replayState.ts","../src/transport.ts"],"sourcesContent":["import { realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport { VERSION } from \"./_version.js\";\nimport type { Bundle } from \"./replay/bundle.js\";\nimport { type PullBundleResult, parseEndpoint, pullBundle, writeBundle } from \"./pull.js\";\n\nconst DEFAULT_OUT = \".stubsmith/bundle.json\";\n\nconst USAGE = `usage: stubsmith [-h] [--version] <command> ...\n\nStubSmith command-line tools.\n\ncommands:\n pull Fetch the replay bundle and write it to disk.\n\nusage: stubsmith pull [-h] [--out PATH] [--endpoint \"METHOD /path/template\"]... [--samples N|all]\n\n --out PATH Destination file (default: ${DEFAULT_OUT})\n --endpoint \"METHOD /path\" Filter to an endpoint, e.g. \"GET /api/users/{id}\".\n Repeat to pull several into one bundle. Each endpoint\n is one request; the responses are merged.\n --samples N|all Recordings to keep per response (default: 1, the newest).\n Requires --endpoint.\n\nEnvironment: STUBSMITH_API_KEY (required), STUBSMITH_API_URL.\n`;\n\ninterface PullArgs {\n out: string;\n endpoints: string[];\n samples: string | null;\n}\n\nfunction out(s: string): void {\n process.stdout.write(`${s}\\n`);\n}\nfunction err(s: string): void {\n process.stderr.write(`${s}\\n`);\n}\n\n/** Parse `pull` flags; a string is an error message. Accepts `--flag value` and `--flag=value`. */\nfunction parsePull(argv: string[]): PullArgs | \"help\" | string {\n const args: PullArgs = { out: DEFAULT_OUT, endpoints: [], samples: null };\n for (let i = 0; i < argv.length; i++) {\n const a = argv[i] as string;\n if (a === \"-h\" || a === \"--help\") return \"help\";\n const m = /^(--(?:out|endpoint|samples))(?:=([\\s\\S]*))?$/.exec(a);\n if (!m) return `unrecognized arguments: ${a}`;\n const flag = m[1] as string;\n let value = m[2];\n if (value === undefined) {\n if (i + 1 >= argv.length) return `argument ${flag}: expected one argument`;\n value = argv[++i] as string;\n }\n if (flag === \"--out\") args.out = value;\n else if (flag === \"--endpoint\") args.endpoints.push(value);\n else args.samples = value;\n }\n return args;\n}\n\nfunction printSummary(bundle: Bundle, outPath: string, s: PullBundleResult[\"summary\"]): void {\n out(`Wrote ${outPath}`);\n out(` endpoints : ${s.endpoints}`);\n out(` stubs : ${s.stubs}`);\n out(` variants : ${s.variants}`);\n out(` cursor : ${bundle.cursor ?? \"\"}`);\n out(` generated : ${bundle.generated_at ?? \"\"}`);\n\n const t = bundle.truncated as Record<string, Record<string, unknown> | Array<Record<string, unknown>>> | null | undefined;\n if (t && typeof t === \"object\" && Object.keys(t).length > 0) {\n out(\"\");\n out(\"WARNING: bundle is truncated - not all data was included.\");\n const stubs = t.stubs as Record<string, unknown> | undefined;\n if (stubs && stubs.dropped) out(` stubs: ${stubs.dropped} stubs dropped (server limit: ${stubs.limit})`);\n const variants = t.variants as Array<Record<string, unknown>> | undefined;\n if (Array.isArray(variants) && variants.length > 0) {\n const dropped = variants.reduce((n, v) => n + (typeof v.dropped === \"number\" ? v.dropped : 0), 0);\n out(\n ` variants: ${dropped} variant(s) dropped across ${variants.length} fingerprint(s) ` +\n `(server limit per fingerprint: ${(variants[0] as Record<string, unknown>).limit})`,\n );\n }\n const samples = t.samples as Record<string, unknown> | undefined;\n if (samples && samples.limit) out(` samples: clamped to the server limit of ${samples.limit} recordings per response`);\n const bb = t.body_bytes as Record<string, unknown> | undefined;\n if (bb) {\n const limitKb = Math.floor((typeof bb.limit === \"number\" ? bb.limit : 0) / 1024);\n out(\n ` body_bytes: ${bb.capped} variant body/bodies omitted (exceeded ${limitKb} KB cap) - ` +\n `these stubs will replay with an empty body`,\n );\n }\n }\n if (s.degraded.length > 0) {\n out(\n `WARNING: ${s.degraded.length} stub(s) marked degraded - ` +\n \"no recorded captures are available; replay will fail for those stubs.\",\n );\n }\n if (s.bodyCapped > 0) {\n out(\n `WARNING: ${s.bodyCapped} variant(s) have body_capped=true - ` +\n \"body exceeded the server size cap and was omitted; \" +\n \"replay will return an empty body for those variants.\",\n );\n }\n}\n\nasync function cmdPull(args: PullArgs): Promise<number> {\n const apiKey = process.env.STUBSMITH_API_KEY ?? \"\";\n if (!apiKey) {\n err(\"Error: STUBSMITH_API_KEY is not set. Export your project API key before running stubsmith pull.\");\n return 1;\n }\n\n for (const raw of args.endpoints) {\n if (!parseEndpoint(raw)) {\n err(`Error: --endpoint must be \"METHOD /path\" (e.g. \"GET /api/users\"), got: ${JSON.stringify(raw)}`);\n return 1;\n }\n }\n\n if (args.samples !== null) {\n const raw = args.samples.trim().toLowerCase();\n if (raw !== \"all\" && !/^\\d+$/.test(raw)) {\n err(`Error: --samples must be a positive integer or 'all', got: ${JSON.stringify(args.samples)}`);\n return 1;\n }\n if (raw !== \"all\" && Number(raw) < 1) {\n err(\"Error: --samples must be at least 1.\");\n return 1;\n }\n if ((raw === \"all\" || Number(raw) > 1) && args.endpoints.length === 0) {\n err(\n \"Error: --samples greater than 1 requires --endpoint.\\n\" +\n \"Fetching every recording for a whole project is not supported; \" +\n 'scope it, e.g. --endpoint \"GET /api/orders\". Repeat --endpoint ' +\n \"to cover several in one bundle.\",\n );\n return 1;\n }\n }\n\n let result;\n try {\n result = await pullBundle({\n apiKey,\n endpoints: args.endpoints,\n ...(args.samples !== null ? { samples: args.samples } : {}),\n onFetch: (method, path) => err(`Fetching ${method} ${path} ...`),\n });\n } catch (e) {\n err(`Error: ${e instanceof Error ? e.message : String(e)}`);\n return 1;\n }\n\n try {\n await writeBundle(args.out, result.bundle);\n } catch (e) {\n err(`Error writing bundle: ${e instanceof Error ? e.message : String(e)}`);\n return 1;\n }\n printSummary(result.bundle, args.out, result.summary);\n return 0;\n}\n\n/** CLI entry point; `argv` excludes the node binary and script path. Returns the exit code. */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n const first = argv[0];\n if (first === \"--version\" || first === \"-V\") {\n out(`stubsmith ${VERSION}`);\n return 0;\n }\n if (first === \"-h\" || first === \"--help\") {\n out(USAGE);\n return 0;\n }\n if (first === undefined) {\n err(USAGE);\n err(\"stubsmith: error: the following arguments are required: <command>\");\n return 1;\n }\n if (first !== \"pull\") {\n err(USAGE);\n err(`stubsmith: error: unrecognized command or argument: ${first}`);\n return 1;\n }\n const parsed = parsePull(argv.slice(1));\n if (parsed === \"help\") {\n out(USAGE);\n return 0;\n }\n if (typeof parsed === \"string\") {\n err(`stubsmith pull: error: ${parsed}`);\n return 1;\n }\n return cmdPull(parsed);\n}\n\nfunction isEntryPoint(): boolean {\n const argv1 = process.argv[1];\n if (!argv1) return false;\n try {\n return import.meta.url === pathToFileURL(realpathSync(argv1)).href;\n } catch {\n return false;\n }\n}\n\nif (isEntryPoint()) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (e: unknown) => {\n err(`Error: ${e instanceof Error ? e.message : String(e)}`);\n process.exitCode = 1;\n },\n );\n}\n","export const VERSION = \"0.2.1\";\n","/**\n * Fetch the replay bundle (`GET /v1/replay/bundle`): `fetchBundle` returns the\n * raw server bundle for one request (as Python `fetch_bundle`), `pullBundle`\n * merges, sorts and summarises several the way the Python `stubsmith pull`\n * does, and `writeBundle` serialises it identically.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\nimport { VERSION } from \"./_version.js\";\nimport { DEFAULT_BACKEND_URL, resolveApiUrl } from \"./env.js\";\nimport { compareCodePoints } from \"./internal/codepoint.js\";\nimport type { Bundle } from \"./replay/bundle.js\";\nimport { checkEndpoint } from \"./endpoint.js\";\nimport { sendRequest } from \"./transport.js\";\n\nconst TIMEOUT_MS = 30_000;\n/*\n * Bundle response cap. The backend bounds a bundle at 2000 stubs, 10 variants\n * per fingerprint and 100 KB per body, so a real one is far below this. The\n * peak memory for a response of N bytes is roughly 3N to 4N: the collected\n * chunks, the concatenated Buffer, the utf8 string and the parsed object tree\n * all coexist briefly. 128 MiB therefore bounds the process near 0.5 GiB.\n */\nconst MAX_BYTES = 128 * 1024 * 1024;\nconst MAX_MIB = MAX_BYTES / (1024 * 1024);\n\ntype Obj = Record<string, unknown>;\n\nexport interface FetchBundleOptions {\n /** Recordings per response; above one (or `\"all\"`) needs an endpoint. */\n samples?: number | \"all\" | string;\n /** Defaults to `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted service. */\n apiUrl?: string;\n /** Defaults to `STUBSMITH_API_KEY`. */\n apiKey?: string;\n}\n\nexport interface PullBundleOptions {\n /** Defaults to `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted service. */\n apiUrl?: string;\n /** Defaults to `STUBSMITH_API_KEY`. */\n apiKey?: string;\n /** `\"METHOD /path\"` filters, one request each; omitted means one unfiltered request. */\n endpoints?: string[];\n /** Recordings per response; above one (or `\"all\"`) needs `endpoints`. */\n samples?: number | \"all\" | string;\n /** Called before each filtered request (the CLI prints progress). */\n onFetch?: (method: string, path: string) => void;\n}\n\nexport interface BundleStubRef {\n domain: string;\n method: string;\n pathTemplate: string;\n fingerprint: string;\n}\n\nexport interface BundleSummary {\n endpoints: number;\n stubs: number;\n variants: number;\n /** Every stub the server marked degraded. */\n degraded: BundleStubRef[];\n /** Variants with `body_capped` set. */\n bodyCapped: number;\n}\n\nexport interface PullBundleResult {\n bundle: Bundle;\n summary: BundleSummary;\n}\n\nfunction isObj(v: unknown): v is Obj {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n\nfunction arr(v: unknown): Obj[] {\n return Array.isArray(v) ? v.filter(isObj) : [];\n}\n\n/** Python `x or \"\"` for a string-ish field. */\nfunction str(v: unknown): string {\n return typeof v === \"string\" ? v : \"\";\n}\n\n/** Split `\"METHOD /path\"` on the first space; null when malformed. */\nexport function parseEndpoint(raw: string): { method: string; path: string } | null {\n const i = raw.indexOf(\" \");\n if (i < 0) return null;\n const method = raw.slice(0, i).trim();\n const p = raw.slice(i + 1).trim();\n if (!method || !p) return null;\n return { method: method.toUpperCase(), path: p };\n}\n\n/** Validate a samples value; returns the normalised string or throws. `msg` builds the error from a reason. */\nexport function normaliseSamples(samples: number | string, endpointGiven: boolean): string {\n const raw = String(samples).trim().toLowerCase();\n if (raw !== \"all\" && !/^\\d+$/.test(raw)) {\n throw new Error(`samples must be a positive integer or 'all', got: ${JSON.stringify(samples)}`);\n }\n if (raw !== \"all\" && Number(raw) < 1) throw new Error(`samples must be at least 1, got: ${JSON.stringify(samples)}`);\n if ((raw === \"all\" || Number(raw) > 1) && !endpointGiven) {\n throw new Error(\n \"samples greater than 1 requires an endpoint. Fetching every recording for a whole project is not \" +\n 'supported; scope it, e.g. endpoints: [\"GET /api/orders\"].',\n );\n }\n return raw;\n}\n\nasync function fetchOne(\n apiUrl: string,\n apiKey: string,\n method: string | null,\n p: string | null,\n samples: string | null,\n): Promise<Bundle> {\n const params = new URLSearchParams();\n if (method) params.set(\"method\", method);\n if (p) params.set(\"path\", p);\n if (samples !== null) params.set(\"samples\", samples);\n checkEndpoint(apiUrl);\n const qs = [...params].length > 0 ? `?${params.toString()}` : \"\";\n const url = `${apiUrl.replace(/\\/+$/, \"\")}/v1/replay/bundle${qs}`;\n\n let res;\n try {\n res = await sendRequest(url, {\n method: \"GET\",\n headers: { authorization: `Bearer ${apiKey}`, \"user-agent\": `stubsmith-cli/${VERSION}` },\n timeoutMs: TIMEOUT_MS,\n holdProcess: true,\n maxBytes: MAX_BYTES,\n }).promise;\n } catch (err) {\n throw new Error(\n `Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}\\n` +\n `Check that the backend is reachable, or set STUBSMITH_API_URL to override (default: ${DEFAULT_BACKEND_URL}).`,\n );\n }\n if (res.truncated) throw new Error(`Bundle response exceeded ${MAX_MIB} MiB; narrow with --endpoint.`);\n const text = res.body.toString(\"utf8\");\n if (res.status === 404) {\n throw new Error(\n `HTTP 404 from ${url}\\n` +\n `The StubSmith backend was not found at that URL.\\n` +\n `For the hosted service the URL is ${DEFAULT_BACKEND_URL}.\\n` +\n `Override with STUBSMITH_API_URL, or unset it to use the default.`,\n );\n }\n if (res.status < 200 || res.status >= 300) throw new Error(`HTTP ${res.status}: ${text}`);\n if (!res.complete) throw new Error(`Connection closed before the response completed (${url}). Retry the pull.`);\n let data: unknown;\n try {\n data = JSON.parse(text);\n } catch (err) {\n throw new Error(`Server returned unparseable JSON: ${err instanceof Error ? err.message : String(err)}`);\n }\n if (!isObj(data)) throw new Error(\"Server returned an unexpected response (not a JSON object)\");\n if (!Array.isArray(data.endpoints)) throw new Error(\"Server returned an unexpected response (no endpoints array)\");\n return data as unknown as Bundle;\n}\n\n/** Python `int(x or 0)` for a cursor string; null when it does not parse. */\nfunction cursorValue(v: unknown): bigint | null {\n if (v === undefined || v === null || v === \"\" || v === 0 || v === false) return 0n;\n if (typeof v === \"number\") return Number.isInteger(v) ? BigInt(v) : BigInt(Math.trunc(v));\n if (typeof v === \"string\" && /^\\s*[+-]?\\d+(?:_\\d+)*\\s*$/.test(v)) return BigInt(v.trim().replace(/_/g, \"\"));\n return null;\n}\n\n/** Merge per-endpoint bundles as if the server had returned one (cli.py `_merge_bundles`). */\nexport function mergeBundles(typed: Bundle[]): Bundle {\n // The merge works on loose records: servers add fields, and generated_at may be null.\n const parts = typed as unknown as Obj[];\n if (parts.length === 0) return { ok: true, version: 1, cursor: \"0\", endpoints: [] };\n if (parts.length === 1) return typed[0] as Bundle;\n\n const merged = new Map<string, Obj & { stubs: Obj[] }>();\n const seen = new Map<string, Set<string>>();\n const truncated = new Map<string, unknown>();\n let cursor = 0n;\n\n for (const part of parts) {\n const c = cursorValue(part.cursor);\n if (c !== null && c > cursor) cursor = c;\n if (isObj(part.truncated)) {\n for (const [k, v] of Object.entries(part.truncated)) if (!truncated.has(k)) truncated.set(k, v);\n }\n for (const ep of arr(part.endpoints)) {\n // JSON array of the key parts: unambiguous whatever the strings contain.\n const key = JSON.stringify([str(ep.domain), str(ep.method).toUpperCase(), str(ep.path_template)]);\n let m = merged.get(key);\n if (!m) {\n m = { ...ep, stubs: [] };\n merged.set(key, m);\n seen.set(key, new Set());\n }\n const fps = seen.get(key) as Set<string>;\n for (const stub of arr(ep.stubs)) {\n const fp = str(stub.fingerprint);\n if (fps.has(fp)) continue;\n fps.add(fp);\n m.stubs.push(stub);\n }\n }\n }\n\n const out: Obj = {\n ok: true,\n version: (parts[0] as Obj).version ?? 1,\n cursor: cursor.toString(),\n generated_at: (parts[parts.length - 1] as Obj).generated_at ?? null,\n endpoints: [...merged.values()],\n };\n // The server sends the same project_settings in every part, except that a\n // part served while it could not read them has none or an empty one. Replay\n // reads them, so keep the last part that holds the setting.\n for (let i = parts.length - 1; i >= 0; i--) {\n const ps = (parts[i] as Obj).project_settings;\n if (isObj(ps) && typeof ps.group_ids_in_paths === \"boolean\") {\n out.project_settings = ps;\n break;\n }\n }\n if (truncated.size > 0) out.truncated = Object.fromEntries(truncated);\n return out as unknown as Bundle;\n}\n\n/** Python `v.get(\"status\") or 0` sort key. */\nfunction statusKey(v: Obj): number {\n return typeof v.status === \"number\" ? v.status : 0;\n}\n\n/** Copy of `data` with endpoints by (domain, method, path_template), stubs by fingerprint, variants by status. */\nexport function sortBundle(typed: Bundle): Bundle {\n const data = typed as unknown as Obj;\n const endpoints = [...arr(data.endpoints)].sort(\n (a, b) =>\n compareCodePoints(str(a.domain), str(b.domain)) ||\n compareCodePoints(str(a.method), str(b.method)) ||\n compareCodePoints(str(a.path_template), str(b.path_template)),\n );\n return {\n ...data,\n endpoints: endpoints.map((ep) => ({\n ...ep,\n stubs: [...arr(ep.stubs)]\n .sort((a, b) => compareCodePoints(str(a.fingerprint), str(b.fingerprint)))\n .map((stub) => ({ ...stub, variants: [...arr(stub.variants)].sort((a, b) => statusKey(a) - statusKey(b)) })),\n })),\n } as unknown as Bundle;\n}\n\nexport function summarizeBundle(typed: Bundle): BundleSummary {\n const data = typed as unknown as Obj;\n const eps = arr(data.endpoints);\n const summary: BundleSummary = { endpoints: eps.length, stubs: 0, variants: 0, degraded: [], bodyCapped: 0 };\n for (const ep of eps) {\n for (const stub of arr(ep.stubs)) {\n summary.stubs++;\n if (stub.degraded) {\n summary.degraded.push({\n domain: str(ep.domain),\n method: str(ep.method),\n pathTemplate: str(ep.path_template),\n fingerprint: str(stub.fingerprint),\n });\n }\n for (const v of arr(stub.variants)) {\n summary.variants++;\n if (v.body_capped) summary.bodyCapped++;\n }\n }\n }\n return summary;\n}\n\nfunction resolveAuth(opts: { apiKey?: string; apiUrl?: string }): { apiKey: string; apiUrl: string } {\n const apiKey = opts.apiKey || process.env.STUBSMITH_API_KEY || \"\";\n if (!apiKey) throw new Error(\"No API key: pass apiKey or set $STUBSMITH_API_KEY.\");\n return { apiKey, apiUrl: opts.apiUrl || resolveApiUrl() };\n}\n\n/**\n * Fetch the raw replay bundle for one request: the whole project, or the\n * `\"METHOD /path\"` endpoint. Returned as the server sent it, unmerged and\n * unsorted, so `replay(await fetchBundle())` works. Does not touch disk.\n */\nexport async function fetchBundle(endpoint?: string, opts: FetchBundleOptions = {}): Promise<Bundle> {\n const { apiKey, apiUrl } = resolveAuth(opts);\n let method: string | null = null;\n let p: string | null = null;\n if (endpoint !== undefined) {\n const ep = parseEndpoint(endpoint);\n if (!ep) throw new Error(`endpoint must be \"METHOD /path\" (e.g. \"GET /api/orders\"), got: ${JSON.stringify(endpoint)}`);\n method = ep.method;\n p = ep.path;\n }\n const samples = opts.samples === undefined ? null : normaliseSamples(opts.samples, endpoint !== undefined);\n return fetchOne(apiUrl, apiKey, method, p, samples);\n}\n\n/**\n * Fetch the replay bundle, one request per endpoint (or one unfiltered\n * request), merged and sorted. Does not touch disk.\n */\nexport async function pullBundle(opts: PullBundleOptions = {}): Promise<PullBundleResult> {\n const { apiKey, apiUrl } = resolveAuth(opts);\n\n const targets: Array<{ method: string | null; path: string | null }> = [];\n for (const raw of opts.endpoints ?? []) {\n const ep = parseEndpoint(raw);\n if (!ep) throw new Error(`endpoint must be \"METHOD /path\" (e.g. \"GET /api/orders\"), got: ${JSON.stringify(raw)}`);\n targets.push({ method: ep.method, path: ep.path });\n }\n const samples = opts.samples === undefined ? null : normaliseSamples(opts.samples, targets.length > 0);\n if (targets.length === 0) targets.push({ method: null, path: null });\n\n const fetched: Bundle[] = [];\n for (const t of targets) {\n if (t.method && t.path) opts.onFetch?.(t.method, t.path);\n fetched.push(await fetchOne(apiUrl, apiKey, t.method, t.path, samples));\n }\n const bundle = sortBundle(mergeBundles(fetched));\n return { bundle, summary: summarizeBundle(bundle) };\n}\n\nfunction compareKeys(a: string, b: string): number {\n return compareCodePoints(a, b);\n}\n\n/*\n * Numbers go through JSON.stringify, so a float would format differently from\n * Python (`12.0` vs `12`, `1e+16` vs `1e16`). The backend emits only integers\n * and strings today, so output matches; revisit if a float field is added.\n */\n/** JSON as Python `json.dumps(sort_keys=True, indent=2, ensure_ascii=False)`: keys sorted by code point, non-ASCII kept. */\nexport function stringifyBundle(value: unknown, indent = \"\"): string {\n if (Array.isArray(value)) {\n if (value.length === 0) return \"[]\";\n const inner = `${indent} `;\n return `[\\n${value.map((v) => inner + stringifyBundle(v === undefined ? null : v, inner)).join(\",\\n\")}\\n${indent}]`;\n }\n if (isObj(value)) {\n const keys = Object.keys(value)\n .filter((k) => value[k] !== undefined)\n .sort(compareKeys);\n if (keys.length === 0) return \"{}\";\n const inner = `${indent} `;\n return `{\\n${keys.map((k) => `${inner}${JSON.stringify(k)}: ${stringifyBundle(value[k], inner)}`).join(\",\\n\")}\\n${indent}}`;\n }\n return JSON.stringify(value) ?? \"null\";\n}\n\n/** Write atomically: temp file in the same directory, chmod 0644, rename into place. */\nexport async function writeBundle(outPath: string, data: Bundle): Promise<void> {\n const dest = path.resolve(outPath);\n const dir = path.dirname(dest);\n await fsp.mkdir(dir, { recursive: true });\n const serialised = `${stringifyBundle(data)}\\n`;\n const tmp = path.join(dir, `.stubsmith-bundle-${randomBytes(6).toString(\"hex\")}`);\n try {\n await fsp.writeFile(tmp, serialised, { encoding: \"utf8\", flag: \"wx\", mode: 0o600 });\n await fsp.chmod(tmp, 0o644);\n await fsp.rename(tmp, dest);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","/**\n * Environment configuration, debug logging and once-per-process warnings.\n * Names and defaults follow SPEC.md section 4 (Python `client.py`).\n */\n\nexport const DEFAULT_URL = \"https://ingest.stubsmith.dev/v1/captures\";\nexport const DEFAULT_BACKEND_URL = \"https://app.stubsmith.dev/api\";\n/** Exit-flush budget in seconds when `STUBSMITH_FLUSH_TIMEOUT` is unset or malformed. */\nexport const DEFAULT_FLUSH_TIMEOUT_S = 1.0;\n\nexport interface EnvConfig {\n url: string;\n apiKey: string;\n backendUrl: string;\n /** `STUBSMITH_API_URL`; empty when unset (the CLI falls back to the backend URL). */\n apiUrl: string;\n debug: boolean;\n /** Seconds, never negative. */\n flushTimeoutS: number;\n maskSalt: string;\n}\n\nfunction env(name: string): string | undefined {\n return process.env[name];\n}\n\n/** `1`, `true`, `yes` (case-insensitive, trimmed) are truthy; anything else is not. */\nexport function parseBool(raw: string | undefined): boolean {\n return [\"1\", \"true\", \"yes\"].includes((raw ?? \"\").trim().toLowerCase());\n}\n\n/** Python `float()` decimal grammar: digits with single underscores between them, optional fraction and exponent. */\nconst PY_FLOAT_RE = /^[+-]?(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[eE][+-]?\\d+(?:_\\d+)*)?$/;\n\n/**\n * Seconds from `raw`, accepting what Python `float()` accepts for decimal\n * numbers (`1_000`, `.5`, `1e2`; not `0x10` or `0b11`, which `Number()` would).\n * A malformed value is ignored (the SDK must never break the host). Python also\n * accepts `inf` and `nan`; those are rejected here (fallback 1.0) because an\n * unbounded exit wait would let a dead endpoint hang the application forever,\n * and a value that overflows to Infinity (`1e999`) is treated the same way.\n */\nexport function parseFlushTimeout(raw: string | undefined): number {\n const s = (raw ?? \"\").trim();\n if (s && PY_FLOAT_RE.test(s)) {\n const n = Number(s.replace(/_/g, \"\"));\n if (Number.isFinite(n)) return Math.max(0, n);\n }\n return DEFAULT_FLUSH_TIMEOUT_S;\n}\n\n/** Read every `STUBSMITH_*` variable with the SPEC defaults. */\nexport function readEnv(): EnvConfig {\n return {\n url: env(\"STUBSMITH_URL\") || DEFAULT_URL,\n apiKey: env(\"STUBSMITH_API_KEY\") ?? \"\",\n backendUrl: env(\"STUBSMITH_BACKEND_URL\") || DEFAULT_BACKEND_URL,\n apiUrl: env(\"STUBSMITH_API_URL\") ?? \"\",\n debug: parseBool(env(\"STUBSMITH_DEBUG\")),\n flushTimeoutS: parseFlushTimeout(env(\"STUBSMITH_FLUSH_TIMEOUT\")),\n maskSalt: env(\"STUBSMITH_MASK_SALT\") ?? \"\",\n };\n}\n\nlet debugOverride: boolean | undefined;\n\n/** Pin the debug flag (a client constructed with an explicit `debug`); `undefined` goes back to the env var. */\nexport function setDebug(on: boolean | undefined): void {\n debugOverride = on;\n}\n\nexport function isDebug(): boolean {\n return debugOverride ?? parseBool(env(\"STUBSMITH_DEBUG\"));\n}\n\n/** Print to stderr only when debug is on. Never throws. */\nexport function debugLog(msg: string): void {\n if (!isDebug()) return;\n try {\n process.stderr.write(`stubsmith: ${msg}\\n`);\n } catch {\n // stderr may be closed; logging must never break the host.\n }\n}\n\n/** Cap on distinct keys remembered by {@link warnOnce} (client.py `_INGEST_MAX_WARNED_CODES`). */\nexport const WARN_ONCE_CAP = 64;\n\nconst warned = new Set<string>();\n\n/**\n * `console.warn(msg)` once per process per `key`. Past {@link WARN_ONCE_CAP}\n * distinct keys new keys stay quiet, so a varying key cannot flood the logs.\n * Returns true when the message was printed.\n */\nexport function warnOnce(key: string, msg: string): boolean {\n if (warned.has(key) || warned.size >= WARN_ONCE_CAP) return false;\n warned.add(key);\n try {\n console.warn(msg);\n } catch {\n // never break the host\n }\n return true;\n}\n\n/** Test hook: forget every warned key. */\nexport function resetWarnOnceForTests(): void {\n warned.clear();\n}\n\n/** Backend base URL for `pull` / `fetchBundle`: `STUBSMITH_API_URL`, then `STUBSMITH_BACKEND_URL`, then the hosted default. */\nexport function resolveApiUrl(): string {\n return env(\"STUBSMITH_API_URL\") || env(\"STUBSMITH_BACKEND_URL\") || DEFAULT_BACKEND_URL;\n}\n\n/** Base URL for the fixtures API: `STUBSMITH_API_URL` only (SPEC 1.4), then the hosted default. */\nexport function resolveFixturesApiUrl(): string {\n return env(\"STUBSMITH_API_URL\") || DEFAULT_BACKEND_URL;\n}\n","/**\n * Compare two strings by Unicode code point, the order Python `sorted()` /\n * `str.__lt__` uses (CPython Objects/unicodeobject.c `unicode_compare`, reached\n * via `PyUnicode_RichCompare`; the file is not vendored here so no line is\n * cited). JS default comparison is by UTF-16 code unit, which places astral\n * characters (surrogates D800-DFFF) before U+E000-U+FFFF.\n *\n * Both strings are walked one code point at a time with `codePointAt`. A\n * valid surrogate pair is one astral code point; a lone surrogate is its own\n * code point (0xD800..0xDFFF), as in a Python str, and is compared\n * numerically against whatever the other string holds at that position.\n */\nexport function compareCodePoints(a: string, b: string): number {\n const la = a.length;\n const lb = b.length;\n let i = 0;\n let j = 0;\n while (i < la && j < lb) {\n const pa = a.codePointAt(i) as number;\n const pb = b.codePointAt(j) as number;\n if (pa !== pb) return pa < pb ? -1 : 1;\n i += pa > 0xffff ? 2 : 1;\n j += pb > 0xffff ? 2 : 1;\n }\n if (i < la) return 1;\n if (j < lb) return -1;\n return 0;\n}\n\n/**\n * Return a new array sorted by Unicode code point (Python `sorted(list)`).\n * The sort is stable. Does not mutate the input.\n */\nexport function sortCodePoints(arr: readonly string[]): string[] {\n return [...arr].sort(compareCodePoints);\n}\n","/**\n * The SDK's own traffic (captures, rules sync, bundle and fixture fetches)\n * goes over https only. Plain http is accepted for a backend on this machine\n * (`localhost`, `127.0.0.1`, `::1`) so local development keeps working.\n * Port of stubsmith/_endpoint.py.\n */\n\nconst LOCAL_HOSTS: ReadonlySet<string> = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\"]);\n\n/** Thrown for a Stubsmith URL the SDK will not send to. */\nexport class InsecureEndpointError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"InsecureEndpointError\";\n }\n}\n\n/**\n * Throw {@link InsecureEndpointError} unless `url` is https or local http.\n * The message names only the scheme and host, so credentials in the URL are\n * never echoed into logs.\n */\nexport function checkEndpoint(url: string): void {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n throw new InsecureEndpointError(\n \"stubsmith: the Stubsmith URL is not a valid URL; the SDK sends only to an https URL.\",\n );\n }\n if (u.protocol === \"https:\" && u.hostname !== \"\") return;\n if (u.protocol === \"http:\" && LOCAL_HOSTS.has(u.hostname)) return;\n const shown = u.hostname ? `${u.protocol}//${u.hostname}` : u.protocol;\n throw new InsecureEndpointError(\n `stubsmith: refusing to send to ${shown}. The SDK sends only over https; ` +\n \"plain http is allowed only for localhost, 127.0.0.1 and ::1.\",\n );\n}\n","/**\n * Process-global shared state. Everything lives on `globalThis` under\n * `Symbol.for(...)` keys so duplicate copies of the package (ESM and CJS both\n * loaded, or two versions) see the same replay depth, capture guard, install\n * singleton and original `http.request`.\n */\nimport { AsyncLocalStorage } from \"node:async_hooks\";\nimport http from \"node:http\";\nimport https from \"node:https\";\n\nconst DEPTH_KEY = Symbol.for(\"stubsmith.replayDepth\");\nconst GUARD_KEY = Symbol.for(\"stubsmith.captureGuard\");\nconst REQUEST_KEY = Symbol.for(\"stubsmith.originalRequest\");\n\ntype Shared = Record<symbol, unknown>;\nconst g = globalThis as unknown as Shared;\n\n/** Shared slot with a lazily created value. */\nexport function sharedSlot<T>(key: symbol, init: () => T): T {\n if (g[key] === undefined) g[key] = init();\n return g[key] as T;\n}\n\ninterface Depth {\n n: number;\n}\nconst depth = sharedSlot<Depth>(DEPTH_KEY, () => ({ n: 0 }));\n\n/** True while at least one replay context is active (capture is suppressed). */\nexport function replayActive(): boolean {\n return depth.n > 0;\n}\n\nexport function enterReplay(): void {\n depth.n += 1;\n}\n\nexport function exitReplay(): void {\n if (depth.n > 0) depth.n -= 1;\n}\n\n/**\n * Outermost-frame guard. A hook that finds a store set does not capture; the\n * fetch hook and every SDK-internal request run inside `captureGuard.run(true, ...)`.\n */\nexport const captureGuard = sharedSlot<AsyncLocalStorage<true>>(GUARD_KEY, () => new AsyncLocalStorage<true>());\n\n/**\n * The `http.request` / `https.request` that existed when the first copy of\n * the SDK loaded. The sender and the rules poller use these (not `fetch`), so\n * their sockets can be unref'd and they bypass the capture hooks.\n */\nexport const ORIGINAL_REQUEST = sharedSlot<{ http: typeof http.request; https: typeof https.request }>(REQUEST_KEY, () => ({\n http: http.request,\n https: https.request,\n}));\n","/**\n * Minimal HTTP client for the SDK's own traffic (ingest POSTs and rules sync).\n *\n * It uses the ORIGINAL `http.request` / `https.request` captured at module\n * load, not `fetch`: fetch sockets are ref'd, so a stalled endpoint would hold\n * the process open at exit. Here the socket is unref'd and the timeouts are\n * explicit, so nothing of the SDK's keeps a finished application alive. The\n * request is created inside `captureGuard.run`, so the capture hooks skip it.\n *\n * Redirects are never followed: `http.request` does not follow them, and the\n * 3xx comes back to the caller as the status. Following one would re-send the\n * `authorization` header to wherever it points, plaintext or a foreign host.\n *\n * Callers whose request IS the work (the `pull` CLI, `fixtures()`) set\n * `holdProcess`: the socket and the deadline timer stay ref'd so the process\n * lives until the response arrives.\n */\nimport type { ClientRequest, IncomingHttpHeaders } from \"node:http\";\nimport { checkEndpoint } from \"./endpoint.js\";\nimport { ORIGINAL_REQUEST, captureGuard } from \"./replayState.js\";\n\nexport interface TransportOptions {\n method: string;\n /** Sent as given (lowercase names keep the wire form of the old fetch-based sender). */\n headers: Record<string, string>;\n body?: Buffer;\n /**\n * Overall deadline for the exchange, also applied as the socket idle timeout.\n * With `holdProcess` it is only the idle timeout (no bytes for this long);\n * the overall deadline is then {@link HOLD_TOTAL_MS} unless `totalTimeoutMs` says otherwise.\n */\n timeoutMs: number;\n /**\n * Keep the process alive until the exchange finishes (no `unref()` on the\n * socket or the deadline timer). Default false: the SDK's background sender\n * and poller must never delay exit. Only standalone callers (CLI, `fixtures`,\n * `fetchBundle`) set it.\n */\n holdProcess?: boolean;\n /** Overall deadline override; defaults to `timeoutMs`, or {@link HOLD_TOTAL_MS} with `holdProcess`. */\n totalTimeoutMs?: number;\n /** Response bytes kept; the rest of the body is dropped and the connection destroyed. */\n maxBytes: number;\n}\n\n/**\n * Total deadline for `holdProcess` callers. A large bundle on a slow link can\n * legitimately take longer than the 30 s idle timeout; Python's per-operation\n * timeout does not cap the whole transfer either.\n */\nexport const HOLD_TOTAL_MS = 120_000;\n\nexport interface TransportResult {\n status: number;\n headers: IncomingHttpHeaders;\n body: Buffer;\n /** The body was cut at `maxBytes`; `body` is a prefix. */\n truncated: boolean;\n /** The response ended normally. False when the connection closed early. */\n complete: boolean;\n}\n\nexport interface Transport {\n /** Rejects on a network error, a timeout or {@link Transport.abort}. */\n promise: Promise<TransportResult>;\n /** Destroy the connection; `promise` rejects. */\n abort(): void;\n}\n\nexport function sendRequest(url: string, opts: TransportOptions): Transport {\n let req: ClientRequest | null = null;\n let aborted = false;\n const abort = (): void => {\n aborted = true;\n req?.destroy(new Error(\"aborted\"));\n };\n\n const promise = new Promise<TransportResult>((resolve, reject) => {\n let settled = false;\n let total: NodeJS.Timeout | null = null;\n const fail = (err: unknown): void => {\n if (settled) return;\n settled = true;\n if (total) clearTimeout(total);\n reject(err instanceof Error ? err : new Error(String(err)));\n };\n const succeed = (r: TransportResult): void => {\n if (settled) return;\n settled = true;\n if (total) clearTimeout(total);\n resolve(r);\n };\n\n let u: URL;\n try {\n u = new URL(url);\n } catch (err) {\n fail(err);\n return;\n }\n try {\n checkEndpoint(url);\n } catch (err) {\n fail(err);\n return;\n }\n const request = u.protocol === \"https:\" ? ORIGINAL_REQUEST.https : u.protocol === \"http:\" ? ORIGINAL_REQUEST.http : null;\n if (request === null) {\n fail(new Error(`unsupported protocol ${u.protocol}`));\n return;\n }\n const headers = { ...opts.headers };\n if (opts.body !== undefined) headers[\"content-length\"] = String(opts.body.length);\n\n try {\n captureGuard.run(true, () => {\n req = request(u, { method: opts.method, headers }, (res) => {\n const chunks: Buffer[] = [];\n let got = 0;\n let truncated = false;\n const finish = (): void => {\n if (aborted) fail(new Error(\"aborted\"));\n else {\n succeed({\n status: res.statusCode ?? 0,\n headers: res.headers,\n body: Buffer.concat(chunks).subarray(0, opts.maxBytes),\n truncated,\n complete: !truncated && res.complete,\n });\n }\n };\n res.on(\"data\", (c: Buffer) => {\n if (got < opts.maxBytes) chunks.push(c);\n got += c.length;\n if (got > opts.maxBytes) {\n truncated = true;\n finish();\n res.destroy();\n }\n });\n res.on(\"end\", finish);\n // A body cut short still reports the status and what was read; `complete` says it was cut.\n res.on(\"error\", finish);\n res.on(\"close\", finish);\n });\n });\n } catch (err) {\n fail(err);\n return;\n }\n const r = req as unknown as ClientRequest;\n const hold = opts.holdProcess === true;\n const totalMs = opts.totalTimeoutMs ?? (hold ? HOLD_TOTAL_MS : opts.timeoutMs);\n total = setTimeout(() => r.destroy(new Error(\"timeout\")), totalMs);\n if (!hold) {\n total.unref();\n r.on(\"socket\", (s) => s.unref());\n }\n r.setTimeout(opts.timeoutMs, () => r.destroy(new Error(\"timeout\")));\n r.on(\"error\", fail);\n r.end(opts.body);\n if (aborted) r.destroy(new Error(\"aborted\"));\n });\n return { promise, abort };\n}\n"],"mappings":"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","names":["err","r","err","out","err","path"]}
|
package/dist/index.cjs
CHANGED
|
@@ -54,7 +54,7 @@ __export(index_exports, {
|
|
|
54
54
|
module.exports = __toCommonJS(index_exports);
|
|
55
55
|
|
|
56
56
|
// src/_version.ts
|
|
57
|
-
var VERSION = "0.2.
|
|
57
|
+
var VERSION = "0.2.1";
|
|
58
58
|
|
|
59
59
|
// src/instrument.ts
|
|
60
60
|
var import_node_http2 = __toESM(require("http"), 1);
|
|
@@ -141,6 +141,31 @@ function truncateUtf8Bytes(s, n) {
|
|
|
141
141
|
// src/client.ts
|
|
142
142
|
var import_node_zlib = require("zlib");
|
|
143
143
|
|
|
144
|
+
// src/endpoint.ts
|
|
145
|
+
var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
146
|
+
var InsecureEndpointError = class extends Error {
|
|
147
|
+
constructor(message) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.name = "InsecureEndpointError";
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
function checkEndpoint(url) {
|
|
153
|
+
let u;
|
|
154
|
+
try {
|
|
155
|
+
u = new URL(url);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new InsecureEndpointError(
|
|
158
|
+
"stubsmith: the Stubsmith URL is not a valid URL; the SDK sends only to an https URL."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (u.protocol === "https:" && u.hostname !== "") return;
|
|
162
|
+
if (u.protocol === "http:" && LOCAL_HOSTS.has(u.hostname)) return;
|
|
163
|
+
const shown = u.hostname ? `${u.protocol}//${u.hostname}` : u.protocol;
|
|
164
|
+
throw new InsecureEndpointError(
|
|
165
|
+
`stubsmith: refusing to send to ${shown}. The SDK sends only over https; plain http is allowed only for localhost, 127.0.0.1 and ::1.`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
144
169
|
// src/internal/codepoint.ts
|
|
145
170
|
function compareCodePoints(a, b) {
|
|
146
171
|
const la = a.length;
|
|
@@ -2676,6 +2701,12 @@ function sendRequest(url, opts) {
|
|
|
2676
2701
|
fail(err);
|
|
2677
2702
|
return;
|
|
2678
2703
|
}
|
|
2704
|
+
try {
|
|
2705
|
+
checkEndpoint(url);
|
|
2706
|
+
} catch (err) {
|
|
2707
|
+
fail(err);
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2679
2710
|
const request = u.protocol === "https:" ? ORIGINAL_REQUEST.https : u.protocol === "http:" ? ORIGINAL_REQUEST.http : null;
|
|
2680
2711
|
if (request === null) {
|
|
2681
2712
|
fail(new Error(`unsupported protocol ${u.protocol}`));
|
|
@@ -2943,9 +2974,22 @@ var StubsmithClient = class _StubsmithClient {
|
|
|
2943
2974
|
counters.set(this, this.counts);
|
|
2944
2975
|
const env2 = readEnv();
|
|
2945
2976
|
this.apiKey = options.apiKey || env2.apiKey;
|
|
2946
|
-
this.enabled = Boolean(this.apiKey);
|
|
2947
2977
|
this.url = options.url || env2.url || DEFAULT_URL;
|
|
2948
2978
|
this.backendUrl = options.backendUrl || env2.backendUrl || DEFAULT_BACKEND_URL;
|
|
2979
|
+
let secure = true;
|
|
2980
|
+
if (this.apiKey) {
|
|
2981
|
+
try {
|
|
2982
|
+
checkEndpoint(this.url);
|
|
2983
|
+
checkEndpoint(this.backendUrl);
|
|
2984
|
+
} catch (err) {
|
|
2985
|
+
secure = false;
|
|
2986
|
+
try {
|
|
2987
|
+
console.warn(`${err instanceof Error ? err.message : String(err)} Capture is off.`);
|
|
2988
|
+
} catch {
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
this.enabled = Boolean(this.apiKey) && secure;
|
|
2949
2993
|
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2950
2994
|
const sr = options.sampleRate ?? 1;
|
|
2951
2995
|
this.sampleRate = Number.isNaN(sr) ? 1 : Math.max(0, Math.min(1, sr));
|
|
@@ -3191,7 +3235,7 @@ var StubsmithClient = class _StubsmithClient {
|
|
|
3191
3235
|
if (this.inflight === t) this.inflight = null;
|
|
3192
3236
|
}
|
|
3193
3237
|
reportIngestRefusal(res.body, res.status, this.debug);
|
|
3194
|
-
if (res.status >=
|
|
3238
|
+
if (res.status >= 300) {
|
|
3195
3239
|
this.sendFailures++;
|
|
3196
3240
|
debugLog(`send failed - status=${res.status} url=${this.url}`);
|
|
3197
3241
|
}
|
|
@@ -3909,6 +3953,7 @@ async function fixturesBundle(spec, opts = {}) {
|
|
|
3909
3953
|
const params = new URLSearchParams({ method: method.toUpperCase(), path: path3 });
|
|
3910
3954
|
if (opts.distinct !== void 0) params.set("distinct", opts.distinct);
|
|
3911
3955
|
if (opts.limit !== void 0) params.set("limit", String(opts.limit));
|
|
3956
|
+
checkEndpoint(base);
|
|
3912
3957
|
const url = `${base.replace(/\/+$/, "")}/v1/fixtures?${params.toString()}`;
|
|
3913
3958
|
let res;
|
|
3914
3959
|
try {
|
|
@@ -3983,6 +4028,7 @@ async function fetchOne(apiUrl, apiKey, method, p, samples) {
|
|
|
3983
4028
|
if (method) params.set("method", method);
|
|
3984
4029
|
if (p) params.set("path", p);
|
|
3985
4030
|
if (samples !== null) params.set("samples", samples);
|
|
4031
|
+
checkEndpoint(apiUrl);
|
|
3986
4032
|
const qs = [...params].length > 0 ? `?${params.toString()}` : "";
|
|
3987
4033
|
const url = `${apiUrl.replace(/\/+$/, "")}/v1/replay/bundle${qs}`;
|
|
3988
4034
|
let res;
|