@patchstack/connect 0.5.0 → 0.5.2
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 +95 -2
- package/README.md +105 -1
- package/dist/cli.js +987 -145
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +9 -2
- 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 +43 -5
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +14 -0
- package/dist/protect.d.ts +14 -0
- package/dist/protect.edge.js +30 -1
- package/dist/protect.edge.js.map +2 -2
- package/dist/protect.js +31 -2
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-LDS35UKV.js → refresh-manifest-2MMPQN2A.js} +10 -3
- package/dist/refresh-manifest-2MMPQN2A.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-LDS35UKV.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 readFileSync15, writeFileSync as writeFileSync10 } 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();
|
|
@@ -1291,6 +1291,25 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1291
1291
|
// src/client.ts
|
|
1292
1292
|
var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
1293
1293
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1294
|
+
var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
1295
|
+
function claimTokenHeader(config) {
|
|
1296
|
+
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1297
|
+
}
|
|
1298
|
+
function claimOutcomeLines(claim3, config) {
|
|
1299
|
+
if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
|
|
1300
|
+
if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
|
|
1301
|
+
const dashboard = typeof claim3.dashboard_url === "string" && claim3.dashboard_url !== "" ? [`Dashboard: ${claim3.dashboard_url}`] : [];
|
|
1302
|
+
return [
|
|
1303
|
+
claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
|
|
1304
|
+
...dashboard
|
|
1305
|
+
];
|
|
1306
|
+
}
|
|
1307
|
+
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
|
+
return [
|
|
1309
|
+
`Not connected to your account: ${why}.`,
|
|
1310
|
+
"Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
|
|
1311
|
+
];
|
|
1312
|
+
}
|
|
1294
1313
|
function buildEndpointUrl(base, siteUuid) {
|
|
1295
1314
|
const trimmed = base.replace(/\/$/, "");
|
|
1296
1315
|
return siteUuid !== void 0 && siteUuid !== null && siteUuid.length > 0 ? `${trimmed}/${encodeURIComponent(siteUuid)}` : trimmed;
|
|
@@ -1310,8 +1329,8 @@ function authFailureMessage(status, config) {
|
|
|
1310
1329
|
}
|
|
1311
1330
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1312
1331
|
const url = new URL(manifestEndpoint);
|
|
1313
|
-
const
|
|
1314
|
-
url.pathname =
|
|
1332
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1333
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
|
|
1315
1334
|
url.search = "";
|
|
1316
1335
|
url.hash = "";
|
|
1317
1336
|
return url.toString();
|
|
@@ -1326,16 +1345,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
|
|
|
1326
1345
|
}
|
|
1327
1346
|
function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
|
|
1328
1347
|
const url = new URL(manifestEndpoint);
|
|
1329
|
-
const
|
|
1330
|
-
url.pathname =
|
|
1348
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1349
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
|
|
1331
1350
|
url.search = "";
|
|
1332
1351
|
url.hash = "";
|
|
1333
1352
|
return url.toString();
|
|
1334
1353
|
}
|
|
1335
1354
|
function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
1336
1355
|
const url = new URL(manifestEndpoint);
|
|
1337
|
-
const
|
|
1338
|
-
url.pathname =
|
|
1356
|
+
const path14 = url.pathname.replace(/\/$/, "");
|
|
1357
|
+
url.pathname = path14.endsWith("/manifest") ? `${path14.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
|
|
1339
1358
|
url.search = "";
|
|
1340
1359
|
url.hash = "";
|
|
1341
1360
|
return url.toString();
|
|
@@ -1454,7 +1473,8 @@ async function postManifest(config, payload) {
|
|
|
1454
1473
|
headers: {
|
|
1455
1474
|
"Content-Type": "application/json",
|
|
1456
1475
|
Accept: "application/json",
|
|
1457
|
-
"User-Agent": "@patchstack/connect"
|
|
1476
|
+
"User-Agent": "@patchstack/connect",
|
|
1477
|
+
...claimTokenHeader(config)
|
|
1458
1478
|
},
|
|
1459
1479
|
body: JSON.stringify(buildManifestBody(config, payload)),
|
|
1460
1480
|
signal: AbortSignal.timeout(timeoutMs)
|
|
@@ -1670,7 +1690,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
|
|
|
1670
1690
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
1671
1691
|
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
1672
1692
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1673
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
1693
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
|
|
1674
1694
|
const now = options.now ?? Date.now;
|
|
1675
1695
|
const started = now();
|
|
1676
1696
|
const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
|
|
@@ -1944,6 +1964,7 @@ async function resolveConfig(options) {
|
|
|
1944
1964
|
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1945
1965
|
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1946
1966
|
const identity = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
|
|
1967
|
+
const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
|
|
1947
1968
|
return {
|
|
1948
1969
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1949
1970
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1953,7 +1974,8 @@ async function resolveConfig(options) {
|
|
|
1953
1974
|
endpoint,
|
|
1954
1975
|
timeoutMs,
|
|
1955
1976
|
environment,
|
|
1956
|
-
widget: fromFile.widget !== false
|
|
1977
|
+
widget: fromFile.widget !== false,
|
|
1978
|
+
claimToken
|
|
1957
1979
|
};
|
|
1958
1980
|
}
|
|
1959
1981
|
async function writeConfigFile(cwd, config) {
|
|
@@ -2989,6 +3011,112 @@ import { join as join10, dirname as dirname5 } from "path";
|
|
|
2989
3011
|
import { execFileSync } from "child_process";
|
|
2990
3012
|
import { existsSync as existsSync7 } from "fs";
|
|
2991
3013
|
import { dirname as dirname4, join as join9, resolve } from "path";
|
|
3014
|
+
|
|
3015
|
+
// src/protect/install/node-flags.ts
|
|
3016
|
+
var flagName = (token) => token.split("=")[0];
|
|
3017
|
+
var NO_OPERAND_FLAGS = /* @__PURE__ */ new Set([
|
|
3018
|
+
"--disallow-code-generation-from-strings",
|
|
3019
|
+
"--enable-source-maps",
|
|
3020
|
+
"--experimental-import-meta-resolve",
|
|
3021
|
+
"--experimental-json-modules",
|
|
3022
|
+
"--experimental-vm-modules",
|
|
3023
|
+
"--frozen-intrinsics",
|
|
3024
|
+
"--no-deprecation",
|
|
3025
|
+
"--no-warnings",
|
|
3026
|
+
"--pending-deprecation",
|
|
3027
|
+
"--preserve-symlinks",
|
|
3028
|
+
"--preserve-symlinks-main",
|
|
3029
|
+
"--throw-deprecation",
|
|
3030
|
+
"--trace-deprecation",
|
|
3031
|
+
"--trace-exit",
|
|
3032
|
+
"--trace-uncaught",
|
|
3033
|
+
"--trace-warnings",
|
|
3034
|
+
"--use-strict",
|
|
3035
|
+
"--zero-fill-buffers"
|
|
3036
|
+
]);
|
|
3037
|
+
var VALUED_FLAGS = /* @__PURE__ */ new Set([
|
|
3038
|
+
"--dns-result-order",
|
|
3039
|
+
"--max-http-header-size",
|
|
3040
|
+
"--max-old-space-size",
|
|
3041
|
+
"--max-semi-space-size",
|
|
3042
|
+
"--stack-size",
|
|
3043
|
+
"--title",
|
|
3044
|
+
"--unhandled-rejections"
|
|
3045
|
+
]);
|
|
3046
|
+
var CODE_LOADING_FLAGS = /* @__PURE__ */ new Set([
|
|
3047
|
+
"--require",
|
|
3048
|
+
"-r",
|
|
3049
|
+
"--import",
|
|
3050
|
+
"--loader",
|
|
3051
|
+
"--experimental-loader",
|
|
3052
|
+
"--eval",
|
|
3053
|
+
"-e",
|
|
3054
|
+
"--print",
|
|
3055
|
+
"-p",
|
|
3056
|
+
"--inspect",
|
|
3057
|
+
"--inspect-brk",
|
|
3058
|
+
"--inspect-wait",
|
|
3059
|
+
"--inspect-port",
|
|
3060
|
+
"--debug",
|
|
3061
|
+
"--debug-brk"
|
|
3062
|
+
]);
|
|
3063
|
+
var TAKES_NEXT_TOKEN = /* @__PURE__ */ new Set(["--require", "-r", "--import", "--loader", "--experimental-loader", "--eval", "-e", "--print", "-p", "--inspect-port"]);
|
|
3064
|
+
function tokenizeNodeOptions(value) {
|
|
3065
|
+
const tokens = [];
|
|
3066
|
+
let current = "";
|
|
3067
|
+
let quoted = false;
|
|
3068
|
+
let started = false;
|
|
3069
|
+
for (const ch of value) {
|
|
3070
|
+
if (ch === '"') {
|
|
3071
|
+
quoted = !quoted;
|
|
3072
|
+
started = true;
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (!quoted && /\s/.test(ch)) {
|
|
3076
|
+
if (started) tokens.push(current);
|
|
3077
|
+
current = "";
|
|
3078
|
+
started = false;
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
current += ch;
|
|
3082
|
+
started = true;
|
|
3083
|
+
}
|
|
3084
|
+
if (quoted) return null;
|
|
3085
|
+
if (started) tokens.push(current);
|
|
3086
|
+
return tokens;
|
|
3087
|
+
}
|
|
3088
|
+
function classifyNodeOptions(value) {
|
|
3089
|
+
const tokens = tokenizeNodeOptions(value);
|
|
3090
|
+
if (tokens === null) return { kind: "refused", why: "NODE_OPTIONS has an unbalanced double quote, so this cannot read it the way Node will" };
|
|
3091
|
+
for (const token of tokens) {
|
|
3092
|
+
const name = flagName(token);
|
|
3093
|
+
if (CODE_LOADING_FLAGS.has(name)) {
|
|
3094
|
+
return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which runs code or opens a port before the listener reporter is in place` };
|
|
3095
|
+
}
|
|
3096
|
+
if (NO_OPERAND_FLAGS.has(token)) continue;
|
|
3097
|
+
if (token.includes("=") && VALUED_FLAGS.has(name)) continue;
|
|
3098
|
+
return { kind: "refused", why: `NODE_OPTIONS carries ${name}, which this does not recognise well enough to say what it does` };
|
|
3099
|
+
}
|
|
3100
|
+
return { kind: "safe" };
|
|
3101
|
+
}
|
|
3102
|
+
function withoutCodeLoading(value) {
|
|
3103
|
+
if (value === void 0 || value.trim() === "") return "";
|
|
3104
|
+
const tokens = tokenizeNodeOptions(value);
|
|
3105
|
+
if (tokens === null) return "";
|
|
3106
|
+
const kept = [];
|
|
3107
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
3108
|
+
const token = tokens[i];
|
|
3109
|
+
const name = flagName(token);
|
|
3110
|
+
if (CODE_LOADING_FLAGS.has(name)) {
|
|
3111
|
+
if (!token.includes("=") && TAKES_NEXT_TOKEN.has(name)) i++;
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
3114
|
+
if (NO_OPERAND_FLAGS.has(token) || token.includes("=") && VALUED_FLAGS.has(name)) kept.push(token);
|
|
3115
|
+
}
|
|
3116
|
+
return kept.map((token) => /\s/.test(token) ? `"${token}"` : token).join(" ");
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/protect/install/source-scope.ts
|
|
2992
3120
|
var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
|
|
2993
3121
|
function lastTopLevelImportLine(source) {
|
|
2994
3122
|
const lines = source.split("\n");
|
|
@@ -3067,7 +3195,10 @@ function advanceDepth(line, inBlockComment, add) {
|
|
|
3067
3195
|
function parses(filePath) {
|
|
3068
3196
|
if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
|
|
3069
3197
|
try {
|
|
3070
|
-
execFileSync(process.execPath, ["--check", filePath], {
|
|
3198
|
+
execFileSync(process.execPath, ["--check", filePath], {
|
|
3199
|
+
stdio: "pipe",
|
|
3200
|
+
env: { ...process.env, NODE_OPTIONS: withoutCodeLoading(process.env.NODE_OPTIONS) }
|
|
3201
|
+
});
|
|
3071
3202
|
return true;
|
|
3072
3203
|
} catch (error) {
|
|
3073
3204
|
return isSyntaxError(error) ? false : null;
|
|
@@ -4336,12 +4467,15 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4336
4467
|
lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
|
|
4337
4468
|
}
|
|
4338
4469
|
lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
|
|
4470
|
+
lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
|
|
4339
4471
|
}
|
|
4340
4472
|
lines.push("");
|
|
4341
4473
|
if (state.claimUrl !== null) {
|
|
4342
4474
|
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
|
|
4343
4475
|
lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
|
|
4344
4476
|
lines.push(detail("Open this link in a browser. The CLI never opens it."));
|
|
4477
|
+
lines.push(detail("Or from this terminal \u2192 npx @patchstack/connect claim"));
|
|
4478
|
+
lines.push(detail(" (prints a link to sign in with, then attaches the site to that account)"));
|
|
4345
4479
|
if (state.endpointOverride !== null) {
|
|
4346
4480
|
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
4347
4481
|
}
|
|
@@ -4384,7 +4518,12 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4384
4518
|
}
|
|
4385
4519
|
|
|
4386
4520
|
// src/login.ts
|
|
4387
|
-
import { mkdtempSync
|
|
4521
|
+
import { mkdtempSync } from "fs";
|
|
4522
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4523
|
+
import path12 from "path";
|
|
4524
|
+
|
|
4525
|
+
// src/device-flow.ts
|
|
4526
|
+
import { readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
4388
4527
|
import { tmpdir } from "os";
|
|
4389
4528
|
import path11 from "path";
|
|
4390
4529
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -4396,108 +4535,126 @@ function baseFrom(manifestEndpoint) {
|
|
|
4396
4535
|
url.hash = "";
|
|
4397
4536
|
return url.toString().replace(/\/$/, "");
|
|
4398
4537
|
}
|
|
4399
|
-
function pendingPath(siteUuid) {
|
|
4538
|
+
function pendingPath(siteUuid, intent) {
|
|
4400
4539
|
const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
|
|
4401
|
-
return path11.join(tmpdir(), `patchstack
|
|
4540
|
+
return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
|
|
4402
4541
|
}
|
|
4403
|
-
function
|
|
4404
|
-
writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), {
|
|
4542
|
+
function savePending(siteUuid, intent, pending) {
|
|
4543
|
+
writeFileSync7(pendingPath(siteUuid, intent), JSON.stringify(pending), {
|
|
4544
|
+
encoding: "utf8",
|
|
4545
|
+
mode: 384
|
|
4546
|
+
});
|
|
4405
4547
|
}
|
|
4406
|
-
function
|
|
4548
|
+
function readPending(siteUuid, intent) {
|
|
4407
4549
|
try {
|
|
4408
|
-
return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
|
|
4550
|
+
return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
|
|
4409
4551
|
} catch {
|
|
4410
4552
|
return null;
|
|
4411
4553
|
}
|
|
4412
4554
|
}
|
|
4413
|
-
function
|
|
4555
|
+
function clearPending(siteUuid, intent) {
|
|
4414
4556
|
try {
|
|
4415
|
-
rmSync(pendingPath(siteUuid));
|
|
4557
|
+
rmSync(pendingPath(siteUuid, intent));
|
|
4416
4558
|
} catch {
|
|
4417
4559
|
}
|
|
4418
4560
|
}
|
|
4419
|
-
async function
|
|
4561
|
+
async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
|
|
4420
4562
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4421
4563
|
const now = deps.now ?? (() => Date.now());
|
|
4422
4564
|
if (config.siteUuid === null) {
|
|
4423
|
-
return { status: "failed", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
|
|
4424
|
-
}
|
|
4425
|
-
const base = baseFrom(config.endpoint);
|
|
4426
|
-
const started = await fetchImpl(`${base}/device/code`, {
|
|
4427
|
-
method: "POST",
|
|
4428
|
-
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4429
|
-
body: JSON.stringify({ site_uuid: config.siteUuid })
|
|
4430
|
-
});
|
|
4431
|
-
if (started.status === 409) {
|
|
4432
4565
|
return {
|
|
4433
|
-
status: "
|
|
4434
|
-
message: "
|
|
4566
|
+
status: "failed",
|
|
4567
|
+
message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
|
|
4435
4568
|
};
|
|
4436
4569
|
}
|
|
4570
|
+
const base = baseFrom(config.endpoint);
|
|
4571
|
+
const started = await pulseFetch(
|
|
4572
|
+
config,
|
|
4573
|
+
`${base}/device/code`,
|
|
4574
|
+
{
|
|
4575
|
+
method: "POST",
|
|
4576
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4577
|
+
body: JSON.stringify({ site_uuid: config.siteUuid, intent })
|
|
4578
|
+
},
|
|
4579
|
+
fetchImpl
|
|
4580
|
+
);
|
|
4581
|
+
if (started.status === 409) return { status: "conflict", message: conflictMessage };
|
|
4437
4582
|
if (started.status === 404) {
|
|
4438
4583
|
return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
|
|
4439
4584
|
}
|
|
4440
4585
|
if (!started.ok) {
|
|
4441
|
-
return { status: "failed", message: `Could not start the
|
|
4586
|
+
return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
|
|
4442
4587
|
}
|
|
4443
4588
|
const body = await started.json();
|
|
4444
4589
|
const pending = {
|
|
4445
4590
|
deviceCode: body.device_code,
|
|
4446
4591
|
userCode: body.user_code,
|
|
4447
|
-
// Points at the API, which redirects to the dashboard SPA — the CLI only
|
|
4448
|
-
//
|
|
4592
|
+
// Points at the API, which redirects to the dashboard SPA — the CLI only knows the API origin,
|
|
4593
|
+
// and the approval page lives on the app.
|
|
4449
4594
|
verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
|
|
4450
4595
|
expiresAt: now() + body.expires_in * 1e3,
|
|
4451
4596
|
intervalMs: Math.max(1, body.interval) * 1e3
|
|
4452
4597
|
};
|
|
4453
|
-
|
|
4598
|
+
savePending(config.siteUuid, intent, pending);
|
|
4454
4599
|
return { status: "started", pending };
|
|
4455
4600
|
}
|
|
4456
|
-
async function
|
|
4601
|
+
async function pollDeviceToken(config, intent, pending, deps = {}) {
|
|
4457
4602
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4458
4603
|
const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
|
|
4459
4604
|
method: "POST",
|
|
4460
4605
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4461
4606
|
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
4462
4607
|
});
|
|
4463
|
-
if (polled.status === 428) return "pending";
|
|
4608
|
+
if (polled.status === 428) return { state: "pending" };
|
|
4464
4609
|
if (!polled.ok) {
|
|
4465
|
-
if (config.siteUuid !== null)
|
|
4466
|
-
return "expired";
|
|
4610
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4611
|
+
return { state: "expired" };
|
|
4467
4612
|
}
|
|
4468
|
-
const
|
|
4469
|
-
if (
|
|
4470
|
-
|
|
4471
|
-
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
4472
|
-
return "approved";
|
|
4613
|
+
const body = await polled.json();
|
|
4614
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4615
|
+
return { state: "approved", body };
|
|
4473
4616
|
}
|
|
4474
|
-
async function
|
|
4475
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4617
|
+
async function pollUntilApproved(config, intent, pending, deps = {}) {
|
|
4476
4618
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4477
4619
|
const now = deps.now ?? (() => Date.now());
|
|
4478
4620
|
const deadline = Math.min(pending.expiresAt, deps.until ?? pending.expiresAt);
|
|
4479
|
-
const expired = { status: "expired", message: "The login request expired. Run the command again." };
|
|
4480
4621
|
while (now() < deadline) {
|
|
4481
4622
|
await sleep(pending.intervalMs);
|
|
4482
|
-
const
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4623
|
+
const outcome = await pollDeviceToken(config, intent, pending, deps);
|
|
4624
|
+
if (outcome.state !== "pending") return outcome;
|
|
4625
|
+
}
|
|
4626
|
+
return { state: "expired" };
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// src/login.ts
|
|
4630
|
+
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.";
|
|
4631
|
+
function readPendingLogin(siteUuid) {
|
|
4632
|
+
return readPending(siteUuid, "login");
|
|
4633
|
+
}
|
|
4634
|
+
async function startLogin(config, deps = {}) {
|
|
4635
|
+
const started = await requestDeviceCode(config, "login", UNCLAIMED, deps);
|
|
4636
|
+
if (started.status === "conflict") return { status: "unclaimed", message: started.message };
|
|
4637
|
+
return started;
|
|
4638
|
+
}
|
|
4639
|
+
async function redeemIfApproved(config, pending, deps = {}) {
|
|
4640
|
+
const polled = await pollDeviceToken(config, "login", pending, deps);
|
|
4641
|
+
if (polled.state !== "approved") return polled.state;
|
|
4642
|
+
const apiKey = polled.body?.api_key;
|
|
4643
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) return "expired";
|
|
4644
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4645
|
+
return "approved";
|
|
4646
|
+
}
|
|
4647
|
+
async function waitForApproval(config, pending, deps = {}) {
|
|
4648
|
+
const polled = await pollUntilApproved(config, "login", pending, deps);
|
|
4649
|
+
if (polled.state === "expired") {
|
|
4650
|
+
return { status: "expired", message: "The login request expired. Run the command again." };
|
|
4651
|
+
}
|
|
4652
|
+
const apiKey = polled.body?.api_key;
|
|
4653
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) {
|
|
4654
|
+
return { status: "failed", message: "Patchstack approved the request but returned no credential." };
|
|
4499
4655
|
}
|
|
4500
|
-
|
|
4656
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4657
|
+
return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
|
|
4501
4658
|
}
|
|
4502
4659
|
async function login(config, onPrompt, deps = {}) {
|
|
4503
4660
|
const started = await startLogin(config, deps);
|
|
@@ -4508,16 +4665,546 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
4508
4665
|
return waitForApproval(config, started.pending, deps);
|
|
4509
4666
|
}
|
|
4510
4667
|
|
|
4668
|
+
// src/claim.ts
|
|
4669
|
+
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.";
|
|
4670
|
+
function readPendingClaim(siteUuid) {
|
|
4671
|
+
return readPending(siteUuid, "claim");
|
|
4672
|
+
}
|
|
4673
|
+
async function startClaim(config, deps = {}) {
|
|
4674
|
+
const started = await requestDeviceCode(config, "claim", ALREADY_CLAIMED, deps);
|
|
4675
|
+
if (started.status === "conflict") return { status: "already-claimed", message: started.message };
|
|
4676
|
+
return started;
|
|
4677
|
+
}
|
|
4678
|
+
async function settle(body) {
|
|
4679
|
+
const account = typeof body?.account === "string" ? body.account : void 0;
|
|
4680
|
+
const dashboardUrl = typeof body?.dashboard_url === "string" ? body.dashboard_url : void 0;
|
|
4681
|
+
const apiKey = body?.api_key;
|
|
4682
|
+
let credentialSaved = false;
|
|
4683
|
+
if (typeof apiKey === "string" && apiKey.length > 0) {
|
|
4684
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4685
|
+
credentialSaved = true;
|
|
4686
|
+
}
|
|
4687
|
+
return { status: "claimed", account, dashboardUrl, credentialSaved };
|
|
4688
|
+
}
|
|
4689
|
+
async function redeemClaimIfApproved(config, pending, deps = {}) {
|
|
4690
|
+
const polled = await pollDeviceToken(config, "claim", pending, deps);
|
|
4691
|
+
if (polled.state === "pending") return "pending";
|
|
4692
|
+
if (polled.state === "expired") {
|
|
4693
|
+
return { status: "expired", message: "The claim request expired. Run the command again." };
|
|
4694
|
+
}
|
|
4695
|
+
return settle(polled.body);
|
|
4696
|
+
}
|
|
4697
|
+
async function waitForClaim(config, pending, deps = {}) {
|
|
4698
|
+
const polled = await pollUntilApproved(config, "claim", pending, deps);
|
|
4699
|
+
if (polled.state === "expired") {
|
|
4700
|
+
return { status: "expired", message: "The claim request expired. Run the command again." };
|
|
4701
|
+
}
|
|
4702
|
+
return settle(polled.body);
|
|
4703
|
+
}
|
|
4704
|
+
async function claim2(config, onPrompt, deps = {}) {
|
|
4705
|
+
const started = await startClaim(config, deps);
|
|
4706
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
4707
|
+
return {
|
|
4708
|
+
status: started.status === "started" ? "failed" : started.status,
|
|
4709
|
+
message: started.message
|
|
4710
|
+
};
|
|
4711
|
+
}
|
|
4712
|
+
onPrompt(started.pending.userCode, started.pending.verificationUri);
|
|
4713
|
+
return waitForClaim(config, started.pending, deps);
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
// src/protect/install/runtime/check.ts
|
|
4717
|
+
import { relative as relative2 } from "path";
|
|
4718
|
+
|
|
4719
|
+
// src/protect/install/runtime/entry.ts
|
|
4720
|
+
import { existsSync as existsSync13, readFileSync as readFileSync8, realpathSync, statSync as statSync3 } from "fs";
|
|
4721
|
+
import { join as join13, resolve as resolve3, sep } from "path";
|
|
4722
|
+
var CONVENTIONAL = [
|
|
4723
|
+
"server.js",
|
|
4724
|
+
"server.mjs",
|
|
4725
|
+
"server.cjs",
|
|
4726
|
+
"app.js",
|
|
4727
|
+
"app.mjs",
|
|
4728
|
+
"app.cjs",
|
|
4729
|
+
"index.js",
|
|
4730
|
+
"index.mjs",
|
|
4731
|
+
"index.cjs",
|
|
4732
|
+
"src/server.js",
|
|
4733
|
+
"src/server.mjs",
|
|
4734
|
+
"src/server.cjs",
|
|
4735
|
+
"src/index.js",
|
|
4736
|
+
"src/index.mjs",
|
|
4737
|
+
"src/index.cjs",
|
|
4738
|
+
"dist/server.js",
|
|
4739
|
+
"dist/index.js",
|
|
4740
|
+
"dist/main.js",
|
|
4741
|
+
"build/index.js",
|
|
4742
|
+
"build/server.js"
|
|
4743
|
+
];
|
|
4744
|
+
var LOADABLE = /\.(?:js|mjs|cjs)$/;
|
|
4745
|
+
function readPackage2(cwd) {
|
|
4746
|
+
try {
|
|
4747
|
+
const parsed = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
|
|
4748
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
4749
|
+
} catch {
|
|
4750
|
+
return null;
|
|
4751
|
+
}
|
|
4752
|
+
}
|
|
4753
|
+
function isFile(path14) {
|
|
4754
|
+
try {
|
|
4755
|
+
return statSync3(path14).isFile();
|
|
4756
|
+
} catch {
|
|
4757
|
+
return false;
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
function loadable(cwd, candidate) {
|
|
4761
|
+
const written = resolve3(cwd, candidate);
|
|
4762
|
+
if (!LOADABLE.test(written) || !isFile(written)) return null;
|
|
4763
|
+
let root;
|
|
4764
|
+
let path14;
|
|
4765
|
+
try {
|
|
4766
|
+
root = realpathSync(cwd);
|
|
4767
|
+
path14 = realpathSync(written);
|
|
4768
|
+
} catch {
|
|
4769
|
+
return null;
|
|
4770
|
+
}
|
|
4771
|
+
if (path14 !== root && !path14.startsWith(root + sep)) return null;
|
|
4772
|
+
return written;
|
|
4773
|
+
}
|
|
4774
|
+
function passable(token) {
|
|
4775
|
+
if (CODE_LOADING_FLAGS.has(flagName(token))) return false;
|
|
4776
|
+
if (NO_OPERAND_FLAGS.has(token)) return true;
|
|
4777
|
+
return token.includes("=") && VALUED_FLAGS.has(flagName(token));
|
|
4778
|
+
}
|
|
4779
|
+
function fromScript(cwd, script) {
|
|
4780
|
+
if (/[&|;><()#$`'"*?\[\]{}\\~\r\n]/.test(script)) return null;
|
|
4781
|
+
const parts = script.trim().split(/\s+/);
|
|
4782
|
+
if (parts[0] !== "node") return null;
|
|
4783
|
+
const nodeArgs = [];
|
|
4784
|
+
let rest = parts.slice(1);
|
|
4785
|
+
while (rest.length > 0 && rest[0].startsWith("-")) {
|
|
4786
|
+
const token = rest[0];
|
|
4787
|
+
if (!passable(token)) return null;
|
|
4788
|
+
nodeArgs.push(token);
|
|
4789
|
+
rest = rest.slice(1);
|
|
4790
|
+
}
|
|
4791
|
+
if (rest.length === 0) return null;
|
|
4792
|
+
const file = loadable(cwd, rest[0]);
|
|
4793
|
+
return file === null ? null : { file, nodeArgs, appArgs: rest.slice(1) };
|
|
4794
|
+
}
|
|
4795
|
+
function resolveEntry(cwd) {
|
|
4796
|
+
const pkg = readPackage2(cwd);
|
|
4797
|
+
const scripts = pkg?.scripts ?? null;
|
|
4798
|
+
const declared = ["start", "serve"].map((name) => ({ name, script: scripts?.[name] })).find((entry) => typeof entry.script === "string" && entry.script.trim() !== "");
|
|
4799
|
+
if (declared !== void 0) {
|
|
4800
|
+
const resolved = fromScript(cwd, declared.script);
|
|
4801
|
+
return resolved === null ? {
|
|
4802
|
+
kind: "unavailable",
|
|
4803
|
+
reason: `the "${declared.name}" script is outside the bounded \`node <file>\` form this check can launch without the project's own toolchain`
|
|
4804
|
+
} : { kind: "entry", ...resolved, from: `the "${declared.name}" script` };
|
|
4805
|
+
}
|
|
4806
|
+
const main2 = pkg?.main;
|
|
4807
|
+
if (typeof main2 === "string") {
|
|
4808
|
+
const file = loadable(cwd, main2);
|
|
4809
|
+
if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: 'the package "main" field' };
|
|
4810
|
+
}
|
|
4811
|
+
for (const candidate of CONVENTIONAL) {
|
|
4812
|
+
const file = loadable(cwd, candidate);
|
|
4813
|
+
if (file !== null) return { kind: "entry", file, nodeArgs: [], appArgs: [], from: candidate };
|
|
4814
|
+
}
|
|
4815
|
+
return {
|
|
4816
|
+
kind: "unavailable",
|
|
4817
|
+
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"
|
|
4818
|
+
};
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// src/protect/install/runtime/probe.ts
|
|
4822
|
+
import { spawn } from "child_process";
|
|
4823
|
+
import { createHash as createHash3, randomBytes } from "crypto";
|
|
4824
|
+
import { existsSync as existsSync14 } from "fs";
|
|
4825
|
+
import { request as httpRequest } from "http";
|
|
4826
|
+
import { request as httpsRequest, Agent as HttpsAgent } from "https";
|
|
4827
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4828
|
+
var CHALLENGE_BYTES = 32;
|
|
4829
|
+
var VERIFY_HEADER_NAME = "x-patchstack-verify";
|
|
4830
|
+
var CHALLENGE_ENV = "PATCHSTACK_VERIFY_CHALLENGE";
|
|
4831
|
+
var SETTLE_MS = 1500;
|
|
4832
|
+
var RUN_MS = 2e4;
|
|
4833
|
+
var PROBE_MS = 5e3;
|
|
4834
|
+
var FREEZE_MS = 500;
|
|
4835
|
+
var OUTPUT_LIMIT = 4096;
|
|
4836
|
+
var REDACTION_OVERLAP = 256;
|
|
4837
|
+
var ANSWER_LIMIT = 8192;
|
|
4838
|
+
var LOOPBACK = ["127.0.0.1", "::1"];
|
|
4839
|
+
var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
4840
|
+
function expectedAnswerFor(challenge) {
|
|
4841
|
+
return createHash3("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
|
|
4842
|
+
}
|
|
4843
|
+
function preloadPath() {
|
|
4844
|
+
const candidates = [
|
|
4845
|
+
new URL("./protect/runtime/report-listeners.cjs", import.meta.url),
|
|
4846
|
+
// beside the built CLI
|
|
4847
|
+
new URL("./report-listeners.cjs", import.meta.url)
|
|
4848
|
+
// beside this file, in the repository
|
|
4849
|
+
];
|
|
4850
|
+
for (const candidate of candidates) {
|
|
4851
|
+
const path14 = fileURLToPath2(candidate);
|
|
4852
|
+
if (existsSync14(path14)) return path14;
|
|
4853
|
+
}
|
|
4854
|
+
return null;
|
|
4855
|
+
}
|
|
4856
|
+
function propagatedNodeOptions(preload, existing) {
|
|
4857
|
+
if (preload.includes('"')) {
|
|
4858
|
+
return {
|
|
4859
|
+
kind: "unavailable",
|
|
4860
|
+
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"
|
|
4861
|
+
};
|
|
4862
|
+
}
|
|
4863
|
+
const ours = `--require "${preload}"`;
|
|
4864
|
+
if (existing === void 0 || existing.trim() === "") return { kind: "options", value: ours };
|
|
4865
|
+
const verdict = classifyNodeOptions(existing);
|
|
4866
|
+
return verdict.kind === "refused" ? { kind: "unavailable", reason: verdict.why } : { kind: "options", value: `${ours} ${existing}` };
|
|
4867
|
+
}
|
|
4868
|
+
function keptOutput(chunks, challenge, answer) {
|
|
4869
|
+
const text = chunks.join("").split(challenge).join("<challenge>").split(answer).join("<answer>");
|
|
4870
|
+
return text.slice(0, OUTPUT_LIMIT);
|
|
4871
|
+
}
|
|
4872
|
+
async function askListener(listener, challenge, answer, budgetMs) {
|
|
4873
|
+
if (budgetMs <= 0) {
|
|
4874
|
+
return { ...listener, outcome: "unreachable", detail: "the run ran out of time before it could be asked" };
|
|
4875
|
+
}
|
|
4876
|
+
const send = listener.scheme === "https" ? httpsRequest : httpRequest;
|
|
4877
|
+
const options = {
|
|
4878
|
+
host: listener.host,
|
|
4879
|
+
port: listener.port,
|
|
4880
|
+
path: "/",
|
|
4881
|
+
method: "GET",
|
|
4882
|
+
headers: { [VERIFY_HEADER_NAME]: challenge },
|
|
4883
|
+
timeout: budgetMs
|
|
4884
|
+
};
|
|
4885
|
+
if (listener.scheme === "https") options.agent = new HttpsAgent({ rejectUnauthorized: false });
|
|
4886
|
+
return new Promise((resolve4) => {
|
|
4887
|
+
const req = send(options, (res) => {
|
|
4888
|
+
const body = [];
|
|
4889
|
+
let read2 = 0;
|
|
4890
|
+
res.on("data", (chunk) => {
|
|
4891
|
+
read2 += chunk.length;
|
|
4892
|
+
if (read2 <= ANSWER_LIMIT) {
|
|
4893
|
+
body.push(chunk);
|
|
4894
|
+
return;
|
|
4895
|
+
}
|
|
4896
|
+
res.destroy();
|
|
4897
|
+
});
|
|
4898
|
+
const finish = () => {
|
|
4899
|
+
const text = Buffer.concat(body).toString("utf8").trim();
|
|
4900
|
+
resolve4({
|
|
4901
|
+
...listener,
|
|
4902
|
+
outcome: text === answer ? "traversed" : "answered-without-sentinel",
|
|
4903
|
+
detail: text === answer ? void 0 : `answered with ${res.statusCode ?? "?"} and no sentinel`
|
|
4904
|
+
});
|
|
4905
|
+
};
|
|
4906
|
+
res.on("end", finish);
|
|
4907
|
+
res.on("close", finish);
|
|
4908
|
+
});
|
|
4909
|
+
req.on("timeout", () => req.destroy(new Error("timed out")));
|
|
4910
|
+
req.on("error", (err) => resolve4({ ...listener, outcome: "unreachable", detail: err.message }));
|
|
4911
|
+
req.end();
|
|
4912
|
+
});
|
|
4913
|
+
}
|
|
4914
|
+
function verdictOf(listeners) {
|
|
4915
|
+
if (listeners.length === 0 || listeners.some((l) => l.outcome === "unreachable")) return "unavailable";
|
|
4916
|
+
if (listeners.some((l) => l.outcome === "answered-without-sentinel")) return "not-traversed";
|
|
4917
|
+
return "proven";
|
|
4918
|
+
}
|
|
4919
|
+
function terminator(child) {
|
|
4920
|
+
let done = false;
|
|
4921
|
+
return () => {
|
|
4922
|
+
if (done) return;
|
|
4923
|
+
done = true;
|
|
4924
|
+
try {
|
|
4925
|
+
if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
|
|
4926
|
+
} catch {
|
|
4927
|
+
try {
|
|
4928
|
+
child.kill("SIGKILL");
|
|
4929
|
+
} catch {
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
};
|
|
4933
|
+
}
|
|
4934
|
+
async function probeRuntimeTraversal(options) {
|
|
4935
|
+
const platform = options.platform ?? process.platform;
|
|
4936
|
+
const challenge = randomBytes(CHALLENGE_BYTES).toString("hex");
|
|
4937
|
+
const answer = expectedAnswerFor(challenge);
|
|
4938
|
+
const nothing = (reason) => ({ outcome: "unavailable", reason, listeners: [], output: "" });
|
|
4939
|
+
if (platform === "win32") return nothing("process-tree cleanup is not implemented on Windows");
|
|
4940
|
+
const preload = options.preload ?? preloadPath();
|
|
4941
|
+
if (preload === null) return nothing("the listener reporter is missing from this installation");
|
|
4942
|
+
const deadline = Date.now() + (options.timeoutMs ?? RUN_MS);
|
|
4943
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
4944
|
+
const propagation = propagatedNodeOptions(preload, process.env.NODE_OPTIONS);
|
|
4945
|
+
if (propagation.kind === "unavailable") return nothing(propagation.reason);
|
|
4946
|
+
let detachMessages = () => {
|
|
4947
|
+
};
|
|
4948
|
+
const child = spawn(process.execPath, [...options.nodeArgs ?? [], "--require", preload, options.entry, ...options.appArgs ?? []], {
|
|
4949
|
+
cwd: options.cwd,
|
|
4950
|
+
detached: true,
|
|
4951
|
+
// its own process group, so cleanup has one stable target
|
|
4952
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
4953
|
+
// The app's own environment, plus the challenge and the propagated reporter. `NODE_ENV` is
|
|
4954
|
+
// deliberately left alone: choosing one here would change which code path the app takes, and the
|
|
4955
|
+
// point is to verify the app as it is.
|
|
4956
|
+
env: {
|
|
4957
|
+
...process.env,
|
|
4958
|
+
[CHALLENGE_ENV]: challenge,
|
|
4959
|
+
// Replaced rather than added to: what may be inherited was decided above, and the reporter is
|
|
4960
|
+
// first in the value that goes out.
|
|
4961
|
+
NODE_OPTIONS: propagation.value
|
|
4962
|
+
}
|
|
4963
|
+
});
|
|
4964
|
+
const terminate = terminator(child);
|
|
4965
|
+
const onSignal = (signal) => {
|
|
4966
|
+
terminate();
|
|
4967
|
+
detachHandlers();
|
|
4968
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
4969
|
+
};
|
|
4970
|
+
const onExit = () => terminate();
|
|
4971
|
+
const detachHandlers = () => {
|
|
4972
|
+
for (const signal of ENDING_SIGNALS) process.removeListener(signal, onSignal);
|
|
4973
|
+
process.removeListener("exit", onExit);
|
|
4974
|
+
};
|
|
4975
|
+
for (const signal of ENDING_SIGNALS) process.prependListener(signal, onSignal);
|
|
4976
|
+
process.on("exit", onExit);
|
|
4977
|
+
try {
|
|
4978
|
+
const chunks = [];
|
|
4979
|
+
let kept = 0;
|
|
4980
|
+
const drain = (stream) => {
|
|
4981
|
+
stream?.setEncoding("utf8");
|
|
4982
|
+
stream?.on("data", (chunk) => {
|
|
4983
|
+
if (kept < OUTPUT_LIMIT + REDACTION_OVERLAP) {
|
|
4984
|
+
chunks.push(chunk);
|
|
4985
|
+
kept += chunk.length;
|
|
4986
|
+
}
|
|
4987
|
+
});
|
|
4988
|
+
};
|
|
4989
|
+
drain(child.stdout);
|
|
4990
|
+
drain(child.stderr);
|
|
4991
|
+
const observed = [];
|
|
4992
|
+
const unsupported = [];
|
|
4993
|
+
const created = [];
|
|
4994
|
+
const workers = [];
|
|
4995
|
+
const late = [];
|
|
4996
|
+
const escaped = [];
|
|
4997
|
+
let loaded = false;
|
|
4998
|
+
let startFailure = null;
|
|
4999
|
+
let exitHow = null;
|
|
5000
|
+
let discovering = true;
|
|
5001
|
+
let onFrozen = null;
|
|
5002
|
+
const freeze = async () => {
|
|
5003
|
+
if (!child.connected) return false;
|
|
5004
|
+
return new Promise((resolve4) => {
|
|
5005
|
+
const settle2 = (confirmed2) => {
|
|
5006
|
+
clearTimeout(timer);
|
|
5007
|
+
onFrozen = null;
|
|
5008
|
+
child.removeListener("exit", onGone);
|
|
5009
|
+
resolve4(confirmed2);
|
|
5010
|
+
};
|
|
5011
|
+
const onGone = () => settle2(false);
|
|
5012
|
+
const timer = setTimeout(() => settle2(false), Math.min(FREEZE_MS, remaining()));
|
|
5013
|
+
onFrozen = () => settle2(true);
|
|
5014
|
+
child.once("exit", onGone);
|
|
5015
|
+
try {
|
|
5016
|
+
child.send({ patchstackVerify: "freeze" });
|
|
5017
|
+
} catch {
|
|
5018
|
+
settle2(false);
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
};
|
|
5022
|
+
await new Promise((resolve4) => {
|
|
5023
|
+
let settleTimer = null;
|
|
5024
|
+
const done = () => {
|
|
5025
|
+
discovering = false;
|
|
5026
|
+
clearTimeout(runTimer);
|
|
5027
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
5028
|
+
resolve4();
|
|
5029
|
+
};
|
|
5030
|
+
const runTimer = setTimeout(done, remaining());
|
|
5031
|
+
const settle2 = () => {
|
|
5032
|
+
if (settleTimer) clearTimeout(settleTimer);
|
|
5033
|
+
settleTimer = setTimeout(done, Math.min(options.settleMs ?? SETTLE_MS, remaining()));
|
|
5034
|
+
};
|
|
5035
|
+
const onMessage = (message) => {
|
|
5036
|
+
const note = message;
|
|
5037
|
+
if (note?.patchstackVerify === "frozen") {
|
|
5038
|
+
onFrozen?.();
|
|
5039
|
+
return;
|
|
5040
|
+
}
|
|
5041
|
+
if (note?.patchstackVerify === "process-created" && typeof note.escape === "string") escaped.push(`${note.what ?? "a process"} ${note.escape}`);
|
|
5042
|
+
if (note?.patchstackVerify === "worker-created" && typeof note.escape === "string") escaped.push(`a worker thread that ${note.escape}`);
|
|
5043
|
+
if (!discovering) {
|
|
5044
|
+
const kind = note?.patchstackVerify;
|
|
5045
|
+
if (kind === "listener") late.push(`an ${note.scheme === "https" ? "HTTPS" : "HTTP"} listener on port ${Number(note.port)}`);
|
|
5046
|
+
else if (kind === "unsupported-listener") late.push(`a listener this cannot probe (${note.why ?? "an unsupported transport"})`);
|
|
5047
|
+
else if (kind === "process-created") late.push(`another process (${note.what ?? "a process"} via ${note.via ?? "child_process"})`);
|
|
5048
|
+
else if (kind === "worker-created") late.push(`a worker thread (${note.what ?? "a worker"})`);
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
5051
|
+
if (note?.patchstackVerify === "listener" && (note.scheme === "http" || note.scheme === "https")) {
|
|
5052
|
+
observed.push({ scheme: note.scheme, host: String(note.host), port: Number(note.port) });
|
|
5053
|
+
settle2();
|
|
5054
|
+
}
|
|
5055
|
+
if (note?.patchstackVerify === "unsupported-listener") {
|
|
5056
|
+
unsupported.push(note.why ?? "an unsupported transport");
|
|
5057
|
+
done();
|
|
5058
|
+
}
|
|
5059
|
+
if (note?.patchstackVerify === "process-created") {
|
|
5060
|
+
created.push(
|
|
5061
|
+
`${note.what ?? "a process"} (via ${note.via ?? "child_process"})${note.node === true ? "" : ", which the listener reporter cannot reach"}`
|
|
5062
|
+
);
|
|
5063
|
+
done();
|
|
5064
|
+
}
|
|
5065
|
+
if (note?.patchstackVerify === "worker-created") {
|
|
5066
|
+
workers.push(note.what ?? "a worker");
|
|
5067
|
+
done();
|
|
5068
|
+
}
|
|
5069
|
+
if (note?.patchstackVerify === "ready") loaded = true;
|
|
5070
|
+
};
|
|
5071
|
+
child.on("message", onMessage);
|
|
5072
|
+
detachMessages = () => child.removeListener("message", onMessage);
|
|
5073
|
+
child.on("error", (err) => {
|
|
5074
|
+
startFailure = `the app could not be started (${err.message})`;
|
|
5075
|
+
done();
|
|
5076
|
+
});
|
|
5077
|
+
child.on("exit", (code, signal) => {
|
|
5078
|
+
exitHow = signal ?? `code ${code}`;
|
|
5079
|
+
done();
|
|
5080
|
+
});
|
|
5081
|
+
});
|
|
5082
|
+
const offLoopback = observed.filter((l) => !LOOPBACK.includes(l.host));
|
|
5083
|
+
const contained = unsupported.length === 0 && created.length === 0 && workers.length === 0 && offLoopback.length === 0;
|
|
5084
|
+
const listeners = contained ? await Promise.all(observed.map((l) => askListener(l, challenge, answer, Math.min(PROBE_MS, remaining())))) : [];
|
|
5085
|
+
const confirmed = await freeze();
|
|
5086
|
+
const output = keptOutput(chunks, challenge, answer);
|
|
5087
|
+
const pid = child.pid;
|
|
5088
|
+
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})`);
|
|
5089
|
+
if (escaped.length > 0) {
|
|
5090
|
+
return {
|
|
5091
|
+
outcome: "unavailable",
|
|
5092
|
+
reason: `the app tried to start something outside this run's one-process scope, and it was refused: ${escaped.join("; ")}`,
|
|
5093
|
+
listeners: [],
|
|
5094
|
+
output,
|
|
5095
|
+
pid
|
|
5096
|
+
};
|
|
5097
|
+
}
|
|
5098
|
+
if (unsupported.length > 0) {
|
|
5099
|
+
return { outcome: "unavailable", reason: `a listener this cannot probe: ${unsupported.join("; ")}`, listeners: [], output, pid };
|
|
5100
|
+
}
|
|
5101
|
+
if (created.length > 0) {
|
|
5102
|
+
return {
|
|
5103
|
+
outcome: "unavailable",
|
|
5104
|
+
reason: `the app attempted to start another process, so this run cannot answer for the whole app: ${created.join("; ")}`,
|
|
5105
|
+
listeners: [],
|
|
5106
|
+
output,
|
|
5107
|
+
pid
|
|
5108
|
+
};
|
|
5109
|
+
}
|
|
5110
|
+
if (workers.length > 0) {
|
|
5111
|
+
return {
|
|
5112
|
+
outcome: "unavailable",
|
|
5113
|
+
reason: `the app started a worker thread, whose listeners cannot report back to this run: ${workers.join("; ")}`,
|
|
5114
|
+
listeners: [],
|
|
5115
|
+
output,
|
|
5116
|
+
pid
|
|
5117
|
+
};
|
|
5118
|
+
}
|
|
5119
|
+
if (offLoopback.length > 0) {
|
|
5120
|
+
return {
|
|
5121
|
+
outcome: "unavailable",
|
|
5122
|
+
reason: `a listener bound an address this verification must not open: ${offLoopback.map((l) => l.host).join("; ")}`,
|
|
5123
|
+
listeners: [],
|
|
5124
|
+
output,
|
|
5125
|
+
pid
|
|
5126
|
+
};
|
|
5127
|
+
}
|
|
5128
|
+
if (late.length > 0) {
|
|
5129
|
+
return {
|
|
5130
|
+
outcome: "unavailable",
|
|
5131
|
+
reason: `the app was still opening things after discovery had ended, so this run cannot answer for the whole app: ${late.join("; ")}`,
|
|
5132
|
+
listeners: [],
|
|
5133
|
+
output,
|
|
5134
|
+
pid
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
if (observed.length === 0) {
|
|
5138
|
+
if (exitNote !== null) return { outcome: "unavailable", reason: exitNote, listeners: [], output, pid };
|
|
5139
|
+
if (!loaded) {
|
|
5140
|
+
return { outcome: "unavailable", reason: "the listener reporter did not load in the app process", listeners: [], output, pid };
|
|
5141
|
+
}
|
|
5142
|
+
return { outcome: "unavailable", reason: "no HTTP listener was observed while starting up", listeners: [], output, pid };
|
|
5143
|
+
}
|
|
5144
|
+
const unconfirmed = !confirmed && loaded && exitHow === null;
|
|
5145
|
+
const outcome = unconfirmed && verdictOf(listeners) === "proven" ? "unavailable" : verdictOf(listeners);
|
|
5146
|
+
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;
|
|
5147
|
+
return { outcome, reason, listeners, output, pid };
|
|
5148
|
+
} finally {
|
|
5149
|
+
terminate();
|
|
5150
|
+
detachHandlers();
|
|
5151
|
+
detachMessages();
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5155
|
+
// src/protect/install/runtime/check.ts
|
|
5156
|
+
async function runRuntimeCheck(cwd, options = {}) {
|
|
5157
|
+
const entry = resolveEntry(cwd);
|
|
5158
|
+
if (entry.kind === "unavailable") return { outcome: "unavailable", reason: entry.reason, listeners: [], output: "" };
|
|
5159
|
+
const result = await probeRuntimeTraversal({
|
|
5160
|
+
cwd,
|
|
5161
|
+
entry: entry.file,
|
|
5162
|
+
nodeArgs: entry.nodeArgs,
|
|
5163
|
+
appArgs: entry.appArgs,
|
|
5164
|
+
timeoutMs: options.timeoutMs,
|
|
5165
|
+
settleMs: options.settleMs,
|
|
5166
|
+
platform: options.platform
|
|
5167
|
+
});
|
|
5168
|
+
return { ...result, entry: { file: relative2(cwd, entry.file).replace(/\\/g, "/"), from: entry.from } };
|
|
5169
|
+
}
|
|
5170
|
+
function runtimeExitCode(report) {
|
|
5171
|
+
if (report.outcome === "proven") return 0;
|
|
5172
|
+
return report.outcome === "not-traversed" ? 1 : 2;
|
|
5173
|
+
}
|
|
5174
|
+
function formatRuntimeCheck(report) {
|
|
5175
|
+
const lines = ["runtime traversal (--runtime):"];
|
|
5176
|
+
if (report.entry) lines.push(` entry: ${report.entry.file} (from ${report.entry.from})`);
|
|
5177
|
+
for (const listener of report.listeners) {
|
|
5178
|
+
const mark = listener.outcome === "traversed" ? "\u2713" : listener.outcome === "answered-without-sentinel" ? "\u2717" : "?";
|
|
5179
|
+
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";
|
|
5180
|
+
const authority = listener.host.includes(":") ? `[${listener.host}]` : listener.host;
|
|
5181
|
+
lines.push(` ${mark} ${listener.scheme}://${authority}:${listener.port} \u2014 ${note}`);
|
|
5182
|
+
}
|
|
5183
|
+
if (report.outcome === "proven") {
|
|
5184
|
+
lines.push("runtime traversal reached the scaffolded guard seam \u2713");
|
|
5185
|
+
} else if (report.outcome === "not-traversed") {
|
|
5186
|
+
lines.push("runtime traversal did NOT reach the scaffolded guard seam \u2717");
|
|
5187
|
+
} else {
|
|
5188
|
+
lines.push(`? runtime traversal could not be established \u2014 ${report.reason ?? "no reason given"}`);
|
|
5189
|
+
lines.push(" This is not a failure. The structural checks above still stand on their own.");
|
|
5190
|
+
}
|
|
5191
|
+
if (report.outcome !== "proven" && report.output.trim() !== "") {
|
|
5192
|
+
lines.push(" the app printed:");
|
|
5193
|
+
for (const line of report.output.trim().split("\n").slice(0, 20)) lines.push(` ${line}`);
|
|
5194
|
+
}
|
|
5195
|
+
return lines;
|
|
5196
|
+
}
|
|
5197
|
+
|
|
4511
5198
|
// src/map-command.ts
|
|
4512
5199
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
4513
5200
|
|
|
4514
5201
|
// src/map/ts-loader.ts
|
|
4515
5202
|
import { createRequire } from "module";
|
|
4516
5203
|
import { pathToFileURL } from "url";
|
|
4517
|
-
import { join as
|
|
5204
|
+
import { join as join14 } from "path";
|
|
4518
5205
|
async function loadTypeScript(cwd) {
|
|
4519
5206
|
try {
|
|
4520
|
-
const req = createRequire(pathToFileURL(
|
|
5207
|
+
const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
|
|
4521
5208
|
const resolved = req.resolve("typescript");
|
|
4522
5209
|
const mod = await import(pathToFileURL(resolved).href);
|
|
4523
5210
|
return mod.default ?? mod;
|
|
@@ -4532,9 +5219,9 @@ async function loadTypeScript(cwd) {
|
|
|
4532
5219
|
}
|
|
4533
5220
|
|
|
4534
5221
|
// src/map/extract.ts
|
|
4535
|
-
import { readFileSync as
|
|
4536
|
-
import { createHash as
|
|
4537
|
-
import { relative as
|
|
5222
|
+
import { readFileSync as readFileSync13, realpathSync as realpathSync4 } from "fs";
|
|
5223
|
+
import { createHash as createHash5 } from "crypto";
|
|
5224
|
+
import { relative as relative5 } from "path";
|
|
4538
5225
|
|
|
4539
5226
|
// src/map/ast.ts
|
|
4540
5227
|
function rootIdentifier(node, ts) {
|
|
@@ -4788,8 +5475,8 @@ function npmPackageOf(spec) {
|
|
|
4788
5475
|
}
|
|
4789
5476
|
|
|
4790
5477
|
// src/map/sources.ts
|
|
4791
|
-
import { readFileSync as
|
|
4792
|
-
import { join as
|
|
5478
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
5479
|
+
import { join as join15, relative as relative3, isAbsolute } from "path";
|
|
4793
5480
|
|
|
4794
5481
|
// src/map/routes.ts
|
|
4795
5482
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -4873,7 +5560,7 @@ function hasEntrySignal(text) {
|
|
|
4873
5560
|
}
|
|
4874
5561
|
function detectFramework(cwd) {
|
|
4875
5562
|
try {
|
|
4876
|
-
const pkg = JSON.parse(
|
|
5563
|
+
const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
|
|
4877
5564
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4878
5565
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
4879
5566
|
if (d["next"]) return "next";
|
|
@@ -4885,11 +5572,11 @@ function detectFramework(cwd) {
|
|
|
4885
5572
|
} catch {
|
|
4886
5573
|
}
|
|
4887
5574
|
try {
|
|
4888
|
-
if (
|
|
5575
|
+
if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
4889
5576
|
} catch {
|
|
4890
5577
|
}
|
|
4891
5578
|
try {
|
|
4892
|
-
if (
|
|
5579
|
+
if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
4893
5580
|
} catch {
|
|
4894
5581
|
}
|
|
4895
5582
|
return "unknown";
|
|
@@ -4931,7 +5618,7 @@ function probeForSource(dir, budget) {
|
|
|
4931
5618
|
for (const e of entries) {
|
|
4932
5619
|
if (budget.left-- <= 0) return "unknown";
|
|
4933
5620
|
if (e.isDirectory()) {
|
|
4934
|
-
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(
|
|
5621
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join15(dir, e.name));
|
|
4935
5622
|
} else if (isSourceFile(e.name)) return true;
|
|
4936
5623
|
else if (e.isSymbolicLink()) return "unknown";
|
|
4937
5624
|
}
|
|
@@ -4944,7 +5631,7 @@ function probeForSource(dir, budget) {
|
|
|
4944
5631
|
function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
|
|
4945
5632
|
let key;
|
|
4946
5633
|
try {
|
|
4947
|
-
key =
|
|
5634
|
+
key = realpathSync2(dir);
|
|
4948
5635
|
} catch {
|
|
4949
5636
|
stats.unwalked++;
|
|
4950
5637
|
return out;
|
|
@@ -4968,19 +5655,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4968
5655
|
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4969
5656
|
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4970
5657
|
else if (probeable && e.isDirectory()) {
|
|
4971
|
-
const held = probeForSource(
|
|
4972
|
-
if (held === true) stats.skippedWithSource.push(
|
|
5658
|
+
const held = probeForSource(join15(dir, e.name), probeBudget);
|
|
5659
|
+
if (held === true) stats.skippedWithSource.push(join15(dir, e.name));
|
|
4973
5660
|
else if (held === "unknown") stats.skippedUnknown++;
|
|
4974
5661
|
}
|
|
4975
5662
|
continue;
|
|
4976
5663
|
}
|
|
4977
|
-
const full =
|
|
5664
|
+
const full = join15(dir, e.name);
|
|
4978
5665
|
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
4979
5666
|
else if (e.isSymbolicLink()) {
|
|
4980
5667
|
let st, real;
|
|
4981
5668
|
try {
|
|
4982
|
-
st =
|
|
4983
|
-
real =
|
|
5669
|
+
st = statSync4(full);
|
|
5670
|
+
real = realpathSync2(full);
|
|
4984
5671
|
} catch {
|
|
4985
5672
|
stats.unwalked++;
|
|
4986
5673
|
continue;
|
|
@@ -5003,7 +5690,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
5003
5690
|
}
|
|
5004
5691
|
function isInside(candidate, boundary) {
|
|
5005
5692
|
if (candidate === boundary) return true;
|
|
5006
|
-
const rel =
|
|
5693
|
+
const rel = relative3(boundary, candidate);
|
|
5007
5694
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
5008
5695
|
}
|
|
5009
5696
|
var DEPLOYMENT_SHAPES = [
|
|
@@ -5030,13 +5717,13 @@ var DEPLOYMENT_SHAPES = [
|
|
|
5030
5717
|
function detectDeploymentShapes(cwd, opts = {}) {
|
|
5031
5718
|
let boundary = opts.boundary ?? cwd;
|
|
5032
5719
|
try {
|
|
5033
|
-
boundary =
|
|
5720
|
+
boundary = realpathSync2(boundary);
|
|
5034
5721
|
} catch {
|
|
5035
5722
|
}
|
|
5036
|
-
const inProject = (
|
|
5723
|
+
const inProject = (path14) => {
|
|
5037
5724
|
if (opts.followOutside) return true;
|
|
5038
5725
|
try {
|
|
5039
|
-
return isInside(
|
|
5726
|
+
return isInside(realpathSync2(path14), boundary);
|
|
5040
5727
|
} catch {
|
|
5041
5728
|
return false;
|
|
5042
5729
|
}
|
|
@@ -5044,9 +5731,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5044
5731
|
const found = [];
|
|
5045
5732
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
5046
5733
|
for (const file of candidate.files ?? []) {
|
|
5047
|
-
const full =
|
|
5734
|
+
const full = join15(cwd, file);
|
|
5048
5735
|
try {
|
|
5049
|
-
if (
|
|
5736
|
+
if (statSync4(full).isFile() && inProject(full)) {
|
|
5050
5737
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
5051
5738
|
break;
|
|
5052
5739
|
}
|
|
@@ -5054,9 +5741,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5054
5741
|
}
|
|
5055
5742
|
}
|
|
5056
5743
|
for (const dir of candidate.dirs ?? []) {
|
|
5057
|
-
const full =
|
|
5744
|
+
const full = join15(cwd, dir);
|
|
5058
5745
|
try {
|
|
5059
|
-
if (!
|
|
5746
|
+
if (!statSync4(full).isDirectory() || !inProject(full)) continue;
|
|
5060
5747
|
} catch {
|
|
5061
5748
|
continue;
|
|
5062
5749
|
}
|
|
@@ -5073,7 +5760,7 @@ function holdsFunctionEntry(dir) {
|
|
|
5073
5760
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5074
5761
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
5075
5762
|
try {
|
|
5076
|
-
for (const nested of readdirSync5(
|
|
5763
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5077
5764
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
5078
5765
|
}
|
|
5079
5766
|
} catch {
|
|
@@ -5089,7 +5776,7 @@ function holdsSourceFile(dir) {
|
|
|
5089
5776
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
5090
5777
|
if (entry.isDirectory()) {
|
|
5091
5778
|
try {
|
|
5092
|
-
for (const nested of readdirSync5(
|
|
5779
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5093
5780
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
5094
5781
|
}
|
|
5095
5782
|
} catch {
|
|
@@ -5102,8 +5789,8 @@ function holdsSourceFile(dir) {
|
|
|
5102
5789
|
}
|
|
5103
5790
|
|
|
5104
5791
|
// src/map/surface.ts
|
|
5105
|
-
import { readFileSync as
|
|
5106
|
-
import { join as
|
|
5792
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
5793
|
+
import { join as join16 } from "path";
|
|
5107
5794
|
var STATIC_GENERATORS = [
|
|
5108
5795
|
{ dep: "vite", label: "vite" },
|
|
5109
5796
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -5141,7 +5828,7 @@ var SHAPE_SIGNAL = {
|
|
|
5141
5828
|
};
|
|
5142
5829
|
function readManifest(cwd) {
|
|
5143
5830
|
try {
|
|
5144
|
-
const parsed = JSON.parse(
|
|
5831
|
+
const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
|
|
5145
5832
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
5146
5833
|
} catch {
|
|
5147
5834
|
return null;
|
|
@@ -5154,7 +5841,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
5154
5841
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
5155
5842
|
let text;
|
|
5156
5843
|
try {
|
|
5157
|
-
text =
|
|
5844
|
+
text = readFileSync10(join16(cwd, file), "utf8");
|
|
5158
5845
|
} catch {
|
|
5159
5846
|
continue;
|
|
5160
5847
|
}
|
|
@@ -5282,7 +5969,7 @@ function surfaceNote(surface) {
|
|
|
5282
5969
|
}
|
|
5283
5970
|
|
|
5284
5971
|
// src/map/sinks.ts
|
|
5285
|
-
import { createHash as
|
|
5972
|
+
import { createHash as createHash4 } from "crypto";
|
|
5286
5973
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
5287
5974
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
5288
5975
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -5381,14 +6068,14 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
5381
6068
|
const root = base ? rootIdentifier(base, ts) : void 0;
|
|
5382
6069
|
if (!root) return {};
|
|
5383
6070
|
const spec = bindings.resolve(root);
|
|
5384
|
-
const
|
|
6071
|
+
const relative6 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
|
|
5385
6072
|
const pkg = npmPackageOf(spec);
|
|
5386
6073
|
if (pkg) return { pkg, root, spec };
|
|
5387
|
-
if (
|
|
6074
|
+
if (relative6 && ctx && spec) {
|
|
5388
6075
|
const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
|
|
5389
6076
|
if (viaModule) return { pkg: viaModule.package, root, spec };
|
|
5390
6077
|
}
|
|
5391
|
-
return { local: bindings.locals.has(root), root, spec, relative:
|
|
6078
|
+
return { local: bindings.locals.has(root), root, spec, relative: relative6 };
|
|
5392
6079
|
};
|
|
5393
6080
|
const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
|
|
5394
6081
|
const dbApi = (pkg, attribution) => {
|
|
@@ -5549,7 +6236,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
|
5549
6236
|
return table?.[index] ?? "unknown";
|
|
5550
6237
|
}
|
|
5551
6238
|
function sinkId(s, owner) {
|
|
5552
|
-
return
|
|
6239
|
+
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);
|
|
5553
6240
|
}
|
|
5554
6241
|
function dedupeSinks(sinks, owner) {
|
|
5555
6242
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5565,8 +6252,8 @@ function dedupeSinks(sinks, owner) {
|
|
|
5565
6252
|
}
|
|
5566
6253
|
|
|
5567
6254
|
// src/map/module-graph.ts
|
|
5568
|
-
import { readFileSync as
|
|
5569
|
-
import { dirname as dirname7, join as
|
|
6255
|
+
import { readFileSync as readFileSync11, realpathSync as realpathSync3, statSync as statSync5 } from "fs";
|
|
6256
|
+
import { dirname as dirname7, join as join17, relative as relative4, resolve as resolvePath } from "path";
|
|
5570
6257
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
5571
6258
|
function createModuleGraph(ts, opts) {
|
|
5572
6259
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5574,7 +6261,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5574
6261
|
if (cache.has(file)) return cache.get(file) ?? null;
|
|
5575
6262
|
let entry = null;
|
|
5576
6263
|
try {
|
|
5577
|
-
const text =
|
|
6264
|
+
const text = readFileSync11(file, "utf8");
|
|
5578
6265
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
5579
6266
|
const bindings = buildModuleBindings(sf, ts);
|
|
5580
6267
|
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
@@ -5590,7 +6277,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5590
6277
|
if (!opts.followOutside) {
|
|
5591
6278
|
let real = target;
|
|
5592
6279
|
try {
|
|
5593
|
-
real =
|
|
6280
|
+
real = realpathSync3(target);
|
|
5594
6281
|
} catch {
|
|
5595
6282
|
}
|
|
5596
6283
|
if (!isInside(real, opts.boundary)) return void 0;
|
|
@@ -5608,7 +6295,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5608
6295
|
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
5609
6296
|
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
5610
6297
|
}
|
|
5611
|
-
const rel =
|
|
6298
|
+
const rel = relative4(opts.cwd, target);
|
|
5612
6299
|
return collected.map((s) => ({ ...s, file: rel }));
|
|
5613
6300
|
},
|
|
5614
6301
|
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
@@ -5644,10 +6331,10 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
5644
6331
|
}
|
|
5645
6332
|
candidates.push(base);
|
|
5646
6333
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
5647
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
6334
|
+
for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
|
|
5648
6335
|
for (const c of candidates) {
|
|
5649
6336
|
try {
|
|
5650
|
-
if (
|
|
6337
|
+
if (statSync5(c).isFile()) return c;
|
|
5651
6338
|
} catch {
|
|
5652
6339
|
}
|
|
5653
6340
|
}
|
|
@@ -5673,8 +6360,8 @@ function collectCallees(sf, ts) {
|
|
|
5673
6360
|
}
|
|
5674
6361
|
|
|
5675
6362
|
// src/map/coordinates.ts
|
|
5676
|
-
function runtimeCoordinate(source,
|
|
5677
|
-
if (/\[\d*\]/.test(
|
|
6363
|
+
function runtimeCoordinate(source, path14) {
|
|
6364
|
+
if (/\[\d*\]/.test(path14)) {
|
|
5678
6365
|
return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
|
|
5679
6366
|
}
|
|
5680
6367
|
switch (source) {
|
|
@@ -5683,15 +6370,15 @@ function runtimeCoordinate(source, path13) {
|
|
|
5683
6370
|
case "multipart":
|
|
5684
6371
|
case "body":
|
|
5685
6372
|
case "server-fn-data":
|
|
5686
|
-
return { runtimeParameter: `post.${
|
|
6373
|
+
return { runtimeParameter: `post.${path14}` };
|
|
5687
6374
|
case "query":
|
|
5688
|
-
return { runtimeParameter: `get.${
|
|
6375
|
+
return { runtimeParameter: `get.${path14}` };
|
|
5689
6376
|
case "cookie":
|
|
5690
|
-
return { runtimeParameter: `cookie.${
|
|
6377
|
+
return { runtimeParameter: `cookie.${path14}` };
|
|
5691
6378
|
case "file":
|
|
5692
|
-
return { runtimeParameter: `files.${
|
|
6379
|
+
return { runtimeParameter: `files.${path14}` };
|
|
5693
6380
|
case "header":
|
|
5694
|
-
return { runtimeParameter: `server.HTTP_${
|
|
6381
|
+
return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
|
|
5695
6382
|
case "route-param":
|
|
5696
6383
|
return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
|
|
5697
6384
|
default:
|
|
@@ -5723,8 +6410,8 @@ function addressSpaceOf(source) {
|
|
|
5723
6410
|
function isProvenFlow(confidence) {
|
|
5724
6411
|
return confidence === "exact-local" || confidence === "transformed-local";
|
|
5725
6412
|
}
|
|
5726
|
-
function inputIdOf(source,
|
|
5727
|
-
return `${addressSpaceOf(source)}:${
|
|
6413
|
+
function inputIdOf(source, path14) {
|
|
6414
|
+
return `${addressSpaceOf(source)}:${path14}`;
|
|
5728
6415
|
}
|
|
5729
6416
|
function withCoordinates(fields, source) {
|
|
5730
6417
|
return fields.map((f) => {
|
|
@@ -5934,8 +6621,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
|
|
|
5934
6621
|
if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
|
|
5935
6622
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
5936
6623
|
const rootPath = /* @__PURE__ */ new Map();
|
|
5937
|
-
const addRoot = (name,
|
|
5938
|
-
if (!rootPath.has(name)) rootPath.set(name, { path:
|
|
6624
|
+
const addRoot = (name, path14, space) => {
|
|
6625
|
+
if (!rootPath.has(name)) rootPath.set(name, { path: path14, space });
|
|
5939
6626
|
};
|
|
5940
6627
|
for (const p of params ?? []) {
|
|
5941
6628
|
if (!p?.name) continue;
|
|
@@ -6080,8 +6767,8 @@ function dedupeLimitations(list) {
|
|
|
6080
6767
|
function join22(base, seg) {
|
|
6081
6768
|
return base ? `${base}.${seg}` : seg;
|
|
6082
6769
|
}
|
|
6083
|
-
function normalizePath(
|
|
6084
|
-
return
|
|
6770
|
+
function normalizePath(path14) {
|
|
6771
|
+
return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
|
|
6085
6772
|
}
|
|
6086
6773
|
function fluentChainCalls(call, ts) {
|
|
6087
6774
|
let root = call;
|
|
@@ -6327,8 +7014,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
6327
7014
|
}
|
|
6328
7015
|
|
|
6329
7016
|
// src/map/imports.ts
|
|
6330
|
-
import { readFileSync as
|
|
6331
|
-
import { join as
|
|
7017
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7018
|
+
import { join as join18 } from "path";
|
|
6332
7019
|
var MAX_SITES = 5;
|
|
6333
7020
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
6334
7021
|
function isNpmPackageName(pkg) {
|
|
@@ -6339,7 +7026,7 @@ function readPathAliases(cwd) {
|
|
|
6339
7026
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
6340
7027
|
let paths;
|
|
6341
7028
|
try {
|
|
6342
|
-
const raw =
|
|
7029
|
+
const raw = readFileSync12(join18(cwd, name), "utf8");
|
|
6343
7030
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
6344
7031
|
paths = parsed.compilerOptions?.paths;
|
|
6345
7032
|
} catch {
|
|
@@ -6699,7 +7386,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6699
7386
|
const failed = [];
|
|
6700
7387
|
let boundary = cwd;
|
|
6701
7388
|
try {
|
|
6702
|
-
boundary =
|
|
7389
|
+
boundary = realpathSync4(cwd);
|
|
6703
7390
|
} catch {
|
|
6704
7391
|
}
|
|
6705
7392
|
const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
|
|
@@ -6718,8 +7405,8 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6718
7405
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
6719
7406
|
for (const file of files) {
|
|
6720
7407
|
try {
|
|
6721
|
-
const text =
|
|
6722
|
-
const relFile =
|
|
7408
|
+
const text = readFileSync13(file, "utf8");
|
|
7409
|
+
const relFile = relative5(cwd, file);
|
|
6723
7410
|
sourceBytes += text.length;
|
|
6724
7411
|
if (!hasEntrySignal(text)) {
|
|
6725
7412
|
preFiltered++;
|
|
@@ -6731,7 +7418,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6731
7418
|
}
|
|
6732
7419
|
parsed++;
|
|
6733
7420
|
entryFiles.add(file);
|
|
6734
|
-
const fingerprint =
|
|
7421
|
+
const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
|
|
6735
7422
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
6736
7423
|
const bindings = buildModuleBindings(sf, ts);
|
|
6737
7424
|
for (const specifier of bindings.imports) {
|
|
@@ -6765,7 +7452,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6765
7452
|
}
|
|
6766
7453
|
} catch (e) {
|
|
6767
7454
|
if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
|
|
6768
|
-
failed.push(
|
|
7455
|
+
failed.push(relative5(cwd, file));
|
|
6769
7456
|
}
|
|
6770
7457
|
}
|
|
6771
7458
|
let hopParsed = 0;
|
|
@@ -6773,10 +7460,10 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6773
7460
|
for (const target of hopTargets) {
|
|
6774
7461
|
if (entryFiles.has(target)) continue;
|
|
6775
7462
|
try {
|
|
6776
|
-
const text =
|
|
7463
|
+
const text = readFileSync13(target, "utf8");
|
|
6777
7464
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
6778
7465
|
const bindings = buildModuleBindings(sf, ts);
|
|
6779
|
-
const called = collectInvocations(sf, ts, bindings, { file: target, owner:
|
|
7466
|
+
const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
|
|
6780
7467
|
invocations.add(called.invocations);
|
|
6781
7468
|
calls.total += called.counts.total;
|
|
6782
7469
|
calls.dependency += called.counts.dependency;
|
|
@@ -6808,7 +7495,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6808
7495
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
6809
7496
|
const importList = imports.list();
|
|
6810
7497
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6811
|
-
const skippedDirsWithSource = stats.skippedWithSource.map((d) =>
|
|
7498
|
+
const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
|
|
6812
7499
|
const importCoverageGaps = {
|
|
6813
7500
|
unreadableFiles: failed.length,
|
|
6814
7501
|
unscannableFiles: importScanFailures,
|
|
@@ -6996,8 +7683,8 @@ async function runMap(flags) {
|
|
|
6996
7683
|
}
|
|
6997
7684
|
|
|
6998
7685
|
// src/setup.ts
|
|
6999
|
-
import { readFileSync as
|
|
7000
|
-
import
|
|
7686
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
7687
|
+
import path13 from "path";
|
|
7001
7688
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
7002
7689
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
7003
7690
|
function setupProtection(cwd) {
|
|
@@ -7015,8 +7702,8 @@ function appendHook(existing, command) {
|
|
|
7015
7702
|
return `${existing} && ${command}`;
|
|
7016
7703
|
}
|
|
7017
7704
|
function wireBuildScripts(cwd, packageManager) {
|
|
7018
|
-
const target =
|
|
7019
|
-
const raw =
|
|
7705
|
+
const target = path13.join(cwd, "package.json");
|
|
7706
|
+
const raw = readFileSync14(target, "utf8");
|
|
7020
7707
|
const pkg = JSON.parse(raw);
|
|
7021
7708
|
const scripts = pkg.scripts ?? {};
|
|
7022
7709
|
const build = scripts.build;
|
|
@@ -7170,6 +7857,15 @@ Usage:
|
|
|
7170
7857
|
demonstrations, not production).
|
|
7171
7858
|
--check verifies the guard is wired (exit 1
|
|
7172
7859
|
if not) \u2014 for the wire-then-verify loop.
|
|
7860
|
+
It reads the app's source and never runs it.
|
|
7861
|
+
--check --runtime additionally STARTS THE APP
|
|
7862
|
+
on a loopback port and sends it one request,
|
|
7863
|
+
to establish that a request reaches the
|
|
7864
|
+
guard. Opt-in, and the only mode that runs
|
|
7865
|
+
the app. Exit 0 traversed, 1 a listener
|
|
7866
|
+
answered instead of the guard, 2 could not
|
|
7867
|
+
be established (e.g. a TypeScript or
|
|
7868
|
+
framework entry this must not start).
|
|
7173
7869
|
patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
|
|
7174
7870
|
walkthrough: verify the vulnerable package,
|
|
7175
7871
|
scan it, wait for live rule 18843, install +
|
|
@@ -7182,6 +7878,20 @@ Usage:
|
|
|
7182
7878
|
what's missing, with tailored commands), then
|
|
7183
7879
|
print the full setup guide. --full prints the
|
|
7184
7880
|
guide even when setup is complete
|
|
7881
|
+
patchstack-connect claim [--wait] Attach this site to a Patchstack account from
|
|
7882
|
+
the terminal, so its reports become visible.
|
|
7883
|
+
Prints a link for the user to open and sign in
|
|
7884
|
+
(or sign up) \u2014 whoever approves becomes the
|
|
7885
|
+
site's owner. In a terminal it then waits. When
|
|
7886
|
+
the output is piped or captured \u2014 an assistant
|
|
7887
|
+
running it \u2014 it prints the link and EXITS, so
|
|
7888
|
+
the link is visible immediately. Run it AGAIN
|
|
7889
|
+
once the user confirms: it resumes the same
|
|
7890
|
+
request rather than starting a new one. --wait
|
|
7891
|
+
blocks instead of returning. Does not rotate
|
|
7892
|
+
the site's credential. The same claim can be
|
|
7893
|
+
done by opening the dashboard link that scan
|
|
7894
|
+
and status print. Not usable in CI
|
|
7185
7895
|
patchstack-connect login [--wait] Recover this site's credential when
|
|
7186
7896
|
.patchstackrc.local.json has been lost. Needs the
|
|
7187
7897
|
site UUID, which lives in the committed
|
|
@@ -7214,6 +7924,10 @@ Options (for scan, setup, status, and uninstall):
|
|
|
7214
7924
|
same under a workspace that pins its own copy), so an
|
|
7215
7925
|
advisory can be matched to the copy your code actually
|
|
7216
7926
|
loads. Off by default; never source file paths
|
|
7927
|
+
--claim-token <token> (scan, setup) Connect the site to the Patchstack account
|
|
7928
|
+
that issued the token \u2014 it comes from the dashboard's
|
|
7929
|
+
"Connect website" prompt. PATCHSTACK_CLAIM_TOKEN works
|
|
7930
|
+
too. Never written to a file, never printed back
|
|
7217
7931
|
|
|
7218
7932
|
Options (for mark-build):
|
|
7219
7933
|
--dir <path> Build output directory (default: auto-detect
|
|
@@ -7247,7 +7961,7 @@ Examples:
|
|
|
7247
7961
|
npx @patchstack/connect demo node-serialize
|
|
7248
7962
|
npx @patchstack/connect demo-guide node-serialize
|
|
7249
7963
|
`;
|
|
7250
|
-
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out"]);
|
|
7964
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
|
|
7251
7965
|
function parseArgs(argv) {
|
|
7252
7966
|
const args = argv.slice(2);
|
|
7253
7967
|
const positional = [];
|
|
@@ -7295,6 +8009,107 @@ async function runInit(args) {
|
|
|
7295
8009
|
console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
|
|
7296
8010
|
return 0;
|
|
7297
8011
|
}
|
|
8012
|
+
async function runClaim(args) {
|
|
8013
|
+
if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
|
|
8014
|
+
console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
|
|
8015
|
+
return 1;
|
|
8016
|
+
}
|
|
8017
|
+
const config = await resolveConfig({
|
|
8018
|
+
cwd: process.cwd(),
|
|
8019
|
+
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
8020
|
+
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
8021
|
+
});
|
|
8022
|
+
const siteUuid = config.siteUuid;
|
|
8023
|
+
if (siteUuid === null) {
|
|
8024
|
+
console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
|
|
8025
|
+
return 1;
|
|
8026
|
+
}
|
|
8027
|
+
const settled = async (result2) => {
|
|
8028
|
+
console.log(
|
|
8029
|
+
`
|
|
8030
|
+
\u2713 Site claimed${result2.account !== void 0 ? ` by ${result2.account}` : ""}.`
|
|
8031
|
+
);
|
|
8032
|
+
console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
|
|
8033
|
+
if (result2.credentialSaved === true) {
|
|
8034
|
+
const ignore = await secretFileIgnored(process.cwd());
|
|
8035
|
+
console.log(` A credential for this site was issued and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
8036
|
+
console.log(
|
|
8037
|
+
ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
|
|
8038
|
+
);
|
|
8039
|
+
}
|
|
8040
|
+
console.log("");
|
|
8041
|
+
return 0;
|
|
8042
|
+
};
|
|
8043
|
+
const prompt = (userCode, verificationUri) => {
|
|
8044
|
+
console.log(`
|
|
8045
|
+
Your code: ${userCode}`);
|
|
8046
|
+
console.log(` Claim at: ${verificationUri}
|
|
8047
|
+
`);
|
|
8048
|
+
console.log(" Open that link and sign in to Patchstack \u2014 or create an account \u2014 to attach");
|
|
8049
|
+
console.log(" this site to it. Whoever approves becomes the site's owner.\n");
|
|
8050
|
+
};
|
|
8051
|
+
if (args.flags.has("wait")) {
|
|
8052
|
+
const pending = readPendingClaim(siteUuid);
|
|
8053
|
+
if (pending === null) {
|
|
8054
|
+
console.error("\n No claim is waiting for approval. Run `patchstack-connect claim` first.\n");
|
|
8055
|
+
return 1;
|
|
8056
|
+
}
|
|
8057
|
+
const resumed = await waitForClaim(config, pending);
|
|
8058
|
+
if (resumed.status === "claimed") return await settled(resumed);
|
|
8059
|
+
console.error(`
|
|
8060
|
+
${resumed.message ?? "Claim failed."}
|
|
8061
|
+
`);
|
|
8062
|
+
return 1;
|
|
8063
|
+
}
|
|
8064
|
+
if (process.stdout.isTTY !== true) {
|
|
8065
|
+
const existing = readPendingClaim(siteUuid);
|
|
8066
|
+
if (existing !== null && Date.now() < existing.expiresAt) {
|
|
8067
|
+
const outcome = await redeemClaimIfApproved(config, existing);
|
|
8068
|
+
if (outcome !== "pending" && outcome.status === "claimed") return await settled(outcome);
|
|
8069
|
+
if (outcome === "pending") {
|
|
8070
|
+
const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
|
|
8071
|
+
console.log(`
|
|
8072
|
+
Still waiting for the site to be claimed with code ${existing.userCode}.`);
|
|
8073
|
+
console.log(` Claim at: ${existing.verificationUri}`);
|
|
8074
|
+
console.log(` (valid for another ${secondsLeft}s \u2014 run this again once the user confirms)
|
|
8075
|
+
`);
|
|
8076
|
+
return 0;
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
const started = await startClaim(config);
|
|
8080
|
+
if (started.status !== "started" || started.pending === void 0) {
|
|
8081
|
+
if (started.status === "already-claimed") {
|
|
8082
|
+
console.log(`
|
|
8083
|
+
${started.message ?? "This site is already claimed."}
|
|
8084
|
+
`);
|
|
8085
|
+
return 0;
|
|
8086
|
+
}
|
|
8087
|
+
console.error(`
|
|
8088
|
+
${started.message ?? "Claim failed."}
|
|
8089
|
+
`);
|
|
8090
|
+
return 1;
|
|
8091
|
+
}
|
|
8092
|
+
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
8093
|
+
console.log(" Give that link to the user. When they confirm they have claimed the site, run");
|
|
8094
|
+
console.log(" this same command again (or `claim --wait` to block until they do).\n");
|
|
8095
|
+
return 0;
|
|
8096
|
+
}
|
|
8097
|
+
const result = await claim2(config, (userCode, verificationUri) => {
|
|
8098
|
+
prompt(userCode, verificationUri);
|
|
8099
|
+
console.log(" Waiting (the code expires in 10 minutes)\u2026");
|
|
8100
|
+
});
|
|
8101
|
+
if (result.status === "claimed") return await settled(result);
|
|
8102
|
+
if (result.status === "already-claimed") {
|
|
8103
|
+
console.log(`
|
|
8104
|
+
${result.message ?? "This site is already claimed."}
|
|
8105
|
+
`);
|
|
8106
|
+
return 0;
|
|
8107
|
+
}
|
|
8108
|
+
console.error(`
|
|
8109
|
+
${result.message ?? "Claim failed."}
|
|
8110
|
+
`);
|
|
8111
|
+
return 1;
|
|
8112
|
+
}
|
|
7298
8113
|
async function runLogin(args) {
|
|
7299
8114
|
if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
|
|
7300
8115
|
console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
|
|
@@ -7381,6 +8196,7 @@ async function runScan(args, options = {}) {
|
|
|
7381
8196
|
cwd: process.cwd(),
|
|
7382
8197
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
7383
8198
|
cliEndpoint: getStringFlag(args.flags, "endpoint"),
|
|
8199
|
+
cliClaimToken: getStringFlag(args.flags, "claim-token"),
|
|
7384
8200
|
// The one command that reports them, so the one command that resolves them.
|
|
7385
8201
|
detectSiteIdentity: true
|
|
7386
8202
|
});
|
|
@@ -7421,6 +8237,9 @@ async function runScan(args, options = {}) {
|
|
|
7421
8237
|
if (typeof body.name === "string") {
|
|
7422
8238
|
console.log(`Reporting this app's name as "${body.name}".`);
|
|
7423
8239
|
}
|
|
8240
|
+
if (typeof config.claimToken === "string" && config.claimToken !== "") {
|
|
8241
|
+
console.log("A claim token is set: the site will be connected to the Patchstack account that issued it.");
|
|
8242
|
+
}
|
|
7424
8243
|
if (dryRun) {
|
|
7425
8244
|
console.log("");
|
|
7426
8245
|
if (config.siteUuid === null) {
|
|
@@ -7475,17 +8294,27 @@ async function runScan(args, options = {}) {
|
|
|
7475
8294
|
} else {
|
|
7476
8295
|
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
7477
8296
|
}
|
|
8297
|
+
const claimLines = claimOutcomeLines(response.claim, config);
|
|
8298
|
+
if (claimLines.length > 0) {
|
|
8299
|
+
console.log("");
|
|
8300
|
+
for (const line of claimLines) console.log(line);
|
|
8301
|
+
}
|
|
8302
|
+
const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
|
|
7478
8303
|
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
7479
8304
|
if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
|
|
7480
8305
|
reportSourceWidget(effectiveUuid);
|
|
7481
8306
|
}
|
|
7482
|
-
|
|
8307
|
+
const linkUuid = response.uuid ?? config.siteUuid;
|
|
8308
|
+
if (!connected && (provisioning || claimLines.length > 0) && linkUuid !== null && linkUuid !== void 0 && linkUuid.length > 0) {
|
|
7483
8309
|
console.log("");
|
|
7484
8310
|
console.log("Open this dashboard link to view vulnerability reports:");
|
|
7485
|
-
console.log(` ${buildClaimUrl(config.endpoint,
|
|
8311
|
+
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
7486
8312
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
7487
8313
|
console.log(" (this URL inherits the endpoint override above)");
|
|
7488
8314
|
}
|
|
8315
|
+
console.log("");
|
|
8316
|
+
console.log("Or attach it to your account from this terminal:");
|
|
8317
|
+
console.log(" npx @patchstack/connect claim");
|
|
7489
8318
|
}
|
|
7490
8319
|
if (options.showRemainingSetup !== false) {
|
|
7491
8320
|
try {
|
|
@@ -7573,6 +8402,11 @@ function reportSourceMarker(framework) {
|
|
|
7573
8402
|
}
|
|
7574
8403
|
}
|
|
7575
8404
|
async function runProtectCommand(args) {
|
|
8405
|
+
const runtime = args.flags.get("runtime") === true;
|
|
8406
|
+
if (runtime && args.flags.get("check") !== true) {
|
|
8407
|
+
console.error("patchstack protect: --runtime only applies to --check. Run `protect --check --runtime`.");
|
|
8408
|
+
return 1;
|
|
8409
|
+
}
|
|
7576
8410
|
if (args.flags.get("check") === true) {
|
|
7577
8411
|
const report = runVerify(process.cwd());
|
|
7578
8412
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
@@ -7591,7 +8425,12 @@ async function runProtectCommand(args) {
|
|
|
7591
8425
|
if (report.checks.some((c) => c.unverifiable)) {
|
|
7592
8426
|
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7593
8427
|
}
|
|
7594
|
-
|
|
8428
|
+
if (!report.wired) return 1;
|
|
8429
|
+
if (!runtime) return 0;
|
|
8430
|
+
console.log("");
|
|
8431
|
+
const runtimeReport = await runRuntimeCheck(process.cwd());
|
|
8432
|
+
for (const line2 of formatRuntimeCheck(runtimeReport)) console.log(line2);
|
|
8433
|
+
return runtimeExitCode(runtimeReport);
|
|
7595
8434
|
}
|
|
7596
8435
|
const demo = args.flags.get("demo") === true;
|
|
7597
8436
|
try {
|
|
@@ -7714,7 +8553,7 @@ async function runGuide(args) {
|
|
|
7714
8553
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
7715
8554
|
console.log("");
|
|
7716
8555
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
7717
|
-
console.log(
|
|
8556
|
+
console.log(readFileSync15(guidePath, "utf8"));
|
|
7718
8557
|
return 0;
|
|
7719
8558
|
}
|
|
7720
8559
|
async function runSetup(args) {
|
|
@@ -7788,6 +8627,7 @@ async function runStatus(args) {
|
|
|
7788
8627
|
console.log(`Environment: ${config.environment}`);
|
|
7789
8628
|
if (config.siteUuid !== null) {
|
|
7790
8629
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
8630
|
+
console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
|
|
7791
8631
|
switch (await fetchSiteStatus(config)) {
|
|
7792
8632
|
case "active":
|
|
7793
8633
|
console.log("Site status: active on Patchstack");
|
|
@@ -7907,7 +8747,7 @@ async function runMarkBuild(args) {
|
|
|
7907
8747
|
let marked = 0;
|
|
7908
8748
|
let widgetTouched = 0;
|
|
7909
8749
|
for (const file of files) {
|
|
7910
|
-
const before =
|
|
8750
|
+
const before = readFileSync15(file, "utf8");
|
|
7911
8751
|
let after = injectMarker(before, snippet);
|
|
7912
8752
|
if (widgetUuid !== null) {
|
|
7913
8753
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -7969,6 +8809,8 @@ async function main() {
|
|
|
7969
8809
|
return runSetup(args);
|
|
7970
8810
|
case "map":
|
|
7971
8811
|
return runMap(args.flags);
|
|
8812
|
+
case "claim":
|
|
8813
|
+
return runClaim(args);
|
|
7972
8814
|
case "login":
|
|
7973
8815
|
return runLogin(args);
|
|
7974
8816
|
default:
|