@patchstack/connect 0.5.12 → 0.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +24 -22
- package/README.md +19 -19
- package/dist/cli.js +637 -484
- 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.cjs +748 -168
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +25 -1
- package/dist/protect.d.ts +25 -1
- package/dist/protect.edge.js +732 -159
- package/dist/protect.edge.js.map +2 -2
- package/dist/protect.js +733 -160
- 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.",
|
|
@@ -3825,7 +3839,7 @@ function classifyArchitecture(cwd) {
|
|
|
3825
3839
|
return {
|
|
3826
3840
|
requestPath: "none",
|
|
3827
3841
|
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
|
|
3842
|
+
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
3843
|
};
|
|
3830
3844
|
}
|
|
3831
3845
|
return {
|
|
@@ -3840,7 +3854,21 @@ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
|
|
|
3840
3854
|
import { join as join5, dirname as dirname2 } from "path";
|
|
3841
3855
|
import { fileURLToPath } from "url";
|
|
3842
3856
|
var read = (p) => readFileSync7(p, "utf8");
|
|
3843
|
-
var
|
|
3857
|
+
var logSink = null;
|
|
3858
|
+
var log = (msg) => {
|
|
3859
|
+
if (logSink !== null) logSink(msg);
|
|
3860
|
+
else console.log(`patchstack protect: ${msg}`);
|
|
3861
|
+
};
|
|
3862
|
+
function collectProtectLog(fn) {
|
|
3863
|
+
const lines = [];
|
|
3864
|
+
const previous = logSink;
|
|
3865
|
+
logSink = (msg) => lines.push(...msg.split("\n"));
|
|
3866
|
+
try {
|
|
3867
|
+
return { result: fn(), lines };
|
|
3868
|
+
} finally {
|
|
3869
|
+
logSink = previous;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3844
3872
|
function hasDependency(cwd, name) {
|
|
3845
3873
|
try {
|
|
3846
3874
|
const pkg = JSON.parse(read(join5(cwd, "package.json")));
|
|
@@ -5502,6 +5530,147 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
5502
5530
|
return { shell, action };
|
|
5503
5531
|
}
|
|
5504
5532
|
|
|
5533
|
+
// src/progress.ts
|
|
5534
|
+
var PROGRESS_STEPS = [
|
|
5535
|
+
{ step: "installed", label: "Install the Patchstack connector" },
|
|
5536
|
+
{ step: "connected", label: "Connect project to Patchstack account" },
|
|
5537
|
+
{ step: "synced", label: "Sync and monitor your project" },
|
|
5538
|
+
{ step: "deployed", label: "Deploy project to protect live app" }
|
|
5539
|
+
];
|
|
5540
|
+
function stepLabel(step, environment) {
|
|
5541
|
+
if (step === "synced" && environment) return `Sync and monitor in ${environment} environment`;
|
|
5542
|
+
return PROGRESS_STEPS.find((entry) => entry.step === step).label;
|
|
5543
|
+
}
|
|
5544
|
+
function deployNote(context) {
|
|
5545
|
+
if (context.environment !== "production") return null;
|
|
5546
|
+
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.";
|
|
5547
|
+
}
|
|
5548
|
+
var ANSI = {
|
|
5549
|
+
reset: "\x1B[0m",
|
|
5550
|
+
bold: "\x1B[1m",
|
|
5551
|
+
dim: "\x1B[2m",
|
|
5552
|
+
green: "\x1B[32m",
|
|
5553
|
+
yellow: "\x1B[33m",
|
|
5554
|
+
cyan: "\x1B[36m"
|
|
5555
|
+
};
|
|
5556
|
+
function nextProgressStep(progress) {
|
|
5557
|
+
return PROGRESS_STEPS.find(({ step }) => !progress[step])?.step ?? null;
|
|
5558
|
+
}
|
|
5559
|
+
function nextStepTitle(step, context) {
|
|
5560
|
+
switch (step) {
|
|
5561
|
+
case "installed":
|
|
5562
|
+
return "install the Patchstack connector";
|
|
5563
|
+
case "connected":
|
|
5564
|
+
return "connect this project to your Patchstack account";
|
|
5565
|
+
case "synced":
|
|
5566
|
+
return "sync this project with Patchstack";
|
|
5567
|
+
case "deployed":
|
|
5568
|
+
return context.environment === "production" ? "open your live app so Patchstack can see it" : "deploy your project to protect the live app";
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
function nextStepLines(step, context) {
|
|
5572
|
+
switch (step) {
|
|
5573
|
+
case "installed":
|
|
5574
|
+
return [`Run: ${context.installCommand}`, "Then run: npx @patchstack/connect setup"];
|
|
5575
|
+
case "connected":
|
|
5576
|
+
if (context.siteUuid === null) {
|
|
5577
|
+
return [
|
|
5578
|
+
"Run: npx @patchstack/connect setup",
|
|
5579
|
+
"It adds this project to Patchstack and prints the link to connect it.",
|
|
5580
|
+
'Cannot run commands here? See "When your tool will not run this CLI" in AGENT-INSTALL.md.'
|
|
5581
|
+
];
|
|
5582
|
+
}
|
|
5583
|
+
return [
|
|
5584
|
+
context.claimUrl !== null ? `Open ${context.claimUrl}` : "Run: npx @patchstack/connect claim",
|
|
5585
|
+
// A production run is the deploy itself, so there is nothing further to point at.
|
|
5586
|
+
...context.environment === "production" ? [] : ["Already connected? Then commit, set PATCHSTACK_API_KEY on your host, and deploy."]
|
|
5587
|
+
];
|
|
5588
|
+
case "synced":
|
|
5589
|
+
return ["Run: npx @patchstack/connect scan"];
|
|
5590
|
+
case "deployed":
|
|
5591
|
+
if (context.environment === "production") {
|
|
5592
|
+
return ["Open the live site once so Patchstack can see it.", "Not published yet? Publish or deploy it now."];
|
|
5593
|
+
}
|
|
5594
|
+
return [
|
|
5595
|
+
"Commit your changes. Never commit .patchstackrc.local.json.",
|
|
5596
|
+
"Set PATCHSTACK_API_KEY (from .patchstackrc.local.json) on your hosting platform.",
|
|
5597
|
+
"Deploy or publish. The live site keeps its old version until you do."
|
|
5598
|
+
];
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5601
|
+
function renderProgress(progress, context, options) {
|
|
5602
|
+
const paint = (code, text) => options.useColor ? `${code}${text}${ANSI.reset}` : text;
|
|
5603
|
+
const lines = [];
|
|
5604
|
+
for (const { step } of PROGRESS_STEPS) {
|
|
5605
|
+
const label = stepLabel(step, context.environment);
|
|
5606
|
+
lines.push(progress[step] ? ` ${paint(ANSI.green, "\u2714")} ${label}` : ` ${paint(ANSI.yellow, "\u2718")} ${label}`);
|
|
5607
|
+
const note = step === "deployed" && !progress.deployed ? deployNote(context) : null;
|
|
5608
|
+
if (note !== null) lines.push(` ${paint(ANSI.dim, note)}`);
|
|
5609
|
+
}
|
|
5610
|
+
const next = nextProgressStep(progress);
|
|
5611
|
+
lines.push("");
|
|
5612
|
+
if (next === null) {
|
|
5613
|
+
lines.push(paint(ANSI.bold, "All done."));
|
|
5614
|
+
return lines;
|
|
5615
|
+
}
|
|
5616
|
+
lines.push(`${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, `Next: ${nextStepTitle(next, context)}`)}`);
|
|
5617
|
+
for (const line of nextStepLines(next, context)) {
|
|
5618
|
+
lines.push(` ${line}`);
|
|
5619
|
+
}
|
|
5620
|
+
return lines;
|
|
5621
|
+
}
|
|
5622
|
+
|
|
5623
|
+
// src/report.ts
|
|
5624
|
+
function emptyReport() {
|
|
5625
|
+
return { done: [], missing: [] };
|
|
5626
|
+
}
|
|
5627
|
+
var ANSI2 = {
|
|
5628
|
+
reset: "\x1B[0m",
|
|
5629
|
+
bold: "\x1B[1m",
|
|
5630
|
+
dim: "\x1B[2m",
|
|
5631
|
+
green: "\x1B[32m",
|
|
5632
|
+
yellow: "\x1B[33m"
|
|
5633
|
+
};
|
|
5634
|
+
function renderStatus(title, report, progress, context, options) {
|
|
5635
|
+
const paint = (code, text) => options.useColor ? `${code}${text}${ANSI2.reset}` : text;
|
|
5636
|
+
const lines = [paint(ANSI2.bold, title), ""];
|
|
5637
|
+
if (report.done.length > 0) {
|
|
5638
|
+
lines.push(paint(ANSI2.bold, "Done"));
|
|
5639
|
+
for (const item of report.done) lines.push(` ${paint(ANSI2.green, "\u2714")} ${item}`);
|
|
5640
|
+
lines.push("");
|
|
5641
|
+
}
|
|
5642
|
+
if (report.missing.length > 0) {
|
|
5643
|
+
lines.push(paint(ANSI2.bold, "Missing"));
|
|
5644
|
+
for (const item of report.missing) {
|
|
5645
|
+
lines.push(` ${paint(ANSI2.yellow, "\u2718")} ${item.text}`);
|
|
5646
|
+
for (const hint of item.hint ?? []) lines.push(` ${hint}`);
|
|
5647
|
+
if (options.verbose === true) {
|
|
5648
|
+
for (const detail2 of item.detail ?? []) lines.push(` ${paint(ANSI2.dim, detail2)}`);
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
lines.push("");
|
|
5652
|
+
}
|
|
5653
|
+
if (options.withoutProgress === true) {
|
|
5654
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
5655
|
+
return lines;
|
|
5656
|
+
}
|
|
5657
|
+
lines.push(...renderProgress(progress, context, { useColor: options.useColor }));
|
|
5658
|
+
return lines;
|
|
5659
|
+
}
|
|
5660
|
+
function renderHookSummary(report, progress, context) {
|
|
5661
|
+
const lowerFirst = (text) => text.charAt(0).toLowerCase() + text.slice(1);
|
|
5662
|
+
const lines = [];
|
|
5663
|
+
if (report.done.length > 0) {
|
|
5664
|
+
lines.push(`Patchstack: ${report.done.map(lowerFirst).join("; ")}.`);
|
|
5665
|
+
}
|
|
5666
|
+
const next = nextProgressStep(progress);
|
|
5667
|
+
if (next !== null) {
|
|
5668
|
+
const first = next === "deployed" && context.environment === "production" ? void 0 : nextStepLines(next, context)[0];
|
|
5669
|
+
lines.push(`Patchstack next step: ${nextStepTitle(next, context)}.${first !== void 0 ? ` ${first}` : ""}`);
|
|
5670
|
+
}
|
|
5671
|
+
return lines;
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5505
5674
|
// src/guide.ts
|
|
5506
5675
|
var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
|
|
5507
5676
|
var WIDGET_NEEDLE2 = "patchstack-widget";
|
|
@@ -5698,8 +5867,12 @@ async function collectGuideState(cwd) {
|
|
|
5698
5867
|
let claimUrl = null;
|
|
5699
5868
|
let endpointOverride = null;
|
|
5700
5869
|
let widgetOptOut = false;
|
|
5870
|
+
let environment = null;
|
|
5871
|
+
let environmentSource = null;
|
|
5701
5872
|
try {
|
|
5702
5873
|
const config = await resolveConfig({ cwd });
|
|
5874
|
+
environment = config.environment;
|
|
5875
|
+
environmentSource = config.environmentSource ?? null;
|
|
5703
5876
|
siteUuid = config.siteUuid;
|
|
5704
5877
|
if (siteUuid !== null && config.endpointTrusted !== false) {
|
|
5705
5878
|
claimUrl = buildClaimUrl(config.endpoint, siteUuid);
|
|
@@ -5726,6 +5899,8 @@ async function collectGuideState(cwd) {
|
|
|
5726
5899
|
siteUuid,
|
|
5727
5900
|
claimUrl,
|
|
5728
5901
|
endpointOverride,
|
|
5902
|
+
environment,
|
|
5903
|
+
environmentSource,
|
|
5729
5904
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
5730
5905
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
5731
5906
|
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
@@ -5744,7 +5919,7 @@ async function collectGuideState(cwd) {
|
|
|
5744
5919
|
protectionApplicable: protection.applicable
|
|
5745
5920
|
};
|
|
5746
5921
|
}
|
|
5747
|
-
var
|
|
5922
|
+
var ANSI3 = {
|
|
5748
5923
|
reset: "\x1B[0m",
|
|
5749
5924
|
bold: "\x1B[1m",
|
|
5750
5925
|
dim: "\x1B[2m",
|
|
@@ -5758,9 +5933,6 @@ function needsSourceProductionMarker(state) {
|
|
|
5758
5933
|
}
|
|
5759
5934
|
return !state.widgetFileHint.toLowerCase().endsWith(".html");
|
|
5760
5935
|
}
|
|
5761
|
-
function widgetTagInPlace(state) {
|
|
5762
|
-
return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
5763
|
-
}
|
|
5764
5936
|
function countRemainingSteps(state) {
|
|
5765
5937
|
return [
|
|
5766
5938
|
state.installed?.section === "dependencies",
|
|
@@ -5772,217 +5944,143 @@ function countRemainingSteps(state) {
|
|
|
5772
5944
|
!state.protectionApplicable || state.protectionWired
|
|
5773
5945
|
].filter((step) => !step).length;
|
|
5774
5946
|
}
|
|
5775
|
-
function
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5947
|
+
function guideProgress(state, known = {}) {
|
|
5948
|
+
return {
|
|
5949
|
+
installed: state.installed !== null,
|
|
5950
|
+
// Claim state lives on the server and nothing on disk records it, so only a caller that has just
|
|
5951
|
+
// heard from the server (a scan's claim outcome) can mark it done.
|
|
5952
|
+
connected: false,
|
|
5953
|
+
// `.patchstackrc.json` only gains a site UUID from a manifest the server stored.
|
|
5954
|
+
synced: state.siteUuid !== null,
|
|
5955
|
+
// Only the dashboard can see the live site, so nothing the CLI runs marks this done.
|
|
5956
|
+
deployed: false,
|
|
5957
|
+
...known
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
function guideNextStepContext(state) {
|
|
5961
|
+
return {
|
|
5962
|
+
installCommand: installCommand(state.packageManager),
|
|
5963
|
+
siteUuid: state.siteUuid,
|
|
5964
|
+
claimUrl: state.claimUrl,
|
|
5965
|
+
environment: state.environment,
|
|
5966
|
+
environmentSource: state.environmentSource
|
|
5967
|
+
};
|
|
5968
|
+
}
|
|
5969
|
+
function notConnectedItem(why = []) {
|
|
5970
|
+
return {
|
|
5971
|
+
text: "Not connected to your Patchstack account",
|
|
5972
|
+
hint: [...why, "Until it is, anyone who opens your app can connect it to their own account."]
|
|
5973
|
+
};
|
|
5974
|
+
}
|
|
5975
|
+
function buildScriptLines(state) {
|
|
5780
5976
|
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)}`));
|
|
5977
|
+
if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
|
|
5978
|
+
if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
|
|
5832
5979
|
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)"));
|
|
5980
|
+
lines.push('"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"');
|
|
5891
5981
|
} 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.`));
|
|
5982
|
+
if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
|
|
5983
|
+
if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
|
|
5914
5984
|
}
|
|
5915
5985
|
}
|
|
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"));
|
|
5986
|
+
return lines;
|
|
5987
|
+
}
|
|
5988
|
+
function guideMissing(state, known = {}) {
|
|
5989
|
+
const missing = [];
|
|
5990
|
+
if (state.installed?.section === "devDependencies") {
|
|
5991
|
+
missing.push({
|
|
5992
|
+
text: "Patchstack is installed as a development tool only, so your live app cannot load it",
|
|
5993
|
+
hint: [`Run: ${installCommand(state.packageManager)}`]
|
|
5994
|
+
});
|
|
5930
5995
|
}
|
|
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`)."));
|
|
5996
|
+
if (state.siteUuid === null) return missing;
|
|
5997
|
+
if (!state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches === false) {
|
|
5998
|
+
missing.push({
|
|
5999
|
+
text: "The Patchstack widget on your page belongs to a different project",
|
|
6000
|
+
hint: [`Set data-site-uuid to '${state.siteUuid}' on the widget tag.`]
|
|
6001
|
+
});
|
|
6002
|
+
} else if (!state.widgetOptOut && !state.widgetInstalled) {
|
|
6003
|
+
missing.push({
|
|
6004
|
+
text: "The Patchstack widget is not on your page yet",
|
|
6005
|
+
hint: [
|
|
6006
|
+
state.widgetFileHint !== null ? `Add this to ${state.widgetFileHint}, just before </body>:` : "Add this to your main page layout, just before </body>:",
|
|
6007
|
+
` ${buildWidgetTag(state.siteUuid)}`
|
|
6008
|
+
]
|
|
6009
|
+
});
|
|
5951
6010
|
}
|
|
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.");
|
|
6011
|
+
if (state.protectionApplicable && !state.protectionWired) {
|
|
6012
|
+
const failing = state.protectionChecks.filter((item) => !item.ok && item.group !== "reporting");
|
|
6013
|
+
const generic = state.protectionStack === "generic";
|
|
6014
|
+
missing.push({
|
|
6015
|
+
key: "runtime-protection",
|
|
6016
|
+
text: generic ? "Runtime protection: not added to your server yet" : `Runtime protection: not finished for ${state.protectionStack}`,
|
|
6017
|
+
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"],
|
|
6018
|
+
detail: failing.map((check) => `${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`)
|
|
6019
|
+
});
|
|
5966
6020
|
}
|
|
5967
|
-
const
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6021
|
+
const scripts = buildScriptLines(state);
|
|
6022
|
+
if (scripts.length > 0) {
|
|
6023
|
+
missing.push({
|
|
6024
|
+
text: "Patchstack does not check your packages on install and build yet",
|
|
6025
|
+
hint: ["Run: npx @patchstack/connect setup (it adds the build steps to package.json)"],
|
|
6026
|
+
detail: [
|
|
6027
|
+
state.packageManager === "bun" ? "Add to package.json scripts:" : "Add to package.json scripts (chain with && if one exists):",
|
|
6028
|
+
...scripts.map((line) => ` ${line}`)
|
|
6029
|
+
]
|
|
6030
|
+
});
|
|
6031
|
+
}
|
|
6032
|
+
if (needsSourceProductionMarker(state) && !state.productionMarkerWired) {
|
|
6033
|
+
const gate = productionGate(state.framework);
|
|
6034
|
+
missing.push(
|
|
6035
|
+
hasJsxShell(state.framework) ? {
|
|
6036
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
6037
|
+
hint: [`Run: npx @patchstack/connect scan (it edits ${state.widgetFileHint})`],
|
|
6038
|
+
detail: [`Or add inside <head>:`, ...buildSourceMarkerSnippet(state.framework).split("\n").map((line) => ` ${line}`)]
|
|
6039
|
+
} : {
|
|
6040
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
6041
|
+
hint: [
|
|
6042
|
+
`Add this inside <head> in ${state.widgetFileHint}, only when ${gate}:`,
|
|
6043
|
+
" <script>window.__PATCHSTACK_PROD__=true;</script>"
|
|
6044
|
+
]
|
|
6045
|
+
}
|
|
5984
6046
|
);
|
|
5985
6047
|
}
|
|
6048
|
+
if (known.connected !== true) missing.push(notConnectedItem());
|
|
6049
|
+
return missing;
|
|
6050
|
+
}
|
|
6051
|
+
function renderGuideChecklist(state, useColor2, known = {}, options = {}) {
|
|
6052
|
+
const paint = (code, text) => useColor2 ? `${code}${text}${ANSI3.reset}` : text;
|
|
6053
|
+
const name = state.projectName ?? path12.basename(process.cwd());
|
|
6054
|
+
const title = `Patchstack status for ${name}`;
|
|
6055
|
+
if (!state.hasPackageJson) {
|
|
6056
|
+
return [
|
|
6057
|
+
paint(ANSI3.bold, title),
|
|
6058
|
+
"",
|
|
6059
|
+
paint(ANSI3.bold, "Missing"),
|
|
6060
|
+
` ${paint(ANSI3.yellow, "\u2718")} No package.json here.`,
|
|
6061
|
+
" For a JS/Node app, run this from its package directory.",
|
|
6062
|
+
' For a plain HTML site, follow "Plain HTML sites" in AGENT-INSTALL.md. It gets the widget only: no dependency scan or runtime protection.',
|
|
6063
|
+
" Do not create a Node project just to add the widget.",
|
|
6064
|
+
" Use the site UUID or widget snippet from the Patchstack dashboard. Never invent one."
|
|
6065
|
+
].join("\n");
|
|
6066
|
+
}
|
|
6067
|
+
const lines = renderStatus(
|
|
6068
|
+
title,
|
|
6069
|
+
{ done: [], missing: guideMissing(state, known) },
|
|
6070
|
+
guideProgress(state, known),
|
|
6071
|
+
guideNextStepContext(state),
|
|
6072
|
+
{ useColor: useColor2, verbose: options.verbose }
|
|
6073
|
+
);
|
|
6074
|
+
if (options.verbose === true) {
|
|
6075
|
+
const verbose2 = [
|
|
6076
|
+
`Project: ${[state.framework, state.packageManager].filter((part) => part !== null).join(" \xB7 ")}`,
|
|
6077
|
+
...state.siteUuid !== null ? [`Site UUID: ${state.siteUuid}`] : [],
|
|
6078
|
+
...state.environment !== null ? [`Environment: ${state.environment}${state.environmentSource !== null ? ` (${state.environmentSource})` : ""}`] : [],
|
|
6079
|
+
...state.endpointOverride !== null ? [`Endpoint override: ${state.endpointOverride}`] : [],
|
|
6080
|
+
...state.widgetOptOut ? ['Widget is off ("widget": false in .patchstackrc.json).'] : []
|
|
6081
|
+
];
|
|
6082
|
+
lines.splice(1, 0, ...verbose2.map((line) => paint(ANSI3.dim, line)));
|
|
6083
|
+
}
|
|
5986
6084
|
return lines.join("\n");
|
|
5987
6085
|
}
|
|
5988
6086
|
|
|
@@ -7494,14 +7592,16 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7494
7592
|
const inner = unwrap(e);
|
|
7495
7593
|
return Boolean(inner && ts.isCallExpression(inner) && ts.isPropertyAccessExpression(inner.expression) && ["json", "formData"].includes(inner.expression.name.text) && ts.isIdentifier(inner.expression.expression) && inner.expression.expression.text === reqName);
|
|
7496
7594
|
};
|
|
7595
|
+
const isCallee = (n) => Boolean(n.parent && (ts.isCallExpression(n.parent) || ts.isNewExpression(n.parent)) && n.parent.expression === n);
|
|
7596
|
+
const ACCESSOR_SOURCES = /* @__PURE__ */ new Set(["header", "cookie", "form-body"]);
|
|
7497
7597
|
const visit = (n) => {
|
|
7498
|
-
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
|
|
7598
|
+
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
|
|
7499
7599
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
7500
7600
|
}
|
|
7501
7601
|
if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
|
|
7502
7602
|
record(n.argumentExpression.text, sourceOfExpr(n.expression));
|
|
7503
7603
|
}
|
|
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])) {
|
|
7604
|
+
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
7605
|
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
7506
7606
|
}
|
|
7507
7607
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
@@ -7553,6 +7653,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7553
7653
|
|
|
7554
7654
|
// src/map/flows.ts
|
|
7555
7655
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
|
|
7656
|
+
var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
|
|
7556
7657
|
function spaceOfKey(key) {
|
|
7557
7658
|
if (key === "query") return "get";
|
|
7558
7659
|
if (key === "params") return "route-param";
|
|
@@ -7578,18 +7679,18 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7578
7679
|
}
|
|
7579
7680
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
7580
7681
|
const rootPath = /* @__PURE__ */ new Map();
|
|
7581
|
-
const addRoot = (name, path17, space) => {
|
|
7582
|
-
if (!rootPath.has(name)) rootPath.set(name, { path: path17, space });
|
|
7682
|
+
const addRoot = (name, path17, space, accessor = false, request = false) => {
|
|
7683
|
+
if (!rootPath.has(name)) rootPath.set(name, { path: path17, space, ...accessor ? { accessor } : {}, ...request ? { request } : {} });
|
|
7583
7684
|
};
|
|
7584
|
-
for (const p of params ?? []) {
|
|
7685
|
+
for (const [index, p] of (params ?? []).entries()) {
|
|
7585
7686
|
if (!p?.name) continue;
|
|
7586
|
-
if (ts.isIdentifier(p.name)) addRoot(p.name.text, "");
|
|
7687
|
+
if (ts.isIdentifier(p.name)) addRoot(p.name.text, "", void 0, false, index === 0);
|
|
7587
7688
|
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7588
7689
|
for (const el of p.name.elements) {
|
|
7589
7690
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7590
7691
|
const key = bindingKey(el, ts);
|
|
7591
7692
|
const container = key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7592
|
-
addRoot(el.name.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0);
|
|
7693
|
+
addRoot(el.name.text, container ? "" : key ?? el.name.text, container ? spaceOfKey(key) : void 0, container && ACCESSOR_NAMESPACES.has(key));
|
|
7593
7694
|
}
|
|
7594
7695
|
}
|
|
7595
7696
|
}
|
|
@@ -7601,7 +7702,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7601
7702
|
const m = cur.expression.name.text;
|
|
7602
7703
|
if (["json", "formData", "text"].includes(m)) {
|
|
7603
7704
|
const root = rootIdentifier(cur.expression.expression, ts);
|
|
7604
|
-
|
|
7705
|
+
if (!root || !rootPath.has(root)) return void 0;
|
|
7706
|
+
return m === "formData" ? { path: "", space: "post", accessor: true } : { path: "", space: "post" };
|
|
7605
7707
|
}
|
|
7606
7708
|
if (["parse", "safeParse", "validate", "cast"].includes(m)) {
|
|
7607
7709
|
for (const a of cur.arguments) {
|
|
@@ -7611,21 +7713,21 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7611
7713
|
return void 0;
|
|
7612
7714
|
}
|
|
7613
7715
|
}
|
|
7614
|
-
|
|
7615
|
-
return p;
|
|
7716
|
+
return pathFromTainted(cur, ts, rootPath) ?? accessorRead(cur, ts, rootPath);
|
|
7616
7717
|
};
|
|
7617
7718
|
const aliasVisit = (n) => {
|
|
7618
7719
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7619
7720
|
const base = requestReadPath(n.initializer);
|
|
7620
7721
|
if (base !== void 0) {
|
|
7621
|
-
if (ts.isIdentifier(n.name)) addRoot(n.name.text, base.path, base.space);
|
|
7722
|
+
if (ts.isIdentifier(n.name)) addRoot(n.name.text, base.path, base.space, base.accessor === true);
|
|
7622
7723
|
else if (ts.isObjectBindingPattern(n.name)) {
|
|
7623
7724
|
for (const el of n.name.elements) {
|
|
7624
7725
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7625
7726
|
const key = bindingKey(el, ts);
|
|
7626
|
-
const
|
|
7627
|
-
const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7628
|
-
|
|
7727
|
+
const namespace = base.request === true && key !== void 0 && REQ_SOURCES.includes(key);
|
|
7728
|
+
const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key) && (namespace || !REQ_SOURCES.includes(key));
|
|
7729
|
+
const space = base.space ?? (namespace ? spaceOfKey(key) : void 0);
|
|
7730
|
+
addRoot(el.name.text, container ? "" : join22(base.path, key ?? el.name.text), space, namespace && ACCESSOR_NAMESPACES.has(key));
|
|
7629
7731
|
}
|
|
7630
7732
|
}
|
|
7631
7733
|
}
|
|
@@ -7657,7 +7759,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7657
7759
|
for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
|
|
7658
7760
|
for (let i = 0; i < args.length; i++) {
|
|
7659
7761
|
const role = argumentRoleOf(sink.kind, method, i, args.length);
|
|
7660
|
-
const whole = pathFromTainted(args[i], ts, rootPath);
|
|
7762
|
+
const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
|
|
7661
7763
|
for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
|
|
7662
7764
|
const key = `${read2.space ?? "*"}:${read2.path}`;
|
|
7663
7765
|
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
|
|
@@ -7819,6 +7921,18 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7819
7921
|
const visit = (n) => {
|
|
7820
7922
|
if (!n) return;
|
|
7821
7923
|
if (!includeDeferredBodies && (ts.isArrowFunction(n) || ts.isFunctionExpression(n) || ts.isFunctionDeclaration(n))) return;
|
|
7924
|
+
if (ts.isCallExpression(n)) {
|
|
7925
|
+
const accessed = accessorRead(n, ts, rootPath);
|
|
7926
|
+
if (accessed !== void 0) {
|
|
7927
|
+
add(accessed);
|
|
7928
|
+
return;
|
|
7929
|
+
}
|
|
7930
|
+
if (ts.isPropertyAccessExpression(n.expression)) {
|
|
7931
|
+
visit(n.expression.expression);
|
|
7932
|
+
for (const arg of n.arguments ?? []) visit(arg);
|
|
7933
|
+
return;
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7822
7936
|
if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
|
|
7823
7937
|
const read2 = pathFromTainted(n, ts, rootPath);
|
|
7824
7938
|
if (read2 !== void 0) {
|
|
@@ -7838,11 +7952,11 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7838
7952
|
function sinkArgumentLimitations(node, ts, rootPath) {
|
|
7839
7953
|
const out = [];
|
|
7840
7954
|
const seen = /* @__PURE__ */ new Set();
|
|
7841
|
-
const add = (kind,
|
|
7842
|
-
const key = `${kind}:${
|
|
7955
|
+
const add = (kind, detail2, n) => {
|
|
7956
|
+
const key = `${kind}:${detail2}`;
|
|
7843
7957
|
if (seen.has(key)) return;
|
|
7844
7958
|
seen.add(key);
|
|
7845
|
-
out.push({ kind, detail, line: lineOf(n) });
|
|
7959
|
+
out.push({ kind, detail: detail2, line: lineOf(n) });
|
|
7846
7960
|
};
|
|
7847
7961
|
const text = (n) => {
|
|
7848
7962
|
try {
|
|
@@ -7869,7 +7983,25 @@ function sinkArgumentLimitations(node, ts, rootPath) {
|
|
|
7869
7983
|
visit(node);
|
|
7870
7984
|
return out;
|
|
7871
7985
|
}
|
|
7872
|
-
function
|
|
7986
|
+
function accessorRead(node, ts, rootPath) {
|
|
7987
|
+
let cur = node;
|
|
7988
|
+
while (cur && (ts.isAwaitExpression(cur) || ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7989
|
+
if (!cur || !ts.isCallExpression(cur) || !ts.isPropertyAccessExpression(cur.expression)) return void 0;
|
|
7990
|
+
const [name] = cur.arguments;
|
|
7991
|
+
if (cur.expression.name.text !== "get" || cur.arguments.length !== 1 || !name || !ts.isStringLiteralLike(name)) return void 0;
|
|
7992
|
+
if (!isAccessor(cur.expression.expression, ts, rootPath)) return void 0;
|
|
7993
|
+
return pathFromTainted(cur.expression.expression, ts, rootPath, [name.text]);
|
|
7994
|
+
}
|
|
7995
|
+
function isAccessor(node, ts, rootPath) {
|
|
7996
|
+
let cur = node;
|
|
7997
|
+
while (cur && (ts.isParenthesizedExpression(cur) || ts.isNonNullExpression(cur))) cur = cur.expression;
|
|
7998
|
+
if (ts.isIdentifier(cur)) return rootPath.get(cur.text)?.accessor === true;
|
|
7999
|
+
if (ts.isPropertyAccessExpression(cur) && ACCESSOR_NAMESPACES.has(cur.name.text) && ts.isIdentifier(cur.expression)) {
|
|
8000
|
+
return rootPath.get(cur.expression.text)?.request === true;
|
|
8001
|
+
}
|
|
8002
|
+
return false;
|
|
8003
|
+
}
|
|
8004
|
+
function pathFromTainted(node, ts, rootPath, trailing = []) {
|
|
7873
8005
|
const segs = [];
|
|
7874
8006
|
let cur = node;
|
|
7875
8007
|
for (; ; ) {
|
|
@@ -7893,12 +8025,14 @@ function pathFromTainted(node, ts, rootPath) {
|
|
|
7893
8025
|
if (!cur || !ts.isIdentifier(cur)) return void 0;
|
|
7894
8026
|
const base = rootPath.get(cur.text);
|
|
7895
8027
|
if (base === void 0) return void 0;
|
|
8028
|
+
segs.push(...trailing);
|
|
7896
8029
|
let space = base.space;
|
|
7897
|
-
if (base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
|
|
8030
|
+
if (base.request === true && base.path === "" && segs.length > 1 && REQ_SOURCES.includes(segs[0])) {
|
|
7898
8031
|
space = spaceOfKey(segs[0]) ?? space;
|
|
7899
8032
|
segs.shift();
|
|
7900
8033
|
}
|
|
7901
|
-
|
|
8034
|
+
const request = base.request === true && segs.length === 0 ? { request: true } : {};
|
|
8035
|
+
return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request };
|
|
7902
8036
|
}
|
|
7903
8037
|
|
|
7904
8038
|
// src/map/invocations.ts
|
|
@@ -8798,7 +8932,7 @@ function isPreBundleBuildHook(env = process.env) {
|
|
|
8798
8932
|
return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
|
|
8799
8933
|
}
|
|
8800
8934
|
function undeliveredReportLines(err, config, cwd) {
|
|
8801
|
-
const lines = [`patchstack:
|
|
8935
|
+
const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
|
|
8802
8936
|
if (err.code === "UNAUTHORIZED") {
|
|
8803
8937
|
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
8804
8938
|
if (hasCredential) {
|
|
@@ -8813,7 +8947,7 @@ function undeliveredReportLines(err, config, cwd) {
|
|
|
8813
8947
|
}
|
|
8814
8948
|
}
|
|
8815
8949
|
lines.push(
|
|
8816
|
-
"patchstack: continuing the build. Patchstack keeps the last
|
|
8950
|
+
"patchstack: continuing the build. Patchstack keeps the last package list it received until a check gets through."
|
|
8817
8951
|
);
|
|
8818
8952
|
return lines;
|
|
8819
8953
|
}
|
|
@@ -8964,9 +9098,9 @@ import path15 from "path";
|
|
|
8964
9098
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
8965
9099
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
8966
9100
|
function setupProtection(cwd) {
|
|
8967
|
-
const install = runProtect(cwd);
|
|
9101
|
+
const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
|
|
8968
9102
|
const verification = runVerify(cwd);
|
|
8969
|
-
return { install, verification };
|
|
9103
|
+
return { install, verification, log: log2 };
|
|
8970
9104
|
}
|
|
8971
9105
|
function appendHook(existing, command) {
|
|
8972
9106
|
if (existing === void 0 || existing.trim().length === 0) {
|
|
@@ -9059,7 +9193,7 @@ Usage:
|
|
|
9059
9193
|
If no UUID is configured, the server
|
|
9060
9194
|
provisions one and we persist it. After a
|
|
9061
9195
|
successful post it also adds/updates the
|
|
9062
|
-
|
|
9196
|
+
Patchstack Connector <script> tag in the root
|
|
9063
9197
|
HTML shell (index.html, public/index.html,
|
|
9064
9198
|
or src/app.html) \u2014 opt out with
|
|
9065
9199
|
"widget": false in .patchstackrc.json.
|
|
@@ -9168,6 +9302,8 @@ Usage:
|
|
|
9168
9302
|
Global:
|
|
9169
9303
|
--version Print the installed version of this package and exit
|
|
9170
9304
|
--help Print this help and exit
|
|
9305
|
+
--verbose Also print the technical detail: site ID, endpoint, checksum,
|
|
9306
|
+
environment and why, files written, installer steps
|
|
9171
9307
|
|
|
9172
9308
|
Options (for scan, setup, status, and uninstall):
|
|
9173
9309
|
--site-uuid <uuid> Override the configured site UUID
|
|
@@ -9223,6 +9359,13 @@ Examples:
|
|
|
9223
9359
|
npx @patchstack/connect demo-guide node-serialize
|
|
9224
9360
|
`;
|
|
9225
9361
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["site-uuid", "endpoint", "dir", "url", "out", "claim-token"]);
|
|
9362
|
+
var verbose = false;
|
|
9363
|
+
function detail(line) {
|
|
9364
|
+
if (verbose) console.log(line);
|
|
9365
|
+
}
|
|
9366
|
+
function useColor() {
|
|
9367
|
+
return process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
|
|
9368
|
+
}
|
|
9226
9369
|
function parseArgs(argv) {
|
|
9227
9370
|
const args = argv.slice(2);
|
|
9228
9371
|
const positional = [];
|
|
@@ -9281,7 +9424,7 @@ async function runInit(args) {
|
|
|
9281
9424
|
const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
|
|
9282
9425
|
console.log(`Wrote ${target}`);
|
|
9283
9426
|
console.log("");
|
|
9284
|
-
console.log("Next: run `npx @patchstack/connect scan
|
|
9427
|
+
console.log("Next: run `npx @patchstack/connect scan`.");
|
|
9285
9428
|
return 0;
|
|
9286
9429
|
}
|
|
9287
9430
|
async function runClaim(args) {
|
|
@@ -9303,7 +9446,7 @@ async function runClaim(args) {
|
|
|
9303
9446
|
console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
|
|
9304
9447
|
if (result2.credentialSaved === true) {
|
|
9305
9448
|
const ignore = await secretFileIgnored(process.cwd());
|
|
9306
|
-
console.log(`
|
|
9449
|
+
console.log(` Saved a new credential to ${SECRET_CONFIG_FILENAME}.`);
|
|
9307
9450
|
console.log(gitignoreOutcomeLine(ignore));
|
|
9308
9451
|
}
|
|
9309
9452
|
console.log("");
|
|
@@ -9314,8 +9457,7 @@ async function runClaim(args) {
|
|
|
9314
9457
|
Your code: ${userCode}`);
|
|
9315
9458
|
console.log(` Claim at: ${verificationUri}
|
|
9316
9459
|
`);
|
|
9317
|
-
console.log(" Open
|
|
9318
|
-
console.log(" this site to it. Whoever approves becomes the site's owner.\n");
|
|
9460
|
+
console.log(" Open the link and sign in (or sign up) to connect this site. Whoever approves owns it.\n");
|
|
9319
9461
|
};
|
|
9320
9462
|
if (args.flags.has("wait")) {
|
|
9321
9463
|
const pending = readPendingClaim(siteUuid);
|
|
@@ -9359,8 +9501,7 @@ async function runClaim(args) {
|
|
|
9359
9501
|
return 1;
|
|
9360
9502
|
}
|
|
9361
9503
|
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");
|
|
9504
|
+
console.log(" Give the link to the user. Once they approve, run this command again (or `claim --wait`).\n");
|
|
9364
9505
|
return 0;
|
|
9365
9506
|
}
|
|
9366
9507
|
const result = await claim2(config, (userCode, verificationUri) => {
|
|
@@ -9390,8 +9531,7 @@ async function runLogin(args) {
|
|
|
9390
9531
|
console.log(`
|
|
9391
9532
|
\u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
9392
9533
|
console.log(gitignoreOutcomeLine(ignore));
|
|
9393
|
-
console.log(" The
|
|
9394
|
-
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
9534
|
+
console.log(" The old credential no longer works. Update it in CI, hosting and other checkouts.\n");
|
|
9395
9535
|
return 0;
|
|
9396
9536
|
};
|
|
9397
9537
|
if (args.flags.has("wait")) {
|
|
@@ -9412,9 +9552,8 @@ async function runLogin(args) {
|
|
|
9412
9552
|
Your code: ${userCode}`);
|
|
9413
9553
|
console.log(` Approve at: ${verificationUri}
|
|
9414
9554
|
`);
|
|
9415
|
-
console.log(" Open
|
|
9416
|
-
console.log("
|
|
9417
|
-
console.log(" machine using it will need the new value.\n");
|
|
9555
|
+
console.log(" Open the link and approve as the site's owner. This replaces the current credential,");
|
|
9556
|
+
console.log(" so CI, deploys and other machines will need the new one.\n");
|
|
9418
9557
|
};
|
|
9419
9558
|
if (process.stdout.isTTY !== true) {
|
|
9420
9559
|
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
@@ -9439,8 +9578,7 @@ async function runLogin(args) {
|
|
|
9439
9578
|
return 1;
|
|
9440
9579
|
}
|
|
9441
9580
|
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");
|
|
9581
|
+
console.log(" Give the link to the user. Once they approve, run this command again (or `login --wait`).\n");
|
|
9444
9582
|
return 0;
|
|
9445
9583
|
}
|
|
9446
9584
|
const result = await login(config, (userCode, verificationUri) => {
|
|
@@ -9458,8 +9596,8 @@ var MAX_RETRY_TIMEOUT_MS = 18e4;
|
|
|
9458
9596
|
async function postManifestAccepted(config, payload) {
|
|
9459
9597
|
const { response, environmentUsed } = await postManifestWithEnvironmentFallback(config, payload);
|
|
9460
9598
|
if (environmentUsed !== config.environment) {
|
|
9461
|
-
|
|
9462
|
-
`
|
|
9599
|
+
detail(
|
|
9600
|
+
`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
9601
|
);
|
|
9464
9602
|
}
|
|
9465
9603
|
return response;
|
|
@@ -9475,23 +9613,27 @@ async function postManifestWithPatience(config, payload) {
|
|
|
9475
9613
|
if (!(err instanceof PatchstackError) || err.code !== "NETWORK_TIMEOUT") throw err;
|
|
9476
9614
|
const timeoutMs = Math.min(config.timeoutMs * RETRY_TIMEOUT_FACTOR, MAX_RETRY_TIMEOUT_MS);
|
|
9477
9615
|
if (timeoutMs <= config.timeoutMs) throw err;
|
|
9478
|
-
console.warn(
|
|
9479
|
-
|
|
9480
|
-
);
|
|
9616
|
+
console.warn("Patchstack is slow to answer. Trying once more\u2026");
|
|
9617
|
+
detail(`The report timed out after ${config.timeoutMs}ms; trying once more with ${timeoutMs}ms.`);
|
|
9481
9618
|
const response = await postManifestAccepted({ ...config, timeoutMs }, payload);
|
|
9482
9619
|
try {
|
|
9483
9620
|
const target = await persistTimeout(process.cwd(), timeoutMs);
|
|
9484
|
-
|
|
9621
|
+
detail(`Saved a ${timeoutMs}ms request timeout to ${target}, so builds inherit it.`);
|
|
9485
9622
|
} catch {
|
|
9486
|
-
console.warn(
|
|
9487
|
-
`patchstack: could not save the timeout; set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where builds run.`
|
|
9488
|
-
);
|
|
9623
|
+
console.warn(`Could not save the longer wait. Set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where your builds run.`);
|
|
9489
9624
|
}
|
|
9490
9625
|
return response;
|
|
9491
9626
|
}
|
|
9492
9627
|
}
|
|
9628
|
+
function plural(count, one, many) {
|
|
9629
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
9630
|
+
}
|
|
9493
9631
|
async function runScan(args, options = {}) {
|
|
9494
9632
|
const dryRun = args.flags.get("dry-run") === true;
|
|
9633
|
+
const say = (line) => {
|
|
9634
|
+
if (verbose || dryRun) console.log(line);
|
|
9635
|
+
};
|
|
9636
|
+
const report = options.report ?? emptyReport();
|
|
9495
9637
|
const config = await resolveCliConfig(args, {
|
|
9496
9638
|
cliClaimToken: getStringFlag(args.flags, "claim-token"),
|
|
9497
9639
|
// The one command that reports them, so the one command that resolves them.
|
|
@@ -9499,48 +9641,41 @@ async function runScan(args, options = {}) {
|
|
|
9499
9641
|
});
|
|
9500
9642
|
const manifest = await scanLockfile(process.cwd());
|
|
9501
9643
|
for (const warning of manifest.warnings ?? []) {
|
|
9502
|
-
console.warn(`patchstack: ${warning}`);
|
|
9644
|
+
if (dryRun) console.warn(`patchstack: ${warning}`);
|
|
9645
|
+
else report.missing.push({ text: warning });
|
|
9503
9646
|
}
|
|
9504
9647
|
const installPaths = args.flags.get("install-paths") === true;
|
|
9505
9648
|
const { payload, stats } = buildWirePayload(manifest, { installPaths });
|
|
9506
|
-
|
|
9507
|
-
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
9508
|
-
);
|
|
9649
|
+
say(`Found ${payload.packages.length} package versions across ${stats.uniqueNames} packages (${manifest.ecosystem}).`);
|
|
9509
9650
|
if (installPaths) {
|
|
9510
9651
|
const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
|
|
9511
9652
|
console.log(
|
|
9512
|
-
payload.installPathsComplete ? `Including
|
|
9653
|
+
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
9654
|
);
|
|
9514
9655
|
}
|
|
9515
9656
|
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
|
-
);
|
|
9657
|
+
say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
|
|
9519
9658
|
} else {
|
|
9520
9659
|
const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
|
|
9521
|
-
|
|
9660
|
+
say(`Environment: ${config.environment}${because}.`);
|
|
9522
9661
|
}
|
|
9523
9662
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9524
|
-
|
|
9525
|
-
`Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
|
|
9526
|
-
);
|
|
9663
|
+
say(`Endpoint override: ${config.endpoint}`);
|
|
9527
9664
|
}
|
|
9528
9665
|
if (stats.duplicateNames.length > 0) {
|
|
9529
9666
|
const sample = stats.duplicateNames.slice(0, 10).join(", ");
|
|
9530
9667
|
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
|
-
);
|
|
9668
|
+
say(`${stats.duplicateNames.length} package(s) at more than one version: ${sample}${more}`);
|
|
9534
9669
|
}
|
|
9535
9670
|
const body = buildManifestBody(config, payload);
|
|
9536
9671
|
if (typeof body.url === "string") {
|
|
9537
|
-
|
|
9672
|
+
say(`Reporting app address: ${body.url}`);
|
|
9538
9673
|
}
|
|
9539
9674
|
if (typeof body.name === "string") {
|
|
9540
|
-
|
|
9675
|
+
say(`Reporting app name: "${body.name}"`);
|
|
9541
9676
|
}
|
|
9542
9677
|
if (typeof config.claimToken === "string" && config.claimToken !== "") {
|
|
9543
|
-
|
|
9678
|
+
say("Claim token set. The site will join the Patchstack account that issued it.");
|
|
9544
9679
|
}
|
|
9545
9680
|
if (dryRun) {
|
|
9546
9681
|
console.log("");
|
|
@@ -9559,15 +9694,15 @@ async function runScan(args, options = {}) {
|
|
|
9559
9694
|
}
|
|
9560
9695
|
if (isPreBundleBuildHook()) {
|
|
9561
9696
|
const cleared = applyBuildStamp(process.cwd(), null);
|
|
9562
|
-
if (cleared.kind === "cleared")
|
|
9697
|
+
if (cleared.kind === "cleared") say(`Removed the previous map binding from ${cleared.file}.`);
|
|
9563
9698
|
}
|
|
9564
9699
|
const shellFramework = detectStack(payload.packages).framework;
|
|
9565
9700
|
if (config.widget) {
|
|
9566
|
-
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
|
|
9701
|
+
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages), report);
|
|
9567
9702
|
}
|
|
9568
9703
|
const provisioning = config.siteUuid === null;
|
|
9569
9704
|
if (provisioning) {
|
|
9570
|
-
|
|
9705
|
+
say("No site yet. Creating one on Patchstack\u2026");
|
|
9571
9706
|
}
|
|
9572
9707
|
let response;
|
|
9573
9708
|
try {
|
|
@@ -9577,113 +9712,147 @@ async function runScan(args, options = {}) {
|
|
|
9577
9712
|
for (const line of undeliveredReportLines(err, config, process.cwd())) console.error(line);
|
|
9578
9713
|
return 0;
|
|
9579
9714
|
}
|
|
9715
|
+
const checked = `Checked ${plural(stats.uniqueNames, "package", "packages")}`;
|
|
9716
|
+
if (response.stored) {
|
|
9717
|
+
report.done.unshift(checked);
|
|
9718
|
+
say(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
|
|
9719
|
+
} else if (response.reason === "duplicate") {
|
|
9720
|
+
report.done.unshift(checked, "No changes since the last check");
|
|
9721
|
+
} else {
|
|
9722
|
+
report.missing.push({
|
|
9723
|
+
text: "Patchstack did not save this check",
|
|
9724
|
+
hint: [response.message ?? "Run this again in a few minutes."],
|
|
9725
|
+
detail: [`Server response: ${JSON.stringify(response)}`]
|
|
9726
|
+
});
|
|
9727
|
+
}
|
|
9580
9728
|
if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
|
|
9581
9729
|
const target = await persistSiteUuid(process.cwd(), response.uuid);
|
|
9582
|
-
|
|
9730
|
+
report.done.push("Added this project to Patchstack");
|
|
9731
|
+
say(`Created site ${response.uuid}. Saved to ${target}.`);
|
|
9583
9732
|
}
|
|
9584
9733
|
if (typeof response.api_key === "string" && response.api_key.length > 0) {
|
|
9585
9734
|
const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
|
|
9586
9735
|
const saved = await persistApiKey(process.cwd(), response.api_key);
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9736
|
+
if (saved.ignored) {
|
|
9737
|
+
report.done.push(`Saved the project's API key in ${SECRET_CONFIG_FILENAME}. Never commit this file`);
|
|
9738
|
+
say(`Saved API key to ${saved.path} (git-ignored).`);
|
|
9739
|
+
} else {
|
|
9740
|
+
report.missing.push({
|
|
9741
|
+
text: `${SECRET_CONFIG_FILENAME} holds your API key, and git does not ignore it`,
|
|
9742
|
+
hint: [`Add ${SECRET_CONFIG_FILENAME} to .gitignore before you commit. Never commit this file.`],
|
|
9743
|
+
detail: [`Saved to ${saved.path}. Not ignored: ${saved.reason ?? "unknown reason"}.`]
|
|
9744
|
+
});
|
|
9745
|
+
}
|
|
9591
9746
|
if (hadCredentialInConfig) {
|
|
9592
|
-
|
|
9593
|
-
"
|
|
9594
|
-
|
|
9747
|
+
report.missing.push({
|
|
9748
|
+
text: "Your API key was in .patchstackrc.json",
|
|
9749
|
+
hint: [
|
|
9750
|
+
`It is now in ${SECRET_CONFIG_FILENAME}. If .patchstackrc.json was ever committed, replace the key in the Patchstack dashboard.`
|
|
9751
|
+
]
|
|
9752
|
+
});
|
|
9595
9753
|
}
|
|
9596
9754
|
}
|
|
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);
|
|
9755
|
+
const claim3 = claimOutcome(response.claim, config);
|
|
9756
|
+
if (claim3 !== null && claim3.connected) {
|
|
9757
|
+
report.done.push(claim3.dashboardUrl !== null ? `${claim3.summary}: ${claim3.dashboardUrl}` : claim3.summary);
|
|
9758
|
+
} else if (claim3 !== null) {
|
|
9759
|
+
report.missing.push(notConnectedItem([`${claim3.summary}.`, ...claim3.hint]));
|
|
9608
9760
|
}
|
|
9609
9761
|
const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
|
|
9610
9762
|
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
9611
9763
|
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) {
|
|
9764
|
+
reportSourceWidget(effectiveUuid, shellFramework, report);
|
|
9765
|
+
}
|
|
9766
|
+
const synced = effectiveUuid !== null && effectiveUuid.length > 0 && (response.stored || response.reason === "duplicate");
|
|
9767
|
+
const outcome = {
|
|
9768
|
+
connected,
|
|
9769
|
+
synced,
|
|
9770
|
+
// Only the dashboard can see the live site; a production scan is reported, not confirmed.
|
|
9771
|
+
deployed: false
|
|
9772
|
+
};
|
|
9773
|
+
options.onReported?.(outcome);
|
|
9774
|
+
if (options.report === void 0) {
|
|
9634
9775
|
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
|
-
}
|
|
9776
|
+
await printScanReport(config, effectiveUuid, outcome, report, options.showRemainingSetup !== false);
|
|
9645
9777
|
} catch {
|
|
9646
9778
|
}
|
|
9647
9779
|
}
|
|
9648
9780
|
return 0;
|
|
9649
9781
|
}
|
|
9650
|
-
function
|
|
9782
|
+
function mergeMissing(...lists) {
|
|
9783
|
+
const merged = [];
|
|
9784
|
+
for (const item of lists.flat()) {
|
|
9785
|
+
const key = item.key ?? item.text;
|
|
9786
|
+
if (!merged.some((existing) => (existing.key ?? existing.text) === key)) merged.push(item);
|
|
9787
|
+
}
|
|
9788
|
+
return merged;
|
|
9789
|
+
}
|
|
9790
|
+
function scanNextStepContext(config, state, siteUuid) {
|
|
9791
|
+
const uuid = siteUuid !== null && siteUuid.length > 0 ? siteUuid : state.siteUuid;
|
|
9792
|
+
return {
|
|
9793
|
+
...guideNextStepContext(state),
|
|
9794
|
+
environment: config.environment,
|
|
9795
|
+
environmentSource: config.environmentSource,
|
|
9796
|
+
siteUuid: uuid,
|
|
9797
|
+
claimUrl: uuid !== null && config.endpointTrusted !== false ? buildClaimUrl(config.endpoint, uuid) : null
|
|
9798
|
+
};
|
|
9799
|
+
}
|
|
9800
|
+
async function printScanReport(config, siteUuid, outcome, report, withProgress) {
|
|
9801
|
+
const state = await collectGuideState(process.cwd());
|
|
9802
|
+
const context = scanNextStepContext(config, state, siteUuid);
|
|
9803
|
+
const progress = guideProgress(state, outcome);
|
|
9804
|
+
if ((isInstallOrBuildHook() || runningInCi()) && !verbose) {
|
|
9805
|
+
for (const item of report.missing) console.warn(`Patchstack: ${[item.text + ".", ...item.hint ?? []].join(" ")}`);
|
|
9806
|
+
for (const line of renderHookSummary(report, progress, context)) console.log(line);
|
|
9807
|
+
return;
|
|
9808
|
+
}
|
|
9809
|
+
if (verbose) console.log("");
|
|
9810
|
+
const missing = mergeMissing(report.missing, withProgress ? guideMissing(state, outcome) : []);
|
|
9811
|
+
for (const line of renderStatus("Patchstack scan", { done: report.done, missing }, progress, context, {
|
|
9812
|
+
useColor: useColor(),
|
|
9813
|
+
verbose,
|
|
9814
|
+
withoutProgress: !withProgress
|
|
9815
|
+
})) {
|
|
9816
|
+
console.log(line);
|
|
9817
|
+
}
|
|
9818
|
+
}
|
|
9819
|
+
function reportSourceWidget(siteUuid, framework, report) {
|
|
9820
|
+
const handAdd = (why) => {
|
|
9821
|
+
report.missing.push({
|
|
9822
|
+
text: "The Patchstack widget is not on your page yet",
|
|
9823
|
+
hint: [why, ` ${buildWidgetTag(siteUuid)}`]
|
|
9824
|
+
});
|
|
9825
|
+
};
|
|
9651
9826
|
try {
|
|
9652
9827
|
const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
|
|
9653
9828
|
const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
|
|
9654
9829
|
const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
|
|
9655
9830
|
switch (result.action) {
|
|
9656
9831
|
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.');
|
|
9832
|
+
report.done.push(`Added the Patchstack widget to ${result.shell}`);
|
|
9659
9833
|
break;
|
|
9660
9834
|
case "updated":
|
|
9661
|
-
|
|
9835
|
+
report.done.push(`Updated the Patchstack widget in ${result.shell}`);
|
|
9662
9836
|
break;
|
|
9663
9837
|
case "unchanged":
|
|
9664
|
-
|
|
9838
|
+
report.done.push(`The Patchstack widget is already in ${result.shell}`);
|
|
9665
9839
|
break;
|
|
9666
9840
|
case "manual":
|
|
9667
|
-
|
|
9841
|
+
report.done.push(`Left your own Patchstack widget in ${result.shell} as it is`);
|
|
9668
9842
|
break;
|
|
9669
9843
|
case "no-body":
|
|
9670
|
-
|
|
9671
|
-
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
9844
|
+
handAdd(`${result.shell} has no </body>. Add this to your main page layout, just before </body>:`);
|
|
9672
9845
|
break;
|
|
9673
9846
|
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)}`);
|
|
9847
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
9677
9848
|
break;
|
|
9678
9849
|
}
|
|
9679
|
-
if (result.action === "added" || result.action === "updated") {
|
|
9680
|
-
console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
|
|
9681
|
-
}
|
|
9682
9850
|
} catch (err) {
|
|
9683
|
-
|
|
9851
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
9852
|
+
detail(`Widget: skipped (${err.message}).`);
|
|
9684
9853
|
}
|
|
9685
9854
|
}
|
|
9686
|
-
function reportSourceMarker(framework, checksum
|
|
9855
|
+
function reportSourceMarker(framework, checksum, report) {
|
|
9687
9856
|
try {
|
|
9688
9857
|
const shell = resolveWidgetFileHint(process.cwd(), framework);
|
|
9689
9858
|
if (shell === null || shell.toLowerCase().endsWith(".html")) {
|
|
@@ -9692,30 +9861,25 @@ function reportSourceMarker(framework, checksum = null) {
|
|
|
9692
9861
|
const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
|
|
9693
9862
|
switch (result.action) {
|
|
9694
9863
|
case "added":
|
|
9695
|
-
|
|
9696
|
-
|
|
9864
|
+
report.done.push(`Set up ${shell} to tell Patchstack when it runs as your live app`);
|
|
9865
|
+
detail(`Production marker: added to ${shell} (only set when ${productionGate(framework)}).`);
|
|
9697
9866
|
break;
|
|
9698
9867
|
case "manual":
|
|
9699
|
-
|
|
9868
|
+
detail(`Production marker: already in ${shell}.`);
|
|
9700
9869
|
break;
|
|
9701
9870
|
case "no-anchor":
|
|
9702
9871
|
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
|
-
}
|
|
9872
|
+
report.missing.push({
|
|
9873
|
+
text: "Your live app does not tell Patchstack it is live yet",
|
|
9874
|
+
hint: hasJsxShell(framework) ? [`Add this inside <head> in ${shell}:`, ...buildSourceMarkerSnippet(framework).split("\n").map((line) => ` ${line}`)] : [
|
|
9875
|
+
`Add this inside <head> in ${shell}, only when ${productionGate(framework)}:`,
|
|
9876
|
+
" <script>window.__PATCHSTACK_PROD__=true;</script>"
|
|
9877
|
+
]
|
|
9878
|
+
});
|
|
9715
9879
|
break;
|
|
9716
9880
|
}
|
|
9717
9881
|
} catch (err) {
|
|
9718
|
-
|
|
9882
|
+
detail(`Production marker: skipped (${err.message}).`);
|
|
9719
9883
|
}
|
|
9720
9884
|
}
|
|
9721
9885
|
async function runProtectCommand(args) {
|
|
@@ -9855,8 +10019,7 @@ async function runGuide(args) {
|
|
|
9855
10019
|
let allDone = false;
|
|
9856
10020
|
try {
|
|
9857
10021
|
const state = await collectGuideState(process.cwd());
|
|
9858
|
-
|
|
9859
|
-
console.log(renderGuideChecklist(state, useColor));
|
|
10022
|
+
console.log(renderGuideChecklist(state, useColor(), {}, { verbose }));
|
|
9860
10023
|
allDone = countRemainingSteps(state) === 0;
|
|
9861
10024
|
} catch {
|
|
9862
10025
|
}
|
|
@@ -9874,96 +10037,85 @@ async function runGuide(args) {
|
|
|
9874
10037
|
console.log(readFileSync19(guidePath, "utf8"));
|
|
9875
10038
|
return 0;
|
|
9876
10039
|
}
|
|
10040
|
+
function reportProtection(protection, report) {
|
|
10041
|
+
for (const line of protection.log) detail(`patchstack protect: ${line}`);
|
|
10042
|
+
if (protection.install.status === "not-applicable") {
|
|
10043
|
+
report.done.push("Runtime protection is not needed: this project has no server");
|
|
10044
|
+
if (protection.install.leftovers.length > 0) {
|
|
10045
|
+
report.missing.push({
|
|
10046
|
+
text: "Files from an earlier Patchstack protection setup are not used here",
|
|
10047
|
+
hint: [`You can delete: ${protection.install.leftovers.join(", ")}`]
|
|
10048
|
+
});
|
|
10049
|
+
}
|
|
10050
|
+
return;
|
|
10051
|
+
}
|
|
10052
|
+
if (protection.verification.wired) {
|
|
10053
|
+
const changed = protection.install.status === "wired" && protection.install.changed.length > 0;
|
|
10054
|
+
report.done.push(
|
|
10055
|
+
changed ? `Added runtime protection (${protection.verification.stack})` : `Runtime protection is already set up (${protection.verification.stack})`
|
|
10056
|
+
);
|
|
10057
|
+
return;
|
|
10058
|
+
}
|
|
10059
|
+
if (protection.install.status === "scaffolded" && protection.install.plan.includes("Could not locate a server entry")) {
|
|
10060
|
+
report.missing.push({
|
|
10061
|
+
key: "runtime-protection",
|
|
10062
|
+
text: "Runtime protection: no server file found",
|
|
10063
|
+
hint: ["Add Patchstack where requests enter your app. Run npx @patchstack/connect protect for the steps."]
|
|
10064
|
+
});
|
|
10065
|
+
}
|
|
10066
|
+
}
|
|
10067
|
+
function buildStepsLine(wired) {
|
|
10068
|
+
if (wired.strategy === "postinstall-only") {
|
|
10069
|
+
return wired.changed ? "Added a package check after each install to package.json" : "The package check after each install is already in package.json";
|
|
10070
|
+
}
|
|
10071
|
+
return wired.changed ? "Added the build steps to package.json" : "The build steps are already in package.json";
|
|
10072
|
+
}
|
|
9877
10073
|
async function runSetup(args) {
|
|
9878
10074
|
if (args.flags.get("dry-run") === true) {
|
|
9879
|
-
console.error("
|
|
10075
|
+
console.error("Setup cannot run as a preview. To see what would be sent, run: npx @patchstack/connect scan --dry-run");
|
|
9880
10076
|
return 1;
|
|
9881
10077
|
}
|
|
9882
10078
|
const before = await collectGuideState(process.cwd());
|
|
9883
10079
|
if (!before.hasPackageJson) {
|
|
9884
|
-
console.error("
|
|
10080
|
+
console.error("No package.json here. Run setup from the folder that has your package.json.");
|
|
9885
10081
|
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
10082
|
return 1;
|
|
9887
10083
|
}
|
|
9888
10084
|
if (before.installed === null) {
|
|
9889
10085
|
console.error(
|
|
9890
|
-
`
|
|
10086
|
+
`The Patchstack connector is not installed in this project yet.
|
|
9891
10087
|
Run: ${installCommand(before.packageManager)}`
|
|
9892
10088
|
);
|
|
9893
10089
|
return 1;
|
|
9894
10090
|
}
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
const scanCode = await runScan(args, {
|
|
10091
|
+
const report = emptyReport();
|
|
10092
|
+
let reported = {};
|
|
10093
|
+
const scanCode = await runScan(args, {
|
|
10094
|
+
report,
|
|
10095
|
+
onReported: (outcome) => {
|
|
10096
|
+
reported = outcome;
|
|
10097
|
+
}
|
|
10098
|
+
});
|
|
9898
10099
|
if (scanCode !== 0) {
|
|
9899
10100
|
return scanCode;
|
|
9900
10101
|
}
|
|
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");
|
|
10102
|
+
reportProtection(setupProtection(process.cwd()), report);
|
|
9923
10103
|
const wired = wireBuildScripts(process.cwd(), before.packageManager);
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
10104
|
+
report.done.push(buildStepsLine(wired));
|
|
10105
|
+
detail(`Build hooks: ${wired.detail}`);
|
|
10106
|
+
const config = await resolveCliConfig(args);
|
|
9927
10107
|
const after = await collectGuideState(process.cwd());
|
|
9928
|
-
const
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
10108
|
+
const missing = mergeMissing(report.missing, guideMissing(after, reported));
|
|
10109
|
+
const context = scanNextStepContext(config, after, after.siteUuid);
|
|
10110
|
+
if (verbose) console.log("");
|
|
10111
|
+
for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported), context, {
|
|
10112
|
+
useColor: useColor(),
|
|
10113
|
+
verbose
|
|
10114
|
+
})) {
|
|
10115
|
+
console.log(line);
|
|
9934
10116
|
}
|
|
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
10117
|
return 0;
|
|
9940
10118
|
}
|
|
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
10119
|
async function runStatus(args) {
|
|
9968
10120
|
const config = await resolveCliConfig(args);
|
|
9969
10121
|
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
@@ -9974,24 +10126,14 @@ async function runStatus(args) {
|
|
|
9974
10126
|
console.log(`Environment: ${config.environment}`);
|
|
9975
10127
|
if (config.siteUuid !== null) {
|
|
9976
10128
|
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`.");
|
|
10129
|
+
console.log(" Not connected yet? Open the link above or run `npx @patchstack/connect claim`.");
|
|
9980
10130
|
switch (await fetchSiteStatus(config)) {
|
|
9981
10131
|
case "active":
|
|
9982
10132
|
console.log("Site status: active on Patchstack");
|
|
9983
10133
|
break;
|
|
9984
10134
|
case "removed":
|
|
9985
10135
|
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
|
-
);
|
|
10136
|
+
console.log(' The local files are still here. See "Uninstalling" in AGENT-INSTALL.md to remove them.');
|
|
9995
10137
|
break;
|
|
9996
10138
|
case "unknown":
|
|
9997
10139
|
console.log("Site status: could not be verified (Patchstack unreachable)");
|
|
@@ -10026,8 +10168,7 @@ async function runUninstall(args) {
|
|
|
10026
10168
|
break;
|
|
10027
10169
|
}
|
|
10028
10170
|
console.log("");
|
|
10029
|
-
console.log(
|
|
10030
|
-
console.log('removed \u2014 follow the "Uninstalling" steps in AGENT-INSTALL.md.');
|
|
10171
|
+
console.log('Local files are not touched. Remove them with the "Uninstalling" steps in AGENT-INSTALL.md.');
|
|
10031
10172
|
return 0;
|
|
10032
10173
|
}
|
|
10033
10174
|
function describeStack(stack) {
|
|
@@ -10060,7 +10201,7 @@ async function reportBuildStamp(config, payload, marker) {
|
|
|
10060
10201
|
await postManifestWithEnvironmentFallback(bounded, payload, marker);
|
|
10061
10202
|
} catch (err) {
|
|
10062
10203
|
console.warn(
|
|
10063
|
-
`mark-build:
|
|
10204
|
+
`mark-build: could not tell Patchstack about this build (${err.message}). The pages were still marked.`
|
|
10064
10205
|
);
|
|
10065
10206
|
}
|
|
10066
10207
|
}
|
|
@@ -10122,22 +10263,14 @@ async function runMarkBuild(args) {
|
|
|
10122
10263
|
const files = findHtmlFiles(dir);
|
|
10123
10264
|
if (files.length === 0) {
|
|
10124
10265
|
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
|
-
);
|
|
10266
|
+
console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
|
|
10131
10267
|
await reportBuildStamp(reported, wirePayload, "no-pages");
|
|
10132
10268
|
return 0;
|
|
10133
10269
|
}
|
|
10134
10270
|
const staleBy = staleBuildAge(files, startedAt);
|
|
10135
10271
|
if (staleBy !== null) {
|
|
10136
10272
|
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."
|
|
10273
|
+
`mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
|
|
10141
10274
|
);
|
|
10142
10275
|
}
|
|
10143
10276
|
const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
|
|
@@ -10160,7 +10293,8 @@ async function runMarkBuild(args) {
|
|
|
10160
10293
|
}
|
|
10161
10294
|
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
10162
10295
|
if (published) {
|
|
10163
|
-
console.log(
|
|
10296
|
+
console.log(`Patchstack: marked ${plural(files.length, "page", "pages")} as your live app.`);
|
|
10297
|
+
detail(
|
|
10164
10298
|
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
10165
10299
|
);
|
|
10166
10300
|
await reportBuildStamp(reported, wirePayload, "stamped");
|
|
@@ -10168,13 +10302,10 @@ async function runMarkBuild(args) {
|
|
|
10168
10302
|
}
|
|
10169
10303
|
const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
|
|
10170
10304
|
console.log(
|
|
10171
|
-
`
|
|
10172
|
-
);
|
|
10173
|
-
console.log(
|
|
10174
|
-
"mark-build: the marker tells Patchstack a page is the live site, so only the build that deploys carries it. Your hosting platform's build stamps it automatically."
|
|
10305
|
+
`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.`
|
|
10175
10306
|
);
|
|
10176
|
-
|
|
10177
|
-
|
|
10307
|
+
detail(
|
|
10308
|
+
`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
10309
|
);
|
|
10179
10310
|
await reportBuildStamp(reported, wirePayload, "withheld");
|
|
10180
10311
|
return 0;
|
|
@@ -10188,8 +10319,28 @@ function packageVersion() {
|
|
|
10188
10319
|
return "unknown";
|
|
10189
10320
|
}
|
|
10190
10321
|
}
|
|
10322
|
+
function plainErrorMessage(err) {
|
|
10323
|
+
switch (err.code) {
|
|
10324
|
+
case "NETWORK_ERROR":
|
|
10325
|
+
return "Could not reach Patchstack. Check your internet connection and try again.";
|
|
10326
|
+
case "NETWORK_TIMEOUT":
|
|
10327
|
+
return "Patchstack took too long to answer. Try again in a minute.";
|
|
10328
|
+
case "SERVER_ERROR":
|
|
10329
|
+
return "Patchstack could not handle the request right now. Try again in a few minutes.";
|
|
10330
|
+
case "LOCKFILE_NOT_FOUND":
|
|
10331
|
+
return "Could not find the list of installed packages. Install your packages first (for example npm install), then try again.";
|
|
10332
|
+
case "LOCKFILE_PARSE_ERROR":
|
|
10333
|
+
case "LOCKFILE_UNSUPPORTED":
|
|
10334
|
+
return "Could not read the list of installed packages. Reinstall your packages (for example npm install), then try again.";
|
|
10335
|
+
case "CONFIG_MISSING":
|
|
10336
|
+
return "This project is not set up with Patchstack yet. Run npx @patchstack/connect setup first.";
|
|
10337
|
+
default:
|
|
10338
|
+
return null;
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10191
10341
|
async function main() {
|
|
10192
10342
|
const args = parseArgs(process.argv);
|
|
10343
|
+
verbose = args.flags.get("verbose") === true;
|
|
10193
10344
|
if (args.flags.has("version") || args.command === "version") {
|
|
10194
10345
|
console.log(packageVersion());
|
|
10195
10346
|
return 0;
|
|
@@ -10234,7 +10385,9 @@ async function main() {
|
|
|
10234
10385
|
}
|
|
10235
10386
|
main().then((code) => process.exit(code)).catch((err) => {
|
|
10236
10387
|
if (err instanceof PatchstackError) {
|
|
10237
|
-
|
|
10388
|
+
const plain = plainErrorMessage(err);
|
|
10389
|
+
console.error(`${plain ?? err.message} (${err.code})`);
|
|
10390
|
+
if (plain !== null && verbose) console.error(`Detail: ${err.message}`);
|
|
10238
10391
|
process.exit(1);
|
|
10239
10392
|
}
|
|
10240
10393
|
console.error("Unexpected error:", err);
|