@patchstack/connect 0.5.1 → 0.5.3
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 +142 -8
- package/README.md +94 -3
- package/dist/{chunk-3G2I6QL6.js → chunk-IOODP4ZB.js} +19 -2
- package/dist/chunk-IOODP4ZB.js.map +1 -0
- package/dist/cli.js +1257 -196
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect/runtime/report-listeners.cjs +360 -0
- package/dist/protect/templates/express-guard.cjs +30 -12
- package/dist/protect/templates/express-guard.js +30 -12
- package/dist/protect/templates/express-guard.ts +35 -12
- package/dist/protect/templates/fastify-plugin.cjs +14 -1
- package/dist/protect/templates/fastify-plugin.js +14 -1
- package/dist/protect/templates/fastify-plugin.ts +14 -1
- package/dist/protect/templates/generic-guard.cjs +36 -12
- package/dist/protect/templates/generic-guard.js +36 -12
- package/dist/protect/templates/generic-guard.ts +41 -12
- package/dist/protect.cjs +215 -23
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +40 -0
- package/dist/protect.d.ts +40 -0
- package/dist/protect.edge.js +202 -22
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +192 -25
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2MMPQN2A.js → refresh-manifest-3QGLYXV5.js} +2 -2
- package/dist/{refresh-manifest-2MMPQN2A.js.map → refresh-manifest-3QGLYXV5.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3G2I6QL6.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
|
|
7
7
|
// src/parsers/index.ts
|
|
@@ -1210,8 +1210,8 @@ function computeManifestChecksum(packages) {
|
|
|
1210
1210
|
var TOKEN_SKEW_MS = 6e4;
|
|
1211
1211
|
function buildTokenUrl(manifestEndpoint) {
|
|
1212
1212
|
const url = new URL(manifestEndpoint);
|
|
1213
|
-
const
|
|
1214
|
-
url.pathname =
|
|
1213
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1214
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
|
|
1215
1215
|
url.search = "";
|
|
1216
1216
|
url.hash = "";
|
|
1217
1217
|
return url.toString();
|
|
@@ -1288,6 +1288,27 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1288
1288
|
return first.response;
|
|
1289
1289
|
}
|
|
1290
1290
|
|
|
1291
|
+
// src/build-id.ts
|
|
1292
|
+
var BUILD_ID = /^[0-9a-f]{64}$/i;
|
|
1293
|
+
function canonicalBuildId(value) {
|
|
1294
|
+
if (typeof value !== "string") return null;
|
|
1295
|
+
const trimmed = value.trim();
|
|
1296
|
+
return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
1297
|
+
}
|
|
1298
|
+
var BUILD_STAMP_KEY = "_patchstack";
|
|
1299
|
+
function readBuildStamp(bundle) {
|
|
1300
|
+
if (bundle === null || typeof bundle !== "object") return null;
|
|
1301
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
1302
|
+
if (namespace === null || typeof namespace !== "object") return null;
|
|
1303
|
+
return canonicalBuildId(namespace.build_id);
|
|
1304
|
+
}
|
|
1305
|
+
function hasRawBuildStamp(bundle) {
|
|
1306
|
+
if (bundle === null || typeof bundle !== "object") return false;
|
|
1307
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
1308
|
+
if (namespace === null || typeof namespace !== "object") return false;
|
|
1309
|
+
return "build_id" in namespace;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1291
1312
|
// src/client.ts
|
|
1292
1313
|
var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
1293
1314
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -1295,16 +1316,16 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
|
1295
1316
|
function claimTokenHeader(config) {
|
|
1296
1317
|
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1297
1318
|
}
|
|
1298
|
-
function claimOutcomeLines(
|
|
1319
|
+
function claimOutcomeLines(claim3, config) {
|
|
1299
1320
|
if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
|
|
1300
|
-
if (
|
|
1301
|
-
const dashboard = typeof
|
|
1321
|
+
if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
|
|
1322
|
+
const dashboard = typeof claim3.dashboard_url === "string" && claim3.dashboard_url !== "" ? [`Dashboard: ${claim3.dashboard_url}`] : [];
|
|
1302
1323
|
return [
|
|
1303
|
-
|
|
1324
|
+
claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
|
|
1304
1325
|
...dashboard
|
|
1305
1326
|
];
|
|
1306
1327
|
}
|
|
1307
|
-
const why =
|
|
1328
|
+
const why = claim3 === void 0 ? "Patchstack did not act on the claim token" : claim3.state === "owned-by-other" ? "this site belongs to a different Patchstack account" : claim3.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
|
|
1308
1329
|
return [
|
|
1309
1330
|
`Not connected to your account: ${why}.`,
|
|
1310
1331
|
"Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
|
|
@@ -1329,8 +1350,8 @@ function authFailureMessage(status, config) {
|
|
|
1329
1350
|
}
|
|
1330
1351
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1331
1352
|
const url = new URL(manifestEndpoint);
|
|
1332
|
-
const
|
|
1333
|
-
url.pathname =
|
|
1353
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1354
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
|
|
1334
1355
|
url.search = "";
|
|
1335
1356
|
url.hash = "";
|
|
1336
1357
|
return url.toString();
|
|
@@ -1345,26 +1366,27 @@ function buildSettingsUrl(endpoint, siteUuid) {
|
|
|
1345
1366
|
}
|
|
1346
1367
|
function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
|
|
1347
1368
|
const url = new URL(manifestEndpoint);
|
|
1348
|
-
const
|
|
1349
|
-
url.pathname =
|
|
1369
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1370
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
|
|
1350
1371
|
url.search = "";
|
|
1351
1372
|
url.hash = "";
|
|
1352
1373
|
return url.toString();
|
|
1353
1374
|
}
|
|
1354
1375
|
function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
1355
1376
|
const url = new URL(manifestEndpoint);
|
|
1356
|
-
const
|
|
1357
|
-
url.pathname =
|
|
1377
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1378
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
|
|
1358
1379
|
url.search = "";
|
|
1359
1380
|
url.hash = "";
|
|
1360
1381
|
return url.toString();
|
|
1361
1382
|
}
|
|
1362
|
-
async function postInputMap(config, map) {
|
|
1383
|
+
async function postInputMap(config, map, buildId) {
|
|
1363
1384
|
if (config.siteUuid === null) {
|
|
1364
1385
|
return { result: "skipped", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
|
|
1365
1386
|
}
|
|
1366
1387
|
const url = buildInputMapUrl(config.endpoint, config.siteUuid);
|
|
1367
1388
|
try {
|
|
1389
|
+
const canonical = canonicalBuildId(buildId);
|
|
1368
1390
|
const response = await pulseFetch(config, url, {
|
|
1369
1391
|
method: "POST",
|
|
1370
1392
|
headers: {
|
|
@@ -1372,7 +1394,12 @@ async function postInputMap(config, map) {
|
|
|
1372
1394
|
"Content-Type": "application/json",
|
|
1373
1395
|
"User-Agent": "@patchstack/connect"
|
|
1374
1396
|
},
|
|
1375
|
-
|
|
1397
|
+
// `build_id` is additive to the v3 document: a reader that ignores it behaves exactly as before.
|
|
1398
|
+
// It is the digest of the policy map carrying these coordinates, so the server can serve a rule pinned
|
|
1399
|
+
// to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
|
|
1400
|
+
// run could not bind the map into a pre-bundle guard — an absent field is a question the server
|
|
1401
|
+
// answers by withholding enforceability, which is the safe direction.
|
|
1402
|
+
body: JSON.stringify(canonical === null ? map : { ...map, build_id: canonical }),
|
|
1376
1403
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
1377
1404
|
});
|
|
1378
1405
|
if (response.status === 404) {
|
|
@@ -1690,7 +1717,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
|
|
|
1690
1717
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
1691
1718
|
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
1692
1719
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1693
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
1720
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
|
|
1694
1721
|
const now = options.now ?? Date.now;
|
|
1695
1722
|
const started = now();
|
|
1696
1723
|
const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
|
|
@@ -3011,6 +3038,112 @@ import { join as join10, dirname as dirname5 } from "path";
|
|
|
3011
3038
|
import { execFileSync } from "child_process";
|
|
3012
3039
|
import { existsSync as existsSync7 } from "fs";
|
|
3013
3040
|
import { dirname as dirname4, join as join9, resolve } from "path";
|
|
3041
|
+
|
|
3042
|
+
// src/protect/install/node-flags.ts
|
|
3043
|
+
var flagName = (token) => token.split("=")[0];
|
|
3044
|
+
var NO_OPERAND_FLAGS = /* @__PURE__ */ new Set([
|
|
3045
|
+
"--disallow-code-generation-from-strings",
|
|
3046
|
+
"--enable-source-maps",
|
|
3047
|
+
"--experimental-import-meta-resolve",
|
|
3048
|
+
"--experimental-json-modules",
|
|
3049
|
+
"--experimental-vm-modules",
|
|
3050
|
+
"--frozen-intrinsics",
|
|
3051
|
+
"--no-deprecation",
|
|
3052
|
+
"--no-warnings",
|
|
3053
|
+
"--pending-deprecation",
|
|
3054
|
+
"--preserve-symlinks",
|
|
3055
|
+
"--preserve-symlinks-main",
|
|
3056
|
+
"--throw-deprecation",
|
|
3057
|
+
"--trace-deprecation",
|
|
3058
|
+
"--trace-exit",
|
|
3059
|
+
"--trace-uncaught",
|
|
3060
|
+
"--trace-warnings",
|
|
3061
|
+
"--use-strict",
|
|
3062
|
+
"--zero-fill-buffers"
|
|
3063
|
+
]);
|
|
3064
|
+
var VALUED_FLAGS = /* @__PURE__ */ new Set([
|
|
3065
|
+
"--dns-result-order",
|
|
3066
|
+
"--max-http-header-size",
|
|
3067
|
+
"--max-old-space-size",
|
|
3068
|
+
"--max-semi-space-size",
|
|
3069
|
+
"--stack-size",
|
|
3070
|
+
"--title",
|
|
3071
|
+
"--unhandled-rejections"
|
|
3072
|
+
]);
|
|
3073
|
+
var CODE_LOADING_FLAGS = /* @__PURE__ */ new Set([
|
|
3074
|
+
"--require",
|
|
3075
|
+
"-r",
|
|
3076
|
+
"--import",
|
|
3077
|
+
"--loader",
|
|
3078
|
+
"--experimental-loader",
|
|
3079
|
+
"--eval",
|
|
3080
|
+
"-e",
|
|
3081
|
+
"--print",
|
|
3082
|
+
"-p",
|
|
3083
|
+
"--inspect",
|
|
3084
|
+
"--inspect-brk",
|
|
3085
|
+
"--inspect-wait",
|
|
3086
|
+
"--inspect-port",
|
|
3087
|
+
"--debug",
|
|
3088
|
+
"--debug-brk"
|
|
3089
|
+
]);
|
|
3090
|
+
var TAKES_NEXT_TOKEN = /* @__PURE__ */ new Set(["--require", "-r", "--import", "--loader", "--experimental-loader", "--eval", "-e", "--print", "-p", "--inspect-port"]);
|
|
3091
|
+
function tokenizeNodeOptions(value) {
|
|
3092
|
+
const tokens = [];
|
|
3093
|
+
let current = "";
|
|
3094
|
+
let quoted = false;
|
|
3095
|
+
let started = false;
|
|
3096
|
+
for (const ch of value) {
|
|
3097
|
+
if (ch === '"') {
|
|
3098
|
+
quoted = !quoted;
|
|
3099
|
+
started = true;
|
|
3100
|
+
continue;
|
|
3101
|
+
}
|
|
3102
|
+
if (!quoted && /\s/.test(ch)) {
|
|
3103
|
+
if (started) tokens.push(current);
|
|
3104
|
+
current = "";
|
|
3105
|
+
started = false;
|
|
3106
|
+
continue;
|
|
3107
|
+
}
|
|
3108
|
+
current += ch;
|
|
3109
|
+
started = true;
|
|
3110
|
+
}
|
|
3111
|
+
if (quoted) return null;
|
|
3112
|
+
if (started) tokens.push(current);
|
|
3113
|
+
return tokens;
|
|
3114
|
+
}
|
|
3115
|
+
function classifyNodeOptions(value) {
|
|
3116
|
+
const tokens = tokenizeNodeOptions(value);
|
|
3117
|
+
if (tokens === null) return { kind: "refused", why: "NODE_OPTIONS has an unbalanced double quote, so this cannot read it the way Node will" };
|
|
3118
|
+
for (const token of tokens) {
|
|
3119
|
+
const name = flagName(token);
|
|
3120
|
+
if (CODE_LOADING_FLAGS.has(name)) {
|
|
3121
|
+
return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which runs code or opens a port before the listener reporter is in place` };
|
|
3122
|
+
}
|
|
3123
|
+
if (NO_OPERAND_FLAGS.has(token)) continue;
|
|
3124
|
+
if (token.includes("=") && VALUED_FLAGS.has(name)) continue;
|
|
3125
|
+
return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which this does not recognise well enough to say what it does` };
|
|
3126
|
+
}
|
|
3127
|
+
return { kind: "safe" };
|
|
3128
|
+
}
|
|
3129
|
+
function withoutCodeLoading(value) {
|
|
3130
|
+
if (value === void 0 || value.trim() === "") return "";
|
|
3131
|
+
const tokens = tokenizeNodeOptions(value);
|
|
3132
|
+
if (tokens === null) return "";
|
|
3133
|
+
const kept = [];
|
|
3134
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3135
|
+
const token = tokens[i];
|
|
3136
|
+
const name = flagName(token);
|
|
3137
|
+
if (CODE_LOADING_FLAGS.has(name)) {
|
|
3138
|
+
if (!token.includes("=") && TAKES_NEXT_TOKEN.has(name)) i++;
|
|
3139
|
+
continue;
|
|
3140
|
+
}
|
|
3141
|
+
if (NO_OPERAND_FLAGS.has(token) || token.includes("=") && VALUED_FLAGS.has(name)) kept.push(token);
|
|
3142
|
+
}
|
|
3143
|
+
return kept.map((token) => /\s/.test(token) ? `"${token}"` : token).join(" ");
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
// src/protect/install/source-scope.ts
|
|
3014
3147
|
var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
|
|
3015
3148
|
function lastTopLevelImportLine(source) {
|
|
3016
3149
|
const lines = source.split("\n");
|
|
@@ -3089,7 +3222,10 @@ function advanceDepth(line, inBlockComment, add) {
|
|
|
3089
3222
|
function parses(filePath) {
|
|
3090
3223
|
if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
|
|
3091
3224
|
try {
|
|
3092
|
-
execFileSync(process.execPath, ["--check", filePath], {
|
|
3225
|
+
execFileSync(process.execPath, ["--check", filePath], {
|
|
3226
|
+
stdio: "pipe",
|
|
3227
|
+
env: { ...process.env, NODE_OPTIONS: withoutCodeLoading(process.env.NODE_OPTIONS) }
|
|
3228
|
+
});
|
|
3093
3229
|
return true;
|
|
3094
3230
|
} catch (error) {
|
|
3095
3231
|
return isSyntaxError(error) ? false : null;
|
|
@@ -4183,9 +4319,9 @@ async function collectGuideState(cwd) {
|
|
|
4183
4319
|
endpointOverride,
|
|
4184
4320
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
4185
4321
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
4186
|
-
//
|
|
4187
|
-
//
|
|
4188
|
-
prebuildWired: (pkg?.scripts?.prebuild ?? "")
|
|
4322
|
+
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
4323
|
+
// must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
|
|
4324
|
+
prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
|
|
4189
4325
|
postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
4190
4326
|
widgetInstalled: widget.found,
|
|
4191
4327
|
widgetTokenMatches: widget.uuidMatches,
|
|
@@ -4358,12 +4494,15 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4358
4494
|
lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
|
|
4359
4495
|
}
|
|
4360
4496
|
lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
|
|
4497
|
+
lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
|
|
4361
4498
|
}
|
|
4362
4499
|
lines.push("");
|
|
4363
4500
|
if (state.claimUrl !== null) {
|
|
4364
4501
|
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
|
|
4365
4502
|
lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
|
|
4366
4503
|
lines.push(detail("Open this link in a browser. The CLI never opens it."));
|
|
4504
|
+
lines.push(detail("Or from this terminal \u2192 npx @patchstack/connect claim"));
|
|
4505
|
+
lines.push(detail(" (prints a link to sign in with, then attaches the site to that account)"));
|
|
4367
4506
|
if (state.endpointOverride !== null) {
|
|
4368
4507
|
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
4369
4508
|
}
|
|
@@ -4406,7 +4545,12 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4406
4545
|
}
|
|
4407
4546
|
|
|
4408
4547
|
// src/login.ts
|
|
4409
|
-
import { mkdtempSync
|
|
4548
|
+
import { mkdtempSync } from "fs";
|
|
4549
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4550
|
+
import path12 from "path";
|
|
4551
|
+
|
|
4552
|
+
// src/device-flow.ts
|
|
4553
|
+
import { readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
4410
4554
|
import { tmpdir } from "os";
|
|
4411
4555
|
import path11 from "path";
|
|
4412
4556
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -4418,108 +4562,126 @@ function baseFrom(manifestEndpoint) {
|
|
|
4418
4562
|
url.hash = "";
|
|
4419
4563
|
return url.toString().replace(/\/$/, "");
|
|
4420
4564
|
}
|
|
4421
|
-
function pendingPath(siteUuid) {
|
|
4565
|
+
function pendingPath(siteUuid, intent) {
|
|
4422
4566
|
const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
|
|
4423
|
-
return path11.join(tmpdir(), `patchstack
|
|
4567
|
+
return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
|
|
4424
4568
|
}
|
|
4425
|
-
function
|
|
4426
|
-
writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), {
|
|
4569
|
+
function savePending(siteUuid, intent, pending) {
|
|
4570
|
+
writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
|
|
4571
|
+
encoding: "utf8",
|
|
4572
|
+
mode: 384
|
|
4573
|
+
});
|
|
4427
4574
|
}
|
|
4428
|
-
function
|
|
4575
|
+
function readPending(siteUuid, intent) {
|
|
4429
4576
|
try {
|
|
4430
|
-
return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
|
|
4577
|
+
return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
|
|
4431
4578
|
} catch {
|
|
4432
4579
|
return null;
|
|
4433
4580
|
}
|
|
4434
4581
|
}
|
|
4435
|
-
function
|
|
4582
|
+
function clearPending(siteUuid, intent) {
|
|
4436
4583
|
try {
|
|
4437
|
-
rmSync(pendingPath(siteUuid));
|
|
4584
|
+
rmSync(pendingPath(siteUuid, intent));
|
|
4438
4585
|
} catch {
|
|
4439
4586
|
}
|
|
4440
4587
|
}
|
|
4441
|
-
async function
|
|
4588
|
+
async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
|
|
4442
4589
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4443
4590
|
const now = deps.now ?? (() => Date.now());
|
|
4444
4591
|
if (config.siteUuid === null) {
|
|
4445
|
-
return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
|
|
4446
|
-
}
|
|
4447
|
-
const base = baseFrom(config.endpoint);
|
|
4448
|
-
const started = await fetchImpl(`${base}/device/code`, {
|
|
4449
|
-
method: "POST",
|
|
4450
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4451
|
-
body: JSON.stringify({ site_uuid: config.siteUuid })
|
|
4452
|
-
});
|
|
4453
|
-
if (started.status === 409) {
|
|
4454
4592
|
return {
|
|
4455
|
-
status: "
|
|
4456
|
-
message: "
|
|
4593
|
+
status: "failed",
|
|
4594
|
+
message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
|
|
4457
4595
|
};
|
|
4458
4596
|
}
|
|
4597
|
+
const base = baseFrom(config.endpoint);
|
|
4598
|
+
const started = await pulseFetch(
|
|
4599
|
+
config,
|
|
4600
|
+
`${base}/device/code`,
|
|
4601
|
+
{
|
|
4602
|
+
method: "POST",
|
|
4603
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4604
|
+
body: JSON.stringify({ site_uuid: config.siteUuid, intent })
|
|
4605
|
+
},
|
|
4606
|
+
fetchImpl
|
|
4607
|
+
);
|
|
4608
|
+
if (started.status === 409) return { status: "conflict", message: conflictMessage };
|
|
4459
4609
|
if (started.status === 404) {
|
|
4460
4610
|
return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
|
|
4461
4611
|
}
|
|
4462
4612
|
if (!started.ok) {
|
|
4463
|
-
return { status: "failed", message: `Could not start the
|
|
4613
|
+
return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
|
|
4464
4614
|
}
|
|
4465
4615
|
const body = await started.json();
|
|
4466
4616
|
const pending = {
|
|
4467
4617
|
deviceCode: body.device_code,
|
|
4468
4618
|
userCode: body.user_code,
|
|
4469
|
-
// Points at the API, which redirects to the dashboard SPA — the CLI only
|
|
4470
|
-
//
|
|
4619
|
+
// Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
|
|
4620
|
+
// and the approval page lives on the app.
|
|
4471
4621
|
verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
|
|
4472
4622
|
expiresAt: now() + body.expires_in * 1e3,
|
|
4473
4623
|
intervalMs: Math.max(1, body.interval) * 1e3
|
|
4474
4624
|
};
|
|
4475
|
-
|
|
4625
|
+
savePending(config.siteUuid, intent, pending);
|
|
4476
4626
|
return { status: "started", pending };
|
|
4477
4627
|
}
|
|
4478
|
-
async function
|
|
4628
|
+
async function pollDeviceToken(config, intent, pending, deps = {}) {
|
|
4479
4629
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4480
4630
|
const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
|
|
4481
4631
|
method: "POST",
|
|
4482
4632
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4483
4633
|
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
4484
4634
|
});
|
|
4485
|
-
if (polled.status === 428) return "pending";
|
|
4635
|
+
if (polled.status === 428) return { state: "pending" };
|
|
4486
4636
|
if (!polled.ok) {
|
|
4487
|
-
if (config.siteUuid !== null)
|
|
4488
|
-
return "expired";
|
|
4637
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4638
|
+
return { state: "expired" };
|
|
4489
4639
|
}
|
|
4490
|
-
const
|
|
4491
|
-
if (
|
|
4492
|
-
|
|
4493
|
-
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
4494
|
-
return "approved";
|
|
4640
|
+
const body = await polled.json();
|
|
4641
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4642
|
+
return { state: "approved", body };
|
|
4495
4643
|
}
|
|
4496
|
-
async function
|
|
4497
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4644
|
+
async function pollUntilApproved(config, intent, pending, deps = {}) {
|
|
4498
4645
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4499
4646
|
const now = deps.now ?? (() => Date.now());
|
|
4500
4647
|
const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
|
|
4501
|
-
const expired = { status: "expired", message: "The login request expired. Run the command again." };
|
|
4502
4648
|
while (now() < deadline) {
|
|
4503
4649
|
await sleep(pending.intervalMs);
|
|
4504
|
-
const
|
|
4505
|
-
|
|
4506
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4507
|
-
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
4508
|
-
});
|
|
4509
|
-
if (polled.status === 428) continue;
|
|
4510
|
-
if (!polled.ok) {
|
|
4511
|
-
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
4512
|
-
return expired;
|
|
4513
|
-
}
|
|
4514
|
-
const { api_key: apiKey } = await polled.json();
|
|
4515
|
-
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
4516
|
-
return { status: "failed", message: "Patchstack approved the request but returned no credential." };
|
|
4517
|
-
}
|
|
4518
|
-
await persistApiKey(process.cwd(), apiKey);
|
|
4519
|
-
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
4520
|
-
return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
|
|
4650
|
+
const outcome = await pollDeviceToken(config, intent, pending, deps);
|
|
4651
|
+
if (outcome.state !== "pending") return outcome;
|
|
4521
4652
|
}
|
|
4522
|
-
return expired;
|
|
4653
|
+
return { state: "expired" };
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
// src/login.ts
|
|
4657
|
+
var UNCLAIMED = "This site has not been claimed yet, so there is no owner to approve the request. Claim it with `patchstack-connect claim`, or delete .patchstackrc.json to provision a new site.";
|
|
4658
|
+
function readPendingLogin(siteUuid) {
|
|
4659
|
+
return readPending(siteUuid, "login");
|
|
4660
|
+
}
|
|
4661
|
+
async function startLogin(config, deps = {}) {
|
|
4662
|
+
const started = await requestDeviceCode(config, "login", UNCLAIMED, deps);
|
|
4663
|
+
if (started.status === "conflict") return { status: "unclaimed", message: started.message };
|
|
4664
|
+
return started;
|
|
4665
|
+
}
|
|
4666
|
+
async function redeemIfApproved(config, pending, deps = {}) {
|
|
4667
|
+
const polled = await pollDeviceToken(config, "login", pending, deps);
|
|
4668
|
+
if (polled.state !== "approved") return polled.state;
|
|
4669
|
+
const apiKey = polled.body?.api_key;
|
|
4670
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
|
|
4671
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4672
|
+
return "approved";
|
|
4673
|
+
}
|
|
4674
|
+
async function waitForApproval(config, pending, deps = {}) {
|
|
4675
|
+
const polled = await pollUntilApproved(config, "login", pending, deps);
|
|
4676
|
+
if (polled.state === "expired") {
|
|
4677
|
+
return { status: "expired", message: "The login request expired. Run the command again." };
|
|
4678
|
+
}
|
|
4679
|
+
const apiKey = polled.body?.api_key;
|
|
4680
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
4681
|
+
return { status: "failed", message: "Patchstack approved the request but returned no credential." };
|
|
4682
|
+
}
|
|
4683
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4684
|
+
return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
|
|
4523
4685
|
}
|
|
4524
4686
|
async function login(config, onPrompt, deps = {}) {
|
|
4525
4687
|
const started = await startLogin(config, deps);
|
|
@@ -4530,16 +4692,546 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
4530
4692
|
return waitForApproval(config, started.pending, deps);
|
|
4531
4693
|
}
|
|
4532
4694
|
|
|
4695
|
+
// src/claim.ts
|
|
4696
|
+
var ALREADY_CLAIMED = "This site is already attached to a Patchstack account. Nothing to claim \u2014 sign in at the dashboard to see it, or use `patchstack-connect login` if you need its credential back.";
|
|
4697
|
+
function readPendingClaim(siteUuid) {
|
|
4698
|
+
return readPending(siteUuid, "claim");
|
|
4699
|
+
}
|
|
4700
|
+
async function startClaim(config, deps = {}) {
|
|
4701
|
+
const started = await requestDeviceCode(config, "claim", ALREADY_CLAIMED, deps);
|
|
4702
|
+
if (started.status === "conflict") return { status: "already-claimed", message: started.message };
|
|
4703
|
+
return started;
|
|
4704
|
+
}
|
|
4705
|
+
async function settle(body) {
|
|
4706
|
+
const account = typeof body?.account === "string" ? body.account : void 0;
|
|
4707
|
+
const dashboardUrl = typeof body?.dashboard_url === "string" ? body.dashboard_url : void 0;
|
|
4708
|
+
const apiKey = body?.api_key;
|
|
4709
|
+
let credentialSaved = false;
|
|
4710
|
+
if (typeof apiKey === "string" && apiKey.length > 0) {
|
|
4711
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4712
|
+
credentialSaved = true;
|
|
4713
|
+
}
|
|
4714
|
+
return { status: "claimed", account, dashboardUrl, credentialSaved };
|
|
4715
|
+
}
|
|
4716
|
+
async function redeemClaimIfApproved(config, pending, deps = {}) {
|
|
4717
|
+
const polled = await pollDeviceToken(config, "claim", pending, deps);
|
|
4718
|
+
if (polled.state === "pending") return "pending";
|
|
4719
|
+
if (polled.state === "expired") {
|
|
4720
|
+
return { status: "expired", message: "The claim request expired. Run the command again." };
|
|
4721
|
+
}
|
|
4722
|
+
return settle(polled.body);
|
|
4723
|
+
}
|
|
4724
|
+
async function waitForClaim(config, pending, deps = {}) {
|
|
4725
|
+
const polled = await pollUntilApproved(config, "claim", pending, deps);
|
|
4726
|
+
if (polled.state === "expired") {
|
|
4727
|
+
return { status: "expired", message: "The claim request expired. Run the command again." };
|
|
4728
|
+
}
|
|
4729
|
+
return settle(polled.body);
|
|
4730
|
+
}
|
|
4731
|
+
async function claim2(config, onPrompt, deps = {}) {
|
|
4732
|
+
const started = await startClaim(config, deps);
|
|
4733
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
4734
|
+
return {
|
|
4735
|
+
status: started.status === "started" ? "failed" : started.status,
|
|
4736
|
+
message: started.message
|
|
4737
|
+
};
|
|
4738
|
+
}
|
|
4739
|
+
onPrompt(started.pending.userCode, started.pending.verificationUri);
|
|
4740
|
+
return waitForClaim(config, started.pending, deps);
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
// src/protect/install/runtime/check.ts
|
|
4744
|
+
import { relative as relative2 } from "path";
|
|
4745
|
+
|
|
4746
|
+
// src/protect/install/runtime/entry.ts
|
|
4747
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, realpathSync, statSync as statSync3 } from "fs";
|
|
4748
|
+
import { join as join13, resolve as resolve3, sep } from "path";
|
|
4749
|
+
var CONVENTIONAL = [
|
|
4750
|
+
"server.js",
|
|
4751
|
+
"server.mjs",
|
|
4752
|
+
"server.cjs",
|
|
4753
|
+
"app.js",
|
|
4754
|
+
"app.mjs",
|
|
4755
|
+
"app.cjs",
|
|
4756
|
+
"index.js",
|
|
4757
|
+
"index.mjs",
|
|
4758
|
+
"index.cjs",
|
|
4759
|
+
"src/server.js",
|
|
4760
|
+
"src/server.mjs",
|
|
4761
|
+
"src/server.cjs",
|
|
4762
|
+
"src/index.js",
|
|
4763
|
+
"src/index.mjs",
|
|
4764
|
+
"src/index.cjs",
|
|
4765
|
+
"dist/server.js",
|
|
4766
|
+
"dist/index.js",
|
|
4767
|
+
"dist/main.js",
|
|
4768
|
+
"build/index.js",
|
|
4769
|
+
"build/server.js"
|
|
4770
|
+
];
|
|
4771
|
+
var LOADABLE = /\.(?:js|mjs|cjs)$/;
|
|
4772
|
+
function readPackage2(cwd) {
|
|
4773
|
+
try {
|
|
4774
|
+
const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
|
|
4775
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
4776
|
+
} catch {
|
|
4777
|
+
return null;
|
|
4778
|
+
}
|
|
4779
|
+
}
|
|
4780
|
+
function isFile(path14) {
|
|
4781
|
+
try {
|
|
4782
|
+
return statSync3(path14).isFile();
|
|
4783
|
+
} catch {
|
|
4784
|
+
return false;
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
function loadable(cwd, candidate) {
|
|
4788
|
+
const written = resolve3(cwd, candidate);
|
|
4789
|
+
if (!LOADABLE.test(written) || !isFile(written)) return null;
|
|
4790
|
+
let root;
|
|
4791
|
+
let path14;
|
|
4792
|
+
try {
|
|
4793
|
+
root = realpathSync(cwd);
|
|
4794
|
+
path14 = realpathSync(written);
|
|
4795
|
+
} catch {
|
|
4796
|
+
return null;
|
|
4797
|
+
}
|
|
4798
|
+
if (path14 !== root && !path14.startsWith(root + sep)) return null;
|
|
4799
|
+
return written;
|
|
4800
|
+
}
|
|
4801
|
+
function passable(token) {
|
|
4802
|
+
if (CODE_LOADING_FLAGS.has(flagName(token))) return false;
|
|
4803
|
+
if (NO_OPERAND_FLAGS.has(token)) return true;
|
|
4804
|
+
return token.includes("=") && VALUED_FLAGS.has(flagName(token));
|
|
4805
|
+
}
|
|
4806
|
+
function fromScript(cwd, script) {
|
|
4807
|
+
if (/[&|;><()#$`'"*?\[\]{}\\~\r\n]/.test(script)) return null;
|
|
4808
|
+
const parts = script.trim().split(/\s+/);
|
|
4809
|
+
if (parts[0] !== "node") return null;
|
|
4810
|
+
const nodeArgs = [];
|
|
4811
|
+
let rest = parts.slice(1);
|
|
4812
|
+
while (rest.length > 0 && rest[0].startsWith("-")) {
|
|
4813
|
+
const token = rest[0];
|
|
4814
|
+
if (!passable(token)) return null;
|
|
4815
|
+
nodeArgs.push(token);
|
|
4816
|
+
rest = rest.slice(1);
|
|
4817
|
+
}
|
|
4818
|
+
if (rest.length === 0) return null;
|
|
4819
|
+
const file = loadable(cwd, rest[0]);
|
|
4820
|
+
return file === null ? null : { file, nodeArgs, appArgs: rest.slice(1) };
|
|
4821
|
+
}
|
|
4822
|
+
function resolveEntry(cwd) {
|
|
4823
|
+
const pkg = readPackage2(cwd);
|
|
4824
|
+
const scripts = pkg?.scripts ?? null;
|
|
4825
|
+
const declared = ["start", "serve"].map((name) => ({ name, script: scripts?.[name] })).find((entry) => typeof entry.script === "string" && entry.script.trim() !== "");
|
|
4826
|
+
if (declared !== void 0) {
|
|
4827
|
+
const resolved = fromScript(cwd, declared.script);
|
|
4828
|
+
return resolved === null ? {
|
|
4829
|
+
kind: "unavailable",
|
|
4830
|
+
reason: `the "${declared.name}" script is outside the bounded \`node <file>\` form this check can launch without the project's own toolchain`
|
|
4831
|
+
} : { kind: "entry", ...resolved, from: `the "${declared.name}" script` };
|
|
4832
|
+
}
|
|
4833
|
+
const main2 = pkg?.main;
|
|
4834
|
+
if (typeof main2 === "string") {
|
|
4835
|
+
const file = loadable(cwd, main2);
|
|
4836
|
+
if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: 'the package "main" field' };
|
|
4837
|
+
}
|
|
4838
|
+
for (const candidate of CONVENTIONAL) {
|
|
4839
|
+
const file = loadable(cwd, candidate);
|
|
4840
|
+
if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: candidate };
|
|
4841
|
+
}
|
|
4842
|
+
return {
|
|
4843
|
+
kind: "unavailable",
|
|
4844
|
+
reason: existsSync13(join13(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
|
|
4845
|
+
};
|
|
4846
|
+
}
|
|
4847
|
+
|
|
4848
|
+
// src/protect/install/runtime/probe.ts
|
|
4849
|
+
import { spawn } from "child_process";
|
|
4850
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
4851
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4852
|
+
import { request as httpRequest } from "http";
|
|
4853
|
+
import { request as httpsRequest, Agent as HttpsAgent } from "https";
|
|
4854
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4855
|
+
var CHALLENGE_BYTES = 32;
|
|
4856
|
+
var VERIFY_HEADER_NAME = "x-patchstack-verify";
|
|
4857
|
+
var CHALLENGE_ENV = "PATCHSTACK_VERIFY_CHALLENGE";
|
|
4858
|
+
var SETTLE_MS = 1500;
|
|
4859
|
+
var RUN_MS = 2e4;
|
|
4860
|
+
var PROBE_MS = 5e3;
|
|
4861
|
+
var FREEZE_MS = 500;
|
|
4862
|
+
var OUTPUT_LIMIT = 4096;
|
|
4863
|
+
var REDACTION_OVERLAP = 256;
|
|
4864
|
+
var ANSWER_LIMIT = 8192;
|
|
4865
|
+
var LOOPBACK = ["127.0.0.1", "::1"];
|
|
4866
|
+
var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
4867
|
+
function expectedAnswerFor(challenge) {
|
|
4868
|
+
return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
|
|
4869
|
+
}
|
|
4870
|
+
function preloadPath() {
|
|
4871
|
+
const candidates = [
|
|
4872
|
+
new URL("./protect/runtime/report-listeners.cjs", import.meta.url),
|
|
4873
|
+
// beside the built CLI
|
|
4874
|
+
new URL("./report-listeners.cjs", import.meta.url)
|
|
4875
|
+
// beside this file, in the repository
|
|
4876
|
+
];
|
|
4877
|
+
for (const candidate of candidates) {
|
|
4878
|
+
const path14 = fileURLToPath2(candidate);
|
|
4879
|
+
if (existsSync14(path14)) return path14;
|
|
4880
|
+
}
|
|
4881
|
+
return null;
|
|
4882
|
+
}
|
|
4883
|
+
function propagatedNodeOptions(preload, existing) {
|
|
4884
|
+
if (preload.includes('"')) {
|
|
4885
|
+
return {
|
|
4886
|
+
kind: "unavailable",
|
|
4887
|
+
reason: "the path to the listener reporter contains a double quote, which NODE_OPTIONS cannot carry \u2014 so a worker thread could not inherit the listener containment"
|
|
4888
|
+
};
|
|
4889
|
+
}
|
|
4890
|
+
const ours = `--require "${preload}"`;
|
|
4891
|
+
if (existing === void 0 || existing.trim() === "") return { kind: "options", value: ours };
|
|
4892
|
+
const verdict = classifyNodeOptions(existing);
|
|
4893
|
+
return verdict.kind === "refused" ? { kind: "unavailable", reason: verdict.why } : { kind: "options", value: `${ours} ${existing}` };
|
|
4894
|
+
}
|
|
4895
|
+
function keptOutput(chunks, challenge, answer) {
|
|
4896
|
+
const text = chunks.join("").split(challenge).join("<challenge>").split(answer).join("<answer>");
|
|
4897
|
+
return text.slice(0, OUTPUT_LIMIT);
|
|
4898
|
+
}
|
|
4899
|
+
async function askListener(listener, challenge, answer, budgetMs) {
|
|
4900
|
+
if (budgetMs <= 0) {
|
|
4901
|
+
return { ...listener, outcome: "unreachable", detail: "the run ran out of time before it could be asked" };
|
|
4902
|
+
}
|
|
4903
|
+
const send = listener.scheme === "https" ? httpsRequest : httpRequest;
|
|
4904
|
+
const options = {
|
|
4905
|
+
host: listener.host,
|
|
4906
|
+
port: listener.port,
|
|
4907
|
+
path: "/",
|
|
4908
|
+
method: "GET",
|
|
4909
|
+
headers: { [VERIFY_HEADER_NAME]: challenge },
|
|
4910
|
+
timeout: budgetMs
|
|
4911
|
+
};
|
|
4912
|
+
if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
|
|
4913
|
+
return new Promise((resolve4) => {
|
|
4914
|
+
const req = send(options, (res) => {
|
|
4915
|
+
const body = [];
|
|
4916
|
+
let read2 = 0;
|
|
4917
|
+
res.on("data", (chunk) => {
|
|
4918
|
+
read2 += chunk.length;
|
|
4919
|
+
if (read2 <= ANSWER_LIMIT) {
|
|
4920
|
+
body.push(chunk);
|
|
4921
|
+
return;
|
|
4922
|
+
}
|
|
4923
|
+
res.destroy();
|
|
4924
|
+
});
|
|
4925
|
+
const finish = () => {
|
|
4926
|
+
const text = Buffer.concat(body).toString("utf8").trim();
|
|
4927
|
+
resolve4({
|
|
4928
|
+
...listener,
|
|
4929
|
+
outcome: text === answer ? "traversed" : "answered-without-sentinel",
|
|
4930
|
+
detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
|
|
4931
|
+
});
|
|
4932
|
+
};
|
|
4933
|
+
res.on("end", finish);
|
|
4934
|
+
res.on("close", finish);
|
|
4935
|
+
});
|
|
4936
|
+
req.on("timeout", () => req.destroy(new Error("timed out")));
|
|
4937
|
+
req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
|
|
4938
|
+
req.end();
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
function verdictOf(listeners) {
|
|
4942
|
+
if (listeners.length === 0 || listeners.some((l) => l.outcome === "unreachable")) return "unavailable";
|
|
4943
|
+
if (listeners.some((l) => l.outcome === "answered-without-sentinel")) return "not-traversed";
|
|
4944
|
+
return "proven";
|
|
4945
|
+
}
|
|
4946
|
+
function terminator(child) {
|
|
4947
|
+
let done = false;
|
|
4948
|
+
return () => {
|
|
4949
|
+
if (done) return;
|
|
4950
|
+
done = true;
|
|
4951
|
+
try {
|
|
4952
|
+
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
4953
|
+
} catch {
|
|
4954
|
+
try {
|
|
4955
|
+
child.kill("SIGKILL");
|
|
4956
|
+
} catch {
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
};
|
|
4960
|
+
}
|
|
4961
|
+
async function probeRuntimeTraversal(options) {
|
|
4962
|
+
const platform = options.platform ?? process.platform;
|
|
4963
|
+
const challenge = randomBytes(CHALLENGE_BYTES).toString("hex");
|
|
4964
|
+
const answer = expectedAnswerFor(challenge);
|
|
4965
|
+
const nothing = (reason) => ({ outcome: "unavailable", reason, listeners: [], output: "" });
|
|
4966
|
+
if (platform === "win32") return nothing("process-tree cleanup is not implemented on Windows");
|
|
4967
|
+
const preload = options.preload ?? preloadPath();
|
|
4968
|
+
if (preload === null) return nothing("the listener reporter is missing from this installation");
|
|
4969
|
+
const deadline = Date.now() + (options.timeoutMs ?? RUN_MS);
|
|
4970
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
4971
|
+
const propagation = propagatedNodeOptions(preload, process.env.NODE_OPTIONS);
|
|
4972
|
+
if (propagation.kind === "unavailable") return nothing(propagation.reason);
|
|
4973
|
+
let detachMessages = () => {
|
|
4974
|
+
};
|
|
4975
|
+
const child = spawn(process.execPath, [...options.nodeArgs ?? [], "--require", preload, options.entry, ...options.appArgs ?? []], {
|
|
4976
|
+
cwd: options.cwd,
|
|
4977
|
+
detached: true,
|
|
4978
|
+
// its own process group, so cleanup has one stable target
|
|
4979
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
4980
|
+
// The app's own environment, plus the challenge and the propagated reporter. `NODE_ENV` is
|
|
4981
|
+
// deliberately left alone: choosing one here would change which code path the app takes, and the
|
|
4982
|
+
// point is to verify the app as it is.
|
|
4983
|
+
env: {
|
|
4984
|
+
...process.env,
|
|
4985
|
+
[CHALLENGE_ENV]: challenge,
|
|
4986
|
+
// Replaced rather than added to: what may be inherited was decided above, and the reporter is
|
|
4987
|
+
// first in the value that goes out.
|
|
4988
|
+
NODE_OPTIONS: propagation.value
|
|
4989
|
+
}
|
|
4990
|
+
});
|
|
4991
|
+
const terminate = terminator(child);
|
|
4992
|
+
const onSignal = (signal) => {
|
|
4993
|
+
terminate();
|
|
4994
|
+
detachHandlers();
|
|
4995
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
4996
|
+
};
|
|
4997
|
+
const onExit = () => terminate();
|
|
4998
|
+
const detachHandlers = () => {
|
|
4999
|
+
for (const signal of ENDING_SIGNALS) process.removeListener(signal, onSignal);
|
|
5000
|
+
process.removeListener("exit", onExit);
|
|
5001
|
+
};
|
|
5002
|
+
for (const signal of ENDING_SIGNALS) process.prependListener(signal, onSignal);
|
|
5003
|
+
process.on("exit", onExit);
|
|
5004
|
+
try {
|
|
5005
|
+
const chunks = [];
|
|
5006
|
+
let kept = 0;
|
|
5007
|
+
const drain = (stream) => {
|
|
5008
|
+
stream?.setEncoding("utf8");
|
|
5009
|
+
stream?.on("data", (chunk) => {
|
|
5010
|
+
if (kept < OUTPUT_LIMIT + REDACTION_OVERLAP) {
|
|
5011
|
+
chunks.push(chunk);
|
|
5012
|
+
kept += chunk.length;
|
|
5013
|
+
}
|
|
5014
|
+
});
|
|
5015
|
+
};
|
|
5016
|
+
drain(child.stdout);
|
|
5017
|
+
drain(child.stderr);
|
|
5018
|
+
const observed = [];
|
|
5019
|
+
const unsupported = [];
|
|
5020
|
+
const created = [];
|
|
5021
|
+
const workers = [];
|
|
5022
|
+
const late = [];
|
|
5023
|
+
const escaped = [];
|
|
5024
|
+
let loaded = false;
|
|
5025
|
+
let startFailure = null;
|
|
5026
|
+
let exitHow = null;
|
|
5027
|
+
let discovering = true;
|
|
5028
|
+
let onFrozen = null;
|
|
5029
|
+
const freeze = async () => {
|
|
5030
|
+
if (!child.connected) return false;
|
|
5031
|
+
return new Promise((resolve4) => {
|
|
5032
|
+
const settle2 = (confirmed2) => {
|
|
5033
|
+
clearTimeout(timer);
|
|
5034
|
+
onFrozen = null;
|
|
5035
|
+
child.removeListener("exit", onGone);
|
|
5036
|
+
resolve4(confirmed2);
|
|
5037
|
+
};
|
|
5038
|
+
const onGone = () => settle2(false);
|
|
5039
|
+
const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
|
|
5040
|
+
onFrozen = () => settle2(true);
|
|
5041
|
+
child.once("exit", onGone);
|
|
5042
|
+
try {
|
|
5043
|
+
child.send({ patchstackVerify: "freeze" });
|
|
5044
|
+
} catch {
|
|
5045
|
+
settle2(false);
|
|
5046
|
+
}
|
|
5047
|
+
});
|
|
5048
|
+
};
|
|
5049
|
+
await new Promise((resolve4) => {
|
|
5050
|
+
let settleTimer = null;
|
|
5051
|
+
const done = () => {
|
|
5052
|
+
discovering = false;
|
|
5053
|
+
clearTimeout(runTimer);
|
|
5054
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
5055
|
+
resolve4();
|
|
5056
|
+
};
|
|
5057
|
+
const runTimer = setTimeout(done, remaining());
|
|
5058
|
+
const settle2 = () => {
|
|
5059
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
5060
|
+
settleTimer = setTimeout(done, Math.min(options.settleMs ?? SETTLE_MS, remaining()));
|
|
5061
|
+
};
|
|
5062
|
+
const onMessage = (message) => {
|
|
5063
|
+
const note = message;
|
|
5064
|
+
if (note?.patchstackVerify === "frozen") {
|
|
5065
|
+
onFrozen?.();
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
if (note?.patchstackVerify === "process-created" && typeof note.escape === "string") escaped.push(`${note.what ?? "a process"} ${note.escape}`);
|
|
5069
|
+
if (note?.patchstackVerify === "worker-created" && typeof note.escape === "string") escaped.push(`a worker thread that ${note.escape}`);
|
|
5070
|
+
if (!discovering) {
|
|
5071
|
+
const kind = note?.patchstackVerify;
|
|
5072
|
+
if (kind === "listener") late.push(`an ${note.scheme === "https" ? "HTTPS" : "HTTP"} listener on port ${Number(note.port)}`);
|
|
5073
|
+
else if (kind === "unsupported-listener") late.push(`a listener this cannot probe (${note.why ?? "an unsupported transport"})`);
|
|
5074
|
+
else if (kind === "process-created") late.push(`another process (${note.what ?? "a process"} via ${note.via ?? "child_process"})`);
|
|
5075
|
+
else if (kind === "worker-created") late.push(`a worker thread (${note.what ?? "a worker"})`);
|
|
5076
|
+
return;
|
|
5077
|
+
}
|
|
5078
|
+
if (note?.patchstackVerify === "listener" && (note.scheme === "http" || note.scheme === "https")) {
|
|
5079
|
+
observed.push({ scheme: note.scheme, host: String(note.host), port: Number(note.port) });
|
|
5080
|
+
settle2();
|
|
5081
|
+
}
|
|
5082
|
+
if (note?.patchstackVerify === "unsupported-listener") {
|
|
5083
|
+
unsupported.push(note.why ?? "an unsupported transport");
|
|
5084
|
+
done();
|
|
5085
|
+
}
|
|
5086
|
+
if (note?.patchstackVerify === "process-created") {
|
|
5087
|
+
created.push(
|
|
5088
|
+
`${note.what ?? "a process"} (via ${note.via ?? "child_process"})${note.node === true ? "" : ", which the listener reporter cannot reach"}`
|
|
5089
|
+
);
|
|
5090
|
+
done();
|
|
5091
|
+
}
|
|
5092
|
+
if (note?.patchstackVerify === "worker-created") {
|
|
5093
|
+
workers.push(note.what ?? "a worker");
|
|
5094
|
+
done();
|
|
5095
|
+
}
|
|
5096
|
+
if (note?.patchstackVerify === "ready") loaded = true;
|
|
5097
|
+
};
|
|
5098
|
+
child.on("message", onMessage);
|
|
5099
|
+
detachMessages = () => child.removeListener("message", onMessage);
|
|
5100
|
+
child.on("error", (err) => {
|
|
5101
|
+
startFailure = `the app could not be started (${err.message})`;
|
|
5102
|
+
done();
|
|
5103
|
+
});
|
|
5104
|
+
child.on("exit", (code, signal) => {
|
|
5105
|
+
exitHow = signal ?? `code ${code}`;
|
|
5106
|
+
done();
|
|
5107
|
+
});
|
|
5108
|
+
});
|
|
5109
|
+
const offLoopback = observed.filter((l) => !LOOPBACK.includes(l.host));
|
|
5110
|
+
const contained = unsupported.length === 0 && created.length === 0 && workers.length === 0 && offLoopback.length === 0;
|
|
5111
|
+
const listeners = contained ? await Promise.all(observed.map((l) => askListener(l, challenge, answer, Math.min(PROBE_MS, remaining())))) : [];
|
|
5112
|
+
const confirmed = await freeze();
|
|
5113
|
+
const output = keptOutput(chunks, challenge, answer);
|
|
5114
|
+
const pid = child.pid;
|
|
5115
|
+
const exitNote = startFailure ?? (exitHow === null ? null : observed.length === 0 ? `the app exited before it listened (${exitHow})` : `the app exited while it was being probed (${exitHow})`);
|
|
5116
|
+
if (escaped.length > 0) {
|
|
5117
|
+
return {
|
|
5118
|
+
outcome: "unavailable",
|
|
5119
|
+
reason: `the app tried to start something outside this run's one-process scope, and it was refused: ${escaped.join("; ")}`,
|
|
5120
|
+
listeners: [],
|
|
5121
|
+
output,
|
|
5122
|
+
pid
|
|
5123
|
+
};
|
|
5124
|
+
}
|
|
5125
|
+
if (unsupported.length > 0) {
|
|
5126
|
+
return { outcome: "unavailable", reason: `a listener this cannot probe: ${unsupported.join("; ")}`, listeners: [], output, pid };
|
|
5127
|
+
}
|
|
5128
|
+
if (created.length > 0) {
|
|
5129
|
+
return {
|
|
5130
|
+
outcome: "unavailable",
|
|
5131
|
+
reason: `the app attempted to start another process, so this run cannot answer for the whole app: ${created.join("; ")}`,
|
|
5132
|
+
listeners: [],
|
|
5133
|
+
output,
|
|
5134
|
+
pid
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
if (workers.length > 0) {
|
|
5138
|
+
return {
|
|
5139
|
+
outcome: "unavailable",
|
|
5140
|
+
reason: `the app started a worker thread, whose listeners cannot report back to this run: ${workers.join("; ")}`,
|
|
5141
|
+
listeners: [],
|
|
5142
|
+
output,
|
|
5143
|
+
pid
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
if (offLoopback.length > 0) {
|
|
5147
|
+
return {
|
|
5148
|
+
outcome: "unavailable",
|
|
5149
|
+
reason: `a listener bound an address this verification must not open: ${offLoopback.map((l) => l.host).join("; ")}`,
|
|
5150
|
+
listeners: [],
|
|
5151
|
+
output,
|
|
5152
|
+
pid
|
|
5153
|
+
};
|
|
5154
|
+
}
|
|
5155
|
+
if (late.length > 0) {
|
|
5156
|
+
return {
|
|
5157
|
+
outcome: "unavailable",
|
|
5158
|
+
reason: `the app was still opening things after discovery had ended, so this run cannot answer for the whole app: ${late.join("; ")}`,
|
|
5159
|
+
listeners: [],
|
|
5160
|
+
output,
|
|
5161
|
+
pid
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
if (observed.length === 0) {
|
|
5165
|
+
if (exitNote !== null) return { outcome: "unavailable", reason: exitNote, listeners: [], output, pid };
|
|
5166
|
+
if (!loaded) {
|
|
5167
|
+
return { outcome: "unavailable", reason: "the listener reporter did not load in the app process", listeners: [], output, pid };
|
|
5168
|
+
}
|
|
5169
|
+
return { outcome: "unavailable", reason: "no HTTP listener was observed while starting up", listeners: [], output, pid };
|
|
5170
|
+
}
|
|
5171
|
+
const unconfirmed = !confirmed && loaded && exitHow === null;
|
|
5172
|
+
const outcome = unconfirmed && verdictOf(listeners) === "proven" ? "unavailable" : verdictOf(listeners);
|
|
5173
|
+
const reason = outcome === "unavailable" ? exitNote ?? (unconfirmed ? "the app never confirmed it had stopped opening listeners, so one it opened last cannot be ruled out" : listeners.find((l) => l.outcome === "unreachable")?.detail ?? "a listener could not be reached") : void 0;
|
|
5174
|
+
return { outcome, reason, listeners, output, pid };
|
|
5175
|
+
} finally {
|
|
5176
|
+
terminate();
|
|
5177
|
+
detachHandlers();
|
|
5178
|
+
detachMessages();
|
|
5179
|
+
}
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
// src/protect/install/runtime/check.ts
|
|
5183
|
+
async function runRuntimeCheck(cwd, options = {}) {
|
|
5184
|
+
const entry = resolveEntry(cwd);
|
|
5185
|
+
if (entry.kind === "unavailable") return { outcome: "unavailable", reason: entry.reason, listeners: [], output: "" };
|
|
5186
|
+
const result = await probeRuntimeTraversal({
|
|
5187
|
+
cwd,
|
|
5188
|
+
entry: entry.file,
|
|
5189
|
+
nodeArgs: entry.nodeArgs,
|
|
5190
|
+
appArgs: entry.appArgs,
|
|
5191
|
+
timeoutMs: options.timeoutMs,
|
|
5192
|
+
settleMs: options.settleMs,
|
|
5193
|
+
platform: options.platform
|
|
5194
|
+
});
|
|
5195
|
+
return { ...result, entry: { file: relative2(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
|
|
5196
|
+
}
|
|
5197
|
+
function runtimeExitCode(report) {
|
|
5198
|
+
if (report.outcome === "proven") return 0;
|
|
5199
|
+
return report.outcome === "not-traversed" ? 1 : 2;
|
|
5200
|
+
}
|
|
5201
|
+
function formatRuntimeCheck(report) {
|
|
5202
|
+
const lines = ["runtime traversal (--runtime):"];
|
|
5203
|
+
if (report.entry) lines.push(` entry: ${report.entry.file} (from ${report.entry.from})`);
|
|
5204
|
+
for (const listener of report.listeners) {
|
|
5205
|
+
const mark = listener.outcome === "traversed" ? "\u2713" : listener.outcome === "answered-without-sentinel" ? "\u2717" : "?";
|
|
5206
|
+
const note = listener.outcome === "traversed" ? "runtime traversal reached the scaffolded guard seam" : listener.outcome === "answered-without-sentinel" ? "the listener answered and the guard seam did not" : listener.detail ?? "could not be reached";
|
|
5207
|
+
const authority = listener.host.includes(":") ? `[${listener.host}]` : listener.host;
|
|
5208
|
+
lines.push(` ${mark} ${listener.scheme}://${authority}:${listener.port} \u2014 ${note}`);
|
|
5209
|
+
}
|
|
5210
|
+
if (report.outcome === "proven") {
|
|
5211
|
+
lines.push("runtime traversal reached the scaffolded guard seam \u2713");
|
|
5212
|
+
} else if (report.outcome === "not-traversed") {
|
|
5213
|
+
lines.push("runtime traversal did NOT reach the scaffolded guard seam \u2717");
|
|
5214
|
+
} else {
|
|
5215
|
+
lines.push(`? runtime traversal could not be established \u2014 ${report.reason ?? "no reason given"}`);
|
|
5216
|
+
lines.push(" This is not a failure. The structural checks above still stand on their own.");
|
|
5217
|
+
}
|
|
5218
|
+
if (report.outcome !== "proven" && report.output.trim() !== "") {
|
|
5219
|
+
lines.push(" the app printed:");
|
|
5220
|
+
for (const line of report.output.trim().split("\n").slice(0, 20)) lines.push(` ${line}`);
|
|
5221
|
+
}
|
|
5222
|
+
return lines;
|
|
5223
|
+
}
|
|
5224
|
+
|
|
4533
5225
|
// src/map-command.ts
|
|
4534
|
-
import { writeFileSync as
|
|
5226
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
4535
5227
|
|
|
4536
5228
|
// src/map/ts-loader.ts
|
|
4537
5229
|
import { createRequire } from "module";
|
|
4538
5230
|
import { pathToFileURL } from "url";
|
|
4539
|
-
import { join as
|
|
5231
|
+
import { join as join14 } from "path";
|
|
4540
5232
|
async function loadTypeScript(cwd) {
|
|
4541
5233
|
try {
|
|
4542
|
-
const req = createRequire(pathToFileURL(
|
|
5234
|
+
const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
|
|
4543
5235
|
const resolved = req.resolve("typescript");
|
|
4544
5236
|
const mod = await import(pathToFileURL(resolved).href);
|
|
4545
5237
|
return mod.default ?? mod;
|
|
@@ -4554,9 +5246,9 @@ async function loadTypeScript(cwd) {
|
|
|
4554
5246
|
}
|
|
4555
5247
|
|
|
4556
5248
|
// src/map/extract.ts
|
|
4557
|
-
import { readFileSync as
|
|
4558
|
-
import { createHash as
|
|
4559
|
-
import { relative as
|
|
5249
|
+
import { readFileSync as readFileSync13, realpathSync as realpathSync4 } from "fs";
|
|
5250
|
+
import { createHash as createHash5 } from "crypto";
|
|
5251
|
+
import { relative as relative5 } from "path";
|
|
4560
5252
|
|
|
4561
5253
|
// src/map/ast.ts
|
|
4562
5254
|
function rootIdentifier(node, ts) {
|
|
@@ -4810,8 +5502,8 @@ function npmPackageOf(spec) {
|
|
|
4810
5502
|
}
|
|
4811
5503
|
|
|
4812
5504
|
// src/map/sources.ts
|
|
4813
|
-
import { readFileSync as
|
|
4814
|
-
import { join as
|
|
5505
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
5506
|
+
import { join as join15, relative as relative3, isAbsolute } from "path";
|
|
4815
5507
|
|
|
4816
5508
|
// src/map/routes.ts
|
|
4817
5509
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -4895,7 +5587,7 @@ function hasEntrySignal(text) {
|
|
|
4895
5587
|
}
|
|
4896
5588
|
function detectFramework(cwd) {
|
|
4897
5589
|
try {
|
|
4898
|
-
const pkg = JSON.parse(
|
|
5590
|
+
const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
|
|
4899
5591
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4900
5592
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
4901
5593
|
if (d["next"]) return "next";
|
|
@@ -4907,11 +5599,11 @@ function detectFramework(cwd) {
|
|
|
4907
5599
|
} catch {
|
|
4908
5600
|
}
|
|
4909
5601
|
try {
|
|
4910
|
-
if (
|
|
5602
|
+
if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
4911
5603
|
} catch {
|
|
4912
5604
|
}
|
|
4913
5605
|
try {
|
|
4914
|
-
if (
|
|
5606
|
+
if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
4915
5607
|
} catch {
|
|
4916
5608
|
}
|
|
4917
5609
|
return "unknown";
|
|
@@ -4953,7 +5645,7 @@ function probeForSource(dir, budget) {
|
|
|
4953
5645
|
for (const e of entries) {
|
|
4954
5646
|
if (budget.left-- <= 0) return "unknown";
|
|
4955
5647
|
if (e.isDirectory()) {
|
|
4956
|
-
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(
|
|
5648
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join15(dir, e.name));
|
|
4957
5649
|
} else if (isSourceFile(e.name)) return true;
|
|
4958
5650
|
else if (e.isSymbolicLink()) return "unknown";
|
|
4959
5651
|
}
|
|
@@ -4966,7 +5658,7 @@ function probeForSource(dir, budget) {
|
|
|
4966
5658
|
function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
|
|
4967
5659
|
let key;
|
|
4968
5660
|
try {
|
|
4969
|
-
key =
|
|
5661
|
+
key = realpathSync2(dir);
|
|
4970
5662
|
} catch {
|
|
4971
5663
|
stats.unwalked++;
|
|
4972
5664
|
return out;
|
|
@@ -4990,19 +5682,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4990
5682
|
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4991
5683
|
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4992
5684
|
else if (probeable && e.isDirectory()) {
|
|
4993
|
-
const held = probeForSource(
|
|
4994
|
-
if (held === true) stats.skippedWithSource.push(
|
|
5685
|
+
const held = probeForSource(join15(dir, e.name), probeBudget);
|
|
5686
|
+
if (held === true) stats.skippedWithSource.push(join15(dir, e.name));
|
|
4995
5687
|
else if (held === "unknown") stats.skippedUnknown++;
|
|
4996
5688
|
}
|
|
4997
5689
|
continue;
|
|
4998
5690
|
}
|
|
4999
|
-
const full =
|
|
5691
|
+
const full = join15(dir, e.name);
|
|
5000
5692
|
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
5001
5693
|
else if (e.isSymbolicLink()) {
|
|
5002
5694
|
let st, real;
|
|
5003
5695
|
try {
|
|
5004
|
-
st =
|
|
5005
|
-
real =
|
|
5696
|
+
st = statSync4(full);
|
|
5697
|
+
real = realpathSync2(full);
|
|
5006
5698
|
} catch {
|
|
5007
5699
|
stats.unwalked++;
|
|
5008
5700
|
continue;
|
|
@@ -5025,7 +5717,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
5025
5717
|
}
|
|
5026
5718
|
function isInside(candidate, boundary) {
|
|
5027
5719
|
if (candidate === boundary) return true;
|
|
5028
|
-
const rel =
|
|
5720
|
+
const rel = relative3(boundary, candidate);
|
|
5029
5721
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
5030
5722
|
}
|
|
5031
5723
|
var DEPLOYMENT_SHAPES = [
|
|
@@ -5052,13 +5744,13 @@ var DEPLOYMENT_SHAPES = [
|
|
|
5052
5744
|
function detectDeploymentShapes(cwd, opts = {}) {
|
|
5053
5745
|
let boundary = opts.boundary ?? cwd;
|
|
5054
5746
|
try {
|
|
5055
|
-
boundary =
|
|
5747
|
+
boundary = realpathSync2(boundary);
|
|
5056
5748
|
} catch {
|
|
5057
5749
|
}
|
|
5058
|
-
const inProject = (
|
|
5750
|
+
const inProject = (path14) => {
|
|
5059
5751
|
if (opts.followOutside) return true;
|
|
5060
5752
|
try {
|
|
5061
|
-
return isInside(
|
|
5753
|
+
return isInside(realpathSync2(path14), boundary);
|
|
5062
5754
|
} catch {
|
|
5063
5755
|
return false;
|
|
5064
5756
|
}
|
|
@@ -5066,9 +5758,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5066
5758
|
const found = [];
|
|
5067
5759
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
5068
5760
|
for (const file of candidate.files ?? []) {
|
|
5069
|
-
const full =
|
|
5761
|
+
const full = join15(cwd, file);
|
|
5070
5762
|
try {
|
|
5071
|
-
if (
|
|
5763
|
+
if (statSync4(full).isFile() && inProject(full)) {
|
|
5072
5764
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
5073
5765
|
break;
|
|
5074
5766
|
}
|
|
@@ -5076,9 +5768,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5076
5768
|
}
|
|
5077
5769
|
}
|
|
5078
5770
|
for (const dir of candidate.dirs ?? []) {
|
|
5079
|
-
const full =
|
|
5771
|
+
const full = join15(cwd, dir);
|
|
5080
5772
|
try {
|
|
5081
|
-
if (!
|
|
5773
|
+
if (!statSync4(full).isDirectory() || !inProject(full)) continue;
|
|
5082
5774
|
} catch {
|
|
5083
5775
|
continue;
|
|
5084
5776
|
}
|
|
@@ -5095,7 +5787,7 @@ function holdsFunctionEntry(dir) {
|
|
|
5095
5787
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5096
5788
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
5097
5789
|
try {
|
|
5098
|
-
for (const nested of readdirSync5(
|
|
5790
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5099
5791
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
5100
5792
|
}
|
|
5101
5793
|
} catch {
|
|
@@ -5111,7 +5803,7 @@ function holdsSourceFile(dir) {
|
|
|
5111
5803
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
5112
5804
|
if (entry.isDirectory()) {
|
|
5113
5805
|
try {
|
|
5114
|
-
for (const nested of readdirSync5(
|
|
5806
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5115
5807
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
5116
5808
|
}
|
|
5117
5809
|
} catch {
|
|
@@ -5124,8 +5816,8 @@ function holdsSourceFile(dir) {
|
|
|
5124
5816
|
}
|
|
5125
5817
|
|
|
5126
5818
|
// src/map/surface.ts
|
|
5127
|
-
import { readFileSync as
|
|
5128
|
-
import { join as
|
|
5819
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
5820
|
+
import { join as join16 } from "path";
|
|
5129
5821
|
var STATIC_GENERATORS = [
|
|
5130
5822
|
{ dep: "vite", label: "vite" },
|
|
5131
5823
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -5163,7 +5855,7 @@ var SHAPE_SIGNAL = {
|
|
|
5163
5855
|
};
|
|
5164
5856
|
function readManifest(cwd) {
|
|
5165
5857
|
try {
|
|
5166
|
-
const parsed = JSON.parse(
|
|
5858
|
+
const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
|
|
5167
5859
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
5168
5860
|
} catch {
|
|
5169
5861
|
return null;
|
|
@@ -5176,7 +5868,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
5176
5868
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
5177
5869
|
let text;
|
|
5178
5870
|
try {
|
|
5179
|
-
text =
|
|
5871
|
+
text = readFileSync10(join16(cwd, file), "utf8");
|
|
5180
5872
|
} catch {
|
|
5181
5873
|
continue;
|
|
5182
5874
|
}
|
|
@@ -5304,7 +5996,7 @@ function surfaceNote(surface) {
|
|
|
5304
5996
|
}
|
|
5305
5997
|
|
|
5306
5998
|
// src/map/sinks.ts
|
|
5307
|
-
import { createHash as
|
|
5999
|
+
import { createHash as createHash4 } from "crypto";
|
|
5308
6000
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
5309
6001
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
5310
6002
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -5403,14 +6095,14 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
5403
6095
|
const root = base ? rootIdentifier(base, ts) : void 0;
|
|
5404
6096
|
if (!root) return {};
|
|
5405
6097
|
const spec = bindings.resolve(root);
|
|
5406
|
-
const
|
|
6098
|
+
const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
|
|
5407
6099
|
const pkg = npmPackageOf(spec);
|
|
5408
6100
|
if (pkg) return { pkg, root, spec };
|
|
5409
|
-
if (
|
|
6101
|
+
if (relative7 && ctx && spec) {
|
|
5410
6102
|
const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
|
|
5411
6103
|
if (viaModule) return { pkg: viaModule.package, root, spec };
|
|
5412
6104
|
}
|
|
5413
|
-
return { local: bindings.locals.has(root), root, spec, relative:
|
|
6105
|
+
return { local: bindings.locals.has(root), root, spec, relative: relative7 };
|
|
5414
6106
|
};
|
|
5415
6107
|
const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
|
|
5416
6108
|
const dbApi = (pkg, attribution) => {
|
|
@@ -5571,7 +6263,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
|
5571
6263
|
return table?.[index] ?? "unknown";
|
|
5572
6264
|
}
|
|
5573
6265
|
function sinkId(s, owner) {
|
|
5574
|
-
return
|
|
6266
|
+
return createHash4("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
|
|
5575
6267
|
}
|
|
5576
6268
|
function dedupeSinks(sinks, owner) {
|
|
5577
6269
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5587,8 +6279,8 @@ function dedupeSinks(sinks, owner) {
|
|
|
5587
6279
|
}
|
|
5588
6280
|
|
|
5589
6281
|
// src/map/module-graph.ts
|
|
5590
|
-
import { readFileSync as
|
|
5591
|
-
import { dirname as dirname7, join as
|
|
6282
|
+
import { readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
|
|
6283
|
+
import { dirname as dirname7, join as join17, relative as relative4, resolve as resolvePath } from "path";
|
|
5592
6284
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
5593
6285
|
function createModuleGraph(ts, opts) {
|
|
5594
6286
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5596,7 +6288,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5596
6288
|
if (cache.has(file)) return cache.get(file) ?? null;
|
|
5597
6289
|
let entry = null;
|
|
5598
6290
|
try {
|
|
5599
|
-
const text =
|
|
6291
|
+
const text = readFileSync11(file, "utf8");
|
|
5600
6292
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
5601
6293
|
const bindings = buildModuleBindings(sf, ts);
|
|
5602
6294
|
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
@@ -5612,7 +6304,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5612
6304
|
if (!opts.followOutside) {
|
|
5613
6305
|
let real = target;
|
|
5614
6306
|
try {
|
|
5615
|
-
real =
|
|
6307
|
+
real = realpathSync3(target);
|
|
5616
6308
|
} catch {
|
|
5617
6309
|
}
|
|
5618
6310
|
if (!isInside(real, opts.boundary)) return void 0;
|
|
@@ -5630,7 +6322,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5630
6322
|
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
5631
6323
|
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
5632
6324
|
}
|
|
5633
|
-
const rel =
|
|
6325
|
+
const rel = relative4(opts.cwd, target);
|
|
5634
6326
|
return collected.map((s) => ({ ...s, file: rel }));
|
|
5635
6327
|
},
|
|
5636
6328
|
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
@@ -5666,10 +6358,10 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
5666
6358
|
}
|
|
5667
6359
|
candidates.push(base);
|
|
5668
6360
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
5669
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
6361
|
+
for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
|
|
5670
6362
|
for (const c of candidates) {
|
|
5671
6363
|
try {
|
|
5672
|
-
if (
|
|
6364
|
+
if (statSync5(c).isFile()) return c;
|
|
5673
6365
|
} catch {
|
|
5674
6366
|
}
|
|
5675
6367
|
}
|
|
@@ -5695,8 +6387,8 @@ function collectCallees(sf, ts) {
|
|
|
5695
6387
|
}
|
|
5696
6388
|
|
|
5697
6389
|
// src/map/coordinates.ts
|
|
5698
|
-
function runtimeCoordinate(source,
|
|
5699
|
-
if (/\[\d*\]/.test(
|
|
6390
|
+
function runtimeCoordinate(source, path14) {
|
|
6391
|
+
if (/\[\d*\]/.test(path14)) {
|
|
5700
6392
|
return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
|
|
5701
6393
|
}
|
|
5702
6394
|
switch (source) {
|
|
@@ -5705,15 +6397,15 @@ function runtimeCoordinate(source, path13) {
|
|
|
5705
6397
|
case "multipart":
|
|
5706
6398
|
case "body":
|
|
5707
6399
|
case "server-fn-data":
|
|
5708
|
-
return { runtimeParameter: `post.${
|
|
6400
|
+
return { runtimeParameter: `post.${path14}` };
|
|
5709
6401
|
case "query":
|
|
5710
|
-
return { runtimeParameter: `get.${
|
|
6402
|
+
return { runtimeParameter: `get.${path14}` };
|
|
5711
6403
|
case "cookie":
|
|
5712
|
-
return { runtimeParameter: `cookie.${
|
|
6404
|
+
return { runtimeParameter: `cookie.${path14}` };
|
|
5713
6405
|
case "file":
|
|
5714
|
-
return { runtimeParameter: `files.${
|
|
6406
|
+
return { runtimeParameter: `files.${path14}` };
|
|
5715
6407
|
case "header":
|
|
5716
|
-
return { runtimeParameter: `server.HTTP_${
|
|
6408
|
+
return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
|
|
5717
6409
|
case "route-param":
|
|
5718
6410
|
return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
|
|
5719
6411
|
default:
|
|
@@ -5745,8 +6437,8 @@ function addressSpaceOf(source) {
|
|
|
5745
6437
|
function isProvenFlow(confidence) {
|
|
5746
6438
|
return confidence === "exact-local" || confidence === "transformed-local";
|
|
5747
6439
|
}
|
|
5748
|
-
function inputIdOf(source,
|
|
5749
|
-
return `${addressSpaceOf(source)}:${
|
|
6440
|
+
function inputIdOf(source, path14) {
|
|
6441
|
+
return `${addressSpaceOf(source)}:${path14}`;
|
|
5750
6442
|
}
|
|
5751
6443
|
function withCoordinates(fields, source) {
|
|
5752
6444
|
return fields.map((f) => {
|
|
@@ -5956,8 +6648,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
|
|
|
5956
6648
|
if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
|
|
5957
6649
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
5958
6650
|
const rootPath = /* @__PURE__ */ new Map();
|
|
5959
|
-
const addRoot = (name,
|
|
5960
|
-
if (!rootPath.has(name)) rootPath.set(name, { path:
|
|
6651
|
+
const addRoot = (name, path14, space) => {
|
|
6652
|
+
if (!rootPath.has(name)) rootPath.set(name, { path: path14, space });
|
|
5961
6653
|
};
|
|
5962
6654
|
for (const p of params ?? []) {
|
|
5963
6655
|
if (!p?.name) continue;
|
|
@@ -6102,8 +6794,8 @@ function dedupeLimitations(list) {
|
|
|
6102
6794
|
function join22(base, seg) {
|
|
6103
6795
|
return base ? `${base}.${seg}` : seg;
|
|
6104
6796
|
}
|
|
6105
|
-
function normalizePath(
|
|
6106
|
-
return
|
|
6797
|
+
function normalizePath(path14) {
|
|
6798
|
+
return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
|
|
6107
6799
|
}
|
|
6108
6800
|
function fluentChainCalls(call, ts) {
|
|
6109
6801
|
let root = call;
|
|
@@ -6349,8 +7041,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
6349
7041
|
}
|
|
6350
7042
|
|
|
6351
7043
|
// src/map/imports.ts
|
|
6352
|
-
import { readFileSync as
|
|
6353
|
-
import { join as
|
|
7044
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7045
|
+
import { join as join18 } from "path";
|
|
6354
7046
|
var MAX_SITES = 5;
|
|
6355
7047
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
6356
7048
|
function isNpmPackageName(pkg) {
|
|
@@ -6361,7 +7053,7 @@ function readPathAliases(cwd) {
|
|
|
6361
7053
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
6362
7054
|
let paths;
|
|
6363
7055
|
try {
|
|
6364
|
-
const raw =
|
|
7056
|
+
const raw = readFileSync12(join18(cwd, name), "utf8");
|
|
6365
7057
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
6366
7058
|
paths = parsed.compilerOptions?.paths;
|
|
6367
7059
|
} catch {
|
|
@@ -6721,7 +7413,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6721
7413
|
const failed = [];
|
|
6722
7414
|
let boundary = cwd;
|
|
6723
7415
|
try {
|
|
6724
|
-
boundary =
|
|
7416
|
+
boundary = realpathSync4(cwd);
|
|
6725
7417
|
} catch {
|
|
6726
7418
|
}
|
|
6727
7419
|
const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
|
|
@@ -6740,8 +7432,8 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6740
7432
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
6741
7433
|
for (const file of files) {
|
|
6742
7434
|
try {
|
|
6743
|
-
const text =
|
|
6744
|
-
const relFile =
|
|
7435
|
+
const text = readFileSync13(file, "utf8");
|
|
7436
|
+
const relFile = relative5(cwd, file);
|
|
6745
7437
|
sourceBytes += text.length;
|
|
6746
7438
|
if (!hasEntrySignal(text)) {
|
|
6747
7439
|
preFiltered++;
|
|
@@ -6753,7 +7445,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6753
7445
|
}
|
|
6754
7446
|
parsed++;
|
|
6755
7447
|
entryFiles.add(file);
|
|
6756
|
-
const fingerprint =
|
|
7448
|
+
const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
|
|
6757
7449
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
6758
7450
|
const bindings = buildModuleBindings(sf, ts);
|
|
6759
7451
|
for (const specifier of bindings.imports) {
|
|
@@ -6787,7 +7479,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6787
7479
|
}
|
|
6788
7480
|
} catch (e) {
|
|
6789
7481
|
if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
|
|
6790
|
-
failed.push(
|
|
7482
|
+
failed.push(relative5(cwd, file));
|
|
6791
7483
|
}
|
|
6792
7484
|
}
|
|
6793
7485
|
let hopParsed = 0;
|
|
@@ -6795,10 +7487,10 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6795
7487
|
for (const target of hopTargets) {
|
|
6796
7488
|
if (entryFiles.has(target)) continue;
|
|
6797
7489
|
try {
|
|
6798
|
-
const text =
|
|
7490
|
+
const text = readFileSync13(target, "utf8");
|
|
6799
7491
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
6800
7492
|
const bindings = buildModuleBindings(sf, ts);
|
|
6801
|
-
const called = collectInvocations(sf, ts, bindings, { file: target, owner:
|
|
7493
|
+
const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
|
|
6802
7494
|
invocations.add(called.invocations);
|
|
6803
7495
|
calls.total += called.counts.total;
|
|
6804
7496
|
calls.dependency += called.counts.dependency;
|
|
@@ -6830,7 +7522,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6830
7522
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
6831
7523
|
const importList = imports.list();
|
|
6832
7524
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6833
|
-
const skippedDirsWithSource = stats.skippedWithSource.map((d) =>
|
|
7525
|
+
const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
|
|
6834
7526
|
const importCoverageGaps = {
|
|
6835
7527
|
unreadableFiles: failed.length,
|
|
6836
7528
|
unscannableFiles: importScanFailures,
|
|
@@ -6943,6 +7635,243 @@ function getStringFlag(flags, name) {
|
|
|
6943
7635
|
return typeof value === "string" ? value : void 0;
|
|
6944
7636
|
}
|
|
6945
7637
|
|
|
7638
|
+
// src/build-stamp.ts
|
|
7639
|
+
import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync5, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
7640
|
+
import { join as join19, relative as relative6, sep as sep2 } from "path";
|
|
7641
|
+
var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
|
|
7642
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
|
|
7643
|
+
var MAX_DEPTH2 = 6;
|
|
7644
|
+
var GUARD_SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
7645
|
+
var MAX_GUARD_SOURCE_BYTES = 1024 * 1024;
|
|
7646
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7647
|
+
function guardImportsRulesFile(dir, rulesName) {
|
|
7648
|
+
const quotedRules = `["']\\./${escapeRegExp(rulesName)}["']`;
|
|
7649
|
+
const importsRules = new RegExp(`(?:\\bfrom\\s*|\\brequire\\s*\\(\\s*|\\bnew\\s+URL\\s*\\(\\s*)${quotedRules}`);
|
|
7650
|
+
let siblings;
|
|
7651
|
+
try {
|
|
7652
|
+
siblings = readdirSync6(dir);
|
|
7653
|
+
} catch {
|
|
7654
|
+
return false;
|
|
7655
|
+
}
|
|
7656
|
+
for (const name of siblings) {
|
|
7657
|
+
if (!GUARD_SOURCE.test(name)) continue;
|
|
7658
|
+
const path14 = join19(dir, name);
|
|
7659
|
+
try {
|
|
7660
|
+
const entry = lstatSync3(path14);
|
|
7661
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
|
|
7662
|
+
const source = stripComments(readFileSync14(path14, "utf8"));
|
|
7663
|
+
if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
|
|
7664
|
+
} catch {
|
|
7665
|
+
}
|
|
7666
|
+
}
|
|
7667
|
+
return false;
|
|
7668
|
+
}
|
|
7669
|
+
function isRuleBundle(value) {
|
|
7670
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
7671
|
+
const record = value;
|
|
7672
|
+
return Array.isArray(record.firewall) && Array.isArray(record.whitelists) && record.whitelist_keys !== null && typeof record.whitelist_keys === "object";
|
|
7673
|
+
}
|
|
7674
|
+
function findRulesFile(cwd) {
|
|
7675
|
+
const found = [];
|
|
7676
|
+
const unreadable = [];
|
|
7677
|
+
let root;
|
|
7678
|
+
try {
|
|
7679
|
+
root = realpathSync5(cwd);
|
|
7680
|
+
} catch {
|
|
7681
|
+
return { kind: "none" };
|
|
7682
|
+
}
|
|
7683
|
+
const contained = (path14) => {
|
|
7684
|
+
try {
|
|
7685
|
+
const real = realpathSync5(path14);
|
|
7686
|
+
return real === root || real.startsWith(root + sep2);
|
|
7687
|
+
} catch {
|
|
7688
|
+
return false;
|
|
7689
|
+
}
|
|
7690
|
+
};
|
|
7691
|
+
const walk2 = (dir, depth) => {
|
|
7692
|
+
if (depth > MAX_DEPTH2 || found.length > 8) return;
|
|
7693
|
+
let entries;
|
|
7694
|
+
try {
|
|
7695
|
+
entries = readdirSync6(dir);
|
|
7696
|
+
} catch {
|
|
7697
|
+
return;
|
|
7698
|
+
}
|
|
7699
|
+
for (const name of entries) {
|
|
7700
|
+
if (name.startsWith(".")) continue;
|
|
7701
|
+
const path14 = join19(dir, name);
|
|
7702
|
+
let entry;
|
|
7703
|
+
try {
|
|
7704
|
+
entry = lstatSync3(path14);
|
|
7705
|
+
} catch {
|
|
7706
|
+
continue;
|
|
7707
|
+
}
|
|
7708
|
+
if (entry.isSymbolicLink()) continue;
|
|
7709
|
+
if (entry.isDirectory()) {
|
|
7710
|
+
if (!SKIP_DIRECTORIES.has(name)) walk2(path14, depth + 1);
|
|
7711
|
+
continue;
|
|
7712
|
+
}
|
|
7713
|
+
if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
|
|
7714
|
+
if (!contained(path14)) continue;
|
|
7715
|
+
if (!guardImportsRulesFile(dir, name)) continue;
|
|
7716
|
+
try {
|
|
7717
|
+
if (isRuleBundle(JSON.parse(readFileSync14(path14, "utf8")))) found.push(path14);
|
|
7718
|
+
} catch {
|
|
7719
|
+
unreadable.push(path14);
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
};
|
|
7723
|
+
walk2(cwd, 0);
|
|
7724
|
+
if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative6(cwd, p)) };
|
|
7725
|
+
if (found.length === 1) return { kind: "one", path: found[0] };
|
|
7726
|
+
if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative6(cwd, p)) };
|
|
7727
|
+
return { kind: "none" };
|
|
7728
|
+
}
|
|
7729
|
+
function detectIndent(text) {
|
|
7730
|
+
const match = /\n([ \t]+)"/.exec(text);
|
|
7731
|
+
if (match === null) return 0;
|
|
7732
|
+
const whitespace = match[1];
|
|
7733
|
+
return whitespace.includes(" ") ? " " : whitespace.length;
|
|
7734
|
+
}
|
|
7735
|
+
function applyBuildStamp(cwd, id) {
|
|
7736
|
+
const location = findRulesFile(cwd);
|
|
7737
|
+
if (location.kind === "none") return { kind: "skipped", reason: "no guard rules file in this project" };
|
|
7738
|
+
if (location.kind === "unreadable") {
|
|
7739
|
+
return { kind: "skipped", reason: `${location.paths.join(", ")} is not valid JSON \u2014 left untouched` };
|
|
7740
|
+
}
|
|
7741
|
+
if (location.kind === "ambiguous") {
|
|
7742
|
+
return {
|
|
7743
|
+
kind: "skipped",
|
|
7744
|
+
reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
const file = relative6(cwd, location.path);
|
|
7748
|
+
let text;
|
|
7749
|
+
try {
|
|
7750
|
+
text = readFileSync14(location.path, "utf8");
|
|
7751
|
+
} catch (err) {
|
|
7752
|
+
return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
|
|
7753
|
+
}
|
|
7754
|
+
let parsed;
|
|
7755
|
+
try {
|
|
7756
|
+
parsed = JSON.parse(text);
|
|
7757
|
+
} catch {
|
|
7758
|
+
return { kind: "skipped", reason: `${file} is not valid JSON \u2014 left untouched` };
|
|
7759
|
+
}
|
|
7760
|
+
if (!isRuleBundle(parsed)) return { kind: "skipped", reason: `${file} is not a guard rules bundle` };
|
|
7761
|
+
const bundle = parsed;
|
|
7762
|
+
const current = readBuildStamp(bundle);
|
|
7763
|
+
if (id === null ? !hasRawBuildStamp(bundle) : current === id) return { kind: "unchanged", file };
|
|
7764
|
+
const namespace = { ...bundle[BUILD_STAMP_KEY] ?? {} };
|
|
7765
|
+
if (id === null) delete namespace.build_id;
|
|
7766
|
+
else namespace.build_id = id;
|
|
7767
|
+
if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
|
|
7768
|
+
else bundle[BUILD_STAMP_KEY] = namespace;
|
|
7769
|
+
const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
|
|
7770
|
+
const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
|
|
7771
|
+
try {
|
|
7772
|
+
const mode = lstatSync3(location.path).mode & 4095;
|
|
7773
|
+
writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
|
|
7774
|
+
chmodSync(temporary, mode);
|
|
7775
|
+
renameSync(temporary, location.path);
|
|
7776
|
+
} catch (err) {
|
|
7777
|
+
try {
|
|
7778
|
+
unlinkSync(temporary);
|
|
7779
|
+
} catch {
|
|
7780
|
+
}
|
|
7781
|
+
return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
|
|
7782
|
+
}
|
|
7783
|
+
return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
// src/build-hook.ts
|
|
7787
|
+
var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
|
|
7788
|
+
"preinstall",
|
|
7789
|
+
"install",
|
|
7790
|
+
"postinstall",
|
|
7791
|
+
"prepare",
|
|
7792
|
+
"prebuild",
|
|
7793
|
+
"build",
|
|
7794
|
+
"postbuild"
|
|
7795
|
+
]);
|
|
7796
|
+
function isInstallOrBuildHook(env = process.env) {
|
|
7797
|
+
const event = env.npm_lifecycle_event;
|
|
7798
|
+
return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
|
|
7799
|
+
}
|
|
7800
|
+
function isPreBundleBuildHook(env = process.env) {
|
|
7801
|
+
const event = env.npm_lifecycle_event;
|
|
7802
|
+
if (event === "prebuild") return true;
|
|
7803
|
+
if (event !== "build") return false;
|
|
7804
|
+
return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
|
|
7805
|
+
}
|
|
7806
|
+
function undeliveredReportLines(err, config, cwd) {
|
|
7807
|
+
const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
|
|
7808
|
+
if (err.code === "UNAUTHORIZED") {
|
|
7809
|
+
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
7810
|
+
if (hasCredential) {
|
|
7811
|
+
lines.push(
|
|
7812
|
+
"patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
|
|
7813
|
+
);
|
|
7814
|
+
} else {
|
|
7815
|
+
lines.push(
|
|
7816
|
+
`patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
|
|
7817
|
+
`patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
|
|
7818
|
+
);
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
lines.push(
|
|
7822
|
+
"patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
|
|
7823
|
+
);
|
|
7824
|
+
return lines;
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
// src/input-map-id.ts
|
|
7828
|
+
import { createHash as createHash6 } from "crypto";
|
|
7829
|
+
var NonCanonicalInputMap = class extends Error {
|
|
7830
|
+
};
|
|
7831
|
+
function canonicalString(value) {
|
|
7832
|
+
let out = '"';
|
|
7833
|
+
for (const character of value) {
|
|
7834
|
+
const code = character.codePointAt(0);
|
|
7835
|
+
if (character === '"') out += '\\"';
|
|
7836
|
+
else if (character === "\\") out += "\\\\";
|
|
7837
|
+
else if (code < 32) out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
7838
|
+
else out += character;
|
|
7839
|
+
}
|
|
7840
|
+
return `${out}"`;
|
|
7841
|
+
}
|
|
7842
|
+
function canonicalText(value) {
|
|
7843
|
+
if (value === null) return "null";
|
|
7844
|
+
if (value === true) return "true";
|
|
7845
|
+
if (value === false) return "false";
|
|
7846
|
+
if (typeof value === "string") return canonicalString(value);
|
|
7847
|
+
if (typeof value === "number") {
|
|
7848
|
+
if (!Number.isSafeInteger(value)) {
|
|
7849
|
+
throw new NonCanonicalInputMap("the input map contains a number outside the safe-integer form");
|
|
7850
|
+
}
|
|
7851
|
+
return String(value === 0 ? 0 : value);
|
|
7852
|
+
}
|
|
7853
|
+
if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
|
|
7854
|
+
if (value !== null && typeof value === "object") {
|
|
7855
|
+
const record = value;
|
|
7856
|
+
const keys = Object.keys(record).sort();
|
|
7857
|
+
return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
|
|
7858
|
+
}
|
|
7859
|
+
throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
|
|
7860
|
+
}
|
|
7861
|
+
function inputMapBuildId(map) {
|
|
7862
|
+
let material = map;
|
|
7863
|
+
if (map !== null && typeof map === "object" && !Array.isArray(map)) {
|
|
7864
|
+
const record = map;
|
|
7865
|
+
const { build_id: _buildId, ...mapDocument } = record;
|
|
7866
|
+
const coverage = record.coverage;
|
|
7867
|
+
if (coverage !== null && typeof coverage === "object" && !Array.isArray(coverage)) {
|
|
7868
|
+
const { analysisMs: _analysisMs, rssBytes: _rssBytes, peakRssBytes: _peakRssBytes, ...stableCoverage } = coverage;
|
|
7869
|
+
material = { ...mapDocument, coverage: stableCoverage };
|
|
7870
|
+
} else material = mapDocument;
|
|
7871
|
+
}
|
|
7872
|
+
return createHash6("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
|
|
7873
|
+
}
|
|
7874
|
+
|
|
6946
7875
|
// src/map-command.ts
|
|
6947
7876
|
async function runMap(flags) {
|
|
6948
7877
|
const cwd = getStringFlag(flags, "dir") ?? process.cwd();
|
|
@@ -6987,7 +7916,7 @@ async function runMap(flags) {
|
|
|
6987
7916
|
const json = JSON.stringify(map, null, 2);
|
|
6988
7917
|
const out = getStringFlag(flags, "out");
|
|
6989
7918
|
if (out) {
|
|
6990
|
-
|
|
7919
|
+
writeFileSync9(out, json);
|
|
6991
7920
|
console.error(`patchstack: wrote ${out}`);
|
|
6992
7921
|
} else if (flags.get("upload") !== true) {
|
|
6993
7922
|
console.log(json);
|
|
@@ -7003,7 +7932,25 @@ async function runMap(flags) {
|
|
|
7003
7932
|
cliSiteUuid: getStringFlag(flags, "site-uuid"),
|
|
7004
7933
|
cliEndpoint: getStringFlag(flags, "endpoint")
|
|
7005
7934
|
});
|
|
7006
|
-
|
|
7935
|
+
let buildId = null;
|
|
7936
|
+
if (isPreBundleBuildHook()) {
|
|
7937
|
+
const candidate = inputMapBuildId(map);
|
|
7938
|
+
const stamp = applyBuildStamp(cwd, candidate);
|
|
7939
|
+
if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
|
|
7940
|
+
buildId = candidate;
|
|
7941
|
+
console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
|
|
7942
|
+
} else {
|
|
7943
|
+
const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
|
|
7944
|
+
console.error(
|
|
7945
|
+
`patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
|
|
7946
|
+
);
|
|
7947
|
+
}
|
|
7948
|
+
} else {
|
|
7949
|
+
console.error(
|
|
7950
|
+
"patchstack: no runtime binding recorded \u2014 run `map --upload` in a prebuild hook before the bundler, so rules generated from these coordinates can be tied to the runtime guard. Until then they detect only, not block."
|
|
7951
|
+
);
|
|
7952
|
+
}
|
|
7953
|
+
const outcome = await postInputMap(config, map, buildId);
|
|
7007
7954
|
if (outcome.result === "stored") {
|
|
7008
7955
|
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
7009
7956
|
} else if (outcome.result === "unchanged") {
|
|
@@ -7018,8 +7965,8 @@ async function runMap(flags) {
|
|
|
7018
7965
|
}
|
|
7019
7966
|
|
|
7020
7967
|
// src/setup.ts
|
|
7021
|
-
import { readFileSync as
|
|
7022
|
-
import
|
|
7968
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
7969
|
+
import path13 from "path";
|
|
7023
7970
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
7024
7971
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
7025
7972
|
function setupProtection(cwd) {
|
|
@@ -7036,9 +7983,16 @@ function appendHook(existing, command) {
|
|
|
7036
7983
|
}
|
|
7037
7984
|
return `${existing} && ${command}`;
|
|
7038
7985
|
}
|
|
7986
|
+
function prependHook(existing, command) {
|
|
7987
|
+
if (existing === void 0 || existing.trim().length === 0) return command;
|
|
7988
|
+
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7989
|
+
if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
|
|
7990
|
+
const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
|
|
7991
|
+
return [command, ...remaining].join(" && ");
|
|
7992
|
+
}
|
|
7039
7993
|
function wireBuildScripts(cwd, packageManager) {
|
|
7040
|
-
const target =
|
|
7041
|
-
const raw =
|
|
7994
|
+
const target = path13.join(cwd, "package.json");
|
|
7995
|
+
const raw = readFileSync15(target, "utf8");
|
|
7042
7996
|
const pkg = JSON.parse(raw);
|
|
7043
7997
|
const scripts = pkg.scripts ?? {};
|
|
7044
7998
|
const build = scripts.build;
|
|
@@ -7053,10 +8007,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7053
8007
|
}
|
|
7054
8008
|
scripts.postinstall = postinstall;
|
|
7055
8009
|
} else if (packageManager === "bun") {
|
|
7056
|
-
let nextBuild = build;
|
|
7057
|
-
if (!nextBuild.includes(SCAN_COMMAND)) {
|
|
7058
|
-
nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
|
|
7059
|
-
}
|
|
8010
|
+
let nextBuild = prependHook(build, SCAN_COMMAND);
|
|
7060
8011
|
if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
|
|
7061
8012
|
nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
|
|
7062
8013
|
}
|
|
@@ -7070,7 +8021,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7070
8021
|
scripts.postinstall = postinstall;
|
|
7071
8022
|
scripts.build = nextBuild;
|
|
7072
8023
|
} else {
|
|
7073
|
-
const prebuild =
|
|
8024
|
+
const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
|
|
7074
8025
|
const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
|
|
7075
8026
|
if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
|
|
7076
8027
|
return {
|
|
@@ -7087,7 +8038,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7087
8038
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
7088
8039
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
7089
8040
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
7090
|
-
|
|
8041
|
+
writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
7091
8042
|
if (build === void 0 || build.trim().length === 0) {
|
|
7092
8043
|
return {
|
|
7093
8044
|
changed: true,
|
|
@@ -7106,41 +8057,6 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7106
8057
|
};
|
|
7107
8058
|
}
|
|
7108
8059
|
|
|
7109
|
-
// src/build-hook.ts
|
|
7110
|
-
var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
|
|
7111
|
-
"preinstall",
|
|
7112
|
-
"install",
|
|
7113
|
-
"postinstall",
|
|
7114
|
-
"prepare",
|
|
7115
|
-
"prebuild",
|
|
7116
|
-
"build",
|
|
7117
|
-
"postbuild"
|
|
7118
|
-
]);
|
|
7119
|
-
function isInstallOrBuildHook(env = process.env) {
|
|
7120
|
-
const event = env.npm_lifecycle_event;
|
|
7121
|
-
return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
|
|
7122
|
-
}
|
|
7123
|
-
function undeliveredReportLines(err, config, cwd) {
|
|
7124
|
-
const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
|
|
7125
|
-
if (err.code === "UNAUTHORIZED") {
|
|
7126
|
-
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
7127
|
-
if (hasCredential) {
|
|
7128
|
-
lines.push(
|
|
7129
|
-
"patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
|
|
7130
|
-
);
|
|
7131
|
-
} else {
|
|
7132
|
-
lines.push(
|
|
7133
|
-
`patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
|
|
7134
|
-
`patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
|
|
7135
|
-
);
|
|
7136
|
-
}
|
|
7137
|
-
}
|
|
7138
|
-
lines.push(
|
|
7139
|
-
"patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
|
|
7140
|
-
);
|
|
7141
|
-
return lines;
|
|
7142
|
-
}
|
|
7143
|
-
|
|
7144
8060
|
// src/cli.ts
|
|
7145
8061
|
var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
|
|
7146
8062
|
|
|
@@ -7168,7 +8084,8 @@ Usage:
|
|
|
7168
8084
|
with how the link was established). Best-effort static
|
|
7169
8085
|
analysis \u2014 reports the DETECTED surface, with
|
|
7170
8086
|
coverage counters. Prints JSON (--out writes a
|
|
7171
|
-
file;
|
|
8087
|
+
file; a pre-bundle --upload also stamps the existing
|
|
8088
|
+
guard rules file; --follow-symlinks leaves the project dir).
|
|
7172
8089
|
Uses the app's own TypeScript
|
|
7173
8090
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
7174
8091
|
with an existing site UUID
|
|
@@ -7192,6 +8109,15 @@ Usage:
|
|
|
7192
8109
|
demonstrations, not production).
|
|
7193
8110
|
--check verifies the guard is wired (exit 1
|
|
7194
8111
|
if not) \u2014 for the wire-then-verify loop.
|
|
8112
|
+
It reads the app's source and never runs it.
|
|
8113
|
+
--check --runtime additionally STARTS THE APP
|
|
8114
|
+
on a loopback port and sends it one request,
|
|
8115
|
+
to establish that a request reaches the
|
|
8116
|
+
guard. Opt-in, and the only mode that runs
|
|
8117
|
+
the app. Exit 0 traversed, 1 a listener
|
|
8118
|
+
answered instead of the guard, 2 could not
|
|
8119
|
+
be established (e.g. a TypeScript or
|
|
8120
|
+
framework entry this must not start).
|
|
7195
8121
|
patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
|
|
7196
8122
|
walkthrough: verify the vulnerable package,
|
|
7197
8123
|
scan it, wait for live rule 18843, install +
|
|
@@ -7204,6 +8130,20 @@ Usage:
|
|
|
7204
8130
|
what's missing, with tailored commands), then
|
|
7205
8131
|
print the full setup guide. --full prints the
|
|
7206
8132
|
guide even when setup is complete
|
|
8133
|
+
patchstack-connect claim [--wait] Attach this site to a Patchstack account from
|
|
8134
|
+
the terminal, so its reports become visible.
|
|
8135
|
+
Prints a link for the user to open and sign in
|
|
8136
|
+
(or sign up) \u2014 whoever approves becomes the
|
|
8137
|
+
site's owner. In a terminal it then waits. When
|
|
8138
|
+
the output is piped or captured \u2014 an assistant
|
|
8139
|
+
running it \u2014 it prints the link and EXITS, so
|
|
8140
|
+
the link is visible immediately. Run it AGAIN
|
|
8141
|
+
once the user confirms: it resumes the same
|
|
8142
|
+
request rather than starting a new one. --wait
|
|
8143
|
+
blocks instead of returning. Does not rotate
|
|
8144
|
+
the site's credential. The same claim can be
|
|
8145
|
+
done by opening the dashboard link that scan
|
|
8146
|
+
and status print. Not usable in CI
|
|
7207
8147
|
patchstack-connect login [--wait] Recover this site's credential when
|
|
7208
8148
|
.patchstackrc.local.json has been lost. Needs the
|
|
7209
8149
|
site UUID, which lives in the committed
|
|
@@ -7321,6 +8261,107 @@ async function runInit(args) {
|
|
|
7321
8261
|
console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
|
|
7322
8262
|
return 0;
|
|
7323
8263
|
}
|
|
8264
|
+
async function runClaim(args) {
|
|
8265
|
+
if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
|
|
8266
|
+
console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
|
|
8267
|
+
return 1;
|
|
8268
|
+
}
|
|
8269
|
+
const config = await resolveConfig({
|
|
8270
|
+
cwd: process.cwd(),
|
|
8271
|
+
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
8272
|
+
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
8273
|
+
});
|
|
8274
|
+
const siteUuid = config.siteUuid;
|
|
8275
|
+
if (siteUuid === null) {
|
|
8276
|
+
console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
|
|
8277
|
+
return 1;
|
|
8278
|
+
}
|
|
8279
|
+
const settled = async (result2) => {
|
|
8280
|
+
console.log(
|
|
8281
|
+
`
|
|
8282
|
+
\u2713 Site claimed${result2.account !== void 0 ? ` by ${result2.account}` : ""}.`
|
|
8283
|
+
);
|
|
8284
|
+
console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
|
|
8285
|
+
if (result2.credentialSaved === true) {
|
|
8286
|
+
const ignore = await secretFileIgnored(process.cwd());
|
|
8287
|
+
console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
8288
|
+
console.log(
|
|
8289
|
+
ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
|
|
8290
|
+
);
|
|
8291
|
+
}
|
|
8292
|
+
console.log("");
|
|
8293
|
+
return 0;
|
|
8294
|
+
};
|
|
8295
|
+
const prompt = (userCode, verificationUri) => {
|
|
8296
|
+
console.log(`
|
|
8297
|
+
Your code: ${userCode}`);
|
|
8298
|
+
console.log(` Claim at: ${verificationUri}
|
|
8299
|
+
`);
|
|
8300
|
+
console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
|
|
8301
|
+
console.log(" this site to it. Whoever approves becomes the site's owner.\n");
|
|
8302
|
+
};
|
|
8303
|
+
if (args.flags.has("wait")) {
|
|
8304
|
+
const pending = readPendingClaim(siteUuid);
|
|
8305
|
+
if (pending === null) {
|
|
8306
|
+
console.error("\n No claim is waiting for approval. Run `patchstack-connect claim` first.\n");
|
|
8307
|
+
return 1;
|
|
8308
|
+
}
|
|
8309
|
+
const resumed = await waitForClaim(config, pending);
|
|
8310
|
+
if (resumed.status === "claimed") return await settled(resumed);
|
|
8311
|
+
console.error(`
|
|
8312
|
+
${resumed.message ?? "Claim failed."}
|
|
8313
|
+
`);
|
|
8314
|
+
return 1;
|
|
8315
|
+
}
|
|
8316
|
+
if (process.stdout.isTTY !== true) {
|
|
8317
|
+
const existing = readPendingClaim(siteUuid);
|
|
8318
|
+
if (existing !== null && Date.now() < existing.expiresAt) {
|
|
8319
|
+
const outcome = await redeemClaimIfApproved(config, existing);
|
|
8320
|
+
if (outcome !== "pending" && outcome.status === "claimed") return await settled(outcome);
|
|
8321
|
+
if (outcome === "pending") {
|
|
8322
|
+
const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
|
|
8323
|
+
console.log(`
|
|
8324
|
+
Still waiting for the site to be claimed with code ${existing.userCode}.`);
|
|
8325
|
+
console.log(` Claim at: ${existing.verificationUri}`);
|
|
8326
|
+
console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
|
|
8327
|
+
`);
|
|
8328
|
+
return 0;
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
const started = await startClaim(config);
|
|
8332
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
8333
|
+
if (started.status === "already-claimed") {
|
|
8334
|
+
console.log(`
|
|
8335
|
+
${started.message ?? "This site is already claimed."}
|
|
8336
|
+
`);
|
|
8337
|
+
return 0;
|
|
8338
|
+
}
|
|
8339
|
+
console.error(`
|
|
8340
|
+
${started.message ?? "Claim failed."}
|
|
8341
|
+
`);
|
|
8342
|
+
return 1;
|
|
8343
|
+
}
|
|
8344
|
+
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
8345
|
+
console.log(" Give that link to the user. When they confirm they have claimed the site, run");
|
|
8346
|
+
console.log(" this same command again (or `claim --wait` to block until they do).\n");
|
|
8347
|
+
return 0;
|
|
8348
|
+
}
|
|
8349
|
+
const result = await claim2(config, (userCode, verificationUri) => {
|
|
8350
|
+
prompt(userCode, verificationUri);
|
|
8351
|
+
console.log(" Waiting (the code expires in 10 minutes)\u2026");
|
|
8352
|
+
});
|
|
8353
|
+
if (result.status === "claimed") return await settled(result);
|
|
8354
|
+
if (result.status === "already-claimed") {
|
|
8355
|
+
console.log(`
|
|
8356
|
+
${result.message ?? "This site is already claimed."}
|
|
8357
|
+
`);
|
|
8358
|
+
return 0;
|
|
8359
|
+
}
|
|
8360
|
+
console.error(`
|
|
8361
|
+
${result.message ?? "Claim failed."}
|
|
8362
|
+
`);
|
|
8363
|
+
return 1;
|
|
8364
|
+
}
|
|
7324
8365
|
async function runLogin(args) {
|
|
7325
8366
|
if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
|
|
7326
8367
|
console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
|
|
@@ -7466,6 +8507,10 @@ async function runScan(args, options = {}) {
|
|
|
7466
8507
|
}
|
|
7467
8508
|
return 0;
|
|
7468
8509
|
}
|
|
8510
|
+
if (isPreBundleBuildHook()) {
|
|
8511
|
+
const cleared = applyBuildStamp(process.cwd(), null);
|
|
8512
|
+
if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
|
|
8513
|
+
}
|
|
7469
8514
|
if (config.widget) {
|
|
7470
8515
|
reportSourceMarker(detectStack(payload.packages).framework);
|
|
7471
8516
|
}
|
|
@@ -7523,6 +8568,9 @@ async function runScan(args, options = {}) {
|
|
|
7523
8568
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
7524
8569
|
console.log(" (this URL inherits the endpoint override above)");
|
|
7525
8570
|
}
|
|
8571
|
+
console.log("");
|
|
8572
|
+
console.log("Or attach it to your account from this terminal:");
|
|
8573
|
+
console.log(" npx @patchstack/connect claim");
|
|
7526
8574
|
}
|
|
7527
8575
|
if (options.showRemainingSetup !== false) {
|
|
7528
8576
|
try {
|
|
@@ -7610,6 +8658,11 @@ function reportSourceMarker(framework) {
|
|
|
7610
8658
|
}
|
|
7611
8659
|
}
|
|
7612
8660
|
async function runProtectCommand(args) {
|
|
8661
|
+
const runtime = args.flags.get("runtime") === true;
|
|
8662
|
+
if (runtime && args.flags.get("check") !== true) {
|
|
8663
|
+
console.error("patchstack protect: --runtime only applies to --check. Run `protect --check --runtime`.");
|
|
8664
|
+
return 1;
|
|
8665
|
+
}
|
|
7613
8666
|
if (args.flags.get("check") === true) {
|
|
7614
8667
|
const report = runVerify(process.cwd());
|
|
7615
8668
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
@@ -7628,7 +8681,12 @@ async function runProtectCommand(args) {
|
|
|
7628
8681
|
if (report.checks.some((c) => c.unverifiable)) {
|
|
7629
8682
|
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7630
8683
|
}
|
|
7631
|
-
|
|
8684
|
+
if (!report.wired) return 1;
|
|
8685
|
+
if (!runtime) return 0;
|
|
8686
|
+
console.log("");
|
|
8687
|
+
const runtimeReport = await runRuntimeCheck(process.cwd());
|
|
8688
|
+
for (const line2 of formatRuntimeCheck(runtimeReport)) console.log(line2);
|
|
8689
|
+
return runtimeExitCode(runtimeReport);
|
|
7632
8690
|
}
|
|
7633
8691
|
const demo = args.flags.get("demo") === true;
|
|
7634
8692
|
try {
|
|
@@ -7751,7 +8809,7 @@ async function runGuide(args) {
|
|
|
7751
8809
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
7752
8810
|
console.log("");
|
|
7753
8811
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
7754
|
-
console.log(
|
|
8812
|
+
console.log(readFileSync16(guidePath, "utf8"));
|
|
7755
8813
|
return 0;
|
|
7756
8814
|
}
|
|
7757
8815
|
async function runSetup(args) {
|
|
@@ -7825,6 +8883,7 @@ async function runStatus(args) {
|
|
|
7825
8883
|
console.log(`Environment: ${config.environment}`);
|
|
7826
8884
|
if (config.siteUuid !== null) {
|
|
7827
8885
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
8886
|
+
console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
|
|
7828
8887
|
switch (await fetchSiteStatus(config)) {
|
|
7829
8888
|
case "active":
|
|
7830
8889
|
console.log("Site status: active on Patchstack");
|
|
@@ -7944,7 +9003,7 @@ async function runMarkBuild(args) {
|
|
|
7944
9003
|
let marked = 0;
|
|
7945
9004
|
let widgetTouched = 0;
|
|
7946
9005
|
for (const file of files) {
|
|
7947
|
-
const before =
|
|
9006
|
+
const before = readFileSync16(file, "utf8");
|
|
7948
9007
|
let after = injectMarker(before, snippet);
|
|
7949
9008
|
if (widgetUuid !== null) {
|
|
7950
9009
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -7954,7 +9013,7 @@ async function runMarkBuild(args) {
|
|
|
7954
9013
|
after = ensured.html;
|
|
7955
9014
|
}
|
|
7956
9015
|
if (after !== before) {
|
|
7957
|
-
|
|
9016
|
+
writeFileSync11(file, after);
|
|
7958
9017
|
marked += 1;
|
|
7959
9018
|
}
|
|
7960
9019
|
}
|
|
@@ -8006,6 +9065,8 @@ async function main() {
|
|
|
8006
9065
|
return runSetup(args);
|
|
8007
9066
|
case "map":
|
|
8008
9067
|
return runMap(args.flags);
|
|
9068
|
+
case "claim":
|
|
9069
|
+
return runClaim(args);
|
|
8009
9070
|
case "login":
|
|
8010
9071
|
return runLogin(args);
|
|
8011
9072
|
default:
|