@patchstack/connect 0.3.19 → 0.3.20
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/AGENT-INSTALL.md +33 -18
- package/README.md +28 -11
- package/dist/cli.js +1204 -1071
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/express-guard.cjs +5 -1
- package/dist/protect/templates/express-guard.js +5 -1
- package/dist/protect/templates/express-guard.ts +5 -1
- package/dist/protect/templates/fastify-plugin.cjs +46 -0
- package/dist/protect/templates/fastify-plugin.js +45 -0
- package/dist/protect/templates/generic-guard.cjs +41 -0
- package/dist/protect/templates/generic-guard.js +42 -0
- package/dist/protect/templates/generic-guard.ts +5 -1
- package/dist/protect/templates/guard.ts +5 -1
- package/dist/protect/templates/nuxt-middleware.ts +43 -0
- package/dist/protect.cjs +1692 -92
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +24 -0
- package/dist/protect.js +501 -82
- package/dist/protect.js.map +1 -1
- package/dist/refresh-manifest-XBOGN446.js +1090 -0
- package/dist/refresh-manifest-XBOGN446.js.map +1 -0
- package/package.json +1 -1
package/dist/protect.cjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
6
11
|
var __export = (target, all) => {
|
|
7
12
|
for (var name in all)
|
|
8
13
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -13,9 +18,1185 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
13
18
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
19
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
20
|
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/types.ts
|
|
34
|
+
var PatchstackError;
|
|
35
|
+
var init_types = __esm({
|
|
36
|
+
"src/types.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
PatchstackError = class extends Error {
|
|
39
|
+
constructor(message, code, cause) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.cause = cause;
|
|
43
|
+
this.name = "PatchstackError";
|
|
44
|
+
}
|
|
45
|
+
code;
|
|
46
|
+
cause;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// src/client.ts
|
|
52
|
+
function buildEndpointUrl(base, siteUuid) {
|
|
53
|
+
const trimmed = base.replace(/\/$/, "");
|
|
54
|
+
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
55
|
+
}
|
|
56
|
+
async function postManifest(config, payload) {
|
|
57
|
+
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
58
|
+
const timeoutMs = config.timeoutMs;
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(url, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
Accept: "application/json",
|
|
66
|
+
"User-Agent": "@patchstack/connect"
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({ ...payload, environment: config.environment }),
|
|
69
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
70
|
+
});
|
|
71
|
+
} catch (cause) {
|
|
72
|
+
if (isTimeoutError(cause)) {
|
|
73
|
+
throw new PatchstackError(
|
|
74
|
+
`Patchstack request to ${url} timed out after ${timeoutMs}ms. Override with PATCHSTACK_TIMEOUT_MS.`,
|
|
75
|
+
"NETWORK_TIMEOUT",
|
|
76
|
+
cause
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
throw new PatchstackError(
|
|
80
|
+
`Could not reach Patchstack at ${url}. Check your network connection.`,
|
|
81
|
+
"NETWORK_ERROR",
|
|
82
|
+
cause
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
const text = await response.text();
|
|
86
|
+
let body = null;
|
|
87
|
+
try {
|
|
88
|
+
body = text.length > 0 ? JSON.parse(text) : null;
|
|
89
|
+
} catch {
|
|
90
|
+
body = null;
|
|
91
|
+
}
|
|
92
|
+
if (response.status === 404) {
|
|
93
|
+
throw new PatchstackError(
|
|
94
|
+
body?.error ?? "Site not found. Check that your site UUID is correct and that the app is registered as a Pulse app in your Patchstack dashboard.",
|
|
95
|
+
"SITE_NOT_FOUND"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (response.status === 422) {
|
|
99
|
+
throw new PatchstackError(
|
|
100
|
+
body?.message ?? "Patchstack rejected the manifest payload (validation failed).",
|
|
101
|
+
"VALIDATION_ERROR"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (response.status < 200 || response.status >= 300) {
|
|
105
|
+
throw new PatchstackError(
|
|
106
|
+
`Patchstack returned ${response.status}: ${text.slice(0, 200)}`,
|
|
107
|
+
"SERVER_ERROR"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (body === null) {
|
|
111
|
+
throw new PatchstackError("Patchstack returned an empty response.", "SERVER_ERROR");
|
|
112
|
+
}
|
|
113
|
+
return body;
|
|
114
|
+
}
|
|
115
|
+
function isTimeoutError(cause) {
|
|
116
|
+
if (cause instanceof Error) {
|
|
117
|
+
return cause.name === "TimeoutError" || cause.name === "AbortError";
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS;
|
|
122
|
+
var init_client = __esm({
|
|
123
|
+
"src/client.ts"() {
|
|
124
|
+
"use strict";
|
|
125
|
+
init_types();
|
|
126
|
+
DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
127
|
+
DEFAULT_TIMEOUT_MS = 3e4;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// src/config.ts
|
|
132
|
+
async function resolveConfig(options) {
|
|
133
|
+
const fromFile = await readConfigFile(options.cwd);
|
|
134
|
+
const fromEnv = readEnv();
|
|
135
|
+
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
136
|
+
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
137
|
+
const timeoutMs = fromEnv.timeoutMs ?? fromFile.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
138
|
+
const environmentRaw = fromEnv.environment ?? fromFile.environment;
|
|
139
|
+
if (environmentRaw !== void 0 && !isEnvironment(environmentRaw)) {
|
|
140
|
+
throw new PatchstackError(
|
|
141
|
+
`Environment must be "production" or "sandbox"; got "${environmentRaw}".`,
|
|
142
|
+
"CONFIG_INVALID"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const environment = environmentRaw ?? DEFAULT_ENVIRONMENT;
|
|
146
|
+
if (siteUuid !== null && siteUuid.length > 0 && !isUuid(siteUuid)) {
|
|
147
|
+
throw new PatchstackError(
|
|
148
|
+
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
149
|
+
"CONFIG_INVALID"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (options.requireSiteUuid && (siteUuid === null || siteUuid.length === 0)) {
|
|
153
|
+
throw new PatchstackError(
|
|
154
|
+
"No site UUID configured. Run `patchstack-connect scan` to provision one, or set PATCHSTACK_SITE_UUID.",
|
|
155
|
+
"CONFIG_MISSING"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
160
|
+
endpoint,
|
|
161
|
+
timeoutMs,
|
|
162
|
+
environment,
|
|
163
|
+
widget: fromFile.widget !== false
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function readConfigFile(cwd) {
|
|
167
|
+
const target = import_node_path.default.join(cwd, CONFIG_FILENAME);
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = await (0, import_promises.readFile)(target, "utf8");
|
|
171
|
+
} catch (err) {
|
|
172
|
+
if (err.code === "ENOENT") {
|
|
173
|
+
return {};
|
|
174
|
+
}
|
|
175
|
+
throw new PatchstackError(
|
|
176
|
+
`Could not read ${target}: ${err.message}`,
|
|
177
|
+
"CONFIG_INVALID",
|
|
178
|
+
err
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
return JSON.parse(raw);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
throw new PatchstackError(
|
|
185
|
+
`Config file ${target} contains invalid JSON.`,
|
|
186
|
+
"CONFIG_INVALID",
|
|
187
|
+
err
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function readEnv() {
|
|
192
|
+
const timeoutRaw = process.env.PATCHSTACK_TIMEOUT_MS;
|
|
193
|
+
let timeoutMs;
|
|
194
|
+
if (timeoutRaw !== void 0 && timeoutRaw.length > 0) {
|
|
195
|
+
const parsed = Number(timeoutRaw);
|
|
196
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
197
|
+
throw new PatchstackError(
|
|
198
|
+
`PATCHSTACK_TIMEOUT_MS must be a positive number; got "${timeoutRaw}".`,
|
|
199
|
+
"CONFIG_INVALID"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
timeoutMs = parsed;
|
|
203
|
+
}
|
|
204
|
+
const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
|
|
205
|
+
return {
|
|
206
|
+
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
|
|
207
|
+
endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
|
|
208
|
+
timeoutMs,
|
|
209
|
+
environment: environmentRaw !== void 0 && environmentRaw.length > 0 ? environmentRaw : void 0
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function isUuid(value) {
|
|
213
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
214
|
+
}
|
|
215
|
+
function isEnvironment(value) {
|
|
216
|
+
return value === "production" || value === "sandbox";
|
|
217
|
+
}
|
|
218
|
+
var import_promises, import_node_path, CONFIG_FILENAME, DEFAULT_ENVIRONMENT;
|
|
219
|
+
var init_config = __esm({
|
|
220
|
+
"src/config.ts"() {
|
|
221
|
+
"use strict";
|
|
222
|
+
import_promises = require("fs/promises");
|
|
223
|
+
import_node_path = __toESM(require("path"), 1);
|
|
224
|
+
init_types();
|
|
225
|
+
init_client();
|
|
226
|
+
CONFIG_FILENAME = ".patchstackrc.json";
|
|
227
|
+
DEFAULT_ENVIRONMENT = "production";
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// src/parsers/consistency.ts
|
|
232
|
+
async function readDeclaredDependencyNames(cwd) {
|
|
233
|
+
let raw;
|
|
234
|
+
try {
|
|
235
|
+
raw = await (0, import_promises2.readFile)(import_node_path2.default.join(cwd, "package.json"), "utf8");
|
|
236
|
+
} catch {
|
|
237
|
+
return [];
|
|
238
|
+
}
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = JSON.parse(raw);
|
|
242
|
+
} catch {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
248
|
+
const names = /* @__PURE__ */ new Set();
|
|
249
|
+
for (const group of ["dependencies", "devDependencies"]) {
|
|
250
|
+
const deps = parsed[group];
|
|
251
|
+
if (typeof deps !== "object" || deps === null) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
255
|
+
if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
names.add(name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return [...names];
|
|
262
|
+
}
|
|
263
|
+
function missingDependencies(declared, packages) {
|
|
264
|
+
const present = new Set(packages.map((entry) => entry.name));
|
|
265
|
+
return declared.filter((name) => !present.has(name));
|
|
266
|
+
}
|
|
267
|
+
var import_promises2, import_node_path2;
|
|
268
|
+
var init_consistency = __esm({
|
|
269
|
+
"src/parsers/consistency.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
import_promises2 = require("fs/promises");
|
|
272
|
+
import_node_path2 = __toESM(require("path"), 1);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// src/parsers/npm.ts
|
|
277
|
+
async function parseNpmLockfile(lockfilePath) {
|
|
278
|
+
let raw;
|
|
279
|
+
try {
|
|
280
|
+
raw = await (0, import_promises3.readFile)(lockfilePath, "utf8");
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
283
|
+
}
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(raw);
|
|
287
|
+
} catch (cause) {
|
|
288
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
|
|
289
|
+
}
|
|
290
|
+
if (parsed.packages) {
|
|
291
|
+
return extractFromV2(parsed.packages);
|
|
292
|
+
}
|
|
293
|
+
if (parsed.dependencies) {
|
|
294
|
+
return extractFromV1(parsed.dependencies);
|
|
295
|
+
}
|
|
296
|
+
throw new PatchstackError(
|
|
297
|
+
`Lockfile at ${lockfilePath} has no "packages" or "dependencies" key`,
|
|
298
|
+
"LOCKFILE_PARSE_ERROR"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
function extractFromV2(packages) {
|
|
302
|
+
const entries = [];
|
|
303
|
+
for (const [pkgPath, pkg] of Object.entries(packages)) {
|
|
304
|
+
if (pkgPath === "") {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (pkg.link === true) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (typeof pkg.version !== "string" || pkg.version.length === 0) {
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const name = pkg.name ?? extractNameFromPath(pkgPath);
|
|
314
|
+
if (name === null) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
entries.push({
|
|
318
|
+
name,
|
|
319
|
+
version: pkg.version,
|
|
320
|
+
path: pkgPath,
|
|
321
|
+
direct: isDirectV2(pkgPath)
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return entries;
|
|
325
|
+
}
|
|
326
|
+
function extractFromV1(deps, acc = [], depth = 0) {
|
|
327
|
+
for (const [name, dep] of Object.entries(deps)) {
|
|
328
|
+
if (typeof dep.version === "string" && dep.version.length > 0) {
|
|
329
|
+
acc.push({ name, version: dep.version, direct: depth === 0 });
|
|
330
|
+
}
|
|
331
|
+
if (dep.dependencies) {
|
|
332
|
+
extractFromV1(dep.dependencies, acc, depth + 1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return acc;
|
|
336
|
+
}
|
|
337
|
+
function extractNameFromPath(pkgPath) {
|
|
338
|
+
const segments = pkgPath.split("node_modules" + import_node_path3.default.sep === pkgPath ? import_node_path3.default.sep : "/");
|
|
339
|
+
const parts = pkgPath.split("/");
|
|
340
|
+
const nmIndex = parts.lastIndexOf("node_modules");
|
|
341
|
+
if (nmIndex === -1 || nmIndex >= parts.length - 1) {
|
|
342
|
+
return segments[segments.length - 1] ?? null;
|
|
343
|
+
}
|
|
344
|
+
const tail = parts.slice(nmIndex + 1);
|
|
345
|
+
if (tail.length === 0) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
const first = tail[0];
|
|
349
|
+
if (first !== void 0 && first.startsWith("@") && tail.length >= 2) {
|
|
350
|
+
return `${first}/${tail[1]}`;
|
|
351
|
+
}
|
|
352
|
+
return first ?? null;
|
|
353
|
+
}
|
|
354
|
+
function isDirectV2(pkgPath) {
|
|
355
|
+
const parts = pkgPath.split("/");
|
|
356
|
+
const nmCount = parts.filter((p) => p === "node_modules").length;
|
|
357
|
+
return nmCount === 1;
|
|
358
|
+
}
|
|
359
|
+
var import_promises3, import_node_path3;
|
|
360
|
+
var init_npm = __esm({
|
|
361
|
+
"src/parsers/npm.ts"() {
|
|
362
|
+
"use strict";
|
|
363
|
+
import_promises3 = require("fs/promises");
|
|
364
|
+
import_node_path3 = __toESM(require("path"), 1);
|
|
365
|
+
init_types();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// src/parsers/node_modules.ts
|
|
370
|
+
async function walkNodeModules(cwd) {
|
|
371
|
+
const root = import_node_path4.default.join(cwd, "node_modules");
|
|
372
|
+
try {
|
|
373
|
+
const info = await (0, import_promises4.stat)(root);
|
|
374
|
+
if (!info.isDirectory()) {
|
|
375
|
+
throw new PatchstackError(
|
|
376
|
+
`${root} exists but is not a directory.`,
|
|
377
|
+
"LOCKFILE_NOT_FOUND"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
} catch (cause) {
|
|
381
|
+
if (cause instanceof PatchstackError) {
|
|
382
|
+
throw cause;
|
|
383
|
+
}
|
|
384
|
+
throw new PatchstackError(
|
|
385
|
+
`node_modules/ not found at ${cwd}. Install dependencies first (e.g. \`bun install\` or \`npm install\`).`,
|
|
386
|
+
"LOCKFILE_NOT_FOUND",
|
|
387
|
+
cause
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
const entries = [];
|
|
391
|
+
await walk(root, entries, 0);
|
|
392
|
+
return entries;
|
|
393
|
+
}
|
|
394
|
+
async function walk(dir, acc, depth) {
|
|
395
|
+
let names;
|
|
396
|
+
try {
|
|
397
|
+
names = await (0, import_promises4.readdir)(dir);
|
|
398
|
+
} catch {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
for (const name of names) {
|
|
402
|
+
if (name.startsWith(".")) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
const fullPath = import_node_path4.default.join(dir, name);
|
|
406
|
+
if (!await isPlainDirectory(fullPath)) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (name.startsWith("@")) {
|
|
410
|
+
let subNames;
|
|
411
|
+
try {
|
|
412
|
+
subNames = await (0, import_promises4.readdir)(fullPath);
|
|
413
|
+
} catch {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
for (const sub of subNames) {
|
|
417
|
+
if (sub.startsWith(".")) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const scopedDir = import_node_path4.default.join(fullPath, sub);
|
|
421
|
+
if (!await isPlainDirectory(scopedDir)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
await readPackage(scopedDir, depth, acc);
|
|
425
|
+
await walkNested(scopedDir, acc, depth);
|
|
426
|
+
}
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
await readPackage(fullPath, depth, acc);
|
|
430
|
+
await walkNested(fullPath, acc, depth);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
async function readPackage(pkgDir, depth, acc) {
|
|
434
|
+
let raw;
|
|
435
|
+
try {
|
|
436
|
+
raw = await (0, import_promises4.readFile)(import_node_path4.default.join(pkgDir, "package.json"), "utf8");
|
|
437
|
+
} catch {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let parsed;
|
|
441
|
+
try {
|
|
442
|
+
parsed = JSON.parse(raw);
|
|
443
|
+
} catch {
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
acc.push({
|
|
453
|
+
name: parsed.name,
|
|
454
|
+
version: parsed.version,
|
|
455
|
+
direct: depth === 0
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function walkNested(pkgDir, acc, depth) {
|
|
459
|
+
const nested = import_node_path4.default.join(pkgDir, "node_modules");
|
|
460
|
+
if (!await isPlainDirectory(nested)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
await walk(nested, acc, depth + 1);
|
|
464
|
+
}
|
|
465
|
+
async function isPlainDirectory(dir) {
|
|
466
|
+
try {
|
|
467
|
+
const info = await (0, import_promises4.lstat)(dir);
|
|
468
|
+
return info.isDirectory() && !info.isSymbolicLink();
|
|
469
|
+
} catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
var import_promises4, import_node_path4;
|
|
474
|
+
var init_node_modules = __esm({
|
|
475
|
+
"src/parsers/node_modules.ts"() {
|
|
476
|
+
"use strict";
|
|
477
|
+
import_promises4 = require("fs/promises");
|
|
478
|
+
import_node_path4 = __toESM(require("path"), 1);
|
|
479
|
+
init_types();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// src/parsers/pnpm.ts
|
|
484
|
+
async function parsePnpmLockfile(lockfilePath) {
|
|
485
|
+
let raw;
|
|
486
|
+
try {
|
|
487
|
+
raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
|
|
488
|
+
} catch (cause) {
|
|
489
|
+
throw new PatchstackError(
|
|
490
|
+
`Could not read lockfile at ${lockfilePath}`,
|
|
491
|
+
"LOCKFILE_NOT_FOUND",
|
|
492
|
+
cause
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const lines = raw.split(/\r?\n/);
|
|
496
|
+
const directNames = collectDirectDepNames(lines);
|
|
497
|
+
const packageKeys = collectPackagesBlockKeys(lines);
|
|
498
|
+
if (packageKeys.length === 0) {
|
|
499
|
+
throw new PatchstackError(
|
|
500
|
+
`Lockfile at ${lockfilePath} has no "packages" entries`,
|
|
501
|
+
"LOCKFILE_PARSE_ERROR"
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
const entries = [];
|
|
505
|
+
for (const key of packageKeys) {
|
|
506
|
+
const parsed = parsePackageKey(key);
|
|
507
|
+
if (parsed === null) {
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
entries.push({
|
|
511
|
+
name: parsed.name,
|
|
512
|
+
version: parsed.version,
|
|
513
|
+
direct: directNames.has(parsed.name)
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return entries;
|
|
517
|
+
}
|
|
518
|
+
function parsePackageKey(rawKey) {
|
|
519
|
+
let k = rawKey.trim();
|
|
520
|
+
if (k.length === 0) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
if (k.startsWith("'") && k.endsWith("'") || k.startsWith('"') && k.endsWith('"')) {
|
|
524
|
+
k = k.slice(1, -1);
|
|
525
|
+
}
|
|
526
|
+
if (k.startsWith("/")) {
|
|
527
|
+
k = k.slice(1);
|
|
528
|
+
}
|
|
529
|
+
const parenIdx = k.indexOf("(");
|
|
530
|
+
if (parenIdx >= 0) {
|
|
531
|
+
k = k.slice(0, parenIdx);
|
|
532
|
+
}
|
|
533
|
+
let scopePrefix = "";
|
|
534
|
+
let body = k;
|
|
535
|
+
if (k.startsWith("@")) {
|
|
536
|
+
const firstSlash = k.indexOf("/");
|
|
537
|
+
if (firstSlash <= 0) {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
scopePrefix = k.slice(0, firstSlash + 1);
|
|
541
|
+
body = k.slice(firstSlash + 1);
|
|
542
|
+
}
|
|
543
|
+
const slashIdx = body.indexOf("/");
|
|
544
|
+
const atIdx = body.indexOf("@");
|
|
545
|
+
let sepIdx;
|
|
546
|
+
if (slashIdx < 0 && atIdx < 0) {
|
|
547
|
+
return null;
|
|
548
|
+
} else if (slashIdx < 0) {
|
|
549
|
+
sepIdx = atIdx;
|
|
550
|
+
} else if (atIdx < 0) {
|
|
551
|
+
sepIdx = slashIdx;
|
|
552
|
+
} else {
|
|
553
|
+
sepIdx = Math.min(slashIdx, atIdx);
|
|
554
|
+
}
|
|
555
|
+
const name = scopePrefix + body.slice(0, sepIdx);
|
|
556
|
+
let version = body.slice(sepIdx + 1);
|
|
557
|
+
const underscoreIdx = version.indexOf("_");
|
|
558
|
+
if (underscoreIdx >= 0) {
|
|
559
|
+
version = version.slice(0, underscoreIdx);
|
|
560
|
+
}
|
|
561
|
+
if (name.length === 0 || version.length === 0) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
return { name, version };
|
|
565
|
+
}
|
|
566
|
+
function indentOf(line) {
|
|
567
|
+
let i = 0;
|
|
568
|
+
while (i < line.length && line[i] === " ") {
|
|
569
|
+
i++;
|
|
570
|
+
}
|
|
571
|
+
return i;
|
|
572
|
+
}
|
|
573
|
+
function isBlankOrComment(line) {
|
|
574
|
+
const trimmed = line.trim();
|
|
575
|
+
return trimmed.length === 0 || trimmed.startsWith("#");
|
|
576
|
+
}
|
|
577
|
+
function collectPackagesBlockKeys(lines) {
|
|
578
|
+
const keys = [];
|
|
579
|
+
let inBlock = false;
|
|
580
|
+
let childIndent = null;
|
|
581
|
+
for (const line of lines) {
|
|
582
|
+
if (isBlankOrComment(line)) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const indent = indentOf(line);
|
|
586
|
+
if (!inBlock) {
|
|
587
|
+
if (indent === 0 && line.trim() === "packages:") {
|
|
588
|
+
inBlock = true;
|
|
589
|
+
}
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (indent === 0) {
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
if (childIndent === null) {
|
|
596
|
+
childIndent = indent;
|
|
597
|
+
}
|
|
598
|
+
if (indent !== childIndent) {
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
const content = line.slice(indent);
|
|
602
|
+
if (!content.endsWith(":")) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
keys.push(content.slice(0, -1));
|
|
606
|
+
}
|
|
607
|
+
return keys;
|
|
608
|
+
}
|
|
609
|
+
function collectDirectDepNames(lines) {
|
|
610
|
+
const names = /* @__PURE__ */ new Set();
|
|
611
|
+
collectFromImporters(lines, names);
|
|
612
|
+
for (const section of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
613
|
+
collectFromTopLevelSection(lines, section, names);
|
|
614
|
+
}
|
|
615
|
+
return names;
|
|
616
|
+
}
|
|
617
|
+
function collectFromImporters(lines, out) {
|
|
618
|
+
let inImporters = false;
|
|
619
|
+
let importerIndent = null;
|
|
620
|
+
let inDepSection = false;
|
|
621
|
+
let depSectionIndent = null;
|
|
622
|
+
let leafIndent = null;
|
|
623
|
+
for (const line of lines) {
|
|
624
|
+
if (isBlankOrComment(line)) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const indent = indentOf(line);
|
|
628
|
+
const trimmed = line.trim();
|
|
629
|
+
if (!inImporters) {
|
|
630
|
+
if (indent === 0 && trimmed === "importers:") {
|
|
631
|
+
inImporters = true;
|
|
632
|
+
}
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (indent === 0) {
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
if (importerIndent === null) {
|
|
639
|
+
importerIndent = indent;
|
|
640
|
+
}
|
|
641
|
+
if (indent === importerIndent) {
|
|
642
|
+
inDepSection = false;
|
|
643
|
+
depSectionIndent = null;
|
|
644
|
+
leafIndent = null;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
if (!inDepSection) {
|
|
648
|
+
const key = stripTrailingColon(trimmed);
|
|
649
|
+
if (key !== null && DEP_SECTIONS.has(key)) {
|
|
650
|
+
inDepSection = true;
|
|
651
|
+
depSectionIndent = indent;
|
|
652
|
+
}
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (depSectionIndent !== null && indent <= depSectionIndent) {
|
|
656
|
+
inDepSection = false;
|
|
657
|
+
depSectionIndent = null;
|
|
658
|
+
leafIndent = null;
|
|
659
|
+
const key = stripTrailingColon(trimmed);
|
|
660
|
+
if (key !== null && DEP_SECTIONS.has(key)) {
|
|
661
|
+
inDepSection = true;
|
|
662
|
+
depSectionIndent = indent;
|
|
663
|
+
}
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (leafIndent === null) {
|
|
667
|
+
leafIndent = indent;
|
|
668
|
+
}
|
|
669
|
+
if (indent !== leafIndent) {
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
const name = extractLeafName(trimmed);
|
|
673
|
+
if (name !== null) {
|
|
674
|
+
out.add(name);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function collectFromTopLevelSection(lines, section, out) {
|
|
679
|
+
let inSection = false;
|
|
680
|
+
let leafIndent = null;
|
|
681
|
+
for (const line of lines) {
|
|
682
|
+
if (isBlankOrComment(line)) {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const indent = indentOf(line);
|
|
686
|
+
const trimmed = line.trim();
|
|
687
|
+
if (!inSection) {
|
|
688
|
+
if (indent === 0 && trimmed === `${section}:`) {
|
|
689
|
+
inSection = true;
|
|
690
|
+
}
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
if (indent === 0) {
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
if (leafIndent === null) {
|
|
697
|
+
leafIndent = indent;
|
|
698
|
+
}
|
|
699
|
+
if (indent !== leafIndent) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
const name = extractLeafName(trimmed);
|
|
703
|
+
if (name !== null) {
|
|
704
|
+
out.add(name);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function stripTrailingColon(s) {
|
|
709
|
+
if (!s.endsWith(":")) {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
return s.slice(0, -1).trim();
|
|
713
|
+
}
|
|
714
|
+
function extractLeafName(trimmed) {
|
|
715
|
+
const colonIdx = trimmed.indexOf(":");
|
|
716
|
+
if (colonIdx < 0) {
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
let name = trimmed.slice(0, colonIdx).trim();
|
|
720
|
+
if (name.length === 0) {
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
if (name.startsWith("'") && name.endsWith("'") || name.startsWith('"') && name.endsWith('"')) {
|
|
724
|
+
name = name.slice(1, -1);
|
|
725
|
+
}
|
|
726
|
+
return name.length > 0 ? name : null;
|
|
727
|
+
}
|
|
728
|
+
var import_promises5, DEP_SECTIONS;
|
|
729
|
+
var init_pnpm = __esm({
|
|
730
|
+
"src/parsers/pnpm.ts"() {
|
|
731
|
+
"use strict";
|
|
732
|
+
import_promises5 = require("fs/promises");
|
|
733
|
+
init_types();
|
|
734
|
+
DEP_SECTIONS = /* @__PURE__ */ new Set([
|
|
735
|
+
"dependencies",
|
|
736
|
+
"devDependencies",
|
|
737
|
+
"optionalDependencies"
|
|
738
|
+
]);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// src/parsers/yarn.ts
|
|
743
|
+
async function parseYarnLockfile(lockfilePath) {
|
|
744
|
+
let raw;
|
|
745
|
+
try {
|
|
746
|
+
raw = await (0, import_promises6.readFile)(lockfilePath, "utf8");
|
|
747
|
+
} catch (cause) {
|
|
748
|
+
throw new PatchstackError(
|
|
749
|
+
`Could not read lockfile at ${lockfilePath}`,
|
|
750
|
+
"LOCKFILE_NOT_FOUND",
|
|
751
|
+
cause
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const blocks = parseBlocks(raw);
|
|
755
|
+
if (blocks.length === 0) {
|
|
756
|
+
throw new PatchstackError(
|
|
757
|
+
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
758
|
+
"LOCKFILE_PARSE_ERROR"
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const directNames = await readDirectDepNames(import_node_path5.default.dirname(lockfilePath));
|
|
762
|
+
const entries = [];
|
|
763
|
+
const seen = /* @__PURE__ */ new Set();
|
|
764
|
+
for (const block of blocks) {
|
|
765
|
+
if (block.version.length === 0 || block.names.size === 0) {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
for (const name of block.names) {
|
|
769
|
+
const dedupKey = `${name}@${block.version}`;
|
|
770
|
+
if (seen.has(dedupKey)) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
seen.add(dedupKey);
|
|
774
|
+
entries.push({
|
|
775
|
+
name,
|
|
776
|
+
version: block.version,
|
|
777
|
+
direct: directNames.has(name)
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
return entries;
|
|
782
|
+
}
|
|
783
|
+
function parseBlocks(raw) {
|
|
784
|
+
const lines = raw.split(/\r?\n/);
|
|
785
|
+
const blocks = [];
|
|
786
|
+
let current = null;
|
|
787
|
+
const finalize = () => {
|
|
788
|
+
if (current !== null && current.version.length > 0 && current.names.size > 0) {
|
|
789
|
+
blocks.push(current);
|
|
790
|
+
}
|
|
791
|
+
current = null;
|
|
792
|
+
};
|
|
793
|
+
for (const line of lines) {
|
|
794
|
+
const trimmed = line.trim();
|
|
795
|
+
if (trimmed.length === 0 || trimmed.startsWith("#")) {
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
const indent = countLeadingSpaces(line);
|
|
799
|
+
if (indent === 0) {
|
|
800
|
+
finalize();
|
|
801
|
+
if (!trimmed.endsWith(":")) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const keyLine = trimmed.slice(0, -1);
|
|
805
|
+
const names = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const spec of splitDescriptors(keyLine)) {
|
|
807
|
+
const name = extractName(spec);
|
|
808
|
+
if (name !== null) {
|
|
809
|
+
names.add(name);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
current = { names, version: "" };
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
if (current === null) {
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const version = parseVersionField(trimmed);
|
|
819
|
+
if (version !== null) {
|
|
820
|
+
current.version = version;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
finalize();
|
|
824
|
+
return blocks;
|
|
825
|
+
}
|
|
826
|
+
function countLeadingSpaces(line) {
|
|
827
|
+
let i = 0;
|
|
828
|
+
while (i < line.length && line[i] === " ") {
|
|
829
|
+
i++;
|
|
830
|
+
}
|
|
831
|
+
return i;
|
|
832
|
+
}
|
|
833
|
+
function splitDescriptors(keyLine) {
|
|
834
|
+
const parts = [];
|
|
835
|
+
let current = "";
|
|
836
|
+
let quote = null;
|
|
837
|
+
for (let i = 0; i < keyLine.length; i++) {
|
|
838
|
+
const c = keyLine[i];
|
|
839
|
+
if (quote !== null) {
|
|
840
|
+
current += c;
|
|
841
|
+
if (c === quote) {
|
|
842
|
+
quote = null;
|
|
843
|
+
}
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (c === '"' || c === "'") {
|
|
847
|
+
quote = c;
|
|
848
|
+
current += c;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (c === ",") {
|
|
852
|
+
const piece = current.trim();
|
|
853
|
+
if (piece.length > 0) {
|
|
854
|
+
parts.push(piece);
|
|
855
|
+
}
|
|
856
|
+
current = "";
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
current += c;
|
|
860
|
+
}
|
|
861
|
+
const tail = current.trim();
|
|
862
|
+
if (tail.length > 0) {
|
|
863
|
+
parts.push(tail);
|
|
864
|
+
}
|
|
865
|
+
return parts;
|
|
866
|
+
}
|
|
867
|
+
function extractName(rawSpec) {
|
|
868
|
+
let s = rawSpec.trim();
|
|
869
|
+
if (s.length === 0) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
873
|
+
s = s.slice(1, -1);
|
|
874
|
+
}
|
|
875
|
+
const atIdx = s.lastIndexOf("@");
|
|
876
|
+
if (atIdx <= 0) {
|
|
877
|
+
return null;
|
|
878
|
+
}
|
|
879
|
+
const name = s.slice(0, atIdx);
|
|
880
|
+
return name.length > 0 ? name : null;
|
|
881
|
+
}
|
|
882
|
+
function parseVersionField(content) {
|
|
883
|
+
if (!content.startsWith("version")) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const after = content.slice("version".length);
|
|
887
|
+
const firstChar = after.charAt(0);
|
|
888
|
+
if (firstChar !== " " && firstChar !== " " && firstChar !== ":") {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
let rest = firstChar === ":" ? after.slice(1) : after;
|
|
892
|
+
rest = rest.trim();
|
|
893
|
+
if (rest.length === 0) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
if (rest.startsWith('"') && rest.endsWith('"') || rest.startsWith("'") && rest.endsWith("'")) {
|
|
897
|
+
rest = rest.slice(1, -1);
|
|
898
|
+
}
|
|
899
|
+
return rest.length > 0 ? rest : null;
|
|
900
|
+
}
|
|
901
|
+
async function readDirectDepNames(cwd) {
|
|
902
|
+
const names = /* @__PURE__ */ new Set();
|
|
903
|
+
let raw;
|
|
904
|
+
try {
|
|
905
|
+
raw = await (0, import_promises6.readFile)(import_node_path5.default.join(cwd, "package.json"), "utf8");
|
|
906
|
+
} catch {
|
|
907
|
+
return names;
|
|
908
|
+
}
|
|
909
|
+
let parsed;
|
|
910
|
+
try {
|
|
911
|
+
parsed = JSON.parse(raw);
|
|
912
|
+
} catch {
|
|
913
|
+
return names;
|
|
914
|
+
}
|
|
915
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
916
|
+
return names;
|
|
917
|
+
}
|
|
918
|
+
const obj = parsed;
|
|
919
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
920
|
+
const section = obj[field];
|
|
921
|
+
if (typeof section !== "object" || section === null) {
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
for (const name of Object.keys(section)) {
|
|
925
|
+
names.add(name);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return names;
|
|
929
|
+
}
|
|
930
|
+
var import_promises6, import_node_path5;
|
|
931
|
+
var init_yarn = __esm({
|
|
932
|
+
"src/parsers/yarn.ts"() {
|
|
933
|
+
"use strict";
|
|
934
|
+
import_promises6 = require("fs/promises");
|
|
935
|
+
import_node_path5 = __toESM(require("path"), 1);
|
|
936
|
+
init_types();
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// src/parsers/index.ts
|
|
941
|
+
async function detectLockfile(cwd) {
|
|
942
|
+
const probed = await Promise.all(
|
|
943
|
+
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
944
|
+
const filePath = import_node_path6.default.join(cwd, candidate.filename);
|
|
945
|
+
return { ...candidate, filePath, present: await exists(filePath) };
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
const match = probed.find((candidate) => candidate.present);
|
|
949
|
+
if (match) {
|
|
950
|
+
return {
|
|
951
|
+
ecosystem: "npm",
|
|
952
|
+
filePath: match.filePath,
|
|
953
|
+
filename: match.filename,
|
|
954
|
+
strategy: match.strategy
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
958
|
+
throw new PatchstackError(
|
|
959
|
+
`No lockfile found in ${cwd}. Expected one of: ${expected}.`,
|
|
960
|
+
"LOCKFILE_NOT_FOUND"
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
async function scanLockfile(cwd) {
|
|
964
|
+
const candidates = await presentLockfiles(cwd);
|
|
965
|
+
if (candidates.length === 0) {
|
|
966
|
+
await detectLockfile(cwd);
|
|
967
|
+
}
|
|
968
|
+
const declared = await readDeclaredDependencyNames(cwd);
|
|
969
|
+
const warnings = [];
|
|
970
|
+
let firstParsed = null;
|
|
971
|
+
let walkTried = false;
|
|
972
|
+
for (const candidate of candidates) {
|
|
973
|
+
let packages;
|
|
974
|
+
try {
|
|
975
|
+
packages = await runStrategy(candidate, cwd);
|
|
976
|
+
} catch {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
walkTried ||= candidate.strategy === "node-modules-walk";
|
|
980
|
+
firstParsed ??= { packages, filename: candidate.filename };
|
|
981
|
+
const missing = missingDependencies(declared, packages);
|
|
982
|
+
if (missing.length === 0) {
|
|
983
|
+
return manifestWith(packages, warnings);
|
|
984
|
+
}
|
|
985
|
+
warnings.push(staleWarning(candidate.filename, missing));
|
|
986
|
+
}
|
|
987
|
+
if (!walkTried) {
|
|
988
|
+
try {
|
|
989
|
+
const packages = await walkNodeModules(cwd);
|
|
990
|
+
const missing = missingDependencies(declared, packages);
|
|
991
|
+
if (missing.length > 0) {
|
|
992
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
993
|
+
}
|
|
994
|
+
warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
|
|
995
|
+
return manifestWith(packages, warnings);
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (firstParsed === null) {
|
|
1000
|
+
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
1001
|
+
throw new PatchstackError(
|
|
1002
|
+
`No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
|
|
1003
|
+
"LOCKFILE_NOT_FOUND"
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
warnings.push(
|
|
1007
|
+
`No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
|
|
1008
|
+
);
|
|
1009
|
+
return manifestWith(firstParsed.packages, warnings);
|
|
1010
|
+
}
|
|
1011
|
+
function manifestWith(packages, warnings) {
|
|
1012
|
+
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
1013
|
+
}
|
|
1014
|
+
function staleWarning(source, missing) {
|
|
1015
|
+
const sample = missing.slice(0, 3).join(", ");
|
|
1016
|
+
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
1017
|
+
return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
|
|
1018
|
+
}
|
|
1019
|
+
async function presentLockfiles(cwd) {
|
|
1020
|
+
const probed = await Promise.all(
|
|
1021
|
+
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
1022
|
+
const filePath = import_node_path6.default.join(cwd, candidate.filename);
|
|
1023
|
+
return { ...candidate, filePath, present: await exists(filePath) };
|
|
1024
|
+
})
|
|
1025
|
+
);
|
|
1026
|
+
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
1027
|
+
}
|
|
1028
|
+
async function runStrategy(detected, cwd) {
|
|
1029
|
+
switch (detected.strategy) {
|
|
1030
|
+
case "npm-lockfile":
|
|
1031
|
+
return parseNpmLockfile(detected.filePath);
|
|
1032
|
+
case "pnpm-lockfile":
|
|
1033
|
+
return parsePnpmLockfile(detected.filePath);
|
|
1034
|
+
case "yarn-lockfile":
|
|
1035
|
+
return parseYarnLockfile(detected.filePath);
|
|
1036
|
+
case "node-modules-walk":
|
|
1037
|
+
return walkNodeModules(cwd);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async function exists(filePath) {
|
|
1041
|
+
try {
|
|
1042
|
+
await (0, import_promises7.access)(filePath);
|
|
1043
|
+
return true;
|
|
1044
|
+
} catch {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
var import_promises7, import_node_path6, LOCKFILE_CANDIDATES;
|
|
1049
|
+
var init_parsers = __esm({
|
|
1050
|
+
"src/parsers/index.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
import_promises7 = require("fs/promises");
|
|
1053
|
+
import_node_path6 = __toESM(require("path"), 1);
|
|
1054
|
+
init_types();
|
|
1055
|
+
init_consistency();
|
|
1056
|
+
init_npm();
|
|
1057
|
+
init_node_modules();
|
|
1058
|
+
init_pnpm();
|
|
1059
|
+
init_yarn();
|
|
1060
|
+
LOCKFILE_CANDIDATES = [
|
|
1061
|
+
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
1062
|
+
{ filename: "bun.lock", strategy: "node-modules-walk" },
|
|
1063
|
+
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
1064
|
+
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
1065
|
+
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
1066
|
+
];
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// src/normalize.ts
|
|
1071
|
+
function buildWirePayload(manifest) {
|
|
1072
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1073
|
+
const wirePackages = [];
|
|
1074
|
+
for (const entry of manifest.packages) {
|
|
1075
|
+
const versions = seen.get(entry.name);
|
|
1076
|
+
if (versions) {
|
|
1077
|
+
if (versions.has(entry.version)) {
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
versions.add(entry.version);
|
|
1081
|
+
} else {
|
|
1082
|
+
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
1083
|
+
}
|
|
1084
|
+
wirePackages.push({ name: entry.name, version: entry.version });
|
|
1085
|
+
}
|
|
1086
|
+
wirePackages.sort((a, b) => {
|
|
1087
|
+
if (a.name === b.name) {
|
|
1088
|
+
return compareVersions(a.version, b.version);
|
|
1089
|
+
}
|
|
1090
|
+
return a.name < b.name ? -1 : 1;
|
|
1091
|
+
});
|
|
1092
|
+
const duplicateNames = [];
|
|
1093
|
+
for (const [name, versions] of seen) {
|
|
1094
|
+
if (versions.size > 1) {
|
|
1095
|
+
duplicateNames.push(name);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
payload: { ecosystem: manifest.ecosystem, packages: wirePackages },
|
|
1100
|
+
stats: {
|
|
1101
|
+
uniqueNames: seen.size,
|
|
1102
|
+
duplicateNames,
|
|
1103
|
+
totalEntries: manifest.packages.length
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
function compareVersions(a, b) {
|
|
1108
|
+
if (a === b) {
|
|
1109
|
+
return 0;
|
|
1110
|
+
}
|
|
1111
|
+
const [aBase, aPre] = splitPrerelease(a);
|
|
1112
|
+
const [bBase, bPre] = splitPrerelease(b);
|
|
1113
|
+
const baseCmp = compareSegments(aBase.split("."), bBase.split("."));
|
|
1114
|
+
if (baseCmp !== 0) {
|
|
1115
|
+
return baseCmp;
|
|
1116
|
+
}
|
|
1117
|
+
if (aPre === null && bPre === null) {
|
|
1118
|
+
return 0;
|
|
1119
|
+
}
|
|
1120
|
+
if (aPre === null) {
|
|
1121
|
+
return 1;
|
|
1122
|
+
}
|
|
1123
|
+
if (bPre === null) {
|
|
1124
|
+
return -1;
|
|
1125
|
+
}
|
|
1126
|
+
return compareSegments(aPre.split("."), bPre.split("."));
|
|
1127
|
+
}
|
|
1128
|
+
function splitPrerelease(version) {
|
|
1129
|
+
const cleaned = version.replace(/^[v=]+/, "").split("+")[0];
|
|
1130
|
+
const dashIndex = cleaned.indexOf("-");
|
|
1131
|
+
if (dashIndex === -1) {
|
|
1132
|
+
return [cleaned, null];
|
|
1133
|
+
}
|
|
1134
|
+
return [cleaned.slice(0, dashIndex), cleaned.slice(dashIndex + 1)];
|
|
1135
|
+
}
|
|
1136
|
+
function compareSegments(a, b) {
|
|
1137
|
+
const max = Math.max(a.length, b.length);
|
|
1138
|
+
for (let i = 0; i < max; i++) {
|
|
1139
|
+
const aPart = a[i];
|
|
1140
|
+
const bPart = b[i];
|
|
1141
|
+
if (aPart === void 0) {
|
|
1142
|
+
return -1;
|
|
1143
|
+
}
|
|
1144
|
+
if (bPart === void 0) {
|
|
1145
|
+
return 1;
|
|
1146
|
+
}
|
|
1147
|
+
const aNum = /^\d+$/.test(aPart);
|
|
1148
|
+
const bNum = /^\d+$/.test(bPart);
|
|
1149
|
+
if (aNum && bNum) {
|
|
1150
|
+
const diff = Number(aPart) - Number(bPart);
|
|
1151
|
+
if (diff !== 0) {
|
|
1152
|
+
return diff < 0 ? -1 : 1;
|
|
1153
|
+
}
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (aNum) {
|
|
1157
|
+
return -1;
|
|
1158
|
+
}
|
|
1159
|
+
if (bNum) {
|
|
1160
|
+
return 1;
|
|
1161
|
+
}
|
|
1162
|
+
if (aPart < bPart) {
|
|
1163
|
+
return -1;
|
|
1164
|
+
}
|
|
1165
|
+
if (aPart > bPart) {
|
|
1166
|
+
return 1;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return 0;
|
|
1170
|
+
}
|
|
1171
|
+
var init_normalize = __esm({
|
|
1172
|
+
"src/normalize.ts"() {
|
|
1173
|
+
"use strict";
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// src/protect/refresh-manifest.ts
|
|
1178
|
+
var refresh_manifest_exports = {};
|
|
1179
|
+
__export(refresh_manifest_exports, {
|
|
1180
|
+
reportManifest: () => reportManifest
|
|
1181
|
+
});
|
|
1182
|
+
async function reportManifest(cwd) {
|
|
1183
|
+
const config = await resolveConfig({ cwd });
|
|
1184
|
+
if (config.siteUuid === null || config.siteUuid.length === 0) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
const manifest = await scanLockfile(cwd);
|
|
1188
|
+
const { payload } = buildWirePayload(manifest);
|
|
1189
|
+
await postManifest(config, payload);
|
|
1190
|
+
}
|
|
1191
|
+
var init_refresh_manifest = __esm({
|
|
1192
|
+
"src/protect/refresh-manifest.ts"() {
|
|
1193
|
+
"use strict";
|
|
1194
|
+
init_config();
|
|
1195
|
+
init_parsers();
|
|
1196
|
+
init_normalize();
|
|
1197
|
+
init_client();
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
19
1200
|
|
|
20
1201
|
// src/protect/runtime.js
|
|
21
1202
|
var runtime_exports = {};
|
|
@@ -43,7 +1224,7 @@ var PatchstackRuleClient = class {
|
|
|
43
1224
|
constructor({ token, baseUrl, cacheTtl, etag } = {}) {
|
|
44
1225
|
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
|
|
45
1226
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
|
|
46
|
-
this.#cacheTtl = cacheTtl
|
|
1227
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
|
|
47
1228
|
this.#etag = etag ?? null;
|
|
48
1229
|
if (!this.#token) {
|
|
49
1230
|
throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
|
|
@@ -83,11 +1264,14 @@ var PatchstackRuleClient = class {
|
|
|
83
1264
|
};
|
|
84
1265
|
}
|
|
85
1266
|
const data = await response.json();
|
|
1267
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
1268
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1269
|
+
}
|
|
86
1270
|
const result = {
|
|
87
1271
|
success: true,
|
|
88
1272
|
notModified: false,
|
|
89
1273
|
etag: response.headers?.get?.("etag") ?? null,
|
|
90
|
-
firewall:
|
|
1274
|
+
firewall: data.firewall,
|
|
91
1275
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
92
1276
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
93
1277
|
};
|
|
@@ -413,13 +1597,14 @@ var RequestResolver = class {
|
|
|
413
1597
|
if (typeof obj !== "object" || obj === null) {
|
|
414
1598
|
return void 0;
|
|
415
1599
|
}
|
|
416
|
-
|
|
1600
|
+
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
1601
|
+
if (own(obj, key)) {
|
|
417
1602
|
return obj[key];
|
|
418
1603
|
}
|
|
419
1604
|
const parts = key.split(".");
|
|
420
1605
|
let current = obj;
|
|
421
1606
|
for (const part of parts) {
|
|
422
|
-
if (current
|
|
1607
|
+
if (!own(current, part)) {
|
|
423
1608
|
return void 0;
|
|
424
1609
|
}
|
|
425
1610
|
current = current[part];
|
|
@@ -632,11 +1817,8 @@ function normalizeObject(value, options = {}) {
|
|
|
632
1817
|
|
|
633
1818
|
// src/protect/engine/engine.js
|
|
634
1819
|
var REDOS_PATTERNS = [
|
|
635
|
-
/\(\
|
|
636
|
-
/\(\
|
|
637
|
-
/\(\w\+\)\*/,
|
|
638
|
-
/\(\w\*\)\*/,
|
|
639
|
-
/\(\w\|\w\)\+/
|
|
1820
|
+
/\([^)]*[+*}][^)]*\)\s*[+*]/,
|
|
1821
|
+
/\([^)]*\|[^)]*\)\s*[+*]/
|
|
640
1822
|
];
|
|
641
1823
|
function safeRegExp(pattern) {
|
|
642
1824
|
if (!pattern) {
|
|
@@ -664,6 +1846,25 @@ function ctypeResult(strValue, isClass, matchVal) {
|
|
|
664
1846
|
const expected = typeof matchVal === "boolean" ? matchVal : true;
|
|
665
1847
|
return isClass === expected;
|
|
666
1848
|
}
|
|
1849
|
+
function walkLeaves(node, segments, cb) {
|
|
1850
|
+
if (node === null || typeof node !== "object") return;
|
|
1851
|
+
if (Array.isArray(node)) {
|
|
1852
|
+
for (const el of node) walkLeaves(el, segments, cb);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const [head, ...rest] = segments;
|
|
1856
|
+
if (head === void 0 || !Object.prototype.hasOwnProperty.call(node, head)) return;
|
|
1857
|
+
if (rest.length === 0) {
|
|
1858
|
+
const leaf = node[head];
|
|
1859
|
+
if (Array.isArray(leaf)) {
|
|
1860
|
+
for (let i = 0; i < leaf.length; i++) cb({ parent: leaf, key: i, value: leaf[i] });
|
|
1861
|
+
} else {
|
|
1862
|
+
cb({ parent: node, key: head, value: leaf });
|
|
1863
|
+
}
|
|
1864
|
+
} else {
|
|
1865
|
+
walkLeaves(node[head], rest, cb);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
667
1868
|
function arrayKeyValue(value, matchObj) {
|
|
668
1869
|
if (!matchObj || !matchObj.match || value === null || typeof value !== "object") {
|
|
669
1870
|
return false;
|
|
@@ -671,23 +1872,11 @@ function arrayKeyValue(value, matchObj) {
|
|
|
671
1872
|
const keys = Array.isArray(matchObj.key) ? matchObj.key : [matchObj.key];
|
|
672
1873
|
const sub = matchObj.match;
|
|
673
1874
|
for (const key of keys) {
|
|
674
|
-
let
|
|
675
|
-
|
|
676
|
-
if (
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
node = node[part];
|
|
681
|
-
}
|
|
682
|
-
if (node === void 0) {
|
|
683
|
-
continue;
|
|
684
|
-
}
|
|
685
|
-
const candidates = Array.isArray(node) ? node : [node];
|
|
686
|
-
for (const candidate of candidates) {
|
|
687
|
-
if (matchValue(sub.type, candidate, sub.value, sub)) {
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
1875
|
+
let matched = false;
|
|
1876
|
+
walkLeaves(value, String(key).split("."), ({ value: leaf }) => {
|
|
1877
|
+
if (!matched && matchValue(sub.type, leaf, sub.value, sub)) matched = true;
|
|
1878
|
+
});
|
|
1879
|
+
if (matched) return true;
|
|
691
1880
|
}
|
|
692
1881
|
return false;
|
|
693
1882
|
}
|
|
@@ -709,16 +1898,68 @@ function isInternalHost(hostname) {
|
|
|
709
1898
|
if (host === "::1" || host === "::") return true;
|
|
710
1899
|
if (host.startsWith("fe80:") || host.startsWith("fc") || host.startsWith("fd")) return true;
|
|
711
1900
|
const v4 = host.match(/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
712
|
-
if (v4)
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
if (
|
|
718
|
-
if (a === 192 && b === 168) return true;
|
|
1901
|
+
if (v4 && isPrivateV4(Number(v4[1]), Number(v4[2]))) return true;
|
|
1902
|
+
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
1903
|
+
if (mapped) {
|
|
1904
|
+
const g1 = parseInt(mapped[1], 16);
|
|
1905
|
+
const g2 = parseInt(mapped[2], 16);
|
|
1906
|
+
if (isPrivateV4(g1 >> 8 & 255, g1 & 255)) return true;
|
|
719
1907
|
}
|
|
720
1908
|
return false;
|
|
721
1909
|
}
|
|
1910
|
+
function isPrivateV4(a, b) {
|
|
1911
|
+
if (a === 127 || a === 10 || a === 0) return true;
|
|
1912
|
+
if (a === 169 && b === 254) return true;
|
|
1913
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1914
|
+
if (a === 192 && b === 168) return true;
|
|
1915
|
+
return false;
|
|
1916
|
+
}
|
|
1917
|
+
function ruleAppliesTo(when, resolver) {
|
|
1918
|
+
try {
|
|
1919
|
+
if (when.method) {
|
|
1920
|
+
const methods = (Array.isArray(when.method) ? when.method : [when.method]).map((m) => String(m).toUpperCase());
|
|
1921
|
+
const actual = String(resolver.resolve("server.REQUEST_METHOD")[0] ?? "GET").toUpperCase();
|
|
1922
|
+
if (!methods.includes(actual)) return false;
|
|
1923
|
+
}
|
|
1924
|
+
if (when.path) {
|
|
1925
|
+
const uri = String(resolver.resolve("server.REQUEST_URI")[0] ?? "/");
|
|
1926
|
+
if (!pathMatches(String(when.path), uri.split("?")[0])) return false;
|
|
1927
|
+
}
|
|
1928
|
+
return true;
|
|
1929
|
+
} catch {
|
|
1930
|
+
return true;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
function pathMatches(pattern, path7) {
|
|
1934
|
+
if (pattern.length > 2 && pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
1935
|
+
const re = safeRegExp(pattern);
|
|
1936
|
+
return re ? re.test(path7) : false;
|
|
1937
|
+
}
|
|
1938
|
+
if (pattern.includes("*")) {
|
|
1939
|
+
const re = safeRegExp("/^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^?]*") + "$/");
|
|
1940
|
+
return re ? re.test(path7) : false;
|
|
1941
|
+
}
|
|
1942
|
+
return path7 === pattern;
|
|
1943
|
+
}
|
|
1944
|
+
function isCrossOrigin(resolver) {
|
|
1945
|
+
try {
|
|
1946
|
+
const host = String(resolver.resolve("server.HTTP_HOST")[0] ?? "").toLowerCase();
|
|
1947
|
+
if (!host) return false;
|
|
1948
|
+
const src = resolver.resolve("server.HTTP_ORIGIN")[0] ?? resolver.resolve("server.HTTP_REFERER")[0];
|
|
1949
|
+
if (!src) return false;
|
|
1950
|
+
const srcHost = hostFromUrl(String(src));
|
|
1951
|
+
return srcHost !== null && srcHost !== host;
|
|
1952
|
+
} catch {
|
|
1953
|
+
return false;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function hostFromUrl(value) {
|
|
1957
|
+
try {
|
|
1958
|
+
return new URL(value).host.toLowerCase();
|
|
1959
|
+
} catch {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
722
1963
|
function matchValue(type, value, matchVal, matchObj) {
|
|
723
1964
|
if (value === null || value === void 0) {
|
|
724
1965
|
if (type === "isset") {
|
|
@@ -764,18 +2005,19 @@ function matchValue(type, value, matchVal, matchObj) {
|
|
|
764
2005
|
case "isset":
|
|
765
2006
|
return true;
|
|
766
2007
|
case "in_array": {
|
|
767
|
-
const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
|
|
2008
|
+
const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
|
|
768
2009
|
return arr.includes(strValue);
|
|
769
2010
|
}
|
|
770
2011
|
case "not_in_array": {
|
|
771
|
-
const arr = Array.isArray(matchVal) ? matchVal : [matchVal];
|
|
2012
|
+
const arr = (Array.isArray(matchVal) ? matchVal : [matchVal]).map((x) => String(x));
|
|
772
2013
|
return !arr.includes(strValue);
|
|
773
2014
|
}
|
|
774
2015
|
case "array_in_array": {
|
|
775
2016
|
if (!Array.isArray(value) || !Array.isArray(matchVal)) {
|
|
776
2017
|
return false;
|
|
777
2018
|
}
|
|
778
|
-
|
|
2019
|
+
const target = matchVal.map((x) => String(x));
|
|
2020
|
+
return value.some((v) => target.includes(String(v)));
|
|
779
2021
|
}
|
|
780
2022
|
case "hostname": {
|
|
781
2023
|
try {
|
|
@@ -852,6 +2094,9 @@ var RuleEngine = class {
|
|
|
852
2094
|
}
|
|
853
2095
|
for (const rule of this.#rules) {
|
|
854
2096
|
try {
|
|
2097
|
+
if (rule.when && !ruleAppliesTo(rule.when, resolver)) {
|
|
2098
|
+
continue;
|
|
2099
|
+
}
|
|
855
2100
|
const conditions = rule.rule_v2;
|
|
856
2101
|
if (!Array.isArray(conditions) || conditions.length === 0) {
|
|
857
2102
|
continue;
|
|
@@ -898,6 +2143,9 @@ var RuleEngine = class {
|
|
|
898
2143
|
if (parameter === "rules" && Array.isArray(condition.rules)) {
|
|
899
2144
|
return this.#evaluateRule(condition.rules, resolver);
|
|
900
2145
|
}
|
|
2146
|
+
if (match && match.type === "cross_origin") {
|
|
2147
|
+
return isCrossOrigin(resolver);
|
|
2148
|
+
}
|
|
901
2149
|
const params = Array.isArray(parameter) ? parameter : [parameter];
|
|
902
2150
|
const values = params.flatMap((p) => resolver.resolve(p));
|
|
903
2151
|
if (values.length === 0) {
|
|
@@ -962,7 +2210,7 @@ var PulseRuleClient = class {
|
|
|
962
2210
|
constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
|
|
963
2211
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
964
2212
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
|
|
965
|
-
this.#cacheTtl = cacheTtl
|
|
2213
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
|
|
966
2214
|
this.#etag = etag ?? null;
|
|
967
2215
|
if (!this.#siteUuid) {
|
|
968
2216
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
@@ -986,11 +2234,14 @@ var PulseRuleClient = class {
|
|
|
986
2234
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
987
2235
|
}
|
|
988
2236
|
const data = await response.json();
|
|
2237
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
2238
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
2239
|
+
}
|
|
989
2240
|
const result = {
|
|
990
2241
|
success: true,
|
|
991
2242
|
notModified: false,
|
|
992
2243
|
etag: response.headers?.get?.("etag") ?? null,
|
|
993
|
-
firewall:
|
|
2244
|
+
firewall: data.firewall,
|
|
994
2245
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
995
2246
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
996
2247
|
};
|
|
@@ -1092,6 +2343,29 @@ async function readCappedText(request, max) {
|
|
|
1092
2343
|
} catch {
|
|
1093
2344
|
return "";
|
|
1094
2345
|
}
|
|
2346
|
+
const body = clone.body;
|
|
2347
|
+
if (body && typeof body.getReader === "function") {
|
|
2348
|
+
const reader = body.getReader();
|
|
2349
|
+
const chunks = [];
|
|
2350
|
+
let buffered = 0;
|
|
2351
|
+
try {
|
|
2352
|
+
for (; ; ) {
|
|
2353
|
+
const { done, value } = await reader.read();
|
|
2354
|
+
if (done) break;
|
|
2355
|
+
if (!value || buffered >= max) continue;
|
|
2356
|
+
const take = Math.min(value.byteLength, max - buffered);
|
|
2357
|
+
chunks.push(take === value.byteLength ? value : value.subarray(0, take));
|
|
2358
|
+
buffered += take;
|
|
2359
|
+
}
|
|
2360
|
+
} catch {
|
|
2361
|
+
return "";
|
|
2362
|
+
}
|
|
2363
|
+
try {
|
|
2364
|
+
return new TextDecoder().decode(concatChunks(chunks, buffered));
|
|
2365
|
+
} catch {
|
|
2366
|
+
return "";
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
1095
2369
|
try {
|
|
1096
2370
|
const text = await clone.text();
|
|
1097
2371
|
return text.length > max ? text.slice(0, max) : text;
|
|
@@ -1099,6 +2373,15 @@ async function readCappedText(request, max) {
|
|
|
1099
2373
|
return "";
|
|
1100
2374
|
}
|
|
1101
2375
|
}
|
|
2376
|
+
function concatChunks(chunks, total) {
|
|
2377
|
+
const out = new Uint8Array(total);
|
|
2378
|
+
let offset = 0;
|
|
2379
|
+
for (const c of chunks) {
|
|
2380
|
+
out.set(c, offset);
|
|
2381
|
+
offset += c.byteLength;
|
|
2382
|
+
}
|
|
2383
|
+
return out;
|
|
2384
|
+
}
|
|
1102
2385
|
function parseMultipart(rawBody, boundary) {
|
|
1103
2386
|
const body = {};
|
|
1104
2387
|
const files = {};
|
|
@@ -1143,7 +2426,16 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1143
2426
|
headers[key.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
1144
2427
|
}
|
|
1145
2428
|
const host = headers.host || "localhost";
|
|
1146
|
-
|
|
2429
|
+
let url;
|
|
2430
|
+
try {
|
|
2431
|
+
url = new URL(req.url || "/", `http://${host}`);
|
|
2432
|
+
} catch {
|
|
2433
|
+
try {
|
|
2434
|
+
url = new URL(req.url || "/", "http://localhost");
|
|
2435
|
+
} catch {
|
|
2436
|
+
url = new URL("http://localhost/");
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
1147
2439
|
const query = {};
|
|
1148
2440
|
for (const [key, value] of url.searchParams) {
|
|
1149
2441
|
if (key in query) {
|
|
@@ -1154,6 +2446,7 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1154
2446
|
}
|
|
1155
2447
|
const contentType = headers["content-type"] || "";
|
|
1156
2448
|
let body = {};
|
|
2449
|
+
let files;
|
|
1157
2450
|
if (rawBody) {
|
|
1158
2451
|
if (contentType.includes("application/json")) {
|
|
1159
2452
|
try {
|
|
@@ -1166,6 +2459,13 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1166
2459
|
for (const [k, v] of new URLSearchParams(rawBody)) {
|
|
1167
2460
|
body[k] = k in body ? [].concat(body[k], v) : v;
|
|
1168
2461
|
}
|
|
2462
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
2463
|
+
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
2464
|
+
if (boundary) {
|
|
2465
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
2466
|
+
body = parsed.body;
|
|
2467
|
+
files = parsed.files;
|
|
2468
|
+
}
|
|
1169
2469
|
}
|
|
1170
2470
|
}
|
|
1171
2471
|
const uri = url.pathname + url.search;
|
|
@@ -1176,6 +2476,7 @@ function fromNodeRequest(req, rawBody = "") {
|
|
|
1176
2476
|
originalUrl: uri,
|
|
1177
2477
|
query,
|
|
1178
2478
|
body,
|
|
2479
|
+
files,
|
|
1179
2480
|
headers,
|
|
1180
2481
|
ip: forwarded.split(",")[0].trim() || req.socket && req.socket.remoteAddress || "",
|
|
1181
2482
|
cookies: parseCookies2(headers.cookie),
|
|
@@ -1199,15 +2500,43 @@ function parseCookies2(header) {
|
|
|
1199
2500
|
}
|
|
1200
2501
|
|
|
1201
2502
|
// src/protect/egress.js
|
|
1202
|
-
async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
2503
|
+
async function installEgressGuard({ shouldBlock, onBlock, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
1203
2504
|
const restores = [];
|
|
1204
2505
|
if (typeof shouldBlock !== "function") return () => {
|
|
1205
2506
|
};
|
|
2507
|
+
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
1206
2508
|
const block = (url, host, method) => {
|
|
1207
2509
|
if (!shouldBlock(url, host, method)) return false;
|
|
1208
2510
|
onBlock?.({ url, host, method });
|
|
1209
2511
|
return true;
|
|
1210
2512
|
};
|
|
2513
|
+
let screen = null;
|
|
2514
|
+
if (dnsScreen) {
|
|
2515
|
+
try {
|
|
2516
|
+
const resolveLookup = lookup ?? (await import("dns")).lookup;
|
|
2517
|
+
const { isIP } = await import("net");
|
|
2518
|
+
if (typeof resolveLookup === "function" && typeof isIP === "function") {
|
|
2519
|
+
screen = { lookup: resolveLookup, isIP, isExempt: (h) => exempt.has(String(h).toLowerCase()) };
|
|
2520
|
+
}
|
|
2521
|
+
} catch {
|
|
2522
|
+
screen = null;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
const resolvesToDisallowed = (url, host, method) => new Promise((resolve) => {
|
|
2526
|
+
if (!screen || !host || screen.isIP(host) !== 0 || screen.isExempt(host)) return resolve(false);
|
|
2527
|
+
try {
|
|
2528
|
+
screen.lookup(host, { all: true }, (err, addresses) => {
|
|
2529
|
+
if (err || !Array.isArray(addresses)) return resolve(false);
|
|
2530
|
+
for (const a of addresses) {
|
|
2531
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
2532
|
+
if (ip && block(url, ip, method)) return resolve(true);
|
|
2533
|
+
}
|
|
2534
|
+
resolve(false);
|
|
2535
|
+
});
|
|
2536
|
+
} catch {
|
|
2537
|
+
resolve(false);
|
|
2538
|
+
}
|
|
2539
|
+
});
|
|
1211
2540
|
const originalFetch = globalThis.fetch;
|
|
1212
2541
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
1213
2542
|
const guarded = async (input, init) => {
|
|
@@ -1219,7 +2548,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1219
2548
|
host = null;
|
|
1220
2549
|
}
|
|
1221
2550
|
const method = init && init.method || input && input.method || "GET";
|
|
1222
|
-
if (block(url, host, method)) {
|
|
2551
|
+
if (block(url, host, method) || await resolvesToDisallowed(url, host, method)) {
|
|
1223
2552
|
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
|
|
1224
2553
|
}
|
|
1225
2554
|
return originalFetch(input, init);
|
|
@@ -1233,7 +2562,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1233
2562
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
1234
2563
|
try {
|
|
1235
2564
|
const mod = await import(moduleName);
|
|
1236
|
-
const restore = patchHttpModule(mod.default ?? mod, block);
|
|
2565
|
+
const restore = patchHttpModule(mod.default ?? mod, block, screen);
|
|
1237
2566
|
if (restore) restores.push(restore);
|
|
1238
2567
|
} catch {
|
|
1239
2568
|
}
|
|
@@ -1271,7 +2600,7 @@ async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
|
1271
2600
|
}
|
|
1272
2601
|
};
|
|
1273
2602
|
}
|
|
1274
|
-
function patchHttpModule(http, block) {
|
|
2603
|
+
function patchHttpModule(http, block, screen) {
|
|
1275
2604
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
1276
2605
|
const originalRequest = http.request;
|
|
1277
2606
|
const originalGet = http.get;
|
|
@@ -1280,14 +2609,25 @@ function patchHttpModule(http, block) {
|
|
|
1280
2609
|
if (target && block(target.url, target.host, target.method)) {
|
|
1281
2610
|
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
1282
2611
|
}
|
|
2612
|
+
if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
|
|
2613
|
+
try {
|
|
2614
|
+
args = withScreeningLookup(args, target, block, screen.lookup);
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
1283
2618
|
return original.apply(this, args);
|
|
1284
2619
|
};
|
|
1285
|
-
|
|
1286
|
-
|
|
2620
|
+
const guardedRequest = wrap(originalRequest);
|
|
2621
|
+
http.request = guardedRequest;
|
|
2622
|
+
let guardedGet;
|
|
2623
|
+
if (typeof originalGet === "function") {
|
|
2624
|
+
guardedGet = wrap(originalGet);
|
|
2625
|
+
http.get = guardedGet;
|
|
2626
|
+
}
|
|
1287
2627
|
http.__patchstackGuarded = true;
|
|
1288
2628
|
return () => {
|
|
1289
|
-
http.request = originalRequest;
|
|
1290
|
-
if (
|
|
2629
|
+
if (http.request === guardedRequest) http.request = originalRequest;
|
|
2630
|
+
if (guardedGet && http.get === guardedGet) http.get = originalGet;
|
|
1291
2631
|
delete http.__patchstackGuarded;
|
|
1292
2632
|
};
|
|
1293
2633
|
}
|
|
@@ -1300,11 +2640,11 @@ function extractHttpTarget(args) {
|
|
|
1300
2640
|
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
1301
2641
|
}
|
|
1302
2642
|
if (first && typeof first === "object") {
|
|
1303
|
-
const host = normalizeHost(first.hostname || first.host);
|
|
2643
|
+
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
1304
2644
|
const protocol = first.protocol || "http:";
|
|
1305
2645
|
const port = first.port ? `:${first.port}` : "";
|
|
1306
|
-
const
|
|
1307
|
-
return { url: `${protocol}//${host}${port}${
|
|
2646
|
+
const path7 = first.path || "/";
|
|
2647
|
+
return { url: `${protocol}//${host}${port}${path7}`, host, method: first.method || "GET" };
|
|
1308
2648
|
}
|
|
1309
2649
|
} catch {
|
|
1310
2650
|
}
|
|
@@ -1317,6 +2657,61 @@ function normalizeHost(raw) {
|
|
|
1317
2657
|
if ((host.match(/:/g) || []).length > 1) return host;
|
|
1318
2658
|
return host.split(":")[0];
|
|
1319
2659
|
}
|
|
2660
|
+
function screenResolved(addresses, target, block) {
|
|
2661
|
+
for (const a of addresses || []) {
|
|
2662
|
+
const ip = a && typeof a === "object" ? a.address : a;
|
|
2663
|
+
if (ip && block(target.url, ip, target.method)) return ip;
|
|
2664
|
+
}
|
|
2665
|
+
return null;
|
|
2666
|
+
}
|
|
2667
|
+
function withScreeningLookup(args, target, block, lookup) {
|
|
2668
|
+
const screeningLookup = (hostname, options, callback) => {
|
|
2669
|
+
let opts = options;
|
|
2670
|
+
let cb = callback;
|
|
2671
|
+
if (typeof opts === "function") {
|
|
2672
|
+
cb = opts;
|
|
2673
|
+
opts = {};
|
|
2674
|
+
}
|
|
2675
|
+
if (!opts || typeof opts !== "object") opts = {};
|
|
2676
|
+
try {
|
|
2677
|
+
lookup(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
2678
|
+
if (err) return cb(err);
|
|
2679
|
+
const list = Array.isArray(addresses) ? addresses : [];
|
|
2680
|
+
const blocked = screenResolved(list, target, block);
|
|
2681
|
+
if (blocked) {
|
|
2682
|
+
return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
|
|
2683
|
+
}
|
|
2684
|
+
if (opts.all) return cb(null, list);
|
|
2685
|
+
const first = list[0];
|
|
2686
|
+
if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
|
|
2687
|
+
return cb(null, first.address, first.family);
|
|
2688
|
+
});
|
|
2689
|
+
} catch {
|
|
2690
|
+
try {
|
|
2691
|
+
lookup(hostname, opts, cb);
|
|
2692
|
+
} catch {
|
|
2693
|
+
cb(new Error(`Patchstack: lookup failed for ${hostname}`));
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
};
|
|
2697
|
+
return injectLookupOption(args, screeningLookup);
|
|
2698
|
+
}
|
|
2699
|
+
function injectLookupOption(args, lookup) {
|
|
2700
|
+
const first = args[0];
|
|
2701
|
+
if (first && typeof first === "object" && !(first instanceof URL)) {
|
|
2702
|
+
return [{ ...first, lookup }, ...args.slice(1)];
|
|
2703
|
+
}
|
|
2704
|
+
const rest = args.slice(1);
|
|
2705
|
+
const optIdx = rest.findIndex((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
2706
|
+
if (optIdx !== -1) {
|
|
2707
|
+
const next = [...rest];
|
|
2708
|
+
next[optIdx] = { ...rest[optIdx], lookup };
|
|
2709
|
+
return [first, ...next];
|
|
2710
|
+
}
|
|
2711
|
+
const cbIdx = rest.findIndex((a) => typeof a === "function");
|
|
2712
|
+
if (cbIdx === -1) return [first, { lookup }, ...rest];
|
|
2713
|
+
return [first, ...rest.slice(0, cbIdx), { lookup }, ...rest.slice(cbIdx)];
|
|
2714
|
+
}
|
|
1320
2715
|
|
|
1321
2716
|
// src/protect/defaults.js
|
|
1322
2717
|
var DEFAULT_RESPONSE_RULES = [
|
|
@@ -1483,7 +2878,7 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
|
1483
2878
|
|
|
1484
2879
|
// src/protect/runtime.js
|
|
1485
2880
|
var import_node_fs = require("fs");
|
|
1486
|
-
var
|
|
2881
|
+
var import_node_path7 = require("path");
|
|
1487
2882
|
|
|
1488
2883
|
// src/protect/supabase-guard.js
|
|
1489
2884
|
var GUARD_PATH = "/_patchstack/guard";
|
|
@@ -1560,22 +2955,33 @@ async function createProtection(options = {}) {
|
|
|
1560
2955
|
const onError = options.onError;
|
|
1561
2956
|
const onDetect = options.onDetect ?? defaultOnDetect;
|
|
1562
2957
|
const bundle = await resolveRules(options);
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
2958
|
+
let requestRules;
|
|
2959
|
+
let responseRules;
|
|
2960
|
+
let egressRules;
|
|
2961
|
+
let screenCap;
|
|
2962
|
+
let engine;
|
|
2963
|
+
let responseRuleSet;
|
|
2964
|
+
let egressEngine;
|
|
2965
|
+
const applyBundle = (delivered) => {
|
|
2966
|
+
const incoming = delivered.firewall ?? [];
|
|
2967
|
+
requestRules = byPhase(incoming, "request");
|
|
2968
|
+
responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
|
|
2969
|
+
screenCap = responseScreenCap(responseRules);
|
|
2970
|
+
egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
|
|
2971
|
+
engine = new RuleEngine({
|
|
2972
|
+
firewall: requestRules,
|
|
2973
|
+
whitelists: delivered.whitelists,
|
|
2974
|
+
whitelist_keys: delivered.whitelist_keys,
|
|
2975
|
+
onError
|
|
2976
|
+
});
|
|
2977
|
+
responseRuleSet = responseRules.map((rule) => ({
|
|
2978
|
+
rule,
|
|
2979
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
2980
|
+
redactors: rule.action === "redact" || rule.action === "encode" ? extractRedactors(rule) : null
|
|
2981
|
+
}));
|
|
2982
|
+
egressEngine = new RuleEngine({ firewall: egressRules, onError });
|
|
2983
|
+
};
|
|
2984
|
+
applyBundle(bundle);
|
|
1579
2985
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
1580
2986
|
const decide = (phase, result, block, allow2) => {
|
|
1581
2987
|
if (!result || !result.blocked) return allow2();
|
|
@@ -1609,20 +3015,26 @@ async function createProtection(options = {}) {
|
|
|
1609
3015
|
const headers = { ...meta.headers || {} };
|
|
1610
3016
|
for (const { rule, redactors } of redactions) {
|
|
1611
3017
|
const mask = maskFn(rule.category);
|
|
1612
|
-
|
|
3018
|
+
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
3019
|
+
const pathRedactors = redactors.filter((r) => r.jsonPath);
|
|
3020
|
+
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
3021
|
+
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
3022
|
+
if (!spanRedactors.length) continue;
|
|
3023
|
+
body = applyRedactors(body, spanRedactors, mask, transform);
|
|
3024
|
+
if (transform) continue;
|
|
1613
3025
|
for (const name of Object.keys(headers)) {
|
|
1614
3026
|
const value = headers[name];
|
|
1615
3027
|
if (typeof value === "string") {
|
|
1616
|
-
headers[name] = applyRedactors(value,
|
|
3028
|
+
headers[name] = applyRedactors(value, spanRedactors, mask);
|
|
1617
3029
|
} else if (Array.isArray(value)) {
|
|
1618
|
-
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
3030
|
+
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item);
|
|
1619
3031
|
}
|
|
1620
3032
|
}
|
|
1621
3033
|
}
|
|
1622
3034
|
return { verdict: "redact", body, headers };
|
|
1623
3035
|
};
|
|
1624
3036
|
const screenResp = async (response) => {
|
|
1625
|
-
const text = await readTextResponse(response);
|
|
3037
|
+
const text = await readTextResponse(response, screenCap);
|
|
1626
3038
|
if (text == null) return response;
|
|
1627
3039
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1628
3040
|
if (r.verdict === "block") return leakResponse();
|
|
@@ -1635,12 +3047,15 @@ async function createProtection(options = {}) {
|
|
|
1635
3047
|
const chunks = [];
|
|
1636
3048
|
let size = 0;
|
|
1637
3049
|
let overflow = false;
|
|
1638
|
-
const MAX =
|
|
3050
|
+
const MAX = screenCap;
|
|
1639
3051
|
const collect = (chunk, enc) => {
|
|
1640
3052
|
if (chunk == null) return;
|
|
1641
3053
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
1642
3054
|
size += buf.length;
|
|
1643
3055
|
if (size > MAX) {
|
|
3056
|
+
for (const c of chunks) origWrite(c);
|
|
3057
|
+
chunks.length = 0;
|
|
3058
|
+
origWrite(buf);
|
|
1644
3059
|
overflow = true;
|
|
1645
3060
|
return;
|
|
1646
3061
|
}
|
|
@@ -1667,6 +3082,7 @@ async function createProtection(options = {}) {
|
|
|
1667
3082
|
return origEnd(cb);
|
|
1668
3083
|
}
|
|
1669
3084
|
collect(chunk, enc);
|
|
3085
|
+
if (overflow) return origEnd(cb);
|
|
1670
3086
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
1671
3087
|
let ct = res.getHeader ? res.getHeader("content-type") : void 0;
|
|
1672
3088
|
if (Array.isArray(ct)) ct = ct[0];
|
|
@@ -1698,6 +3114,7 @@ async function createProtection(options = {}) {
|
|
|
1698
3114
|
if (r.headers && res.setHeader) {
|
|
1699
3115
|
const current = res.getHeaders ? res.getHeaders() : {};
|
|
1700
3116
|
for (const [name, value] of Object.entries(r.headers)) {
|
|
3117
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
1701
3118
|
if (Array.isArray(value)) {
|
|
1702
3119
|
try {
|
|
1703
3120
|
res.setHeader(name, value);
|
|
@@ -1733,7 +3150,9 @@ async function createProtection(options = {}) {
|
|
|
1733
3150
|
};
|
|
1734
3151
|
const protection = {
|
|
1735
3152
|
mode,
|
|
1736
|
-
rules
|
|
3153
|
+
get rules() {
|
|
3154
|
+
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
3155
|
+
},
|
|
1737
3156
|
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
1738
3157
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
1739
3158
|
screenResponse: (response) => screenResp(response),
|
|
@@ -1844,27 +3263,101 @@ async function createProtection(options = {}) {
|
|
|
1844
3263
|
}
|
|
1845
3264
|
};
|
|
1846
3265
|
if (options.egress) {
|
|
1847
|
-
protection.uninstallEgress = await installEgressGuard({
|
|
3266
|
+
protection.uninstallEgress = await installEgressGuard({
|
|
3267
|
+
shouldBlock: egressShouldBlock,
|
|
3268
|
+
onBlock: options.onEgressBlock,
|
|
3269
|
+
dnsScreen: options.screenDns !== false,
|
|
3270
|
+
allowHosts: options.allowHosts
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
if (options.refreshMs > 0 && (options.siteUuid || options.token)) {
|
|
3274
|
+
const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
|
|
3275
|
+
let reportManifest2 = null;
|
|
3276
|
+
if (options.siteUuid && options.reportManifest !== false && cwd) {
|
|
3277
|
+
try {
|
|
3278
|
+
({ reportManifest: reportManifest2 } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
|
|
3279
|
+
} catch (err) {
|
|
3280
|
+
onError?.(err);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
const timer = setInterval(() => {
|
|
3284
|
+
(async () => {
|
|
3285
|
+
if (reportManifest2) {
|
|
3286
|
+
try {
|
|
3287
|
+
await reportManifest2(cwd);
|
|
3288
|
+
} catch (err) {
|
|
3289
|
+
onError?.(err);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
applyBundle(await resolveRules(options));
|
|
3293
|
+
})().catch((err) => onError?.(err));
|
|
3294
|
+
}, options.refreshMs);
|
|
3295
|
+
timer.unref?.();
|
|
3296
|
+
protection.stopRefresh = () => clearInterval(timer);
|
|
1848
3297
|
}
|
|
1849
3298
|
return protection;
|
|
1850
3299
|
}
|
|
1851
3300
|
function byPhase(rules, phase) {
|
|
1852
3301
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
1853
3302
|
}
|
|
1854
|
-
async function readTextResponse(response) {
|
|
3303
|
+
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
1855
3304
|
if (!response || typeof response.clone !== "function") return null;
|
|
1856
3305
|
const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
|
|
1857
3306
|
const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1858
3307
|
if (!isText) return null;
|
|
1859
3308
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
1860
|
-
if (len && len >
|
|
3309
|
+
if (len && len > cap) return null;
|
|
3310
|
+
let clone;
|
|
3311
|
+
try {
|
|
3312
|
+
clone = response.clone();
|
|
3313
|
+
} catch {
|
|
3314
|
+
return null;
|
|
3315
|
+
}
|
|
3316
|
+
const body = clone.body;
|
|
3317
|
+
if (body && typeof body.getReader === "function") {
|
|
3318
|
+
const reader = body.getReader();
|
|
3319
|
+
const chunks = [];
|
|
3320
|
+
let size = 0;
|
|
3321
|
+
let over = false;
|
|
3322
|
+
try {
|
|
3323
|
+
for (; ; ) {
|
|
3324
|
+
const { done, value } = await reader.read();
|
|
3325
|
+
if (done) break;
|
|
3326
|
+
if (!value) continue;
|
|
3327
|
+
size += value.byteLength;
|
|
3328
|
+
if (over) continue;
|
|
3329
|
+
if (size > cap) {
|
|
3330
|
+
over = true;
|
|
3331
|
+
continue;
|
|
3332
|
+
}
|
|
3333
|
+
chunks.push(value);
|
|
3334
|
+
}
|
|
3335
|
+
} catch {
|
|
3336
|
+
return null;
|
|
3337
|
+
}
|
|
3338
|
+
if (over) return null;
|
|
3339
|
+
try {
|
|
3340
|
+
return new TextDecoder().decode(concatBytes(chunks, size));
|
|
3341
|
+
} catch {
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
1861
3345
|
try {
|
|
1862
|
-
const text = await
|
|
1863
|
-
return text.length >
|
|
3346
|
+
const text = await clone.text();
|
|
3347
|
+
return text.length > cap ? null : text;
|
|
1864
3348
|
} catch {
|
|
1865
3349
|
return null;
|
|
1866
3350
|
}
|
|
1867
3351
|
}
|
|
3352
|
+
function concatBytes(chunks, total) {
|
|
3353
|
+
const out = new Uint8Array(total);
|
|
3354
|
+
let offset = 0;
|
|
3355
|
+
for (const c of chunks) {
|
|
3356
|
+
out.set(c, offset);
|
|
3357
|
+
offset += c.byteLength;
|
|
3358
|
+
}
|
|
3359
|
+
return out;
|
|
3360
|
+
}
|
|
1868
3361
|
function headerObject(headers) {
|
|
1869
3362
|
const out = {};
|
|
1870
3363
|
headers?.forEach?.((v, k) => {
|
|
@@ -1876,36 +3369,143 @@ function headerObject(headers) {
|
|
|
1876
3369
|
}
|
|
1877
3370
|
function extractRedactors(rule) {
|
|
1878
3371
|
const out = [];
|
|
1879
|
-
const
|
|
3372
|
+
const walk2 = (conds) => {
|
|
1880
3373
|
for (const c of conds ?? []) {
|
|
1881
|
-
if (Array.isArray(c.rules))
|
|
3374
|
+
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
1882
3375
|
const m = c.match;
|
|
1883
3376
|
if (!m) continue;
|
|
1884
3377
|
if (m.type === "regex" && typeof m.value === "string") {
|
|
1885
|
-
const
|
|
1886
|
-
if (
|
|
1887
|
-
const flags =
|
|
3378
|
+
const safe = safeRegExp(m.value);
|
|
3379
|
+
if (safe) {
|
|
3380
|
+
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
1888
3381
|
try {
|
|
1889
|
-
out.push({ re: new RegExp(
|
|
3382
|
+
out.push({ re: new RegExp(safe.source, flags) });
|
|
1890
3383
|
} catch {
|
|
1891
3384
|
}
|
|
1892
3385
|
}
|
|
1893
3386
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
1894
3387
|
out.push({ literal: String(m.value) });
|
|
3388
|
+
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
3389
|
+
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
3390
|
+
for (const key of keys) {
|
|
3391
|
+
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
3392
|
+
}
|
|
1895
3393
|
}
|
|
1896
3394
|
}
|
|
1897
3395
|
};
|
|
1898
|
-
|
|
3396
|
+
walk2(rule.rule_v2);
|
|
1899
3397
|
return out;
|
|
1900
3398
|
}
|
|
1901
|
-
function
|
|
3399
|
+
function htmlEscape(str) {
|
|
3400
|
+
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3401
|
+
}
|
|
3402
|
+
function applyRedactors(body, redactors, mask, transform) {
|
|
1902
3403
|
let out = body;
|
|
1903
3404
|
for (const r of redactors) {
|
|
1904
|
-
if (r.re) out = out.replace(r.re, mask);
|
|
1905
|
-
else if (r.literal) out = out.split(r.literal).join(mask);
|
|
3405
|
+
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
3406
|
+
else if (r.literal) out = out.split(r.literal).join(transform ? transform(r.literal) : mask);
|
|
1906
3407
|
}
|
|
1907
3408
|
return out;
|
|
1908
3409
|
}
|
|
3410
|
+
function isBodyParam(parameter) {
|
|
3411
|
+
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
3412
|
+
}
|
|
3413
|
+
function conditionPredicate(condition) {
|
|
3414
|
+
if (!condition || !condition.type) return () => true;
|
|
3415
|
+
return (value) => {
|
|
3416
|
+
try {
|
|
3417
|
+
return matchValue(condition.type, value, condition.value, condition);
|
|
3418
|
+
} catch {
|
|
3419
|
+
return false;
|
|
3420
|
+
}
|
|
3421
|
+
};
|
|
3422
|
+
}
|
|
3423
|
+
var DEFAULT_SCREEN_CAP = 512 * 1024;
|
|
3424
|
+
function responseScreenCap(rules) {
|
|
3425
|
+
let cap = DEFAULT_SCREEN_CAP;
|
|
3426
|
+
for (const r of rules ?? []) {
|
|
3427
|
+
if (r && r.bypass_limit === true) return Infinity;
|
|
3428
|
+
const n = Number(r && r.max_bytes);
|
|
3429
|
+
if (Number.isFinite(n) && n > cap) cap = n;
|
|
3430
|
+
}
|
|
3431
|
+
return cap;
|
|
3432
|
+
}
|
|
3433
|
+
function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
3434
|
+
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
3435
|
+
const head = text.trimStart()[0];
|
|
3436
|
+
if (head !== "{" && head !== "[") return text;
|
|
3437
|
+
const preserved = preserveBigInts(text);
|
|
3438
|
+
let obj;
|
|
3439
|
+
try {
|
|
3440
|
+
obj = JSON.parse(preserved);
|
|
3441
|
+
} catch {
|
|
3442
|
+
return text;
|
|
3443
|
+
}
|
|
3444
|
+
let changed = false;
|
|
3445
|
+
for (const r of pathRedactors) {
|
|
3446
|
+
const pred = conditionPredicate(r.condition);
|
|
3447
|
+
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
3448
|
+
try {
|
|
3449
|
+
if (pred(loc.value)) {
|
|
3450
|
+
loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
|
|
3451
|
+
changed = true;
|
|
3452
|
+
}
|
|
3453
|
+
} catch {
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
return changed ? restoreBigInts(JSON.stringify(obj)) : text;
|
|
3458
|
+
}
|
|
3459
|
+
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
3460
|
+
var BIGINT_CLOSE = "__DNEGIB__";
|
|
3461
|
+
function preserveBigInts(text) {
|
|
3462
|
+
let out = "";
|
|
3463
|
+
let inStr = false;
|
|
3464
|
+
for (let i = 0; i < text.length; ) {
|
|
3465
|
+
const ch = text[i];
|
|
3466
|
+
if (inStr) {
|
|
3467
|
+
out += ch;
|
|
3468
|
+
if (ch === "\\") {
|
|
3469
|
+
out += text[i + 1] ?? "";
|
|
3470
|
+
i += 2;
|
|
3471
|
+
continue;
|
|
3472
|
+
}
|
|
3473
|
+
if (ch === '"') inStr = false;
|
|
3474
|
+
i++;
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (ch === '"') {
|
|
3478
|
+
inStr = true;
|
|
3479
|
+
out += ch;
|
|
3480
|
+
i++;
|
|
3481
|
+
continue;
|
|
3482
|
+
}
|
|
3483
|
+
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
3484
|
+
let j = ch === "-" ? i + 1 : i;
|
|
3485
|
+
let digits = 0;
|
|
3486
|
+
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
3487
|
+
digits++;
|
|
3488
|
+
j++;
|
|
3489
|
+
}
|
|
3490
|
+
const next = text[j];
|
|
3491
|
+
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
3492
|
+
if (isIntToken && digits >= 16) {
|
|
3493
|
+
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
3494
|
+
} else {
|
|
3495
|
+
out += text.slice(i, j || i + 1);
|
|
3496
|
+
}
|
|
3497
|
+
i = j > i ? j : i + 1;
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
out += ch;
|
|
3501
|
+
i++;
|
|
3502
|
+
}
|
|
3503
|
+
return out;
|
|
3504
|
+
}
|
|
3505
|
+
function restoreBigInts(text) {
|
|
3506
|
+
if (!text.includes(BIGINT_OPEN)) return text;
|
|
3507
|
+
return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
|
|
3508
|
+
}
|
|
1909
3509
|
function rebuildResponse(response, body, redactedHeaders) {
|
|
1910
3510
|
const headers = new Headers(response.headers);
|
|
1911
3511
|
headers.delete("content-length");
|
|
@@ -2016,7 +3616,7 @@ function makeCache(options) {
|
|
|
2016
3616
|
};
|
|
2017
3617
|
}
|
|
2018
3618
|
function cachePath(dir) {
|
|
2019
|
-
return (0,
|
|
3619
|
+
return (0, import_node_path7.join)(dir, "patchstack-rules.json");
|
|
2020
3620
|
}
|
|
2021
3621
|
function cacheWrite(dir, env) {
|
|
2022
3622
|
if (!dir) return;
|