@patchstack/connect 0.5.12 → 0.5.16
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 +60 -32
- package/README.md +19 -19
- package/dist/cli.js +1275 -642
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +16 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +16 -9
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +2132 -915
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +135 -17
- package/dist/protect.d.ts +135 -17
- package/dist/protect.edge.js +2195 -985
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +2198 -988
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-47HJRCDX.js → refresh-manifest-DZQOEO3Q.js} +17 -10
- package/dist/refresh-manifest-DZQOEO3Q.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-47HJRCDX.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -221,7 +221,7 @@ function unreadableWarning(filename, report) {
|
|
|
221
221
|
if (report.unreadable === 0) return null;
|
|
222
222
|
const shown = report.samples.join(", ");
|
|
223
223
|
const one = report.unreadable === 1;
|
|
224
|
-
return `${filename}: ${report.unreadable} entr${one ? "y
|
|
224
|
+
return `${filename}: ${report.unreadable} entr${one ? "y" : "ies"} could not be read (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "that package is" : "those packages are"} not being checked.`;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// src/parsers/bun.ts
|
|
@@ -1123,7 +1123,7 @@ async function scanLockfile(cwd) {
|
|
|
1123
1123
|
warnings.push(staleWarning("node_modules/", missing));
|
|
1124
1124
|
}
|
|
1125
1125
|
warnings.push(
|
|
1126
|
-
walkWasCandidate ? "
|
|
1126
|
+
walkWasCandidate ? "Checked the installed packages in node_modules/, which is what the build loads." : "Checked the installed packages in node_modules/ instead. Delete the out-of-date package list file to stop this warning."
|
|
1127
1127
|
);
|
|
1128
1128
|
return manifestWith(walked, warnings);
|
|
1129
1129
|
}
|
|
@@ -1135,7 +1135,7 @@ async function scanLockfile(cwd) {
|
|
|
1135
1135
|
);
|
|
1136
1136
|
}
|
|
1137
1137
|
warnings.push(
|
|
1138
|
-
`No
|
|
1138
|
+
`No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
|
|
1139
1139
|
);
|
|
1140
1140
|
return manifestWith(firstParsed.packages, warnings);
|
|
1141
1141
|
}
|
|
@@ -1149,12 +1149,19 @@ function otherCompleteSources(all, current, declared) {
|
|
|
1149
1149
|
}
|
|
1150
1150
|
function conflictWarning(filename, conflicts) {
|
|
1151
1151
|
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
1152
|
-
return `${filename} and another
|
|
1153
|
-
}
|
|
1152
|
+
return `${filename} and another package list file give different versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Checked node_modules/ instead. Delete the file your package manager does not use.`;
|
|
1153
|
+
}
|
|
1154
|
+
var INSTALL_BY_SOURCE = {
|
|
1155
|
+
"package-lock.json": "npm install",
|
|
1156
|
+
"bun.lock": "bun install",
|
|
1157
|
+
"bun.lockb": "bun install",
|
|
1158
|
+
"pnpm-lock.yaml": "pnpm install",
|
|
1159
|
+
"yarn.lock": "yarn install"
|
|
1160
|
+
};
|
|
1154
1161
|
function staleWarning(source, missing) {
|
|
1155
1162
|
const sample = missing.slice(0, 3).join(", ");
|
|
1156
1163
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
1157
|
-
return `${source}
|
|
1164
|
+
return `${source} is out of date: it is missing ${missing.length} package${missing.length === 1 ? "" : "s"} listed in package.json (${sample}${suffix}). Run ${INSTALL_BY_SOURCE[source] ?? "your install command"} to update it.`;
|
|
1158
1165
|
}
|
|
1159
1166
|
async function presentLockfiles(cwd) {
|
|
1160
1167
|
const probed = await Promise.all(
|
|
@@ -1622,21 +1629,28 @@ var CLAIM_TOKEN_HEADER = "X-Patchstack-Claim-Token";
|
|
|
1622
1629
|
function claimTokenHeader(config) {
|
|
1623
1630
|
return config.endpointTrusted !== false && isHeaderValue(config.claimToken) ? { [CLAIM_TOKEN_HEADER]: config.claimToken } : {};
|
|
1624
1631
|
}
|
|
1625
|
-
function
|
|
1626
|
-
if (typeof config.claimToken !== "string" || config.claimToken === "") return
|
|
1632
|
+
function claimOutcome(claim3, config) {
|
|
1633
|
+
if (typeof config.claimToken !== "string" || config.claimToken === "") return null;
|
|
1627
1634
|
if (claim3?.state === "claimed" || claim3?.state === "owned-by-you") {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1635
|
+
return {
|
|
1636
|
+
connected: true,
|
|
1637
|
+
summary: claim3.state === "claimed" ? "Connected to your Patchstack account" : "This project is already connected to your Patchstack account",
|
|
1638
|
+
dashboardUrl: safeRemoteUrl(claim3.dashboard_url),
|
|
1639
|
+
hint: []
|
|
1640
|
+
};
|
|
1634
1641
|
}
|
|
1635
|
-
const why = claim3
|
|
1636
|
-
return
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1642
|
+
const why = claimFailureReason(claim3);
|
|
1643
|
+
return {
|
|
1644
|
+
connected: false,
|
|
1645
|
+
summary: `Not connected to your account: ${why}`,
|
|
1646
|
+
dashboardUrl: null,
|
|
1647
|
+
hint: ["Open the dashboard link below to connect it, or copy a fresh prompt from the dashboard."]
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
function claimFailureReason(claim3) {
|
|
1651
|
+
if (claim3 === void 0) return "Patchstack did not act on the claim token";
|
|
1652
|
+
if (claim3.state === "owned-by-other") return "this project belongs to a different Patchstack account";
|
|
1653
|
+
return claim3.reason === "expired" ? "the claim token has expired" : "Patchstack did not recognise the claim token";
|
|
1640
1654
|
}
|
|
1641
1655
|
function buildEndpointUrl(base, siteUuid) {
|
|
1642
1656
|
const trimmed = base.replace(/\/$/, "");
|
|
@@ -1645,13 +1659,13 @@ function buildEndpointUrl(base, siteUuid) {
|
|
|
1645
1659
|
function authFailureMessage(status, config) {
|
|
1646
1660
|
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
1647
1661
|
if (status === 401 && !hasCredential) {
|
|
1648
|
-
return "Patchstack
|
|
1662
|
+
return "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY.";
|
|
1649
1663
|
}
|
|
1650
1664
|
if (status === 401) {
|
|
1651
|
-
return "Patchstack
|
|
1665
|
+
return "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one.";
|
|
1652
1666
|
}
|
|
1653
1667
|
if (status === 403) {
|
|
1654
|
-
return "This API
|
|
1668
|
+
return "This API key belongs to a different project. Check that siteUuid in .patchstackrc.json is the project the key was made for.";
|
|
1655
1669
|
}
|
|
1656
1670
|
return null;
|
|
1657
1671
|
}
|
|
@@ -2064,7 +2078,7 @@ function renderDemoGuide(state) {
|
|
|
2064
2078
|
`Patchstack demo guide \u2014 ${scenario.name}`,
|
|
2065
2079
|
"",
|
|
2066
2080
|
"Goal: prove that a live Patchstack virtual patch blocks the vulnerable payload while normal traffic still succeeds.",
|
|
2067
|
-
"Deployment required: no. Keep the app running locally;
|
|
2081
|
+
"Deployment required: no. Keep the app running locally; Connect and the generated guard contact Patchstack\u2019s production API.",
|
|
2068
2082
|
"",
|
|
2069
2083
|
`1. ${siteReady ? "\u2713" : "\u25CB"} Connect this project from Bolt\u2019s Host account dropdown`,
|
|
2070
2084
|
siteReady ? ` Ready \u2014 .patchstackrc.json contains site ${state.siteUuid}.` : " Click \u201CConnect Patchstack\u201D in Bolt. This creates .patchstackrc.json; no separate CLI login is needed.",
|
|
@@ -3105,6 +3119,16 @@ function rootIdentifier(node, ts) {
|
|
|
3105
3119
|
}
|
|
3106
3120
|
return void 0;
|
|
3107
3121
|
}
|
|
3122
|
+
function rootIdentifierNode(node, ts) {
|
|
3123
|
+
let cur = node;
|
|
3124
|
+
while (cur) {
|
|
3125
|
+
if (ts.isIdentifier(cur)) return cur;
|
|
3126
|
+
if (ts.isPropertyAccessExpression(cur) || ts.isElementAccessExpression(cur) || ts.isCallExpression(cur) || ts.isNewExpression(cur) || ts.isNonNullExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAwaitExpression(cur)) {
|
|
3127
|
+
cur = cur.expression;
|
|
3128
|
+
} else return void 0;
|
|
3129
|
+
}
|
|
3130
|
+
return void 0;
|
|
3131
|
+
}
|
|
3108
3132
|
function spanOf(node) {
|
|
3109
3133
|
const out = { line: lineOf(node) };
|
|
3110
3134
|
try {
|
|
@@ -3209,6 +3233,15 @@ function opCallOf(propAccess, ts) {
|
|
|
3209
3233
|
const p = propAccess?.parent;
|
|
3210
3234
|
return p && ts.isCallExpression(p) && p.expression === propAccess ? p : propAccess;
|
|
3211
3235
|
}
|
|
3236
|
+
function localCallIdentifiers(node, ts) {
|
|
3237
|
+
const out = [];
|
|
3238
|
+
const visit = (n) => {
|
|
3239
|
+
if (ts.isCallExpression(n) && ts.isIdentifier(n.expression)) out.push(n.expression);
|
|
3240
|
+
ts.forEachChild(n, visit);
|
|
3241
|
+
};
|
|
3242
|
+
visit(node);
|
|
3243
|
+
return out;
|
|
3244
|
+
}
|
|
3212
3245
|
function localCalls(node, ts) {
|
|
3213
3246
|
const names = [];
|
|
3214
3247
|
const visit = (n) => {
|
|
@@ -3245,8 +3278,9 @@ function routeFromFilePath(relFile) {
|
|
|
3245
3278
|
segs = dirs.slice(at("routes") + 1);
|
|
3246
3279
|
} else if (at("pages") !== -1) {
|
|
3247
3280
|
segs = [...dirs.slice(at("pages") + 1), ...base === "index" ? [] : [base]];
|
|
3248
|
-
} else if (
|
|
3249
|
-
|
|
3281
|
+
} else if (nuxtRoot(dirs) !== void 0) {
|
|
3282
|
+
const root = nuxtRoot(dirs);
|
|
3283
|
+
segs = [...root, ...base === "index" ? [] : [base.replace(/\.(get|post|put|patch|delete|head|options)$/i, "")]];
|
|
3250
3284
|
}
|
|
3251
3285
|
if (!segs) return {};
|
|
3252
3286
|
segs = segs.filter((s) => !(s.startsWith("(") && s.endsWith(")")) && !s.startsWith("@") && !s.startsWith("_"));
|
|
@@ -3262,6 +3296,21 @@ function routeFromFilePath(relFile) {
|
|
|
3262
3296
|
const route = "/" + mapped.join("/");
|
|
3263
3297
|
return { route: route.length > 1 ? route.replace(/\/+$/, "") : "/", dynamic };
|
|
3264
3298
|
}
|
|
3299
|
+
function isPagesApiFile(relFile) {
|
|
3300
|
+
const dirs = relFile.split(/[\\/]/).filter(Boolean).slice(0, -1);
|
|
3301
|
+
const i = dirs.lastIndexOf("pages");
|
|
3302
|
+
return i !== -1 && dirs[i + 1] === "api";
|
|
3303
|
+
}
|
|
3304
|
+
function nuxtRoot(dirs) {
|
|
3305
|
+
const i = dirs.lastIndexOf("server");
|
|
3306
|
+
if (i === -1) return void 0;
|
|
3307
|
+
if (dirs[i + 1] === "api") return dirs.slice(i + 1);
|
|
3308
|
+
if (dirs[i + 1] === "routes") return dirs.slice(i + 2);
|
|
3309
|
+
return void 0;
|
|
3310
|
+
}
|
|
3311
|
+
function isRoutePath(route) {
|
|
3312
|
+
return route.startsWith("/") || route === "*";
|
|
3313
|
+
}
|
|
3265
3314
|
function routeFromChain(expr, ts) {
|
|
3266
3315
|
let cur = expr;
|
|
3267
3316
|
while (cur && ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
|
|
@@ -3322,7 +3371,35 @@ function detectFramework(cwd) {
|
|
|
3322
3371
|
}
|
|
3323
3372
|
return "unknown";
|
|
3324
3373
|
}
|
|
3325
|
-
var isSourceFile = (name) => /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(name) && !name.endsWith(".d.ts");
|
|
3374
|
+
var isSourceFile = (name) => /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(name) && !name.endsWith(".d.ts") || isComponentFile(name);
|
|
3375
|
+
var isComponentFile = (name) => /\.(vue|svelte|astro)$/.test(name);
|
|
3376
|
+
function componentScript(text, file) {
|
|
3377
|
+
const keep = [];
|
|
3378
|
+
if (file.endsWith(".astro")) {
|
|
3379
|
+
const open2 = /^?\s*---[^\S\r\n]*\r?\n/.exec(text);
|
|
3380
|
+
if (open2) {
|
|
3381
|
+
const start = open2[0].length;
|
|
3382
|
+
const close = /\r?\n---[^\S\r\n]*(\r?\n|$)/g;
|
|
3383
|
+
close.lastIndex = start - 1;
|
|
3384
|
+
const end = close.exec(text);
|
|
3385
|
+
if (!end) return null;
|
|
3386
|
+
if (end.index > start) keep.push([start, end.index]);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
const opening = /<script\b[^>]*>/gi;
|
|
3390
|
+
for (let m = opening.exec(text); m; m = opening.exec(text)) {
|
|
3391
|
+
const start = m.index + m[0].length;
|
|
3392
|
+
const closing = /<\/script\s*>/gi;
|
|
3393
|
+
closing.lastIndex = start;
|
|
3394
|
+
const end = closing.exec(text);
|
|
3395
|
+
if (!end) return null;
|
|
3396
|
+
keep.push([start, end.index]);
|
|
3397
|
+
opening.lastIndex = end.index + end[0].length;
|
|
3398
|
+
}
|
|
3399
|
+
let out = text.replace(/[^\r\n]/g, " ");
|
|
3400
|
+
for (const [start, end] of keep) out = out.slice(0, start) + text.slice(start, end) + out.slice(end);
|
|
3401
|
+
return out;
|
|
3402
|
+
}
|
|
3326
3403
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
3327
3404
|
"node_modules",
|
|
3328
3405
|
"dist",
|
|
@@ -3825,7 +3902,7 @@ function classifyArchitecture(cwd) {
|
|
|
3825
3902
|
return {
|
|
3826
3903
|
requestPath: "none",
|
|
3827
3904
|
evidence: statics.map((label) => `static build: ${label}`),
|
|
3828
|
-
note: `This project builds a static site (${statics.join(", ")}) and nothing in it receives a request, so there is no request path for a runtime guard to attach to. Dependency monitoring and the
|
|
3905
|
+
note: `This project builds a static site (${statics.join(", ")}) and nothing in it receives a request, so there is no request path for a runtime guard to attach to. Dependency monitoring and the Patchstack Connector still apply; runtime protection does not.`
|
|
3829
3906
|
};
|
|
3830
3907
|
}
|
|
3831
3908
|
return {
|
|
@@ -3840,7 +3917,21 @@ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
|
|
|
3840
3917
|
import { join as join5, dirname as dirname2 } from "path";
|
|
3841
3918
|
import { fileURLToPath } from "url";
|
|
3842
3919
|
var read = (p) => readFileSync7(p, "utf8");
|
|
3843
|
-
var
|
|
3920
|
+
var logSink = null;
|
|
3921
|
+
var log = (msg) => {
|
|
3922
|
+
if (logSink !== null) logSink(msg);
|
|
3923
|
+
else console.log(`patchstack protect: ${msg}`);
|
|
3924
|
+
};
|
|
3925
|
+
function collectProtectLog(fn) {
|
|
3926
|
+
const lines = [];
|
|
3927
|
+
const previous = logSink;
|
|
3928
|
+
logSink = (msg) => lines.push(...msg.split("\n"));
|
|
3929
|
+
try {
|
|
3930
|
+
return { result: fn(), lines };
|
|
3931
|
+
} finally {
|
|
3932
|
+
logSink = previous;
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3844
3935
|
function hasDependency(cwd, name) {
|
|
3845
3936
|
try {
|
|
3846
3937
|
const pkg = JSON.parse(read(join5(cwd, "package.json")));
|
|
@@ -5502,6 +5593,147 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
5502
5593
|
return { shell, action };
|
|
5503
5594
|
}
|
|
5504
5595
|
|
|
5596
|
+
// src/progress.ts
|
|
5597
|
+
var PROGRESS_STEPS = [
|
|
5598
|
+
{ step: "installed", label: "Install the Patchstack connector" },
|
|
5599
|
+
{ step: "connected", label: "Connect project to Patchstack account" },
|
|
5600
|
+
{ step: "synced", label: "Sync and monitor your project" },
|
|
5601
|
+
{ step: "deployed", label: "Deploy project to protect live app" }
|
|
5602
|
+
];
|
|
5603
|
+
function stepLabel(step, environment) {
|
|
5604
|
+
if (step === "synced" && environment) return `Sync and monitor in ${environment} environment`;
|
|
5605
|
+
return PROGRESS_STEPS.find((entry) => entry.step === step).label;
|
|
5606
|
+
}
|
|
5607
|
+
function deployNote(context) {
|
|
5608
|
+
if (context.environment !== "production") return null;
|
|
5609
|
+
return context.environmentSource === "builder" ? "Reported as a publish. Patchstack ticks this once it sees the live site." : "Built for production. Patchstack ticks this once it sees the live site.";
|
|
5610
|
+
}
|
|
5611
|
+
var ANSI = {
|
|
5612
|
+
reset: "\x1B[0m",
|
|
5613
|
+
bold: "\x1B[1m",
|
|
5614
|
+
dim: "\x1B[2m",
|
|
5615
|
+
green: "\x1B[32m",
|
|
5616
|
+
yellow: "\x1B[33m",
|
|
5617
|
+
cyan: "\x1B[36m"
|
|
5618
|
+
};
|
|
5619
|
+
function nextProgressStep(progress) {
|
|
5620
|
+
return PROGRESS_STEPS.find(({ step }) => !progress[step])?.step ?? null;
|
|
5621
|
+
}
|
|
5622
|
+
function nextStepTitle(step, context) {
|
|
5623
|
+
switch (step) {
|
|
5624
|
+
case "installed":
|
|
5625
|
+
return "install the Patchstack connector";
|
|
5626
|
+
case "connected":
|
|
5627
|
+
return "connect this project to your Patchstack account";
|
|
5628
|
+
case "synced":
|
|
5629
|
+
return "sync this project with Patchstack";
|
|
5630
|
+
case "deployed":
|
|
5631
|
+
return context.environment === "production" ? "open your live app so Patchstack can see it" : "deploy your project to protect the live app";
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5634
|
+
function nextStepLines(step, context) {
|
|
5635
|
+
switch (step) {
|
|
5636
|
+
case "installed":
|
|
5637
|
+
return [`Run: ${context.installCommand}`, "Then run: npx @patchstack/connect setup"];
|
|
5638
|
+
case "connected":
|
|
5639
|
+
if (context.siteUuid === null) {
|
|
5640
|
+
return [
|
|
5641
|
+
"Run: npx @patchstack/connect setup",
|
|
5642
|
+
"It adds this project to Patchstack and prints the link to connect it.",
|
|
5643
|
+
'Cannot run commands here? See "When your tool will not run this CLI" in AGENT-INSTALL.md.'
|
|
5644
|
+
];
|
|
5645
|
+
}
|
|
5646
|
+
return [
|
|
5647
|
+
context.claimUrl !== null ? `Open ${context.claimUrl}` : "Run: npx @patchstack/connect claim",
|
|
5648
|
+
// A production run is the deploy itself, so there is nothing further to point at.
|
|
5649
|
+
...context.environment === "production" ? [] : ["Already connected? Then commit, set PATCHSTACK_API_KEY on your host, and deploy."]
|
|
5650
|
+
];
|
|
5651
|
+
case "synced":
|
|
5652
|
+
return ["Run: npx @patchstack/connect scan"];
|
|
5653
|
+
case "deployed":
|
|
5654
|
+
if (context.environment === "production") {
|
|
5655
|
+
return ["Open the live site once so Patchstack can see it.", "Not published yet? Publish or deploy it now."];
|
|
5656
|
+
}
|
|
5657
|
+
return [
|
|
5658
|
+
"Commit your changes. Never commit .patchstackrc.local.json.",
|
|
5659
|
+
"Set PATCHSTACK_API_KEY (from .patchstackrc.local.json) on your hosting platform.",
|
|
5660
|
+
"Deploy or publish. The live site keeps its old version until you do."
|
|
5661
|
+
];
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
function renderProgress(progress, context, options) {
|
|
5665
|
+
const paint = (code, text) => options.useColor ? `${code}${text}${ANSI.reset}` : text;
|
|
5666
|
+
const lines = [];
|
|
5667
|
+
for (const { step } of PROGRESS_STEPS) {
|
|
5668
|
+
const label = stepLabel(step, context.environment);
|
|
5669
|
+
lines.push(progress[step] ? ` ${paint(ANSI.green, "\u2714")} ${label}` : ` ${paint(ANSI.yellow, "\u2718")} ${label}`);
|
|
5670
|
+
const note = step === "deployed" && !progress.deployed ? deployNote(context) : null;
|
|
5671
|
+
if (note !== null) lines.push(` ${paint(ANSI.dim, note)}`);
|
|
5672
|
+
}
|
|
5673
|
+
const next = nextProgressStep(progress);
|
|
5674
|
+
lines.push("");
|
|
5675
|
+
if (next === null) {
|
|
5676
|
+
lines.push(paint(ANSI.bold, "All done."));
|
|
5677
|
+
return lines;
|
|
5678
|
+
}
|
|
5679
|
+
lines.push(`${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, `Next: ${nextStepTitle(next, context)}`)}`);
|
|
5680
|
+
for (const line of nextStepLines(next, context)) {
|
|
5681
|
+
lines.push(` ${line}`);
|
|
5682
|
+
}
|
|
5683
|
+
return lines;
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
// src/report.ts
|
|
5687
|
+
function emptyReport() {
|
|
5688
|
+
return { done: [], missing: [] };
|
|
5689
|
+
}
|
|
5690
|
+
var ANSI2 = {
|
|
5691
|
+
reset: "\x1B[0m",
|
|
5692
|
+
bold: "\x1B[1m",
|
|
5693
|
+
dim: "\x1B[2m",
|
|
5694
|
+
green: "\x1B[32m",
|
|
5695
|
+
yellow: "\x1B[33m"
|
|
5696
|
+
};
|
|
5697
|
+
function renderStatus(title, report, progress, context, options) {
|
|
5698
|
+
const paint = (code, text) => options.useColor ? `${code}${text}${ANSI2.reset}` : text;
|
|
5699
|
+
const lines = [paint(ANSI2.bold, title), ""];
|
|
5700
|
+
if (report.done.length > 0) {
|
|
5701
|
+
lines.push(paint(ANSI2.bold, "Done"));
|
|
5702
|
+
for (const item of report.done) lines.push(` ${paint(ANSI2.green, "\u2714")} ${item}`);
|
|
5703
|
+
lines.push("");
|
|
5704
|
+
}
|
|
5705
|
+
if (report.missing.length > 0) {
|
|
5706
|
+
lines.push(paint(ANSI2.bold, "Missing"));
|
|
5707
|
+
for (const item of report.missing) {
|
|
5708
|
+
lines.push(` ${paint(ANSI2.yellow, "\u2718")} ${item.text}`);
|
|
5709
|
+
for (const hint of item.hint ?? []) lines.push(` ${hint}`);
|
|
5710
|
+
if (options.verbose === true) {
|
|
5711
|
+
for (const detail2 of item.detail ?? []) lines.push(` ${paint(ANSI2.dim, detail2)}`);
|
|
5712
|
+
}
|
|
5713
|
+
}
|
|
5714
|
+
lines.push("");
|
|
5715
|
+
}
|
|
5716
|
+
if (options.withoutProgress === true) {
|
|
5717
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
5718
|
+
return lines;
|
|
5719
|
+
}
|
|
5720
|
+
lines.push(...renderProgress(progress, context, { useColor: options.useColor }));
|
|
5721
|
+
return lines;
|
|
5722
|
+
}
|
|
5723
|
+
function renderHookSummary(report, progress, context) {
|
|
5724
|
+
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
5725
|
+
const lines = [];
|
|
5726
|
+
if (report.done.length > 0) {
|
|
5727
|
+
lines.push(`Patchstack: ${report.done.map(lowerFirst).join("; ")}.`);
|
|
5728
|
+
}
|
|
5729
|
+
const next = nextProgressStep(progress);
|
|
5730
|
+
if (next !== null) {
|
|
5731
|
+
const first = next === "deployed" && context.environment === "production" ? void 0 : nextStepLines(next, context)[0];
|
|
5732
|
+
lines.push(`Patchstack next step: ${nextStepTitle(next, context)}.${first !== void 0 ? ` ${first}` : ""}`);
|
|
5733
|
+
}
|
|
5734
|
+
return lines;
|
|
5735
|
+
}
|
|
5736
|
+
|
|
5505
5737
|
// src/guide.ts
|
|
5506
5738
|
var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
|
|
5507
5739
|
var WIDGET_NEEDLE2 = "patchstack-widget";
|
|
@@ -5698,8 +5930,12 @@ async function collectGuideState(cwd) {
|
|
|
5698
5930
|
let claimUrl = null;
|
|
5699
5931
|
let endpointOverride = null;
|
|
5700
5932
|
let widgetOptOut = false;
|
|
5933
|
+
let environment = null;
|
|
5934
|
+
let environmentSource = null;
|
|
5701
5935
|
try {
|
|
5702
5936
|
const config = await resolveConfig({ cwd });
|
|
5937
|
+
environment = config.environment;
|
|
5938
|
+
environmentSource = config.environmentSource ?? null;
|
|
5703
5939
|
siteUuid = config.siteUuid;
|
|
5704
5940
|
if (siteUuid !== null && config.endpointTrusted !== false) {
|
|
5705
5941
|
claimUrl = buildClaimUrl(config.endpoint, siteUuid);
|
|
@@ -5726,6 +5962,8 @@ async function collectGuideState(cwd) {
|
|
|
5726
5962
|
siteUuid,
|
|
5727
5963
|
claimUrl,
|
|
5728
5964
|
endpointOverride,
|
|
5965
|
+
environment,
|
|
5966
|
+
environmentSource,
|
|
5729
5967
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
5730
5968
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
5731
5969
|
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
@@ -5744,7 +5982,7 @@ async function collectGuideState(cwd) {
|
|
|
5744
5982
|
protectionApplicable: protection.applicable
|
|
5745
5983
|
};
|
|
5746
5984
|
}
|
|
5747
|
-
var
|
|
5985
|
+
var ANSI3 = {
|
|
5748
5986
|
reset: "\x1B[0m",
|
|
5749
5987
|
bold: "\x1B[1m",
|
|
5750
5988
|
dim: "\x1B[2m",
|
|
@@ -5758,9 +5996,6 @@ function needsSourceProductionMarker(state) {
|
|
|
5758
5996
|
}
|
|
5759
5997
|
return !state.widgetFileHint.toLowerCase().endsWith(".html");
|
|
5760
5998
|
}
|
|
5761
|
-
function widgetTagInPlace(state) {
|
|
5762
|
-
return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
5763
|
-
}
|
|
5764
5999
|
function countRemainingSteps(state) {
|
|
5765
6000
|
return [
|
|
5766
6001
|
state.installed?.section === "dependencies",
|
|
@@ -5772,217 +6007,143 @@ function countRemainingSteps(state) {
|
|
|
5772
6007
|
!state.protectionApplicable || state.protectionWired
|
|
5773
6008
|
].filter((step) => !step).length;
|
|
5774
6009
|
}
|
|
5775
|
-
function
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
6010
|
+
function guideProgress(state, known = {}) {
|
|
6011
|
+
return {
|
|
6012
|
+
installed: state.installed !== null,
|
|
6013
|
+
// Claim state lives on the server and nothing on disk records it, so only a caller that has just
|
|
6014
|
+
// heard from the server (a scan's claim outcome) can mark it done.
|
|
6015
|
+
connected: false,
|
|
6016
|
+
// `.patchstackrc.json` only gains a site UUID from a manifest the server stored.
|
|
6017
|
+
synced: state.siteUuid !== null,
|
|
6018
|
+
// Only the dashboard can see the live site, so nothing the CLI runs marks this done.
|
|
6019
|
+
deployed: false,
|
|
6020
|
+
...known
|
|
6021
|
+
};
|
|
6022
|
+
}
|
|
6023
|
+
function guideNextStepContext(state) {
|
|
6024
|
+
return {
|
|
6025
|
+
installCommand: installCommand(state.packageManager),
|
|
6026
|
+
siteUuid: state.siteUuid,
|
|
6027
|
+
claimUrl: state.claimUrl,
|
|
6028
|
+
environment: state.environment,
|
|
6029
|
+
environmentSource: state.environmentSource
|
|
6030
|
+
};
|
|
6031
|
+
}
|
|
6032
|
+
function notConnectedItem(why = []) {
|
|
6033
|
+
return {
|
|
6034
|
+
text: "Not connected to your Patchstack account",
|
|
6035
|
+
hint: [...why, "Until it is, anyone who opens your app can connect it to their own account."]
|
|
6036
|
+
};
|
|
6037
|
+
}
|
|
6038
|
+
function buildScriptLines(state) {
|
|
5780
6039
|
const lines = [];
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
);
|
|
5784
|
-
const name = state.projectName ?? path12.basename(process.cwd());
|
|
5785
|
-
lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
|
|
5786
|
-
if (state.endpointOverride !== null) {
|
|
5787
|
-
lines.push(
|
|
5788
|
-
detail(
|
|
5789
|
-
`endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
|
|
5790
|
-
)
|
|
5791
|
-
);
|
|
5792
|
-
}
|
|
5793
|
-
lines.push("");
|
|
5794
|
-
if (!state.hasPackageJson) {
|
|
5795
|
-
lines.push(todo("No package.json found in this directory."));
|
|
5796
|
-
lines.push(detail("For a JS/Node app, run the guide from its package directory; check that package.json is readable and valid."));
|
|
5797
|
-
lines.push(detail("For a standalone HTML/CSS/browser-JavaScript site, use the disclosure widget directly."));
|
|
5798
|
-
lines.push(detail("Do not create a Node project, build hooks, or a server just to install the widget."));
|
|
5799
|
-
lines.push(detail("Use the correct site UUID or widget snippet from the Patchstack dashboard; never invent one."));
|
|
5800
|
-
lines.push(detail('See "Plain HTML sites" in AGENT-INSTALL.md. Widget-only setup provides no dependency scan or runtime protection.'));
|
|
5801
|
-
return lines.join("\n");
|
|
5802
|
-
}
|
|
5803
|
-
if (countRemainingSteps(state) > 0) {
|
|
5804
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Tell the user what you are doing as you go.")}`);
|
|
5805
|
-
lines.push(" Installs here take minutes and they are watching output they cannot read.");
|
|
5806
|
-
lines.push(" Say it in plain words: their site is built out of hundreds of ready-made");
|
|
5807
|
-
lines.push(" pieces of code, Patchstack keeps track of which ones it uses and warns them");
|
|
5808
|
-
lines.push(" when one turns out to be unsafe, and nothing their visitors see changes.");
|
|
5809
|
-
lines.push(" Avoid lockfile / dependency / manifest / npm / UUID when speaking to them.");
|
|
5810
|
-
lines.push("");
|
|
5811
|
-
}
|
|
5812
|
-
if (state.siteUuid === null) {
|
|
5813
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Nothing is set up yet \u2014 two commands cover the steps below:")}`);
|
|
5814
|
-
if (state.installed?.section !== "dependencies") {
|
|
5815
|
-
lines.push(` ${installCommand(state.packageManager)}`);
|
|
5816
|
-
}
|
|
5817
|
-
lines.push(" npx @patchstack/connect setup");
|
|
5818
|
-
lines.push(" setup provisions the site, adds the widget, installs and verifies runtime protection,");
|
|
5819
|
-
lines.push(" and wires the install and build scans, then prints this checklist again. Anything");
|
|
5820
|
-
lines.push(" still listed there is a step setup could not apply safely, and is yours to finish.");
|
|
5821
|
-
lines.push("");
|
|
5822
|
-
}
|
|
5823
|
-
if (state.installed?.section === "dependencies") {
|
|
5824
|
-
lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
|
|
5825
|
-
} else if (state.installed !== null) {
|
|
5826
|
-
lines.push(todo(`Move @patchstack/connect to runtime dependencies (currently ${state.installed.section})`));
|
|
5827
|
-
lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
|
|
5828
|
-
lines.push(detail("The generated guard imports @patchstack/connect/protect at runtime."));
|
|
5829
|
-
} else {
|
|
5830
|
-
lines.push(todo("Install @patchstack/connect as a runtime dependency"));
|
|
5831
|
-
lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
|
|
6040
|
+
if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
|
|
6041
|
+
if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
|
|
5832
6042
|
if (state.packageManager === "bun") {
|
|
5833
|
-
lines.push(
|
|
5834
|
-
}
|
|
5835
|
-
}
|
|
5836
|
-
if (state.siteUuid !== null) {
|
|
5837
|
-
lines.push(done(`Site provisioned (${state.siteUuid})`));
|
|
5838
|
-
} else {
|
|
5839
|
-
lines.push(todo("Provision the site \u2014 run the first scan"));
|
|
5840
|
-
lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
|
|
5841
|
-
lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
|
|
5842
|
-
lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
|
|
5843
|
-
lines.push(detail("If your tool refuses to run this command, hand it to the person instead of working"));
|
|
5844
|
-
lines.push(detail('around it \u2014 see "When your tool will not run this CLI" in the reference guide.'));
|
|
5845
|
-
}
|
|
5846
|
-
if (state.installScanWired) {
|
|
5847
|
-
lines.push(done("Dependency-install scan wired (postinstall)"));
|
|
5848
|
-
} else {
|
|
5849
|
-
lines.push(todo("Scan again whenever dependencies are installed"));
|
|
5850
|
-
lines.push(detail('Edit package.json \u2192 "postinstall": "patchstack-connect scan"'));
|
|
5851
|
-
}
|
|
5852
|
-
if (!state.hasBuildScript) {
|
|
5853
|
-
lines.push(done("No build script to integrate (postinstall covers dependency changes)"));
|
|
5854
|
-
} else if (state.prebuildWired && state.postbuildWired) {
|
|
5855
|
-
lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
|
|
5856
|
-
} else if (state.packageManager === "bun") {
|
|
5857
|
-
lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
|
|
5858
|
-
lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
|
|
5859
|
-
} else {
|
|
5860
|
-
lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
|
|
5861
|
-
if (!state.prebuildWired) {
|
|
5862
|
-
lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
|
|
5863
|
-
}
|
|
5864
|
-
if (!state.postbuildWired) {
|
|
5865
|
-
lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
|
|
5866
|
-
}
|
|
5867
|
-
}
|
|
5868
|
-
const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
5869
|
-
if (state.widgetOptOut && !widgetOk) {
|
|
5870
|
-
lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
|
|
5871
|
-
} else if (widgetOk) {
|
|
5872
|
-
lines.push(done("Disclosure widget installed"));
|
|
5873
|
-
} else if (state.widgetInstalled) {
|
|
5874
|
-
lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
|
|
5875
|
-
lines.push(detail(`Edit the widget tag \u2192 set data-site-uuid (or userToken) to '${state.siteUuid}' (a wrong UUID makes the widget silently no-op)`));
|
|
5876
|
-
} else if (state.siteUuid === null) {
|
|
5877
|
-
lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
|
|
5878
|
-
lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
|
|
5879
|
-
lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
|
|
5880
|
-
} else {
|
|
5881
|
-
lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
|
|
5882
|
-
lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
|
|
5883
|
-
const placement = state.widgetFileHint !== null ? `Edit ${state.widgetFileHint} \u2192 put it just before </body>:` : "Edit your root layout \u2192 put it just before </body> (the framework's HTML/layout mechanism, never a JS entry point):";
|
|
5884
|
-
lines.push(detail(placement));
|
|
5885
|
-
lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
|
|
5886
|
-
lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
|
|
5887
|
-
}
|
|
5888
|
-
if (needsSourceProductionMarker(state)) {
|
|
5889
|
-
if (state.productionMarkerWired) {
|
|
5890
|
-
lines.push(done("Production marker wired (widget switches to report mode on the published site)"));
|
|
6043
|
+
lines.push('"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"');
|
|
5891
6044
|
} else {
|
|
5892
|
-
lines.push(
|
|
5893
|
-
lines.push(
|
|
5894
|
-
detail(
|
|
5895
|
-
"Without it the widget treats the published site as build mode and shows the claim flow to visitors."
|
|
5896
|
-
)
|
|
5897
|
-
);
|
|
5898
|
-
const gate = productionGate(state.framework);
|
|
5899
|
-
if (hasJsxShell(state.framework)) {
|
|
5900
|
-
lines.push(detail(`Run \u2192 npx @patchstack/connect scan (adds it to ${state.widgetFileHint} automatically)`));
|
|
5901
|
-
lines.push(detail("Or add it by hand, in <head> above the widget tag:"));
|
|
5902
|
-
for (const snippetLine of buildSourceMarkerSnippet(state.framework).split("\n")) {
|
|
5903
|
-
lines.push(detail(` ${snippetLine}`));
|
|
5904
|
-
}
|
|
5905
|
-
} else {
|
|
5906
|
-
lines.push(
|
|
5907
|
-
detail(
|
|
5908
|
-
`Edit ${state.widgetFileHint} \u2192 using the framework's head mechanism, emit an inline`
|
|
5909
|
-
)
|
|
5910
|
-
);
|
|
5911
|
-
lines.push(detail(` <script>window.__PATCHSTACK_PROD__=true;</script> only when ${gate}.`));
|
|
5912
|
-
}
|
|
5913
|
-
lines.push(detail(`The ${gate} guard is required \u2014 an ungated marker also hides the claim flow in preview.`));
|
|
6045
|
+
if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
|
|
6046
|
+
if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
|
|
5914
6047
|
}
|
|
5915
6048
|
}
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
for (const check of state.protectionChecks.filter((item) => !item.ok)) {
|
|
5926
|
-
lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
|
|
5927
|
-
}
|
|
5928
|
-
lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
|
|
5929
|
-
lines.push(detail("Prove a request reaches the guard (starts your app) \u2192 npx @patchstack/connect protect --check --runtime"));
|
|
6049
|
+
return lines;
|
|
6050
|
+
}
|
|
6051
|
+
function guideMissing(state, known = {}) {
|
|
6052
|
+
const missing = [];
|
|
6053
|
+
if (state.installed?.section === "devDependencies") {
|
|
6054
|
+
missing.push({
|
|
6055
|
+
text: "Patchstack is installed as a development tool only, so your live app cannot load it",
|
|
6056
|
+
hint: [`Run: ${installCommand(state.packageManager)}`]
|
|
6057
|
+
});
|
|
5930
6058
|
}
|
|
5931
|
-
|
|
5932
|
-
if (state.
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
lines.push(detail("anyone who loads it."));
|
|
5946
|
-
if (state.endpointOverride !== null) {
|
|
5947
|
-
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
5948
|
-
}
|
|
5949
|
-
} else {
|
|
5950
|
-
lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
|
|
6059
|
+
if (state.siteUuid === null) return missing;
|
|
6060
|
+
if (!state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches === false) {
|
|
6061
|
+
missing.push({
|
|
6062
|
+
text: "The Patchstack widget on your page belongs to a different project",
|
|
6063
|
+
hint: [`Set data-site-uuid to '${state.siteUuid}' on the widget tag.`]
|
|
6064
|
+
});
|
|
6065
|
+
} else if (!state.widgetOptOut && !state.widgetInstalled) {
|
|
6066
|
+
missing.push({
|
|
6067
|
+
text: "The Patchstack widget is not on your page yet",
|
|
6068
|
+
hint: [
|
|
6069
|
+
state.widgetFileHint !== null ? `Add this to ${state.widgetFileHint}, just before </body>:` : "Add this to your main page layout, just before </body>:",
|
|
6070
|
+
` ${buildWidgetTag(state.siteUuid)}`
|
|
6071
|
+
]
|
|
6072
|
+
});
|
|
5951
6073
|
}
|
|
5952
|
-
if (
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
if (state.siteUuid !== null) {
|
|
5962
|
-
lines.push("");
|
|
5963
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
|
|
5964
|
-
lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
|
|
5965
|
-
lines.push(" so visitors only get the widget after you deploy (or hit Publish) again.");
|
|
6074
|
+
if (state.protectionApplicable && !state.protectionWired) {
|
|
6075
|
+
const failing = state.protectionChecks.filter((item) => !item.ok && item.group !== "reporting");
|
|
6076
|
+
const generic = state.protectionStack === "generic";
|
|
6077
|
+
missing.push({
|
|
6078
|
+
key: "runtime-protection",
|
|
6079
|
+
text: generic ? "Runtime protection: not added to your server yet" : `Runtime protection: not finished for ${state.protectionStack}`,
|
|
6080
|
+
hint: generic ? ["Add Patchstack where requests enter your app. Run npx @patchstack/connect protect for the steps."] : ["Run: npx @patchstack/connect protect, then npx @patchstack/connect protect --check"],
|
|
6081
|
+
detail: failing.map((check) => `${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`)
|
|
6082
|
+
});
|
|
5966
6083
|
}
|
|
5967
|
-
const
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6084
|
+
const scripts = buildScriptLines(state);
|
|
6085
|
+
if (scripts.length > 0) {
|
|
6086
|
+
missing.push({
|
|
6087
|
+
text: "Patchstack does not check your packages on install and build yet",
|
|
6088
|
+
hint: ["Run: npx @patchstack/connect setup (it adds the build steps to package.json)"],
|
|
6089
|
+
detail: [
|
|
6090
|
+
state.packageManager === "bun" ? "Add to package.json scripts:" : "Add to package.json scripts (chain with && if one exists):",
|
|
6091
|
+
...scripts.map((line) => ` ${line}`)
|
|
6092
|
+
]
|
|
6093
|
+
});
|
|
6094
|
+
}
|
|
6095
|
+
if (needsSourceProductionMarker(state) && !state.productionMarkerWired) {
|
|
6096
|
+
const gate = productionGate(state.framework);
|
|
6097
|
+
missing.push(
|
|
6098
|
+
hasJsxShell(state.framework) ? {
|
|
6099
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
6100
|
+
hint: [`Run: npx @patchstack/connect scan (it edits ${state.widgetFileHint})`],
|
|
6101
|
+
detail: [`Or add inside <head>:`, ...buildSourceMarkerSnippet(state.framework).split("\n").map((line) => ` ${line}`)]
|
|
6102
|
+
} : {
|
|
6103
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
6104
|
+
hint: [
|
|
6105
|
+
`Add this inside <head> in ${state.widgetFileHint}, only when ${gate}:`,
|
|
6106
|
+
" <script>window.__PATCHSTACK_PROD__=true;</script>"
|
|
6107
|
+
]
|
|
6108
|
+
}
|
|
5984
6109
|
);
|
|
5985
6110
|
}
|
|
6111
|
+
if (known.connected !== true) missing.push(notConnectedItem());
|
|
6112
|
+
return missing;
|
|
6113
|
+
}
|
|
6114
|
+
function renderGuideChecklist(state, useColor2, known = {}, options = {}) {
|
|
6115
|
+
const paint = (code, text) => useColor2 ? `${code}${text}${ANSI3.reset}` : text;
|
|
6116
|
+
const name = state.projectName ?? path12.basename(process.cwd());
|
|
6117
|
+
const title = `Patchstack status for ${name}`;
|
|
6118
|
+
if (!state.hasPackageJson) {
|
|
6119
|
+
return [
|
|
6120
|
+
paint(ANSI3.bold, title),
|
|
6121
|
+
"",
|
|
6122
|
+
paint(ANSI3.bold, "Missing"),
|
|
6123
|
+
` ${paint(ANSI3.yellow, "\u2718")} No package.json here.`,
|
|
6124
|
+
" For a JS/Node app, run this from its package directory.",
|
|
6125
|
+
' For a plain HTML site, follow "Plain HTML sites" in AGENT-INSTALL.md. It gets the widget only: no dependency scan or runtime protection.',
|
|
6126
|
+
" Do not create a Node project just to add the widget.",
|
|
6127
|
+
" Use the site UUID or widget snippet from the Patchstack dashboard. Never invent one."
|
|
6128
|
+
].join("\n");
|
|
6129
|
+
}
|
|
6130
|
+
const lines = renderStatus(
|
|
6131
|
+
title,
|
|
6132
|
+
{ done: [], missing: guideMissing(state, known) },
|
|
6133
|
+
guideProgress(state, known),
|
|
6134
|
+
guideNextStepContext(state),
|
|
6135
|
+
{ useColor: useColor2, verbose: options.verbose }
|
|
6136
|
+
);
|
|
6137
|
+
if (options.verbose === true) {
|
|
6138
|
+
const verbose2 = [
|
|
6139
|
+
`Project: ${[state.framework, state.packageManager].filter((part) => part !== null).join(" \xB7 ")}`,
|
|
6140
|
+
...state.siteUuid !== null ? [`Site UUID: ${state.siteUuid}`] : [],
|
|
6141
|
+
...state.environment !== null ? [`Environment: ${state.environment}${state.environmentSource !== null ? ` (${state.environmentSource})` : ""}`] : [],
|
|
6142
|
+
...state.endpointOverride !== null ? [`Endpoint override: ${state.endpointOverride}`] : [],
|
|
6143
|
+
...state.widgetOptOut ? ['Widget is off ("widget": false in .patchstackrc.json).'] : []
|
|
6144
|
+
];
|
|
6145
|
+
lines.splice(1, 0, ...verbose2.map((line) => paint(ANSI3.dim, line)));
|
|
6146
|
+
}
|
|
5986
6147
|
return lines.join("\n");
|
|
5987
6148
|
}
|
|
5988
6149
|
|
|
@@ -6919,6 +7080,97 @@ function npmPackageOf(spec) {
|
|
|
6919
7080
|
|
|
6920
7081
|
// src/map/sinks.ts
|
|
6921
7082
|
import { createHash as createHash4 } from "crypto";
|
|
7083
|
+
|
|
7084
|
+
// src/map/scope.ts
|
|
7085
|
+
var scopeCache = /* @__PURE__ */ new WeakMap();
|
|
7086
|
+
function declarationOf(id, ts) {
|
|
7087
|
+
if (!id || !ts.isIdentifier(id)) return void 0;
|
|
7088
|
+
const name = id.text;
|
|
7089
|
+
for (let cur = id.parent; cur; cur = cur.parent) {
|
|
7090
|
+
const declared = declaredIn(cur, ts);
|
|
7091
|
+
if (declared?.has(name)) return declared.get(name);
|
|
7092
|
+
}
|
|
7093
|
+
return void 0;
|
|
7094
|
+
}
|
|
7095
|
+
function isGlobal(node, name, ts) {
|
|
7096
|
+
if (ts.isIdentifier(node)) return node.text === name && declarationOf(node, ts) === void 0;
|
|
7097
|
+
return ts.isPropertyAccessExpression(node) && node.name.text === name && ts.isIdentifier(node.expression) && node.expression.text === "globalThis" && declarationOf(node.expression, ts) === void 0;
|
|
7098
|
+
}
|
|
7099
|
+
function boundIdentifiers(name, ts) {
|
|
7100
|
+
if (!name) return [];
|
|
7101
|
+
if (ts.isIdentifier(name)) return [name];
|
|
7102
|
+
if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) {
|
|
7103
|
+
return name.elements.flatMap((el) => ts.isBindingElement(el) ? boundIdentifiers(el.name, ts) : []);
|
|
7104
|
+
}
|
|
7105
|
+
return [];
|
|
7106
|
+
}
|
|
7107
|
+
function isFunctionScope(n, ts) {
|
|
7108
|
+
return ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n) || ts.isConstructorDeclaration(n) || ts.isGetAccessorDeclaration(n) || ts.isSetAccessorDeclaration(n) || ts.isSourceFile(n);
|
|
7109
|
+
}
|
|
7110
|
+
function declaredIn(n, ts) {
|
|
7111
|
+
const cached2 = scopeCache.get(n);
|
|
7112
|
+
if (cached2) return cached2;
|
|
7113
|
+
const out = /* @__PURE__ */ new Map();
|
|
7114
|
+
const add = (ids) => {
|
|
7115
|
+
for (const i of ids) if (!out.has(i.text)) out.set(i.text, i);
|
|
7116
|
+
};
|
|
7117
|
+
if (isFunctionScope(n, ts)) {
|
|
7118
|
+
if ((ts.isFunctionExpression(n) || ts.isClassExpression?.(n)) && n.name) add([n.name]);
|
|
7119
|
+
for (const p of n.parameters ?? []) add(boundIdentifiers(p.name, ts));
|
|
7120
|
+
collectVarScoped(ts.isSourceFile(n) ? n : n.body, ts, add);
|
|
7121
|
+
if (ts.isSourceFile(n)) collectLexical(n.statements, ts, add, true);
|
|
7122
|
+
} else if (ts.isBlock(n) || ts.isModuleBlock(n) || ts.isCaseBlock(n)) {
|
|
7123
|
+
const statements = ts.isCaseBlock(n) ? n.clauses.flatMap((c) => [...c.statements]) : n.statements;
|
|
7124
|
+
collectLexical(statements, ts, add, false);
|
|
7125
|
+
} else if (ts.isForStatement(n) || ts.isForInStatement(n) || ts.isForOfStatement(n)) {
|
|
7126
|
+
const init = n.initializer;
|
|
7127
|
+
if (init && ts.isVariableDeclarationList(init) && isLexicalList(init, ts)) {
|
|
7128
|
+
for (const d of init.declarations) add(boundIdentifiers(d.name, ts));
|
|
7129
|
+
}
|
|
7130
|
+
} else if (ts.isCatchClause(n)) {
|
|
7131
|
+
if (n.variableDeclaration) add(boundIdentifiers(n.variableDeclaration.name, ts));
|
|
7132
|
+
} else if (ts.isClassExpression?.(n) && n.name) {
|
|
7133
|
+
add([n.name]);
|
|
7134
|
+
} else {
|
|
7135
|
+
return void 0;
|
|
7136
|
+
}
|
|
7137
|
+
scopeCache.set(n, out);
|
|
7138
|
+
return out;
|
|
7139
|
+
}
|
|
7140
|
+
function isLexicalList(list, ts) {
|
|
7141
|
+
return (list.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
|
7142
|
+
}
|
|
7143
|
+
function collectLexical(statements, ts, add, topLevel) {
|
|
7144
|
+
for (const s of statements ?? []) {
|
|
7145
|
+
if (ts.isVariableStatement(s) && isLexicalList(s.declarationList, ts)) {
|
|
7146
|
+
for (const d of s.declarationList.declarations) add(boundIdentifiers(d.name, ts));
|
|
7147
|
+
} else if ((ts.isClassDeclaration(s) || ts.isFunctionDeclaration(s) || ts.isEnumDeclaration(s)) && s.name) {
|
|
7148
|
+
add([s.name]);
|
|
7149
|
+
} else if (topLevel && ts.isImportDeclaration(s) && s.importClause) {
|
|
7150
|
+
const clause = s.importClause;
|
|
7151
|
+
if (clause.name) add([clause.name]);
|
|
7152
|
+
const bindings = clause.namedBindings;
|
|
7153
|
+
if (bindings && ts.isNamespaceImport(bindings)) add([bindings.name]);
|
|
7154
|
+
if (bindings && ts.isNamedImports(bindings)) add(bindings.elements.map((e) => e.name));
|
|
7155
|
+
} else if (topLevel && ts.isImportEqualsDeclaration(s)) {
|
|
7156
|
+
add([s.name]);
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
function collectVarScoped(node, ts, add) {
|
|
7161
|
+
const visit = (n) => {
|
|
7162
|
+
if (!n) return;
|
|
7163
|
+
if (ts.isVariableDeclarationList(n) && !isLexicalList(n, ts)) {
|
|
7164
|
+
for (const d of n.declarations) add(boundIdentifiers(d.name, ts));
|
|
7165
|
+
}
|
|
7166
|
+
if (n !== node && isFunctionScope(n, ts)) return;
|
|
7167
|
+
if (ts.isClassDeclaration(n) || ts.isClassExpression?.(n)) return;
|
|
7168
|
+
ts.forEachChild(n, visit);
|
|
7169
|
+
};
|
|
7170
|
+
visit(node);
|
|
7171
|
+
}
|
|
7172
|
+
|
|
7173
|
+
// src/map/sinks.ts
|
|
6922
7174
|
var DB_OPS = /* @__PURE__ */ new Set(["insert", "update", "delete", "select", "upsert", "rpc"]);
|
|
6923
7175
|
var PRISMA_OPS = /* @__PURE__ */ new Set(["create", "createMany", "update", "updateMany", "delete", "deleteMany", "upsert", "findFirst", "findUnique", "findMany", "count", "aggregate"]);
|
|
6924
7176
|
var FS_CALLS = /^(readFile|writeFile|readFileSync|writeFileSync|appendFile|createReadStream|createWriteStream|unlink|rm|rmSync|mkdir|readdir|stat|open)$/;
|
|
@@ -6961,29 +7213,39 @@ function recognizedSinkKinds(pkg) {
|
|
|
6961
7213
|
return kinds;
|
|
6962
7214
|
}
|
|
6963
7215
|
function collectLocalSinks(sf, ts, bindings, ctx) {
|
|
6964
|
-
const
|
|
6965
|
-
const
|
|
6966
|
-
|
|
7216
|
+
const byDeclaration = /* @__PURE__ */ new Map();
|
|
7217
|
+
const byName = /* @__PURE__ */ new Map();
|
|
7218
|
+
const record = (name, body, topLevel) => {
|
|
7219
|
+
const sinks = directSinks(body, ts, bindings, ctx);
|
|
7220
|
+
byDeclaration.set(name, sinks);
|
|
7221
|
+
if (topLevel || !byName.has(name.text)) byName.set(name.text, sinks);
|
|
7222
|
+
};
|
|
7223
|
+
const visit = (node, depth) => {
|
|
7224
|
+
const topLevel = depth === 0;
|
|
7225
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) record(node.name, node.body, topLevel);
|
|
6967
7226
|
else if (ts.isVariableStatement(node)) {
|
|
6968
7227
|
for (const decl of node.declarationList.declarations) {
|
|
6969
7228
|
if (ts.isIdentifier(decl.name) && decl.initializer && isFnLike(decl.initializer, ts)) {
|
|
6970
|
-
|
|
7229
|
+
record(decl.name, decl.initializer.body, topLevel);
|
|
6971
7230
|
}
|
|
6972
7231
|
}
|
|
6973
7232
|
}
|
|
6974
|
-
ts.forEachChild(node, visit);
|
|
7233
|
+
ts.forEachChild(node, (child) => visit(child, depth + (ts.isSourceFile(node) ? 0 : 1)));
|
|
6975
7234
|
};
|
|
6976
|
-
visit(sf);
|
|
6977
|
-
return
|
|
7235
|
+
visit(sf, 0);
|
|
7236
|
+
return { byDeclaration, byName };
|
|
6978
7237
|
}
|
|
6979
7238
|
function sinksFrom(arrowOrNode, ts, localSinks, bindings, ctx) {
|
|
6980
7239
|
if (!arrowOrNode) return [];
|
|
6981
7240
|
const body = arrowOrNode.isSyntheticBody ? arrowOrNode.body : isFnLike(arrowOrNode, ts) ? arrowOrNode.body : arrowOrNode;
|
|
6982
7241
|
if (!body) return [];
|
|
6983
7242
|
const sinks = directSinks(body, ts, bindings, ctx);
|
|
6984
|
-
for (const
|
|
6985
|
-
|
|
6986
|
-
|
|
7243
|
+
for (const callee of localCallIdentifiers(body, ts)) {
|
|
7244
|
+
const called = callee.text;
|
|
7245
|
+
const declaration = declarationOf(callee, ts);
|
|
7246
|
+
const helper = declaration === void 0 ? void 0 : localSinks.byDeclaration.get(declaration);
|
|
7247
|
+
for (const s of helper ?? []) sinks.push(s);
|
|
7248
|
+
if (ctx && (declaration === void 0 || isModuleLevel(declaration, ts))) {
|
|
6987
7249
|
const spec = bindings.resolve(called);
|
|
6988
7250
|
if (spec && spec.startsWith(".")) {
|
|
6989
7251
|
for (const s of ctx.graph.importedSinks(ctx.file, spec, bindings.exportNameOf(called) ?? called)) sinks.push(s);
|
|
@@ -7000,6 +7262,22 @@ function sinksFrom(arrowOrNode, ts, localSinks, bindings, ctx) {
|
|
|
7000
7262
|
}
|
|
7001
7263
|
return dedupeSinks(sinks, ctx?.owner ?? "");
|
|
7002
7264
|
}
|
|
7265
|
+
function isContextRequest(node, ts) {
|
|
7266
|
+
if (!ts.isPropertyAccessExpression(node) || node.name.text !== "req" || !ts.isIdentifier(node.expression)) return false;
|
|
7267
|
+
const declaration = declarationOf(node.expression, ts);
|
|
7268
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7269
|
+
if (ts.isParameter(cur)) return true;
|
|
7270
|
+
if (!ts.isBindingElement(cur) && !ts.isObjectBindingPattern(cur) && !ts.isArrayBindingPattern(cur)) return false;
|
|
7271
|
+
}
|
|
7272
|
+
return false;
|
|
7273
|
+
}
|
|
7274
|
+
function isModuleLevel(declaration, ts) {
|
|
7275
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7276
|
+
if (ts.isSourceFile(cur)) return true;
|
|
7277
|
+
if (ts.isFunctionLike(cur) || ts.isBlock(cur) || ts.isClassLike(cur)) return false;
|
|
7278
|
+
}
|
|
7279
|
+
return false;
|
|
7280
|
+
}
|
|
7003
7281
|
function namespaceMemberCalls(node, ts) {
|
|
7004
7282
|
const out = [];
|
|
7005
7283
|
const visit = (n) => {
|
|
@@ -7060,7 +7338,7 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
7060
7338
|
const prismaLikely = b.pkg === "@prisma/client" || !b.pkg && (bindings.imports.has("@prisma/client") || /prisma/i.test(b.root ?? ""));
|
|
7061
7339
|
if (prismaLikely) push({ kind: "db", ...b.pkg ? { provider: "prisma" } : {}, package: "@prisma/client", table: callee.expression.name.text, op: method, attribution: b.pkg ? "import" : "inferred", ...spanOf(n) });
|
|
7062
7340
|
}
|
|
7063
|
-
if (method === "query" || method === "execute") {
|
|
7341
|
+
if ((method === "query" || method === "execute") && !isContextRequest(callee.expression, ts)) {
|
|
7064
7342
|
const pkg = b.pkg ?? infer("db");
|
|
7065
7343
|
const attribution = attributionOf(b, pkg);
|
|
7066
7344
|
push({ kind: "db", ...dbApi(pkg, attribution), package: pkg, op: method, attribution, ...spanOf(n) });
|
|
@@ -7200,114 +7478,6 @@ function dedupeSinks(sinks, owner) {
|
|
|
7200
7478
|
return out;
|
|
7201
7479
|
}
|
|
7202
7480
|
|
|
7203
|
-
// src/map/module-graph.ts
|
|
7204
|
-
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
7205
|
-
import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
|
|
7206
|
-
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
7207
|
-
function createModuleGraph(ts, opts) {
|
|
7208
|
-
const cache = /* @__PURE__ */ new Map();
|
|
7209
|
-
const load = (file) => {
|
|
7210
|
-
if (cache.has(file)) return cache.get(file) ?? null;
|
|
7211
|
-
let entry = null;
|
|
7212
|
-
try {
|
|
7213
|
-
const text = readFileSync14(file, "utf8");
|
|
7214
|
-
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
7215
|
-
const bindings = buildModuleBindings(sf, ts);
|
|
7216
|
-
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
7217
|
-
} catch {
|
|
7218
|
-
entry = null;
|
|
7219
|
-
}
|
|
7220
|
-
cache.set(file, entry);
|
|
7221
|
-
return entry;
|
|
7222
|
-
};
|
|
7223
|
-
const resolveInProject = (fromFile, specifier) => {
|
|
7224
|
-
const target = resolveRelativeModule(fromFile, specifier);
|
|
7225
|
-
if (!target) return void 0;
|
|
7226
|
-
if (!opts.followOutside) {
|
|
7227
|
-
let real = target;
|
|
7228
|
-
try {
|
|
7229
|
-
real = realpathSync5(target);
|
|
7230
|
-
} catch {
|
|
7231
|
-
}
|
|
7232
|
-
if (!isInside(real, opts.boundary)) return void 0;
|
|
7233
|
-
}
|
|
7234
|
-
return target;
|
|
7235
|
-
};
|
|
7236
|
-
return {
|
|
7237
|
-
resolveLocal: resolveInProject,
|
|
7238
|
-
importedSinks(fromFile, specifier, exportName) {
|
|
7239
|
-
const target = resolveInProject(fromFile, specifier);
|
|
7240
|
-
if (!target) return [];
|
|
7241
|
-
const mod = load(target);
|
|
7242
|
-
if (!mod) return [];
|
|
7243
|
-
const collected = [...mod.fnSinks.get(exportName) ?? []];
|
|
7244
|
-
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
7245
|
-
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
7246
|
-
}
|
|
7247
|
-
const rel = relative5(opts.cwd, target);
|
|
7248
|
-
return collected.map((s) => ({ ...s, file: rel }));
|
|
7249
|
-
},
|
|
7250
|
-
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
7251
|
-
// export TRACE TO". The common AI-built layout puts the client in a lib file —
|
|
7252
|
-
// `export const db = createClient(...)` in `lib/db.ts`, imported everywhere — so the receiver of
|
|
7253
|
-
// `db.from('orders').insert(...)` resolves to a relative specifier and nothing else. Refusing it (as
|
|
7254
|
-
// an unattributable receiver) is right for app code but wrong here: one hop away it is a real
|
|
7255
|
-
// dependency, and that chain is import-to-import, fully static — evidence, not inference.
|
|
7256
|
-
importedBinding(fromFile, specifier, exportName) {
|
|
7257
|
-
const target = resolveInProject(fromFile, specifier);
|
|
7258
|
-
if (!target) return void 0;
|
|
7259
|
-
const mod = load(target);
|
|
7260
|
-
if (!mod) return void 0;
|
|
7261
|
-
const pkg = npmPackageOf(mod.bindings.resolve(exportName));
|
|
7262
|
-
if (pkg === void 0) return void 0;
|
|
7263
|
-
return {
|
|
7264
|
-
package: pkg,
|
|
7265
|
-
origin: mod.bindings.originOf(exportName) ?? "direct",
|
|
7266
|
-
name: mod.bindings.exportNameOf(exportName) ?? exportName
|
|
7267
|
-
};
|
|
7268
|
-
}
|
|
7269
|
-
};
|
|
7270
|
-
}
|
|
7271
|
-
function resolveRelativeModule(fromFile, spec) {
|
|
7272
|
-
if (!spec.startsWith(".")) return void 0;
|
|
7273
|
-
const dir = dirname8(fromFile);
|
|
7274
|
-
const base = resolvePath(dir, spec);
|
|
7275
|
-
const candidates = [];
|
|
7276
|
-
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
7277
|
-
if (jsLike) {
|
|
7278
|
-
const stem = base.slice(0, -jsLike[0].length);
|
|
7279
|
-
for (const e of RESOLVE_EXTS) candidates.push(stem + e);
|
|
7280
|
-
}
|
|
7281
|
-
candidates.push(base);
|
|
7282
|
-
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
7283
|
-
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
7284
|
-
for (const c of candidates) {
|
|
7285
|
-
try {
|
|
7286
|
-
if (statSync7(c).isFile()) return c;
|
|
7287
|
-
} catch {
|
|
7288
|
-
}
|
|
7289
|
-
}
|
|
7290
|
-
return void 0;
|
|
7291
|
-
}
|
|
7292
|
-
function collectCallees(sf, ts) {
|
|
7293
|
-
const map = /* @__PURE__ */ new Map();
|
|
7294
|
-
const visit = (node) => {
|
|
7295
|
-
let name;
|
|
7296
|
-
let body;
|
|
7297
|
-
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
7298
|
-
name = node.name.text;
|
|
7299
|
-
body = node.body;
|
|
7300
|
-
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFnLike(node.initializer, ts)) {
|
|
7301
|
-
name = node.name.text;
|
|
7302
|
-
body = node.initializer.body;
|
|
7303
|
-
}
|
|
7304
|
-
if (name && body) map.set(name, localCalls(body, ts));
|
|
7305
|
-
ts.forEachChild(node, visit);
|
|
7306
|
-
};
|
|
7307
|
-
visit(sf);
|
|
7308
|
-
return map;
|
|
7309
|
-
}
|
|
7310
|
-
|
|
7311
7481
|
// src/map/coordinates.ts
|
|
7312
7482
|
function runtimeCoordinate(source, path17) {
|
|
7313
7483
|
if (/\[\d*\]/.test(path17)) {
|
|
@@ -7379,7 +7549,9 @@ function inputsFromValidator(validatorCall, ts, bindings) {
|
|
|
7379
7549
|
}
|
|
7380
7550
|
function inputsFromHandler(params, body, ts, bindings, opts = {}) {
|
|
7381
7551
|
const schemaSource = opts.validatorSource ?? "json-body";
|
|
7382
|
-
const
|
|
7552
|
+
const inline = zodObjectFields(body, ts, bindings);
|
|
7553
|
+
const referenced = inline.length > 0 ? { fields: [], unresolved: false } : referencedSchemaFields(params, body, ts, bindings);
|
|
7554
|
+
const schemaFields = inline.length > 0 ? inline : referenced.fields;
|
|
7383
7555
|
const reads = requestMemberAccesses(params, body, ts, opts);
|
|
7384
7556
|
const byId = /* @__PURE__ */ new Map();
|
|
7385
7557
|
const put = (name, source, extra = {}) => {
|
|
@@ -7392,7 +7564,58 @@ function inputsFromHandler(params, body, ts, bindings, opts = {}) {
|
|
|
7392
7564
|
put(name, source ?? schemaSource, shape);
|
|
7393
7565
|
}
|
|
7394
7566
|
for (const { name, sources } of reads) for (const source of sources) put(name, source);
|
|
7395
|
-
return [...byId.values()];
|
|
7567
|
+
return { inputs: [...byId.values()], schemaUnresolved: referenced.unresolved };
|
|
7568
|
+
}
|
|
7569
|
+
var SCHEMA_METHODS = /* @__PURE__ */ new Set(["parse", "safeParse", "parseAsync", "safeParseAsync"]);
|
|
7570
|
+
function referencedSchemaFields(params, body, ts, bindings) {
|
|
7571
|
+
if (!body) return { fields: [], unresolved: false };
|
|
7572
|
+
const paramDecls = /* @__PURE__ */ new Set();
|
|
7573
|
+
for (const p of params ?? []) {
|
|
7574
|
+
if (!p?.name) continue;
|
|
7575
|
+
if (ts.isIdentifier(p.name)) paramDecls.add(p.name);
|
|
7576
|
+
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7577
|
+
for (const el of p.name.elements) if (ts.isIdentifier(el.name)) paramDecls.add(el.name);
|
|
7578
|
+
}
|
|
7579
|
+
}
|
|
7580
|
+
const fromParams = (e) => {
|
|
7581
|
+
const root = rootIdentifierNode(e, ts);
|
|
7582
|
+
const declaration = root ? declarationOf(root, ts) : void 0;
|
|
7583
|
+
if (declaration === void 0) return false;
|
|
7584
|
+
if (paramDecls.has(declaration)) return true;
|
|
7585
|
+
const owner = declaration.parent;
|
|
7586
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.name === declaration && owner.initializer) {
|
|
7587
|
+
const inner = rootIdentifierNode(owner.initializer, ts);
|
|
7588
|
+
const innerDeclaration = inner ? declarationOf(inner, ts) : void 0;
|
|
7589
|
+
return innerDeclaration !== void 0 && paramDecls.has(innerDeclaration);
|
|
7590
|
+
}
|
|
7591
|
+
return false;
|
|
7592
|
+
};
|
|
7593
|
+
let fields = [];
|
|
7594
|
+
let unresolved = false;
|
|
7595
|
+
const visit = (n) => {
|
|
7596
|
+
if (fields.length > 0) return;
|
|
7597
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && SCHEMA_METHODS.has(n.expression.name.text) && ts.isIdentifier(n.expression.expression) && n.arguments.some((a) => fromParams(a))) {
|
|
7598
|
+
const declaration = declarationOf(n.expression.expression, ts);
|
|
7599
|
+
const owner = declaration?.parent;
|
|
7600
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.name === declaration && owner.initializer) {
|
|
7601
|
+
const literal = findValidatorObject(owner.initializer, ts, bindings);
|
|
7602
|
+
if (literal) fields = fieldsOfObject(literal, ts, bindings, "");
|
|
7603
|
+
else unresolved = true;
|
|
7604
|
+
} else if (declaration !== void 0 && isImported(declaration, ts)) {
|
|
7605
|
+
unresolved = true;
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
ts.forEachChild(n, visit);
|
|
7609
|
+
};
|
|
7610
|
+
visit(body);
|
|
7611
|
+
return { fields, unresolved: fields.length === 0 && unresolved };
|
|
7612
|
+
}
|
|
7613
|
+
function isImported(declaration, ts) {
|
|
7614
|
+
for (let cur = declaration?.parent; cur; cur = cur.parent) {
|
|
7615
|
+
if (ts.isImportDeclaration(cur)) return true;
|
|
7616
|
+
if (ts.isSourceFile(cur) || ts.isStatement(cur)) return false;
|
|
7617
|
+
}
|
|
7618
|
+
return false;
|
|
7396
7619
|
}
|
|
7397
7620
|
function findValidatorObject(node, ts, bindings) {
|
|
7398
7621
|
let found = null;
|
|
@@ -7471,16 +7694,29 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7471
7694
|
out.set(name, list);
|
|
7472
7695
|
};
|
|
7473
7696
|
const p0 = params?.[0];
|
|
7474
|
-
const
|
|
7697
|
+
const reqDecl = p0 && ts.isIdentifier(p0.name) ? p0.name : void 0;
|
|
7698
|
+
const requestDecls = new Set(reqDecl ? [reqDecl] : []);
|
|
7699
|
+
const isRequest = (e) => ts.isIdentifier(e) && requestDecls.has(declarationOf(e, ts));
|
|
7700
|
+
const isRequestObject = (e) => isRequest(e) || ts.isPropertyAccessExpression(e) && e.name.text === "req" && isRequest(e.expression);
|
|
7701
|
+
const urlDecls = /* @__PURE__ */ new Set();
|
|
7702
|
+
const searchParamDecls = /* @__PURE__ */ new Set();
|
|
7475
7703
|
const sourceNames = /* @__PURE__ */ new Map();
|
|
7476
7704
|
const payloadNames = /* @__PURE__ */ new Set();
|
|
7477
|
-
|
|
7478
|
-
if (
|
|
7705
|
+
const sourceOf = (e) => ts.isIdentifier(e) ? sourceNames.get(declarationOf(e, ts)) : void 0;
|
|
7706
|
+
if (opts.payloadParam && p0 && ts.isIdentifier(p0.name)) payloadNames.add(p0.name);
|
|
7707
|
+
if (p0 && !reqDecl && ts.isObjectBindingPattern(p0.name)) {
|
|
7479
7708
|
for (const el of p0.name.elements) {
|
|
7480
7709
|
const key = bindingKey(el, ts);
|
|
7481
|
-
if (key
|
|
7482
|
-
|
|
7483
|
-
|
|
7710
|
+
if (!key || !ts.isIdentifier(el.name)) continue;
|
|
7711
|
+
if (REQ_SOURCES.includes(key)) sourceNames.set(el.name, namespaceSource(key));
|
|
7712
|
+
else if (key === "request") requestDecls.add(el.name);
|
|
7713
|
+
else if (key === "url") urlDecls.add(el.name);
|
|
7714
|
+
}
|
|
7715
|
+
}
|
|
7716
|
+
for (const p of (params ?? []).slice(1)) {
|
|
7717
|
+
if (!p?.name || !ts.isObjectBindingPattern(p.name)) continue;
|
|
7718
|
+
for (const el of p.name.elements) {
|
|
7719
|
+
if (bindingKey(el, ts) === "params" && ts.isIdentifier(el.name)) sourceNames.set(el.name, "route-param");
|
|
7484
7720
|
}
|
|
7485
7721
|
}
|
|
7486
7722
|
const unwrap = (e) => {
|
|
@@ -7488,29 +7724,74 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7488
7724
|
while (cur && (ts.isAwaitExpression(cur) || ts.isAsExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7489
7725
|
return cur;
|
|
7490
7726
|
};
|
|
7491
|
-
const isPayloadExpr = (e) => ts.isIdentifier(e) && payloadNames.has(e
|
|
7492
|
-
const isReqSourceExpr = (e) => isPayloadExpr(e) || ts.isPropertyAccessExpression(e) &&
|
|
7727
|
+
const isPayloadExpr = (e) => ts.isIdentifier(e) && payloadNames.has(declarationOf(e, ts));
|
|
7728
|
+
const isReqSourceExpr = (e) => isPayloadExpr(e) || ts.isPropertyAccessExpression(e) && isRequest(e.expression) && REQ_SOURCES.includes(e.name.text) || sourceOf(e) !== void 0;
|
|
7493
7729
|
const isBodyReadCall = (e) => {
|
|
7494
7730
|
const inner = unwrap(e);
|
|
7495
|
-
return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) &&
|
|
7731
|
+
return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) && isRequestObject(inner.expression.expression));
|
|
7732
|
+
};
|
|
7733
|
+
const isRequestUrl = (e) => {
|
|
7734
|
+
const cur = unwrap(e);
|
|
7735
|
+
if (!cur) return false;
|
|
7736
|
+
if (ts.isIdentifier(cur)) return urlDecls.has(declarationOf(cur, ts));
|
|
7737
|
+
if (ts.isPropertyAccessExpression(cur) && cur.name.text === "nextUrl") return isRequest(cur.expression);
|
|
7738
|
+
if (ts.isNewExpression(cur) && isGlobal(cur.expression, "URL", ts)) {
|
|
7739
|
+
const [href] = cur.arguments ?? [];
|
|
7740
|
+
return Boolean(href && ts.isPropertyAccessExpression(href) && href.name.text === "url" && isRequestObject(href.expression));
|
|
7741
|
+
}
|
|
7742
|
+
return false;
|
|
7743
|
+
};
|
|
7744
|
+
const isSearchParams = (e) => {
|
|
7745
|
+
const cur = unwrap(e);
|
|
7746
|
+
if (!cur) return false;
|
|
7747
|
+
if (ts.isIdentifier(cur)) return searchParamDecls.has(declarationOf(cur, ts));
|
|
7748
|
+
return ts.isPropertyAccessExpression(cur) && cur.name.text === "searchParams" && isRequestUrl(cur.expression);
|
|
7749
|
+
};
|
|
7750
|
+
const literalArgument = (call) => {
|
|
7751
|
+
const [arg] = call.arguments ?? [];
|
|
7752
|
+
return call.arguments?.length === 1 && arg && ts.isStringLiteral(arg) ? arg.text : void 0;
|
|
7496
7753
|
};
|
|
7754
|
+
const isCallee = (n) => Boolean(n.parent && (ts.isCallExpression(n.parent) || ts.isNewExpression(n.parent)) && n.parent.expression === n);
|
|
7755
|
+
const ACCESSOR_SOURCES = /* @__PURE__ */ new Set(["header", "cookie", "form-body"]);
|
|
7756
|
+
const HONO_ACCESSORS = { query: "query", param: "route-param", header: "header" };
|
|
7497
7757
|
const visit = (n) => {
|
|
7498
|
-
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
|
|
7758
|
+
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
|
|
7499
7759
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
7500
7760
|
}
|
|
7501
7761
|
if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
|
|
7502
7762
|
record(n.argumentExpression.text, sourceOfExpr(n.expression));
|
|
7503
7763
|
}
|
|
7504
|
-
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) {
|
|
7764
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0]) && ACCESSOR_SOURCES.has(sourceOfExpr(n.expression.expression))) {
|
|
7505
7765
|
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
7506
7766
|
}
|
|
7767
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isSearchParams(n.expression.expression)) {
|
|
7768
|
+
const name = literalArgument(n);
|
|
7769
|
+
if (name !== void 0) record(name, "query");
|
|
7770
|
+
}
|
|
7771
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && ts.isPropertyAccessExpression(n.expression.expression) && n.expression.expression.name.text === "req" && isRequest(n.expression.expression.expression)) {
|
|
7772
|
+
const source = HONO_ACCESSORS[n.expression.name.text];
|
|
7773
|
+
const name = literalArgument(n);
|
|
7774
|
+
if (source && name !== void 0) record(name, source);
|
|
7775
|
+
}
|
|
7507
7776
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7508
7777
|
const init = unwrap(n.initializer);
|
|
7509
|
-
if (ts.isIdentifier(n.name)
|
|
7510
|
-
|
|
7778
|
+
if (ts.isIdentifier(n.name)) {
|
|
7779
|
+
if (isRequest(init)) requestDecls.add(n.name);
|
|
7780
|
+
else if (ts.isPropertyAccessExpression(init) && isRequest(init.expression) && REQ_SOURCES.includes(init.name.text)) {
|
|
7781
|
+
sourceNames.set(n.name, namespaceSource(init.name.text));
|
|
7782
|
+
} else if (isRequestUrl(init)) urlDecls.add(n.name);
|
|
7783
|
+
else if (isSearchParams(init)) searchParamDecls.add(n.name);
|
|
7784
|
+
}
|
|
7785
|
+
if (ts.isObjectBindingPattern(n.name) && isRequestUrl(init)) {
|
|
7786
|
+
for (const el of n.name.elements) {
|
|
7787
|
+
if (bindingKey(el, ts) === "searchParams" && ts.isIdentifier(el.name)) searchParamDecls.add(el.name);
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name, bodyReadSource(n.initializer));
|
|
7791
|
+
if (ts.isObjectBindingPattern(n.name) && isRequest(init)) {
|
|
7511
7792
|
for (const el of n.name.elements) {
|
|
7512
7793
|
const key = bindingKey(el, ts);
|
|
7513
|
-
if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) sourceNames.set(el.name
|
|
7794
|
+
if (key && REQ_SOURCES.includes(key) && ts.isIdentifier(el.name)) sourceNames.set(el.name, namespaceSource(key));
|
|
7514
7795
|
}
|
|
7515
7796
|
}
|
|
7516
7797
|
if (ts.isObjectBindingPattern(n.name) && (isReqSourceExpr(init) || isBodyReadCall(n.initializer))) {
|
|
@@ -7532,7 +7813,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7532
7813
|
if (REQ_SOURCES.includes(e.name.text)) return named;
|
|
7533
7814
|
}
|
|
7534
7815
|
if (ts.isIdentifier(e)) {
|
|
7535
|
-
const recorded =
|
|
7816
|
+
const recorded = sourceOf(e);
|
|
7536
7817
|
if (recorded) return recorded;
|
|
7537
7818
|
}
|
|
7538
7819
|
return "body";
|
|
@@ -7553,6 +7834,75 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7553
7834
|
|
|
7554
7835
|
// src/map/flows.ts
|
|
7555
7836
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
|
|
7837
|
+
var MAX_UNPROVEN_FLOWS_PER_ENDPOINT = 200;
|
|
7838
|
+
function boundUnprovenFlows(flows, limit) {
|
|
7839
|
+
let unproven = 0;
|
|
7840
|
+
let truncated = false;
|
|
7841
|
+
const kept = flows.filter((f) => {
|
|
7842
|
+
if (isProvenFlow(f.confidence)) return true;
|
|
7843
|
+
if (unproven >= limit) {
|
|
7844
|
+
truncated = true;
|
|
7845
|
+
return false;
|
|
7846
|
+
}
|
|
7847
|
+
unproven++;
|
|
7848
|
+
return true;
|
|
7849
|
+
});
|
|
7850
|
+
return { flows: kept, truncated };
|
|
7851
|
+
}
|
|
7852
|
+
var Roots = class {
|
|
7853
|
+
constructor(ts) {
|
|
7854
|
+
this.ts = ts;
|
|
7855
|
+
}
|
|
7856
|
+
ts;
|
|
7857
|
+
byDeclaration = /* @__PURE__ */ new Map();
|
|
7858
|
+
add(declaration, root) {
|
|
7859
|
+
if (!this.byDeclaration.has(declaration)) this.byDeclaration.set(declaration, root);
|
|
7860
|
+
}
|
|
7861
|
+
/** The root an identifier occurrence refers to, when its binding is tainted. */
|
|
7862
|
+
get(id) {
|
|
7863
|
+
const declaration = declarationOf(id, this.ts);
|
|
7864
|
+
return declaration === void 0 ? void 0 : this.byDeclaration.get(declaration);
|
|
7865
|
+
}
|
|
7866
|
+
};
|
|
7867
|
+
function reassignedDeclarations(body, ts) {
|
|
7868
|
+
const out = /* @__PURE__ */ new Set();
|
|
7869
|
+
const target = (n) => {
|
|
7870
|
+
if (!n) return;
|
|
7871
|
+
if (ts.isParenthesizedExpression(n)) return target(n.expression);
|
|
7872
|
+
if (ts.isIdentifier(n)) {
|
|
7873
|
+
const declaration = declarationOf(n, ts);
|
|
7874
|
+
if (declaration !== void 0) out.add(declaration);
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
if (ts.isObjectLiteralExpression(n)) {
|
|
7878
|
+
for (const p of n.properties) {
|
|
7879
|
+
if (ts.isShorthandPropertyAssignment(p)) target(p.name);
|
|
7880
|
+
else if (ts.isPropertyAssignment(p)) target(p.initializer);
|
|
7881
|
+
else if (ts.isSpreadAssignment(p)) target(p.expression);
|
|
7882
|
+
}
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7885
|
+
if (ts.isArrayLiteralExpression(n)) {
|
|
7886
|
+
for (const e of n.elements) target(ts.isSpreadElement(e) ? e.expression : e);
|
|
7887
|
+
return;
|
|
7888
|
+
}
|
|
7889
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind === ts.SyntaxKind.EqualsToken) target(n.left);
|
|
7890
|
+
};
|
|
7891
|
+
const visit = (n) => {
|
|
7892
|
+
if (!n) return;
|
|
7893
|
+
if (ts.isBinaryExpression(n) && n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) {
|
|
7894
|
+
target(n.left);
|
|
7895
|
+
}
|
|
7896
|
+
if ((ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) && (n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
7897
|
+
target(n.operand);
|
|
7898
|
+
}
|
|
7899
|
+
if ((ts.isForOfStatement(n) || ts.isForInStatement(n)) && !ts.isVariableDeclarationList(n.initializer)) target(n.initializer);
|
|
7900
|
+
ts.forEachChild(n, visit);
|
|
7901
|
+
};
|
|
7902
|
+
visit(body);
|
|
7903
|
+
return out;
|
|
7904
|
+
}
|
|
7905
|
+
var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
|
|
7556
7906
|
function spaceOfKey(key) {
|
|
7557
7907
|
if (key === "query") return "get";
|
|
7558
7908
|
if (key === "params") return "route-param";
|
|
@@ -7564,9 +7914,11 @@ function spaceOfKey(key) {
|
|
|
7564
7914
|
return void 0;
|
|
7565
7915
|
}
|
|
7566
7916
|
function linkedFlows(body, params, inputs, sinks, ts, invocations = []) {
|
|
7567
|
-
const { flows, limitations, dependencyInputFlows, dependencyInputFlowsTruncated } = linkFlows(body, params, inputs, sinks, ts, invocations);
|
|
7917
|
+
const { flows: linked, limitations, dependencyInputFlows, dependencyInputFlowsTruncated } = linkFlows(body, params, inputs, sinks, ts, invocations);
|
|
7918
|
+
const { flows, truncated } = boundUnprovenFlows(linked, MAX_UNPROVEN_FLOWS_PER_ENDPOINT);
|
|
7568
7919
|
return {
|
|
7569
7920
|
flows,
|
|
7921
|
+
...truncated ? { flowsTruncated: true } : {},
|
|
7570
7922
|
...limitations.length > 0 ? { limitations } : {},
|
|
7571
7923
|
...dependencyInputFlows.length > 0 ? { dependencyInputFlows } : {},
|
|
7572
7924
|
...dependencyInputFlowsTruncated ? { dependencyInputFlowsTruncated: true } : {}
|
|
@@ -7577,19 +7929,29 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7577
7929
|
return { flows: [], limitations: [], dependencyInputFlows: [], dependencyInputFlowsTruncated: false };
|
|
7578
7930
|
}
|
|
7579
7931
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
7580
|
-
const rootPath =
|
|
7581
|
-
const
|
|
7582
|
-
|
|
7932
|
+
const rootPath = new Roots(ts);
|
|
7933
|
+
const reassigned = reassignedDeclarations(bodyNode, ts);
|
|
7934
|
+
const addRoot = (declaration, path17, space, accessor = false, request = false, inherited = false, url = false) => {
|
|
7935
|
+
const changed = inherited || reassigned.has(declaration);
|
|
7936
|
+
rootPath.add(declaration, { path: path17, space, ...accessor ? { accessor } : {}, ...request ? { request } : {}, ...changed ? { reassigned: true } : {}, ...url ? { url } : {} });
|
|
7583
7937
|
};
|
|
7584
|
-
for (const p of params ?? []) {
|
|
7938
|
+
for (const [index, p] of (params ?? []).entries()) {
|
|
7585
7939
|
if (!p?.name) continue;
|
|
7586
|
-
if (ts.isIdentifier(p.name)) addRoot(p.name
|
|
7940
|
+
if (ts.isIdentifier(p.name)) addRoot(p.name, "", void 0, false, index === 0);
|
|
7587
7941
|
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7588
7942
|
for (const el of p.name.elements) {
|
|
7589
7943
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7590
7944
|
const key = bindingKey(el, ts);
|
|
7945
|
+
if (index === 0 && key === "request") {
|
|
7946
|
+
addRoot(el.name, "", void 0, false, true);
|
|
7947
|
+
continue;
|
|
7948
|
+
}
|
|
7949
|
+
if (index === 0 && key === "url") {
|
|
7950
|
+
addRoot(el.name, "", void 0, false, false, false, true);
|
|
7951
|
+
continue;
|
|
7952
|
+
}
|
|
7591
7953
|
const container = key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7592
|
-
addRoot(el.name
|
|
7954
|
+
addRoot(el.name, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0, container && ACCESSOR_NAMESPACES.has(key));
|
|
7593
7955
|
}
|
|
7594
7956
|
}
|
|
7595
7957
|
}
|
|
@@ -7597,11 +7959,17 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7597
7959
|
let cur = init;
|
|
7598
7960
|
while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isAsExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7599
7961
|
if (!cur) return void 0;
|
|
7962
|
+
const params2 = searchParamsOrigin(cur, ts, rootPath);
|
|
7963
|
+
if (params2) return { path: "", space: "get", accessor: true, ...carried(params2) };
|
|
7964
|
+
const url = requestUrlOrigin(cur, ts, rootPath);
|
|
7965
|
+
if (url) return { path: "", url: true, ...carried(url) };
|
|
7600
7966
|
if (ts.isCallExpression(cur) && ts.isPropertyAccessExpression(cur.expression)) {
|
|
7601
7967
|
const m = cur.expression.name.text;
|
|
7602
7968
|
if (["json", "formData", "text"].includes(m)) {
|
|
7603
|
-
const root =
|
|
7604
|
-
|
|
7969
|
+
const root = rootIdentifierNode(cur.expression.expression, ts);
|
|
7970
|
+
const owner = root ? rootPath.get(root) : void 0;
|
|
7971
|
+
if (!owner) return void 0;
|
|
7972
|
+
return m === "formData" ? { path: "", space: "post", accessor: true, ...carried(owner) } : { path: "", space: "post", ...carried(owner) };
|
|
7605
7973
|
}
|
|
7606
7974
|
if (["parse", "safeParse", "validate", "cast"].includes(m)) {
|
|
7607
7975
|
for (const a of cur.arguments) {
|
|
@@ -7611,21 +7979,25 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7611
7979
|
return void 0;
|
|
7612
7980
|
}
|
|
7613
7981
|
}
|
|
7614
|
-
|
|
7615
|
-
return p;
|
|
7982
|
+
return pathFromTainted(cur, ts, rootPath) ?? accessorRead(cur, ts, rootPath);
|
|
7616
7983
|
};
|
|
7617
7984
|
const aliasVisit = (n) => {
|
|
7618
7985
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7619
7986
|
const base = requestReadPath(n.initializer);
|
|
7620
7987
|
if (base !== void 0) {
|
|
7621
|
-
if (ts.isIdentifier(n.name)) addRoot(n.name
|
|
7988
|
+
if (ts.isIdentifier(n.name)) addRoot(n.name, base.path, base.space, base.accessor === true, base.request === true, base.reassigned === true, base.url === true);
|
|
7622
7989
|
else if (ts.isObjectBindingPattern(n.name)) {
|
|
7623
7990
|
for (const el of n.name.elements) {
|
|
7624
7991
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7625
7992
|
const key = bindingKey(el, ts);
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7993
|
+
if (base.url === true) {
|
|
7994
|
+
if (key === "searchParams") addRoot(el.name, "", "get", true, false, base.reassigned === true);
|
|
7995
|
+
continue;
|
|
7996
|
+
}
|
|
7997
|
+
const namespace = base.request === true && key !== void 0 && REQ_SOURCES.includes(key);
|
|
7998
|
+
const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key) && (namespace || !REQ_SOURCES.includes(key));
|
|
7999
|
+
const space = base.space ?? (namespace ? spaceOfKey(key) : void 0);
|
|
8000
|
+
addRoot(el.name, container ? "" : join22(base.path, key ?? el.name.text), space, namespace && ACCESSOR_NAMESPACES.has(key), false, base.reassigned === true);
|
|
7629
8001
|
}
|
|
7630
8002
|
}
|
|
7631
8003
|
}
|
|
@@ -7657,10 +8029,10 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7657
8029
|
for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
|
|
7658
8030
|
for (let i = 0; i < args.length; i++) {
|
|
7659
8031
|
const role = argumentRoleOf(sink.kind, method, i, args.length);
|
|
7660
|
-
const whole = pathFromTainted(args[i], ts, rootPath);
|
|
8032
|
+
const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
|
|
7661
8033
|
for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
|
|
7662
8034
|
const key = `${read2.space ?? "*"}:${read2.path}`;
|
|
7663
|
-
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
|
|
8035
|
+
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space && whole.reassigned !== true;
|
|
7664
8036
|
const entry = reads.get(key) ?? { read: read2, roles: /* @__PURE__ */ new Set(), exact: false };
|
|
7665
8037
|
entry.roles.add(role);
|
|
7666
8038
|
entry.exact = entry.exact || exact;
|
|
@@ -7750,7 +8122,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7750
8122
|
kind: invocation.kind,
|
|
7751
8123
|
resolution: invocation.resolution,
|
|
7752
8124
|
argumentIndex,
|
|
7753
|
-
argumentUse: whole?.path === read2.path && whole.space === read2.space ? "direct" : "within-expression",
|
|
8125
|
+
argumentUse: whole?.path === read2.path && whole.space === read2.space && whole.reassigned !== true ? "direct" : "within-expression",
|
|
7754
8126
|
line: site.line,
|
|
7755
8127
|
start: site.start,
|
|
7756
8128
|
end: site.end
|
|
@@ -7819,6 +8191,18 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7819
8191
|
const visit = (n) => {
|
|
7820
8192
|
if (!n) return;
|
|
7821
8193
|
if (!includeDeferredBodies && (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))) return;
|
|
8194
|
+
if (ts.isCallExpression(n)) {
|
|
8195
|
+
const accessed = accessorRead(n, ts, rootPath);
|
|
8196
|
+
if (accessed !== void 0) {
|
|
8197
|
+
add(accessed);
|
|
8198
|
+
return;
|
|
8199
|
+
}
|
|
8200
|
+
if (ts.isPropertyAccessExpression(n.expression)) {
|
|
8201
|
+
visit(n.expression.expression);
|
|
8202
|
+
for (const arg of n.arguments ?? []) visit(arg);
|
|
8203
|
+
return;
|
|
8204
|
+
}
|
|
8205
|
+
}
|
|
7822
8206
|
if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
|
|
7823
8207
|
const read2 = pathFromTainted(n, ts, rootPath);
|
|
7824
8208
|
if (read2 !== void 0) {
|
|
@@ -7826,9 +8210,9 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7826
8210
|
return;
|
|
7827
8211
|
}
|
|
7828
8212
|
}
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
if (
|
|
8213
|
+
const bound = ts.isIdentifier(n) && isValueRead(n, ts) ? rootPath.get(n) : void 0;
|
|
8214
|
+
if (bound) {
|
|
8215
|
+
if (bound.path) add({ path: normalizePath(bound.path), space: bound.space });
|
|
7832
8216
|
}
|
|
7833
8217
|
ts.forEachChild(n, visit);
|
|
7834
8218
|
};
|
|
@@ -7838,11 +8222,11 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7838
8222
|
function sinkArgumentLimitations(node, ts, rootPath) {
|
|
7839
8223
|
const out = [];
|
|
7840
8224
|
const seen = /* @__PURE__ */ new Set();
|
|
7841
|
-
const add = (kind,
|
|
7842
|
-
const key = `${kind}:${
|
|
8225
|
+
const add = (kind, detail2, n) => {
|
|
8226
|
+
const key = `${kind}:${detail2}`;
|
|
7843
8227
|
if (seen.has(key)) return;
|
|
7844
8228
|
seen.add(key);
|
|
7845
|
-
out.push({ kind, detail, line: lineOf(n) });
|
|
8229
|
+
out.push({ kind, detail: detail2, line: lineOf(n) });
|
|
7846
8230
|
};
|
|
7847
8231
|
const text = (n) => {
|
|
7848
8232
|
try {
|
|
@@ -7854,22 +8238,90 @@ function sinkArgumentLimitations(node, ts, rootPath) {
|
|
|
7854
8238
|
const visit = (n) => {
|
|
7855
8239
|
if (!n) return;
|
|
7856
8240
|
if (ts.isElementAccessExpression(n)) {
|
|
7857
|
-
const root =
|
|
8241
|
+
const root = rootIdentifierNode(n.expression, ts);
|
|
7858
8242
|
const arg = n.argumentExpression;
|
|
7859
|
-
if (root && rootPath.
|
|
8243
|
+
if (root && rootPath.get(root) && arg && !ts.isStringLiteralLike(arg) && !ts.isNumericLiteral(arg)) {
|
|
7860
8244
|
add("dynamic-key", text(n), n);
|
|
7861
8245
|
}
|
|
7862
8246
|
}
|
|
7863
8247
|
if ((ts.isSpreadAssignment?.(n) || ts.isSpreadElement(n)) && n.expression) {
|
|
7864
|
-
const root =
|
|
7865
|
-
if (root && rootPath.
|
|
8248
|
+
const root = rootIdentifierNode(n.expression, ts);
|
|
8249
|
+
if (root && rootPath.get(root)) add("spread-into-sink", text(n.parent ?? n), n);
|
|
7866
8250
|
}
|
|
7867
8251
|
ts.forEachChild(n, visit);
|
|
7868
8252
|
};
|
|
7869
8253
|
visit(node);
|
|
7870
8254
|
return out;
|
|
7871
8255
|
}
|
|
7872
|
-
function
|
|
8256
|
+
function accessorRead(node, ts, rootPath) {
|
|
8257
|
+
let cur = node;
|
|
8258
|
+
while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
8259
|
+
if (!cur || !ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return void 0;
|
|
8260
|
+
const [name] = cur.arguments;
|
|
8261
|
+
if (cur.arguments.length !== 1 || !name || !ts.isStringLiteralLike(name)) return void 0;
|
|
8262
|
+
const method = cur.expression.name.text;
|
|
8263
|
+
const receiver = cur.expression.expression;
|
|
8264
|
+
const hono = HONO_ACCESSOR_SPACES[method];
|
|
8265
|
+
if (hono && ts.isPropertyAccessExpression(receiver) && receiver.name.text === "req" && ts.isIdentifier(receiver.expression)) {
|
|
8266
|
+
const context = rootPath.get(receiver.expression);
|
|
8267
|
+
if (context?.request === true) return { path: normalizePath(name.text), space: hono, ...carried(context) };
|
|
8268
|
+
}
|
|
8269
|
+
if (method !== "get") return void 0;
|
|
8270
|
+
const params = ts.isIdentifier(receiver) ? void 0 : searchParamsOrigin(receiver, ts, rootPath);
|
|
8271
|
+
if (params) return { path: normalizePath(name.text), space: "get", ...carried(params) };
|
|
8272
|
+
if (!isAccessor(receiver, ts, rootPath)) return void 0;
|
|
8273
|
+
return pathFromTainted(receiver, ts, rootPath, [name.text]);
|
|
8274
|
+
}
|
|
8275
|
+
var HONO_ACCESSOR_SPACES = { query: "get", param: "route-param", header: "server" };
|
|
8276
|
+
function carried(origin) {
|
|
8277
|
+
return origin.reassigned === true ? { reassigned: true } : {};
|
|
8278
|
+
}
|
|
8279
|
+
function requestUrlOrigin(node, ts, rootPath) {
|
|
8280
|
+
let cur = node;
|
|
8281
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur) || ts.isAwaitExpression(cur) || ts.isAsExpression(cur))) cur = cur.expression;
|
|
8282
|
+
if (!cur) return void 0;
|
|
8283
|
+
if (ts.isIdentifier(cur)) {
|
|
8284
|
+
const root = rootPath.get(cur);
|
|
8285
|
+
return root?.url === true ? root : void 0;
|
|
8286
|
+
}
|
|
8287
|
+
const request = (e) => {
|
|
8288
|
+
let inner = e;
|
|
8289
|
+
while (inner && (ts.isParenthesizedExpression(inner) || ts.isNonNullExpression(inner) || ts.isAsExpression(inner))) inner = inner.expression;
|
|
8290
|
+
if (!inner || !ts.isIdentifier(inner)) return void 0;
|
|
8291
|
+
const root = rootPath.get(inner);
|
|
8292
|
+
return root?.request === true ? root : void 0;
|
|
8293
|
+
};
|
|
8294
|
+
if (ts.isPropertyAccessExpression(cur) && cur.name.text === "nextUrl") return request(cur.expression);
|
|
8295
|
+
if (ts.isNewExpression(cur) && isGlobal(cur.expression, "URL", ts)) {
|
|
8296
|
+
const [href] = cur.arguments ?? [];
|
|
8297
|
+
if (!href || !ts.isPropertyAccessExpression(href) || href.name.text !== "url") return void 0;
|
|
8298
|
+
const owner = href.expression;
|
|
8299
|
+
if (ts.isPropertyAccessExpression(owner) && owner.name.text === "req") return request(owner.expression) ?? request(owner);
|
|
8300
|
+
return request(owner);
|
|
8301
|
+
}
|
|
8302
|
+
return void 0;
|
|
8303
|
+
}
|
|
8304
|
+
function searchParamsOrigin(node, ts, rootPath) {
|
|
8305
|
+
let cur = node;
|
|
8306
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur) || ts.isAsExpression(cur))) cur = cur.expression;
|
|
8307
|
+
if (!cur) return void 0;
|
|
8308
|
+
if (ts.isIdentifier(cur)) {
|
|
8309
|
+
const root = rootPath.get(cur);
|
|
8310
|
+
return root?.accessor === true && root.space === "get" ? root : void 0;
|
|
8311
|
+
}
|
|
8312
|
+
if (!ts.isPropertyAccessExpression(cur) || cur.name.text !== "searchParams") return void 0;
|
|
8313
|
+
return requestUrlOrigin(cur.expression, ts, rootPath);
|
|
8314
|
+
}
|
|
8315
|
+
function isAccessor(node, ts, rootPath) {
|
|
8316
|
+
let cur = node;
|
|
8317
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
8318
|
+
if (ts.isIdentifier(cur)) return rootPath.get(cur)?.accessor === true;
|
|
8319
|
+
if (ts.isPropertyAccessExpression(cur) && ACCESSOR_NAMESPACES.has(cur.name.text) && ts.isIdentifier(cur.expression)) {
|
|
8320
|
+
return rootPath.get(cur.expression)?.request === true;
|
|
8321
|
+
}
|
|
8322
|
+
return false;
|
|
8323
|
+
}
|
|
8324
|
+
function pathFromTainted(node, ts, rootPath, trailing = []) {
|
|
7873
8325
|
const segs = [];
|
|
7874
8326
|
let cur = node;
|
|
7875
8327
|
for (; ; ) {
|
|
@@ -7891,14 +8343,128 @@ function pathFromTainted(node, ts, rootPath) {
|
|
|
7891
8343
|
break;
|
|
7892
8344
|
}
|
|
7893
8345
|
if (!cur || !ts.isIdentifier(cur)) return void 0;
|
|
7894
|
-
const base = rootPath.get(cur
|
|
7895
|
-
if (base === void 0) return void 0;
|
|
8346
|
+
const base = rootPath.get(cur);
|
|
8347
|
+
if (base === void 0 || base.url === true) return void 0;
|
|
8348
|
+
segs.push(...trailing);
|
|
7896
8349
|
let space = base.space;
|
|
7897
|
-
|
|
7898
|
-
|
|
8350
|
+
let namespace;
|
|
8351
|
+
if (base.request === true && base.path === "" && segs.length > 0 && REQ_SOURCES.includes(segs[0])) {
|
|
8352
|
+
namespace = segs[0];
|
|
8353
|
+
space = spaceOfKey(namespace) ?? space;
|
|
7899
8354
|
segs.shift();
|
|
7900
8355
|
}
|
|
7901
|
-
|
|
8356
|
+
const request = base.request === true && namespace === void 0 && segs.length === 0 ? { request: true } : {};
|
|
8357
|
+
const accessor = namespace !== void 0 && segs.length === 0 && ACCESSOR_NAMESPACES.has(namespace) ? { accessor: true } : {};
|
|
8358
|
+
const reassigned = base.reassigned === true ? { reassigned: true } : {};
|
|
8359
|
+
return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request, ...accessor, ...reassigned };
|
|
8360
|
+
}
|
|
8361
|
+
|
|
8362
|
+
// src/map/module-graph.ts
|
|
8363
|
+
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
8364
|
+
import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
|
|
8365
|
+
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
8366
|
+
function createModuleGraph(ts, opts) {
|
|
8367
|
+
const cache = /* @__PURE__ */ new Map();
|
|
8368
|
+
const load = (file) => {
|
|
8369
|
+
if (cache.has(file)) return cache.get(file) ?? null;
|
|
8370
|
+
let entry = null;
|
|
8371
|
+
try {
|
|
8372
|
+
const text = readFileSync14(file, "utf8");
|
|
8373
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
8374
|
+
const bindings = buildModuleBindings(sf, ts);
|
|
8375
|
+
entry = { fnSinks: collectLocalSinks(sf, ts, bindings).byName, calleesOf: collectCallees(sf, ts), bindings };
|
|
8376
|
+
} catch {
|
|
8377
|
+
entry = null;
|
|
8378
|
+
}
|
|
8379
|
+
cache.set(file, entry);
|
|
8380
|
+
return entry;
|
|
8381
|
+
};
|
|
8382
|
+
const resolveInProject = (fromFile, specifier) => {
|
|
8383
|
+
const target = resolveRelativeModule(fromFile, specifier);
|
|
8384
|
+
if (!target) return void 0;
|
|
8385
|
+
if (!opts.followOutside) {
|
|
8386
|
+
let real = target;
|
|
8387
|
+
try {
|
|
8388
|
+
real = realpathSync5(target);
|
|
8389
|
+
} catch {
|
|
8390
|
+
}
|
|
8391
|
+
if (!isInside(real, opts.boundary)) return void 0;
|
|
8392
|
+
}
|
|
8393
|
+
return target;
|
|
8394
|
+
};
|
|
8395
|
+
return {
|
|
8396
|
+
resolveLocal: resolveInProject,
|
|
8397
|
+
importedSinks(fromFile, specifier, exportName) {
|
|
8398
|
+
const target = resolveInProject(fromFile, specifier);
|
|
8399
|
+
if (!target) return [];
|
|
8400
|
+
const mod = load(target);
|
|
8401
|
+
if (!mod) return [];
|
|
8402
|
+
const collected = [...mod.fnSinks.get(exportName) ?? []];
|
|
8403
|
+
for (const callee of mod.calleesOf.get(exportName) ?? []) {
|
|
8404
|
+
for (const s of mod.fnSinks.get(callee) ?? []) collected.push(s);
|
|
8405
|
+
}
|
|
8406
|
+
const rel = relative5(opts.cwd, target);
|
|
8407
|
+
return collected.map((s) => ({ ...s, file: rel }));
|
|
8408
|
+
},
|
|
8409
|
+
// A different question about the same module: not "what sinks are in there" but "what does this
|
|
8410
|
+
// export TRACE TO". The common AI-built layout puts the client in a lib file —
|
|
8411
|
+
// `export const db = createClient(...)` in `lib/db.ts`, imported everywhere — so the receiver of
|
|
8412
|
+
// `db.from('orders').insert(...)` resolves to a relative specifier and nothing else. Refusing it (as
|
|
8413
|
+
// an unattributable receiver) is right for app code but wrong here: one hop away it is a real
|
|
8414
|
+
// dependency, and that chain is import-to-import, fully static — evidence, not inference.
|
|
8415
|
+
importedBinding(fromFile, specifier, exportName) {
|
|
8416
|
+
const target = resolveInProject(fromFile, specifier);
|
|
8417
|
+
if (!target) return void 0;
|
|
8418
|
+
const mod = load(target);
|
|
8419
|
+
if (!mod) return void 0;
|
|
8420
|
+
const pkg = npmPackageOf(mod.bindings.resolve(exportName));
|
|
8421
|
+
if (pkg === void 0) return void 0;
|
|
8422
|
+
return {
|
|
8423
|
+
package: pkg,
|
|
8424
|
+
origin: mod.bindings.originOf(exportName) ?? "direct",
|
|
8425
|
+
name: mod.bindings.exportNameOf(exportName) ?? exportName
|
|
8426
|
+
};
|
|
8427
|
+
}
|
|
8428
|
+
};
|
|
8429
|
+
}
|
|
8430
|
+
function resolveRelativeModule(fromFile, spec) {
|
|
8431
|
+
if (!spec.startsWith(".")) return void 0;
|
|
8432
|
+
const dir = dirname8(fromFile);
|
|
8433
|
+
const base = resolvePath(dir, spec);
|
|
8434
|
+
const candidates = [];
|
|
8435
|
+
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
8436
|
+
if (jsLike) {
|
|
8437
|
+
const stem = base.slice(0, -jsLike[0].length);
|
|
8438
|
+
for (const e of RESOLVE_EXTS) candidates.push(stem + e);
|
|
8439
|
+
}
|
|
8440
|
+
candidates.push(base);
|
|
8441
|
+
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
8442
|
+
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
8443
|
+
for (const c of candidates) {
|
|
8444
|
+
try {
|
|
8445
|
+
if (statSync7(c).isFile()) return c;
|
|
8446
|
+
} catch {
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
8449
|
+
return void 0;
|
|
8450
|
+
}
|
|
8451
|
+
function collectCallees(sf, ts) {
|
|
8452
|
+
const map = /* @__PURE__ */ new Map();
|
|
8453
|
+
const visit = (node) => {
|
|
8454
|
+
let name;
|
|
8455
|
+
let body;
|
|
8456
|
+
if (ts.isFunctionDeclaration(node) && node.name && node.body) {
|
|
8457
|
+
name = node.name.text;
|
|
8458
|
+
body = node.body;
|
|
8459
|
+
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isFnLike(node.initializer, ts)) {
|
|
8460
|
+
name = node.name.text;
|
|
8461
|
+
body = node.initializer.body;
|
|
8462
|
+
}
|
|
8463
|
+
if (name && body) map.set(name, localCalls(body, ts));
|
|
8464
|
+
ts.forEachChild(node, visit);
|
|
8465
|
+
};
|
|
8466
|
+
visit(sf);
|
|
8467
|
+
return map;
|
|
7902
8468
|
}
|
|
7903
8469
|
|
|
7904
8470
|
// src/map/invocations.ts
|
|
@@ -8020,6 +8586,7 @@ var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELE
|
|
|
8020
8586
|
function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
8021
8587
|
const out = [];
|
|
8022
8588
|
const isServerActionsFile = fileHasUseServer(sf, ts);
|
|
8589
|
+
const pagesApi = isPagesApiFile(ctx.owner);
|
|
8023
8590
|
const visit = (node) => {
|
|
8024
8591
|
if (ts.isVariableStatement(node) && hasExport(node, ts)) {
|
|
8025
8592
|
for (const decl of node.declarationList.declarations) {
|
|
@@ -8061,6 +8628,12 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8061
8628
|
out.push(handlerEntry(node.name.text, "server-action", node.parameters, node.body, ts, localSinks, bindings, ctx, spanOf(node)));
|
|
8062
8629
|
}
|
|
8063
8630
|
}
|
|
8631
|
+
if (pagesApi) {
|
|
8632
|
+
const handler = defaultExportedHandler(node, ts);
|
|
8633
|
+
if (handler) {
|
|
8634
|
+
out.push(handlerEntry(handler.name ?? "default", "default-export", handler.fn.parameters, handler.fn.body, ts, localSinks, bindings, ctx, spanOf(handler.fn)));
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8064
8637
|
if (ts.isCallExpression(node)) {
|
|
8065
8638
|
const c = node.expression;
|
|
8066
8639
|
const denoServe = ts.isPropertyAccessExpression(c) && c.name.text === "serve" && ts.isIdentifier(c.expression) && c.expression.text === "Deno";
|
|
@@ -8079,7 +8652,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8079
8652
|
const first = args[0];
|
|
8080
8653
|
const route = first && ts.isStringLiteralLike(first) ? first.text : routeFromChain(node.expression.expression, ts);
|
|
8081
8654
|
const handler = args[args.length - 1];
|
|
8082
|
-
if (route !== void 0 && handler && isFnLike(handler, ts)) {
|
|
8655
|
+
if (route !== void 0 && isRoutePath(route) && handler && isFnLike(handler, ts)) {
|
|
8083
8656
|
out.push(handlerEntry(route, "route-registration", handler.parameters, handler.body, ts, localSinks, bindings, ctx, {
|
|
8084
8657
|
// `use`/`all` register handlers but are not HTTP methods — leave method undefined.
|
|
8085
8658
|
method: HTTP_METHODS.has(mname.toUpperCase()) ? mname.toUpperCase() : void 0,
|
|
@@ -8092,7 +8665,7 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8092
8665
|
const arg = node.arguments[0];
|
|
8093
8666
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
8094
8667
|
const reg = routeObject(arg, ts);
|
|
8095
|
-
if (reg.url && reg.handler) {
|
|
8668
|
+
if (reg.url && isRoutePath(reg.url) && reg.handler) {
|
|
8096
8669
|
for (const m of reg.methods.length ? reg.methods : [void 0]) {
|
|
8097
8670
|
out.push(handlerEntry(reg.url, "route-registration", reg.handler.parameters, reg.handler.body, ts, localSinks, bindings, ctx, { method: m, route: reg.url, ...spanOf(node) }));
|
|
8098
8671
|
}
|
|
@@ -8105,6 +8678,19 @@ function extractFromFile(sf, ts, localSinks, bindings, ctx) {
|
|
|
8105
8678
|
visit(sf);
|
|
8106
8679
|
return out;
|
|
8107
8680
|
}
|
|
8681
|
+
function defaultExportedHandler(node, ts) {
|
|
8682
|
+
const isDefault = (n) => Boolean(n.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
8683
|
+
if (ts.isFunctionDeclaration(node) && hasExport(node, ts) && isDefault(node) && node.body) return { name: node.name?.text, fn: node };
|
|
8684
|
+
if (!ts.isExportAssignment(node) || node.isExportEquals) return void 0;
|
|
8685
|
+
const target = node.expression;
|
|
8686
|
+
if (isFnLike(target, ts)) return { fn: target };
|
|
8687
|
+
if (!ts.isIdentifier(target)) return void 0;
|
|
8688
|
+
const declaration = declarationOf(target, ts);
|
|
8689
|
+
const owner = declaration?.parent;
|
|
8690
|
+
if (owner && ts.isFunctionDeclaration(owner) && owner.body) return { name: target.text, fn: owner };
|
|
8691
|
+
if (owner && ts.isVariableDeclaration(owner) && owner.initializer && isFnLike(owner.initializer, ts)) return { name: target.text, fn: owner.initializer };
|
|
8692
|
+
return void 0;
|
|
8693
|
+
}
|
|
8108
8694
|
function fileHasUseServer(sf, ts) {
|
|
8109
8695
|
const first = sf.statements?.[0];
|
|
8110
8696
|
return Boolean(first && ts.isExpressionStatement(first) && ts.isStringLiteralLike(first.expression) && first.expression.text === "use server");
|
|
@@ -8120,7 +8706,7 @@ function handlerInvocations(body, ts, bindings, ctx) {
|
|
|
8120
8706
|
function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, ctx, extra = {}) {
|
|
8121
8707
|
const entryKind = kindLabel === "route-registration" || kindLabel === "server-action" || kindLabel === "edge-function" ? kindLabel : "route-handler";
|
|
8122
8708
|
const payloadStyle = kindLabel === "server-action";
|
|
8123
|
-
const inputs = inputsFromHandler(params, body, ts, bindings, {
|
|
8709
|
+
const { inputs, schemaUnresolved } = inputsFromHandler(params, body, ts, bindings, {
|
|
8124
8710
|
payloadParam: payloadStyle,
|
|
8125
8711
|
validatorSource: payloadStyle ? "server-fn-data" : "json-body"
|
|
8126
8712
|
});
|
|
@@ -8135,7 +8721,9 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
8135
8721
|
end: extra.end,
|
|
8136
8722
|
inputs,
|
|
8137
8723
|
sinks,
|
|
8138
|
-
...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx))
|
|
8724
|
+
...linkedFlows(body, params, inputs, sinks, ts, handlerInvocations(body, ts, bindings, ctx)),
|
|
8725
|
+
// The handler validates its request with a schema declared in another module: its fields are unknown.
|
|
8726
|
+
...schemaUnresolved ? { inputsResolved: false } : {}
|
|
8139
8727
|
};
|
|
8140
8728
|
}
|
|
8141
8729
|
|
|
@@ -8394,6 +8982,7 @@ function lineAt(starts, pos) {
|
|
|
8394
8982
|
|
|
8395
8983
|
// src/map/extract.ts
|
|
8396
8984
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_MAP = 500;
|
|
8985
|
+
var MAX_UNPROVEN_FLOWS_PER_MAP = 1e3;
|
|
8397
8986
|
async function extractInputMap(cwd, ts, options = {}) {
|
|
8398
8987
|
const notes = [];
|
|
8399
8988
|
const endpoints = [];
|
|
@@ -8409,9 +8998,11 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8409
8998
|
const imports = createImportInventory(readPathAliases(cwd));
|
|
8410
8999
|
const invocations = createInvocationInventory();
|
|
8411
9000
|
let dependencyInputFlowCount = 0;
|
|
9001
|
+
let unprovenFlowCount = 0;
|
|
8412
9002
|
let parsed = 0;
|
|
8413
9003
|
let preFiltered = 0;
|
|
8414
9004
|
let importScanFailures = 0;
|
|
9005
|
+
let componentFiles = 0;
|
|
8415
9006
|
let unresolvableImports = 0;
|
|
8416
9007
|
let sourceBytes = 0;
|
|
8417
9008
|
const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
|
|
@@ -8423,7 +9014,17 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8423
9014
|
const text = readFileSync16(file, "utf8");
|
|
8424
9015
|
const relFile = relative6(cwd, file);
|
|
8425
9016
|
sourceBytes += text.length;
|
|
8426
|
-
if (
|
|
9017
|
+
if (isComponentFile(file)) {
|
|
9018
|
+
componentFiles++;
|
|
9019
|
+
preFiltered++;
|
|
9020
|
+
const script = componentScript(text, file);
|
|
9021
|
+
const scanned = script === null ? null : scanFileImports(script, ts);
|
|
9022
|
+
if (scanned === null) importScanFailures++;
|
|
9023
|
+
else imports.add(relFile, scanned, false);
|
|
9024
|
+
if (script !== null) unresolvableImports += countUnresolvableImports(script, ts);
|
|
9025
|
+
continue;
|
|
9026
|
+
}
|
|
9027
|
+
if (!hasEntrySignal(text) && !isPagesApiFile(relFile)) {
|
|
8427
9028
|
preFiltered++;
|
|
8428
9029
|
const scanned = scanFileImports(text, ts);
|
|
8429
9030
|
if (scanned === null) importScanFailures++;
|
|
@@ -8459,11 +9060,17 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8459
9060
|
ep.dependencyInputFlowsTruncated = true;
|
|
8460
9061
|
}
|
|
8461
9062
|
dependencyInputFlowCount += ep.dependencyInputFlows?.length ?? 0;
|
|
9063
|
+
const bounded = boundUnprovenFlows(ep.flows, Math.max(0, MAX_UNPROVEN_FLOWS_PER_MAP - unprovenFlowCount));
|
|
9064
|
+
if (bounded.truncated) {
|
|
9065
|
+
ep.flows = bounded.flows;
|
|
9066
|
+
ep.flowsTruncated = true;
|
|
9067
|
+
}
|
|
9068
|
+
unprovenFlowCount += ep.flows.filter((f) => !isProvenFlow(f.confidence)).length;
|
|
8462
9069
|
if (ep.route === void 0 && ep.entryKind === "edge-function") {
|
|
8463
9070
|
const fn = functionNameFromPath(relFile);
|
|
8464
9071
|
if (fn) ep.route = "/" + fn;
|
|
8465
9072
|
}
|
|
8466
|
-
if (ep.route === void 0 &&
|
|
9073
|
+
if (ep.route === void 0 && ep.entryKind === "route-handler") {
|
|
8467
9074
|
const derived = routeFromFilePath(relFile);
|
|
8468
9075
|
if (derived.route) {
|
|
8469
9076
|
ep.route = derived.route;
|
|
@@ -8514,6 +9121,10 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8514
9121
|
if (truncatedDependencyFlows > 0) {
|
|
8515
9122
|
notes.push(`${truncatedDependencyFlows} endpoint(s) had more dependency-input links than the bounded map carries; those endpoint records are marked dependencyInputFlowsTruncated.`);
|
|
8516
9123
|
}
|
|
9124
|
+
const truncatedFlows = endpoints.filter((e) => e.flowsTruncated).length;
|
|
9125
|
+
if (truncatedFlows > 0) {
|
|
9126
|
+
notes.push(`${truncatedFlows} endpoint(s) had more unproven flows than the bounded map carries; those endpoint records are marked flowsTruncated. Every proven flow is kept.`);
|
|
9127
|
+
}
|
|
8517
9128
|
if (unresolved > 0) {
|
|
8518
9129
|
notes.push(`${unresolved} endpoint(s) declare an input validator that could not be statically parsed \u2014 their inputs are UNKNOWN, not empty (marked inputsResolved: false).`);
|
|
8519
9130
|
}
|
|
@@ -8521,6 +9132,9 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
8521
9132
|
if (heuristicOnly > 0) {
|
|
8522
9133
|
notes.push(`${heuristicOnly} endpoint(s) have inputs and sinks but no proven data link \u2014 their flows say "may reach", not "does reach" (see each flow's confidence).`);
|
|
8523
9134
|
}
|
|
9135
|
+
if (componentFiles > 0) {
|
|
9136
|
+
notes.push(`${componentFiles} single-file component(s) (.vue, .svelte, .astro) were scanned for imports only. Code in their script blocks and Astro frontmatter can run on the server, but it is not analyzed for endpoints, inputs or sinks.`);
|
|
9137
|
+
}
|
|
8524
9138
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
8525
9139
|
const importList = imports.list();
|
|
8526
9140
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
@@ -8798,7 +9412,7 @@ function isPreBundleBuildHook(env = process.env) {
|
|
|
8798
9412
|
return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
|
|
8799
9413
|
}
|
|
8800
9414
|
function undeliveredReportLines(err, config, cwd) {
|
|
8801
|
-
const lines = [`patchstack:
|
|
9415
|
+
const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
|
|
8802
9416
|
if (err.code === "UNAUTHORIZED") {
|
|
8803
9417
|
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
8804
9418
|
if (hasCredential) {
|
|
@@ -8813,7 +9427,7 @@ function undeliveredReportLines(err, config, cwd) {
|
|
|
8813
9427
|
}
|
|
8814
9428
|
}
|
|
8815
9429
|
lines.push(
|
|
8816
|
-
"patchstack: continuing the build. Patchstack keeps the last
|
|
9430
|
+
"patchstack: continuing the build. Patchstack keeps the last package list it received until a check gets through."
|
|
8817
9431
|
);
|
|
8818
9432
|
return lines;
|
|
8819
9433
|
}
|
|
@@ -8964,9 +9578,9 @@ import path15 from "path";
|
|
|
8964
9578
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
8965
9579
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
8966
9580
|
function setupProtection(cwd) {
|
|
8967
|
-
const install = runProtect(cwd);
|
|
9581
|
+
const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
|
|
8968
9582
|
const verification = runVerify(cwd);
|
|
8969
|
-
return { install, verification };
|
|
9583
|
+
return { install, verification, log: log2 };
|
|
8970
9584
|
}
|
|
8971
9585
|
function appendHook(existing, command) {
|
|
8972
9586
|
if (existing === void 0 || existing.trim().length === 0) {
|
|
@@ -9059,7 +9673,7 @@ Usage:
|
|
|
9059
9673
|
If no UUID is configured, the server
|
|
9060
9674
|
provisions one and we persist it. After a
|
|
9061
9675
|
successful post it also adds/updates the
|
|
9062
|
-
|
|
9676
|
+
Patchstack Connector <script> tag in the root
|
|
9063
9677
|
HTML shell (index.html, public/index.html,
|
|
9064
9678
|
or src/app.html) \u2014 opt out with
|
|
9065
9679
|
"widget": false in .patchstackrc.json.
|
|
@@ -9168,6 +9782,8 @@ Usage:
|
|
|
9168
9782
|
Global:
|
|
9169
9783
|
--version Print the installed version of this package and exit
|
|
9170
9784
|
--help Print this help and exit
|
|
9785
|
+
--verbose Also print the technical detail: site ID, endpoint, checksum,
|
|
9786
|
+
environment and why, files written, installer steps
|
|
9171
9787
|
|
|
9172
9788
|
Options (for scan, setup, status, and uninstall):
|
|
9173
9789
|
--site-uuid <uuid> Override the configured site UUID
|
|
@@ -9223,6 +9839,13 @@ Examples:
|
|
|
9223
9839
|
npx @patchstack/connect demo-guide node-serialize
|
|
9224
9840
|
`;
|
|
9225
9841
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
|
|
9842
|
+
var verbose = false;
|
|
9843
|
+
function detail(line) {
|
|
9844
|
+
if (verbose) console.log(line);
|
|
9845
|
+
}
|
|
9846
|
+
function useColor() {
|
|
9847
|
+
return process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
|
|
9848
|
+
}
|
|
9226
9849
|
function parseArgs(argv) {
|
|
9227
9850
|
const args = argv.slice(2);
|
|
9228
9851
|
const positional = [];
|
|
@@ -9281,7 +9904,7 @@ async function runInit(args) {
|
|
|
9281
9904
|
const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
|
|
9282
9905
|
console.log(`Wrote ${target}`);
|
|
9283
9906
|
console.log("");
|
|
9284
|
-
console.log("Next: run `npx @patchstack/connect scan
|
|
9907
|
+
console.log("Next: run `npx @patchstack/connect scan`.");
|
|
9285
9908
|
return 0;
|
|
9286
9909
|
}
|
|
9287
9910
|
async function runClaim(args) {
|
|
@@ -9303,7 +9926,7 @@ async function runClaim(args) {
|
|
|
9303
9926
|
console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
|
|
9304
9927
|
if (result2.credentialSaved === true) {
|
|
9305
9928
|
const ignore = await secretFileIgnored(process.cwd());
|
|
9306
|
-
console.log(`
|
|
9929
|
+
console.log(` Saved a new credential to ${SECRET_CONFIG_FILENAME}.`);
|
|
9307
9930
|
console.log(gitignoreOutcomeLine(ignore));
|
|
9308
9931
|
}
|
|
9309
9932
|
console.log("");
|
|
@@ -9314,8 +9937,7 @@ async function runClaim(args) {
|
|
|
9314
9937
|
Your code: ${userCode}`);
|
|
9315
9938
|
console.log(` Claim at: ${verificationUri}
|
|
9316
9939
|
`);
|
|
9317
|
-
console.log(" Open
|
|
9318
|
-
console.log(" this site to it. Whoever approves becomes the site's owner.\n");
|
|
9940
|
+
console.log(" Open the link and sign in (or sign up) to connect this site. Whoever approves owns it.\n");
|
|
9319
9941
|
};
|
|
9320
9942
|
if (args.flags.has("wait")) {
|
|
9321
9943
|
const pending = readPendingClaim(siteUuid);
|
|
@@ -9359,8 +9981,7 @@ async function runClaim(args) {
|
|
|
9359
9981
|
return 1;
|
|
9360
9982
|
}
|
|
9361
9983
|
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
9362
|
-
console.log(" Give
|
|
9363
|
-
console.log(" this same command again (or `claim --wait` to block until they do).\n");
|
|
9984
|
+
console.log(" Give the link to the user. Once they approve, run this command again (or `claim --wait`).\n");
|
|
9364
9985
|
return 0;
|
|
9365
9986
|
}
|
|
9366
9987
|
const result = await claim2(config, (userCode, verificationUri) => {
|
|
@@ -9390,8 +10011,7 @@ async function runLogin(args) {
|
|
|
9390
10011
|
console.log(`
|
|
9391
10012
|
\u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
9392
10013
|
console.log(gitignoreOutcomeLine(ignore));
|
|
9393
|
-
console.log(" The
|
|
9394
|
-
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
10014
|
+
console.log(" The old credential no longer works. Update it in CI, hosting and other checkouts.\n");
|
|
9395
10015
|
return 0;
|
|
9396
10016
|
};
|
|
9397
10017
|
if (args.flags.has("wait")) {
|
|
@@ -9412,9 +10032,8 @@ async function runLogin(args) {
|
|
|
9412
10032
|
Your code: ${userCode}`);
|
|
9413
10033
|
console.log(` Approve at: ${verificationUri}
|
|
9414
10034
|
`);
|
|
9415
|
-
console.log(" Open
|
|
9416
|
-
console.log("
|
|
9417
|
-
console.log(" machine using it will need the new value.\n");
|
|
10035
|
+
console.log(" Open the link and approve as the site's owner. This replaces the current credential,");
|
|
10036
|
+
console.log(" so CI, deploys and other machines will need the new one.\n");
|
|
9418
10037
|
};
|
|
9419
10038
|
if (process.stdout.isTTY !== true) {
|
|
9420
10039
|
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
@@ -9439,8 +10058,7 @@ async function runLogin(args) {
|
|
|
9439
10058
|
return 1;
|
|
9440
10059
|
}
|
|
9441
10060
|
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
9442
|
-
console.log(" Give
|
|
9443
|
-
console.log(" this same command again (or `login --wait` to block until they do).\n");
|
|
10061
|
+
console.log(" Give the link to the user. Once they approve, run this command again (or `login --wait`).\n");
|
|
9444
10062
|
return 0;
|
|
9445
10063
|
}
|
|
9446
10064
|
const result = await login(config, (userCode, verificationUri) => {
|
|
@@ -9458,8 +10076,8 @@ var MAX_RETRY_TIMEOUT_MS = 18e4;
|
|
|
9458
10076
|
async function postManifestAccepted(config, payload) {
|
|
9459
10077
|
const { response, environmentUsed } = await postManifestWithEnvironmentFallback(config, payload);
|
|
9460
10078
|
if (environmentUsed !== config.environment) {
|
|
9461
|
-
|
|
9462
|
-
`
|
|
10079
|
+
detail(
|
|
10080
|
+
`This Patchstack API does not know the ${config.environment} label yet; the report was accepted as ${environmentUsed}, which also keeps it apart from production.`
|
|
9463
10081
|
);
|
|
9464
10082
|
}
|
|
9465
10083
|
return response;
|
|
@@ -9475,23 +10093,27 @@ async function postManifestWithPatience(config, payload) {
|
|
|
9475
10093
|
if (!(err instanceof PatchstackError) || err.code !== "NETWORK_TIMEOUT") throw err;
|
|
9476
10094
|
const timeoutMs = Math.min(config.timeoutMs * RETRY_TIMEOUT_FACTOR, MAX_RETRY_TIMEOUT_MS);
|
|
9477
10095
|
if (timeoutMs <= config.timeoutMs) throw err;
|
|
9478
|
-
console.warn(
|
|
9479
|
-
|
|
9480
|
-
);
|
|
10096
|
+
console.warn("Patchstack is slow to answer. Trying once more\u2026");
|
|
10097
|
+
detail(`The report timed out after ${config.timeoutMs}ms; trying once more with ${timeoutMs}ms.`);
|
|
9481
10098
|
const response = await postManifestAccepted({ ...config, timeoutMs }, payload);
|
|
9482
10099
|
try {
|
|
9483
10100
|
const target = await persistTimeout(process.cwd(), timeoutMs);
|
|
9484
|
-
|
|
10101
|
+
detail(`Saved a ${timeoutMs}ms request timeout to ${target}, so builds inherit it.`);
|
|
9485
10102
|
} catch {
|
|
9486
|
-
console.warn(
|
|
9487
|
-
`patchstack: could not save the timeout; set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where builds run.`
|
|
9488
|
-
);
|
|
10103
|
+
console.warn(`Could not save the longer wait. Set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where your builds run.`);
|
|
9489
10104
|
}
|
|
9490
10105
|
return response;
|
|
9491
10106
|
}
|
|
9492
10107
|
}
|
|
10108
|
+
function plural(count, one, many) {
|
|
10109
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
10110
|
+
}
|
|
9493
10111
|
async function runScan(args, options = {}) {
|
|
9494
10112
|
const dryRun = args.flags.get("dry-run") === true;
|
|
10113
|
+
const say = (line) => {
|
|
10114
|
+
if (verbose || dryRun) console.log(line);
|
|
10115
|
+
};
|
|
10116
|
+
const report = options.report ?? emptyReport();
|
|
9495
10117
|
const config = await resolveCliConfig(args, {
|
|
9496
10118
|
cliClaimToken: getStringFlag(args.flags, "claim-token"),
|
|
9497
10119
|
// The one command that reports them, so the one command that resolves them.
|
|
@@ -9499,48 +10121,41 @@ async function runScan(args, options = {}) {
|
|
|
9499
10121
|
});
|
|
9500
10122
|
const manifest = await scanLockfile(process.cwd());
|
|
9501
10123
|
for (const warning of manifest.warnings ?? []) {
|
|
9502
|
-
console.warn(`patchstack: ${warning}`);
|
|
10124
|
+
if (dryRun) console.warn(`patchstack: ${warning}`);
|
|
10125
|
+
else report.missing.push({ text: warning });
|
|
9503
10126
|
}
|
|
9504
10127
|
const installPaths = args.flags.get("install-paths") === true;
|
|
9505
10128
|
const { payload, stats } = buildWirePayload(manifest, { installPaths });
|
|
9506
|
-
|
|
9507
|
-
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
9508
|
-
);
|
|
10129
|
+
say(`Found ${payload.packages.length} package versions across ${stats.uniqueNames} packages (${manifest.ecosystem}).`);
|
|
9509
10130
|
if (installPaths) {
|
|
9510
10131
|
const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
|
|
9511
10132
|
console.log(
|
|
9512
|
-
payload.installPathsComplete ? `Including
|
|
10133
|
+
payload.installPathsComplete ? `Including install locations (--install-paths) for all ${located}.` : `Including install locations (--install-paths) for ${located} of ${payload.packages.length}. This lockfile does not record the rest; they are sent as "not recorded".`
|
|
9513
10134
|
);
|
|
9514
10135
|
}
|
|
9515
10136
|
if (config.environment === "local") {
|
|
9516
|
-
|
|
9517
|
-
"Reporting from this machine as the local environment: the dashboard will show the app as configured, not deployed. A build on a platform this recognises reports production or sandbox on its own; on a platform it does not know, set PATCHSTACK_ENVIRONMENT=production for the build that goes live."
|
|
9518
|
-
);
|
|
10137
|
+
say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
|
|
9519
10138
|
} else {
|
|
9520
10139
|
const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
|
|
9521
|
-
|
|
10140
|
+
say(`Environment: ${config.environment}${because}.`);
|
|
9522
10141
|
}
|
|
9523
10142
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9524
|
-
|
|
9525
|
-
`Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
|
|
9526
|
-
);
|
|
10143
|
+
say(`Endpoint override: ${config.endpoint}`);
|
|
9527
10144
|
}
|
|
9528
10145
|
if (stats.duplicateNames.length > 0) {
|
|
9529
10146
|
const sample = stats.duplicateNames.slice(0, 10).join(", ");
|
|
9530
10147
|
const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
|
|
9531
|
-
|
|
9532
|
-
`${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
|
|
9533
|
-
);
|
|
10148
|
+
say(`${stats.duplicateNames.length} package(s) at more than one version: ${sample}${more}`);
|
|
9534
10149
|
}
|
|
9535
10150
|
const body = buildManifestBody(config, payload);
|
|
9536
10151
|
if (typeof body.url === "string") {
|
|
9537
|
-
|
|
10152
|
+
say(`Reporting app address: ${body.url}`);
|
|
9538
10153
|
}
|
|
9539
10154
|
if (typeof body.name === "string") {
|
|
9540
|
-
|
|
10155
|
+
say(`Reporting app name: "${body.name}"`);
|
|
9541
10156
|
}
|
|
9542
10157
|
if (typeof config.claimToken === "string" && config.claimToken !== "") {
|
|
9543
|
-
|
|
10158
|
+
say("Claim token set. The site will join the Patchstack account that issued it.");
|
|
9544
10159
|
}
|
|
9545
10160
|
if (dryRun) {
|
|
9546
10161
|
console.log("");
|
|
@@ -9559,15 +10174,15 @@ async function runScan(args, options = {}) {
|
|
|
9559
10174
|
}
|
|
9560
10175
|
if (isPreBundleBuildHook()) {
|
|
9561
10176
|
const cleared = applyBuildStamp(process.cwd(), null);
|
|
9562
|
-
if (cleared.kind === "cleared")
|
|
10177
|
+
if (cleared.kind === "cleared") say(`Removed the previous map binding from ${cleared.file}.`);
|
|
9563
10178
|
}
|
|
9564
10179
|
const shellFramework = detectStack(payload.packages).framework;
|
|
9565
10180
|
if (config.widget) {
|
|
9566
|
-
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
|
|
10181
|
+
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages), report);
|
|
9567
10182
|
}
|
|
9568
10183
|
const provisioning = config.siteUuid === null;
|
|
9569
10184
|
if (provisioning) {
|
|
9570
|
-
|
|
10185
|
+
say("No site yet. Creating one on Patchstack\u2026");
|
|
9571
10186
|
}
|
|
9572
10187
|
let response;
|
|
9573
10188
|
try {
|
|
@@ -9577,113 +10192,147 @@ async function runScan(args, options = {}) {
|
|
|
9577
10192
|
for (const line of undeliveredReportLines(err, config, process.cwd())) console.error(line);
|
|
9578
10193
|
return 0;
|
|
9579
10194
|
}
|
|
10195
|
+
const checked = `Checked ${plural(stats.uniqueNames, "package", "packages")}`;
|
|
10196
|
+
if (response.stored) {
|
|
10197
|
+
report.done.unshift(checked);
|
|
10198
|
+
say(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
|
|
10199
|
+
} else if (response.reason === "duplicate") {
|
|
10200
|
+
report.done.unshift(checked, "No changes since the last check");
|
|
10201
|
+
} else {
|
|
10202
|
+
report.missing.push({
|
|
10203
|
+
text: "Patchstack did not save this check",
|
|
10204
|
+
hint: [response.message ?? "Run this again in a few minutes."],
|
|
10205
|
+
detail: [`Server response: ${JSON.stringify(response)}`]
|
|
10206
|
+
});
|
|
10207
|
+
}
|
|
9580
10208
|
if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
|
|
9581
10209
|
const target = await persistSiteUuid(process.cwd(), response.uuid);
|
|
9582
|
-
|
|
10210
|
+
report.done.push("Added this project to Patchstack");
|
|
10211
|
+
say(`Created site ${response.uuid}. Saved to ${target}.`);
|
|
9583
10212
|
}
|
|
9584
10213
|
if (typeof response.api_key === "string" && response.api_key.length > 0) {
|
|
9585
10214
|
const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
|
|
9586
10215
|
const saved = await persistApiKey(process.cwd(), response.api_key);
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
10216
|
+
if (saved.ignored) {
|
|
10217
|
+
report.done.push(`Saved the project's API key in ${SECRET_CONFIG_FILENAME}. Never commit this file`);
|
|
10218
|
+
say(`Saved API key to ${saved.path} (git-ignored).`);
|
|
10219
|
+
} else {
|
|
10220
|
+
report.missing.push({
|
|
10221
|
+
text: `${SECRET_CONFIG_FILENAME} holds your API key, and git does not ignore it`,
|
|
10222
|
+
hint: [`Add ${SECRET_CONFIG_FILENAME} to .gitignore before you commit. Never commit this file.`],
|
|
10223
|
+
detail: [`Saved to ${saved.path}. Not ignored: ${saved.reason ?? "unknown reason"}.`]
|
|
10224
|
+
});
|
|
10225
|
+
}
|
|
9591
10226
|
if (hadCredentialInConfig) {
|
|
9592
|
-
|
|
9593
|
-
"
|
|
9594
|
-
|
|
10227
|
+
report.missing.push({
|
|
10228
|
+
text: "Your API key was in .patchstackrc.json",
|
|
10229
|
+
hint: [
|
|
10230
|
+
`It is now in ${SECRET_CONFIG_FILENAME}. If .patchstackrc.json was ever committed, replace the key in the Patchstack dashboard.`
|
|
10231
|
+
]
|
|
10232
|
+
});
|
|
9595
10233
|
}
|
|
9596
10234
|
}
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
9603
|
-
}
|
|
9604
|
-
const claimLines = claimOutcomeLines(response.claim, config);
|
|
9605
|
-
if (claimLines.length > 0) {
|
|
9606
|
-
console.log("");
|
|
9607
|
-
for (const line of claimLines) console.log(line);
|
|
10235
|
+
const claim3 = claimOutcome(response.claim, config);
|
|
10236
|
+
if (claim3 !== null && claim3.connected) {
|
|
10237
|
+
report.done.push(claim3.dashboardUrl !== null ? `${claim3.summary}: ${claim3.dashboardUrl}` : claim3.summary);
|
|
10238
|
+
} else if (claim3 !== null) {
|
|
10239
|
+
report.missing.push(notConnectedItem([`${claim3.summary}.`, ...claim3.hint]));
|
|
9608
10240
|
}
|
|
9609
10241
|
const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
|
|
9610
10242
|
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
9611
10243
|
if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
|
|
9612
|
-
reportSourceWidget(effectiveUuid, shellFramework);
|
|
9613
|
-
}
|
|
9614
|
-
const
|
|
9615
|
-
|
|
9616
|
-
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
console.log(" 3. From this terminal: npx @patchstack/connect claim");
|
|
9624
|
-
} else {
|
|
9625
|
-
console.log(" 1. Open this dashboard link in a browser:");
|
|
9626
|
-
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
9627
|
-
console.log(" 2. From this terminal: npx @patchstack/connect claim");
|
|
9628
|
-
}
|
|
9629
|
-
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9630
|
-
console.log(" (this URL inherits the endpoint override above)");
|
|
9631
|
-
}
|
|
9632
|
-
}
|
|
9633
|
-
if (options.showRemainingSetup !== false) {
|
|
10244
|
+
reportSourceWidget(effectiveUuid, shellFramework, report);
|
|
10245
|
+
}
|
|
10246
|
+
const synced = effectiveUuid !== null && effectiveUuid.length > 0 && (response.stored || response.reason === "duplicate");
|
|
10247
|
+
const outcome = {
|
|
10248
|
+
connected,
|
|
10249
|
+
synced,
|
|
10250
|
+
// Only the dashboard can see the live site; a production scan is reported, not confirmed.
|
|
10251
|
+
deployed: false
|
|
10252
|
+
};
|
|
10253
|
+
options.onReported?.(outcome);
|
|
10254
|
+
if (options.report === void 0) {
|
|
9634
10255
|
try {
|
|
9635
|
-
|
|
9636
|
-
const remaining = countRemainingSteps(state);
|
|
9637
|
-
if (remaining > 0) {
|
|
9638
|
-
const hooksMissing = !(state.prebuildWired && state.postbuildWired);
|
|
9639
|
-
console.log("");
|
|
9640
|
-
console.log(
|
|
9641
|
-
`Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
|
|
9642
|
-
);
|
|
9643
|
-
console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
|
|
9644
|
-
}
|
|
10256
|
+
await printScanReport(config, effectiveUuid, outcome, report, options.showRemainingSetup !== false);
|
|
9645
10257
|
} catch {
|
|
9646
10258
|
}
|
|
9647
10259
|
}
|
|
9648
10260
|
return 0;
|
|
9649
10261
|
}
|
|
9650
|
-
function
|
|
10262
|
+
function mergeMissing(...lists) {
|
|
10263
|
+
const merged = [];
|
|
10264
|
+
for (const item of lists.flat()) {
|
|
10265
|
+
const key = item.key ?? item.text;
|
|
10266
|
+
if (!merged.some((existing) => (existing.key ?? existing.text) === key)) merged.push(item);
|
|
10267
|
+
}
|
|
10268
|
+
return merged;
|
|
10269
|
+
}
|
|
10270
|
+
function scanNextStepContext(config, state, siteUuid) {
|
|
10271
|
+
const uuid = siteUuid !== null && siteUuid.length > 0 ? siteUuid : state.siteUuid;
|
|
10272
|
+
return {
|
|
10273
|
+
...guideNextStepContext(state),
|
|
10274
|
+
environment: config.environment,
|
|
10275
|
+
environmentSource: config.environmentSource,
|
|
10276
|
+
siteUuid: uuid,
|
|
10277
|
+
claimUrl: uuid !== null && config.endpointTrusted !== false ? buildClaimUrl(config.endpoint, uuid) : null
|
|
10278
|
+
};
|
|
10279
|
+
}
|
|
10280
|
+
async function printScanReport(config, siteUuid, outcome, report, withProgress) {
|
|
10281
|
+
const state = await collectGuideState(process.cwd());
|
|
10282
|
+
const context = scanNextStepContext(config, state, siteUuid);
|
|
10283
|
+
const progress = guideProgress(state, outcome);
|
|
10284
|
+
if ((isInstallOrBuildHook() || runningInCi()) && !verbose) {
|
|
10285
|
+
for (const item of report.missing) console.warn(`Patchstack: ${[item.text + ".", ...item.hint ?? []].join(" ")}`);
|
|
10286
|
+
for (const line of renderHookSummary(report, progress, context)) console.log(line);
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
if (verbose) console.log("");
|
|
10290
|
+
const missing = mergeMissing(report.missing, withProgress ? guideMissing(state, outcome) : []);
|
|
10291
|
+
for (const line of renderStatus("Patchstack scan", { done: report.done, missing }, progress, context, {
|
|
10292
|
+
useColor: useColor(),
|
|
10293
|
+
verbose,
|
|
10294
|
+
withoutProgress: !withProgress
|
|
10295
|
+
})) {
|
|
10296
|
+
console.log(line);
|
|
10297
|
+
}
|
|
10298
|
+
}
|
|
10299
|
+
function reportSourceWidget(siteUuid, framework, report) {
|
|
10300
|
+
const handAdd = (why) => {
|
|
10301
|
+
report.missing.push({
|
|
10302
|
+
text: "The Patchstack widget is not on your page yet",
|
|
10303
|
+
hint: [why, ` ${buildWidgetTag(siteUuid)}`]
|
|
10304
|
+
});
|
|
10305
|
+
};
|
|
9651
10306
|
try {
|
|
9652
10307
|
const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
|
|
9653
10308
|
const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
|
|
9654
10309
|
const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
|
|
9655
10310
|
switch (result.action) {
|
|
9656
10311
|
case "added":
|
|
9657
|
-
|
|
9658
|
-
console.log(' Unclaimed, it shows a "Connect this website" panel; the "Report a vulnerability" button replaces it once the site is claimed.');
|
|
10312
|
+
report.done.push(`Added the Patchstack widget to ${result.shell}`);
|
|
9659
10313
|
break;
|
|
9660
10314
|
case "updated":
|
|
9661
|
-
|
|
10315
|
+
report.done.push(`Updated the Patchstack widget in ${result.shell}`);
|
|
9662
10316
|
break;
|
|
9663
10317
|
case "unchanged":
|
|
9664
|
-
|
|
10318
|
+
report.done.push(`The Patchstack widget is already in ${result.shell}`);
|
|
9665
10319
|
break;
|
|
9666
10320
|
case "manual":
|
|
9667
|
-
|
|
10321
|
+
report.done.push(`Left your own Patchstack widget in ${result.shell} as it is`);
|
|
9668
10322
|
break;
|
|
9669
10323
|
case "no-body":
|
|
9670
|
-
|
|
9671
|
-
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
10324
|
+
handAdd(`${result.shell} has no </body>. Add this to your main page layout, just before </body>:`);
|
|
9672
10325
|
break;
|
|
9673
10326
|
case "no-shell":
|
|
9674
|
-
|
|
9675
|
-
console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
|
|
9676
|
-
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
10327
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
9677
10328
|
break;
|
|
9678
10329
|
}
|
|
9679
|
-
if (result.action === "added" || result.action === "updated") {
|
|
9680
|
-
console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
|
|
9681
|
-
}
|
|
9682
10330
|
} catch (err) {
|
|
9683
|
-
|
|
10331
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
10332
|
+
detail(`Widget: skipped (${err.message}).`);
|
|
9684
10333
|
}
|
|
9685
10334
|
}
|
|
9686
|
-
function reportSourceMarker(framework, checksum
|
|
10335
|
+
function reportSourceMarker(framework, checksum, report) {
|
|
9687
10336
|
try {
|
|
9688
10337
|
const shell = resolveWidgetFileHint(process.cwd(), framework);
|
|
9689
10338
|
if (shell === null || shell.toLowerCase().endsWith(".html")) {
|
|
@@ -9692,30 +10341,25 @@ function reportSourceMarker(framework, checksum = null) {
|
|
|
9692
10341
|
const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
|
|
9693
10342
|
switch (result.action) {
|
|
9694
10343
|
case "added":
|
|
9695
|
-
|
|
9696
|
-
|
|
10344
|
+
report.done.push(`Set up ${shell} to tell Patchstack when it runs as your live app`);
|
|
10345
|
+
detail(`Production marker: added to ${shell} (only set when ${productionGate(framework)}).`);
|
|
9697
10346
|
break;
|
|
9698
10347
|
case "manual":
|
|
9699
|
-
|
|
10348
|
+
detail(`Production marker: already in ${shell}.`);
|
|
9700
10349
|
break;
|
|
9701
10350
|
case "no-anchor":
|
|
9702
10351
|
case "unsupported":
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
} else {
|
|
9711
|
-
console.log(
|
|
9712
|
-
` Emit <script>window.__PATCHSTACK_PROD__=true;</script> only when ${productionGate(framework)}.`
|
|
9713
|
-
);
|
|
9714
|
-
}
|
|
10352
|
+
report.missing.push({
|
|
10353
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
10354
|
+
hint: hasJsxShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
|
|
10355
|
+
`Add this inside <head> in ${shell}, only when ${productionGate(framework)}:`,
|
|
10356
|
+
" <script>window.__PATCHSTACK_PROD__=true;</script>"
|
|
10357
|
+
]
|
|
10358
|
+
});
|
|
9715
10359
|
break;
|
|
9716
10360
|
}
|
|
9717
10361
|
} catch (err) {
|
|
9718
|
-
|
|
10362
|
+
detail(`Production marker: skipped (${err.message}).`);
|
|
9719
10363
|
}
|
|
9720
10364
|
}
|
|
9721
10365
|
async function runProtectCommand(args) {
|
|
@@ -9855,8 +10499,7 @@ async function runGuide(args) {
|
|
|
9855
10499
|
let allDone = false;
|
|
9856
10500
|
try {
|
|
9857
10501
|
const state = await collectGuideState(process.cwd());
|
|
9858
|
-
|
|
9859
|
-
console.log(renderGuideChecklist(state, useColor));
|
|
10502
|
+
console.log(renderGuideChecklist(state, useColor(), {}, { verbose }));
|
|
9860
10503
|
allDone = countRemainingSteps(state) === 0;
|
|
9861
10504
|
} catch {
|
|
9862
10505
|
}
|
|
@@ -9874,96 +10517,85 @@ async function runGuide(args) {
|
|
|
9874
10517
|
console.log(readFileSync19(guidePath, "utf8"));
|
|
9875
10518
|
return 0;
|
|
9876
10519
|
}
|
|
10520
|
+
function reportProtection(protection, report) {
|
|
10521
|
+
for (const line of protection.log) detail(`patchstack protect: ${line}`);
|
|
10522
|
+
if (protection.install.status === "not-applicable") {
|
|
10523
|
+
report.done.push("Runtime protection is not needed: this project has no server");
|
|
10524
|
+
if (protection.install.leftovers.length > 0) {
|
|
10525
|
+
report.missing.push({
|
|
10526
|
+
text: "Files from an earlier Patchstack protection setup are not used here",
|
|
10527
|
+
hint: [`You can delete: ${protection.install.leftovers.join(", ")}`]
|
|
10528
|
+
});
|
|
10529
|
+
}
|
|
10530
|
+
return;
|
|
10531
|
+
}
|
|
10532
|
+
if (protection.verification.wired) {
|
|
10533
|
+
const changed = protection.install.status === "wired" && protection.install.changed.length > 0;
|
|
10534
|
+
report.done.push(
|
|
10535
|
+
changed ? `Added runtime protection (${protection.verification.stack})` : `Runtime protection is already set up (${protection.verification.stack})`
|
|
10536
|
+
);
|
|
10537
|
+
return;
|
|
10538
|
+
}
|
|
10539
|
+
if (protection.install.status === "scaffolded" && protection.install.plan.includes("Could not locate a server entry")) {
|
|
10540
|
+
report.missing.push({
|
|
10541
|
+
key: "runtime-protection",
|
|
10542
|
+
text: "Runtime protection: no server file found",
|
|
10543
|
+
hint: ["Add Patchstack where requests enter your app. Run npx @patchstack/connect protect for the steps."]
|
|
10544
|
+
});
|
|
10545
|
+
}
|
|
10546
|
+
}
|
|
10547
|
+
function buildStepsLine(wired) {
|
|
10548
|
+
if (wired.strategy === "postinstall-only") {
|
|
10549
|
+
return wired.changed ? "Added a package check after each install to package.json" : "The package check after each install is already in package.json";
|
|
10550
|
+
}
|
|
10551
|
+
return wired.changed ? "Added the build steps to package.json" : "The build steps are already in package.json";
|
|
10552
|
+
}
|
|
9877
10553
|
async function runSetup(args) {
|
|
9878
10554
|
if (args.flags.get("dry-run") === true) {
|
|
9879
|
-
console.error("
|
|
10555
|
+
console.error("Setup cannot run as a preview. To see what would be sent, run: npx @patchstack/connect scan --dry-run");
|
|
9880
10556
|
return 1;
|
|
9881
10557
|
}
|
|
9882
10558
|
const before = await collectGuideState(process.cwd());
|
|
9883
10559
|
if (!before.hasPackageJson) {
|
|
9884
|
-
console.error("
|
|
10560
|
+
console.error("No package.json here. Run setup from the folder that has your package.json.");
|
|
9885
10561
|
console.error("For a standalone HTML site, use the widget-only instructions in AGENT-INSTALL.md; do not create a Node project just to run setup.");
|
|
9886
10562
|
return 1;
|
|
9887
10563
|
}
|
|
9888
10564
|
if (before.installed === null) {
|
|
9889
10565
|
console.error(
|
|
9890
|
-
`
|
|
10566
|
+
`The Patchstack connector is not installed in this project yet.
|
|
9891
10567
|
Run: ${installCommand(before.packageManager)}`
|
|
9892
10568
|
);
|
|
9893
10569
|
return 1;
|
|
9894
10570
|
}
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
const scanCode = await runScan(args, {
|
|
10571
|
+
const report = emptyReport();
|
|
10572
|
+
let reported = {};
|
|
10573
|
+
const scanCode = await runScan(args, {
|
|
10574
|
+
report,
|
|
10575
|
+
onReported: (outcome) => {
|
|
10576
|
+
reported = outcome;
|
|
10577
|
+
}
|
|
10578
|
+
});
|
|
9898
10579
|
if (scanCode !== 0) {
|
|
9899
10580
|
return scanCode;
|
|
9900
10581
|
}
|
|
9901
|
-
|
|
9902
|
-
console.log(" 2. Install and verify runtime protection");
|
|
9903
|
-
const protection = setupProtection(process.cwd());
|
|
9904
|
-
if (protection.install.status === "not-applicable") {
|
|
9905
|
-
console.log("Runtime protection: not applicable to this project \u2014 nothing installed.");
|
|
9906
|
-
console.log(` ${protection.install.reason}`);
|
|
9907
|
-
if (protection.install.leftovers.length > 0) {
|
|
9908
|
-
console.log(" An earlier run scaffolded a guard here before that was established. These files do");
|
|
9909
|
-
console.log(" nothing on this project and can be deleted:");
|
|
9910
|
-
for (const file of protection.install.leftovers) console.log(` ${file}`);
|
|
9911
|
-
}
|
|
9912
|
-
} else if (protection.verification.wired) {
|
|
9913
|
-
console.log(`Runtime protection: wired (${protection.verification.stack}).`);
|
|
9914
|
-
} else {
|
|
9915
|
-
console.log(`Runtime protection: manual wiring remains (${protection.verification.stack}):`);
|
|
9916
|
-
for (const check of protection.verification.checks) {
|
|
9917
|
-
console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
|
|
9918
|
-
}
|
|
9919
|
-
console.log("Run `npx @patchstack/connect protect --check` after completing the failed checks.");
|
|
9920
|
-
}
|
|
9921
|
-
console.log("");
|
|
9922
|
-
console.log(" 3. Wire dependency-install and production-build scans into package.json");
|
|
10582
|
+
reportProtection(setupProtection(process.cwd()), report);
|
|
9923
10583
|
const wired = wireBuildScripts(process.cwd(), before.packageManager);
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
10584
|
+
report.done.push(buildStepsLine(wired));
|
|
10585
|
+
detail(`Build hooks: ${wired.detail}`);
|
|
10586
|
+
const config = await resolveCliConfig(args);
|
|
9927
10587
|
const after = await collectGuideState(process.cwd());
|
|
9928
|
-
const
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
10588
|
+
const missing = mergeMissing(report.missing, guideMissing(after, reported));
|
|
10589
|
+
const context = scanNextStepContext(config, after, after.siteUuid);
|
|
10590
|
+
if (verbose) console.log("");
|
|
10591
|
+
for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported), context, {
|
|
10592
|
+
useColor: useColor(),
|
|
10593
|
+
verbose
|
|
10594
|
+
})) {
|
|
10595
|
+
console.log(line);
|
|
9934
10596
|
}
|
|
9935
|
-
const outcome = setupOutcome(after, protection, wired);
|
|
9936
|
-
console.log("");
|
|
9937
|
-
console.log("Outcome \u2014 relay this to the user as it is:");
|
|
9938
|
-
for (const [label, value] of outcome) console.log(` ${label}: ${value}`);
|
|
9939
10597
|
return 0;
|
|
9940
10598
|
}
|
|
9941
|
-
function setupOutcome(state, protection, wired) {
|
|
9942
|
-
const remaining = countRemainingSteps(state);
|
|
9943
|
-
const lines = [];
|
|
9944
|
-
lines.push(["Status", remaining === 0 ? "Ready to deploy (configured locally; nothing is live yet)" : `Configured locally; ${remaining} step(s) still to finish (see the checklist above)`]);
|
|
9945
|
-
lines.push(["Monitoring", "starts with the first build that runs on your hosting platform"]);
|
|
9946
|
-
if (protection.install.status === "not-applicable") {
|
|
9947
|
-
lines.push(["Runtime protection", "not applicable \u2014 this project has no request path (static build)"]);
|
|
9948
|
-
} else if (protection.verification.wired) {
|
|
9949
|
-
lines.push(["Runtime protection", `wired (${protection.verification.stack}) \u2014 local wiring verified; verify against the live site after deploy with \`protect --check --runtime\``]);
|
|
9950
|
-
} else {
|
|
9951
|
-
lines.push(["Runtime protection", `not wired yet (${protection.verification.stack}) \u2014 finish the checks above, then \`npx @patchstack/connect protect --check\``]);
|
|
9952
|
-
}
|
|
9953
|
-
lines.push(["Next steps", [
|
|
9954
|
-
"add PATCHSTACK_API_KEY (from .patchstackrc.local.json) to your hosting platform's environment variables",
|
|
9955
|
-
"commit .patchstackrc.json, package.json and the widget change (never .patchstackrc.local.json)",
|
|
9956
|
-
'deploy, then check the site in the Patchstack dashboard \u2014 it reads "Deployed" once the live site is seen'
|
|
9957
|
-
].join("; ")]);
|
|
9958
|
-
const warnings = [];
|
|
9959
|
-
if (!wired.changed && wired.strategy === "postinstall-only") warnings.push("no build script, so only dependency installs are scanned");
|
|
9960
|
-
if (protection.install.status === "not-applicable" && protection.install.leftovers.length > 0) {
|
|
9961
|
-
warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(", ")}`);
|
|
9962
|
-
}
|
|
9963
|
-
if (state.claimUrl !== null) warnings.push('the site is not attached to an account until someone signs in through the widget\'s "Connect this website" panel, the dashboard link is opened, or `npx @patchstack/connect claim` completes');
|
|
9964
|
-
if (warnings.length > 0) lines.push(["Warnings", warnings.join("; ")]);
|
|
9965
|
-
return lines;
|
|
9966
|
-
}
|
|
9967
10599
|
async function runStatus(args) {
|
|
9968
10600
|
const config = await resolveCliConfig(args);
|
|
9969
10601
|
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
@@ -9974,24 +10606,14 @@ async function runStatus(args) {
|
|
|
9974
10606
|
console.log(`Environment: ${config.environment}`);
|
|
9975
10607
|
if (config.siteUuid !== null) {
|
|
9976
10608
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
9977
|
-
console.log(
|
|
9978
|
-
console.log(' website" panel on the preview, open the link above, or run');
|
|
9979
|
-
console.log(" `npx @patchstack/connect claim`.");
|
|
10609
|
+
console.log(" Not connected yet? Open the link above or run `npx @patchstack/connect claim`.");
|
|
9980
10610
|
switch (await fetchSiteStatus(config)) {
|
|
9981
10611
|
case "active":
|
|
9982
10612
|
console.log("Site status: active on Patchstack");
|
|
9983
10613
|
break;
|
|
9984
10614
|
case "removed":
|
|
9985
10615
|
console.log("Site status: removed from Patchstack");
|
|
9986
|
-
console.log(
|
|
9987
|
-
" The site record no longer exists (deleted from the dashboard or via the"
|
|
9988
|
-
);
|
|
9989
|
-
console.log(
|
|
9990
|
-
" widget uninstall flow). The local integration files are still in this"
|
|
9991
|
-
);
|
|
9992
|
-
console.log(
|
|
9993
|
-
' project \u2014 see "Uninstalling" in AGENT-INSTALL.md to remove them.'
|
|
9994
|
-
);
|
|
10616
|
+
console.log(' The local files are still here. See "Uninstalling" in AGENT-INSTALL.md to remove them.');
|
|
9995
10617
|
break;
|
|
9996
10618
|
case "unknown":
|
|
9997
10619
|
console.log("Site status: could not be verified (Patchstack unreachable)");
|
|
@@ -10026,8 +10648,7 @@ async function runUninstall(args) {
|
|
|
10026
10648
|
break;
|
|
10027
10649
|
}
|
|
10028
10650
|
console.log("");
|
|
10029
|
-
console.log(
|
|
10030
|
-
console.log('removed \u2014 follow the "Uninstalling" steps in AGENT-INSTALL.md.');
|
|
10651
|
+
console.log('Local files are not touched. Remove them with the "Uninstalling" steps in AGENT-INSTALL.md.');
|
|
10031
10652
|
return 0;
|
|
10032
10653
|
}
|
|
10033
10654
|
function describeStack(stack) {
|
|
@@ -10060,7 +10681,7 @@ async function reportBuildStamp(config, payload, marker) {
|
|
|
10060
10681
|
await postManifestWithEnvironmentFallback(bounded, payload, marker);
|
|
10061
10682
|
} catch (err) {
|
|
10062
10683
|
console.warn(
|
|
10063
|
-
`mark-build:
|
|
10684
|
+
`mark-build: could not tell Patchstack about this build (${err.message}). The pages were still marked.`
|
|
10064
10685
|
);
|
|
10065
10686
|
}
|
|
10066
10687
|
}
|
|
@@ -10122,22 +10743,14 @@ async function runMarkBuild(args) {
|
|
|
10122
10743
|
const files = findHtmlFiles(dir);
|
|
10123
10744
|
if (files.length === 0) {
|
|
10124
10745
|
console.warn(`mark-build: found ${dir} but no HTML files in it.`);
|
|
10125
|
-
console.warn(
|
|
10126
|
-
"mark-build: this build looks server-rendered, so the production flag has nothing to stamp."
|
|
10127
|
-
);
|
|
10128
|
-
console.warn(
|
|
10129
|
-
"mark-build: add the marker to your root shell instead \u2014 run `patchstack-connect guide` for the snippet."
|
|
10130
|
-
);
|
|
10746
|
+
console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
|
|
10131
10747
|
await reportBuildStamp(reported, wirePayload, "no-pages");
|
|
10132
10748
|
return 0;
|
|
10133
10749
|
}
|
|
10134
10750
|
const staleBy = staleBuildAge(files, startedAt);
|
|
10135
10751
|
if (staleBy !== null) {
|
|
10136
10752
|
console.warn(
|
|
10137
|
-
`mark-build: nothing in ${dir}
|
|
10138
|
-
);
|
|
10139
|
-
console.warn(
|
|
10140
|
-
"mark-build: run it as a postbuild hook (or straight after the build) so what is stamped is what was just produced."
|
|
10753
|
+
`mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
|
|
10141
10754
|
);
|
|
10142
10755
|
}
|
|
10143
10756
|
const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
|
|
@@ -10160,7 +10773,8 @@ async function runMarkBuild(args) {
|
|
|
10160
10773
|
}
|
|
10161
10774
|
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
10162
10775
|
if (published) {
|
|
10163
|
-
console.log(
|
|
10776
|
+
console.log(`Patchstack: marked ${plural(files.length, "page", "pages")} as your live app.`);
|
|
10777
|
+
detail(
|
|
10164
10778
|
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
10165
10779
|
);
|
|
10166
10780
|
await reportBuildStamp(reported, wirePayload, "stamped");
|
|
@@ -10168,13 +10782,10 @@ async function runMarkBuild(args) {
|
|
|
10168
10782
|
}
|
|
10169
10783
|
const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
|
|
10170
10784
|
console.log(
|
|
10171
|
-
`
|
|
10785
|
+
`Patchstack: ${environment} build, so ${plural(files.length, "page is", "pages are")} not marked as your live app. Publishing this folder by hand? Run again with --production.`
|
|
10172
10786
|
);
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
);
|
|
10176
|
-
console.log(
|
|
10177
|
-
"mark-build: publishing this directory by hand? Re-run with --production (or set PATCHSTACK_ENVIRONMENT=production) so the deployed pages carry it."
|
|
10787
|
+
detail(
|
|
10788
|
+
`mark-build: ${environment} build${because}, so no production marker in ${files.length} HTML file(s) in ${dir}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
10178
10789
|
);
|
|
10179
10790
|
await reportBuildStamp(reported, wirePayload, "withheld");
|
|
10180
10791
|
return 0;
|
|
@@ -10188,8 +10799,28 @@ function packageVersion() {
|
|
|
10188
10799
|
return "unknown";
|
|
10189
10800
|
}
|
|
10190
10801
|
}
|
|
10802
|
+
function plainErrorMessage(err) {
|
|
10803
|
+
switch (err.code) {
|
|
10804
|
+
case "NETWORK_ERROR":
|
|
10805
|
+
return "Could not reach Patchstack. Check your internet connection and try again.";
|
|
10806
|
+
case "NETWORK_TIMEOUT":
|
|
10807
|
+
return "Patchstack took too long to answer. Try again in a minute.";
|
|
10808
|
+
case "SERVER_ERROR":
|
|
10809
|
+
return "Patchstack could not handle the request right now. Try again in a few minutes.";
|
|
10810
|
+
case "LOCKFILE_NOT_FOUND":
|
|
10811
|
+
return "Could not find the list of installed packages. Install your packages first (for example npm install), then try again.";
|
|
10812
|
+
case "LOCKFILE_PARSE_ERROR":
|
|
10813
|
+
case "LOCKFILE_UNSUPPORTED":
|
|
10814
|
+
return "Could not read the list of installed packages. Reinstall your packages (for example npm install), then try again.";
|
|
10815
|
+
case "CONFIG_MISSING":
|
|
10816
|
+
return "This project is not set up with Patchstack yet. Run npx @patchstack/connect setup first.";
|
|
10817
|
+
default:
|
|
10818
|
+
return null;
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10191
10821
|
async function main() {
|
|
10192
10822
|
const args = parseArgs(process.argv);
|
|
10823
|
+
verbose = args.flags.get("verbose") === true;
|
|
10193
10824
|
if (args.flags.has("version") || args.command === "version") {
|
|
10194
10825
|
console.log(packageVersion());
|
|
10195
10826
|
return 0;
|
|
@@ -10234,7 +10865,9 @@ async function main() {
|
|
|
10234
10865
|
}
|
|
10235
10866
|
main().then((code) => process.exit(code)).catch((err) => {
|
|
10236
10867
|
if (err instanceof PatchstackError) {
|
|
10237
|
-
|
|
10868
|
+
const plain = plainErrorMessage(err);
|
|
10869
|
+
console.error(`${plain ?? err.message} (${err.code})`);
|
|
10870
|
+
if (plain !== null && verbose) console.error(`Detail: ${err.message}`);
|
|
10238
10871
|
process.exit(1);
|
|
10239
10872
|
}
|
|
10240
10873
|
console.error("Unexpected error:", err);
|