@patchstack/connect 0.4.2 → 0.5.1
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 +24 -4
- package/README.md +27 -3
- package/dist/cli.js +319 -61
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +230 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +227 -17
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +22 -1
- package/dist/protect/templates/express-guard.cjs +54 -4
- package/dist/protect/templates/express-guard.js +54 -4
- package/dist/protect/templates/express-guard.ts +43 -5
- package/dist/protect/templates/fastify-plugin.cjs +41 -3
- package/dist/protect/templates/fastify-plugin.js +41 -3
- package/dist/protect/templates/fastify-plugin.ts +30 -2
- package/dist/protect/templates/generic-guard.cjs +58 -5
- package/dist/protect/templates/generic-guard.js +58 -5
- package/dist/protect/templates/generic-guard.ts +47 -4
- package/dist/protect/templates/guard.ts +42 -8
- package/dist/protect/templates/next-middleware.ts +22 -1
- package/dist/protect/templates/nuxt-middleware.ts +22 -1
- package/dist/protect/templates/sveltekit-hooks.ts +22 -1
- package/dist/protect.cjs +351 -97
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.edge.js +56 -24
- package/dist/protect.edge.js.map +1 -1
- package/dist/protect.js +57 -25
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-JNCTIAM5.js → refresh-manifest-2MMPQN2A.js} +243 -35
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +3 -3
- package/dist/refresh-manifest-JNCTIAM5.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -38,8 +38,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
38
38
|
// src/pulse-token.ts
|
|
39
39
|
function buildTokenUrl(manifestEndpoint) {
|
|
40
40
|
const url = new URL(manifestEndpoint);
|
|
41
|
-
const
|
|
42
|
-
url.pathname =
|
|
41
|
+
const path8 = url.pathname.replace(/\/$/, "");
|
|
42
|
+
url.pathname = path8.endsWith("/manifest") ? `${path8.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
|
|
43
43
|
url.search = "";
|
|
44
44
|
url.hash = "";
|
|
45
45
|
return url.toString();
|
|
@@ -142,6 +142,9 @@ var init_types = __esm({
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
// src/client.ts
|
|
145
|
+
function claimTokenHeader(config) {
|
|
146
|
+
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
147
|
+
}
|
|
145
148
|
function buildEndpointUrl(base, siteUuid) {
|
|
146
149
|
const trimmed = base.replace(/\/$/, "");
|
|
147
150
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
@@ -159,6 +162,14 @@ function authFailureMessage(status, config) {
|
|
|
159
162
|
}
|
|
160
163
|
return null;
|
|
161
164
|
}
|
|
165
|
+
function buildManifestBody(config, payload) {
|
|
166
|
+
return {
|
|
167
|
+
...payload,
|
|
168
|
+
environment: config.environment,
|
|
169
|
+
...typeof config.siteUrl === "string" && config.siteUrl !== "" ? { url: config.siteUrl } : {},
|
|
170
|
+
...typeof config.siteName === "string" && config.siteName !== "" ? { name: config.siteName } : {}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
162
173
|
async function postManifest(config, payload) {
|
|
163
174
|
const url = buildEndpointUrl(config.endpoint, config.siteUuid);
|
|
164
175
|
const timeoutMs = config.timeoutMs;
|
|
@@ -169,9 +180,10 @@ async function postManifest(config, payload) {
|
|
|
169
180
|
headers: {
|
|
170
181
|
"Content-Type": "application/json",
|
|
171
182
|
Accept: "application/json",
|
|
172
|
-
"User-Agent": "@patchstack/connect"
|
|
183
|
+
"User-Agent": "@patchstack/connect",
|
|
184
|
+
...claimTokenHeader(config)
|
|
173
185
|
},
|
|
174
|
-
body: JSON.stringify(
|
|
186
|
+
body: JSON.stringify(buildManifestBody(config, payload)),
|
|
175
187
|
signal: AbortSignal.timeout(timeoutMs)
|
|
176
188
|
});
|
|
177
189
|
} catch (cause) {
|
|
@@ -228,7 +240,7 @@ function isTimeoutError(cause) {
|
|
|
228
240
|
}
|
|
229
241
|
return false;
|
|
230
242
|
}
|
|
231
|
-
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS;
|
|
243
|
+
var DEFAULT_ENDPOINT, DEFAULT_TIMEOUT_MS, CLAIM_TOKEN_HEADER;
|
|
232
244
|
var init_client = __esm({
|
|
233
245
|
"src/client.ts"() {
|
|
234
246
|
"use strict";
|
|
@@ -236,10 +248,211 @@ var init_client = __esm({
|
|
|
236
248
|
init_pulse_token();
|
|
237
249
|
DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
238
250
|
DEFAULT_TIMEOUT_MS = 3e4;
|
|
251
|
+
CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// src/site-url.ts
|
|
256
|
+
function isUnpublishableHost(hostname) {
|
|
257
|
+
const host = hostname.toLowerCase().replace(/\.+$/, "");
|
|
258
|
+
if (host === "") return true;
|
|
259
|
+
if (host.startsWith("[")) return true;
|
|
260
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
261
|
+
if (!host.includes(".")) return true;
|
|
262
|
+
return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
263
|
+
}
|
|
264
|
+
function normaliseSiteUrl(value) {
|
|
265
|
+
const trimmed = (value ?? "").trim();
|
|
266
|
+
if (trimmed === "") return null;
|
|
267
|
+
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
268
|
+
let parsed;
|
|
269
|
+
try {
|
|
270
|
+
parsed = new URL(withScheme);
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
275
|
+
if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
|
|
276
|
+
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
277
|
+
return origin.length <= 191 ? origin : null;
|
|
278
|
+
}
|
|
279
|
+
function detectSiteUrl(env = process.env) {
|
|
280
|
+
for (const source of PRODUCTION_URL_SOURCES) {
|
|
281
|
+
const url = normaliseSiteUrl(source.read(env));
|
|
282
|
+
if (url !== null) {
|
|
283
|
+
return { url, platform: source.platform };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
var RESERVED_SUFFIXES, PRODUCTION_URL_SOURCES;
|
|
289
|
+
var init_site_url = __esm({
|
|
290
|
+
"src/site-url.ts"() {
|
|
291
|
+
"use strict";
|
|
292
|
+
RESERVED_SUFFIXES = [
|
|
293
|
+
".local",
|
|
294
|
+
".localhost",
|
|
295
|
+
".localdomain",
|
|
296
|
+
".internal",
|
|
297
|
+
".intranet",
|
|
298
|
+
".invalid",
|
|
299
|
+
".test",
|
|
300
|
+
".example",
|
|
301
|
+
".home.arpa",
|
|
302
|
+
".in-addr.arpa",
|
|
303
|
+
".ip6.arpa"
|
|
304
|
+
];
|
|
305
|
+
PRODUCTION_URL_SOURCES = [
|
|
306
|
+
{
|
|
307
|
+
// VERCEL_URL is per-deployment and changes every build, so it is deliberately not read here.
|
|
308
|
+
platform: "vercel",
|
|
309
|
+
read: (env) => env.VERCEL_ENV === "production" ? env.VERCEL_PROJECT_PRODUCTION_URL : void 0
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
// `URL` is generic enough to belong to anything, so it counts only alongside Netlify's own markers.
|
|
313
|
+
platform: "netlify",
|
|
314
|
+
read: (env) => env.NETLIFY === "true" && env.CONTEXT === "production" ? env.URL : void 0
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
platform: "render",
|
|
318
|
+
read: (env) => env.RENDER === "true" && env.IS_PULL_REQUEST !== "true" ? env.RENDER_EXTERNAL_URL : void 0
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
platform: "railway",
|
|
322
|
+
read: (env) => env.RAILWAY_ENVIRONMENT_NAME === "production" ? env.RAILWAY_PUBLIC_DOMAIN : void 0
|
|
323
|
+
}
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// src/site-name.ts
|
|
329
|
+
function normaliseSiteName(value) {
|
|
330
|
+
const collapsed = (value ?? "").replace(/\s+/g, " ").trim();
|
|
331
|
+
if (collapsed === "") return null;
|
|
332
|
+
return collapsed.slice(0, NAME_MAX_LENGTH);
|
|
333
|
+
}
|
|
334
|
+
function decodeEntities(text) {
|
|
335
|
+
return text.replace(/&([a-z]+);/gi, (whole, name) => NAMED_ENTITIES2[name.toLowerCase()] ?? whole);
|
|
336
|
+
}
|
|
337
|
+
function nameFromHtmlTitle(html) {
|
|
338
|
+
const match = /<title\b[^>]*>([\s\S]*?)<\/title>/i.exec(html);
|
|
339
|
+
if (match === null) return null;
|
|
340
|
+
const name = normaliseSiteName(decodeEntities(match[1] ?? ""));
|
|
341
|
+
if (name === null || TEMPLATE_TITLES.has(name.toLowerCase())) return null;
|
|
342
|
+
return name;
|
|
343
|
+
}
|
|
344
|
+
function nameFromPackageName(raw) {
|
|
345
|
+
if (typeof raw !== "string") return null;
|
|
346
|
+
const unscoped = raw.trim().replace(/^@[^/]+\//, "");
|
|
347
|
+
if (unscoped === "" || TEMPLATE_PACKAGE_NAMES.has(unscoped.toLowerCase())) return null;
|
|
348
|
+
const words = unscoped.split(/[-_.\s]+/).filter((word) => word !== "").map((word) => word === word.toUpperCase() ? word : word.charAt(0).toUpperCase() + word.slice(1));
|
|
349
|
+
return normaliseSiteName(words.join(" "));
|
|
350
|
+
}
|
|
351
|
+
async function readIfPresent(file) {
|
|
352
|
+
try {
|
|
353
|
+
return await (0, import_promises.readFile)(file, "utf8");
|
|
354
|
+
} catch {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function detectSiteName(cwd) {
|
|
359
|
+
for (const shell of ["index.html", import_node_path.default.join("public", "index.html")]) {
|
|
360
|
+
const html = await readIfPresent(import_node_path.default.join(cwd, shell));
|
|
361
|
+
if (html === null) continue;
|
|
362
|
+
const name = nameFromHtmlTitle(html);
|
|
363
|
+
if (name !== null) return { name, source: "index.html" };
|
|
364
|
+
}
|
|
365
|
+
const manifest = await readIfPresent(import_node_path.default.join(cwd, "package.json"));
|
|
366
|
+
if (manifest !== null) {
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(manifest);
|
|
369
|
+
const name = nameFromPackageName(
|
|
370
|
+
typeof parsed === "object" && parsed !== null ? parsed.name : void 0
|
|
371
|
+
);
|
|
372
|
+
if (name !== null) return { name, source: "package.json" };
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
var import_promises, import_node_path, NAME_MAX_LENGTH, TEMPLATE_PACKAGE_NAMES, TEMPLATE_TITLES, NAMED_ENTITIES2;
|
|
379
|
+
var init_site_name = __esm({
|
|
380
|
+
"src/site-name.ts"() {
|
|
381
|
+
"use strict";
|
|
382
|
+
import_promises = require("fs/promises");
|
|
383
|
+
import_node_path = __toESM(require("path"), 1);
|
|
384
|
+
NAME_MAX_LENGTH = 191;
|
|
385
|
+
TEMPLATE_PACKAGE_NAMES = /* @__PURE__ */ new Set([
|
|
386
|
+
// Lovable
|
|
387
|
+
"vite_react_shadcn_ts",
|
|
388
|
+
// Bolt
|
|
389
|
+
"vite-react-typescript-starter",
|
|
390
|
+
// Scaffolders and the names people accept from them
|
|
391
|
+
"vite-project",
|
|
392
|
+
"my-app",
|
|
393
|
+
"my-project",
|
|
394
|
+
"my-v0-project",
|
|
395
|
+
"app",
|
|
396
|
+
"frontend",
|
|
397
|
+
"web",
|
|
398
|
+
"client",
|
|
399
|
+
"project",
|
|
400
|
+
"template",
|
|
401
|
+
"example",
|
|
402
|
+
"react-app",
|
|
403
|
+
"next-app",
|
|
404
|
+
"nextjs",
|
|
405
|
+
"starter",
|
|
406
|
+
"test"
|
|
407
|
+
]);
|
|
408
|
+
TEMPLATE_TITLES = /* @__PURE__ */ new Set([
|
|
409
|
+
"vite + react",
|
|
410
|
+
"vite + react + ts",
|
|
411
|
+
"vite + vue",
|
|
412
|
+
"vite + vue + ts",
|
|
413
|
+
"vite app",
|
|
414
|
+
"react app",
|
|
415
|
+
"create next app",
|
|
416
|
+
"document",
|
|
417
|
+
"untitled",
|
|
418
|
+
"index",
|
|
419
|
+
"home",
|
|
420
|
+
"app"
|
|
421
|
+
]);
|
|
422
|
+
NAMED_ENTITIES2 = {
|
|
423
|
+
amp: "&",
|
|
424
|
+
lt: "<",
|
|
425
|
+
gt: ">",
|
|
426
|
+
quot: '"',
|
|
427
|
+
apos: "'",
|
|
428
|
+
nbsp: " "
|
|
429
|
+
};
|
|
239
430
|
}
|
|
240
431
|
});
|
|
241
432
|
|
|
242
433
|
// src/config.ts
|
|
434
|
+
async function resolveSiteIdentity(cwd, fromEnv, fromFile) {
|
|
435
|
+
const configuredUrl = stated(fromEnv.url) ?? stated(fromFile.url);
|
|
436
|
+
const configuredName = stated(fromEnv.name) ?? stated(fromFile.name);
|
|
437
|
+
let url;
|
|
438
|
+
if (configuredUrl === null) {
|
|
439
|
+
url = detectSiteUrl(process.env)?.url ?? null;
|
|
440
|
+
} else {
|
|
441
|
+
url = normaliseSiteUrl(configuredUrl);
|
|
442
|
+
if (url === null) {
|
|
443
|
+
throw new PatchstackError(
|
|
444
|
+
`Site URL "${configuredUrl}" is not an address the public can reach this site at. Set "url" in .patchstackrc.json (or PATCHSTACK_SITE_URL) to the http(s) address visitors use \u2014 a hostname, not an IP address \u2014 or remove it to let the build environment answer.`,
|
|
445
|
+
"CONFIG_INVALID"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
const name = configuredName === null ? (await detectSiteName(cwd))?.name ?? null : normaliseSiteName(configuredName);
|
|
450
|
+
return { url, name };
|
|
451
|
+
}
|
|
452
|
+
function stated(value) {
|
|
453
|
+
const trimmed = (value ?? "").trim();
|
|
454
|
+
return trimmed === "" ? null : trimmed;
|
|
455
|
+
}
|
|
243
456
|
async function resolveConfig(options) {
|
|
244
457
|
const fromFile = await readConfigFile(options.cwd);
|
|
245
458
|
const fromSecretFile = await readSecretFile(options.cwd);
|
|
@@ -269,14 +482,19 @@ async function resolveConfig(options) {
|
|
|
269
482
|
}
|
|
270
483
|
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
271
484
|
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
485
|
+
const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
|
|
486
|
+
const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
|
|
272
487
|
return {
|
|
273
488
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
274
489
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
275
490
|
pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
|
|
491
|
+
siteUrl: identity.url,
|
|
492
|
+
siteName: identity.name,
|
|
276
493
|
endpoint,
|
|
277
494
|
timeoutMs,
|
|
278
495
|
environment,
|
|
279
|
-
widget: fromFile.widget !== false
|
|
496
|
+
widget: fromFile.widget !== false,
|
|
497
|
+
claimToken
|
|
280
498
|
};
|
|
281
499
|
}
|
|
282
500
|
async function readSecretFile(cwd) {
|
|
@@ -287,10 +505,10 @@ async function readConfigFile(cwd) {
|
|
|
287
505
|
return readJsonFile(cwd, CONFIG_FILENAME);
|
|
288
506
|
}
|
|
289
507
|
async function readJsonFile(cwd, filename) {
|
|
290
|
-
const target =
|
|
508
|
+
const target = import_node_path2.default.join(cwd, filename);
|
|
291
509
|
let raw;
|
|
292
510
|
try {
|
|
293
|
-
raw = await (0,
|
|
511
|
+
raw = await (0, import_promises2.readFile)(target, "utf8");
|
|
294
512
|
} catch (err) {
|
|
295
513
|
if (err.code === "ENOENT") {
|
|
296
514
|
return {};
|
|
@@ -327,6 +545,8 @@ function readEnv() {
|
|
|
327
545
|
const environmentRaw = process.env.PATCHSTACK_ENVIRONMENT;
|
|
328
546
|
return {
|
|
329
547
|
siteUuid: process.env.PATCHSTACK_SITE_UUID ?? void 0,
|
|
548
|
+
url: process.env.PATCHSTACK_SITE_URL ?? void 0,
|
|
549
|
+
name: process.env.PATCHSTACK_SITE_NAME ?? void 0,
|
|
330
550
|
apiKey: process.env.PATCHSTACK_API_KEY ?? void 0,
|
|
331
551
|
pulseAuth: process.env.PATCHSTACK_PULSE_AUTH ?? void 0,
|
|
332
552
|
endpoint: process.env.PATCHSTACK_ENDPOINT ?? void 0,
|
|
@@ -340,14 +560,16 @@ function isUuid(value) {
|
|
|
340
560
|
function isEnvironment(value) {
|
|
341
561
|
return value === "production" || value === "sandbox";
|
|
342
562
|
}
|
|
343
|
-
var
|
|
563
|
+
var import_promises2, import_node_path2, CONFIG_FILENAME, SECRET_FILENAME, DEFAULT_ENVIRONMENT;
|
|
344
564
|
var init_config = __esm({
|
|
345
565
|
"src/config.ts"() {
|
|
346
566
|
"use strict";
|
|
347
|
-
|
|
348
|
-
|
|
567
|
+
import_promises2 = require("fs/promises");
|
|
568
|
+
import_node_path2 = __toESM(require("path"), 1);
|
|
349
569
|
init_types();
|
|
350
570
|
init_client();
|
|
571
|
+
init_site_url();
|
|
572
|
+
init_site_name();
|
|
351
573
|
CONFIG_FILENAME = ".patchstackrc.json";
|
|
352
574
|
SECRET_FILENAME = ".patchstackrc.local.json";
|
|
353
575
|
DEFAULT_ENVIRONMENT = "production";
|
|
@@ -358,7 +580,7 @@ var init_config = __esm({
|
|
|
358
580
|
async function readDeclaredDependencyNames(cwd) {
|
|
359
581
|
let raw;
|
|
360
582
|
try {
|
|
361
|
-
raw = await (0,
|
|
583
|
+
raw = await (0, import_promises3.readFile)(import_node_path3.default.join(cwd, "package.json"), "utf8");
|
|
362
584
|
} catch {
|
|
363
585
|
return [];
|
|
364
586
|
}
|
|
@@ -407,12 +629,12 @@ function disagreements(packages, other) {
|
|
|
407
629
|
}
|
|
408
630
|
return conflicts;
|
|
409
631
|
}
|
|
410
|
-
var
|
|
632
|
+
var import_promises3, import_node_path3;
|
|
411
633
|
var init_consistency = __esm({
|
|
412
634
|
"src/parsers/consistency.ts"() {
|
|
413
635
|
"use strict";
|
|
414
|
-
|
|
415
|
-
|
|
636
|
+
import_promises3 = require("fs/promises");
|
|
637
|
+
import_node_path3 = __toESM(require("path"), 1);
|
|
416
638
|
}
|
|
417
639
|
});
|
|
418
640
|
|
|
@@ -443,7 +665,7 @@ var init_report = __esm({
|
|
|
443
665
|
async function parseBunLockfile(lockfilePath, report) {
|
|
444
666
|
let raw;
|
|
445
667
|
try {
|
|
446
|
-
raw = await (0,
|
|
668
|
+
raw = await (0, import_promises4.readFile)(lockfilePath, "utf8");
|
|
447
669
|
} catch (cause) {
|
|
448
670
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
449
671
|
}
|
|
@@ -557,11 +779,11 @@ function stripTrailingCommas(source) {
|
|
|
557
779
|
}
|
|
558
780
|
return out.join("");
|
|
559
781
|
}
|
|
560
|
-
var
|
|
782
|
+
var import_promises4, NON_REGISTRY_VERSION;
|
|
561
783
|
var init_bun = __esm({
|
|
562
784
|
"src/parsers/bun.ts"() {
|
|
563
785
|
"use strict";
|
|
564
|
-
|
|
786
|
+
import_promises4 = require("fs/promises");
|
|
565
787
|
init_types();
|
|
566
788
|
init_report();
|
|
567
789
|
NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
|
|
@@ -572,7 +794,7 @@ var init_bun = __esm({
|
|
|
572
794
|
async function parseNpmLockfile(lockfilePath) {
|
|
573
795
|
let raw;
|
|
574
796
|
try {
|
|
575
|
-
raw = await (0,
|
|
797
|
+
raw = await (0, import_promises5.readFile)(lockfilePath, "utf8");
|
|
576
798
|
} catch (cause) {
|
|
577
799
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
578
800
|
}
|
|
@@ -630,7 +852,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
|
|
|
630
852
|
return acc;
|
|
631
853
|
}
|
|
632
854
|
function extractNameFromPath(pkgPath) {
|
|
633
|
-
const segments = pkgPath.split("node_modules" +
|
|
855
|
+
const segments = pkgPath.split("node_modules" + import_node_path4.default.sep === pkgPath ? import_node_path4.default.sep : "/");
|
|
634
856
|
const parts = pkgPath.split("/");
|
|
635
857
|
const nmIndex = parts.lastIndexOf("node_modules");
|
|
636
858
|
if (nmIndex === -1 || nmIndex >= parts.length - 1) {
|
|
@@ -651,21 +873,21 @@ function isDirectV2(pkgPath) {
|
|
|
651
873
|
const nmCount = parts.filter((p) => p === "node_modules").length;
|
|
652
874
|
return nmCount === 1;
|
|
653
875
|
}
|
|
654
|
-
var
|
|
876
|
+
var import_promises5, import_node_path4;
|
|
655
877
|
var init_npm = __esm({
|
|
656
878
|
"src/parsers/npm.ts"() {
|
|
657
879
|
"use strict";
|
|
658
|
-
|
|
659
|
-
|
|
880
|
+
import_promises5 = require("fs/promises");
|
|
881
|
+
import_node_path4 = __toESM(require("path"), 1);
|
|
660
882
|
init_types();
|
|
661
883
|
}
|
|
662
884
|
});
|
|
663
885
|
|
|
664
886
|
// src/parsers/node_modules.ts
|
|
665
887
|
async function walkNodeModules(cwd) {
|
|
666
|
-
const root =
|
|
888
|
+
const root = import_node_path5.default.join(cwd, "node_modules");
|
|
667
889
|
try {
|
|
668
|
-
const info = await (0,
|
|
890
|
+
const info = await (0, import_promises6.stat)(root);
|
|
669
891
|
if (!info.isDirectory()) {
|
|
670
892
|
throw new PatchstackError(
|
|
671
893
|
`${root} exists but is not a directory.`,
|
|
@@ -684,7 +906,7 @@ async function walkNodeModules(cwd) {
|
|
|
684
906
|
}
|
|
685
907
|
const seen = /* @__PURE__ */ new Map();
|
|
686
908
|
try {
|
|
687
|
-
seen.set(await (0,
|
|
909
|
+
seen.set(await (0, import_promises6.realpath)(cwd), -1);
|
|
688
910
|
} catch {
|
|
689
911
|
}
|
|
690
912
|
return dedupeByShallowest(await walk(root, 0, seen, cwd));
|
|
@@ -700,26 +922,26 @@ function dedupeByShallowest(found) {
|
|
|
700
922
|
async function walk(dir, depth, seen, root) {
|
|
701
923
|
let names;
|
|
702
924
|
try {
|
|
703
|
-
names = await (0,
|
|
925
|
+
names = await (0, import_promises6.readdir)(dir);
|
|
704
926
|
} catch {
|
|
705
927
|
return [];
|
|
706
928
|
}
|
|
707
929
|
const perEntry = await mapLimit(names, async (name) => {
|
|
708
930
|
if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
|
|
709
|
-
const fullPath =
|
|
931
|
+
const fullPath = import_node_path5.default.join(dir, name);
|
|
710
932
|
const resolved = await claim(fullPath, seen, depth);
|
|
711
933
|
if (resolved === null) return [];
|
|
712
934
|
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
|
|
713
935
|
if (name.startsWith("@")) {
|
|
714
936
|
let subNames;
|
|
715
937
|
try {
|
|
716
|
-
subNames = await (0,
|
|
938
|
+
subNames = await (0, import_promises6.readdir)(fullPath);
|
|
717
939
|
} catch {
|
|
718
940
|
return [];
|
|
719
941
|
}
|
|
720
942
|
const scoped = await mapLimit(subNames, async (sub) => {
|
|
721
943
|
if (sub.startsWith(".")) return [];
|
|
722
|
-
const scopedDir =
|
|
944
|
+
const scopedDir = import_node_path5.default.join(fullPath, sub);
|
|
723
945
|
const scopedReal = await claim(scopedDir, seen, depth);
|
|
724
946
|
if (scopedReal === null) return [];
|
|
725
947
|
return packageAndNested(scopedDir, scopedReal, depth, seen, root);
|
|
@@ -746,13 +968,13 @@ async function mapLimit(items, fn) {
|
|
|
746
968
|
await Promise.all(workers);
|
|
747
969
|
return results;
|
|
748
970
|
}
|
|
749
|
-
function installLocation(root, pkgDir, impl =
|
|
971
|
+
function installLocation(root, pkgDir, impl = import_node_path5.default) {
|
|
750
972
|
return impl.relative(root, pkgDir).split(impl.sep).join("/");
|
|
751
973
|
}
|
|
752
974
|
async function readPackage(pkgDir, depth, root) {
|
|
753
975
|
let raw;
|
|
754
976
|
try {
|
|
755
|
-
raw = await (0,
|
|
977
|
+
raw = await (0, import_promises6.readFile)(import_node_path5.default.join(pkgDir, "package.json"), "utf8");
|
|
756
978
|
} catch {
|
|
757
979
|
return null;
|
|
758
980
|
}
|
|
@@ -771,7 +993,7 @@ async function readPackage(pkgDir, depth, root) {
|
|
|
771
993
|
return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
|
|
772
994
|
}
|
|
773
995
|
async function walkNested(pkgDir, depth, seen, root) {
|
|
774
|
-
const nested =
|
|
996
|
+
const nested = import_node_path5.default.join(pkgDir, "node_modules");
|
|
775
997
|
if (await claim(nested, seen, depth + 1) === null) {
|
|
776
998
|
return [];
|
|
777
999
|
}
|
|
@@ -780,10 +1002,10 @@ async function walkNested(pkgDir, depth, seen, root) {
|
|
|
780
1002
|
async function claim(dir, seen, depth) {
|
|
781
1003
|
let resolved;
|
|
782
1004
|
try {
|
|
783
|
-
const info = await (0,
|
|
1005
|
+
const info = await (0, import_promises6.lstat)(dir);
|
|
784
1006
|
if (!info.isDirectory() && !info.isSymbolicLink()) return null;
|
|
785
|
-
resolved = await (0,
|
|
786
|
-
const target = await (0,
|
|
1007
|
+
resolved = await (0, import_promises6.realpath)(dir);
|
|
1008
|
+
const target = await (0, import_promises6.stat)(resolved);
|
|
787
1009
|
if (!target.isDirectory()) return null;
|
|
788
1010
|
} catch {
|
|
789
1011
|
return null;
|
|
@@ -793,12 +1015,12 @@ async function claim(dir, seen, depth) {
|
|
|
793
1015
|
seen.set(resolved, depth);
|
|
794
1016
|
return resolved;
|
|
795
1017
|
}
|
|
796
|
-
var
|
|
1018
|
+
var import_promises6, import_node_path5, PACKAGE_STORE_DIRS, CONCURRENCY;
|
|
797
1019
|
var init_node_modules = __esm({
|
|
798
1020
|
"src/parsers/node_modules.ts"() {
|
|
799
1021
|
"use strict";
|
|
800
|
-
|
|
801
|
-
|
|
1022
|
+
import_promises6 = require("fs/promises");
|
|
1023
|
+
import_node_path5 = __toESM(require("path"), 1);
|
|
802
1024
|
init_types();
|
|
803
1025
|
PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
|
|
804
1026
|
CONCURRENCY = 32;
|
|
@@ -809,7 +1031,7 @@ var init_node_modules = __esm({
|
|
|
809
1031
|
async function parsePnpmLockfile(lockfilePath, report) {
|
|
810
1032
|
let raw;
|
|
811
1033
|
try {
|
|
812
|
-
raw = await (0,
|
|
1034
|
+
raw = await (0, import_promises7.readFile)(lockfilePath, "utf8");
|
|
813
1035
|
} catch (cause) {
|
|
814
1036
|
throw new PatchstackError(
|
|
815
1037
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -1051,11 +1273,11 @@ function extractLeafName(trimmed) {
|
|
|
1051
1273
|
}
|
|
1052
1274
|
return name.length > 0 ? name : null;
|
|
1053
1275
|
}
|
|
1054
|
-
var
|
|
1276
|
+
var import_promises7, DEP_SECTIONS;
|
|
1055
1277
|
var init_pnpm = __esm({
|
|
1056
1278
|
"src/parsers/pnpm.ts"() {
|
|
1057
1279
|
"use strict";
|
|
1058
|
-
|
|
1280
|
+
import_promises7 = require("fs/promises");
|
|
1059
1281
|
init_types();
|
|
1060
1282
|
init_report();
|
|
1061
1283
|
DEP_SECTIONS = /* @__PURE__ */ new Set([
|
|
@@ -1070,7 +1292,7 @@ var init_pnpm = __esm({
|
|
|
1070
1292
|
async function parseYarnLockfile(lockfilePath, report) {
|
|
1071
1293
|
let raw;
|
|
1072
1294
|
try {
|
|
1073
|
-
raw = await (0,
|
|
1295
|
+
raw = await (0, import_promises8.readFile)(lockfilePath, "utf8");
|
|
1074
1296
|
} catch (cause) {
|
|
1075
1297
|
throw new PatchstackError(
|
|
1076
1298
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -1085,7 +1307,7 @@ async function parseYarnLockfile(lockfilePath, report) {
|
|
|
1085
1307
|
"LOCKFILE_PARSE_ERROR"
|
|
1086
1308
|
);
|
|
1087
1309
|
}
|
|
1088
|
-
const directNames = await readDirectDepNames(
|
|
1310
|
+
const directNames = await readDirectDepNames(import_node_path6.default.dirname(lockfilePath));
|
|
1089
1311
|
const entries = [];
|
|
1090
1312
|
const seen = /* @__PURE__ */ new Set();
|
|
1091
1313
|
for (const block of blocks) {
|
|
@@ -1250,7 +1472,7 @@ async function readDirectDepNames(cwd) {
|
|
|
1250
1472
|
const names = /* @__PURE__ */ new Set();
|
|
1251
1473
|
let raw;
|
|
1252
1474
|
try {
|
|
1253
|
-
raw = await (0,
|
|
1475
|
+
raw = await (0, import_promises8.readFile)(import_node_path6.default.join(cwd, "package.json"), "utf8");
|
|
1254
1476
|
} catch {
|
|
1255
1477
|
return names;
|
|
1256
1478
|
}
|
|
@@ -1275,12 +1497,12 @@ async function readDirectDepNames(cwd) {
|
|
|
1275
1497
|
}
|
|
1276
1498
|
return names;
|
|
1277
1499
|
}
|
|
1278
|
-
var
|
|
1500
|
+
var import_promises8, import_node_path6, NON_PACKAGE_KEYS, NON_REGISTRY_PROTOCOLS;
|
|
1279
1501
|
var init_yarn = __esm({
|
|
1280
1502
|
"src/parsers/yarn.ts"() {
|
|
1281
1503
|
"use strict";
|
|
1282
|
-
|
|
1283
|
-
|
|
1504
|
+
import_promises8 = require("fs/promises");
|
|
1505
|
+
import_node_path6 = __toESM(require("path"), 1);
|
|
1284
1506
|
init_types();
|
|
1285
1507
|
init_report();
|
|
1286
1508
|
NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
|
|
@@ -1292,7 +1514,7 @@ var init_yarn = __esm({
|
|
|
1292
1514
|
async function detectLockfile(cwd) {
|
|
1293
1515
|
const probed = await Promise.all(
|
|
1294
1516
|
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
1295
|
-
const filePath =
|
|
1517
|
+
const filePath = import_node_path7.default.join(cwd, candidate.filename);
|
|
1296
1518
|
return { ...candidate, filePath, present: await exists(filePath) };
|
|
1297
1519
|
})
|
|
1298
1520
|
);
|
|
@@ -1403,7 +1625,7 @@ function staleWarning(source, missing) {
|
|
|
1403
1625
|
async function presentLockfiles(cwd) {
|
|
1404
1626
|
const probed = await Promise.all(
|
|
1405
1627
|
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
1406
|
-
const filePath =
|
|
1628
|
+
const filePath = import_node_path7.default.join(cwd, candidate.filename);
|
|
1407
1629
|
return { ...candidate, filePath, present: await exists(filePath) };
|
|
1408
1630
|
})
|
|
1409
1631
|
);
|
|
@@ -1427,18 +1649,18 @@ async function runStrategy(detected, cwd, report) {
|
|
|
1427
1649
|
}
|
|
1428
1650
|
async function exists(filePath) {
|
|
1429
1651
|
try {
|
|
1430
|
-
await (0,
|
|
1652
|
+
await (0, import_promises9.access)(filePath);
|
|
1431
1653
|
return true;
|
|
1432
1654
|
} catch {
|
|
1433
1655
|
return false;
|
|
1434
1656
|
}
|
|
1435
1657
|
}
|
|
1436
|
-
var
|
|
1658
|
+
var import_promises9, import_node_path7, LOCKFILE_CANDIDATES;
|
|
1437
1659
|
var init_parsers = __esm({
|
|
1438
1660
|
"src/parsers/index.ts"() {
|
|
1439
1661
|
"use strict";
|
|
1440
|
-
|
|
1441
|
-
|
|
1662
|
+
import_promises9 = require("fs/promises");
|
|
1663
|
+
import_node_path7 = __toESM(require("path"), 1);
|
|
1442
1664
|
init_types();
|
|
1443
1665
|
init_consistency();
|
|
1444
1666
|
init_bun();
|
|
@@ -2879,12 +3101,12 @@ function isRegexForm(pattern) {
|
|
|
2879
3101
|
const parts = REGEX_FORM.exec(pattern);
|
|
2880
3102
|
return parts !== null && REGEX_SYNTAX.test(parts[1]);
|
|
2881
3103
|
}
|
|
2882
|
-
function pathMatches(pattern,
|
|
3104
|
+
function pathMatches(pattern, path8) {
|
|
2883
3105
|
if (isRegexForm(pattern)) {
|
|
2884
3106
|
const re = safeRegExp(pattern);
|
|
2885
|
-
return re ? re.test(
|
|
3107
|
+
return re ? re.test(path8) : false;
|
|
2886
3108
|
}
|
|
2887
|
-
const target = foldPathForScope(
|
|
3109
|
+
const target = foldPathForScope(path8);
|
|
2888
3110
|
const folded = foldPathForScope(pattern);
|
|
2889
3111
|
if (folded.includes("*")) {
|
|
2890
3112
|
const trailing = folded.endsWith("/*");
|
|
@@ -4426,7 +4648,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4426
4648
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
4427
4649
|
try {
|
|
4428
4650
|
const mod = await import(moduleName);
|
|
4429
|
-
const restore = patchHttpModule(mod.default ?? mod, block, screen);
|
|
4651
|
+
const restore = patchHttpModule(mod.default ?? mod, block, screen, skip);
|
|
4430
4652
|
if (restore) restores.push(restore);
|
|
4431
4653
|
} catch {
|
|
4432
4654
|
}
|
|
@@ -4440,7 +4662,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
4440
4662
|
}
|
|
4441
4663
|
};
|
|
4442
4664
|
}
|
|
4443
|
-
function patchHttpModule(http, block, screen) {
|
|
4665
|
+
function patchHttpModule(http, block, screen, skip) {
|
|
4444
4666
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
4445
4667
|
const originalRequest = http.request;
|
|
4446
4668
|
const originalGet = http.get;
|
|
@@ -4451,7 +4673,7 @@ function patchHttpModule(http, block, screen) {
|
|
|
4451
4673
|
}
|
|
4452
4674
|
if (target && screen && target.host && screen.isIP(target.host) === 0 && !screen.isExempt(target.host)) {
|
|
4453
4675
|
try {
|
|
4454
|
-
args = withScreeningLookup(args, target, block, screen.lookup);
|
|
4676
|
+
args = withScreeningLookup(args, target, block, screen.lookup, skip);
|
|
4455
4677
|
} catch {
|
|
4456
4678
|
}
|
|
4457
4679
|
}
|
|
@@ -4483,8 +4705,8 @@ function extractHttpTarget(args) {
|
|
|
4483
4705
|
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
4484
4706
|
const protocol = first.protocol || "http:";
|
|
4485
4707
|
const port = first.port ? `:${first.port}` : "";
|
|
4486
|
-
const
|
|
4487
|
-
return { url: `${protocol}//${host}${port}${
|
|
4708
|
+
const path8 = first.path || "/";
|
|
4709
|
+
return { url: `${protocol}//${host}${port}${path8}`, host, method: first.method || "GET" };
|
|
4488
4710
|
}
|
|
4489
4711
|
} catch {
|
|
4490
4712
|
}
|
|
@@ -4504,7 +4726,9 @@ function screenResolved(addresses, target, block) {
|
|
|
4504
4726
|
}
|
|
4505
4727
|
return null;
|
|
4506
4728
|
}
|
|
4507
|
-
function withScreeningLookup(args, target, block, lookup) {
|
|
4729
|
+
function withScreeningLookup(args, target, block, lookup, skip) {
|
|
4730
|
+
const own = callerLookup(args);
|
|
4731
|
+
const resolve = typeof own === "function" ? own : lookup;
|
|
4508
4732
|
const screeningLookup = (hostname, options, callback) => {
|
|
4509
4733
|
let opts = options;
|
|
4510
4734
|
let cb = callback;
|
|
@@ -4513,29 +4737,43 @@ function withScreeningLookup(args, target, block, lookup) {
|
|
|
4513
4737
|
opts = {};
|
|
4514
4738
|
}
|
|
4515
4739
|
if (!opts || typeof opts !== "object") opts = {};
|
|
4740
|
+
const handOver = (list) => {
|
|
4741
|
+
if (opts.all) return cb(null, list);
|
|
4742
|
+
const first = list[0];
|
|
4743
|
+
if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
|
|
4744
|
+
return cb(null, first.address, first.family);
|
|
4745
|
+
};
|
|
4746
|
+
let answered = false;
|
|
4516
4747
|
try {
|
|
4517
|
-
|
|
4748
|
+
resolve(hostname, { ...opts, all: true }, (err, addresses) => {
|
|
4749
|
+
answered = true;
|
|
4518
4750
|
if (err) return cb(err);
|
|
4519
4751
|
const list = Array.isArray(addresses) ? addresses : [];
|
|
4520
|
-
|
|
4752
|
+
let blocked;
|
|
4753
|
+
try {
|
|
4754
|
+
blocked = screenResolved(list, target, block);
|
|
4755
|
+
} catch {
|
|
4756
|
+
skip("screen-failed", { host: hostname });
|
|
4757
|
+
return handOver(list);
|
|
4758
|
+
}
|
|
4521
4759
|
if (blocked) {
|
|
4522
4760
|
return cb(new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host} resolved to ${blocked}`));
|
|
4523
4761
|
}
|
|
4524
|
-
|
|
4525
|
-
const first = list[0];
|
|
4526
|
-
if (!first) return cb(new Error(`Patchstack: could not resolve ${hostname}`));
|
|
4527
|
-
return cb(null, first.address, first.family);
|
|
4762
|
+
return handOver(list);
|
|
4528
4763
|
});
|
|
4529
|
-
} catch {
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
} catch {
|
|
4533
|
-
cb(new Error(`Patchstack: lookup failed for ${hostname}`));
|
|
4534
|
-
}
|
|
4764
|
+
} catch (err) {
|
|
4765
|
+
if (answered) throw err;
|
|
4766
|
+
return cb(err);
|
|
4535
4767
|
}
|
|
4536
4768
|
};
|
|
4537
4769
|
return injectLookupOption(args, screeningLookup);
|
|
4538
4770
|
}
|
|
4771
|
+
function callerLookup(args) {
|
|
4772
|
+
for (const arg of args) {
|
|
4773
|
+
if (arg && typeof arg === "object" && !(arg instanceof URL) && typeof arg.lookup === "function") return arg.lookup;
|
|
4774
|
+
}
|
|
4775
|
+
return void 0;
|
|
4776
|
+
}
|
|
4539
4777
|
function injectLookupOption(args, lookup) {
|
|
4540
4778
|
const first = args[0];
|
|
4541
4779
|
if (first && typeof first === "object" && !(first instanceof URL)) {
|
|
@@ -4833,12 +5071,12 @@ var fsMod;
|
|
|
4833
5071
|
async function loadFs() {
|
|
4834
5072
|
if (fsMod !== void 0) return fsMod;
|
|
4835
5073
|
try {
|
|
4836
|
-
const [fs,
|
|
5074
|
+
const [fs, path8] = await Promise.all([import("fs"), import("path")]);
|
|
4837
5075
|
fsMod = {
|
|
4838
5076
|
readFileSync: fs.readFileSync,
|
|
4839
5077
|
writeFileSync: fs.writeFileSync,
|
|
4840
5078
|
mkdirSync: fs.mkdirSync,
|
|
4841
|
-
join:
|
|
5079
|
+
join: path8.join
|
|
4842
5080
|
};
|
|
4843
5081
|
} catch {
|
|
4844
5082
|
fsMod = null;
|
|
@@ -5132,13 +5370,13 @@ var MAX_CAPTURED_VALUE_CHARS = 512;
|
|
|
5132
5370
|
var MAX_QUERY_KEYS = 10;
|
|
5133
5371
|
var MAX_METHOD_CHARS = 16;
|
|
5134
5372
|
var MAX_BODY_BYTES2 = 64 * 1024;
|
|
5135
|
-
function queryKeysOf(
|
|
5136
|
-
if (typeof
|
|
5137
|
-
const start =
|
|
5373
|
+
function queryKeysOf(path8) {
|
|
5374
|
+
if (typeof path8 !== "string") return { keys: [], total: 0 };
|
|
5375
|
+
const start = path8.indexOf("?");
|
|
5138
5376
|
if (start === -1) return { keys: [], total: 0 };
|
|
5139
5377
|
let names;
|
|
5140
5378
|
try {
|
|
5141
|
-
names = [...new URLSearchParams(
|
|
5379
|
+
names = [...new URLSearchParams(path8.slice(start + 1)).keys()];
|
|
5142
5380
|
} catch {
|
|
5143
5381
|
return { keys: [], total: 0 };
|
|
5144
5382
|
}
|
|
@@ -5277,10 +5515,10 @@ function revisionOf(rule) {
|
|
|
5277
5515
|
if (typeof revision === "number" && Number.isFinite(revision)) return String(revision);
|
|
5278
5516
|
return null;
|
|
5279
5517
|
}
|
|
5280
|
-
function routeOf(
|
|
5281
|
-
if (typeof
|
|
5282
|
-
const cut =
|
|
5283
|
-
return cut === -1 ?
|
|
5518
|
+
function routeOf(path8) {
|
|
5519
|
+
if (typeof path8 !== "string" || path8 === "") return null;
|
|
5520
|
+
const cut = path8.search(/[?#]/);
|
|
5521
|
+
return cut === -1 ? path8 : path8.slice(0, cut);
|
|
5284
5522
|
}
|
|
5285
5523
|
function createDetectionReporter(opts) {
|
|
5286
5524
|
const siteUuid = opts.siteUuid ?? process.env?.PATCHSTACK_SITE_UUID;
|
|
@@ -6116,8 +6354,17 @@ function createServerFnGuard({ protection }) {
|
|
|
6116
6354
|
return { rule: body.rule, message: body.message || "Blocked by Patchstack" };
|
|
6117
6355
|
};
|
|
6118
6356
|
}
|
|
6357
|
+
function listOption(value, fallback, name, onError) {
|
|
6358
|
+
if (value === void 0) return fallback;
|
|
6359
|
+
if (Array.isArray(value)) return value;
|
|
6360
|
+
notify(onError, new Error(`patchstack: \`${name}\` has to be an array; the value configured is ignored`), "onError");
|
|
6361
|
+
return fallback;
|
|
6362
|
+
}
|
|
6119
6363
|
async function createProtection(options = {}) {
|
|
6120
6364
|
const onError = options.onError;
|
|
6365
|
+
const allowHosts = listOption(options.allowHosts, [], "allowHosts", onError);
|
|
6366
|
+
const configuredResponseRules = listOption(options.responseRules, DEFAULT_RESPONSE_RULES, "responseRules", onError);
|
|
6367
|
+
const configuredEgressRules = listOption(options.egressRules, DEFAULT_EGRESS_RULES, "egressRules", onError);
|
|
6121
6368
|
const userOnDetect = options.onDetect ?? defaultOnDetect;
|
|
6122
6369
|
const apiKey = await resolveApiKey(options);
|
|
6123
6370
|
const firewallLog = apiKey && telemetryEnabled() && options.reportFirewallLog !== false ? createFirewallLogReporter({
|
|
@@ -6217,9 +6464,9 @@ async function createProtection(options = {}) {
|
|
|
6217
6464
|
const applyBundle = (delivered) => {
|
|
6218
6465
|
const incoming = delivered.firewall ?? [];
|
|
6219
6466
|
requestRules = byPhase(incoming, "request");
|
|
6220
|
-
responseRules = [...
|
|
6467
|
+
responseRules = [...configuredResponseRules, ...byPhase(incoming, "response")];
|
|
6221
6468
|
screenCap = responseScreenCap(responseRules);
|
|
6222
|
-
egressRules = [...
|
|
6469
|
+
egressRules = [...configuredEgressRules, ...byPhase(incoming, "egress")];
|
|
6223
6470
|
engine = new RuleEngine({
|
|
6224
6471
|
firewall: requestRules,
|
|
6225
6472
|
whitelists: delivered.whitelists,
|
|
@@ -6688,7 +6935,7 @@ async function createProtection(options = {}) {
|
|
|
6688
6935
|
return origEnd(cb);
|
|
6689
6936
|
};
|
|
6690
6937
|
};
|
|
6691
|
-
const allow = new Set(
|
|
6938
|
+
const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
|
|
6692
6939
|
const egressShouldBlock = (url, host, method) => {
|
|
6693
6940
|
if (host && allow.has(host.toLowerCase())) return false;
|
|
6694
6941
|
let egressPath = null;
|
|
@@ -6740,10 +6987,17 @@ async function createProtection(options = {}) {
|
|
|
6740
6987
|
return { request: requestRules, response: responseRules, egress: egressRules };
|
|
6741
6988
|
},
|
|
6742
6989
|
/**
|
|
6743
|
-
* Enforcement coverage: how often the guard
|
|
6744
|
-
* `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
|
|
6745
|
-
* `egress:resolver-failed`
|
|
6746
|
-
*
|
|
6990
|
+
* Enforcement coverage: how often the guard failed open rather than inspecting, in the cases it can
|
|
6991
|
+
* observe itself — keyed `<phase>:<reason>` (e.g. `response:body-cap`, `request:body-cap`,
|
|
6992
|
+
* `response:live-stream`, `egress:resolver-failed` for a fetch call whose address could not be
|
|
6993
|
+
* resolved). "Always-on" is not "always inspected", so surface this (or pass `onSkip`) rather than
|
|
6994
|
+
* assuming coverage.
|
|
6995
|
+
*
|
|
6996
|
+
* Not a total, in two ways. An outbound call can miss the address check without being counted:
|
|
6997
|
+
* whether it resolved through the screening resolver depends on the agent and the socket pool,
|
|
6998
|
+
* which this guard cannot observe. And a resolver failure on the node path appears nowhere here
|
|
6999
|
+
* because it is not a bypass — that resolver is the connection's, so the call fails instead.
|
|
7000
|
+
* Zero skips means nothing the guard can see was bypassed, not that nothing was.
|
|
6747
7001
|
*/
|
|
6748
7002
|
coverage() {
|
|
6749
7003
|
return { skipped: { ...skipCounts } };
|
|
@@ -6876,7 +7130,7 @@ async function createProtection(options = {}) {
|
|
|
6876
7130
|
// same skip accounting as the request/response phases.
|
|
6877
7131
|
onSkip: ({ reason, detail }) => recordSkip("egress", reason, detail),
|
|
6878
7132
|
dnsScreen: options.screenDns !== false,
|
|
6879
|
-
allowHosts
|
|
7133
|
+
allowHosts
|
|
6880
7134
|
});
|
|
6881
7135
|
}
|
|
6882
7136
|
const cwd = options.cwd ?? (typeof process !== "undefined" ? process.cwd() : void 0);
|
|
@@ -7378,20 +7632,20 @@ function requestMetaFromContext(reqCtx) {
|
|
|
7378
7632
|
}
|
|
7379
7633
|
function requestMeta(shaped, request) {
|
|
7380
7634
|
const client = shaped?._clientIp ?? { ip: null, source: "unavailable" };
|
|
7381
|
-
let
|
|
7635
|
+
let path8 = null;
|
|
7382
7636
|
let method = null;
|
|
7383
7637
|
let userAgent = null;
|
|
7384
7638
|
if (request) {
|
|
7385
7639
|
try {
|
|
7386
7640
|
const u = new URL(request.url);
|
|
7387
|
-
|
|
7641
|
+
path8 = u.pathname + u.search;
|
|
7388
7642
|
} catch {
|
|
7389
|
-
|
|
7643
|
+
path8 = typeof request.url === "string" ? request.url : null;
|
|
7390
7644
|
}
|
|
7391
7645
|
method = request.method ?? null;
|
|
7392
7646
|
userAgent = request.headers?.get?.("user-agent") ?? null;
|
|
7393
7647
|
}
|
|
7394
|
-
return { method, path:
|
|
7648
|
+
return { method, path: path8, ip: client.ip, clientIpSource: client.source, userAgent, event: eventFor(request) };
|
|
7395
7649
|
}
|
|
7396
7650
|
function nodeRequestMeta(req, client) {
|
|
7397
7651
|
if (!req) return {};
|