manifest 7.1.0 → 7.2.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 CHANGED
@@ -60,3 +60,15 @@ Each captured failure permits one retry. A retry response, including another fai
60
60
  HTTP status must be 200–599. Transport failures use a fixed message without exception prose. HTTP status zero is not a wire status. Transport failures and unattempted retries are inconclusive evidence; neither can verify or invalidate a patch. The server determines the verdict from the raw evidence, with the first accepted report winning.
61
61
 
62
62
  Reports are best effort, bounded, and observable through Node warnings with code `MNFST`. The SDK sends the failed retry's raw body so the app can distinguish recurrence from a newly revealed issue. It does not assert `succeeded` or `failed` itself.
63
+
64
+ ## Tracked requests
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`:
67
+
68
+ ```json
69
+ {"requests":[{"traceId":"a-uuid","method":"POST","url":"https://example.com/orders","statusCode":200,"responseTimeMs":84,"occurredAt":"2026-09-23T10:14:07.512Z"}]}
70
+ ```
71
+
72
+ Metadata only. The URL carries scheme, host, port and path: no query string, userinfo or fragment. No headers and no request or response body are sent, and a response body is never read to record a call. A call sent to `/v1/heal` is not also tracked.
73
+
74
+ Recording is an in-memory append and never delays or fails the caller's request. A batch is sent when 500 calls are queued or every five seconds, at most once per second, one send at a time, up to 500 calls per request, with a five-second deadline. At most 5,000 calls are buffered; newer calls are dropped past that. A network error, timeout, 429 or 5xx is retried once, then the batch is dropped; any other answer, including 404 from a server without the route, drops it. HTTP 403 with `{"error":"project_disabled"}` suspends sending for five minutes, as for healing. Methods are upper-cased; a record whose method exceeds 16 characters or whose URL exceeds 4,096 is not sent. Buffered calls are sent once when the event loop drains (`beforeExit`), for at most two seconds: a send still in flight then is aborted and the rest is dropped, so a script never hangs on an unreachable server. A process killed or frozen first (serverless) can lose them.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- ![Manifest SDK Architecture](./docs/github-sdk.png)
3
+ ![Manifest SDK Architecture](https://raw.githubusercontent.com/mnfst/manifest-node/main/docs/github-sdk.png)
4
4
 
5
5
  # Manifest for Node.js
6
6
 
@@ -24,7 +24,9 @@ Manifest is a self-healing layer that fixes and retries failed API requests on t
24
24
 
25
25
  ## How it works
26
26
 
27
- ![How Manifest heals a failed request: a 400 reaches Manifest, drops to a patch from the knowledge base or the healing agents, and is retried once, returning a 200 OK](./docs/sdk-flow-diagram.png)
27
+ ![How the SDK works: every call Manifest does not heal is recorded in the background as metadata (method, URL, status, timing, no body); a failure Manifest can heal is sent with its error, patched, and retried once, returning a 200 OK](./docs/sdk-flow-diagram.png)
28
+
29
+ Every call your app makes is reported to Manifest as metadata only (method, URL without its query string, status and timing), in the background. A failure Manifest can heal is sent in full, so it can be repaired. [What is sent](docs/guide.md#data-sent-to-manifest).
28
30
 
29
31
  ## Prerequisites
30
32
 
package/dist/bin.cjs CHANGED
@@ -33,7 +33,7 @@ var import_node_path = __toESM(require("path"), 1);
33
33
  var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
34
34
 
35
35
  // src/api.ts
36
- var VERSION = "7.1.0";
36
+ var VERSION = "7.2.0";
37
37
 
38
38
  // src/cli.ts
39
39
  var DEFAULT_URL = "https://api.manifest.build";
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  VERSION,
4
4
  isObject
5
- } from "./chunk-AR23I2QK.js";
5
+ } from "./chunk-NHEF7GUP.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { createRequire } from "module";
@@ -10,9 +10,10 @@ import {
10
10
  safeHeaders,
11
11
  safeUrl,
12
12
  serializeRequestBody,
13
+ trackedUrl,
13
14
  travelingBody,
14
15
  warn
15
- } from "./chunk-AR23I2QK.js";
16
+ } from "./chunk-NHEF7GUP.js";
16
17
 
17
18
  // src/http.ts
18
19
  import http from "http";
@@ -23,6 +24,100 @@ import { createBrotliDecompress, createUnzip } from "zlib";
23
24
 
24
25
  // src/runtime.ts
25
26
  import { randomUUID } from "crypto";
27
+
28
+ // src/tracking.ts
29
+ var MAX_BUFFER = 5e3;
30
+ var MAX_BATCH = 500;
31
+ var CallBuffer = class {
32
+ constructor(send, options = {}) {
33
+ this.send = send;
34
+ this.flushAt = options.flushAt ?? 500;
35
+ this.minGapMs = options.minGapMs ?? 1e3;
36
+ this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
37
+ this.timer.unref();
38
+ }
39
+ send;
40
+ queue = [];
41
+ inFlight = null;
42
+ lastSentAt = -Infinity;
43
+ /** Past this instant, nothing is retried or waited for, and a send in flight is aborted. */
44
+ deadline = Infinity;
45
+ controller = new AbortController();
46
+ timer;
47
+ flushAt;
48
+ minGapMs;
49
+ record(call) {
50
+ if (this.queue.length >= MAX_BUFFER) return;
51
+ this.queue.push(call);
52
+ if (this.queue.length >= this.flushAt) void this.kick();
53
+ }
54
+ size() {
55
+ return this.queue.length;
56
+ }
57
+ /** Resolves when the send in flight, if any, has settled. */
58
+ async idle() {
59
+ await this.inFlight;
60
+ }
61
+ /**
62
+ * Send everything buffered now, one batch at a time through the same
63
+ * in-flight guard as the timer. With `deadlineMs` (the exit path), it gives
64
+ * up at the deadline: the send in flight is aborted, nothing is retried, and
65
+ * what is left is dropped, so a script never hangs on an unreachable server.
66
+ */
67
+ async flush(deadlineMs = Infinity) {
68
+ this.deadline = performance.now() + deadlineMs;
69
+ const abortAt = Number.isFinite(deadlineMs) ? setTimeout(() => this.controller.abort(), deadlineMs) : null;
70
+ try {
71
+ while ((this.queue.length > 0 || this.inFlight) && performance.now() < this.deadline) {
72
+ await (this.inFlight ?? this.start());
73
+ }
74
+ } finally {
75
+ if (abortAt) clearTimeout(abortAt);
76
+ if (this.controller.signal.aborted) this.controller = new AbortController();
77
+ this.deadline = Infinity;
78
+ }
79
+ }
80
+ stop() {
81
+ clearInterval(this.timer);
82
+ }
83
+ kick() {
84
+ if (this.inFlight || this.queue.length === 0) return;
85
+ if (performance.now() - this.lastSentAt < this.minGapMs) return;
86
+ void this.start();
87
+ }
88
+ start() {
89
+ this.inFlight = this.sendOne().finally(() => {
90
+ this.inFlight = null;
91
+ });
92
+ return this.inFlight;
93
+ }
94
+ async sendOne() {
95
+ const batch = this.queue.splice(0, MAX_BATCH);
96
+ for (let attempt = 0; attempt < 2; attempt++) {
97
+ if (attempt > 0 && performance.now() >= this.deadline) return;
98
+ await this.waitForGap();
99
+ this.lastSentAt = performance.now();
100
+ try {
101
+ await this.send(batch, this.controller.signal);
102
+ return;
103
+ } catch {
104
+ }
105
+ if (this.controller.signal.aborted) {
106
+ this.controller = new AbortController();
107
+ return;
108
+ }
109
+ }
110
+ }
111
+ // Referenced on purpose: this wait only happens while a send or an exit
112
+ // flush is under way, and an unref'd wait would let the process exit with
113
+ // the batch unsent. It never outlasts the flush deadline.
114
+ async waitForGap() {
115
+ const wait = Math.min(this.lastSentAt + this.minGapMs, this.deadline) - performance.now();
116
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
117
+ }
118
+ };
119
+
120
+ // src/runtime.ts
26
121
  var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
27
122
  var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
28
123
  var neverBodied = (method) => method === "GET" || method === "HEAD";
@@ -32,20 +127,47 @@ var Runtime = class {
32
127
  this.options = options;
33
128
  this.original = original;
34
129
  this.api = api ?? new HealApi(original, options.key, options.url);
130
+ this.tracker = new CallBuffer((batch, signal) => this.api.sendRequests(batch, signal));
35
131
  }
36
132
  options;
37
133
  original;
38
134
  api;
135
+ /** Calls that did not go to `/v1/heal`, any status, on their way to `/v1/requests`. */
136
+ tracker;
137
+ /**
138
+ * Record a call that is not being healed. An in-memory append: never awaited,
139
+ * never throws, reads nothing of the response.
140
+ */
141
+ track(method, url, statusCode, startedAt, responseTimeMs) {
142
+ try {
143
+ const reported = trackedUrl(url());
144
+ const verb = method.toUpperCase();
145
+ if (!reported || reported.length > 4096 || !verb || verb.length > 16 || statusCode < 100 || statusCode > 599) return;
146
+ this.tracker.record({
147
+ traceId: randomUUID(),
148
+ method: verb,
149
+ url: reported,
150
+ statusCode,
151
+ responseTimeMs: Math.round(responseTimeMs),
152
+ occurredAt: new Date(startedAt).toISOString()
153
+ });
154
+ } catch {
155
+ }
156
+ }
39
157
  fetch = async (input, init) => {
40
158
  const request = new Request(input, init);
41
159
  const extras = { ...init };
42
160
  delete extras.body;
43
161
  delete extras.headers;
44
162
  const bodyPromise = captureRequest(request).catch(() => ({ body: null, complete: false }));
163
+ const startedAt = Date.now();
45
164
  const started = performance.now();
46
165
  const response = await this.original(request, extras);
47
166
  const responseTimeMs = performance.now() - started;
48
- if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
167
+ if (!eligible(response.status) || response.redirected || !this.api.canHeal()) {
168
+ this.track(request.method, () => request.url, response.status, startedAt, responseTimeMs);
169
+ return response;
170
+ }
49
171
  return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
50
172
  };
51
173
  async handleResponse(request, response, body, responseTimeMs, extras = {}) {
@@ -175,6 +297,7 @@ function wrapRequest(original, protocol, runtime) {
175
297
  return ((...received) => {
176
298
  const args = [...received];
177
299
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
300
+ const startedAt = Date.now();
178
301
  const started = performance.now();
179
302
  const request = original(...args);
180
303
  const capture = captureBody(request);
@@ -183,7 +306,14 @@ function wrapRequest(original, protocol, runtime) {
183
306
  request.emit = ((event, ...values) => {
184
307
  if (event !== "response") return emit(event, ...values);
185
308
  const response = values[0];
186
- if (!eligible(response.statusCode ?? 0) || !runtime.api.enabled()) {
309
+ if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
310
+ runtime.track(
311
+ request.method,
312
+ () => requestUrl(request, protocol).toString(),
313
+ response.statusCode ?? 0,
314
+ startedAt,
315
+ performance.now() - started
316
+ );
187
317
  return emit(event, ...values);
188
318
  }
189
319
  void handleResponse(runtime, request, response, protocol, capture.body(), signal, started).then((healed) => emit("response", healed)).catch((error) => {
@@ -202,6 +332,10 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
202
332
  const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
203
333
  return incomingResponse(healed, clientRequest);
204
334
  }
335
+ function requestUrl(request, protocol) {
336
+ const authority = String(request.getHeader("host") ?? request.host);
337
+ return new URL(request.path, `${protocol}//${authority}`);
338
+ }
205
339
  function webRequest(request, protocol, captured, signal) {
206
340
  const headers = new Headers();
207
341
  for (const name of request.getHeaderNames()) {
@@ -210,8 +344,7 @@ function webRequest(request, protocol, captured, signal) {
210
344
  if (item !== void 0) headers.append(name, String(item));
211
345
  }
212
346
  }
213
- const authority = String(request.getHeader("host") ?? request.host);
214
- const url = new URL(request.path, `${protocol}//${authority}`);
347
+ const url = requestUrl(request, protocol);
215
348
  const method = request.method;
216
349
  return new Request(url, {
217
350
  method,
@@ -320,6 +453,7 @@ function cancellation(request, external) {
320
453
 
321
454
  // src/index.ts
322
455
  var STATE = /* @__PURE__ */ Symbol.for("mnfst.node.runtime.v1");
456
+ var EXIT_FLUSH_MS = 2e3;
323
457
  var globals = globalThis;
324
458
  function manifest(options = {}) {
325
459
  const key = options.key || process.env.MNFST_KEY;
@@ -351,6 +485,9 @@ function manifest(options = {}) {
351
485
  installHttp(runtime);
352
486
  globals[STATE] = runtime;
353
487
  runtime.api.hello(`node-${process.versions.node}`);
488
+ process.once("beforeExit", () => {
489
+ if (runtime.tracker.size() > 0) void runtime.tracker.flush(EXIT_FLUSH_MS);
490
+ });
354
491
  }
355
492
 
356
493
  export {
@@ -37,6 +37,19 @@ function safeUrl(raw) {
37
37
  url.search = new URLSearchParams(pairs).toString();
38
38
  return url.toString();
39
39
  }
40
+ function trackedUrl(raw) {
41
+ try {
42
+ const url = new URL(raw);
43
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
44
+ url.username = "";
45
+ url.password = "";
46
+ url.search = "";
47
+ url.hash = "";
48
+ return url.toString();
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
40
53
  function safeHeaders(headers) {
41
54
  return Object.fromEntries([...headers].map(([key, value]) => [
42
55
  key,
@@ -281,7 +294,8 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
281
294
  }
282
295
 
283
296
  // src/api.ts
284
- var VERSION = "7.1.0";
297
+ var VERSION = "7.2.0";
298
+ var MAX_HEALS_IN_FLIGHT = 8;
285
299
  var warn = (message) => process.emitWarning(message, { code: "MNFST" });
286
300
  var HealApi = class {
287
301
  constructor(rawFetch, key, url, timeoutMs = 6e4, reportTimeoutMs = 5e3) {
@@ -303,6 +317,14 @@ var HealApi = class {
303
317
  enabled() {
304
318
  return performance.now() >= this.disabledUntil;
305
319
  }
320
+ /**
321
+ * Whether a heal call would be sent right now: the project is not disabled
322
+ * and a heal slot is free. A healable failure that cannot be sent is tracked
323
+ * instead, so it is never lost from both ledgers.
324
+ */
325
+ canHeal() {
326
+ return this.enabled() && this.inFlight < MAX_HEALS_IN_FLIGHT;
327
+ }
306
328
  headers() {
307
329
  return {
308
330
  authorization: `Bearer ${this.key}`,
@@ -311,7 +333,7 @@ var HealApi = class {
311
333
  };
312
334
  }
313
335
  async heal(capture, signal) {
314
- if (!this.enabled() || this.inFlight >= 8) return null;
336
+ if (!this.canHeal()) return null;
315
337
  this.inFlight++;
316
338
  const controller = new AbortController();
317
339
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -386,6 +408,41 @@ var HealApi = class {
386
408
  }).catch(() => {
387
409
  }).finally(() => clearTimeout(timer));
388
410
  }
411
+ /**
412
+ * Ship one batch of tracked calls to `POST /v1/requests`. Throws only when a
413
+ * retry could help (network error, timeout, 429, 5xx), so the buffer retries
414
+ * once; any other answer, including 404 from a backend that predates the
415
+ * route, drops the batch quietly. Uses `rawFetch`, so the send is never
416
+ * tracked or healed by our own patch.
417
+ */
418
+ async sendRequests(calls, signal) {
419
+ if (!this.enabled() || calls.length === 0) return;
420
+ const controller = new AbortController();
421
+ const timer = setTimeout(() => controller.abort(), this.reportTimeoutMs);
422
+ try {
423
+ const response = await this.rawFetch(new URL("v1/requests", this.url), {
424
+ method: "POST",
425
+ headers: this.headers(),
426
+ body: JSON.stringify({ requests: calls }),
427
+ signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
428
+ redirect: "error"
429
+ });
430
+ if (response.status === 403) {
431
+ const body = await response.json().catch(() => null);
432
+ if (isObject(body) && body.error === "project_disabled") {
433
+ this.disabledUntil = performance.now() + 3e5;
434
+ }
435
+ return;
436
+ }
437
+ void response.body?.cancel().catch(() => {
438
+ });
439
+ if (response.status === 429 || response.status >= 500) {
440
+ throw new Error(`tracked calls refused (${response.status})`);
441
+ }
442
+ } finally {
443
+ clearTimeout(timer);
444
+ }
445
+ }
389
446
  report(id, outcome) {
390
447
  if (!id) return;
391
448
  if (this.pending.size >= 64) {
@@ -425,6 +482,7 @@ var HealApi = class {
425
482
  export {
426
483
  isObject,
427
484
  safeUrl,
485
+ trackedUrl,
428
486
  safeHeaders,
429
487
  travelingBody,
430
488
  mergeBody,
package/dist/index.cjs CHANGED
@@ -81,6 +81,19 @@ function safeUrl(raw) {
81
81
  url.search = new URLSearchParams(pairs).toString();
82
82
  return url.toString();
83
83
  }
84
+ function trackedUrl(raw) {
85
+ try {
86
+ const url = new URL(raw);
87
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
88
+ url.username = "";
89
+ url.password = "";
90
+ url.search = "";
91
+ url.hash = "";
92
+ return url.toString();
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
84
97
  function safeHeaders(headers) {
85
98
  return Object.fromEntries([...headers].map(([key, value]) => [
86
99
  key,
@@ -328,7 +341,8 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
328
341
  var import_node_crypto = require("crypto");
329
342
 
330
343
  // src/api.ts
331
- var VERSION = "7.1.0";
344
+ var VERSION = "7.2.0";
345
+ var MAX_HEALS_IN_FLIGHT = 8;
332
346
  var warn = (message) => process.emitWarning(message, { code: "MNFST" });
333
347
  var HealApi = class {
334
348
  constructor(rawFetch, key, url, timeoutMs = 6e4, reportTimeoutMs = 5e3) {
@@ -350,6 +364,14 @@ var HealApi = class {
350
364
  enabled() {
351
365
  return performance.now() >= this.disabledUntil;
352
366
  }
367
+ /**
368
+ * Whether a heal call would be sent right now: the project is not disabled
369
+ * and a heal slot is free. A healable failure that cannot be sent is tracked
370
+ * instead, so it is never lost from both ledgers.
371
+ */
372
+ canHeal() {
373
+ return this.enabled() && this.inFlight < MAX_HEALS_IN_FLIGHT;
374
+ }
353
375
  headers() {
354
376
  return {
355
377
  authorization: `Bearer ${this.key}`,
@@ -358,7 +380,7 @@ var HealApi = class {
358
380
  };
359
381
  }
360
382
  async heal(capture, signal) {
361
- if (!this.enabled() || this.inFlight >= 8) return null;
383
+ if (!this.canHeal()) return null;
362
384
  this.inFlight++;
363
385
  const controller = new AbortController();
364
386
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -433,6 +455,41 @@ var HealApi = class {
433
455
  }).catch(() => {
434
456
  }).finally(() => clearTimeout(timer));
435
457
  }
458
+ /**
459
+ * Ship one batch of tracked calls to `POST /v1/requests`. Throws only when a
460
+ * retry could help (network error, timeout, 429, 5xx), so the buffer retries
461
+ * once; any other answer, including 404 from a backend that predates the
462
+ * route, drops the batch quietly. Uses `rawFetch`, so the send is never
463
+ * tracked or healed by our own patch.
464
+ */
465
+ async sendRequests(calls, signal) {
466
+ if (!this.enabled() || calls.length === 0) return;
467
+ const controller = new AbortController();
468
+ const timer = setTimeout(() => controller.abort(), this.reportTimeoutMs);
469
+ try {
470
+ const response = await this.rawFetch(new URL("v1/requests", this.url), {
471
+ method: "POST",
472
+ headers: this.headers(),
473
+ body: JSON.stringify({ requests: calls }),
474
+ signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
475
+ redirect: "error"
476
+ });
477
+ if (response.status === 403) {
478
+ const body = await response.json().catch(() => null);
479
+ if (isObject(body) && body.error === "project_disabled") {
480
+ this.disabledUntil = performance.now() + 3e5;
481
+ }
482
+ return;
483
+ }
484
+ void response.body?.cancel().catch(() => {
485
+ });
486
+ if (response.status === 429 || response.status >= 500) {
487
+ throw new Error(`tracked calls refused (${response.status})`);
488
+ }
489
+ } finally {
490
+ clearTimeout(timer);
491
+ }
492
+ }
436
493
  report(id, outcome) {
437
494
  if (!id) return;
438
495
  if (this.pending.size >= 64) {
@@ -469,6 +526,98 @@ var HealApi = class {
469
526
  }
470
527
  };
471
528
 
529
+ // src/tracking.ts
530
+ var MAX_BUFFER = 5e3;
531
+ var MAX_BATCH = 500;
532
+ var CallBuffer = class {
533
+ constructor(send, options = {}) {
534
+ this.send = send;
535
+ this.flushAt = options.flushAt ?? 500;
536
+ this.minGapMs = options.minGapMs ?? 1e3;
537
+ this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
538
+ this.timer.unref();
539
+ }
540
+ send;
541
+ queue = [];
542
+ inFlight = null;
543
+ lastSentAt = -Infinity;
544
+ /** Past this instant, nothing is retried or waited for, and a send in flight is aborted. */
545
+ deadline = Infinity;
546
+ controller = new AbortController();
547
+ timer;
548
+ flushAt;
549
+ minGapMs;
550
+ record(call) {
551
+ if (this.queue.length >= MAX_BUFFER) return;
552
+ this.queue.push(call);
553
+ if (this.queue.length >= this.flushAt) void this.kick();
554
+ }
555
+ size() {
556
+ return this.queue.length;
557
+ }
558
+ /** Resolves when the send in flight, if any, has settled. */
559
+ async idle() {
560
+ await this.inFlight;
561
+ }
562
+ /**
563
+ * Send everything buffered now, one batch at a time through the same
564
+ * in-flight guard as the timer. With `deadlineMs` (the exit path), it gives
565
+ * up at the deadline: the send in flight is aborted, nothing is retried, and
566
+ * what is left is dropped, so a script never hangs on an unreachable server.
567
+ */
568
+ async flush(deadlineMs = Infinity) {
569
+ this.deadline = performance.now() + deadlineMs;
570
+ const abortAt = Number.isFinite(deadlineMs) ? setTimeout(() => this.controller.abort(), deadlineMs) : null;
571
+ try {
572
+ while ((this.queue.length > 0 || this.inFlight) && performance.now() < this.deadline) {
573
+ await (this.inFlight ?? this.start());
574
+ }
575
+ } finally {
576
+ if (abortAt) clearTimeout(abortAt);
577
+ if (this.controller.signal.aborted) this.controller = new AbortController();
578
+ this.deadline = Infinity;
579
+ }
580
+ }
581
+ stop() {
582
+ clearInterval(this.timer);
583
+ }
584
+ kick() {
585
+ if (this.inFlight || this.queue.length === 0) return;
586
+ if (performance.now() - this.lastSentAt < this.minGapMs) return;
587
+ void this.start();
588
+ }
589
+ start() {
590
+ this.inFlight = this.sendOne().finally(() => {
591
+ this.inFlight = null;
592
+ });
593
+ return this.inFlight;
594
+ }
595
+ async sendOne() {
596
+ const batch = this.queue.splice(0, MAX_BATCH);
597
+ for (let attempt = 0; attempt < 2; attempt++) {
598
+ if (attempt > 0 && performance.now() >= this.deadline) return;
599
+ await this.waitForGap();
600
+ this.lastSentAt = performance.now();
601
+ try {
602
+ await this.send(batch, this.controller.signal);
603
+ return;
604
+ } catch {
605
+ }
606
+ if (this.controller.signal.aborted) {
607
+ this.controller = new AbortController();
608
+ return;
609
+ }
610
+ }
611
+ }
612
+ // Referenced on purpose: this wait only happens while a send or an exit
613
+ // flush is under way, and an unref'd wait would let the process exit with
614
+ // the batch unsent. It never outlasts the flush deadline.
615
+ async waitForGap() {
616
+ const wait = Math.min(this.lastSentAt + this.minGapMs, this.deadline) - performance.now();
617
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
618
+ }
619
+ };
620
+
472
621
  // src/runtime.ts
473
622
  var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
474
623
  var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
@@ -479,20 +628,47 @@ var Runtime = class {
479
628
  this.options = options;
480
629
  this.original = original;
481
630
  this.api = api ?? new HealApi(original, options.key, options.url);
631
+ this.tracker = new CallBuffer((batch, signal) => this.api.sendRequests(batch, signal));
482
632
  }
483
633
  options;
484
634
  original;
485
635
  api;
636
+ /** Calls that did not go to `/v1/heal`, any status, on their way to `/v1/requests`. */
637
+ tracker;
638
+ /**
639
+ * Record a call that is not being healed. An in-memory append: never awaited,
640
+ * never throws, reads nothing of the response.
641
+ */
642
+ track(method, url, statusCode, startedAt, responseTimeMs) {
643
+ try {
644
+ const reported = trackedUrl(url());
645
+ const verb = method.toUpperCase();
646
+ if (!reported || reported.length > 4096 || !verb || verb.length > 16 || statusCode < 100 || statusCode > 599) return;
647
+ this.tracker.record({
648
+ traceId: (0, import_node_crypto.randomUUID)(),
649
+ method: verb,
650
+ url: reported,
651
+ statusCode,
652
+ responseTimeMs: Math.round(responseTimeMs),
653
+ occurredAt: new Date(startedAt).toISOString()
654
+ });
655
+ } catch {
656
+ }
657
+ }
486
658
  fetch = async (input, init) => {
487
659
  const request = new Request(input, init);
488
660
  const extras = { ...init };
489
661
  delete extras.body;
490
662
  delete extras.headers;
491
663
  const bodyPromise = captureRequest(request).catch(() => ({ body: null, complete: false }));
664
+ const startedAt = Date.now();
492
665
  const started = performance.now();
493
666
  const response = await this.original(request, extras);
494
667
  const responseTimeMs = performance.now() - started;
495
- if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
668
+ if (!eligible(response.status) || response.redirected || !this.api.canHeal()) {
669
+ this.track(request.method, () => request.url, response.status, startedAt, responseTimeMs);
670
+ return response;
671
+ }
496
672
  return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
497
673
  };
498
674
  async handleResponse(request, response, body, responseTimeMs, extras = {}) {
@@ -622,6 +798,7 @@ function wrapRequest(original, protocol, runtime) {
622
798
  return ((...received) => {
623
799
  const args = [...received];
624
800
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
801
+ const startedAt = Date.now();
625
802
  const started = performance.now();
626
803
  const request = original(...args);
627
804
  const capture = captureBody(request);
@@ -630,7 +807,14 @@ function wrapRequest(original, protocol, runtime) {
630
807
  request.emit = ((event, ...values) => {
631
808
  if (event !== "response") return emit(event, ...values);
632
809
  const response = values[0];
633
- if (!eligible(response.statusCode ?? 0) || !runtime.api.enabled()) {
810
+ if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
811
+ runtime.track(
812
+ request.method,
813
+ () => requestUrl(request, protocol).toString(),
814
+ response.statusCode ?? 0,
815
+ startedAt,
816
+ performance.now() - started
817
+ );
634
818
  return emit(event, ...values);
635
819
  }
636
820
  void handleResponse(runtime, request, response, protocol, capture.body(), signal, started).then((healed) => emit("response", healed)).catch((error) => {
@@ -649,6 +833,10 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
649
833
  const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
650
834
  return incomingResponse(healed, clientRequest);
651
835
  }
836
+ function requestUrl(request, protocol) {
837
+ const authority = String(request.getHeader("host") ?? request.host);
838
+ return new URL(request.path, `${protocol}//${authority}`);
839
+ }
652
840
  function webRequest(request, protocol, captured, signal) {
653
841
  const headers = new Headers();
654
842
  for (const name of request.getHeaderNames()) {
@@ -657,8 +845,7 @@ function webRequest(request, protocol, captured, signal) {
657
845
  if (item !== void 0) headers.append(name, String(item));
658
846
  }
659
847
  }
660
- const authority = String(request.getHeader("host") ?? request.host);
661
- const url = new URL(request.path, `${protocol}//${authority}`);
848
+ const url = requestUrl(request, protocol);
662
849
  const method = request.method;
663
850
  return new Request(url, {
664
851
  method,
@@ -767,6 +954,7 @@ function cancellation(request, external) {
767
954
 
768
955
  // src/index.ts
769
956
  var STATE = /* @__PURE__ */ Symbol.for("mnfst.node.runtime.v1");
957
+ var EXIT_FLUSH_MS = 2e3;
770
958
  var globals = globalThis;
771
959
  function manifest(options = {}) {
772
960
  const key = options.key || process.env.MNFST_KEY;
@@ -798,6 +986,9 @@ function manifest(options = {}) {
798
986
  installHttp(runtime);
799
987
  globals[STATE] = runtime;
800
988
  runtime.api.hello(`node-${process.versions.node}`);
989
+ process.once("beforeExit", () => {
990
+ if (runtime.tracker.size() > 0) void runtime.tracker.flush(EXIT_FLUSH_MS);
991
+ });
801
992
  }
802
993
  // Annotate the CommonJS export names for ESM import in node:
803
994
  0 && (module.exports = {
package/dist/index.d.cts CHANGED
@@ -12,7 +12,7 @@ interface HealEvent {
12
12
  operations?: unknown[];
13
13
  }
14
14
 
15
- declare const VERSION = "7.1.0";
15
+ declare const VERSION = "7.2.0";
16
16
 
17
17
  /** Install once, before libraries capture their own reference to global fetch. */
18
18
  declare function manifest(options?: ManifestOptions): void;
package/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ interface HealEvent {
12
12
  operations?: unknown[];
13
13
  }
14
14
 
15
- declare const VERSION = "7.1.0";
15
+ declare const VERSION = "7.2.0";
16
16
 
17
17
  /** Install once, before libraries capture their own reference to global fetch. */
18
18
  declare function manifest(options?: ManifestOptions): void;
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  manifest
3
- } from "./chunk-4OVLT3PO.js";
3
+ } from "./chunk-GC5H22R2.js";
4
4
  import {
5
5
  VERSION
6
- } from "./chunk-AR23I2QK.js";
6
+ } from "./chunk-NHEF7GUP.js";
7
7
  export {
8
8
  VERSION,
9
9
  manifest
package/dist/register.cjs CHANGED
@@ -68,6 +68,19 @@ function safeUrl(raw) {
68
68
  url.search = new URLSearchParams(pairs).toString();
69
69
  return url.toString();
70
70
  }
71
+ function trackedUrl(raw) {
72
+ try {
73
+ const url = new URL(raw);
74
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
75
+ url.username = "";
76
+ url.password = "";
77
+ url.search = "";
78
+ url.hash = "";
79
+ return url.toString();
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
71
84
  function safeHeaders(headers) {
72
85
  return Object.fromEntries([...headers].map(([key, value]) => [
73
86
  key,
@@ -315,7 +328,8 @@ async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAP
315
328
  var import_node_crypto = require("crypto");
316
329
 
317
330
  // src/api.ts
318
- var VERSION = "7.1.0";
331
+ var VERSION = "7.2.0";
332
+ var MAX_HEALS_IN_FLIGHT = 8;
319
333
  var warn = (message) => process.emitWarning(message, { code: "MNFST" });
320
334
  var HealApi = class {
321
335
  constructor(rawFetch, key, url, timeoutMs = 6e4, reportTimeoutMs = 5e3) {
@@ -337,6 +351,14 @@ var HealApi = class {
337
351
  enabled() {
338
352
  return performance.now() >= this.disabledUntil;
339
353
  }
354
+ /**
355
+ * Whether a heal call would be sent right now: the project is not disabled
356
+ * and a heal slot is free. A healable failure that cannot be sent is tracked
357
+ * instead, so it is never lost from both ledgers.
358
+ */
359
+ canHeal() {
360
+ return this.enabled() && this.inFlight < MAX_HEALS_IN_FLIGHT;
361
+ }
340
362
  headers() {
341
363
  return {
342
364
  authorization: `Bearer ${this.key}`,
@@ -345,7 +367,7 @@ var HealApi = class {
345
367
  };
346
368
  }
347
369
  async heal(capture, signal) {
348
- if (!this.enabled() || this.inFlight >= 8) return null;
370
+ if (!this.canHeal()) return null;
349
371
  this.inFlight++;
350
372
  const controller = new AbortController();
351
373
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
@@ -420,6 +442,41 @@ var HealApi = class {
420
442
  }).catch(() => {
421
443
  }).finally(() => clearTimeout(timer));
422
444
  }
445
+ /**
446
+ * Ship one batch of tracked calls to `POST /v1/requests`. Throws only when a
447
+ * retry could help (network error, timeout, 429, 5xx), so the buffer retries
448
+ * once; any other answer, including 404 from a backend that predates the
449
+ * route, drops the batch quietly. Uses `rawFetch`, so the send is never
450
+ * tracked or healed by our own patch.
451
+ */
452
+ async sendRequests(calls, signal) {
453
+ if (!this.enabled() || calls.length === 0) return;
454
+ const controller = new AbortController();
455
+ const timer = setTimeout(() => controller.abort(), this.reportTimeoutMs);
456
+ try {
457
+ const response = await this.rawFetch(new URL("v1/requests", this.url), {
458
+ method: "POST",
459
+ headers: this.headers(),
460
+ body: JSON.stringify({ requests: calls }),
461
+ signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
462
+ redirect: "error"
463
+ });
464
+ if (response.status === 403) {
465
+ const body = await response.json().catch(() => null);
466
+ if (isObject(body) && body.error === "project_disabled") {
467
+ this.disabledUntil = performance.now() + 3e5;
468
+ }
469
+ return;
470
+ }
471
+ void response.body?.cancel().catch(() => {
472
+ });
473
+ if (response.status === 429 || response.status >= 500) {
474
+ throw new Error(`tracked calls refused (${response.status})`);
475
+ }
476
+ } finally {
477
+ clearTimeout(timer);
478
+ }
479
+ }
423
480
  report(id, outcome) {
424
481
  if (!id) return;
425
482
  if (this.pending.size >= 64) {
@@ -456,6 +513,98 @@ var HealApi = class {
456
513
  }
457
514
  };
458
515
 
516
+ // src/tracking.ts
517
+ var MAX_BUFFER = 5e3;
518
+ var MAX_BATCH = 500;
519
+ var CallBuffer = class {
520
+ constructor(send, options = {}) {
521
+ this.send = send;
522
+ this.flushAt = options.flushAt ?? 500;
523
+ this.minGapMs = options.minGapMs ?? 1e3;
524
+ this.timer = setInterval(() => void this.kick(), options.intervalMs ?? 5e3);
525
+ this.timer.unref();
526
+ }
527
+ send;
528
+ queue = [];
529
+ inFlight = null;
530
+ lastSentAt = -Infinity;
531
+ /** Past this instant, nothing is retried or waited for, and a send in flight is aborted. */
532
+ deadline = Infinity;
533
+ controller = new AbortController();
534
+ timer;
535
+ flushAt;
536
+ minGapMs;
537
+ record(call) {
538
+ if (this.queue.length >= MAX_BUFFER) return;
539
+ this.queue.push(call);
540
+ if (this.queue.length >= this.flushAt) void this.kick();
541
+ }
542
+ size() {
543
+ return this.queue.length;
544
+ }
545
+ /** Resolves when the send in flight, if any, has settled. */
546
+ async idle() {
547
+ await this.inFlight;
548
+ }
549
+ /**
550
+ * Send everything buffered now, one batch at a time through the same
551
+ * in-flight guard as the timer. With `deadlineMs` (the exit path), it gives
552
+ * up at the deadline: the send in flight is aborted, nothing is retried, and
553
+ * what is left is dropped, so a script never hangs on an unreachable server.
554
+ */
555
+ async flush(deadlineMs = Infinity) {
556
+ this.deadline = performance.now() + deadlineMs;
557
+ const abortAt = Number.isFinite(deadlineMs) ? setTimeout(() => this.controller.abort(), deadlineMs) : null;
558
+ try {
559
+ while ((this.queue.length > 0 || this.inFlight) && performance.now() < this.deadline) {
560
+ await (this.inFlight ?? this.start());
561
+ }
562
+ } finally {
563
+ if (abortAt) clearTimeout(abortAt);
564
+ if (this.controller.signal.aborted) this.controller = new AbortController();
565
+ this.deadline = Infinity;
566
+ }
567
+ }
568
+ stop() {
569
+ clearInterval(this.timer);
570
+ }
571
+ kick() {
572
+ if (this.inFlight || this.queue.length === 0) return;
573
+ if (performance.now() - this.lastSentAt < this.minGapMs) return;
574
+ void this.start();
575
+ }
576
+ start() {
577
+ this.inFlight = this.sendOne().finally(() => {
578
+ this.inFlight = null;
579
+ });
580
+ return this.inFlight;
581
+ }
582
+ async sendOne() {
583
+ const batch = this.queue.splice(0, MAX_BATCH);
584
+ for (let attempt = 0; attempt < 2; attempt++) {
585
+ if (attempt > 0 && performance.now() >= this.deadline) return;
586
+ await this.waitForGap();
587
+ this.lastSentAt = performance.now();
588
+ try {
589
+ await this.send(batch, this.controller.signal);
590
+ return;
591
+ } catch {
592
+ }
593
+ if (this.controller.signal.aborted) {
594
+ this.controller = new AbortController();
595
+ return;
596
+ }
597
+ }
598
+ }
599
+ // Referenced on purpose: this wait only happens while a send or an exit
600
+ // flush is under way, and an unref'd wait would let the process exit with
601
+ // the batch unsent. It never outlasts the flush deadline.
602
+ async waitForGap() {
603
+ const wait = Math.min(this.lastSentAt + this.minGapMs, this.deadline) - performance.now();
604
+ if (wait > 0) await new Promise((resolve) => setTimeout(resolve, wait));
605
+ }
606
+ };
607
+
459
608
  // src/runtime.ts
460
609
  var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
461
610
  var bodyless = ["GET", "HEAD", "DELETE", "OPTIONS"];
@@ -466,20 +615,47 @@ var Runtime = class {
466
615
  this.options = options;
467
616
  this.original = original;
468
617
  this.api = api ?? new HealApi(original, options.key, options.url);
618
+ this.tracker = new CallBuffer((batch, signal) => this.api.sendRequests(batch, signal));
469
619
  }
470
620
  options;
471
621
  original;
472
622
  api;
623
+ /** Calls that did not go to `/v1/heal`, any status, on their way to `/v1/requests`. */
624
+ tracker;
625
+ /**
626
+ * Record a call that is not being healed. An in-memory append: never awaited,
627
+ * never throws, reads nothing of the response.
628
+ */
629
+ track(method, url, statusCode, startedAt, responseTimeMs) {
630
+ try {
631
+ const reported = trackedUrl(url());
632
+ const verb = method.toUpperCase();
633
+ if (!reported || reported.length > 4096 || !verb || verb.length > 16 || statusCode < 100 || statusCode > 599) return;
634
+ this.tracker.record({
635
+ traceId: (0, import_node_crypto.randomUUID)(),
636
+ method: verb,
637
+ url: reported,
638
+ statusCode,
639
+ responseTimeMs: Math.round(responseTimeMs),
640
+ occurredAt: new Date(startedAt).toISOString()
641
+ });
642
+ } catch {
643
+ }
644
+ }
473
645
  fetch = async (input, init) => {
474
646
  const request = new Request(input, init);
475
647
  const extras = { ...init };
476
648
  delete extras.body;
477
649
  delete extras.headers;
478
650
  const bodyPromise = captureRequest(request).catch(() => ({ body: null, complete: false }));
651
+ const startedAt = Date.now();
479
652
  const started = performance.now();
480
653
  const response = await this.original(request, extras);
481
654
  const responseTimeMs = performance.now() - started;
482
- if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
655
+ if (!eligible(response.status) || response.redirected || !this.api.canHeal()) {
656
+ this.track(request.method, () => request.url, response.status, startedAt, responseTimeMs);
657
+ return response;
658
+ }
483
659
  return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
484
660
  };
485
661
  async handleResponse(request, response, body, responseTimeMs, extras = {}) {
@@ -609,6 +785,7 @@ function wrapRequest(original, protocol, runtime) {
609
785
  return ((...received) => {
610
786
  const args = [...received];
611
787
  const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
788
+ const startedAt = Date.now();
612
789
  const started = performance.now();
613
790
  const request = original(...args);
614
791
  const capture = captureBody(request);
@@ -617,7 +794,14 @@ function wrapRequest(original, protocol, runtime) {
617
794
  request.emit = ((event, ...values) => {
618
795
  if (event !== "response") return emit(event, ...values);
619
796
  const response = values[0];
620
- if (!eligible(response.statusCode ?? 0) || !runtime.api.enabled()) {
797
+ if (!eligible(response.statusCode ?? 0) || !runtime.api.canHeal()) {
798
+ runtime.track(
799
+ request.method,
800
+ () => requestUrl(request, protocol).toString(),
801
+ response.statusCode ?? 0,
802
+ startedAt,
803
+ performance.now() - started
804
+ );
621
805
  return emit(event, ...values);
622
806
  }
623
807
  void handleResponse(runtime, request, response, protocol, capture.body(), signal, started).then((healed) => emit("response", healed)).catch((error) => {
@@ -636,6 +820,10 @@ async function handleResponse(runtime, clientRequest, incoming, protocol, body,
636
820
  const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
637
821
  return incomingResponse(healed, clientRequest);
638
822
  }
823
+ function requestUrl(request, protocol) {
824
+ const authority = String(request.getHeader("host") ?? request.host);
825
+ return new URL(request.path, `${protocol}//${authority}`);
826
+ }
639
827
  function webRequest(request, protocol, captured, signal) {
640
828
  const headers = new Headers();
641
829
  for (const name of request.getHeaderNames()) {
@@ -644,8 +832,7 @@ function webRequest(request, protocol, captured, signal) {
644
832
  if (item !== void 0) headers.append(name, String(item));
645
833
  }
646
834
  }
647
- const authority = String(request.getHeader("host") ?? request.host);
648
- const url = new URL(request.path, `${protocol}//${authority}`);
835
+ const url = requestUrl(request, protocol);
649
836
  const method = request.method;
650
837
  return new Request(url, {
651
838
  method,
@@ -754,6 +941,7 @@ function cancellation(request, external) {
754
941
 
755
942
  // src/index.ts
756
943
  var STATE = /* @__PURE__ */ Symbol.for("mnfst.node.runtime.v1");
944
+ var EXIT_FLUSH_MS = 2e3;
757
945
  var globals = globalThis;
758
946
  function manifest(options = {}) {
759
947
  const key = options.key || process.env.MNFST_KEY;
@@ -785,6 +973,9 @@ function manifest(options = {}) {
785
973
  installHttp(runtime);
786
974
  globals[STATE] = runtime;
787
975
  runtime.api.hello(`node-${process.versions.node}`);
976
+ process.once("beforeExit", () => {
977
+ if (runtime.tracker.size() > 0) void runtime.tracker.flush(EXIT_FLUSH_MS);
978
+ });
788
979
  }
789
980
 
790
981
  // src/register.ts
package/dist/register.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  manifest
3
- } from "./chunk-4OVLT3PO.js";
4
- import "./chunk-AR23I2QK.js";
3
+ } from "./chunk-GC5H22R2.js";
4
+ import "./chunk-NHEF7GUP.js";
5
5
 
6
6
  // src/register.ts
7
7
  manifest();
package/docs/guide.md CHANGED
@@ -105,6 +105,7 @@ You can also verify by hand: send a JSON request that your test API rejects with
105
105
  - JSON and `application/x-www-form-urlencoded` APIs, including nested form fields. No provider-specific request format is required.
106
106
  - One retry per capture. Same-origin URL and header repairs are supported by the SDK; the current app returns structured body repairs.
107
107
  - Successful calls and successful retries remain streamed. Failed responses retain their bytes, status, headers, URL and redirect metadata.
108
+ - 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").
108
109
 
109
110
  **Not covered:** browser JavaScript, HTTP/2, directly imported `undici.fetch`, and `fetch` references saved before initialization (see `manifest/register` above). Those transports need separate integration. This SDK does not claim to intercept every Node HTTP client.
110
111
 
@@ -123,7 +124,9 @@ Outcome reports are best effort, limited to 64 concurrent requests with five-sec
123
124
 
124
125
  ## Data sent to Manifest
125
126
 
126
- Failed URLs, request headers, JSON or form-urlencoded bodies, and raw error responses go to the configured server. Known credential names in query parameters and headers are masked; credential-named top-level request body fields are withheld and restored on retry. Exception prose is not sent for transport failures.
127
+ **Every call (metadata only).** For each call that is not healed, whatever its status, the SDK sends its method, URL without the query string, userinfo or fragment, status code, response time and time of the call. No headers and no bodies. Calls are batched and sent in the background, at most once per second; recording one never slows the call. Calls still buffered when a serverless runtime freezes the process can be lost.
128
+
129
+ **Healable failures (full capture).** Failed URLs, request headers, JSON or form-urlencoded bodies, and raw error responses go to the configured server. Known credential names in query parameters and headers are masked; credential-named top-level request body fields are withheld and restored on retry. Exception prose is not sent for transport failures.
127
130
 
128
131
  This is not general secret detection: nested fields, arbitrary secret names, business data and response bodies may contain sensitive information. Enable it only for traffic you permit Manifest to process and store. The SDK makes the actual retry locally.
129
132
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "manifest",
3
- "version": "7.1.0",
3
+ "version": "7.2.0",
4
4
  "description": "Repair eligible failed API calls made by Node HTTP clients.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -35,7 +35,9 @@
35
35
  "dist",
36
36
  "README.md",
37
37
  "CONTRACT.md",
38
- "docs"
38
+ "docs",
39
+ "!docs/github-sdk.png",
40
+ "!docs/sdk-flow-diagram.svg"
39
41
  ],
40
42
  "engines": {
41
43
  "node": ">=22"
Binary file