@patchstack/connect 0.5.1 → 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 +91 -1
- package/README.md +88 -0
- package/dist/cli.js +950 -145
- package/dist/cli.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 +33 -2
- 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 +30 -1
- package/dist/protect.js.map +1 -1
- package/package.json +1 -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();
|
|
@@ -1295,16 +1295,16 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
|
1295
1295
|
function claimTokenHeader(config) {
|
|
1296
1296
|
return typeof config.claimToken === "string" && config.claimToken !== "" ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1297
1297
|
}
|
|
1298
|
-
function claimOutcomeLines(
|
|
1298
|
+
function claimOutcomeLines(claim3, config) {
|
|
1299
1299
|
if (typeof config.claimToken !== "string" || config.claimToken === "") return [];
|
|
1300
|
-
if (
|
|
1301
|
-
const dashboard = typeof
|
|
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
1302
|
return [
|
|
1303
|
-
|
|
1303
|
+
claim3.state === "claimed" ? "Connected to your Patchstack account." : "This site is already connected to your Patchstack account.",
|
|
1304
1304
|
...dashboard
|
|
1305
1305
|
];
|
|
1306
1306
|
}
|
|
1307
|
-
const why =
|
|
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
1308
|
return [
|
|
1309
1309
|
`Not connected to your account: ${why}.`,
|
|
1310
1310
|
"Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."
|
|
@@ -1329,8 +1329,8 @@ function authFailureMessage(status, config) {
|
|
|
1329
1329
|
}
|
|
1330
1330
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1331
1331
|
const url = new URL(manifestEndpoint);
|
|
1332
|
-
const
|
|
1333
|
-
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)}`;
|
|
1334
1334
|
url.search = "";
|
|
1335
1335
|
url.hash = "";
|
|
1336
1336
|
return url.toString();
|
|
@@ -1345,16 +1345,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
|
|
|
1345
1345
|
}
|
|
1346
1346
|
function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
|
|
1347
1347
|
const url = new URL(manifestEndpoint);
|
|
1348
|
-
const
|
|
1349
|
-
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)}`;
|
|
1350
1350
|
url.search = "";
|
|
1351
1351
|
url.hash = "";
|
|
1352
1352
|
return url.toString();
|
|
1353
1353
|
}
|
|
1354
1354
|
function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
1355
1355
|
const url = new URL(manifestEndpoint);
|
|
1356
|
-
const
|
|
1357
|
-
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)}`;
|
|
1358
1358
|
url.search = "";
|
|
1359
1359
|
url.hash = "";
|
|
1360
1360
|
return url.toString();
|
|
@@ -1690,7 +1690,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
|
|
|
1690
1690
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
1691
1691
|
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
1692
1692
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1693
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
1693
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve4) => setTimeout(resolve4, ms)));
|
|
1694
1694
|
const now = options.now ?? Date.now;
|
|
1695
1695
|
const started = now();
|
|
1696
1696
|
const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
|
|
@@ -3011,6 +3011,112 @@ import { join as join10, dirname as dirname5 } from "path";
|
|
|
3011
3011
|
import { execFileSync } from "child_process";
|
|
3012
3012
|
import { existsSync as existsSync7 } from "fs";
|
|
3013
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
|
|
3014
3120
|
var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
|
|
3015
3121
|
function lastTopLevelImportLine(source) {
|
|
3016
3122
|
const lines = source.split("\n");
|
|
@@ -3089,7 +3195,10 @@ function advanceDepth(line, inBlockComment, add) {
|
|
|
3089
3195
|
function parses(filePath) {
|
|
3090
3196
|
if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
|
|
3091
3197
|
try {
|
|
3092
|
-
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
|
+
});
|
|
3093
3202
|
return true;
|
|
3094
3203
|
} catch (error) {
|
|
3095
3204
|
return isSyntaxError(error) ? false : null;
|
|
@@ -4358,12 +4467,15 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4358
4467
|
lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
|
|
4359
4468
|
}
|
|
4360
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"));
|
|
4361
4471
|
}
|
|
4362
4472
|
lines.push("");
|
|
4363
4473
|
if (state.claimUrl !== null) {
|
|
4364
4474
|
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
|
|
4365
4475
|
lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
|
|
4366
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)"));
|
|
4367
4479
|
if (state.endpointOverride !== null) {
|
|
4368
4480
|
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
4369
4481
|
}
|
|
@@ -4406,7 +4518,12 @@ function renderGuideChecklist(state, useColor) {
|
|
|
4406
4518
|
}
|
|
4407
4519
|
|
|
4408
4520
|
// src/login.ts
|
|
4409
|
-
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";
|
|
4410
4527
|
import { tmpdir } from "os";
|
|
4411
4528
|
import path11 from "path";
|
|
4412
4529
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -4418,108 +4535,126 @@ function baseFrom(manifestEndpoint) {
|
|
|
4418
4535
|
url.hash = "";
|
|
4419
4536
|
return url.toString().replace(/\/$/, "");
|
|
4420
4537
|
}
|
|
4421
|
-
function pendingPath(siteUuid) {
|
|
4538
|
+
function pendingPath(siteUuid, intent) {
|
|
4422
4539
|
const key = createHash2("sha256").update(siteUuid).digest("hex").slice(0, 16);
|
|
4423
|
-
return path11.join(tmpdir(), `patchstack
|
|
4540
|
+
return path11.join(tmpdir(), `patchstack-${intent}-${key}.json`);
|
|
4424
4541
|
}
|
|
4425
|
-
function
|
|
4426
|
-
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
|
+
});
|
|
4427
4547
|
}
|
|
4428
|
-
function
|
|
4548
|
+
function readPending(siteUuid, intent) {
|
|
4429
4549
|
try {
|
|
4430
|
-
return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
|
|
4550
|
+
return JSON.parse(readFileSync7(pendingPath(siteUuid, intent), "utf8"));
|
|
4431
4551
|
} catch {
|
|
4432
4552
|
return null;
|
|
4433
4553
|
}
|
|
4434
4554
|
}
|
|
4435
|
-
function
|
|
4555
|
+
function clearPending(siteUuid, intent) {
|
|
4436
4556
|
try {
|
|
4437
|
-
rmSync(pendingPath(siteUuid));
|
|
4557
|
+
rmSync(pendingPath(siteUuid, intent));
|
|
4438
4558
|
} catch {
|
|
4439
4559
|
}
|
|
4440
4560
|
}
|
|
4441
|
-
async function
|
|
4561
|
+
async function requestDeviceCode(config, intent, conflictMessage, deps = {}) {
|
|
4442
4562
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4443
4563
|
const now = deps.now ?? (() => Date.now());
|
|
4444
4564
|
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
4565
|
return {
|
|
4455
|
-
status: "
|
|
4456
|
-
message: "
|
|
4566
|
+
status: "failed",
|
|
4567
|
+
message: "No site UUID configured \u2014 run `patchstack-connect scan` first."
|
|
4457
4568
|
};
|
|
4458
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 };
|
|
4459
4582
|
if (started.status === 404) {
|
|
4460
4583
|
return { status: "not-found", message: "Patchstack does not recognise this site UUID." };
|
|
4461
4584
|
}
|
|
4462
4585
|
if (!started.ok) {
|
|
4463
|
-
return { status: "failed", message: `Could not start the
|
|
4586
|
+
return { status: "failed", message: `Could not start the request (HTTP ${started.status}).` };
|
|
4464
4587
|
}
|
|
4465
4588
|
const body = await started.json();
|
|
4466
4589
|
const pending = {
|
|
4467
4590
|
deviceCode: body.device_code,
|
|
4468
4591
|
userCode: body.user_code,
|
|
4469
|
-
// Points at the API, which redirects to the dashboard SPA — the CLI only
|
|
4470
|
-
//
|
|
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.
|
|
4471
4594
|
verificationUri: `${base}/device?code=${encodeURIComponent(body.user_code)}`,
|
|
4472
4595
|
expiresAt: now() + body.expires_in * 1e3,
|
|
4473
4596
|
intervalMs: Math.max(1, body.interval) * 1e3
|
|
4474
4597
|
};
|
|
4475
|
-
|
|
4598
|
+
savePending(config.siteUuid, intent, pending);
|
|
4476
4599
|
return { status: "started", pending };
|
|
4477
4600
|
}
|
|
4478
|
-
async function
|
|
4601
|
+
async function pollDeviceToken(config, intent, pending, deps = {}) {
|
|
4479
4602
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4480
4603
|
const polled = await fetchImpl(`${baseFrom(config.endpoint)}/device/token`, {
|
|
4481
4604
|
method: "POST",
|
|
4482
4605
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
4483
4606
|
body: JSON.stringify({ device_code: pending.deviceCode })
|
|
4484
4607
|
});
|
|
4485
|
-
if (polled.status === 428) return "pending";
|
|
4608
|
+
if (polled.status === 428) return { state: "pending" };
|
|
4486
4609
|
if (!polled.ok) {
|
|
4487
|
-
if (config.siteUuid !== null)
|
|
4488
|
-
return "expired";
|
|
4610
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4611
|
+
return { state: "expired" };
|
|
4489
4612
|
}
|
|
4490
|
-
const
|
|
4491
|
-
if (
|
|
4492
|
-
|
|
4493
|
-
if (config.siteUuid !== null) clearPendingLogin(config.siteUuid);
|
|
4494
|
-
return "approved";
|
|
4613
|
+
const body = await polled.json();
|
|
4614
|
+
if (config.siteUuid !== null) clearPending(config.siteUuid, intent);
|
|
4615
|
+
return { state: "approved", body };
|
|
4495
4616
|
}
|
|
4496
|
-
async function
|
|
4497
|
-
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
4617
|
+
async function pollUntilApproved(config, intent, pending, deps = {}) {
|
|
4498
4618
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
4499
4619
|
const now = deps.now ?? (() => Date.now());
|
|
4500
4620
|
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
4621
|
while (now() < deadline) {
|
|
4503
4622
|
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 };
|
|
4623
|
+
const outcome = await pollDeviceToken(config, intent, pending, deps);
|
|
4624
|
+
if (outcome.state !== "pending") return outcome;
|
|
4521
4625
|
}
|
|
4522
|
-
return expired;
|
|
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." };
|
|
4655
|
+
}
|
|
4656
|
+
await persistApiKey(process.cwd(), apiKey);
|
|
4657
|
+
return { status: "approved", userCode: pending.userCode, verificationUri: pending.verificationUri };
|
|
4523
4658
|
}
|
|
4524
4659
|
async function login(config, onPrompt, deps = {}) {
|
|
4525
4660
|
const started = await startLogin(config, deps);
|
|
@@ -4530,16 +4665,546 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
4530
4665
|
return waitForApproval(config, started.pending, deps);
|
|
4531
4666
|
}
|
|
4532
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
|
+
|
|
4533
5198
|
// src/map-command.ts
|
|
4534
5199
|
import { writeFileSync as writeFileSync8 } from "fs";
|
|
4535
5200
|
|
|
4536
5201
|
// src/map/ts-loader.ts
|
|
4537
5202
|
import { createRequire } from "module";
|
|
4538
5203
|
import { pathToFileURL } from "url";
|
|
4539
|
-
import { join as
|
|
5204
|
+
import { join as join14 } from "path";
|
|
4540
5205
|
async function loadTypeScript(cwd) {
|
|
4541
5206
|
try {
|
|
4542
|
-
const req = createRequire(pathToFileURL(
|
|
5207
|
+
const req = createRequire(pathToFileURL(join14(cwd, "package.json")));
|
|
4543
5208
|
const resolved = req.resolve("typescript");
|
|
4544
5209
|
const mod = await import(pathToFileURL(resolved).href);
|
|
4545
5210
|
return mod.default ?? mod;
|
|
@@ -4554,9 +5219,9 @@ async function loadTypeScript(cwd) {
|
|
|
4554
5219
|
}
|
|
4555
5220
|
|
|
4556
5221
|
// src/map/extract.ts
|
|
4557
|
-
import { readFileSync as
|
|
4558
|
-
import { createHash as
|
|
4559
|
-
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";
|
|
4560
5225
|
|
|
4561
5226
|
// src/map/ast.ts
|
|
4562
5227
|
function rootIdentifier(node, ts) {
|
|
@@ -4810,8 +5475,8 @@ function npmPackageOf(spec) {
|
|
|
4810
5475
|
}
|
|
4811
5476
|
|
|
4812
5477
|
// src/map/sources.ts
|
|
4813
|
-
import { readFileSync as
|
|
4814
|
-
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";
|
|
4815
5480
|
|
|
4816
5481
|
// src/map/routes.ts
|
|
4817
5482
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -4895,7 +5560,7 @@ function hasEntrySignal(text) {
|
|
|
4895
5560
|
}
|
|
4896
5561
|
function detectFramework(cwd) {
|
|
4897
5562
|
try {
|
|
4898
|
-
const pkg = JSON.parse(
|
|
5563
|
+
const pkg = JSON.parse(readFileSync9(join15(cwd, "package.json"), "utf8"));
|
|
4899
5564
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4900
5565
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
4901
5566
|
if (d["next"]) return "next";
|
|
@@ -4907,11 +5572,11 @@ function detectFramework(cwd) {
|
|
|
4907
5572
|
} catch {
|
|
4908
5573
|
}
|
|
4909
5574
|
try {
|
|
4910
|
-
if (
|
|
5575
|
+
if (statSync4(join15(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
4911
5576
|
} catch {
|
|
4912
5577
|
}
|
|
4913
5578
|
try {
|
|
4914
|
-
if (
|
|
5579
|
+
if (statSync4(join15(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
4915
5580
|
} catch {
|
|
4916
5581
|
}
|
|
4917
5582
|
return "unknown";
|
|
@@ -4953,7 +5618,7 @@ function probeForSource(dir, budget) {
|
|
|
4953
5618
|
for (const e of entries) {
|
|
4954
5619
|
if (budget.left-- <= 0) return "unknown";
|
|
4955
5620
|
if (e.isDirectory()) {
|
|
4956
|
-
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(
|
|
5621
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join15(dir, e.name));
|
|
4957
5622
|
} else if (isSourceFile(e.name)) return true;
|
|
4958
5623
|
else if (e.isSymbolicLink()) return "unknown";
|
|
4959
5624
|
}
|
|
@@ -4966,7 +5631,7 @@ function probeForSource(dir, budget) {
|
|
|
4966
5631
|
function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
|
|
4967
5632
|
let key;
|
|
4968
5633
|
try {
|
|
4969
|
-
key =
|
|
5634
|
+
key = realpathSync2(dir);
|
|
4970
5635
|
} catch {
|
|
4971
5636
|
stats.unwalked++;
|
|
4972
5637
|
return out;
|
|
@@ -4990,19 +5655,19 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
4990
5655
|
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4991
5656
|
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4992
5657
|
else if (probeable && e.isDirectory()) {
|
|
4993
|
-
const held = probeForSource(
|
|
4994
|
-
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));
|
|
4995
5660
|
else if (held === "unknown") stats.skippedUnknown++;
|
|
4996
5661
|
}
|
|
4997
5662
|
continue;
|
|
4998
5663
|
}
|
|
4999
|
-
const full =
|
|
5664
|
+
const full = join15(dir, e.name);
|
|
5000
5665
|
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
5001
5666
|
else if (e.isSymbolicLink()) {
|
|
5002
5667
|
let st, real;
|
|
5003
5668
|
try {
|
|
5004
|
-
st =
|
|
5005
|
-
real =
|
|
5669
|
+
st = statSync4(full);
|
|
5670
|
+
real = realpathSync2(full);
|
|
5006
5671
|
} catch {
|
|
5007
5672
|
stats.unwalked++;
|
|
5008
5673
|
continue;
|
|
@@ -5025,7 +5690,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
5025
5690
|
}
|
|
5026
5691
|
function isInside(candidate, boundary) {
|
|
5027
5692
|
if (candidate === boundary) return true;
|
|
5028
|
-
const rel =
|
|
5693
|
+
const rel = relative3(boundary, candidate);
|
|
5029
5694
|
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
5030
5695
|
}
|
|
5031
5696
|
var DEPLOYMENT_SHAPES = [
|
|
@@ -5052,13 +5717,13 @@ var DEPLOYMENT_SHAPES = [
|
|
|
5052
5717
|
function detectDeploymentShapes(cwd, opts = {}) {
|
|
5053
5718
|
let boundary = opts.boundary ?? cwd;
|
|
5054
5719
|
try {
|
|
5055
|
-
boundary =
|
|
5720
|
+
boundary = realpathSync2(boundary);
|
|
5056
5721
|
} catch {
|
|
5057
5722
|
}
|
|
5058
|
-
const inProject = (
|
|
5723
|
+
const inProject = (path14) => {
|
|
5059
5724
|
if (opts.followOutside) return true;
|
|
5060
5725
|
try {
|
|
5061
|
-
return isInside(
|
|
5726
|
+
return isInside(realpathSync2(path14), boundary);
|
|
5062
5727
|
} catch {
|
|
5063
5728
|
return false;
|
|
5064
5729
|
}
|
|
@@ -5066,9 +5731,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5066
5731
|
const found = [];
|
|
5067
5732
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
5068
5733
|
for (const file of candidate.files ?? []) {
|
|
5069
|
-
const full =
|
|
5734
|
+
const full = join15(cwd, file);
|
|
5070
5735
|
try {
|
|
5071
|
-
if (
|
|
5736
|
+
if (statSync4(full).isFile() && inProject(full)) {
|
|
5072
5737
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
5073
5738
|
break;
|
|
5074
5739
|
}
|
|
@@ -5076,9 +5741,9 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
5076
5741
|
}
|
|
5077
5742
|
}
|
|
5078
5743
|
for (const dir of candidate.dirs ?? []) {
|
|
5079
|
-
const full =
|
|
5744
|
+
const full = join15(cwd, dir);
|
|
5080
5745
|
try {
|
|
5081
|
-
if (!
|
|
5746
|
+
if (!statSync4(full).isDirectory() || !inProject(full)) continue;
|
|
5082
5747
|
} catch {
|
|
5083
5748
|
continue;
|
|
5084
5749
|
}
|
|
@@ -5095,7 +5760,7 @@ function holdsFunctionEntry(dir) {
|
|
|
5095
5760
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5096
5761
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
5097
5762
|
try {
|
|
5098
|
-
for (const nested of readdirSync5(
|
|
5763
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5099
5764
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
5100
5765
|
}
|
|
5101
5766
|
} catch {
|
|
@@ -5111,7 +5776,7 @@ function holdsSourceFile(dir) {
|
|
|
5111
5776
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
5112
5777
|
if (entry.isDirectory()) {
|
|
5113
5778
|
try {
|
|
5114
|
-
for (const nested of readdirSync5(
|
|
5779
|
+
for (const nested of readdirSync5(join15(dir, entry.name), { withFileTypes: true })) {
|
|
5115
5780
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
5116
5781
|
}
|
|
5117
5782
|
} catch {
|
|
@@ -5124,8 +5789,8 @@ function holdsSourceFile(dir) {
|
|
|
5124
5789
|
}
|
|
5125
5790
|
|
|
5126
5791
|
// src/map/surface.ts
|
|
5127
|
-
import { readFileSync as
|
|
5128
|
-
import { join as
|
|
5792
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
5793
|
+
import { join as join16 } from "path";
|
|
5129
5794
|
var STATIC_GENERATORS = [
|
|
5130
5795
|
{ dep: "vite", label: "vite" },
|
|
5131
5796
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -5163,7 +5828,7 @@ var SHAPE_SIGNAL = {
|
|
|
5163
5828
|
};
|
|
5164
5829
|
function readManifest(cwd) {
|
|
5165
5830
|
try {
|
|
5166
|
-
const parsed = JSON.parse(
|
|
5831
|
+
const parsed = JSON.parse(readFileSync10(join16(cwd, "package.json"), "utf8"));
|
|
5167
5832
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
5168
5833
|
} catch {
|
|
5169
5834
|
return null;
|
|
@@ -5176,7 +5841,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
5176
5841
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
5177
5842
|
let text;
|
|
5178
5843
|
try {
|
|
5179
|
-
text =
|
|
5844
|
+
text = readFileSync10(join16(cwd, file), "utf8");
|
|
5180
5845
|
} catch {
|
|
5181
5846
|
continue;
|
|
5182
5847
|
}
|
|
@@ -5304,7 +5969,7 @@ function surfaceNote(surface) {
|
|
|
5304
5969
|
}
|
|
5305
5970
|
|
|
5306
5971
|
// src/map/sinks.ts
|
|
5307
|
-
import { createHash as
|
|
5972
|
+
import { createHash as createHash4 } from "crypto";
|
|
5308
5973
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
5309
5974
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
5310
5975
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -5403,14 +6068,14 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
5403
6068
|
const root = base ? rootIdentifier(base, ts) : void 0;
|
|
5404
6069
|
if (!root) return {};
|
|
5405
6070
|
const spec = bindings.resolve(root);
|
|
5406
|
-
const
|
|
6071
|
+
const relative6 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
|
|
5407
6072
|
const pkg = npmPackageOf(spec);
|
|
5408
6073
|
if (pkg) return { pkg, root, spec };
|
|
5409
|
-
if (
|
|
6074
|
+
if (relative6 && ctx && spec) {
|
|
5410
6075
|
const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
|
|
5411
6076
|
if (viaModule) return { pkg: viaModule.package, root, spec };
|
|
5412
6077
|
}
|
|
5413
|
-
return { local: bindings.locals.has(root), root, spec, relative:
|
|
6078
|
+
return { local: bindings.locals.has(root), root, spec, relative: relative6 };
|
|
5414
6079
|
};
|
|
5415
6080
|
const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
|
|
5416
6081
|
const dbApi = (pkg, attribution) => {
|
|
@@ -5571,7 +6236,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
|
5571
6236
|
return table?.[index] ?? "unknown";
|
|
5572
6237
|
}
|
|
5573
6238
|
function sinkId(s, owner) {
|
|
5574
|
-
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);
|
|
5575
6240
|
}
|
|
5576
6241
|
function dedupeSinks(sinks, owner) {
|
|
5577
6242
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -5587,8 +6252,8 @@ function dedupeSinks(sinks, owner) {
|
|
|
5587
6252
|
}
|
|
5588
6253
|
|
|
5589
6254
|
// src/map/module-graph.ts
|
|
5590
|
-
import { readFileSync as
|
|
5591
|
-
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";
|
|
5592
6257
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
5593
6258
|
function createModuleGraph(ts, opts) {
|
|
5594
6259
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -5596,7 +6261,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5596
6261
|
if (cache.has(file)) return cache.get(file) ?? null;
|
|
5597
6262
|
let entry = null;
|
|
5598
6263
|
try {
|
|
5599
|
-
const text =
|
|
6264
|
+
const text = readFileSync11(file, "utf8");
|
|
5600
6265
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
5601
6266
|
const bindings = buildModuleBindings(sf, ts);
|
|
5602
6267
|
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
@@ -5612,7 +6277,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5612
6277
|
if (!opts.followOutside) {
|
|
5613
6278
|
let real = target;
|
|
5614
6279
|
try {
|
|
5615
|
-
real =
|
|
6280
|
+
real = realpathSync3(target);
|
|
5616
6281
|
} catch {
|
|
5617
6282
|
}
|
|
5618
6283
|
if (!isInside(real, opts.boundary)) return void 0;
|
|
@@ -5630,7 +6295,7 @@ function createModuleGraph(ts, opts) {
|
|
|
5630
6295
|
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
5631
6296
|
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
5632
6297
|
}
|
|
5633
|
-
const rel =
|
|
6298
|
+
const rel = relative4(opts.cwd, target);
|
|
5634
6299
|
return collected.map((s) => ({ ...s, file: rel }));
|
|
5635
6300
|
},
|
|
5636
6301
|
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
@@ -5666,10 +6331,10 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
5666
6331
|
}
|
|
5667
6332
|
candidates.push(base);
|
|
5668
6333
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
5669
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
6334
|
+
for (const e of RESOLVE_EXTS) candidates.push(join17(base, "index" + e));
|
|
5670
6335
|
for (const c of candidates) {
|
|
5671
6336
|
try {
|
|
5672
|
-
if (
|
|
6337
|
+
if (statSync5(c).isFile()) return c;
|
|
5673
6338
|
} catch {
|
|
5674
6339
|
}
|
|
5675
6340
|
}
|
|
@@ -5695,8 +6360,8 @@ function collectCallees(sf, ts) {
|
|
|
5695
6360
|
}
|
|
5696
6361
|
|
|
5697
6362
|
// src/map/coordinates.ts
|
|
5698
|
-
function runtimeCoordinate(source,
|
|
5699
|
-
if (/\[\d*\]/.test(
|
|
6363
|
+
function runtimeCoordinate(source, path14) {
|
|
6364
|
+
if (/\[\d*\]/.test(path14)) {
|
|
5700
6365
|
return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
|
|
5701
6366
|
}
|
|
5702
6367
|
switch (source) {
|
|
@@ -5705,15 +6370,15 @@ function runtimeCoordinate(source, path13) {
|
|
|
5705
6370
|
case "multipart":
|
|
5706
6371
|
case "body":
|
|
5707
6372
|
case "server-fn-data":
|
|
5708
|
-
return { runtimeParameter: `post.${
|
|
6373
|
+
return { runtimeParameter: `post.${path14}` };
|
|
5709
6374
|
case "query":
|
|
5710
|
-
return { runtimeParameter: `get.${
|
|
6375
|
+
return { runtimeParameter: `get.${path14}` };
|
|
5711
6376
|
case "cookie":
|
|
5712
|
-
return { runtimeParameter: `cookie.${
|
|
6377
|
+
return { runtimeParameter: `cookie.${path14}` };
|
|
5713
6378
|
case "file":
|
|
5714
|
-
return { runtimeParameter: `files.${
|
|
6379
|
+
return { runtimeParameter: `files.${path14}` };
|
|
5715
6380
|
case "header":
|
|
5716
|
-
return { runtimeParameter: `server.HTTP_${
|
|
6381
|
+
return { runtimeParameter: `server.HTTP_${path14.toUpperCase().replace(/-/g, "_")}` };
|
|
5717
6382
|
case "route-param":
|
|
5718
6383
|
return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
|
|
5719
6384
|
default:
|
|
@@ -5745,8 +6410,8 @@ function addressSpaceOf(source) {
|
|
|
5745
6410
|
function isProvenFlow(confidence) {
|
|
5746
6411
|
return confidence === "exact-local" || confidence === "transformed-local";
|
|
5747
6412
|
}
|
|
5748
|
-
function inputIdOf(source,
|
|
5749
|
-
return `${addressSpaceOf(source)}:${
|
|
6413
|
+
function inputIdOf(source, path14) {
|
|
6414
|
+
return `${addressSpaceOf(source)}:${path14}`;
|
|
5750
6415
|
}
|
|
5751
6416
|
function withCoordinates(fields, source) {
|
|
5752
6417
|
return fields.map((f) => {
|
|
@@ -5956,8 +6621,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts) {
|
|
|
5956
6621
|
if (!bodyNode || sinks.length === 0 || inputs.length === 0) return { flows: [], limitations: [] };
|
|
5957
6622
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
5958
6623
|
const rootPath = /* @__PURE__ */ new Map();
|
|
5959
|
-
const addRoot = (name,
|
|
5960
|
-
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 });
|
|
5961
6626
|
};
|
|
5962
6627
|
for (const p of params ?? []) {
|
|
5963
6628
|
if (!p?.name) continue;
|
|
@@ -6102,8 +6767,8 @@ function dedupeLimitations(list) {
|
|
|
6102
6767
|
function join22(base, seg) {
|
|
6103
6768
|
return base ? `${base}.${seg}` : seg;
|
|
6104
6769
|
}
|
|
6105
|
-
function normalizePath(
|
|
6106
|
-
return
|
|
6770
|
+
function normalizePath(path14) {
|
|
6771
|
+
return path14.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
|
|
6107
6772
|
}
|
|
6108
6773
|
function fluentChainCalls(call, ts) {
|
|
6109
6774
|
let root = call;
|
|
@@ -6349,8 +7014,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
6349
7014
|
}
|
|
6350
7015
|
|
|
6351
7016
|
// src/map/imports.ts
|
|
6352
|
-
import { readFileSync as
|
|
6353
|
-
import { join as
|
|
7017
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
7018
|
+
import { join as join18 } from "path";
|
|
6354
7019
|
var MAX_SITES = 5;
|
|
6355
7020
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
6356
7021
|
function isNpmPackageName(pkg) {
|
|
@@ -6361,7 +7026,7 @@ function readPathAliases(cwd) {
|
|
|
6361
7026
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
6362
7027
|
let paths;
|
|
6363
7028
|
try {
|
|
6364
|
-
const raw =
|
|
7029
|
+
const raw = readFileSync12(join18(cwd, name), "utf8");
|
|
6365
7030
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
6366
7031
|
paths = parsed.compilerOptions?.paths;
|
|
6367
7032
|
} catch {
|
|
@@ -6721,7 +7386,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6721
7386
|
const failed = [];
|
|
6722
7387
|
let boundary = cwd;
|
|
6723
7388
|
try {
|
|
6724
|
-
boundary =
|
|
7389
|
+
boundary = realpathSync4(cwd);
|
|
6725
7390
|
} catch {
|
|
6726
7391
|
}
|
|
6727
7392
|
const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
|
|
@@ -6740,8 +7405,8 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6740
7405
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
6741
7406
|
for (const file of files) {
|
|
6742
7407
|
try {
|
|
6743
|
-
const text =
|
|
6744
|
-
const relFile =
|
|
7408
|
+
const text = readFileSync13(file, "utf8");
|
|
7409
|
+
const relFile = relative5(cwd, file);
|
|
6745
7410
|
sourceBytes += text.length;
|
|
6746
7411
|
if (!hasEntrySignal(text)) {
|
|
6747
7412
|
preFiltered++;
|
|
@@ -6753,7 +7418,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6753
7418
|
}
|
|
6754
7419
|
parsed++;
|
|
6755
7420
|
entryFiles.add(file);
|
|
6756
|
-
const fingerprint =
|
|
7421
|
+
const fingerprint = createHash5("sha256").update(text).digest("hex").slice(0, 16);
|
|
6757
7422
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
6758
7423
|
const bindings = buildModuleBindings(sf, ts);
|
|
6759
7424
|
for (const specifier of bindings.imports) {
|
|
@@ -6787,7 +7452,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6787
7452
|
}
|
|
6788
7453
|
} catch (e) {
|
|
6789
7454
|
if (typeof process !== "undefined" && process.env?.PS_MAP_DEBUG) console.error("[patchstack] map error", file, e);
|
|
6790
|
-
failed.push(
|
|
7455
|
+
failed.push(relative5(cwd, file));
|
|
6791
7456
|
}
|
|
6792
7457
|
}
|
|
6793
7458
|
let hopParsed = 0;
|
|
@@ -6795,10 +7460,10 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6795
7460
|
for (const target of hopTargets) {
|
|
6796
7461
|
if (entryFiles.has(target)) continue;
|
|
6797
7462
|
try {
|
|
6798
|
-
const text =
|
|
7463
|
+
const text = readFileSync13(target, "utf8");
|
|
6799
7464
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
6800
7465
|
const bindings = buildModuleBindings(sf, ts);
|
|
6801
|
-
const called = collectInvocations(sf, ts, bindings, { file: target, owner:
|
|
7466
|
+
const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative5(cwd, target), graph });
|
|
6802
7467
|
invocations.add(called.invocations);
|
|
6803
7468
|
calls.total += called.counts.total;
|
|
6804
7469
|
calls.dependency += called.counts.dependency;
|
|
@@ -6830,7 +7495,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
6830
7495
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
6831
7496
|
const importList = imports.list();
|
|
6832
7497
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6833
|
-
const skippedDirsWithSource = stats.skippedWithSource.map((d) =>
|
|
7498
|
+
const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative5(cwd, d));
|
|
6834
7499
|
const importCoverageGaps = {
|
|
6835
7500
|
unreadableFiles: failed.length,
|
|
6836
7501
|
unscannableFiles: importScanFailures,
|
|
@@ -7018,8 +7683,8 @@ async function runMap(flags) {
|
|
|
7018
7683
|
}
|
|
7019
7684
|
|
|
7020
7685
|
// src/setup.ts
|
|
7021
|
-
import { readFileSync as
|
|
7022
|
-
import
|
|
7686
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } from "fs";
|
|
7687
|
+
import path13 from "path";
|
|
7023
7688
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
7024
7689
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
7025
7690
|
function setupProtection(cwd) {
|
|
@@ -7037,8 +7702,8 @@ function appendHook(existing, command) {
|
|
|
7037
7702
|
return `${existing} && ${command}`;
|
|
7038
7703
|
}
|
|
7039
7704
|
function wireBuildScripts(cwd, packageManager) {
|
|
7040
|
-
const target =
|
|
7041
|
-
const raw =
|
|
7705
|
+
const target = path13.join(cwd, "package.json");
|
|
7706
|
+
const raw = readFileSync14(target, "utf8");
|
|
7042
7707
|
const pkg = JSON.parse(raw);
|
|
7043
7708
|
const scripts = pkg.scripts ?? {};
|
|
7044
7709
|
const build = scripts.build;
|
|
@@ -7192,6 +7857,15 @@ Usage:
|
|
|
7192
7857
|
demonstrations, not production).
|
|
7193
7858
|
--check verifies the guard is wired (exit 1
|
|
7194
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).
|
|
7195
7869
|
patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
|
|
7196
7870
|
walkthrough: verify the vulnerable package,
|
|
7197
7871
|
scan it, wait for live rule 18843, install +
|
|
@@ -7204,6 +7878,20 @@ Usage:
|
|
|
7204
7878
|
what's missing, with tailored commands), then
|
|
7205
7879
|
print the full setup guide. --full prints the
|
|
7206
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
|
|
7207
7895
|
patchstack-connect login [--wait] Recover this site's credential when
|
|
7208
7896
|
.patchstackrc.local.json has been lost. Needs the
|
|
7209
7897
|
site UUID, which lives in the committed
|
|
@@ -7321,6 +8009,107 @@ async function runInit(args) {
|
|
|
7321
8009
|
console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
|
|
7322
8010
|
return 0;
|
|
7323
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
|
+
}
|
|
7324
8113
|
async function runLogin(args) {
|
|
7325
8114
|
if (process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false") {
|
|
7326
8115
|
console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
|
|
@@ -7523,6 +8312,9 @@ async function runScan(args, options = {}) {
|
|
|
7523
8312
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
7524
8313
|
console.log(" (this URL inherits the endpoint override above)");
|
|
7525
8314
|
}
|
|
8315
|
+
console.log("");
|
|
8316
|
+
console.log("Or attach it to your account from this terminal:");
|
|
8317
|
+
console.log(" npx @patchstack/connect claim");
|
|
7526
8318
|
}
|
|
7527
8319
|
if (options.showRemainingSetup !== false) {
|
|
7528
8320
|
try {
|
|
@@ -7610,6 +8402,11 @@ function reportSourceMarker(framework) {
|
|
|
7610
8402
|
}
|
|
7611
8403
|
}
|
|
7612
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
|
+
}
|
|
7613
8410
|
if (args.flags.get("check") === true) {
|
|
7614
8411
|
const report = runVerify(process.cwd());
|
|
7615
8412
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
@@ -7628,7 +8425,12 @@ async function runProtectCommand(args) {
|
|
|
7628
8425
|
if (report.checks.some((c) => c.unverifiable)) {
|
|
7629
8426
|
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7630
8427
|
}
|
|
7631
|
-
|
|
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);
|
|
7632
8434
|
}
|
|
7633
8435
|
const demo = args.flags.get("demo") === true;
|
|
7634
8436
|
try {
|
|
@@ -7751,7 +8553,7 @@ async function runGuide(args) {
|
|
|
7751
8553
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
7752
8554
|
console.log("");
|
|
7753
8555
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
7754
|
-
console.log(
|
|
8556
|
+
console.log(readFileSync15(guidePath, "utf8"));
|
|
7755
8557
|
return 0;
|
|
7756
8558
|
}
|
|
7757
8559
|
async function runSetup(args) {
|
|
@@ -7825,6 +8627,7 @@ async function runStatus(args) {
|
|
|
7825
8627
|
console.log(`Environment: ${config.environment}`);
|
|
7826
8628
|
if (config.siteUuid !== null) {
|
|
7827
8629
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
8630
|
+
console.log(" Not attached to an account yet? Run `npx @patchstack/connect claim`.");
|
|
7828
8631
|
switch (await fetchSiteStatus(config)) {
|
|
7829
8632
|
case "active":
|
|
7830
8633
|
console.log("Site status: active on Patchstack");
|
|
@@ -7944,7 +8747,7 @@ async function runMarkBuild(args) {
|
|
|
7944
8747
|
let marked = 0;
|
|
7945
8748
|
let widgetTouched = 0;
|
|
7946
8749
|
for (const file of files) {
|
|
7947
|
-
const before =
|
|
8750
|
+
const before = readFileSync15(file, "utf8");
|
|
7948
8751
|
let after = injectMarker(before, snippet);
|
|
7949
8752
|
if (widgetUuid !== null) {
|
|
7950
8753
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -8006,6 +8809,8 @@ async function main() {
|
|
|
8006
8809
|
return runSetup(args);
|
|
8007
8810
|
case "map":
|
|
8008
8811
|
return runMap(args.flags);
|
|
8812
|
+
case "claim":
|
|
8813
|
+
return runClaim(args);
|
|
8009
8814
|
case "login":
|
|
8010
8815
|
return runLogin(args);
|
|
8011
8816
|
default:
|