manifest 7.2.1 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +1 -1
- package/README.md +19 -2
- package/dist/bin.cjs +65 -25
- package/dist/bin.js +62 -20
- package/dist/{chunk-RO6CIYR2.js → chunk-6RNG7PBO.js} +105 -6
- package/dist/{chunk-OXOZQLZ3.js → chunk-TX6KHYCJ.js} +1 -1
- package/dist/index.cjs +105 -6
- package/dist/index.d.cts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +2 -2
- package/dist/register.cjs +105 -6
- package/dist/register.js +2 -2
- package/docs/guide.md +8 -3
- package/package.json +3 -2
package/CONTRACT.md
CHANGED
|
@@ -63,7 +63,7 @@ Reports are best effort, bounded, and observable through Node warnings with code
|
|
|
63
63
|
|
|
64
64
|
## Tracked requests
|
|
65
65
|
|
|
66
|
-
Every call the SDK sees and does not send to `POST /v1/heal`, whatever its status (2xx, 3xx, 401, 402, 403, 429, 5xx, and a 4xx that is not sent for healing: after a redirect, while the project is disabled, or while all eight heal slots are busy), is recorded and sent in batches to `POST /v1/requests
|
|
66
|
+
Every call the SDK sees and does not send to `POST /v1/heal`, whatever its status (2xx, 3xx, 401, 402, 403, 429, 5xx, and a 4xx that is not sent for healing: after a redirect, while the project is disabled, or while all eight heal slots are busy), is recorded and sent in batches to `POST /v1/requests`. So is every call made through the `undici` package directly or through a `fetch` reference saved before `manifest()` ran: the SDK observes those on undici's diagnostics channels, so they are tracked, never healed. The SDK's own calls to Manifest are never tracked.
|
|
67
67
|
|
|
68
68
|
```json
|
|
69
69
|
{"requests":[{"traceId":"a-uuid","method":"POST","url":"https://example.com/orders","statusCode":200,"responseTimeMs":84,"occurredAt":"2026-09-23T10:14:07.512Z"}]}
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ The prompt adds the one-line install to your entry file and stops to let you pas
|
|
|
65
65
|
|
|
66
66
|
TypeScript, ESM and CommonJS. Zero dependencies.
|
|
67
67
|
|
|
68
|
-
4. Set your key in the environment of your app
|
|
68
|
+
4. Set your key in the environment of your app, or in the project's `.env`:
|
|
69
69
|
|
|
70
70
|
```sh
|
|
71
71
|
export MNFST_KEY='your-project-key'
|
|
@@ -77,7 +77,7 @@ The prompt adds the one-line install to your entry file and stops to let you pas
|
|
|
77
77
|
npx manifest doctor
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
It resolves the installed SDK version, masks and validates the key, checks that Manifest loads before your app, and prints the runtime coverage.
|
|
80
|
+
It reads the key from the shell, then from the project's `.env.local` or `.env`. It resolves the installed SDK version, masks and validates the key, checks that Manifest loads before your app, and prints the runtime coverage.
|
|
81
81
|
|
|
82
82
|
Some clients keep the `fetch` they saw when they were built, and a client built at import time runs before your call. Where that happens, or where the start command is not yours to change, preload the SDK instead:
|
|
83
83
|
|
|
@@ -133,6 +133,23 @@ console.log(res.status);
|
|
|
133
133
|
|
|
134
134
|
The failed request appears in your [Manifest dashboard](https://dashboard.manifest.build), grouped with others like it in an issue. Once Manifest has a patch for that error, the next request that fails the same way is repaired and retried: `onHeal` reports `patched` or `unverified` with the retry's status code, and your app receives the answer to the retry.
|
|
135
135
|
|
|
136
|
+
## Choosing which calls reach Manifest
|
|
137
|
+
|
|
138
|
+
Keep calls out of Manifest entirely: they are neither repaired nor tracked, and nothing about them leaves your app. Each entry is a domain or a domain with a path:
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
MNFST_ALLOWLIST=stripe.com # only Stripe
|
|
142
|
+
MNFST_ALLOWLIST=stripe.com/v1/payment_intents # only this Stripe endpoint
|
|
143
|
+
MNFST_DENYLIST=stripe.com/v1/charges,internal.example.com # never these
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- A domain covers its subdomains, with or without a path: `stripe.com` and `stripe.com/v1/charges` both match `api.stripe.com`.
|
|
147
|
+
- A path matches whole segments: `/v1/charges` covers `/v1/charges/ch_123`, not `/v1/charges_export`. Paths are case-sensitive.
|
|
148
|
+
- A scheme, port, query or fragment in an entry is ignored. `*` in a path is not supported yet: the entry is skipped with a warning, and an allowlist made only of skipped entries lets nothing through.
|
|
149
|
+
- The denylist wins over the allowlist. With no allowlist, every call is eligible.
|
|
150
|
+
|
|
151
|
+
Or in code: `manifest({ denylist: ['stripe.com/v1/charges'] })`. An option overrides its environment variable. When Manifest starts from a preload (`node -r manifest/register`), set the environment variables: a later `manifest()` call cannot change the configuration.
|
|
152
|
+
|
|
136
153
|
## More
|
|
137
154
|
|
|
138
155
|
[Documentation](https://docs.manifest.build) · [Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python) · [PHP SDK](https://github.com/mnfst/manifest-php) · [Website](https://manifest.build)
|
package/dist/bin.cjs
CHANGED
|
@@ -25,15 +25,51 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
27
|
var import_node_module = require("module");
|
|
28
|
-
var
|
|
28
|
+
var import_node_fs2 = require("fs");
|
|
29
29
|
var import_promises = require("fs/promises");
|
|
30
|
-
var
|
|
30
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
31
31
|
|
|
32
32
|
// src/wire.ts
|
|
33
|
-
var isObject = (
|
|
33
|
+
var isObject = (value2) => value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
34
34
|
|
|
35
35
|
// src/api.ts
|
|
36
|
-
var VERSION = "7.
|
|
36
|
+
var VERSION = "7.4.0";
|
|
37
|
+
|
|
38
|
+
// src/dotenv.ts
|
|
39
|
+
var import_node_fs = require("fs");
|
|
40
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
41
|
+
var FILES = [".env.local", ".env"];
|
|
42
|
+
var LINE = /^\s*(?:export\s+)?(MNFST_KEY|MNFST_URL)\s*=(.*)$/;
|
|
43
|
+
function value(raw) {
|
|
44
|
+
const trimmed = raw.trim();
|
|
45
|
+
const quote = trimmed[0];
|
|
46
|
+
if (quote === '"' || quote === "'") {
|
|
47
|
+
const end = trimmed.indexOf(quote, 1);
|
|
48
|
+
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end);
|
|
49
|
+
}
|
|
50
|
+
const hash = trimmed.indexOf(" #");
|
|
51
|
+
return (hash === -1 ? trimmed : trimmed.slice(0, hash)).trim();
|
|
52
|
+
}
|
|
53
|
+
function readDotEnv(cwd) {
|
|
54
|
+
const found = {};
|
|
55
|
+
for (const file of FILES) {
|
|
56
|
+
let contents;
|
|
57
|
+
try {
|
|
58
|
+
contents = (0, import_node_fs.readFileSync)(import_node_path.default.join(cwd, file), "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
63
|
+
const match = LINE.exec(line);
|
|
64
|
+
if (!match) continue;
|
|
65
|
+
const name = match[1];
|
|
66
|
+
if (found[name] !== void 0) continue;
|
|
67
|
+
const parsed = value(match[2] ?? "");
|
|
68
|
+
if (parsed) found[name] = parsed;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
37
73
|
|
|
38
74
|
// src/cli.ts
|
|
39
75
|
var DEFAULT_URL = "https://api.manifest.build";
|
|
@@ -139,17 +175,17 @@ async function probe(url, key, doFetch) {
|
|
|
139
175
|
}
|
|
140
176
|
function installedVersion(cwd) {
|
|
141
177
|
try {
|
|
142
|
-
const entry = (0, import_node_module.createRequire)(
|
|
178
|
+
const entry = (0, import_node_module.createRequire)(import_node_path2.default.join(cwd, "package.json")).resolve(
|
|
143
179
|
"manifest"
|
|
144
180
|
);
|
|
145
|
-
let dir =
|
|
181
|
+
let dir = import_node_path2.default.dirname(entry);
|
|
146
182
|
while (true) {
|
|
147
|
-
const manifest =
|
|
148
|
-
if ((0,
|
|
149
|
-
const pkg = JSON.parse((0,
|
|
183
|
+
const manifest = import_node_path2.default.join(dir, "package.json");
|
|
184
|
+
if ((0, import_node_fs2.existsSync)(manifest)) {
|
|
185
|
+
const pkg = JSON.parse((0, import_node_fs2.readFileSync)(manifest, "utf8"));
|
|
150
186
|
if (pkg.name === "manifest" && pkg.version) return pkg.version;
|
|
151
187
|
}
|
|
152
|
-
const parent =
|
|
188
|
+
const parent = import_node_path2.default.dirname(dir);
|
|
153
189
|
if (parent === dir) return void 0;
|
|
154
190
|
dir = parent;
|
|
155
191
|
}
|
|
@@ -158,10 +194,10 @@ function installedVersion(cwd) {
|
|
|
158
194
|
}
|
|
159
195
|
}
|
|
160
196
|
async function nearestPackage(cwd) {
|
|
161
|
-
let dir =
|
|
197
|
+
let dir = import_node_path2.default.resolve(cwd);
|
|
162
198
|
while (true) {
|
|
163
|
-
const file =
|
|
164
|
-
if ((0,
|
|
199
|
+
const file = import_node_path2.default.join(dir, "package.json");
|
|
200
|
+
if ((0, import_node_fs2.existsSync)(file)) {
|
|
165
201
|
try {
|
|
166
202
|
return {
|
|
167
203
|
dir,
|
|
@@ -171,7 +207,7 @@ async function nearestPackage(cwd) {
|
|
|
171
207
|
return void 0;
|
|
172
208
|
}
|
|
173
209
|
}
|
|
174
|
-
const parent =
|
|
210
|
+
const parent = import_node_path2.default.dirname(dir);
|
|
175
211
|
if (parent === dir) return void 0;
|
|
176
212
|
dir = parent;
|
|
177
213
|
}
|
|
@@ -195,9 +231,9 @@ async function loadCheck(cwd) {
|
|
|
195
231
|
};
|
|
196
232
|
const scripts = isObject(found.pkg.scripts) ? found.pkg.scripts : {};
|
|
197
233
|
const values = Object.values(scripts).filter(
|
|
198
|
-
(
|
|
234
|
+
(value2) => typeof value2 === "string"
|
|
199
235
|
);
|
|
200
|
-
if (values.some((
|
|
236
|
+
if (values.some((value2) => value2.includes("manifest/register")))
|
|
201
237
|
return {
|
|
202
238
|
label,
|
|
203
239
|
status: "ok",
|
|
@@ -209,8 +245,8 @@ async function loadCheck(cwd) {
|
|
|
209
245
|
};
|
|
210
246
|
if ("next" in dependencies) {
|
|
211
247
|
for (const relative of instrumentationFiles) {
|
|
212
|
-
const file =
|
|
213
|
-
if (!(0,
|
|
248
|
+
const file = import_node_path2.default.join(found.dir, relative);
|
|
249
|
+
if (!(0, import_node_fs2.existsSync)(file)) continue;
|
|
214
250
|
if (/manifest/.test(await (0, import_promises.readFile)(file, "utf8")))
|
|
215
251
|
return {
|
|
216
252
|
label,
|
|
@@ -248,19 +284,23 @@ function sdkCheck(cwd, injected) {
|
|
|
248
284
|
};
|
|
249
285
|
return { label, status: "ok", detail: `manifest ${version}` };
|
|
250
286
|
}
|
|
287
|
+
function nonBlank(value2) {
|
|
288
|
+
return value2 && value2.trim() !== "" ? value2 : void 0;
|
|
289
|
+
}
|
|
251
290
|
async function runDoctor(options = {}) {
|
|
252
291
|
const cwd = options.cwd ?? process.cwd();
|
|
253
292
|
const env = options.env ?? process.env;
|
|
254
293
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
255
294
|
const checks = [sdkCheck(cwd, options.sdkVersion)];
|
|
256
|
-
const
|
|
295
|
+
const dotenv = readDotEnv(cwd);
|
|
296
|
+
const rawUrl = options.url ?? nonBlank(env.MNFST_URL) ?? dotenv.MNFST_URL ?? DEFAULT_URL;
|
|
257
297
|
const url = normalizeBase(rawUrl);
|
|
258
|
-
const key = env.MNFST_KEY;
|
|
298
|
+
const key = nonBlank(env.MNFST_KEY) ?? dotenv.MNFST_KEY;
|
|
259
299
|
if (!key) {
|
|
260
300
|
checks.push({
|
|
261
301
|
label: "MNFST_KEY set",
|
|
262
302
|
status: "fail",
|
|
263
|
-
detail: "MNFST_KEY is not set"
|
|
303
|
+
detail: "MNFST_KEY is not set in the environment or the project's .env"
|
|
264
304
|
});
|
|
265
305
|
} else {
|
|
266
306
|
checks.push({ label: "MNFST_KEY set", status: "ok", detail: maskKey(key) });
|
|
@@ -332,7 +372,7 @@ function render(report) {
|
|
|
332
372
|
lines.push("");
|
|
333
373
|
lines.push("Runtime coverage");
|
|
334
374
|
lines.push(
|
|
335
|
-
" Node.js runtime fetch, http.request, https.request, http.get are patched"
|
|
375
|
+
" Node.js runtime fetch, http.request, https.request, http.get are patched; undici is tracked"
|
|
336
376
|
);
|
|
337
377
|
lines.push(
|
|
338
378
|
" Edge runtime not supported \u2014 middleware.ts and Edge route handlers are never covered"
|
|
@@ -347,9 +387,9 @@ function parseArgs(argv) {
|
|
|
347
387
|
if (arg === "-h" || arg === "--help") args.help = true;
|
|
348
388
|
else if (arg === "-v" || arg === "--version") args.version = true;
|
|
349
389
|
else if (arg === "--url") {
|
|
350
|
-
const
|
|
351
|
-
if (!
|
|
352
|
-
args.url =
|
|
390
|
+
const value2 = argv[++index];
|
|
391
|
+
if (!value2) throw new Error("--url needs a value");
|
|
392
|
+
args.url = value2;
|
|
353
393
|
} else if (arg.startsWith("--url=")) args.url = arg.slice("--url=".length);
|
|
354
394
|
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
355
395
|
else if (args.command === void 0) args.command = arg;
|
package/dist/bin.js
CHANGED
|
@@ -2,13 +2,51 @@
|
|
|
2
2
|
import {
|
|
3
3
|
VERSION,
|
|
4
4
|
isObject
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-TX6KHYCJ.js";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
8
|
import { createRequire } from "module";
|
|
9
|
-
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
10
10
|
import { readFile } from "fs/promises";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
|
|
13
|
+
// src/dotenv.ts
|
|
14
|
+
import { readFileSync } from "fs";
|
|
11
15
|
import path from "path";
|
|
16
|
+
var FILES = [".env.local", ".env"];
|
|
17
|
+
var LINE = /^\s*(?:export\s+)?(MNFST_KEY|MNFST_URL)\s*=(.*)$/;
|
|
18
|
+
function value(raw) {
|
|
19
|
+
const trimmed = raw.trim();
|
|
20
|
+
const quote = trimmed[0];
|
|
21
|
+
if (quote === '"' || quote === "'") {
|
|
22
|
+
const end = trimmed.indexOf(quote, 1);
|
|
23
|
+
return end === -1 ? trimmed.slice(1) : trimmed.slice(1, end);
|
|
24
|
+
}
|
|
25
|
+
const hash = trimmed.indexOf(" #");
|
|
26
|
+
return (hash === -1 ? trimmed : trimmed.slice(0, hash)).trim();
|
|
27
|
+
}
|
|
28
|
+
function readDotEnv(cwd) {
|
|
29
|
+
const found = {};
|
|
30
|
+
for (const file of FILES) {
|
|
31
|
+
let contents;
|
|
32
|
+
try {
|
|
33
|
+
contents = readFileSync(path.join(cwd, file), "utf8");
|
|
34
|
+
} catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
38
|
+
const match = LINE.exec(line);
|
|
39
|
+
if (!match) continue;
|
|
40
|
+
const name = match[1];
|
|
41
|
+
if (found[name] !== void 0) continue;
|
|
42
|
+
const parsed = value(match[2] ?? "");
|
|
43
|
+
if (parsed) found[name] = parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/cli.ts
|
|
12
50
|
var DEFAULT_URL = "https://api.manifest.build";
|
|
13
51
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
14
52
|
var symbols = {
|
|
@@ -112,17 +150,17 @@ async function probe(url, key, doFetch) {
|
|
|
112
150
|
}
|
|
113
151
|
function installedVersion(cwd) {
|
|
114
152
|
try {
|
|
115
|
-
const entry = createRequire(
|
|
153
|
+
const entry = createRequire(path2.join(cwd, "package.json")).resolve(
|
|
116
154
|
"manifest"
|
|
117
155
|
);
|
|
118
|
-
let dir =
|
|
156
|
+
let dir = path2.dirname(entry);
|
|
119
157
|
while (true) {
|
|
120
|
-
const manifest =
|
|
158
|
+
const manifest = path2.join(dir, "package.json");
|
|
121
159
|
if (existsSync(manifest)) {
|
|
122
|
-
const pkg = JSON.parse(
|
|
160
|
+
const pkg = JSON.parse(readFileSync2(manifest, "utf8"));
|
|
123
161
|
if (pkg.name === "manifest" && pkg.version) return pkg.version;
|
|
124
162
|
}
|
|
125
|
-
const parent =
|
|
163
|
+
const parent = path2.dirname(dir);
|
|
126
164
|
if (parent === dir) return void 0;
|
|
127
165
|
dir = parent;
|
|
128
166
|
}
|
|
@@ -131,9 +169,9 @@ function installedVersion(cwd) {
|
|
|
131
169
|
}
|
|
132
170
|
}
|
|
133
171
|
async function nearestPackage(cwd) {
|
|
134
|
-
let dir =
|
|
172
|
+
let dir = path2.resolve(cwd);
|
|
135
173
|
while (true) {
|
|
136
|
-
const file =
|
|
174
|
+
const file = path2.join(dir, "package.json");
|
|
137
175
|
if (existsSync(file)) {
|
|
138
176
|
try {
|
|
139
177
|
return {
|
|
@@ -144,7 +182,7 @@ async function nearestPackage(cwd) {
|
|
|
144
182
|
return void 0;
|
|
145
183
|
}
|
|
146
184
|
}
|
|
147
|
-
const parent =
|
|
185
|
+
const parent = path2.dirname(dir);
|
|
148
186
|
if (parent === dir) return void 0;
|
|
149
187
|
dir = parent;
|
|
150
188
|
}
|
|
@@ -168,9 +206,9 @@ async function loadCheck(cwd) {
|
|
|
168
206
|
};
|
|
169
207
|
const scripts = isObject(found.pkg.scripts) ? found.pkg.scripts : {};
|
|
170
208
|
const values = Object.values(scripts).filter(
|
|
171
|
-
(
|
|
209
|
+
(value2) => typeof value2 === "string"
|
|
172
210
|
);
|
|
173
|
-
if (values.some((
|
|
211
|
+
if (values.some((value2) => value2.includes("manifest/register")))
|
|
174
212
|
return {
|
|
175
213
|
label,
|
|
176
214
|
status: "ok",
|
|
@@ -182,7 +220,7 @@ async function loadCheck(cwd) {
|
|
|
182
220
|
};
|
|
183
221
|
if ("next" in dependencies) {
|
|
184
222
|
for (const relative of instrumentationFiles) {
|
|
185
|
-
const file =
|
|
223
|
+
const file = path2.join(found.dir, relative);
|
|
186
224
|
if (!existsSync(file)) continue;
|
|
187
225
|
if (/manifest/.test(await readFile(file, "utf8")))
|
|
188
226
|
return {
|
|
@@ -221,19 +259,23 @@ function sdkCheck(cwd, injected) {
|
|
|
221
259
|
};
|
|
222
260
|
return { label, status: "ok", detail: `manifest ${version}` };
|
|
223
261
|
}
|
|
262
|
+
function nonBlank(value2) {
|
|
263
|
+
return value2 && value2.trim() !== "" ? value2 : void 0;
|
|
264
|
+
}
|
|
224
265
|
async function runDoctor(options = {}) {
|
|
225
266
|
const cwd = options.cwd ?? process.cwd();
|
|
226
267
|
const env = options.env ?? process.env;
|
|
227
268
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
228
269
|
const checks = [sdkCheck(cwd, options.sdkVersion)];
|
|
229
|
-
const
|
|
270
|
+
const dotenv = readDotEnv(cwd);
|
|
271
|
+
const rawUrl = options.url ?? nonBlank(env.MNFST_URL) ?? dotenv.MNFST_URL ?? DEFAULT_URL;
|
|
230
272
|
const url = normalizeBase(rawUrl);
|
|
231
|
-
const key = env.MNFST_KEY;
|
|
273
|
+
const key = nonBlank(env.MNFST_KEY) ?? dotenv.MNFST_KEY;
|
|
232
274
|
if (!key) {
|
|
233
275
|
checks.push({
|
|
234
276
|
label: "MNFST_KEY set",
|
|
235
277
|
status: "fail",
|
|
236
|
-
detail: "MNFST_KEY is not set"
|
|
278
|
+
detail: "MNFST_KEY is not set in the environment or the project's .env"
|
|
237
279
|
});
|
|
238
280
|
} else {
|
|
239
281
|
checks.push({ label: "MNFST_KEY set", status: "ok", detail: maskKey(key) });
|
|
@@ -305,7 +347,7 @@ function render(report) {
|
|
|
305
347
|
lines.push("");
|
|
306
348
|
lines.push("Runtime coverage");
|
|
307
349
|
lines.push(
|
|
308
|
-
" Node.js runtime fetch, http.request, https.request, http.get are patched"
|
|
350
|
+
" Node.js runtime fetch, http.request, https.request, http.get are patched; undici is tracked"
|
|
309
351
|
);
|
|
310
352
|
lines.push(
|
|
311
353
|
" Edge runtime not supported \u2014 middleware.ts and Edge route handlers are never covered"
|
|
@@ -320,9 +362,9 @@ function parseArgs(argv) {
|
|
|
320
362
|
if (arg === "-h" || arg === "--help") args.help = true;
|
|
321
363
|
else if (arg === "-v" || arg === "--version") args.version = true;
|
|
322
364
|
else if (arg === "--url") {
|
|
323
|
-
const
|
|
324
|
-
if (!
|
|
325
|
-
args.url =
|
|
365
|
+
const value2 = argv[++index];
|
|
366
|
+
if (!value2) throw new Error("--url needs a value");
|
|
367
|
+
args.url = value2;
|
|
326
368
|
} else if (arg.startsWith("--url=")) args.url = arg.slice("--url=".length);
|
|
327
369
|
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
328
370
|
else if (args.command === void 0) args.command = arg;
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
trackedUrl,
|
|
16
16
|
travelingBody,
|
|
17
17
|
warn
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-TX6KHYCJ.js";
|
|
19
19
|
|
|
20
20
|
// src/http.ts
|
|
21
21
|
import http from "http";
|
|
@@ -140,6 +140,85 @@ var CallBuffer = class {
|
|
|
140
140
|
}
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
+
// src/undici.ts
|
|
144
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
145
|
+
import { subscribe, unsubscribe } from "diagnostics_channel";
|
|
146
|
+
var handling = new AsyncLocalStorage();
|
|
147
|
+
var handled = (work) => handling.run(true, work);
|
|
148
|
+
function installUndici(runtime) {
|
|
149
|
+
const manifestOrigin = new URL(runtime.options.url).origin;
|
|
150
|
+
const inFlight = /* @__PURE__ */ new WeakMap();
|
|
151
|
+
const onCreate = (message) => {
|
|
152
|
+
const { request } = message;
|
|
153
|
+
if (handling.getStore() || String(request.origin) === manifestOrigin) return;
|
|
154
|
+
inFlight.set(request, { startedAt: Date.now(), started: performance.now() });
|
|
155
|
+
};
|
|
156
|
+
const onHeaders = (message) => {
|
|
157
|
+
const { request, response } = message;
|
|
158
|
+
const started = inFlight.get(request);
|
|
159
|
+
if (!started) return;
|
|
160
|
+
inFlight.delete(request);
|
|
161
|
+
let url;
|
|
162
|
+
try {
|
|
163
|
+
url = new URL(request.path ?? "/", String(request.origin)).href;
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (runtime.excluded(url)) return;
|
|
168
|
+
runtime.track(
|
|
169
|
+
request.method ?? "GET",
|
|
170
|
+
() => url,
|
|
171
|
+
response.statusCode,
|
|
172
|
+
started.startedAt,
|
|
173
|
+
performance.now() - started.started
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
subscribe("undici:request:create", onCreate);
|
|
177
|
+
subscribe("undici:request:headers", onHeaders);
|
|
178
|
+
return () => {
|
|
179
|
+
unsubscribe("undici:request:create", onCreate);
|
|
180
|
+
unsubscribe("undici:request:headers", onHeaders);
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/filter.ts
|
|
185
|
+
var HOST = /^(?:[a-z0-9_-]+(?:\.[a-z0-9_-]+)*|\[[0-9a-f:.]+\])$/;
|
|
186
|
+
function parseRule(entry) {
|
|
187
|
+
const rest = entry.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[?#]/)[0] ?? "";
|
|
188
|
+
const slash = rest.indexOf("/");
|
|
189
|
+
const authority = (slash < 0 ? rest : rest.slice(0, slash)).toLowerCase();
|
|
190
|
+
const path = slash < 0 ? "" : rest.slice(slash).replace(/\/+$/, "");
|
|
191
|
+
const host = authority.replace(/^\*\./, "").replace(/:\d+$/, "").replace(/^\.+|\.+$/g, "");
|
|
192
|
+
if (!HOST.test(host) || path.includes("*")) return null;
|
|
193
|
+
return { host: host.replace(/^\[|\]$/g, ""), path: path || null };
|
|
194
|
+
}
|
|
195
|
+
var entries = (value) => (typeof value === "string" ? value.split(",") : value ?? []).map((e) => String(e).trim()).filter(Boolean);
|
|
196
|
+
function resolveFilter(options, env) {
|
|
197
|
+
const invalid = [];
|
|
198
|
+
const pick = (option, name) => {
|
|
199
|
+
const given = entries(option).length > 0 ? entries(option) : entries(env[name]);
|
|
200
|
+
const rules = given.map((e) => parseRule(e) ?? (invalid.push(e), null)).filter((r) => r !== null);
|
|
201
|
+
return { given: given.length > 0, rules };
|
|
202
|
+
};
|
|
203
|
+
const allow = pick(options.allowlist, "MNFST_ALLOWLIST");
|
|
204
|
+
const deny = pick(options.denylist, "MNFST_DENYLIST");
|
|
205
|
+
return { filter: { allow: allow.given ? allow.rules : null, deny: deny.rules }, invalid };
|
|
206
|
+
}
|
|
207
|
+
var covers = (rule, host, path) => (host === rule.host || host.endsWith("." + rule.host)) && (rule.path === null || path === rule.path || path.startsWith(rule.path + "/"));
|
|
208
|
+
function isExcluded(filter, url) {
|
|
209
|
+
let host;
|
|
210
|
+
let path;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = new URL(url);
|
|
213
|
+
host = parsed.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
214
|
+
path = parsed.pathname || "/";
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
if (filter.deny.some((r) => covers(r, host, path))) return true;
|
|
219
|
+
return filter.allow !== null && !filter.allow.some((r) => covers(r, host, path));
|
|
220
|
+
}
|
|
221
|
+
|
|
143
222
|
// src/runtime.ts
|
|
144
223
|
var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
|
|
145
224
|
var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
|
|
@@ -151,7 +230,7 @@ var Runtime = class {
|
|
|
151
230
|
this.original = original;
|
|
152
231
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
153
232
|
this.tracker = new CallBuffer(
|
|
154
|
-
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
233
|
+
(batch, signal) => handled(() => this.api.sendRequests(batch, signal)),
|
|
155
234
|
{ immediate: isServerless() }
|
|
156
235
|
);
|
|
157
236
|
}
|
|
@@ -180,7 +259,16 @@ var Runtime = class {
|
|
|
180
259
|
} catch {
|
|
181
260
|
}
|
|
182
261
|
}
|
|
183
|
-
|
|
262
|
+
/** A call kept out of Manifest by the allowlist or denylist: never healed, never tracked. */
|
|
263
|
+
excluded(url) {
|
|
264
|
+
return this.options.filter ? isExcluded(this.options.filter, url) : false;
|
|
265
|
+
}
|
|
266
|
+
// Marked as handled, so the undici channels do not count this call (or its
|
|
267
|
+
// retry) a second time.
|
|
268
|
+
fetch = (input, init) => handled(() => this.send(input, init));
|
|
269
|
+
async send(input, init) {
|
|
270
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
271
|
+
if (this.excluded(target)) return this.original(input, init);
|
|
184
272
|
const request = new Request(input, init);
|
|
185
273
|
const extras = { ...init };
|
|
186
274
|
delete extras.body;
|
|
@@ -195,7 +283,7 @@ var Runtime = class {
|
|
|
195
283
|
return response;
|
|
196
284
|
}
|
|
197
285
|
return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
|
|
198
|
-
}
|
|
286
|
+
}
|
|
199
287
|
async handleResponse(request, response, body, responseTimeMs, extras = {}) {
|
|
200
288
|
if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
|
|
201
289
|
return this.repair(request, extras, response, body, responseTimeMs);
|
|
@@ -331,6 +419,7 @@ function wrapRequest(original, protocol, runtime) {
|
|
|
331
419
|
const emit = request.emit.bind(request);
|
|
332
420
|
request.emit = ((event, ...values) => {
|
|
333
421
|
if (event !== "response") return emit(event, ...values);
|
|
422
|
+
if (excluded(runtime, request, protocol)) return emit(event, ...values);
|
|
334
423
|
const response = values[0];
|
|
335
424
|
if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
|
|
336
425
|
runtime.track(
|
|
@@ -358,6 +447,13 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
|
|
|
358
447
|
const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
|
|
359
448
|
return incomingResponse(healed, clientRequest);
|
|
360
449
|
}
|
|
450
|
+
function excluded(runtime, request, protocol) {
|
|
451
|
+
try {
|
|
452
|
+
return runtime.excluded(requestUrl(request, protocol).toString());
|
|
453
|
+
} catch {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
361
457
|
function requestUrl(request, protocol) {
|
|
362
458
|
const authority = String(request.getHeader("host") ?? request.host);
|
|
363
459
|
return new URL(request.path, `${protocol}//${authority}`);
|
|
@@ -496,10 +592,12 @@ function manifest(options = {}) {
|
|
|
496
592
|
);
|
|
497
593
|
}
|
|
498
594
|
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
499
|
-
const
|
|
595
|
+
const { filter, invalid } = resolveFilter(options, process.env);
|
|
596
|
+
if (invalid.length > 0) warn(`Ignoring unreadable allowlist/denylist entries: ${invalid.join(", ")}`);
|
|
597
|
+
const resolved = { ...options, key, url: url.toString(), filter };
|
|
500
598
|
const existing = globals[STATE];
|
|
501
599
|
if (existing) {
|
|
502
|
-
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal) {
|
|
600
|
+
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal || JSON.stringify(existing.options.filter) !== JSON.stringify(filter)) {
|
|
503
601
|
warn(
|
|
504
602
|
"Manifest is already configured; changing configuration requires a process restart"
|
|
505
603
|
);
|
|
@@ -509,6 +607,7 @@ function manifest(options = {}) {
|
|
|
509
607
|
const runtime = new Runtime(resolved, globalThis.fetch.bind(globalThis));
|
|
510
608
|
globalThis.fetch = runtime.fetch;
|
|
511
609
|
installHttp(runtime);
|
|
610
|
+
installUndici(runtime);
|
|
512
611
|
globals[STATE] = runtime;
|
|
513
612
|
runtime.api.hello(`node-${process.versions.node}`);
|
|
514
613
|
process.once("beforeExit", () => {
|
package/dist/index.cjs
CHANGED
|
@@ -361,7 +361,7 @@ function keepAlive(promise) {
|
|
|
361
361
|
}
|
|
362
362
|
|
|
363
363
|
// src/api.ts
|
|
364
|
-
var VERSION = "7.
|
|
364
|
+
var VERSION = "7.4.0";
|
|
365
365
|
var MAX_HEALS_IN_FLIGHT = 8;
|
|
366
366
|
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
367
367
|
var HealApi = class {
|
|
@@ -660,6 +660,85 @@ var CallBuffer = class {
|
|
|
660
660
|
}
|
|
661
661
|
};
|
|
662
662
|
|
|
663
|
+
// src/undici.ts
|
|
664
|
+
var import_node_async_hooks = require("async_hooks");
|
|
665
|
+
var import_node_diagnostics_channel = require("diagnostics_channel");
|
|
666
|
+
var handling = new import_node_async_hooks.AsyncLocalStorage();
|
|
667
|
+
var handled = (work) => handling.run(true, work);
|
|
668
|
+
function installUndici(runtime) {
|
|
669
|
+
const manifestOrigin = new URL(runtime.options.url).origin;
|
|
670
|
+
const inFlight = /* @__PURE__ */ new WeakMap();
|
|
671
|
+
const onCreate = (message) => {
|
|
672
|
+
const { request } = message;
|
|
673
|
+
if (handling.getStore() || String(request.origin) === manifestOrigin) return;
|
|
674
|
+
inFlight.set(request, { startedAt: Date.now(), started: performance.now() });
|
|
675
|
+
};
|
|
676
|
+
const onHeaders = (message) => {
|
|
677
|
+
const { request, response } = message;
|
|
678
|
+
const started = inFlight.get(request);
|
|
679
|
+
if (!started) return;
|
|
680
|
+
inFlight.delete(request);
|
|
681
|
+
let url;
|
|
682
|
+
try {
|
|
683
|
+
url = new URL(request.path ?? "/", String(request.origin)).href;
|
|
684
|
+
} catch {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (runtime.excluded(url)) return;
|
|
688
|
+
runtime.track(
|
|
689
|
+
request.method ?? "GET",
|
|
690
|
+
() => url,
|
|
691
|
+
response.statusCode,
|
|
692
|
+
started.startedAt,
|
|
693
|
+
performance.now() - started.started
|
|
694
|
+
);
|
|
695
|
+
};
|
|
696
|
+
(0, import_node_diagnostics_channel.subscribe)("undici:request:create", onCreate);
|
|
697
|
+
(0, import_node_diagnostics_channel.subscribe)("undici:request:headers", onHeaders);
|
|
698
|
+
return () => {
|
|
699
|
+
(0, import_node_diagnostics_channel.unsubscribe)("undici:request:create", onCreate);
|
|
700
|
+
(0, import_node_diagnostics_channel.unsubscribe)("undici:request:headers", onHeaders);
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/filter.ts
|
|
705
|
+
var HOST = /^(?:[a-z0-9_-]+(?:\.[a-z0-9_-]+)*|\[[0-9a-f:.]+\])$/;
|
|
706
|
+
function parseRule(entry) {
|
|
707
|
+
const rest = entry.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[?#]/)[0] ?? "";
|
|
708
|
+
const slash = rest.indexOf("/");
|
|
709
|
+
const authority = (slash < 0 ? rest : rest.slice(0, slash)).toLowerCase();
|
|
710
|
+
const path = slash < 0 ? "" : rest.slice(slash).replace(/\/+$/, "");
|
|
711
|
+
const host = authority.replace(/^\*\./, "").replace(/:\d+$/, "").replace(/^\.+|\.+$/g, "");
|
|
712
|
+
if (!HOST.test(host) || path.includes("*")) return null;
|
|
713
|
+
return { host: host.replace(/^\[|\]$/g, ""), path: path || null };
|
|
714
|
+
}
|
|
715
|
+
var entries = (value) => (typeof value === "string" ? value.split(",") : value ?? []).map((e) => String(e).trim()).filter(Boolean);
|
|
716
|
+
function resolveFilter(options, env) {
|
|
717
|
+
const invalid = [];
|
|
718
|
+
const pick = (option, name) => {
|
|
719
|
+
const given = entries(option).length > 0 ? entries(option) : entries(env[name]);
|
|
720
|
+
const rules = given.map((e) => parseRule(e) ?? (invalid.push(e), null)).filter((r) => r !== null);
|
|
721
|
+
return { given: given.length > 0, rules };
|
|
722
|
+
};
|
|
723
|
+
const allow = pick(options.allowlist, "MNFST_ALLOWLIST");
|
|
724
|
+
const deny = pick(options.denylist, "MNFST_DENYLIST");
|
|
725
|
+
return { filter: { allow: allow.given ? allow.rules : null, deny: deny.rules }, invalid };
|
|
726
|
+
}
|
|
727
|
+
var covers = (rule, host, path) => (host === rule.host || host.endsWith("." + rule.host)) && (rule.path === null || path === rule.path || path.startsWith(rule.path + "/"));
|
|
728
|
+
function isExcluded(filter, url) {
|
|
729
|
+
let host;
|
|
730
|
+
let path;
|
|
731
|
+
try {
|
|
732
|
+
const parsed = new URL(url);
|
|
733
|
+
host = parsed.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
734
|
+
path = parsed.pathname || "/";
|
|
735
|
+
} catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
if (filter.deny.some((r) => covers(r, host, path))) return true;
|
|
739
|
+
return filter.allow !== null && !filter.allow.some((r) => covers(r, host, path));
|
|
740
|
+
}
|
|
741
|
+
|
|
663
742
|
// src/runtime.ts
|
|
664
743
|
var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
|
|
665
744
|
var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
|
|
@@ -671,7 +750,7 @@ var Runtime = class {
|
|
|
671
750
|
this.original = original;
|
|
672
751
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
673
752
|
this.tracker = new CallBuffer(
|
|
674
|
-
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
753
|
+
(batch, signal) => handled(() => this.api.sendRequests(batch, signal)),
|
|
675
754
|
{ immediate: isServerless() }
|
|
676
755
|
);
|
|
677
756
|
}
|
|
@@ -700,7 +779,16 @@ var Runtime = class {
|
|
|
700
779
|
} catch {
|
|
701
780
|
}
|
|
702
781
|
}
|
|
703
|
-
|
|
782
|
+
/** A call kept out of Manifest by the allowlist or denylist: never healed, never tracked. */
|
|
783
|
+
excluded(url) {
|
|
784
|
+
return this.options.filter ? isExcluded(this.options.filter, url) : false;
|
|
785
|
+
}
|
|
786
|
+
// Marked as handled, so the undici channels do not count this call (or its
|
|
787
|
+
// retry) a second time.
|
|
788
|
+
fetch = (input, init) => handled(() => this.send(input, init));
|
|
789
|
+
async send(input, init) {
|
|
790
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
791
|
+
if (this.excluded(target)) return this.original(input, init);
|
|
704
792
|
const request = new Request(input, init);
|
|
705
793
|
const extras = { ...init };
|
|
706
794
|
delete extras.body;
|
|
@@ -715,7 +803,7 @@ var Runtime = class {
|
|
|
715
803
|
return response;
|
|
716
804
|
}
|
|
717
805
|
return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
|
|
718
|
-
}
|
|
806
|
+
}
|
|
719
807
|
async handleResponse(request, response, body, responseTimeMs, extras = {}) {
|
|
720
808
|
if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
|
|
721
809
|
return this.repair(request, extras, response, body, responseTimeMs);
|
|
@@ -851,6 +939,7 @@ function wrapRequest(original, protocol, runtime) {
|
|
|
851
939
|
const emit = request.emit.bind(request);
|
|
852
940
|
request.emit = ((event, ...values) => {
|
|
853
941
|
if (event !== "response") return emit(event, ...values);
|
|
942
|
+
if (excluded(runtime, request, protocol)) return emit(event, ...values);
|
|
854
943
|
const response = values[0];
|
|
855
944
|
if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
|
|
856
945
|
runtime.track(
|
|
@@ -878,6 +967,13 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
|
|
|
878
967
|
const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
|
|
879
968
|
return incomingResponse(healed, clientRequest);
|
|
880
969
|
}
|
|
970
|
+
function excluded(runtime, request, protocol) {
|
|
971
|
+
try {
|
|
972
|
+
return runtime.excluded(requestUrl(request, protocol).toString());
|
|
973
|
+
} catch {
|
|
974
|
+
return false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
881
977
|
function requestUrl(request, protocol) {
|
|
882
978
|
const authority = String(request.getHeader("host") ?? request.host);
|
|
883
979
|
return new URL(request.path, `${protocol}//${authority}`);
|
|
@@ -1016,10 +1112,12 @@ function manifest(options = {}) {
|
|
|
1016
1112
|
);
|
|
1017
1113
|
}
|
|
1018
1114
|
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
1019
|
-
const
|
|
1115
|
+
const { filter, invalid } = resolveFilter(options, process.env);
|
|
1116
|
+
if (invalid.length > 0) warn(`Ignoring unreadable allowlist/denylist entries: ${invalid.join(", ")}`);
|
|
1117
|
+
const resolved = { ...options, key, url: url.toString(), filter };
|
|
1020
1118
|
const existing = globals[STATE];
|
|
1021
1119
|
if (existing) {
|
|
1022
|
-
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal) {
|
|
1120
|
+
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal || JSON.stringify(existing.options.filter) !== JSON.stringify(filter)) {
|
|
1023
1121
|
warn(
|
|
1024
1122
|
"Manifest is already configured; changing configuration requires a process restart"
|
|
1025
1123
|
);
|
|
@@ -1029,6 +1127,7 @@ function manifest(options = {}) {
|
|
|
1029
1127
|
const runtime = new Runtime(resolved, globalThis.fetch.bind(globalThis));
|
|
1030
1128
|
globalThis.fetch = runtime.fetch;
|
|
1031
1129
|
installHttp(runtime);
|
|
1130
|
+
installUndici(runtime);
|
|
1032
1131
|
globals[STATE] = runtime;
|
|
1033
1132
|
runtime.api.hello(`node-${process.versions.node}`);
|
|
1034
1133
|
process.once("beforeExit", () => {
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,13 @@ interface ManifestOptions {
|
|
|
2
2
|
key?: string;
|
|
3
3
|
url?: string;
|
|
4
4
|
onHeal?: (event: HealEvent) => void | Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Only these calls reach Manifest. Each entry is a domain (`stripe.com`, subdomains included)
|
|
7
|
+
* or a domain with a path (`stripe.com/v1/charges`). Default: `MNFST_ALLOWLIST`.
|
|
8
|
+
*/
|
|
9
|
+
allowlist?: string[] | string;
|
|
10
|
+
/** These calls never reach Manifest; same entries as `allowlist`, and it wins. Default: `MNFST_DENYLIST`. */
|
|
11
|
+
denylist?: string[] | string;
|
|
5
12
|
}
|
|
6
13
|
interface HealEvent {
|
|
7
14
|
url: string;
|
|
@@ -12,7 +19,7 @@ interface HealEvent {
|
|
|
12
19
|
operations?: unknown[];
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
declare const VERSION = "7.
|
|
22
|
+
declare const VERSION = "7.4.0";
|
|
16
23
|
|
|
17
24
|
/** Install once, before libraries capture their own reference to global fetch. */
|
|
18
25
|
declare function manifest(options?: ManifestOptions): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ interface ManifestOptions {
|
|
|
2
2
|
key?: string;
|
|
3
3
|
url?: string;
|
|
4
4
|
onHeal?: (event: HealEvent) => void | Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* Only these calls reach Manifest. Each entry is a domain (`stripe.com`, subdomains included)
|
|
7
|
+
* or a domain with a path (`stripe.com/v1/charges`). Default: `MNFST_ALLOWLIST`.
|
|
8
|
+
*/
|
|
9
|
+
allowlist?: string[] | string;
|
|
10
|
+
/** These calls never reach Manifest; same entries as `allowlist`, and it wins. Default: `MNFST_DENYLIST`. */
|
|
11
|
+
denylist?: string[] | string;
|
|
5
12
|
}
|
|
6
13
|
interface HealEvent {
|
|
7
14
|
url: string;
|
|
@@ -12,7 +19,7 @@ interface HealEvent {
|
|
|
12
19
|
operations?: unknown[];
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
declare const VERSION = "7.
|
|
22
|
+
declare const VERSION = "7.4.0";
|
|
16
23
|
|
|
17
24
|
/** Install once, before libraries capture their own reference to global fetch. */
|
|
18
25
|
declare function manifest(options?: ManifestOptions): void;
|
package/dist/index.js
CHANGED
package/dist/register.cjs
CHANGED
|
@@ -348,7 +348,7 @@ function keepAlive(promise) {
|
|
|
348
348
|
}
|
|
349
349
|
|
|
350
350
|
// src/api.ts
|
|
351
|
-
var VERSION = "7.
|
|
351
|
+
var VERSION = "7.4.0";
|
|
352
352
|
var MAX_HEALS_IN_FLIGHT = 8;
|
|
353
353
|
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
354
354
|
var HealApi = class {
|
|
@@ -647,6 +647,85 @@ var CallBuffer = class {
|
|
|
647
647
|
}
|
|
648
648
|
};
|
|
649
649
|
|
|
650
|
+
// src/undici.ts
|
|
651
|
+
var import_node_async_hooks = require("async_hooks");
|
|
652
|
+
var import_node_diagnostics_channel = require("diagnostics_channel");
|
|
653
|
+
var handling = new import_node_async_hooks.AsyncLocalStorage();
|
|
654
|
+
var handled = (work) => handling.run(true, work);
|
|
655
|
+
function installUndici(runtime) {
|
|
656
|
+
const manifestOrigin = new URL(runtime.options.url).origin;
|
|
657
|
+
const inFlight = /* @__PURE__ */ new WeakMap();
|
|
658
|
+
const onCreate = (message) => {
|
|
659
|
+
const { request } = message;
|
|
660
|
+
if (handling.getStore() || String(request.origin) === manifestOrigin) return;
|
|
661
|
+
inFlight.set(request, { startedAt: Date.now(), started: performance.now() });
|
|
662
|
+
};
|
|
663
|
+
const onHeaders = (message) => {
|
|
664
|
+
const { request, response } = message;
|
|
665
|
+
const started = inFlight.get(request);
|
|
666
|
+
if (!started) return;
|
|
667
|
+
inFlight.delete(request);
|
|
668
|
+
let url;
|
|
669
|
+
try {
|
|
670
|
+
url = new URL(request.path ?? "/", String(request.origin)).href;
|
|
671
|
+
} catch {
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (runtime.excluded(url)) return;
|
|
675
|
+
runtime.track(
|
|
676
|
+
request.method ?? "GET",
|
|
677
|
+
() => url,
|
|
678
|
+
response.statusCode,
|
|
679
|
+
started.startedAt,
|
|
680
|
+
performance.now() - started.started
|
|
681
|
+
);
|
|
682
|
+
};
|
|
683
|
+
(0, import_node_diagnostics_channel.subscribe)("undici:request:create", onCreate);
|
|
684
|
+
(0, import_node_diagnostics_channel.subscribe)("undici:request:headers", onHeaders);
|
|
685
|
+
return () => {
|
|
686
|
+
(0, import_node_diagnostics_channel.unsubscribe)("undici:request:create", onCreate);
|
|
687
|
+
(0, import_node_diagnostics_channel.unsubscribe)("undici:request:headers", onHeaders);
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// src/filter.ts
|
|
692
|
+
var HOST = /^(?:[a-z0-9_-]+(?:\.[a-z0-9_-]+)*|\[[0-9a-f:.]+\])$/;
|
|
693
|
+
function parseRule(entry) {
|
|
694
|
+
const rest = entry.trim().replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").split(/[?#]/)[0] ?? "";
|
|
695
|
+
const slash = rest.indexOf("/");
|
|
696
|
+
const authority = (slash < 0 ? rest : rest.slice(0, slash)).toLowerCase();
|
|
697
|
+
const path = slash < 0 ? "" : rest.slice(slash).replace(/\/+$/, "");
|
|
698
|
+
const host = authority.replace(/^\*\./, "").replace(/:\d+$/, "").replace(/^\.+|\.+$/g, "");
|
|
699
|
+
if (!HOST.test(host) || path.includes("*")) return null;
|
|
700
|
+
return { host: host.replace(/^\[|\]$/g, ""), path: path || null };
|
|
701
|
+
}
|
|
702
|
+
var entries = (value) => (typeof value === "string" ? value.split(",") : value ?? []).map((e) => String(e).trim()).filter(Boolean);
|
|
703
|
+
function resolveFilter(options, env) {
|
|
704
|
+
const invalid = [];
|
|
705
|
+
const pick = (option, name) => {
|
|
706
|
+
const given = entries(option).length > 0 ? entries(option) : entries(env[name]);
|
|
707
|
+
const rules = given.map((e) => parseRule(e) ?? (invalid.push(e), null)).filter((r) => r !== null);
|
|
708
|
+
return { given: given.length > 0, rules };
|
|
709
|
+
};
|
|
710
|
+
const allow = pick(options.allowlist, "MNFST_ALLOWLIST");
|
|
711
|
+
const deny = pick(options.denylist, "MNFST_DENYLIST");
|
|
712
|
+
return { filter: { allow: allow.given ? allow.rules : null, deny: deny.rules }, invalid };
|
|
713
|
+
}
|
|
714
|
+
var covers = (rule, host, path) => (host === rule.host || host.endsWith("." + rule.host)) && (rule.path === null || path === rule.path || path.startsWith(rule.path + "/"));
|
|
715
|
+
function isExcluded(filter, url) {
|
|
716
|
+
let host;
|
|
717
|
+
let path;
|
|
718
|
+
try {
|
|
719
|
+
const parsed = new URL(url);
|
|
720
|
+
host = parsed.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
|
|
721
|
+
path = parsed.pathname || "/";
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
if (filter.deny.some((r) => covers(r, host, path))) return true;
|
|
726
|
+
return filter.allow !== null && !filter.allow.some((r) => covers(r, host, path));
|
|
727
|
+
}
|
|
728
|
+
|
|
650
729
|
// src/runtime.ts
|
|
651
730
|
var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
|
|
652
731
|
var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
|
|
@@ -658,7 +737,7 @@ var Runtime = class {
|
|
|
658
737
|
this.original = original;
|
|
659
738
|
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
660
739
|
this.tracker = new CallBuffer(
|
|
661
|
-
(batch, signal) => this.api.sendRequests(batch, signal),
|
|
740
|
+
(batch, signal) => handled(() => this.api.sendRequests(batch, signal)),
|
|
662
741
|
{ immediate: isServerless() }
|
|
663
742
|
);
|
|
664
743
|
}
|
|
@@ -687,7 +766,16 @@ var Runtime = class {
|
|
|
687
766
|
} catch {
|
|
688
767
|
}
|
|
689
768
|
}
|
|
690
|
-
|
|
769
|
+
/** A call kept out of Manifest by the allowlist or denylist: never healed, never tracked. */
|
|
770
|
+
excluded(url) {
|
|
771
|
+
return this.options.filter ? isExcluded(this.options.filter, url) : false;
|
|
772
|
+
}
|
|
773
|
+
// Marked as handled, so the undici channels do not count this call (or its
|
|
774
|
+
// retry) a second time.
|
|
775
|
+
fetch = (input, init) => handled(() => this.send(input, init));
|
|
776
|
+
async send(input, init) {
|
|
777
|
+
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
778
|
+
if (this.excluded(target)) return this.original(input, init);
|
|
691
779
|
const request = new Request(input, init);
|
|
692
780
|
const extras = { ...init };
|
|
693
781
|
delete extras.body;
|
|
@@ -702,7 +790,7 @@ var Runtime = class {
|
|
|
702
790
|
return response;
|
|
703
791
|
}
|
|
704
792
|
return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
|
|
705
|
-
}
|
|
793
|
+
}
|
|
706
794
|
async handleResponse(request, response, body, responseTimeMs, extras = {}) {
|
|
707
795
|
if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
|
|
708
796
|
return this.repair(request, extras, response, body, responseTimeMs);
|
|
@@ -838,6 +926,7 @@ function wrapRequest(original, protocol, runtime) {
|
|
|
838
926
|
const emit = request.emit.bind(request);
|
|
839
927
|
request.emit = ((event, ...values) => {
|
|
840
928
|
if (event !== "response") return emit(event, ...values);
|
|
929
|
+
if (excluded(runtime, request, protocol)) return emit(event, ...values);
|
|
841
930
|
const response = values[0];
|
|
842
931
|
if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
|
|
843
932
|
runtime.track(
|
|
@@ -865,6 +954,13 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
|
|
|
865
954
|
const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
|
|
866
955
|
return incomingResponse(healed, clientRequest);
|
|
867
956
|
}
|
|
957
|
+
function excluded(runtime, request, protocol) {
|
|
958
|
+
try {
|
|
959
|
+
return runtime.excluded(requestUrl(request, protocol).toString());
|
|
960
|
+
} catch {
|
|
961
|
+
return false;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
868
964
|
function requestUrl(request, protocol) {
|
|
869
965
|
const authority = String(request.getHeader("host") ?? request.host);
|
|
870
966
|
return new URL(request.path, `${protocol}//${authority}`);
|
|
@@ -1003,10 +1099,12 @@ function manifest(options = {}) {
|
|
|
1003
1099
|
);
|
|
1004
1100
|
}
|
|
1005
1101
|
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
1006
|
-
const
|
|
1102
|
+
const { filter, invalid } = resolveFilter(options, process.env);
|
|
1103
|
+
if (invalid.length > 0) warn(`Ignoring unreadable allowlist/denylist entries: ${invalid.join(", ")}`);
|
|
1104
|
+
const resolved = { ...options, key, url: url.toString(), filter };
|
|
1007
1105
|
const existing = globals[STATE];
|
|
1008
1106
|
if (existing) {
|
|
1009
|
-
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal) {
|
|
1107
|
+
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal || JSON.stringify(existing.options.filter) !== JSON.stringify(filter)) {
|
|
1010
1108
|
warn(
|
|
1011
1109
|
"Manifest is already configured; changing configuration requires a process restart"
|
|
1012
1110
|
);
|
|
@@ -1016,6 +1114,7 @@ function manifest(options = {}) {
|
|
|
1016
1114
|
const runtime = new Runtime(resolved, globalThis.fetch.bind(globalThis));
|
|
1017
1115
|
globalThis.fetch = runtime.fetch;
|
|
1018
1116
|
installHttp(runtime);
|
|
1117
|
+
installUndici(runtime);
|
|
1019
1118
|
globals[STATE] = runtime;
|
|
1020
1119
|
runtime.api.hello(`node-${process.versions.node}`);
|
|
1021
1120
|
process.once("beforeExit", () => {
|
package/dist/register.js
CHANGED
package/docs/guide.md
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
Call `manifest()` once at startup, before other libraries save a reference to `fetch`, `node:http` or `node:https`. Where a client is built at import time and would capture the original `fetch` first, [preload the register entry](#preloading) instead.
|
|
8
8
|
|
|
9
|
-
It reads `MNFST_KEY
|
|
9
|
+
It reads `MNFST_KEY`, `MNFST_URL`, `MNFST_ALLOWLIST` and `MNFST_DENYLIST`; every option is optional.
|
|
10
10
|
|
|
11
11
|
| Option | Environment | Default |
|
|
12
12
|
| --- | --- | --- |
|
|
13
13
|
| `key` | `MNFST_KEY` | Disabled with a warning if missing |
|
|
14
14
|
| `url` | `MNFST_URL` | `https://api.manifest.build` |
|
|
15
15
|
| `onHeal` | — | Optional local callback |
|
|
16
|
+
| `allowlist` | `MNFST_ALLOWLIST` | Every call eligible ([entries](../README.md#choosing-which-calls-reach-manifest)) |
|
|
17
|
+
| `denylist` | `MNFST_DENYLIST` | None excluded |
|
|
16
18
|
|
|
17
19
|
Explicit options take precedence. Reconfiguration requires a restart. ESM and CommonJS imports share one process-wide installation; CommonJS uses `const { manifest } = require('manifest')`.
|
|
18
20
|
|
|
@@ -75,6 +77,8 @@ Run the doctor from the project directory. It checks the install without guessin
|
|
|
75
77
|
npx manifest doctor
|
|
76
78
|
```
|
|
77
79
|
|
|
80
|
+
It reads `MNFST_KEY` and `MNFST_URL` from the environment first, then from the project's `.env.local` or `.env` (the first file that sets a variable wins) — the files Next.js and `dotenv` keep them in.
|
|
81
|
+
|
|
78
82
|
```
|
|
79
83
|
✅ SDK installed manifest 7.0.0
|
|
80
84
|
✅ MNFST_KEY set mnfst_proj_…DZDw
|
|
@@ -84,7 +88,7 @@ npx manifest doctor
|
|
|
84
88
|
⚠️ Requests received no requests received yet
|
|
85
89
|
|
|
86
90
|
Runtime coverage
|
|
87
|
-
Node.js runtime fetch, http.request, https.request, http.get are patched
|
|
91
|
+
Node.js runtime fetch, http.request, https.request, http.get are patched; undici is tracked
|
|
88
92
|
Edge runtime not supported — middleware.ts and Edge route handlers are never covered
|
|
89
93
|
```
|
|
90
94
|
|
|
@@ -106,8 +110,9 @@ You can also verify by hand: send a JSON request that your test API rejects with
|
|
|
106
110
|
- One retry per capture. Same-origin URL and header repairs are supported by the SDK; the current app returns structured body repairs.
|
|
107
111
|
- Successful calls and successful retries remain streamed. Failed responses retain their bytes, status, headers, URL and redirect metadata.
|
|
108
112
|
- Every call that is not healed, whatever its status, is tracked as metadata only and sent in batches, off the request path (see "Data sent to Manifest").
|
|
113
|
+
- Calls made through the `undici` package directly (its `fetch` or `request`, as `@vercel/blob` does) and through a `fetch` reference saved before initialization are **tracked, not healed**: the SDK observes them on undici's diagnostics channels, which can see a response but not replace it.
|
|
109
114
|
|
|
110
|
-
**Not covered:** browser JavaScript
|
|
115
|
+
**Not covered:** browser JavaScript and HTTP/2. Those transports need separate integration. This SDK does not claim to intercept every Node HTTP client.
|
|
111
116
|
|
|
112
117
|
## Limits and failure behavior
|
|
113
118
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "manifest",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "Repair eligible failed API calls made by Node HTTP clients.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"axios": "^1.7.0",
|
|
58
58
|
"tsup": "^8.5.0",
|
|
59
59
|
"tsx": "^4.20.0",
|
|
60
|
-
"typescript": "^5.9.0"
|
|
60
|
+
"typescript": "^5.9.0",
|
|
61
|
+
"undici": "^5.29.0"
|
|
61
62
|
},
|
|
62
63
|
"repository": {
|
|
63
64
|
"type": "git",
|