@patchstack/connect 0.3.19 → 0.3.21
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 +1887 -210
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +35 -0
- package/dist/protect.js +689 -193
- 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_path2.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_path2, 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_path2 = __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_path3.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_path3;
|
|
268
|
+
var init_consistency = __esm({
|
|
269
|
+
"src/parsers/consistency.ts"() {
|
|
270
|
+
"use strict";
|
|
271
|
+
import_promises2 = require("fs/promises");
|
|
272
|
+
import_node_path3 = __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_path4.default.sep === pkgPath ? import_node_path4.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_path4;
|
|
360
|
+
var init_npm = __esm({
|
|
361
|
+
"src/parsers/npm.ts"() {
|
|
362
|
+
"use strict";
|
|
363
|
+
import_promises3 = require("fs/promises");
|
|
364
|
+
import_node_path4 = __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_path5.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_path5.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_path5.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_path5.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_path5.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_path5;
|
|
474
|
+
var init_node_modules = __esm({
|
|
475
|
+
"src/parsers/node_modules.ts"() {
|
|
476
|
+
"use strict";
|
|
477
|
+
import_promises4 = require("fs/promises");
|
|
478
|
+
import_node_path5 = __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_path6.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_path6.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_path6;
|
|
931
|
+
var init_yarn = __esm({
|
|
932
|
+
"src/parsers/yarn.ts"() {
|
|
933
|
+
"use strict";
|
|
934
|
+
import_promises6 = require("fs/promises");
|
|
935
|
+
import_node_path6 = __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_path7.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_path7.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_path7, LOCKFILE_CANDIDATES;
|
|
1049
|
+
var init_parsers = __esm({
|
|
1050
|
+
"src/parsers/index.ts"() {
|
|
1051
|
+
"use strict";
|
|
1052
|
+
import_promises7 = require("fs/promises");
|
|
1053
|
+
import_node_path7 = __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 = [
|
|
@@ -1470,20 +2865,200 @@ function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
|
1470
2865
|
navigator.clipboard.writeText(copyText.value);
|
|
1471
2866
|
}
|
|
1472
2867
|
|
|
1473
|
-
function hover(element) {
|
|
1474
|
-
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
|
|
2868
|
+
function hover(element) {
|
|
2869
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
function unhover(element) {
|
|
2873
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
|
|
2874
|
+
}
|
|
2875
|
+
</script>
|
|
2876
|
+
</html>`;
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// src/protect/rules/store.js
|
|
2880
|
+
var import_node_fs = require("fs");
|
|
2881
|
+
var import_node_path = require("path");
|
|
2882
|
+
function makeStore(options = {}) {
|
|
2883
|
+
let mem = null;
|
|
2884
|
+
const durable = durableTier(options);
|
|
2885
|
+
return {
|
|
2886
|
+
async read() {
|
|
2887
|
+
if (mem) return mem;
|
|
2888
|
+
const env = await durable.read();
|
|
2889
|
+
if (env) mem = env;
|
|
2890
|
+
return env;
|
|
2891
|
+
},
|
|
2892
|
+
async write(env) {
|
|
2893
|
+
if (env) mem = env;
|
|
2894
|
+
await durable.write(env);
|
|
2895
|
+
}
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
function durableTier(options) {
|
|
2899
|
+
const adapter = options.ruleCache;
|
|
2900
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
2901
|
+
return {
|
|
2902
|
+
read: async () => {
|
|
2903
|
+
try {
|
|
2904
|
+
return toEnvelope(await adapter.read());
|
|
2905
|
+
} catch {
|
|
2906
|
+
return null;
|
|
2907
|
+
}
|
|
2908
|
+
},
|
|
2909
|
+
write: async (env) => {
|
|
2910
|
+
try {
|
|
2911
|
+
await adapter.write(env);
|
|
2912
|
+
} catch {
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
};
|
|
2916
|
+
}
|
|
2917
|
+
const dir = options.cacheDir;
|
|
2918
|
+
return {
|
|
2919
|
+
read: async () => cacheRead(dir),
|
|
2920
|
+
write: async (env) => cacheWrite(dir, env)
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
function cachePath(dir) {
|
|
2924
|
+
return (0, import_node_path.join)(dir, "patchstack-rules.json");
|
|
2925
|
+
}
|
|
2926
|
+
function cacheWrite(dir, env) {
|
|
2927
|
+
if (!dir) return;
|
|
2928
|
+
try {
|
|
2929
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
2930
|
+
(0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
|
|
2931
|
+
} catch {
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
function cacheRead(dir) {
|
|
2935
|
+
if (!dir) return null;
|
|
2936
|
+
try {
|
|
2937
|
+
return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
|
|
2938
|
+
} catch {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
function toEnvelope(value) {
|
|
2943
|
+
if (!value || typeof value !== "object") return null;
|
|
2944
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
2945
|
+
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
2946
|
+
}
|
|
2947
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
2948
|
+
return { bundle: value, etag: null };
|
|
2949
|
+
}
|
|
2950
|
+
return null;
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// src/protect/rules/source.js
|
|
2954
|
+
async function resolveRules(options, store) {
|
|
2955
|
+
if (options.siteUuid) {
|
|
2956
|
+
const prior = await store.read();
|
|
2957
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
2958
|
+
const res = await client.getRules();
|
|
2959
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
2960
|
+
if (res.success && !res.notModified) {
|
|
2961
|
+
const bundle = normalizeBundle(res);
|
|
2962
|
+
await store.write({ bundle, etag: res.etag ?? null });
|
|
2963
|
+
return bundle;
|
|
1475
2964
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
2965
|
+
if (prior?.bundle) {
|
|
2966
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
2967
|
+
return normalizeBundle(prior.bundle);
|
|
1479
2968
|
}
|
|
1480
|
-
|
|
1481
|
-
|
|
2969
|
+
if (options.rules) {
|
|
2970
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
2971
|
+
return normalizeBundle(options.rules);
|
|
2972
|
+
}
|
|
2973
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
2974
|
+
return emptyBundle();
|
|
2975
|
+
}
|
|
2976
|
+
if (options.token) {
|
|
2977
|
+
const prior = await store.read();
|
|
2978
|
+
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
2979
|
+
const res = await client.getRules();
|
|
2980
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
2981
|
+
if (res.success && !res.notModified) {
|
|
2982
|
+
const bundle = normalizeBundle(res);
|
|
2983
|
+
await store.write({ bundle, etag: res.etag ?? null });
|
|
2984
|
+
return bundle;
|
|
2985
|
+
}
|
|
2986
|
+
if (prior?.bundle) {
|
|
2987
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
2988
|
+
return normalizeBundle(prior.bundle);
|
|
2989
|
+
}
|
|
2990
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
2991
|
+
return emptyBundle();
|
|
2992
|
+
}
|
|
2993
|
+
if (options.rules) {
|
|
2994
|
+
return normalizeBundle(options.rules);
|
|
2995
|
+
}
|
|
2996
|
+
return emptyBundle();
|
|
2997
|
+
}
|
|
2998
|
+
function normalizeBundle(b) {
|
|
2999
|
+
return {
|
|
3000
|
+
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
3001
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
3002
|
+
whitelist_keys: b.whitelist_keys ?? {}
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function emptyBundle() {
|
|
3006
|
+
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1482
3007
|
}
|
|
1483
3008
|
|
|
1484
|
-
// src/protect/
|
|
1485
|
-
var
|
|
1486
|
-
var
|
|
3009
|
+
// src/protect/rules/refresh.js
|
|
3010
|
+
var JITTER_FRACTION3 = 0.1;
|
|
3011
|
+
var MAX_BACKOFF_MULTIPLIER = 8;
|
|
3012
|
+
function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
3013
|
+
let stopped = false;
|
|
3014
|
+
let failures = 0;
|
|
3015
|
+
let timer = null;
|
|
3016
|
+
const schedule = () => {
|
|
3017
|
+
if (stopped) return;
|
|
3018
|
+
const backoff = Math.min(2 ** failures, MAX_BACKOFF_MULTIPLIER);
|
|
3019
|
+
const jitter = 1 - Math.random() * JITTER_FRACTION3;
|
|
3020
|
+
const delay = refreshMs * backoff * jitter;
|
|
3021
|
+
timer = setTimeout(run, delay);
|
|
3022
|
+
timer?.unref?.();
|
|
3023
|
+
};
|
|
3024
|
+
const run = async () => {
|
|
3025
|
+
if (stopped) return;
|
|
3026
|
+
try {
|
|
3027
|
+
await tick();
|
|
3028
|
+
failures = 0;
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
failures++;
|
|
3031
|
+
onError?.(err);
|
|
3032
|
+
}
|
|
3033
|
+
schedule();
|
|
3034
|
+
};
|
|
3035
|
+
schedule();
|
|
3036
|
+
return {
|
|
3037
|
+
stop() {
|
|
3038
|
+
stopped = true;
|
|
3039
|
+
if (timer) clearTimeout(timer);
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
}
|
|
3043
|
+
function makeRefreshHandler(tick, secret) {
|
|
3044
|
+
return async (request) => {
|
|
3045
|
+
if (!secret) return new Response("not found", { status: 404 });
|
|
3046
|
+
let provided = null;
|
|
3047
|
+
try {
|
|
3048
|
+
provided = request?.headers?.get?.("x-patchstack-refresh") ?? new URL(request.url).searchParams.get("token");
|
|
3049
|
+
} catch {
|
|
3050
|
+
provided = null;
|
|
3051
|
+
}
|
|
3052
|
+
if (provided !== secret) return new Response("forbidden", { status: 403 });
|
|
3053
|
+
let refreshed = true;
|
|
3054
|
+
try {
|
|
3055
|
+
await tick();
|
|
3056
|
+
} catch {
|
|
3057
|
+
refreshed = false;
|
|
3058
|
+
}
|
|
3059
|
+
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
3060
|
+
};
|
|
3061
|
+
}
|
|
1487
3062
|
|
|
1488
3063
|
// src/protect/supabase-guard.js
|
|
1489
3064
|
var GUARD_PATH = "/_patchstack/guard";
|
|
@@ -1559,23 +3134,35 @@ async function createProtection(options = {}) {
|
|
|
1559
3134
|
const mode = options.mode === "block" ? "block" : "dry-run";
|
|
1560
3135
|
const onError = options.onError;
|
|
1561
3136
|
const onDetect = options.onDetect ?? defaultOnDetect;
|
|
1562
|
-
const
|
|
1563
|
-
const
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
3137
|
+
const store = makeStore(options);
|
|
3138
|
+
const bundle = await resolveRules(options, store);
|
|
3139
|
+
let requestRules;
|
|
3140
|
+
let responseRules;
|
|
3141
|
+
let egressRules;
|
|
3142
|
+
let screenCap;
|
|
3143
|
+
let engine;
|
|
3144
|
+
let responseRuleSet;
|
|
3145
|
+
let egressEngine;
|
|
3146
|
+
const applyBundle = (delivered) => {
|
|
3147
|
+
const incoming = delivered.firewall ?? [];
|
|
3148
|
+
requestRules = byPhase(incoming, "request");
|
|
3149
|
+
responseRules = [...options.responseRules ?? DEFAULT_RESPONSE_RULES, ...byPhase(incoming, "response")];
|
|
3150
|
+
screenCap = responseScreenCap(responseRules);
|
|
3151
|
+
egressRules = [...options.egressRules ?? DEFAULT_EGRESS_RULES, ...byPhase(incoming, "egress")];
|
|
3152
|
+
engine = new RuleEngine({
|
|
3153
|
+
firewall: requestRules,
|
|
3154
|
+
whitelists: delivered.whitelists,
|
|
3155
|
+
whitelist_keys: delivered.whitelist_keys,
|
|
3156
|
+
onError
|
|
3157
|
+
});
|
|
3158
|
+
responseRuleSet = responseRules.map((rule) => ({
|
|
3159
|
+
rule,
|
|
3160
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
3161
|
+
redactors: rule.action === "redact" || rule.action === "encode" ? extractRedactors(rule) : null
|
|
3162
|
+
}));
|
|
3163
|
+
egressEngine = new RuleEngine({ firewall: egressRules, onError });
|
|
3164
|
+
};
|
|
3165
|
+
applyBundle(bundle);
|
|
1579
3166
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
1580
3167
|
const decide = (phase, result, block, allow2) => {
|
|
1581
3168
|
if (!result || !result.blocked) return allow2();
|
|
@@ -1609,20 +3196,26 @@ async function createProtection(options = {}) {
|
|
|
1609
3196
|
const headers = { ...meta.headers || {} };
|
|
1610
3197
|
for (const { rule, redactors } of redactions) {
|
|
1611
3198
|
const mask = maskFn(rule.category);
|
|
1612
|
-
|
|
3199
|
+
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
3200
|
+
const pathRedactors = redactors.filter((r) => r.jsonPath);
|
|
3201
|
+
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
3202
|
+
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
3203
|
+
if (!spanRedactors.length) continue;
|
|
3204
|
+
body = applyRedactors(body, spanRedactors, mask, transform);
|
|
3205
|
+
if (transform) continue;
|
|
1613
3206
|
for (const name of Object.keys(headers)) {
|
|
1614
3207
|
const value = headers[name];
|
|
1615
3208
|
if (typeof value === "string") {
|
|
1616
|
-
headers[name] = applyRedactors(value,
|
|
3209
|
+
headers[name] = applyRedactors(value, spanRedactors, mask);
|
|
1617
3210
|
} else if (Array.isArray(value)) {
|
|
1618
|
-
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
3211
|
+
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, spanRedactors, mask) : item);
|
|
1619
3212
|
}
|
|
1620
3213
|
}
|
|
1621
3214
|
}
|
|
1622
3215
|
return { verdict: "redact", body, headers };
|
|
1623
3216
|
};
|
|
1624
3217
|
const screenResp = async (response) => {
|
|
1625
|
-
const text = await readTextResponse(response);
|
|
3218
|
+
const text = await readTextResponse(response, screenCap);
|
|
1626
3219
|
if (text == null) return response;
|
|
1627
3220
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1628
3221
|
if (r.verdict === "block") return leakResponse();
|
|
@@ -1635,12 +3228,15 @@ async function createProtection(options = {}) {
|
|
|
1635
3228
|
const chunks = [];
|
|
1636
3229
|
let size = 0;
|
|
1637
3230
|
let overflow = false;
|
|
1638
|
-
const MAX =
|
|
3231
|
+
const MAX = screenCap;
|
|
1639
3232
|
const collect = (chunk, enc) => {
|
|
1640
3233
|
if (chunk == null) return;
|
|
1641
3234
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
1642
3235
|
size += buf.length;
|
|
1643
3236
|
if (size > MAX) {
|
|
3237
|
+
for (const c of chunks) origWrite(c);
|
|
3238
|
+
chunks.length = 0;
|
|
3239
|
+
origWrite(buf);
|
|
1644
3240
|
overflow = true;
|
|
1645
3241
|
return;
|
|
1646
3242
|
}
|
|
@@ -1667,6 +3263,7 @@ async function createProtection(options = {}) {
|
|
|
1667
3263
|
return origEnd(cb);
|
|
1668
3264
|
}
|
|
1669
3265
|
collect(chunk, enc);
|
|
3266
|
+
if (overflow) return origEnd(cb);
|
|
1670
3267
|
const text = Buffer.concat(chunks).toString("utf8");
|
|
1671
3268
|
let ct = res.getHeader ? res.getHeader("content-type") : void 0;
|
|
1672
3269
|
if (Array.isArray(ct)) ct = ct[0];
|
|
@@ -1698,6 +3295,7 @@ async function createProtection(options = {}) {
|
|
|
1698
3295
|
if (r.headers && res.setHeader) {
|
|
1699
3296
|
const current = res.getHeaders ? res.getHeaders() : {};
|
|
1700
3297
|
for (const [name, value] of Object.entries(r.headers)) {
|
|
3298
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
1701
3299
|
if (Array.isArray(value)) {
|
|
1702
3300
|
try {
|
|
1703
3301
|
res.setHeader(name, value);
|
|
@@ -1733,7 +3331,9 @@ async function createProtection(options = {}) {
|
|
|
1733
3331
|
};
|
|
1734
3332
|
const protection = {
|
|
1735
3333
|
mode,
|
|
1736
|
-
rules
|
|
3334
|
+
get rules() {
|
|
3335
|
+
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
3336
|
+
},
|
|
1737
3337
|
// Screen a fetch Response through the response-phase rules (redact/block). Used by
|
|
1738
3338
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
1739
3339
|
screenResponse: (response) => screenResp(response),
|
|
@@ -1844,27 +3444,106 @@ async function createProtection(options = {}) {
|
|
|
1844
3444
|
}
|
|
1845
3445
|
};
|
|
1846
3446
|
if (options.egress) {
|
|
1847
|
-
protection.uninstallEgress = await installEgressGuard({
|
|
3447
|
+
protection.uninstallEgress = await installEgressGuard({
|
|
3448
|
+
shouldBlock: egressShouldBlock,
|
|
3449
|
+
onBlock: options.onEgressBlock,
|
|
3450
|
+
dnsScreen: options.screenDns !== false,
|
|
3451
|
+
allowHosts: options.allowHosts
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
|
|
3455
|
+
const live = Boolean(options.siteUuid || options.token);
|
|
3456
|
+
const refreshSecret = options.refreshSecret ?? (typeof process !== "undefined" ? process.env.PATCHSTACK_REFRESH_SECRET : void 0);
|
|
3457
|
+
const refreshable = live && (options.refreshMs > 0 || Boolean(refreshSecret));
|
|
3458
|
+
let reporter = null;
|
|
3459
|
+
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
3460
|
+
try {
|
|
3461
|
+
({ reportManifest: reporter } = await Promise.resolve().then(() => (init_refresh_manifest(), refresh_manifest_exports)));
|
|
3462
|
+
} catch (err) {
|
|
3463
|
+
onError?.(err);
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
const runRefreshTick = async () => {
|
|
3467
|
+
if (reporter) {
|
|
3468
|
+
try {
|
|
3469
|
+
await reporter(cwd);
|
|
3470
|
+
} catch (err) {
|
|
3471
|
+
onError?.(err);
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
applyBundle(await resolveRules(options, store));
|
|
3475
|
+
};
|
|
3476
|
+
if (live) {
|
|
3477
|
+
protection.refresh = () => runRefreshTick();
|
|
3478
|
+
protection.refreshHandler = () => makeRefreshHandler(runRefreshTick, refreshSecret);
|
|
3479
|
+
}
|
|
3480
|
+
if (options.refreshMs > 0 && live) {
|
|
3481
|
+
const loop = startRefresh(runRefreshTick, { refreshMs: options.refreshMs, onError });
|
|
3482
|
+
protection.stopRefresh = loop.stop;
|
|
1848
3483
|
}
|
|
1849
3484
|
return protection;
|
|
1850
3485
|
}
|
|
1851
3486
|
function byPhase(rules, phase) {
|
|
1852
3487
|
return (rules ?? []).filter((r) => (r.phase ?? "request") === phase);
|
|
1853
3488
|
}
|
|
1854
|
-
async function readTextResponse(response) {
|
|
3489
|
+
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
1855
3490
|
if (!response || typeof response.clone !== "function") return null;
|
|
1856
3491
|
const ct = (response.headers?.get?.("content-type") || "").toLowerCase();
|
|
1857
3492
|
const isText = ct === "" || /(json|text|xml|html|javascript|csv|yaml|x-www-form-urlencoded)/.test(ct);
|
|
1858
3493
|
if (!isText) return null;
|
|
1859
3494
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
1860
|
-
if (len && len >
|
|
3495
|
+
if (len && len > cap) return null;
|
|
3496
|
+
let clone;
|
|
3497
|
+
try {
|
|
3498
|
+
clone = response.clone();
|
|
3499
|
+
} catch {
|
|
3500
|
+
return null;
|
|
3501
|
+
}
|
|
3502
|
+
const body = clone.body;
|
|
3503
|
+
if (body && typeof body.getReader === "function") {
|
|
3504
|
+
const reader = body.getReader();
|
|
3505
|
+
const chunks = [];
|
|
3506
|
+
let size = 0;
|
|
3507
|
+
let over = false;
|
|
3508
|
+
try {
|
|
3509
|
+
for (; ; ) {
|
|
3510
|
+
const { done, value } = await reader.read();
|
|
3511
|
+
if (done) break;
|
|
3512
|
+
if (!value) continue;
|
|
3513
|
+
size += value.byteLength;
|
|
3514
|
+
if (over) continue;
|
|
3515
|
+
if (size > cap) {
|
|
3516
|
+
over = true;
|
|
3517
|
+
continue;
|
|
3518
|
+
}
|
|
3519
|
+
chunks.push(value);
|
|
3520
|
+
}
|
|
3521
|
+
} catch {
|
|
3522
|
+
return null;
|
|
3523
|
+
}
|
|
3524
|
+
if (over) return null;
|
|
3525
|
+
try {
|
|
3526
|
+
return new TextDecoder().decode(concatBytes(chunks, size));
|
|
3527
|
+
} catch {
|
|
3528
|
+
return null;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
1861
3531
|
try {
|
|
1862
|
-
const text = await
|
|
1863
|
-
return text.length >
|
|
3532
|
+
const text = await clone.text();
|
|
3533
|
+
return text.length > cap ? null : text;
|
|
1864
3534
|
} catch {
|
|
1865
3535
|
return null;
|
|
1866
3536
|
}
|
|
1867
3537
|
}
|
|
3538
|
+
function concatBytes(chunks, total) {
|
|
3539
|
+
const out = new Uint8Array(total);
|
|
3540
|
+
let offset = 0;
|
|
3541
|
+
for (const c of chunks) {
|
|
3542
|
+
out.set(c, offset);
|
|
3543
|
+
offset += c.byteLength;
|
|
3544
|
+
}
|
|
3545
|
+
return out;
|
|
3546
|
+
}
|
|
1868
3547
|
function headerObject(headers) {
|
|
1869
3548
|
const out = {};
|
|
1870
3549
|
headers?.forEach?.((v, k) => {
|
|
@@ -1876,36 +3555,143 @@ function headerObject(headers) {
|
|
|
1876
3555
|
}
|
|
1877
3556
|
function extractRedactors(rule) {
|
|
1878
3557
|
const out = [];
|
|
1879
|
-
const
|
|
3558
|
+
const walk2 = (conds) => {
|
|
1880
3559
|
for (const c of conds ?? []) {
|
|
1881
|
-
if (Array.isArray(c.rules))
|
|
3560
|
+
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
1882
3561
|
const m = c.match;
|
|
1883
3562
|
if (!m) continue;
|
|
1884
3563
|
if (m.type === "regex" && typeof m.value === "string") {
|
|
1885
|
-
const
|
|
1886
|
-
if (
|
|
1887
|
-
const flags =
|
|
3564
|
+
const safe = safeRegExp(m.value);
|
|
3565
|
+
if (safe) {
|
|
3566
|
+
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
1888
3567
|
try {
|
|
1889
|
-
out.push({ re: new RegExp(
|
|
3568
|
+
out.push({ re: new RegExp(safe.source, flags) });
|
|
1890
3569
|
} catch {
|
|
1891
3570
|
}
|
|
1892
3571
|
}
|
|
1893
3572
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
1894
3573
|
out.push({ literal: String(m.value) });
|
|
3574
|
+
} else if (m.type === "array_key_value" && m.match && isBodyParam(c.parameter)) {
|
|
3575
|
+
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
3576
|
+
for (const key of keys) {
|
|
3577
|
+
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
3578
|
+
}
|
|
1895
3579
|
}
|
|
1896
3580
|
}
|
|
1897
3581
|
};
|
|
1898
|
-
|
|
3582
|
+
walk2(rule.rule_v2);
|
|
1899
3583
|
return out;
|
|
1900
3584
|
}
|
|
1901
|
-
function
|
|
3585
|
+
function htmlEscape(str) {
|
|
3586
|
+
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3587
|
+
}
|
|
3588
|
+
function applyRedactors(body, redactors, mask, transform) {
|
|
1902
3589
|
let out = body;
|
|
1903
3590
|
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);
|
|
3591
|
+
if (r.re) out = out.replace(r.re, transform ? (m) => transform(m) : mask);
|
|
3592
|
+
else if (r.literal) out = out.split(r.literal).join(transform ? transform(r.literal) : mask);
|
|
3593
|
+
}
|
|
3594
|
+
return out;
|
|
3595
|
+
}
|
|
3596
|
+
function isBodyParam(parameter) {
|
|
3597
|
+
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
3598
|
+
}
|
|
3599
|
+
function conditionPredicate(condition) {
|
|
3600
|
+
if (!condition || !condition.type) return () => true;
|
|
3601
|
+
return (value) => {
|
|
3602
|
+
try {
|
|
3603
|
+
return matchValue(condition.type, value, condition.value, condition);
|
|
3604
|
+
} catch {
|
|
3605
|
+
return false;
|
|
3606
|
+
}
|
|
3607
|
+
};
|
|
3608
|
+
}
|
|
3609
|
+
var DEFAULT_SCREEN_CAP = 512 * 1024;
|
|
3610
|
+
function responseScreenCap(rules) {
|
|
3611
|
+
let cap = DEFAULT_SCREEN_CAP;
|
|
3612
|
+
for (const r of rules ?? []) {
|
|
3613
|
+
if (r && r.bypass_limit === true) return Infinity;
|
|
3614
|
+
const n = Number(r && r.max_bytes);
|
|
3615
|
+
if (Number.isFinite(n) && n > cap) cap = n;
|
|
3616
|
+
}
|
|
3617
|
+
return cap;
|
|
3618
|
+
}
|
|
3619
|
+
function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
3620
|
+
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
3621
|
+
const head = text.trimStart()[0];
|
|
3622
|
+
if (head !== "{" && head !== "[") return text;
|
|
3623
|
+
const preserved = preserveBigInts(text);
|
|
3624
|
+
let obj;
|
|
3625
|
+
try {
|
|
3626
|
+
obj = JSON.parse(preserved);
|
|
3627
|
+
} catch {
|
|
3628
|
+
return text;
|
|
3629
|
+
}
|
|
3630
|
+
let changed = false;
|
|
3631
|
+
for (const r of pathRedactors) {
|
|
3632
|
+
const pred = conditionPredicate(r.condition);
|
|
3633
|
+
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
3634
|
+
try {
|
|
3635
|
+
if (pred(loc.value)) {
|
|
3636
|
+
loc.parent[loc.key] = transform ? transform(String(loc.value)) : mask;
|
|
3637
|
+
changed = true;
|
|
3638
|
+
}
|
|
3639
|
+
} catch {
|
|
3640
|
+
}
|
|
3641
|
+
});
|
|
3642
|
+
}
|
|
3643
|
+
return changed ? restoreBigInts(JSON.stringify(obj)) : text;
|
|
3644
|
+
}
|
|
3645
|
+
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
3646
|
+
var BIGINT_CLOSE = "__DNEGIB__";
|
|
3647
|
+
function preserveBigInts(text) {
|
|
3648
|
+
let out = "";
|
|
3649
|
+
let inStr = false;
|
|
3650
|
+
for (let i = 0; i < text.length; ) {
|
|
3651
|
+
const ch = text[i];
|
|
3652
|
+
if (inStr) {
|
|
3653
|
+
out += ch;
|
|
3654
|
+
if (ch === "\\") {
|
|
3655
|
+
out += text[i + 1] ?? "";
|
|
3656
|
+
i += 2;
|
|
3657
|
+
continue;
|
|
3658
|
+
}
|
|
3659
|
+
if (ch === '"') inStr = false;
|
|
3660
|
+
i++;
|
|
3661
|
+
continue;
|
|
3662
|
+
}
|
|
3663
|
+
if (ch === '"') {
|
|
3664
|
+
inStr = true;
|
|
3665
|
+
out += ch;
|
|
3666
|
+
i++;
|
|
3667
|
+
continue;
|
|
3668
|
+
}
|
|
3669
|
+
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
3670
|
+
let j = ch === "-" ? i + 1 : i;
|
|
3671
|
+
let digits = 0;
|
|
3672
|
+
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
3673
|
+
digits++;
|
|
3674
|
+
j++;
|
|
3675
|
+
}
|
|
3676
|
+
const next = text[j];
|
|
3677
|
+
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
3678
|
+
if (isIntToken && digits >= 16) {
|
|
3679
|
+
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
3680
|
+
} else {
|
|
3681
|
+
out += text.slice(i, j || i + 1);
|
|
3682
|
+
}
|
|
3683
|
+
i = j > i ? j : i + 1;
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
out += ch;
|
|
3687
|
+
i++;
|
|
1906
3688
|
}
|
|
1907
3689
|
return out;
|
|
1908
3690
|
}
|
|
3691
|
+
function restoreBigInts(text) {
|
|
3692
|
+
if (!text.includes(BIGINT_OPEN)) return text;
|
|
3693
|
+
return text.replace(new RegExp(`"${BIGINT_OPEN}(-?\\d+)${BIGINT_CLOSE}"`, "g"), "$1");
|
|
3694
|
+
}
|
|
1909
3695
|
function rebuildResponse(response, body, redactedHeaders) {
|
|
1910
3696
|
const headers = new Headers(response.headers);
|
|
1911
3697
|
headers.delete("content-length");
|
|
@@ -1935,115 +3721,6 @@ function leakResponse() {
|
|
|
1935
3721
|
headers: { "content-type": "application/json" }
|
|
1936
3722
|
});
|
|
1937
3723
|
}
|
|
1938
|
-
async function resolveRules(options) {
|
|
1939
|
-
const cache = makeCache(options);
|
|
1940
|
-
if (options.siteUuid) {
|
|
1941
|
-
const prior = await cache.read();
|
|
1942
|
-
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
1943
|
-
const res = await client.getRules();
|
|
1944
|
-
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1945
|
-
if (res.success && !res.notModified) {
|
|
1946
|
-
const bundle = normalizeBundle(res);
|
|
1947
|
-
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1948
|
-
return bundle;
|
|
1949
|
-
}
|
|
1950
|
-
if (prior?.bundle) {
|
|
1951
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1952
|
-
return normalizeBundle(prior.bundle);
|
|
1953
|
-
}
|
|
1954
|
-
if (options.rules) {
|
|
1955
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
1956
|
-
return normalizeBundle(options.rules);
|
|
1957
|
-
}
|
|
1958
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1959
|
-
return emptyBundle();
|
|
1960
|
-
}
|
|
1961
|
-
if (options.token) {
|
|
1962
|
-
const prior = await cache.read();
|
|
1963
|
-
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
1964
|
-
const res = await client.getRules();
|
|
1965
|
-
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1966
|
-
if (res.success && !res.notModified) {
|
|
1967
|
-
const bundle = normalizeBundle(res);
|
|
1968
|
-
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1969
|
-
return bundle;
|
|
1970
|
-
}
|
|
1971
|
-
if (prior?.bundle) {
|
|
1972
|
-
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1973
|
-
return normalizeBundle(prior.bundle);
|
|
1974
|
-
}
|
|
1975
|
-
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1976
|
-
return emptyBundle();
|
|
1977
|
-
}
|
|
1978
|
-
if (options.rules) {
|
|
1979
|
-
return normalizeBundle(options.rules);
|
|
1980
|
-
}
|
|
1981
|
-
return emptyBundle();
|
|
1982
|
-
}
|
|
1983
|
-
function normalizeBundle(b) {
|
|
1984
|
-
return {
|
|
1985
|
-
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
1986
|
-
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
1987
|
-
whitelist_keys: b.whitelist_keys ?? {}
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
function emptyBundle() {
|
|
1991
|
-
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1992
|
-
}
|
|
1993
|
-
function makeCache(options) {
|
|
1994
|
-
const adapter = options.ruleCache;
|
|
1995
|
-
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
1996
|
-
return {
|
|
1997
|
-
read: async () => {
|
|
1998
|
-
try {
|
|
1999
|
-
return toEnvelope(await adapter.read());
|
|
2000
|
-
} catch {
|
|
2001
|
-
return null;
|
|
2002
|
-
}
|
|
2003
|
-
},
|
|
2004
|
-
write: async (env) => {
|
|
2005
|
-
try {
|
|
2006
|
-
await adapter.write(env);
|
|
2007
|
-
} catch {
|
|
2008
|
-
}
|
|
2009
|
-
}
|
|
2010
|
-
};
|
|
2011
|
-
}
|
|
2012
|
-
const dir = options.cacheDir;
|
|
2013
|
-
return {
|
|
2014
|
-
read: async () => cacheRead(dir),
|
|
2015
|
-
write: async (env) => cacheWrite(dir, env)
|
|
2016
|
-
};
|
|
2017
|
-
}
|
|
2018
|
-
function cachePath(dir) {
|
|
2019
|
-
return (0, import_node_path.join)(dir, "patchstack-rules.json");
|
|
2020
|
-
}
|
|
2021
|
-
function cacheWrite(dir, env) {
|
|
2022
|
-
if (!dir) return;
|
|
2023
|
-
try {
|
|
2024
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
2025
|
-
(0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
|
|
2026
|
-
} catch {
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
function cacheRead(dir) {
|
|
2030
|
-
if (!dir) return null;
|
|
2031
|
-
try {
|
|
2032
|
-
return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
|
|
2033
|
-
} catch {
|
|
2034
|
-
return null;
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
function toEnvelope(value) {
|
|
2038
|
-
if (!value || typeof value !== "object") return null;
|
|
2039
|
-
if (value.bundle && typeof value.bundle === "object") {
|
|
2040
|
-
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
2041
|
-
}
|
|
2042
|
-
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
2043
|
-
return { bundle: value, etag: null };
|
|
2044
|
-
}
|
|
2045
|
-
return null;
|
|
2046
|
-
}
|
|
2047
3724
|
var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
|
|
2048
3725
|
function blockBody(result) {
|
|
2049
3726
|
return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
|