manifest 6.0.0 → 7.0.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 +2 -2
- package/README.md +28 -51
- package/dist/chunk-DP2C4E42.js +720 -0
- package/dist/index.cjs +311 -26
- package/dist/index.d.cts +2 -6
- package/dist/index.d.ts +2 -6
- package/dist/index.js +4 -439
- package/dist/register.cjs +742 -0
- package/dist/register.d.cts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +6 -0
- package/docs/guide.md +36 -19
- package/docs/healing-diagram.svg +549 -0
- package/package.json +17 -3
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
import http from "http";
|
|
3
|
+
import https from "https";
|
|
4
|
+
import { syncBuiltinESMExports } from "module";
|
|
5
|
+
import { Readable } from "stream";
|
|
6
|
+
import { createBrotliDecompress, createUnzip } from "zlib";
|
|
7
|
+
|
|
8
|
+
// src/wire.ts
|
|
9
|
+
var secretNames = /* @__PURE__ */ new Set([
|
|
10
|
+
"api_key",
|
|
11
|
+
"apikey",
|
|
12
|
+
"api_token",
|
|
13
|
+
"key",
|
|
14
|
+
"token",
|
|
15
|
+
"access_token",
|
|
16
|
+
"refresh_token",
|
|
17
|
+
"auth",
|
|
18
|
+
"authorization",
|
|
19
|
+
"signature",
|
|
20
|
+
"sig",
|
|
21
|
+
"secret",
|
|
22
|
+
"client_secret",
|
|
23
|
+
"password",
|
|
24
|
+
"session",
|
|
25
|
+
"session_id",
|
|
26
|
+
"bearer",
|
|
27
|
+
"jwt",
|
|
28
|
+
"id_token",
|
|
29
|
+
"auth_token",
|
|
30
|
+
"pwd",
|
|
31
|
+
"passwd",
|
|
32
|
+
"private_key"
|
|
33
|
+
]);
|
|
34
|
+
var roots = ["auth", "key", "token", "secret", "session", "password", "passwd", "cookie", "signature", "credential", "bearer", "jwt"];
|
|
35
|
+
var normalize = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replaceAll("-", "_").toLowerCase().replace(/^x_/, "");
|
|
36
|
+
var isSecret = (name) => secretNames.has(normalize(name));
|
|
37
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
function safeUrl(raw) {
|
|
39
|
+
const url = new URL(raw);
|
|
40
|
+
url.username = "";
|
|
41
|
+
url.password = "";
|
|
42
|
+
url.hash = "";
|
|
43
|
+
const pairs = [...url.searchParams].map(([key, value]) => [key, isSecret(key) ? "REDACTED" : value]);
|
|
44
|
+
url.search = new URLSearchParams(pairs).toString();
|
|
45
|
+
return url.toString();
|
|
46
|
+
}
|
|
47
|
+
function safeHeaders(headers) {
|
|
48
|
+
return Object.fromEntries([...headers].map(([key, value]) => [
|
|
49
|
+
key,
|
|
50
|
+
isSecret(key) || roots.some((root) => normalize(key).includes(root)) ? "REDACTED" : value.slice(0, 1024)
|
|
51
|
+
]));
|
|
52
|
+
}
|
|
53
|
+
function travelingBody(body) {
|
|
54
|
+
return isObject(body) ? Object.fromEntries(Object.entries(body).filter(([key]) => !isSecret(key))) : body;
|
|
55
|
+
}
|
|
56
|
+
function mergeBody(original, healed) {
|
|
57
|
+
if (!isObject(original) || !isObject(healed)) return healed;
|
|
58
|
+
return { ...Object.fromEntries(Object.entries(original).filter(([key]) => isSecret(key) && !Object.hasOwn(healed, key))), ...healed };
|
|
59
|
+
}
|
|
60
|
+
function boundedJson(value) {
|
|
61
|
+
const stack = [[value, 0]];
|
|
62
|
+
let count = 0;
|
|
63
|
+
while (stack.length) {
|
|
64
|
+
const [item, depth] = stack.pop();
|
|
65
|
+
if (++count > 1e5 || depth > 64) return false;
|
|
66
|
+
if (item !== null && typeof item === "object") {
|
|
67
|
+
for (const child of Object.values(item)) stack.push([child, depth + 1]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
function parseJson(bytes) {
|
|
73
|
+
try {
|
|
74
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
75
|
+
return boundedJson(value) ? value : null;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
var unsafeFormKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
81
|
+
var arrayIndex = (value) => /^(0|[1-9]\d*)$/.test(value) && Number(value) <= 1e5;
|
|
82
|
+
var formEncoded = (contentType) => contentType?.split(";", 1)[0]?.trim().toLowerCase() === "application/x-www-form-urlencoded";
|
|
83
|
+
function formPath(key) {
|
|
84
|
+
const bracket = key.indexOf("[");
|
|
85
|
+
const root = bracket === -1 ? key : key.slice(0, bracket);
|
|
86
|
+
if (!root || root.includes("]")) return null;
|
|
87
|
+
const path = [root];
|
|
88
|
+
let offset = root.length;
|
|
89
|
+
while (offset < key.length) {
|
|
90
|
+
if (key[offset] !== "[") return null;
|
|
91
|
+
const end = key.indexOf("]", offset + 1);
|
|
92
|
+
if (end === -1) return null;
|
|
93
|
+
const part = key.slice(offset + 1, end);
|
|
94
|
+
if (part.includes("[")) return null;
|
|
95
|
+
path.push(part);
|
|
96
|
+
offset = end + 1;
|
|
97
|
+
}
|
|
98
|
+
return path.length <= 64 && path.every((part) => !unsafeFormKeys.has(part)) ? path : null;
|
|
99
|
+
}
|
|
100
|
+
function putFormValue(root, path, value) {
|
|
101
|
+
let current = root;
|
|
102
|
+
for (let index = 0; index < path.length; index++) {
|
|
103
|
+
const part = path[index];
|
|
104
|
+
const last = index === path.length - 1;
|
|
105
|
+
if (Array.isArray(current)) {
|
|
106
|
+
if (part !== "" && !arrayIndex(part)) return false;
|
|
107
|
+
const position = part === "" ? current.length : Number(part);
|
|
108
|
+
if (last) {
|
|
109
|
+
const existing3 = Object.hasOwn(current, position) ? current[position] : void 0;
|
|
110
|
+
if (existing3 === void 0) current[position] = value;
|
|
111
|
+
else if (typeof existing3 === "string") current[position] = [existing3, value];
|
|
112
|
+
else if (Array.isArray(existing3) && existing3.every((item) => typeof item === "string")) existing3.push(value);
|
|
113
|
+
else return false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const next2 = path[index + 1];
|
|
117
|
+
const needsArray2 = next2 === "" || arrayIndex(next2);
|
|
118
|
+
const existing2 = Object.hasOwn(current, position) ? current[position] : void 0;
|
|
119
|
+
if (existing2 === void 0) current[position] = needsArray2 ? [] : {};
|
|
120
|
+
else if (typeof existing2 !== "object" || existing2 === null || Array.isArray(existing2) !== needsArray2) return false;
|
|
121
|
+
current = current[position];
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!part) return false;
|
|
125
|
+
if (last) {
|
|
126
|
+
const existing2 = Object.hasOwn(current, part) ? current[part] : void 0;
|
|
127
|
+
if (existing2 === void 0) current[part] = value;
|
|
128
|
+
else if (typeof existing2 === "string") current[part] = [existing2, value];
|
|
129
|
+
else if (Array.isArray(existing2) && existing2.every((item) => typeof item === "string")) existing2.push(value);
|
|
130
|
+
else return false;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const next = path[index + 1];
|
|
134
|
+
const needsArray = next === "" || arrayIndex(next);
|
|
135
|
+
const existing = Object.hasOwn(current, part) ? current[part] : void 0;
|
|
136
|
+
if (existing === void 0) current[part] = needsArray ? [] : {};
|
|
137
|
+
else if (typeof existing !== "object" || existing === null || Array.isArray(existing) !== needsArray) return false;
|
|
138
|
+
current = current[part];
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
function parseForm(bytes) {
|
|
143
|
+
try {
|
|
144
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
145
|
+
const body = {};
|
|
146
|
+
let fields = 0;
|
|
147
|
+
for (const pair of text.split("&")) {
|
|
148
|
+
if (!pair) continue;
|
|
149
|
+
if (++fields > 1e5) return { body: null, valid: false };
|
|
150
|
+
const separator = pair.indexOf("=");
|
|
151
|
+
const rawKey = separator === -1 ? pair : pair.slice(0, separator);
|
|
152
|
+
const rawValue = separator === -1 ? "" : pair.slice(separator + 1);
|
|
153
|
+
const key = decodeURIComponent(rawKey.replaceAll("+", " "));
|
|
154
|
+
const value = decodeURIComponent(rawValue.replaceAll("+", " "));
|
|
155
|
+
const path = formPath(key);
|
|
156
|
+
if (!path || !putFormValue(body, path, value)) return { body: null, valid: false };
|
|
157
|
+
}
|
|
158
|
+
return boundedJson(body) ? { body, valid: true } : { body: null, valid: false };
|
|
159
|
+
} catch {
|
|
160
|
+
return { body: null, valid: false };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function parseRequestBody(bytes, contentType) {
|
|
164
|
+
return formEncoded(contentType) ? parseForm(bytes) : { body: parseJson(bytes), valid: true };
|
|
165
|
+
}
|
|
166
|
+
function serializeRequestBody(body, contentType) {
|
|
167
|
+
if (!formEncoded(contentType)) return JSON.stringify(body);
|
|
168
|
+
if (!isObject(body)) throw new TypeError("Form body must be an object");
|
|
169
|
+
const params = new URLSearchParams();
|
|
170
|
+
let fields = 0;
|
|
171
|
+
const append = (value, key, depth) => {
|
|
172
|
+
if (++fields > 1e5 || depth > 64) throw new TypeError("Form body exceeds structural limits");
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
for (let index = 0; index < value.length; index++) {
|
|
175
|
+
if (Object.hasOwn(value, index)) append(value[index], `${key}[${index}]`, depth + 1);
|
|
176
|
+
}
|
|
177
|
+
} else if (isObject(value)) {
|
|
178
|
+
for (const [name, child] of Object.entries(value)) {
|
|
179
|
+
if (unsafeFormKeys.has(name)) throw new TypeError("Form body contains an unsafe key");
|
|
180
|
+
append(child, key ? `${key}[${name}]` : name, depth + 1);
|
|
181
|
+
}
|
|
182
|
+
} else if (value === null) params.append(key, "");
|
|
183
|
+
else if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) params.append(key, String(value));
|
|
184
|
+
else throw new TypeError("Form body contains an unsupported value");
|
|
185
|
+
};
|
|
186
|
+
for (const [key, value] of Object.entries(body)) {
|
|
187
|
+
if (unsafeFormKeys.has(key)) throw new TypeError("Form body contains an unsafe key");
|
|
188
|
+
append(value, key, 1);
|
|
189
|
+
}
|
|
190
|
+
return params.toString();
|
|
191
|
+
}
|
|
192
|
+
function errorBody(bytes) {
|
|
193
|
+
const parsed = parseJson(bytes);
|
|
194
|
+
const text = Buffer.from(bytes).toString("utf8");
|
|
195
|
+
return parsed ?? new TextDecoder().decode(Buffer.from(text).subarray(0, 65536), { stream: true });
|
|
196
|
+
}
|
|
197
|
+
var TRANSPORT_ERROR = "Upstream retry failed before an HTTP response";
|
|
198
|
+
|
|
199
|
+
// src/capture.ts
|
|
200
|
+
var REQUEST_LIMIT = 262144;
|
|
201
|
+
var RESPONSE_LIMIT = 65536;
|
|
202
|
+
var CAPTURE_MS = 1e3;
|
|
203
|
+
async function prefix(reader, limit, timeoutMs) {
|
|
204
|
+
const chunks = [];
|
|
205
|
+
let size = 0;
|
|
206
|
+
let pending;
|
|
207
|
+
let timer;
|
|
208
|
+
const deadline = new Promise((resolve) => {
|
|
209
|
+
timer = setTimeout(() => resolve(null), timeoutMs);
|
|
210
|
+
});
|
|
211
|
+
let complete = false;
|
|
212
|
+
try {
|
|
213
|
+
while (size <= limit) {
|
|
214
|
+
pending = reader.read();
|
|
215
|
+
const read = await Promise.race([pending, deadline]);
|
|
216
|
+
if (read === null) break;
|
|
217
|
+
pending = void 0;
|
|
218
|
+
if (read.done) {
|
|
219
|
+
complete = true;
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
chunks.push(read.value);
|
|
223
|
+
size += read.value.byteLength;
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
} finally {
|
|
227
|
+
clearTimeout(timer);
|
|
228
|
+
}
|
|
229
|
+
return { chunks, complete, pending, bytes: Buffer.concat(chunks, size).subarray(0, limit) };
|
|
230
|
+
}
|
|
231
|
+
async function captureRequest(request) {
|
|
232
|
+
if (!request.body) return { body: null, complete: true };
|
|
233
|
+
const clone = request.clone();
|
|
234
|
+
const reader = clone.body.getReader();
|
|
235
|
+
try {
|
|
236
|
+
const result = await prefix(reader, REQUEST_LIMIT, CAPTURE_MS);
|
|
237
|
+
if (!result.complete) return { body: null, complete: false };
|
|
238
|
+
const parsed = parseRequestBody(result.bytes, request.headers.get("content-type"));
|
|
239
|
+
return { body: parsed.body, complete: parsed.valid };
|
|
240
|
+
} finally {
|
|
241
|
+
void reader.cancel().catch(() => {
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function metadata(response, original) {
|
|
246
|
+
const clone = response.clone.bind(response);
|
|
247
|
+
Object.defineProperties(response, {
|
|
248
|
+
url: { value: original.url },
|
|
249
|
+
redirected: { value: original.redirected },
|
|
250
|
+
type: { value: original.type },
|
|
251
|
+
clone: { value: () => metadata(clone(), original) }
|
|
252
|
+
});
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
async function captureResponse(original, limit = RESPONSE_LIMIT, timeoutMs = CAPTURE_MS) {
|
|
256
|
+
if (!original.body) return { response: original, body: null, truncated: false, complete: true };
|
|
257
|
+
const reader = original.body.getReader();
|
|
258
|
+
const captured = await prefix(reader, limit, timeoutMs);
|
|
259
|
+
let index = 0;
|
|
260
|
+
let pending = captured.pending;
|
|
261
|
+
const stream = new ReadableStream({
|
|
262
|
+
async pull(controller) {
|
|
263
|
+
try {
|
|
264
|
+
if (index < captured.chunks.length) {
|
|
265
|
+
controller.enqueue(captured.chunks[index++]);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const read = await (pending ?? reader.read());
|
|
269
|
+
pending = void 0;
|
|
270
|
+
if (read.done) {
|
|
271
|
+
reader.releaseLock();
|
|
272
|
+
controller.close();
|
|
273
|
+
} else controller.enqueue(read.value);
|
|
274
|
+
} catch (error) {
|
|
275
|
+
controller.error(error);
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async cancel(reason) {
|
|
279
|
+
await reader.cancel(reason);
|
|
280
|
+
}
|
|
281
|
+
}, { highWaterMark: 0 });
|
|
282
|
+
const response = metadata(new Response(stream, {
|
|
283
|
+
status: original.status,
|
|
284
|
+
statusText: original.statusText,
|
|
285
|
+
headers: original.headers
|
|
286
|
+
}), original);
|
|
287
|
+
return { response, body: errorBody(captured.bytes), truncated: !captured.complete || Buffer.byteLength(Buffer.from(captured.bytes).toString("utf8")) > limit, complete: captured.complete };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/runtime.ts
|
|
291
|
+
import { randomUUID } from "crypto";
|
|
292
|
+
|
|
293
|
+
// src/api.ts
|
|
294
|
+
var VERSION = "7.0.0";
|
|
295
|
+
var warn = (message) => process.emitWarning(message, { code: "MNFST" });
|
|
296
|
+
var HealApi = class {
|
|
297
|
+
constructor(rawFetch, key, url, timeoutMs = 6e4, reportTimeoutMs = 5e3) {
|
|
298
|
+
this.rawFetch = rawFetch;
|
|
299
|
+
this.key = key;
|
|
300
|
+
this.url = url;
|
|
301
|
+
this.timeoutMs = timeoutMs;
|
|
302
|
+
this.reportTimeoutMs = reportTimeoutMs;
|
|
303
|
+
}
|
|
304
|
+
rawFetch;
|
|
305
|
+
key;
|
|
306
|
+
url;
|
|
307
|
+
timeoutMs;
|
|
308
|
+
reportTimeoutMs;
|
|
309
|
+
disabledUntil = 0;
|
|
310
|
+
inFlight = 0;
|
|
311
|
+
/** @internal In-flight outcome reports; exposed so tests can await delivery. */
|
|
312
|
+
pending = /* @__PURE__ */ new Set();
|
|
313
|
+
enabled() {
|
|
314
|
+
return performance.now() >= this.disabledUntil;
|
|
315
|
+
}
|
|
316
|
+
headers() {
|
|
317
|
+
return {
|
|
318
|
+
authorization: `Bearer ${this.key}`,
|
|
319
|
+
"content-type": "application/json",
|
|
320
|
+
"user-agent": `mnfst-node/${VERSION}`,
|
|
321
|
+
"x-mnfst-source": "node-sdk"
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async heal(capture, signal) {
|
|
325
|
+
if (!this.enabled() || this.inFlight >= 8) return null;
|
|
326
|
+
this.inFlight++;
|
|
327
|
+
const controller = new AbortController();
|
|
328
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
329
|
+
try {
|
|
330
|
+
const response = await this.rawFetch(new URL("v1/heal", this.url), {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: this.headers(),
|
|
333
|
+
body: JSON.stringify(capture),
|
|
334
|
+
signal: AbortSignal.any([signal, controller.signal]),
|
|
335
|
+
redirect: "error"
|
|
336
|
+
});
|
|
337
|
+
if (response.status !== 200 && response.status !== 403) {
|
|
338
|
+
void response.body?.cancel().catch(() => {
|
|
339
|
+
});
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const captured = await captureResponse(response, 1048576, this.timeoutMs);
|
|
343
|
+
void captured.response.body?.cancel().catch(() => {
|
|
344
|
+
});
|
|
345
|
+
if (!captured.complete || !isObject(captured.body) || !boundedJson(captured.body)) return null;
|
|
346
|
+
if (response.status === 403 && captured.body.error === "project_disabled") {
|
|
347
|
+
this.disabledUntil = performance.now() + 3e5;
|
|
348
|
+
}
|
|
349
|
+
if (response.status !== 200 || typeof captured.body.status !== "string") return null;
|
|
350
|
+
return {
|
|
351
|
+
status: captured.body.status,
|
|
352
|
+
...typeof captured.body.healAttemptId === "string" ? { healAttemptId: captured.body.healAttemptId } : {},
|
|
353
|
+
...Array.isArray(captured.body.operations) ? { operations: captured.body.operations } : {},
|
|
354
|
+
...isObject(captured.body.healedRequest) ? { healedRequest: captured.body.healedRequest } : {}
|
|
355
|
+
};
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
} finally {
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
this.inFlight--;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
report(id, outcome) {
|
|
364
|
+
if (!id) return;
|
|
365
|
+
if (this.pending.size >= 64) {
|
|
366
|
+
warn("Outcome report capacity reached; report dropped");
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const promise = this.send(id, outcome);
|
|
370
|
+
this.pending.add(promise);
|
|
371
|
+
void promise.then(() => this.pending.delete(promise));
|
|
372
|
+
}
|
|
373
|
+
async send(id, outcome) {
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
const timer = setTimeout(() => controller.abort(), this.reportTimeoutMs);
|
|
376
|
+
try {
|
|
377
|
+
const response = await this.rawFetch(new URL(`v1/heal-attempts/${encodeURIComponent(id)}`, this.url), {
|
|
378
|
+
method: "PATCH",
|
|
379
|
+
headers: this.headers(),
|
|
380
|
+
body: JSON.stringify(outcome),
|
|
381
|
+
signal: controller.signal,
|
|
382
|
+
redirect: "error"
|
|
383
|
+
});
|
|
384
|
+
void response.body?.cancel().catch(() => {
|
|
385
|
+
});
|
|
386
|
+
if (!response.ok) warn("Outcome report rejected; attempt remains unconfirmed");
|
|
387
|
+
} catch {
|
|
388
|
+
warn("Outcome report failed; attempt remains unconfirmed");
|
|
389
|
+
} finally {
|
|
390
|
+
clearTimeout(timer);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/runtime.ts
|
|
396
|
+
var forbidden = /* @__PURE__ */ new Set([401, 402, 403, 429]);
|
|
397
|
+
var eligible = (status) => status >= 400 && status < 500 && !forbidden.has(status);
|
|
398
|
+
var Runtime = class {
|
|
399
|
+
constructor(options, original, api) {
|
|
400
|
+
this.options = options;
|
|
401
|
+
this.original = original;
|
|
402
|
+
this.api = api ?? new HealApi(original, options.key, options.url);
|
|
403
|
+
}
|
|
404
|
+
options;
|
|
405
|
+
original;
|
|
406
|
+
api;
|
|
407
|
+
fetch = async (input, init) => {
|
|
408
|
+
const request = new Request(input, init);
|
|
409
|
+
const extras = { ...init };
|
|
410
|
+
delete extras.body;
|
|
411
|
+
delete extras.headers;
|
|
412
|
+
const bodyPromise = captureRequest(request).catch(() => ({ body: null, complete: false }));
|
|
413
|
+
const started = performance.now();
|
|
414
|
+
const response = await this.original(request, extras);
|
|
415
|
+
const responseTimeMs = performance.now() - started;
|
|
416
|
+
if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
|
|
417
|
+
return this.handleResponse(request, response, await bodyPromise, responseTimeMs, extras);
|
|
418
|
+
};
|
|
419
|
+
async handleResponse(request, response, body, responseTimeMs, extras = {}) {
|
|
420
|
+
if (!eligible(response.status) || response.redirected || !this.api.enabled()) return response;
|
|
421
|
+
return this.repair(request, extras, response, body, responseTimeMs);
|
|
422
|
+
}
|
|
423
|
+
async repair(request, extras, original, body, responseTimeMs) {
|
|
424
|
+
let response = original;
|
|
425
|
+
let result = null;
|
|
426
|
+
const started = performance.now();
|
|
427
|
+
let replayStatusCode = null;
|
|
428
|
+
let replayAttempted = false;
|
|
429
|
+
try {
|
|
430
|
+
const captured = await captureResponse(response);
|
|
431
|
+
response = captured.response;
|
|
432
|
+
const payload = {
|
|
433
|
+
traceId: randomUUID(),
|
|
434
|
+
request: { method: request.method, url: safeUrl(request.url), headers: safeHeaders(request.headers), body: travelingBody(body.body) },
|
|
435
|
+
response: { statusCode: response.status, body: captured.body, truncated: captured.truncated },
|
|
436
|
+
responseTimeMs: Math.round(responseTimeMs)
|
|
437
|
+
};
|
|
438
|
+
request.signal.throwIfAborted();
|
|
439
|
+
result = await this.api.heal(payload, request.signal);
|
|
440
|
+
request.signal.throwIfAborted();
|
|
441
|
+
const retry = body.complete && captured.complete ? buildRetry(request, body.body, result) : null;
|
|
442
|
+
if (!retry) {
|
|
443
|
+
this.api.report(result?.healAttemptId, { failure: { kind: "not_attempted", message: "replay_not_attempted" } });
|
|
444
|
+
return response;
|
|
445
|
+
}
|
|
446
|
+
replayAttempted = true;
|
|
447
|
+
let retried;
|
|
448
|
+
try {
|
|
449
|
+
retried = await this.original(retry, extras);
|
|
450
|
+
} catch {
|
|
451
|
+
this.api.report(result?.healAttemptId, { failure: { kind: "transport_error", message: TRANSPORT_ERROR } });
|
|
452
|
+
request.signal.throwIfAborted();
|
|
453
|
+
return response;
|
|
454
|
+
}
|
|
455
|
+
replayStatusCode = retried.status;
|
|
456
|
+
if (retried.status >= 400) {
|
|
457
|
+
const capturedRetry = await captureResponse(retried);
|
|
458
|
+
retried = capturedRetry.response;
|
|
459
|
+
this.api.report(result?.healAttemptId, { response: { statusCode: retried.status, body: capturedRetry.body, truncated: capturedRetry.truncated } });
|
|
460
|
+
} else {
|
|
461
|
+
this.api.report(result?.healAttemptId, { response: { statusCode: retried.status } });
|
|
462
|
+
}
|
|
463
|
+
void response.body?.cancel().catch(() => {
|
|
464
|
+
});
|
|
465
|
+
return retried;
|
|
466
|
+
} catch {
|
|
467
|
+
request.signal.throwIfAborted();
|
|
468
|
+
this.api.report(result?.healAttemptId, { failure: { kind: "not_attempted", message: "replay_not_attempted" } });
|
|
469
|
+
return response;
|
|
470
|
+
} finally {
|
|
471
|
+
if (this.options.onHeal) {
|
|
472
|
+
try {
|
|
473
|
+
void Promise.resolve(this.options.onHeal({
|
|
474
|
+
url: safeUrl(request.url),
|
|
475
|
+
statusCode: original.status,
|
|
476
|
+
healStatus: replayAttempted && replayStatusCode === null ? "replay_failed" : result?.status ?? "heal_unreachable",
|
|
477
|
+
replayStatusCode,
|
|
478
|
+
healMs: Math.round(performance.now() - started),
|
|
479
|
+
operations: result?.operations
|
|
480
|
+
})).catch(() => warn("onHeal callback failed"));
|
|
481
|
+
} catch {
|
|
482
|
+
warn("onHeal callback failed");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
function buildRetry(request, originalBody, result) {
|
|
489
|
+
if (!result || !["patched", "unverified"].includes(result.status) || !isObject(result.healedRequest)) return null;
|
|
490
|
+
const healed = result.healedRequest;
|
|
491
|
+
if (!["url", "headers", "body"].some((key) => Object.hasOwn(healed, key))) return null;
|
|
492
|
+
try {
|
|
493
|
+
const url = new URL(healed.url ?? request.url);
|
|
494
|
+
if (url.origin !== new URL(request.url).origin || url.username || url.password) return null;
|
|
495
|
+
const headers = new Headers(request.headers);
|
|
496
|
+
const contentType = headers.get("content-type");
|
|
497
|
+
headers.delete("content-length");
|
|
498
|
+
if (healed.headers !== void 0 && !isObject(healed.headers)) return null;
|
|
499
|
+
for (const [name, value] of Object.entries(healed.headers ?? {})) {
|
|
500
|
+
if (value === null) headers.delete(name);
|
|
501
|
+
else if (typeof value === "string") headers.set(name, value);
|
|
502
|
+
else return null;
|
|
503
|
+
}
|
|
504
|
+
const body = Object.hasOwn(healed, "body") ? mergeBody(originalBody, healed.body) : originalBody;
|
|
505
|
+
if (body === null && !["GET", "HEAD"].includes(request.method)) return null;
|
|
506
|
+
return new Request(url, {
|
|
507
|
+
method: request.method,
|
|
508
|
+
headers,
|
|
509
|
+
body: ["GET", "HEAD"].includes(request.method) ? void 0 : serializeRequestBody(body, contentType),
|
|
510
|
+
signal: request.signal,
|
|
511
|
+
redirect: request.redirect,
|
|
512
|
+
credentials: request.credentials,
|
|
513
|
+
cache: request.cache,
|
|
514
|
+
integrity: request.integrity,
|
|
515
|
+
keepalive: request.keepalive,
|
|
516
|
+
mode: request.mode,
|
|
517
|
+
referrer: request.referrer,
|
|
518
|
+
referrerPolicy: request.referrerPolicy
|
|
519
|
+
});
|
|
520
|
+
} catch {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/http.ts
|
|
526
|
+
function installHttp(runtime) {
|
|
527
|
+
const httpRequest = wrapRequest(http.request, "http:", runtime);
|
|
528
|
+
const httpsRequest = wrapRequest(https.request, "https:", runtime);
|
|
529
|
+
http.request = httpRequest;
|
|
530
|
+
https.request = httpsRequest;
|
|
531
|
+
http.get = wrapGet(httpRequest);
|
|
532
|
+
https.get = wrapGet(httpsRequest);
|
|
533
|
+
syncBuiltinESMExports();
|
|
534
|
+
}
|
|
535
|
+
function wrapGet(request) {
|
|
536
|
+
return ((...args) => {
|
|
537
|
+
const result = request(...args);
|
|
538
|
+
result.end();
|
|
539
|
+
return result;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function wrapRequest(original, protocol, runtime) {
|
|
543
|
+
return ((...received) => {
|
|
544
|
+
const args = [...received];
|
|
545
|
+
const callback = typeof args.at(-1) === "function" ? args.pop() : void 0;
|
|
546
|
+
const started = performance.now();
|
|
547
|
+
const request = original(...args);
|
|
548
|
+
const capture = captureBody(request);
|
|
549
|
+
const signal = cancellation(request, requestSignal(args));
|
|
550
|
+
const emit = request.emit.bind(request);
|
|
551
|
+
request.emit = ((event, ...values) => {
|
|
552
|
+
if (event !== "response") return emit(event, ...values);
|
|
553
|
+
const response = values[0];
|
|
554
|
+
if (!eligible(response.statusCode ?? 0) || !runtime.api.enabled()) {
|
|
555
|
+
return emit(event, ...values);
|
|
556
|
+
}
|
|
557
|
+
void handleResponse(runtime, request, response, protocol, capture.body(), signal, started).then((healed) => emit("response", healed)).catch((error) => {
|
|
558
|
+
response.destroy();
|
|
559
|
+
if (!request.destroyed) request.destroy(error instanceof Error ? error : void 0);
|
|
560
|
+
});
|
|
561
|
+
return true;
|
|
562
|
+
});
|
|
563
|
+
if (callback) request.once("response", callback);
|
|
564
|
+
return request;
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
async function handleResponse(runtime, clientRequest, incoming, protocol, body, signal, started) {
|
|
568
|
+
const request = webRequest(clientRequest, protocol, body, signal);
|
|
569
|
+
const response = webResponse(incoming, request.url);
|
|
570
|
+
const healed = await runtime.handleResponse(request, response, body, performance.now() - started);
|
|
571
|
+
return incomingResponse(healed, clientRequest);
|
|
572
|
+
}
|
|
573
|
+
function webRequest(request, protocol, captured, signal) {
|
|
574
|
+
const headers = new Headers();
|
|
575
|
+
for (const name of request.getHeaderNames()) {
|
|
576
|
+
const value = request.getHeader(name);
|
|
577
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
578
|
+
if (item !== void 0) headers.append(name, String(item));
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const authority = String(request.getHeader("host") ?? request.host);
|
|
582
|
+
const url = new URL(request.path, `${protocol}//${authority}`);
|
|
583
|
+
const method = request.method;
|
|
584
|
+
return new Request(url, {
|
|
585
|
+
method,
|
|
586
|
+
headers,
|
|
587
|
+
signal,
|
|
588
|
+
body: ["GET", "HEAD"].includes(method) || !captured.complete || captured.body === null ? void 0 : serializeRequestBody(captured.body, headers.get("content-type"))
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
function webResponse(response, url) {
|
|
592
|
+
const headers = new Headers();
|
|
593
|
+
for (const [name, value] of Object.entries(response.headers)) {
|
|
594
|
+
for (const item of Array.isArray(value) ? value : [value]) {
|
|
595
|
+
if (item !== void 0) headers.append(name, String(item));
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const encoding = headers.get("content-encoding")?.toLowerCase();
|
|
599
|
+
let body = response;
|
|
600
|
+
if (encoding === "gzip" || encoding === "x-gzip" || encoding === "deflate") {
|
|
601
|
+
body = response.pipe(createUnzip());
|
|
602
|
+
headers.delete("content-encoding");
|
|
603
|
+
headers.delete("content-length");
|
|
604
|
+
} else if (encoding === "br") {
|
|
605
|
+
body = response.pipe(createBrotliDecompress());
|
|
606
|
+
headers.delete("content-encoding");
|
|
607
|
+
headers.delete("content-length");
|
|
608
|
+
}
|
|
609
|
+
const converted = new Response(
|
|
610
|
+
Readable.toWeb(body),
|
|
611
|
+
{ status: response.statusCode, statusText: response.statusMessage, headers }
|
|
612
|
+
);
|
|
613
|
+
Object.defineProperty(converted, "url", { value: url });
|
|
614
|
+
return converted;
|
|
615
|
+
}
|
|
616
|
+
function incomingResponse(response, request) {
|
|
617
|
+
const stream = response.body ? Readable.fromWeb(response.body) : Readable.from([]);
|
|
618
|
+
const headers = {};
|
|
619
|
+
for (const [name, value] of response.headers) headers[name] = value;
|
|
620
|
+
if (["gzip", "x-gzip", "deflate", "br"].includes(String(headers["content-encoding"]).toLowerCase())) {
|
|
621
|
+
delete headers["content-encoding"];
|
|
622
|
+
delete headers["content-length"];
|
|
623
|
+
}
|
|
624
|
+
const cookies = response.headers.getSetCookie();
|
|
625
|
+
if (cookies.length) headers["set-cookie"] = cookies;
|
|
626
|
+
const rawHeaders = Object.entries(headers).flatMap(([name, value]) => (Array.isArray(value) ? value : [value]).flatMap((item) => item === void 0 ? [] : [name, String(item)]));
|
|
627
|
+
return Object.assign(stream, {
|
|
628
|
+
statusCode: response.status,
|
|
629
|
+
statusMessage: response.statusText,
|
|
630
|
+
headers,
|
|
631
|
+
rawHeaders,
|
|
632
|
+
trailers: {},
|
|
633
|
+
rawTrailers: [],
|
|
634
|
+
httpVersion: "1.1",
|
|
635
|
+
httpVersionMajor: 1,
|
|
636
|
+
httpVersionMinor: 1,
|
|
637
|
+
complete: true,
|
|
638
|
+
req: request
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
function captureBody(request) {
|
|
642
|
+
let chunks = [];
|
|
643
|
+
let size = 0;
|
|
644
|
+
let complete = true;
|
|
645
|
+
const record = (chunk, encoding) => {
|
|
646
|
+
if (chunk === void 0 || chunk === null || typeof chunk === "function" || !complete) return;
|
|
647
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
|
|
648
|
+
size += bytes.byteLength;
|
|
649
|
+
if (size > REQUEST_LIMIT) {
|
|
650
|
+
complete = false;
|
|
651
|
+
chunks = [];
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
chunks.push(bytes);
|
|
655
|
+
};
|
|
656
|
+
const write = request.write;
|
|
657
|
+
request.write = ((...args) => {
|
|
658
|
+
record(args[0], typeof args[1] === "string" ? args[1] : void 0);
|
|
659
|
+
return Reflect.apply(write, request, args);
|
|
660
|
+
});
|
|
661
|
+
const end = request.end;
|
|
662
|
+
request.end = ((...args) => {
|
|
663
|
+
record(args[0], typeof args[1] === "string" ? args[1] : void 0);
|
|
664
|
+
return Reflect.apply(end, request, args);
|
|
665
|
+
});
|
|
666
|
+
return { body: () => {
|
|
667
|
+
if (!complete) return { body: null, complete: false };
|
|
668
|
+
const header = request.getHeader("content-type");
|
|
669
|
+
const contentType = Array.isArray(header) ? header[0] : header === void 0 ? void 0 : String(header);
|
|
670
|
+
const parsed = parseRequestBody(Buffer.concat(chunks, size), contentType);
|
|
671
|
+
return { body: parsed.body, complete: parsed.valid };
|
|
672
|
+
} };
|
|
673
|
+
}
|
|
674
|
+
function requestSignal(args) {
|
|
675
|
+
for (const value of args.slice(0, 2)) {
|
|
676
|
+
if (value && typeof value === "object" && "signal" in value && value.signal instanceof AbortSignal) return value.signal;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
function cancellation(request, external) {
|
|
680
|
+
const controller = new AbortController();
|
|
681
|
+
const destroy = request.destroy.bind(request);
|
|
682
|
+
request.destroy = ((error) => {
|
|
683
|
+
if (!controller.signal.aborted) controller.abort(error);
|
|
684
|
+
return destroy(error);
|
|
685
|
+
});
|
|
686
|
+
return external ? AbortSignal.any([external, controller.signal]) : controller.signal;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/index.ts
|
|
690
|
+
var STATE = /* @__PURE__ */ Symbol.for("mnfst.node.runtime.v1");
|
|
691
|
+
var globals = globalThis;
|
|
692
|
+
function manifest(options = {}) {
|
|
693
|
+
const key = options.key || process.env.MNFST_KEY;
|
|
694
|
+
if (!key) {
|
|
695
|
+
warn("MNFST_KEY is not set; Manifest is disabled");
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const url = new URL(options.url || process.env.MNFST_URL || "https://api.manifest.build");
|
|
699
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
700
|
+
throw new TypeError("Manifest URL must be an HTTP(S) base URL without credentials, query or fragment");
|
|
701
|
+
}
|
|
702
|
+
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
703
|
+
const resolved = { ...options, key, url: url.toString() };
|
|
704
|
+
const existing = globals[STATE];
|
|
705
|
+
if (existing) {
|
|
706
|
+
if (existing.options.key !== key || existing.options.url !== resolved.url || existing.options.onHeal !== options.onHeal) {
|
|
707
|
+
warn("Manifest is already configured; changing configuration requires a process restart");
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const runtime = new Runtime(resolved, globalThis.fetch.bind(globalThis));
|
|
712
|
+
globalThis.fetch = runtime.fetch;
|
|
713
|
+
installHttp(runtime);
|
|
714
|
+
globals[STATE] = runtime;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export {
|
|
718
|
+
VERSION,
|
|
719
|
+
manifest
|
|
720
|
+
};
|