@patchstack/connect 0.5.11 → 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 +49 -20
- package/README.md +19 -19
- package/dist/cli.js +760 -644
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +20 -12
- 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 +20 -12
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +751 -170
- 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-PPV22Q2E.js → refresh-manifest-DZQOEO3Q.js} +19 -11
- package/dist/refresh-manifest-DZQOEO3Q.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-PPV22Q2E.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { readFileSync as readFileSync19 } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path16 from "path";
|
|
6
6
|
import { createRequire as createRequire2 } from "module";
|
|
7
7
|
|
|
8
8
|
// src/safe-file.ts
|
|
@@ -25,9 +25,9 @@ import { dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
|
25
25
|
function pathError(message) {
|
|
26
26
|
return Object.assign(new Error(message), { code: "EINVAL" });
|
|
27
27
|
}
|
|
28
|
-
function existingEntry(
|
|
28
|
+
function existingEntry(path17) {
|
|
29
29
|
try {
|
|
30
|
-
return lstatSync(
|
|
30
|
+
return lstatSync(path17);
|
|
31
31
|
} catch (error) {
|
|
32
32
|
if (error.code === "ENOENT") return null;
|
|
33
33
|
throw error;
|
|
@@ -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(
|
|
@@ -1488,8 +1495,8 @@ async function readBoundedJson(response, maxBytes = MAX_CONTROL_RESPONSE_BYTES)
|
|
|
1488
1495
|
var TOKEN_SKEW_MS = 6e4;
|
|
1489
1496
|
function buildTokenUrl(manifestEndpoint) {
|
|
1490
1497
|
const url = new URL(manifestEndpoint);
|
|
1491
|
-
const
|
|
1492
|
-
url.pathname =
|
|
1498
|
+
const path17 = url.pathname.replace(/\/$/, "");
|
|
1499
|
+
url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/token` : "/monitor/pulse/token";
|
|
1493
1500
|
url.search = "";
|
|
1494
1501
|
url.hash = "";
|
|
1495
1502
|
return url.toString();
|
|
@@ -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,20 +1659,20 @@ 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
|
}
|
|
1658
1672
|
function buildRulesUrl(manifestEndpoint, siteUuid) {
|
|
1659
1673
|
const url = new URL(manifestEndpoint);
|
|
1660
|
-
const
|
|
1661
|
-
url.pathname =
|
|
1674
|
+
const path17 = url.pathname.replace(/\/$/, "");
|
|
1675
|
+
url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/rules/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/rules/${encodeURIComponent(siteUuid)}`;
|
|
1662
1676
|
url.search = "";
|
|
1663
1677
|
url.hash = "";
|
|
1664
1678
|
return url.toString();
|
|
@@ -1673,16 +1687,16 @@ function buildSettingsUrl(endpoint, siteUuid) {
|
|
|
1673
1687
|
}
|
|
1674
1688
|
function buildPackageRemovedUrl(manifestEndpoint, siteUuid) {
|
|
1675
1689
|
const url = new URL(manifestEndpoint);
|
|
1676
|
-
const
|
|
1677
|
-
url.pathname =
|
|
1690
|
+
const path17 = url.pathname.replace(/\/$/, "");
|
|
1691
|
+
url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/package-removed/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/package-removed/${encodeURIComponent(siteUuid)}`;
|
|
1678
1692
|
url.search = "";
|
|
1679
1693
|
url.hash = "";
|
|
1680
1694
|
return url.toString();
|
|
1681
1695
|
}
|
|
1682
1696
|
function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
1683
1697
|
const url = new URL(manifestEndpoint);
|
|
1684
|
-
const
|
|
1685
|
-
url.pathname =
|
|
1698
|
+
const path17 = url.pathname.replace(/\/$/, "");
|
|
1699
|
+
url.pathname = path17.endsWith("/manifest") ? `${path17.slice(0, -"/manifest".length)}/input-map/${encodeURIComponent(siteUuid)}` : `/monitor/pulse/input-map/${encodeURIComponent(siteUuid)}`;
|
|
1686
1700
|
url.search = "";
|
|
1687
1701
|
url.hash = "";
|
|
1688
1702
|
return url.toString();
|
|
@@ -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.",
|
|
@@ -2336,6 +2350,7 @@ function isUnpublishableHost(hostname) {
|
|
|
2336
2350
|
if (!host.includes(".")) return true;
|
|
2337
2351
|
return RESERVED_SUFFIXES.some((suffix) => host.endsWith(suffix));
|
|
2338
2352
|
}
|
|
2353
|
+
var URL_MAX_LENGTH = 191;
|
|
2339
2354
|
function normaliseSiteUrl(value) {
|
|
2340
2355
|
const trimmed = (value ?? "").trim();
|
|
2341
2356
|
if (trimmed === "") return null;
|
|
@@ -2349,7 +2364,7 @@ function normaliseSiteUrl(value) {
|
|
|
2349
2364
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
2350
2365
|
if (parsed.hostname === "" || isUnpublishableHost(parsed.hostname)) return null;
|
|
2351
2366
|
const origin = `${parsed.protocol}//${parsed.host}`;
|
|
2352
|
-
return origin.length <=
|
|
2367
|
+
return origin.length <= URL_MAX_LENGTH ? origin : null;
|
|
2353
2368
|
}
|
|
2354
2369
|
var PRODUCTION_URL_SOURCES = [
|
|
2355
2370
|
{
|
|
@@ -2678,10 +2693,10 @@ async function persistTimeout(cwd, timeoutMs) {
|
|
|
2678
2693
|
return writeConfigFile(cwd, { ...existing, timeoutMs });
|
|
2679
2694
|
}
|
|
2680
2695
|
async function persistApiKey(cwd, apiKey) {
|
|
2681
|
-
const { apiKey:
|
|
2696
|
+
const { apiKey: movedKey, pulseAuth: movedPulse, ...publicConfig } = await readConfigFile(cwd);
|
|
2682
2697
|
const existingSecrets = await readSecretFile(cwd);
|
|
2683
2698
|
const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
|
|
2684
|
-
if (
|
|
2699
|
+
if (movedKey !== void 0 || movedPulse !== void 0) {
|
|
2685
2700
|
await writeConfigFile(cwd, publicConfig);
|
|
2686
2701
|
}
|
|
2687
2702
|
return target;
|
|
@@ -3460,10 +3475,10 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3460
3475
|
boundary = realpathSync3(boundary);
|
|
3461
3476
|
} catch {
|
|
3462
3477
|
}
|
|
3463
|
-
const inProject = (
|
|
3478
|
+
const inProject = (path17) => {
|
|
3464
3479
|
if (opts.followOutside) return true;
|
|
3465
3480
|
try {
|
|
3466
|
-
return isInside(realpathSync3(
|
|
3481
|
+
return isInside(realpathSync3(path17), boundary);
|
|
3467
3482
|
} catch {
|
|
3468
3483
|
return false;
|
|
3469
3484
|
}
|
|
@@ -3824,7 +3839,7 @@ function classifyArchitecture(cwd) {
|
|
|
3824
3839
|
return {
|
|
3825
3840
|
requestPath: "none",
|
|
3826
3841
|
evidence: statics.map((label) => `static build: ${label}`),
|
|
3827
|
-
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.`
|
|
3828
3843
|
};
|
|
3829
3844
|
}
|
|
3830
3845
|
return {
|
|
@@ -3839,7 +3854,21 @@ import { readFileSync as readFileSync7, existsSync as existsSync3 } from "fs";
|
|
|
3839
3854
|
import { join as join5, dirname as dirname2 } from "path";
|
|
3840
3855
|
import { fileURLToPath } from "url";
|
|
3841
3856
|
var read = (p) => readFileSync7(p, "utf8");
|
|
3842
|
-
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
|
+
}
|
|
3843
3872
|
function hasDependency(cwd, name) {
|
|
3844
3873
|
try {
|
|
3845
3874
|
const pkg = JSON.parse(read(join5(cwd, "package.json")));
|
|
@@ -4100,14 +4129,6 @@ var tanstackSupabaseAdapter = {
|
|
|
4100
4129
|
// src/protect/install/adapters/next.ts
|
|
4101
4130
|
import { existsSync as existsSync5 } from "fs";
|
|
4102
4131
|
import { join as join7 } from "path";
|
|
4103
|
-
function hasNextDep(cwd) {
|
|
4104
|
-
try {
|
|
4105
|
-
const pkg = JSON.parse(read(join7(cwd, "package.json")));
|
|
4106
|
-
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
|
|
4107
|
-
} catch {
|
|
4108
|
-
return false;
|
|
4109
|
-
}
|
|
4110
|
-
}
|
|
4111
4132
|
function middlewareInfo(cwd) {
|
|
4112
4133
|
const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
|
|
4113
4134
|
for (const rel of candidates) {
|
|
@@ -4117,7 +4138,7 @@ function middlewareInfo(cwd) {
|
|
|
4117
4138
|
return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
|
|
4118
4139
|
}
|
|
4119
4140
|
function detect2(cwd) {
|
|
4120
|
-
return
|
|
4141
|
+
return hasDependency(cwd, "next");
|
|
4121
4142
|
}
|
|
4122
4143
|
function rulesFile(relDir) {
|
|
4123
4144
|
return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
|
|
@@ -4171,9 +4192,6 @@ var nextAdapter = {
|
|
|
4171
4192
|
verify: verify2
|
|
4172
4193
|
};
|
|
4173
4194
|
|
|
4174
|
-
// src/protect/install/adapters/sveltekit.ts
|
|
4175
|
-
import { join as join9 } from "path";
|
|
4176
|
-
|
|
4177
4195
|
// src/protect/install/seam.ts
|
|
4178
4196
|
import { existsSync as existsSync6 } from "fs";
|
|
4179
4197
|
import { join as join8, dirname as dirname3 } from "path";
|
|
@@ -4231,12 +4249,7 @@ var SPEC = {
|
|
|
4231
4249
|
seamLabel: "SvelteKit server hook"
|
|
4232
4250
|
};
|
|
4233
4251
|
function detect3(cwd) {
|
|
4234
|
-
|
|
4235
|
-
const pkg = JSON.parse(read(join9(cwd, "package.json")));
|
|
4236
|
-
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
|
|
4237
|
-
} catch {
|
|
4238
|
-
return false;
|
|
4239
|
-
}
|
|
4252
|
+
return hasDependency(cwd, "@sveltejs/kit");
|
|
4240
4253
|
}
|
|
4241
4254
|
function wire3(cwd, opts) {
|
|
4242
4255
|
return wireSeam(cwd, opts, SPEC);
|
|
@@ -4253,7 +4266,6 @@ var sveltekitAdapter = {
|
|
|
4253
4266
|
};
|
|
4254
4267
|
|
|
4255
4268
|
// src/protect/install/adapters/astro.ts
|
|
4256
|
-
import { join as join10 } from "path";
|
|
4257
4269
|
var SPEC2 = {
|
|
4258
4270
|
templateName: "astro-middleware.ts",
|
|
4259
4271
|
candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
|
|
@@ -4263,12 +4275,7 @@ var SPEC2 = {
|
|
|
4263
4275
|
seamLabel: "Astro middleware"
|
|
4264
4276
|
};
|
|
4265
4277
|
function detect4(cwd) {
|
|
4266
|
-
|
|
4267
|
-
const pkg = JSON.parse(read(join10(cwd, "package.json")));
|
|
4268
|
-
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
|
|
4269
|
-
} catch {
|
|
4270
|
-
return false;
|
|
4271
|
-
}
|
|
4278
|
+
return hasDependency(cwd, "astro");
|
|
4272
4279
|
}
|
|
4273
4280
|
function wire4(cwd, opts) {
|
|
4274
4281
|
return wireSeam(cwd, opts, SPEC2);
|
|
@@ -4312,7 +4319,7 @@ var nuxtAdapter = {
|
|
|
4312
4319
|
|
|
4313
4320
|
// src/protect/install/find-app.ts
|
|
4314
4321
|
import { readFileSync as readFileSync8, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
|
|
4315
|
-
import { join as
|
|
4322
|
+
import { join as join9, relative as relative3, dirname as dirname4 } from "path";
|
|
4316
4323
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
4317
4324
|
"node_modules",
|
|
4318
4325
|
"patchstack",
|
|
@@ -4344,7 +4351,7 @@ function findAppInstance(cwd, re) {
|
|
|
4344
4351
|
}
|
|
4345
4352
|
for (const name of entries) {
|
|
4346
4353
|
if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
|
|
4347
|
-
const p =
|
|
4354
|
+
const p = join9(dir, name);
|
|
4348
4355
|
let st;
|
|
4349
4356
|
try {
|
|
4350
4357
|
st = lstatSync2(p);
|
|
@@ -4403,7 +4410,7 @@ function declaredEntries(cwd) {
|
|
|
4403
4410
|
const paths = /* @__PURE__ */ new Set();
|
|
4404
4411
|
let pkg;
|
|
4405
4412
|
try {
|
|
4406
|
-
pkg = JSON.parse(readFileSync8(
|
|
4413
|
+
pkg = JSON.parse(readFileSync8(join9(cwd, "package.json"), "utf8"));
|
|
4407
4414
|
} catch {
|
|
4408
4415
|
return paths;
|
|
4409
4416
|
}
|
|
@@ -4430,16 +4437,16 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
|
4430
4437
|
|
|
4431
4438
|
// src/protect/install/register.ts
|
|
4432
4439
|
import { existsSync as existsSync9 } from "fs";
|
|
4433
|
-
import { join as
|
|
4440
|
+
import { join as join12, resolve as resolve3 } from "path";
|
|
4434
4441
|
|
|
4435
4442
|
// src/protect/install/generic.ts
|
|
4436
4443
|
import { readFileSync as readFileSync9, existsSync as existsSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
|
|
4437
|
-
import { join as
|
|
4444
|
+
import { join as join11, dirname as dirname6 } from "path";
|
|
4438
4445
|
|
|
4439
4446
|
// src/protect/install/source-scope.ts
|
|
4440
4447
|
import { execFileSync } from "child_process";
|
|
4441
4448
|
import { existsSync as existsSync7 } from "fs";
|
|
4442
|
-
import { dirname as dirname5, join as
|
|
4449
|
+
import { dirname as dirname5, join as join10, resolve as resolve2 } from "path";
|
|
4443
4450
|
|
|
4444
4451
|
// src/protect/install/node-flags.ts
|
|
4445
4452
|
var flagName = (token) => token.split("=")[0];
|
|
@@ -4729,8 +4736,8 @@ function moduleKindOf(filePath, packageIsModule) {
|
|
|
4729
4736
|
}
|
|
4730
4737
|
function resolvesToGuardModule(specifier, query) {
|
|
4731
4738
|
const resolved = resolve2(dirname5(query.fromFile), specifier);
|
|
4732
|
-
if (withoutModuleExtension(resolved) !==
|
|
4733
|
-
const existing = GUARD_FILENAMES.filter((name) => existsSync7(
|
|
4739
|
+
if (withoutModuleExtension(resolved) !== join10(query.guardDir, "guard")) return false;
|
|
4740
|
+
const existing = GUARD_FILENAMES.filter((name) => existsSync7(join10(query.guardDir, name)));
|
|
4734
4741
|
if (existing.length === 0) return false;
|
|
4735
4742
|
const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
|
|
4736
4743
|
if (!query.strictExtensions) return true;
|
|
@@ -4749,16 +4756,16 @@ function withoutModuleExtension(filePath) {
|
|
|
4749
4756
|
// src/protect/install/generic.ts
|
|
4750
4757
|
function packageIsEsm(cwd) {
|
|
4751
4758
|
try {
|
|
4752
|
-
return JSON.parse(read(
|
|
4759
|
+
return JSON.parse(read(join11(cwd, "package.json"))).type === "module";
|
|
4753
4760
|
} catch {
|
|
4754
4761
|
return false;
|
|
4755
4762
|
}
|
|
4756
4763
|
}
|
|
4757
4764
|
function genericDir(cwd) {
|
|
4758
|
-
return existsSync8(
|
|
4765
|
+
return existsSync8(join11(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
4759
4766
|
}
|
|
4760
4767
|
function hasTypeScript(cwd) {
|
|
4761
|
-
return existsSync8(
|
|
4768
|
+
return existsSync8(join11(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
|
|
4762
4769
|
}
|
|
4763
4770
|
function genericGuardTarget(cwd) {
|
|
4764
4771
|
if (hasTypeScript(cwd)) {
|
|
@@ -4772,7 +4779,7 @@ function genericGuardTarget(cwd) {
|
|
|
4772
4779
|
function genericScaffoldFiles(cwd) {
|
|
4773
4780
|
const dir = genericDir(cwd);
|
|
4774
4781
|
const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
|
|
4775
|
-
return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync8(
|
|
4782
|
+
return candidates.map((file) => `${dir}/${file}`).filter((relative8) => existsSync8(join11(cwd, relative8)));
|
|
4776
4783
|
}
|
|
4777
4784
|
function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
4778
4785
|
const target = genericGuardTarget(cwd);
|
|
@@ -4780,15 +4787,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
|
4780
4787
|
guardFile ??= target.file;
|
|
4781
4788
|
const templates = templatesDir();
|
|
4782
4789
|
const dir = genericDir(cwd);
|
|
4783
|
-
const dst =
|
|
4790
|
+
const dst = join11(cwd, dir);
|
|
4784
4791
|
ensureProjectDirectorySync(cwd, dst);
|
|
4785
|
-
copyProjectFileSync(cwd,
|
|
4792
|
+
copyProjectFileSync(cwd, join11(templates, guardTemplate), join11(dst, guardFile));
|
|
4786
4793
|
const guardRel = `${dir}/${guardFile}`;
|
|
4787
4794
|
const changed = [guardRel];
|
|
4788
4795
|
if (!opts.demo) bakeSiteUuid(cwd, guardRel);
|
|
4789
|
-
const rulesDst =
|
|
4796
|
+
const rulesDst = join11(dst, "rules.json");
|
|
4790
4797
|
if (opts.demo || !existsSync8(rulesDst)) {
|
|
4791
|
-
copyProjectFileSync(cwd,
|
|
4798
|
+
copyProjectFileSync(cwd, join11(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
4792
4799
|
changed.push(`${dir}/rules.json`);
|
|
4793
4800
|
}
|
|
4794
4801
|
return { changed, dir };
|
|
@@ -4809,25 +4816,17 @@ function candidateEntries(cwd) {
|
|
|
4809
4816
|
"app.ts",
|
|
4810
4817
|
"app.js"
|
|
4811
4818
|
];
|
|
4812
|
-
const hits = names.filter((n) => existsSync8(
|
|
4819
|
+
const hits = names.filter((n) => existsSync8(join11(cwd, n)));
|
|
4813
4820
|
try {
|
|
4814
|
-
const pkg = JSON.parse(read(
|
|
4815
|
-
if (typeof pkg.main === "string" && existsSync8(
|
|
4821
|
+
const pkg = JSON.parse(read(join11(cwd, "package.json")));
|
|
4822
|
+
if (typeof pkg.main === "string" && existsSync8(join11(cwd, pkg.main))) hits.push(pkg.main);
|
|
4816
4823
|
} catch {
|
|
4817
4824
|
}
|
|
4818
4825
|
return [...new Set(hits)];
|
|
4819
4826
|
}
|
|
4820
|
-
function usesExpress(cwd) {
|
|
4821
|
-
try {
|
|
4822
|
-
const pkg = JSON.parse(read(join13(cwd, "package.json")));
|
|
4823
|
-
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
|
|
4824
|
-
} catch {
|
|
4825
|
-
return false;
|
|
4826
|
-
}
|
|
4827
|
-
}
|
|
4828
4827
|
function wiringPlan(cwd, dir) {
|
|
4829
4828
|
const entries = candidateEntries(cwd);
|
|
4830
|
-
const express =
|
|
4829
|
+
const express = hasDependency(cwd, "express");
|
|
4831
4830
|
const target = genericGuardTarget(cwd);
|
|
4832
4831
|
const lines = [
|
|
4833
4832
|
`no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/${target.file} + ${dir}/rules.json.`,
|
|
@@ -4923,9 +4922,9 @@ function escapeForRegExp(value) {
|
|
|
4923
4922
|
function genericVerify(cwd) {
|
|
4924
4923
|
const dir = genericDir(cwd);
|
|
4925
4924
|
const target = genericGuardTarget(cwd);
|
|
4926
|
-
const present2 = GUARD_FILENAMES.find((name) => existsSync8(
|
|
4925
|
+
const present2 = GUARD_FILENAMES.find((name) => existsSync8(join11(cwd, dir, name)));
|
|
4927
4926
|
const scaffolded = present2 !== void 0;
|
|
4928
|
-
const imported = scaffolded && guardIsImported(cwd,
|
|
4927
|
+
const imported = scaffolded && guardIsImported(cwd, join11(cwd, dir, present2));
|
|
4929
4928
|
return {
|
|
4930
4929
|
wired: scaffolded && imported,
|
|
4931
4930
|
checks: [
|
|
@@ -4961,7 +4960,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
|
|
|
4961
4960
|
function guardIsImported(cwd, guardPath) {
|
|
4962
4961
|
const guardDir = dirname6(guardPath);
|
|
4963
4962
|
const packageIsModule = packageIsEsm(cwd);
|
|
4964
|
-
const root = existsSync8(
|
|
4963
|
+
const root = existsSync8(join11(cwd, "src")) ? join11(cwd, "src") : cwd;
|
|
4965
4964
|
let found = false;
|
|
4966
4965
|
const walk2 = (d, depth) => {
|
|
4967
4966
|
if (found || depth > 8) return;
|
|
@@ -4974,7 +4973,7 @@ function guardIsImported(cwd, guardPath) {
|
|
|
4974
4973
|
for (const name of entries) {
|
|
4975
4974
|
if (found) return;
|
|
4976
4975
|
if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
|
|
4977
|
-
const p =
|
|
4976
|
+
const p = join11(d, name);
|
|
4978
4977
|
let st;
|
|
4979
4978
|
try {
|
|
4980
4979
|
st = lstatSync3(p);
|
|
@@ -5033,7 +5032,7 @@ function guardTarget(cwd, entryRel, spec) {
|
|
|
5033
5032
|
}
|
|
5034
5033
|
function packageType(cwd) {
|
|
5035
5034
|
try {
|
|
5036
|
-
return JSON.parse(read(
|
|
5035
|
+
return JSON.parse(read(join12(cwd, "package.json"))).type;
|
|
5037
5036
|
} catch {
|
|
5038
5037
|
return void 0;
|
|
5039
5038
|
}
|
|
@@ -5069,8 +5068,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
|
|
|
5069
5068
|
}
|
|
5070
5069
|
function namesGuardModule(specifier, fileRel, guard) {
|
|
5071
5070
|
return resolvesToGuardModule(specifier, {
|
|
5072
|
-
fromFile:
|
|
5073
|
-
guardDir: resolve3(
|
|
5071
|
+
fromFile: join12(guard.cwd, fileRel),
|
|
5072
|
+
guardDir: resolve3(join12(guard.cwd, guard.guardDir)),
|
|
5074
5073
|
kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
|
|
5075
5074
|
strictExtensions: true
|
|
5076
5075
|
});
|
|
@@ -5089,7 +5088,7 @@ function wireRegister(cwd, opts, spec) {
|
|
|
5089
5088
|
}
|
|
5090
5089
|
const target = guardTarget(cwd, entry.relPath, spec);
|
|
5091
5090
|
const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
|
|
5092
|
-
const p =
|
|
5091
|
+
const p = join12(cwd, entry.relPath);
|
|
5093
5092
|
const s = read(p);
|
|
5094
5093
|
const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
|
|
5095
5094
|
if (state.importAtTopLevel && state.callInAppScope) {
|
|
@@ -5159,17 +5158,17 @@ function firstStatementLine(lines) {
|
|
|
5159
5158
|
return i;
|
|
5160
5159
|
}
|
|
5161
5160
|
function serverIsGuarded(cwd, spec, server, guardDir) {
|
|
5162
|
-
const source = read(
|
|
5161
|
+
const source = read(join12(cwd, server.relPath));
|
|
5163
5162
|
const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
|
|
5164
5163
|
if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
|
|
5165
5164
|
return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
|
|
5166
5165
|
}
|
|
5167
5166
|
function verifyRegister(cwd, spec) {
|
|
5168
|
-
const dir = existsSync9(
|
|
5167
|
+
const dir = existsSync9(join12(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
5169
5168
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
5170
5169
|
const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
|
|
5171
|
-
const scaffolded = target ? existsSync9(
|
|
5172
|
-
const entrySource = entry ? read(
|
|
5170
|
+
const scaffolded = target ? existsSync9(join12(cwd, dir, target.file)) : false;
|
|
5171
|
+
const entrySource = entry ? read(join12(cwd, entry.relPath)) : "";
|
|
5173
5172
|
const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
|
|
5174
5173
|
const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
|
|
5175
5174
|
const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
|
|
@@ -5260,7 +5259,7 @@ var expressAdapter = {
|
|
|
5260
5259
|
|
|
5261
5260
|
// src/protect/install/reporting.ts
|
|
5262
5261
|
import { existsSync as existsSync10 } from "fs";
|
|
5263
|
-
import { join as
|
|
5262
|
+
import { join as join13 } from "path";
|
|
5264
5263
|
|
|
5265
5264
|
// src/protect/reporting-state.js
|
|
5266
5265
|
var REPORTING_STATES = Object.freeze([
|
|
@@ -5318,7 +5317,7 @@ function explainReportingState(state) {
|
|
|
5318
5317
|
|
|
5319
5318
|
// src/protect/install/reporting.ts
|
|
5320
5319
|
function siteIdentity(cwd, env) {
|
|
5321
|
-
const rc =
|
|
5320
|
+
const rc = join13(cwd, ".patchstackrc.json");
|
|
5322
5321
|
let recorded;
|
|
5323
5322
|
if (existsSync10(rc)) {
|
|
5324
5323
|
try {
|
|
@@ -5531,6 +5530,147 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
5531
5530
|
return { shell, action };
|
|
5532
5531
|
}
|
|
5533
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
|
+
|
|
5534
5674
|
// src/guide.ts
|
|
5535
5675
|
var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
|
|
5536
5676
|
var WIDGET_NEEDLE2 = "patchstack-widget";
|
|
@@ -5727,8 +5867,12 @@ async function collectGuideState(cwd) {
|
|
|
5727
5867
|
let claimUrl = null;
|
|
5728
5868
|
let endpointOverride = null;
|
|
5729
5869
|
let widgetOptOut = false;
|
|
5870
|
+
let environment = null;
|
|
5871
|
+
let environmentSource = null;
|
|
5730
5872
|
try {
|
|
5731
5873
|
const config = await resolveConfig({ cwd });
|
|
5874
|
+
environment = config.environment;
|
|
5875
|
+
environmentSource = config.environmentSource ?? null;
|
|
5732
5876
|
siteUuid = config.siteUuid;
|
|
5733
5877
|
if (siteUuid !== null && config.endpointTrusted !== false) {
|
|
5734
5878
|
claimUrl = buildClaimUrl(config.endpoint, siteUuid);
|
|
@@ -5755,6 +5899,8 @@ async function collectGuideState(cwd) {
|
|
|
5755
5899
|
siteUuid,
|
|
5756
5900
|
claimUrl,
|
|
5757
5901
|
endpointOverride,
|
|
5902
|
+
environment,
|
|
5903
|
+
environmentSource,
|
|
5758
5904
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
5759
5905
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
5760
5906
|
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
@@ -5773,7 +5919,7 @@ async function collectGuideState(cwd) {
|
|
|
5773
5919
|
protectionApplicable: protection.applicable
|
|
5774
5920
|
};
|
|
5775
5921
|
}
|
|
5776
|
-
var
|
|
5922
|
+
var ANSI3 = {
|
|
5777
5923
|
reset: "\x1B[0m",
|
|
5778
5924
|
bold: "\x1B[1m",
|
|
5779
5925
|
dim: "\x1B[2m",
|
|
@@ -5787,9 +5933,6 @@ function needsSourceProductionMarker(state) {
|
|
|
5787
5933
|
}
|
|
5788
5934
|
return !state.widgetFileHint.toLowerCase().endsWith(".html");
|
|
5789
5935
|
}
|
|
5790
|
-
function widgetTagInPlace(state) {
|
|
5791
|
-
return state.siteUuid !== null && !state.widgetOptOut && state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
5792
|
-
}
|
|
5793
5936
|
function countRemainingSteps(state) {
|
|
5794
5937
|
return [
|
|
5795
5938
|
state.installed?.section === "dependencies",
|
|
@@ -5801,214 +5944,146 @@ function countRemainingSteps(state) {
|
|
|
5801
5944
|
!state.protectionApplicable || state.protectionWired
|
|
5802
5945
|
].filter((step) => !step).length;
|
|
5803
5946
|
}
|
|
5804
|
-
function
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
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) {
|
|
5809
5976
|
const lines = [];
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
);
|
|
5813
|
-
const name = state.projectName ?? path12.basename(process.cwd());
|
|
5814
|
-
lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
|
|
5815
|
-
if (state.endpointOverride !== null) {
|
|
5816
|
-
lines.push(
|
|
5817
|
-
detail(
|
|
5818
|
-
`endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
|
|
5819
|
-
)
|
|
5820
|
-
);
|
|
5821
|
-
}
|
|
5822
|
-
lines.push("");
|
|
5823
|
-
if (!state.hasPackageJson) {
|
|
5824
|
-
lines.push(todo("No package.json found in this directory."));
|
|
5825
|
-
lines.push(detail("For a JS/Node app, run the guide from its package directory; check that package.json is readable and valid."));
|
|
5826
|
-
lines.push(detail("For a standalone HTML/CSS/browser-JavaScript site, use the disclosure widget directly."));
|
|
5827
|
-
lines.push(detail("Do not create a Node project, build hooks, or a server just to install the widget."));
|
|
5828
|
-
lines.push(detail("Use the correct site UUID or widget snippet from the Patchstack dashboard; never invent one."));
|
|
5829
|
-
lines.push(detail('See "Plain HTML sites" in AGENT-INSTALL.md. Widget-only setup provides no dependency scan or runtime protection.'));
|
|
5830
|
-
return lines.join("\n");
|
|
5831
|
-
}
|
|
5832
|
-
if (countRemainingSteps(state) > 0) {
|
|
5833
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Tell the user what you are doing as you go.")}`);
|
|
5834
|
-
lines.push(" Installs here take minutes and they are watching output they cannot read.");
|
|
5835
|
-
lines.push(" Say it in plain words: their site is built out of hundreds of ready-made");
|
|
5836
|
-
lines.push(" pieces of code, Patchstack keeps track of which ones it uses and warns them");
|
|
5837
|
-
lines.push(" when one turns out to be unsafe, and nothing their visitors see changes.");
|
|
5838
|
-
lines.push(" Avoid lockfile / dependency / manifest / npm / UUID when speaking to them.");
|
|
5839
|
-
lines.push("");
|
|
5840
|
-
}
|
|
5841
|
-
if (state.installed?.section === "dependencies") {
|
|
5842
|
-
lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
|
|
5843
|
-
} else if (state.installed !== null) {
|
|
5844
|
-
lines.push(todo(`Move @patchstack/connect to runtime dependencies (currently ${state.installed.section})`));
|
|
5845
|
-
lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
|
|
5846
|
-
lines.push(detail("The generated guard imports @patchstack/connect/protect at runtime."));
|
|
5847
|
-
} else {
|
|
5848
|
-
lines.push(todo("Install @patchstack/connect as a runtime dependency"));
|
|
5849
|
-
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)) {
|
|
5850
5979
|
if (state.packageManager === "bun") {
|
|
5851
|
-
lines.push(
|
|
5852
|
-
}
|
|
5853
|
-
}
|
|
5854
|
-
if (state.siteUuid !== null) {
|
|
5855
|
-
lines.push(done(`Site provisioned (${state.siteUuid})`));
|
|
5856
|
-
} else {
|
|
5857
|
-
lines.push(todo("Provision the site \u2014 run the first scan"));
|
|
5858
|
-
lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
|
|
5859
|
-
lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
|
|
5860
|
-
lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
|
|
5861
|
-
lines.push(detail("If your tool refuses to run this command, hand it to the person instead of working"));
|
|
5862
|
-
lines.push(detail('around it \u2014 see "When your tool will not run this CLI" in the reference guide.'));
|
|
5863
|
-
}
|
|
5864
|
-
if (state.installScanWired) {
|
|
5865
|
-
lines.push(done("Dependency-install scan wired (postinstall)"));
|
|
5866
|
-
} else {
|
|
5867
|
-
lines.push(todo("Scan again whenever dependencies are installed"));
|
|
5868
|
-
lines.push(detail('Edit package.json \u2192 "postinstall": "patchstack-connect scan"'));
|
|
5869
|
-
}
|
|
5870
|
-
if (!state.hasBuildScript) {
|
|
5871
|
-
lines.push(done("No build script to integrate (postinstall covers dependency changes)"));
|
|
5872
|
-
} else if (state.prebuildWired && state.postbuildWired) {
|
|
5873
|
-
lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
|
|
5874
|
-
} else if (state.packageManager === "bun") {
|
|
5875
|
-
lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
|
|
5876
|
-
lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
|
|
5877
|
-
} else {
|
|
5878
|
-
lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
|
|
5879
|
-
if (!state.prebuildWired) {
|
|
5880
|
-
lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
|
|
5881
|
-
}
|
|
5882
|
-
if (!state.postbuildWired) {
|
|
5883
|
-
lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
|
|
5884
|
-
}
|
|
5885
|
-
}
|
|
5886
|
-
const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
5887
|
-
if (state.widgetOptOut && !widgetOk) {
|
|
5888
|
-
lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
|
|
5889
|
-
} else if (widgetOk) {
|
|
5890
|
-
lines.push(done("Disclosure widget installed"));
|
|
5891
|
-
} else if (state.widgetInstalled) {
|
|
5892
|
-
lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
|
|
5893
|
-
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)`));
|
|
5894
|
-
} else if (state.siteUuid === null) {
|
|
5895
|
-
lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
|
|
5896
|
-
lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
|
|
5897
|
-
lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
|
|
5898
|
-
} else {
|
|
5899
|
-
lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
|
|
5900
|
-
lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
|
|
5901
|
-
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):";
|
|
5902
|
-
lines.push(detail(placement));
|
|
5903
|
-
lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
|
|
5904
|
-
lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
|
|
5905
|
-
}
|
|
5906
|
-
if (needsSourceProductionMarker(state)) {
|
|
5907
|
-
if (state.productionMarkerWired) {
|
|
5908
|
-
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"');
|
|
5909
5981
|
} else {
|
|
5910
|
-
lines.push(
|
|
5911
|
-
lines.push(
|
|
5912
|
-
detail(
|
|
5913
|
-
"Without it the widget treats the published site as build mode and shows the claim flow to visitors."
|
|
5914
|
-
)
|
|
5915
|
-
);
|
|
5916
|
-
const gate = productionGate(state.framework);
|
|
5917
|
-
if (hasJsxShell(state.framework)) {
|
|
5918
|
-
lines.push(detail(`Run \u2192 npx @patchstack/connect scan (adds it to ${state.widgetFileHint} automatically)`));
|
|
5919
|
-
lines.push(detail("Or add it by hand, in <head> above the widget tag:"));
|
|
5920
|
-
for (const snippetLine of buildSourceMarkerSnippet(state.framework).split("\n")) {
|
|
5921
|
-
lines.push(detail(` ${snippetLine}`));
|
|
5922
|
-
}
|
|
5923
|
-
} else {
|
|
5924
|
-
lines.push(
|
|
5925
|
-
detail(
|
|
5926
|
-
`Edit ${state.widgetFileHint} \u2192 using the framework's head mechanism, emit an inline`
|
|
5927
|
-
)
|
|
5928
|
-
);
|
|
5929
|
-
lines.push(detail(` <script>window.__PATCHSTACK_PROD__=true;</script> only when ${gate}.`));
|
|
5930
|
-
}
|
|
5931
|
-
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"');
|
|
5932
5984
|
}
|
|
5933
5985
|
}
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
for (const check of state.protectionChecks.filter((item) => !item.ok)) {
|
|
5944
|
-
lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
|
|
5945
|
-
}
|
|
5946
|
-
lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
|
|
5947
|
-
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
|
+
});
|
|
5948
5995
|
}
|
|
5949
|
-
|
|
5950
|
-
if (state.
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
lines.push(detail("anyone who loads it."));
|
|
5964
|
-
if (state.endpointOverride !== null) {
|
|
5965
|
-
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
5966
|
-
}
|
|
5967
|
-
} else {
|
|
5968
|
-
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
|
+
});
|
|
5969
6010
|
}
|
|
5970
|
-
if (
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
if (state.siteUuid !== null) {
|
|
5980
|
-
lines.push("");
|
|
5981
|
-
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Deploy to put this on your live site:")}`);
|
|
5982
|
-
lines.push(" These are source changes. Your deployed site keeps serving its previous build,");
|
|
5983
|
-
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
|
+
});
|
|
5984
6020
|
}
|
|
5985
|
-
const
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
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
|
+
}
|
|
6002
6046
|
);
|
|
6003
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
|
+
}
|
|
6004
6084
|
return lines.join("\n");
|
|
6005
6085
|
}
|
|
6006
6086
|
|
|
6007
|
-
// src/login.ts
|
|
6008
|
-
import { mkdtempSync } from "fs";
|
|
6009
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
6010
|
-
import path14 from "path";
|
|
6011
|
-
|
|
6012
6087
|
// src/device-flow.ts
|
|
6013
6088
|
import {
|
|
6014
6089
|
closeSync as closeSync2,
|
|
@@ -6309,7 +6384,7 @@ import { relative as relative4 } from "path";
|
|
|
6309
6384
|
|
|
6310
6385
|
// src/protect/install/runtime/entry.ts
|
|
6311
6386
|
import { existsSync as existsSync13, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
|
|
6312
|
-
import { join as
|
|
6387
|
+
import { join as join14, resolve as resolve4, sep as sep2 } from "path";
|
|
6313
6388
|
var CONVENTIONAL = [
|
|
6314
6389
|
"server.js",
|
|
6315
6390
|
"server.mjs",
|
|
@@ -6335,15 +6410,15 @@ var CONVENTIONAL = [
|
|
|
6335
6410
|
var LOADABLE = /\.(?:js|mjs|cjs)$/;
|
|
6336
6411
|
function readPackage2(cwd) {
|
|
6337
6412
|
try {
|
|
6338
|
-
const parsed = JSON.parse(readFileSync13(
|
|
6413
|
+
const parsed = JSON.parse(readFileSync13(join14(cwd, "package.json"), "utf8"));
|
|
6339
6414
|
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
6340
6415
|
} catch {
|
|
6341
6416
|
return null;
|
|
6342
6417
|
}
|
|
6343
6418
|
}
|
|
6344
|
-
function isFile(
|
|
6419
|
+
function isFile(path17) {
|
|
6345
6420
|
try {
|
|
6346
|
-
return statSync6(
|
|
6421
|
+
return statSync6(path17).isFile();
|
|
6347
6422
|
} catch {
|
|
6348
6423
|
return false;
|
|
6349
6424
|
}
|
|
@@ -6352,14 +6427,14 @@ function loadable(cwd, candidate) {
|
|
|
6352
6427
|
const written = resolve4(cwd, candidate);
|
|
6353
6428
|
if (!LOADABLE.test(written) || !isFile(written)) return null;
|
|
6354
6429
|
let root;
|
|
6355
|
-
let
|
|
6430
|
+
let path17;
|
|
6356
6431
|
try {
|
|
6357
6432
|
root = realpathSync4(cwd);
|
|
6358
|
-
|
|
6433
|
+
path17 = realpathSync4(written);
|
|
6359
6434
|
} catch {
|
|
6360
6435
|
return null;
|
|
6361
6436
|
}
|
|
6362
|
-
if (
|
|
6437
|
+
if (path17 !== root && !path17.startsWith(root + sep2)) return null;
|
|
6363
6438
|
return written;
|
|
6364
6439
|
}
|
|
6365
6440
|
function passable(token) {
|
|
@@ -6405,7 +6480,7 @@ function resolveEntry(cwd) {
|
|
|
6405
6480
|
}
|
|
6406
6481
|
return {
|
|
6407
6482
|
kind: "unavailable",
|
|
6408
|
-
reason: existsSync13(
|
|
6483
|
+
reason: existsSync13(join14(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
|
|
6409
6484
|
};
|
|
6410
6485
|
}
|
|
6411
6486
|
|
|
@@ -6439,8 +6514,8 @@ function preloadPath() {
|
|
|
6439
6514
|
// beside this file, in the repository
|
|
6440
6515
|
];
|
|
6441
6516
|
for (const candidate of candidates) {
|
|
6442
|
-
const
|
|
6443
|
-
if (existsSync14(
|
|
6517
|
+
const path17 = fileURLToPath2(candidate);
|
|
6518
|
+
if (existsSync14(path17)) return path17;
|
|
6444
6519
|
}
|
|
6445
6520
|
return null;
|
|
6446
6521
|
}
|
|
@@ -6787,15 +6862,15 @@ function formatRuntimeCheck(report) {
|
|
|
6787
6862
|
}
|
|
6788
6863
|
|
|
6789
6864
|
// src/map-command.ts
|
|
6790
|
-
import
|
|
6865
|
+
import path14 from "path";
|
|
6791
6866
|
|
|
6792
6867
|
// src/map/ts-loader.ts
|
|
6793
6868
|
import { createRequire } from "module";
|
|
6794
6869
|
import { pathToFileURL } from "url";
|
|
6795
|
-
import { join as
|
|
6870
|
+
import { join as join15 } from "path";
|
|
6796
6871
|
async function loadTypeScript(cwd) {
|
|
6797
6872
|
try {
|
|
6798
|
-
const req = createRequire(pathToFileURL(
|
|
6873
|
+
const req = createRequire(pathToFileURL(join15(cwd, "package.json")));
|
|
6799
6874
|
const resolved = req.resolve("typescript");
|
|
6800
6875
|
const mod = await import(pathToFileURL(resolved).href);
|
|
6801
6876
|
return mod.default ?? mod;
|
|
@@ -7225,7 +7300,7 @@ function dedupeSinks(sinks, owner) {
|
|
|
7225
7300
|
|
|
7226
7301
|
// src/map/module-graph.ts
|
|
7227
7302
|
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
7228
|
-
import { dirname as dirname8, join as
|
|
7303
|
+
import { dirname as dirname8, join as join16, relative as relative5, resolve as resolvePath } from "path";
|
|
7229
7304
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
7230
7305
|
function createModuleGraph(ts, opts) {
|
|
7231
7306
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -7303,7 +7378,7 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
7303
7378
|
}
|
|
7304
7379
|
candidates.push(base);
|
|
7305
7380
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
7306
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
7381
|
+
for (const e of RESOLVE_EXTS) candidates.push(join16(base, "index" + e));
|
|
7307
7382
|
for (const c of candidates) {
|
|
7308
7383
|
try {
|
|
7309
7384
|
if (statSync7(c).isFile()) return c;
|
|
@@ -7332,8 +7407,8 @@ function collectCallees(sf, ts) {
|
|
|
7332
7407
|
}
|
|
7333
7408
|
|
|
7334
7409
|
// src/map/coordinates.ts
|
|
7335
|
-
function runtimeCoordinate(source,
|
|
7336
|
-
if (/\[\d*\]/.test(
|
|
7410
|
+
function runtimeCoordinate(source, path17) {
|
|
7411
|
+
if (/\[\d*\]/.test(path17)) {
|
|
7337
7412
|
return { runtimeParameter: null, runtimeParameterReason: "array traversal: needs an array_key_value rule, not a dotted parameter" };
|
|
7338
7413
|
}
|
|
7339
7414
|
switch (source) {
|
|
@@ -7342,15 +7417,15 @@ function runtimeCoordinate(source, path18) {
|
|
|
7342
7417
|
case "multipart":
|
|
7343
7418
|
case "body":
|
|
7344
7419
|
case "server-fn-data":
|
|
7345
|
-
return { runtimeParameter: `post.${
|
|
7420
|
+
return { runtimeParameter: `post.${path17}` };
|
|
7346
7421
|
case "query":
|
|
7347
|
-
return { runtimeParameter: `get.${
|
|
7422
|
+
return { runtimeParameter: `get.${path17}` };
|
|
7348
7423
|
case "cookie":
|
|
7349
|
-
return { runtimeParameter: `cookie.${
|
|
7424
|
+
return { runtimeParameter: `cookie.${path17}` };
|
|
7350
7425
|
case "file":
|
|
7351
|
-
return { runtimeParameter: `files.${
|
|
7426
|
+
return { runtimeParameter: `files.${path17}` };
|
|
7352
7427
|
case "header":
|
|
7353
|
-
return { runtimeParameter: `server.HTTP_${
|
|
7428
|
+
return { runtimeParameter: `server.HTTP_${path17.toUpperCase().replace(/-/g, "_")}` };
|
|
7354
7429
|
case "route-param":
|
|
7355
7430
|
return { runtimeParameter: null, runtimeParameterReason: "route parameters are not exposed by the runtime resolver" };
|
|
7356
7431
|
default:
|
|
@@ -7382,8 +7457,8 @@ function addressSpaceOf(source) {
|
|
|
7382
7457
|
function isProvenFlow(confidence) {
|
|
7383
7458
|
return confidence === "exact-local" || confidence === "transformed-local";
|
|
7384
7459
|
}
|
|
7385
|
-
function inputIdOf(source,
|
|
7386
|
-
return `${addressSpaceOf(source)}:${
|
|
7460
|
+
function inputIdOf(source, path17) {
|
|
7461
|
+
return `${addressSpaceOf(source)}:${path17}`;
|
|
7387
7462
|
}
|
|
7388
7463
|
function withCoordinates(fields, source) {
|
|
7389
7464
|
return fields.map((f) => {
|
|
@@ -7517,14 +7592,16 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7517
7592
|
const inner = unwrap(e);
|
|
7518
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);
|
|
7519
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"]);
|
|
7520
7597
|
const visit = (n) => {
|
|
7521
|
-
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
|
|
7598
|
+
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression) && !isCallee(n)) {
|
|
7522
7599
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
7523
7600
|
}
|
|
7524
7601
|
if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
|
|
7525
7602
|
record(n.argumentExpression.text, sourceOfExpr(n.expression));
|
|
7526
7603
|
}
|
|
7527
|
-
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))) {
|
|
7528
7605
|
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
7529
7606
|
}
|
|
7530
7607
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
@@ -7576,6 +7653,7 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
7576
7653
|
|
|
7577
7654
|
// src/map/flows.ts
|
|
7578
7655
|
var MAX_DEPENDENCY_INPUT_FLOWS_PER_ENDPOINT = 100;
|
|
7656
|
+
var ACCESSOR_NAMESPACES = /* @__PURE__ */ new Set(["headers", "cookies"]);
|
|
7579
7657
|
function spaceOfKey(key) {
|
|
7580
7658
|
if (key === "query") return "get";
|
|
7581
7659
|
if (key === "params") return "route-param";
|
|
@@ -7601,18 +7679,18 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7601
7679
|
}
|
|
7602
7680
|
const CONTAINER_KEYS = /* @__PURE__ */ new Set([...REQ_SOURCES, "data", "input", "payload"]);
|
|
7603
7681
|
const rootPath = /* @__PURE__ */ new Map();
|
|
7604
|
-
const addRoot = (name,
|
|
7605
|
-
if (!rootPath.has(name)) rootPath.set(name, { path:
|
|
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 } : {} });
|
|
7606
7684
|
};
|
|
7607
|
-
for (const p of params ?? []) {
|
|
7685
|
+
for (const [index, p] of (params ?? []).entries()) {
|
|
7608
7686
|
if (!p?.name) continue;
|
|
7609
|
-
if (ts.isIdentifier(p.name)) addRoot(p.name.text, "");
|
|
7687
|
+
if (ts.isIdentifier(p.name)) addRoot(p.name.text, "", void 0, false, index === 0);
|
|
7610
7688
|
else if (ts.isObjectBindingPattern(p.name)) {
|
|
7611
7689
|
for (const el of p.name.elements) {
|
|
7612
7690
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7613
7691
|
const key = bindingKey(el, ts);
|
|
7614
7692
|
const container = key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7615
|
-
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));
|
|
7616
7694
|
}
|
|
7617
7695
|
}
|
|
7618
7696
|
}
|
|
@@ -7624,7 +7702,8 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7624
7702
|
const m = cur.expression.name.text;
|
|
7625
7703
|
if (["json", "formData", "text"].includes(m)) {
|
|
7626
7704
|
const root = rootIdentifier(cur.expression.expression, ts);
|
|
7627
|
-
|
|
7705
|
+
if (!root || !rootPath.has(root)) return void 0;
|
|
7706
|
+
return m === "formData" ? { path: "", space: "post", accessor: true } : { path: "", space: "post" };
|
|
7628
7707
|
}
|
|
7629
7708
|
if (["parse", "safeParse", "validate", "cast"].includes(m)) {
|
|
7630
7709
|
for (const a of cur.arguments) {
|
|
@@ -7634,21 +7713,21 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7634
7713
|
return void 0;
|
|
7635
7714
|
}
|
|
7636
7715
|
}
|
|
7637
|
-
|
|
7638
|
-
return p;
|
|
7716
|
+
return pathFromTainted(cur, ts, rootPath) ?? accessorRead(cur, ts, rootPath);
|
|
7639
7717
|
};
|
|
7640
7718
|
const aliasVisit = (n) => {
|
|
7641
7719
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
7642
7720
|
const base = requestReadPath(n.initializer);
|
|
7643
7721
|
if (base !== void 0) {
|
|
7644
|
-
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);
|
|
7645
7723
|
else if (ts.isObjectBindingPattern(n.name)) {
|
|
7646
7724
|
for (const el of n.name.elements) {
|
|
7647
7725
|
if (!ts.isBindingElement(el) || !ts.isIdentifier(el.name)) continue;
|
|
7648
7726
|
const key = bindingKey(el, ts);
|
|
7649
|
-
const
|
|
7650
|
-
const container = base.path === "" && key !== void 0 && CONTAINER_KEYS.has(key);
|
|
7651
|
-
|
|
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));
|
|
7652
7731
|
}
|
|
7653
7732
|
}
|
|
7654
7733
|
}
|
|
@@ -7680,7 +7759,7 @@ function linkFlows(bodyNode, params, inputs, sinks, ts, invocations) {
|
|
|
7680
7759
|
for (const a of args) for (const l of sinkArgumentLimitations(a, ts, rootPath)) sinkLimits.push(l);
|
|
7681
7760
|
for (let i = 0; i < args.length; i++) {
|
|
7682
7761
|
const role = argumentRoleOf(sink.kind, method, i, args.length);
|
|
7683
|
-
const whole = pathFromTainted(args[i], ts, rootPath);
|
|
7762
|
+
const whole = pathFromTainted(args[i], ts, rootPath) ?? accessorRead(args[i], ts, rootPath);
|
|
7684
7763
|
for (const read2 of taintedReadPaths(args[i], ts, rootPath)) {
|
|
7685
7764
|
const key = `${read2.space ?? "*"}:${read2.path}`;
|
|
7686
7765
|
const exact = whole !== void 0 && whole.path === read2.path && whole.space === read2.space;
|
|
@@ -7797,8 +7876,8 @@ function dedupeLimitations(list) {
|
|
|
7797
7876
|
function join22(base, seg) {
|
|
7798
7877
|
return base ? `${base}.${seg}` : seg;
|
|
7799
7878
|
}
|
|
7800
|
-
function normalizePath(
|
|
7801
|
-
return
|
|
7879
|
+
function normalizePath(path17) {
|
|
7880
|
+
return path17.replace(/\[\d*\]/g, "").split(".").filter(Boolean).join(".");
|
|
7802
7881
|
}
|
|
7803
7882
|
function fluentChainCalls(call, ts) {
|
|
7804
7883
|
let root = call;
|
|
@@ -7842,6 +7921,18 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7842
7921
|
const visit = (n) => {
|
|
7843
7922
|
if (!n) return;
|
|
7844
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
|
+
}
|
|
7845
7936
|
if (ts.isPropertyAccessExpression(n) || ts.isElementAccessExpression(n)) {
|
|
7846
7937
|
const read2 = pathFromTainted(n, ts, rootPath);
|
|
7847
7938
|
if (read2 !== void 0) {
|
|
@@ -7861,11 +7952,11 @@ function taintedReadPaths(node, ts, rootPath, includeDeferredBodies = true) {
|
|
|
7861
7952
|
function sinkArgumentLimitations(node, ts, rootPath) {
|
|
7862
7953
|
const out = [];
|
|
7863
7954
|
const seen = /* @__PURE__ */ new Set();
|
|
7864
|
-
const add = (kind,
|
|
7865
|
-
const key = `${kind}:${
|
|
7955
|
+
const add = (kind, detail2, n) => {
|
|
7956
|
+
const key = `${kind}:${detail2}`;
|
|
7866
7957
|
if (seen.has(key)) return;
|
|
7867
7958
|
seen.add(key);
|
|
7868
|
-
out.push({ kind, detail, line: lineOf(n) });
|
|
7959
|
+
out.push({ kind, detail: detail2, line: lineOf(n) });
|
|
7869
7960
|
};
|
|
7870
7961
|
const text = (n) => {
|
|
7871
7962
|
try {
|
|
@@ -7892,7 +7983,25 @@ function sinkArgumentLimitations(node, ts, rootPath) {
|
|
|
7892
7983
|
visit(node);
|
|
7893
7984
|
return out;
|
|
7894
7985
|
}
|
|
7895
|
-
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 = []) {
|
|
7896
8005
|
const segs = [];
|
|
7897
8006
|
let cur = node;
|
|
7898
8007
|
for (; ; ) {
|
|
@@ -7916,12 +8025,14 @@ function pathFromTainted(node, ts, rootPath) {
|
|
|
7916
8025
|
if (!cur || !ts.isIdentifier(cur)) return void 0;
|
|
7917
8026
|
const base = rootPath.get(cur.text);
|
|
7918
8027
|
if (base === void 0) return void 0;
|
|
8028
|
+
segs.push(...trailing);
|
|
7919
8029
|
let space = base.space;
|
|
7920
|
-
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])) {
|
|
7921
8031
|
space = spaceOfKey(segs[0]) ?? space;
|
|
7922
8032
|
segs.shift();
|
|
7923
8033
|
}
|
|
7924
|
-
|
|
8034
|
+
const request = base.request === true && segs.length === 0 ? { request: true } : {};
|
|
8035
|
+
return { path: normalizePath([base.path, ...segs].filter(Boolean).join(".")), space, ...request };
|
|
7925
8036
|
}
|
|
7926
8037
|
|
|
7927
8038
|
// src/map/invocations.ts
|
|
@@ -8164,7 +8275,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
8164
8275
|
|
|
8165
8276
|
// src/map/imports.ts
|
|
8166
8277
|
import { readFileSync as readFileSync15 } from "fs";
|
|
8167
|
-
import { join as
|
|
8278
|
+
import { join as join17 } from "path";
|
|
8168
8279
|
var MAX_SITES2 = 5;
|
|
8169
8280
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
8170
8281
|
function isNpmPackageName(pkg) {
|
|
@@ -8175,7 +8286,7 @@ function readPathAliases(cwd) {
|
|
|
8175
8286
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
8176
8287
|
let paths;
|
|
8177
8288
|
try {
|
|
8178
|
-
const raw = readFileSync15(
|
|
8289
|
+
const raw = readFileSync15(join17(cwd, name), "utf8");
|
|
8179
8290
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
8180
8291
|
paths = parsed.compilerOptions?.paths;
|
|
8181
8292
|
} catch {
|
|
@@ -8662,7 +8773,7 @@ function getStringFlag(flags, name) {
|
|
|
8662
8773
|
|
|
8663
8774
|
// src/build-stamp.ts
|
|
8664
8775
|
import { lstatSync as lstatSync4, readFileSync as readFileSync17, readdirSync as readdirSync6, realpathSync as realpathSync7 } from "fs";
|
|
8665
|
-
import { join as
|
|
8776
|
+
import { join as join18, relative as relative7, sep as sep3 } from "path";
|
|
8666
8777
|
var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
|
|
8667
8778
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
|
|
8668
8779
|
var MAX_DEPTH2 = 6;
|
|
@@ -8680,11 +8791,11 @@ function guardImportsRulesFile(dir, rulesName) {
|
|
|
8680
8791
|
}
|
|
8681
8792
|
for (const name of siblings) {
|
|
8682
8793
|
if (!GUARD_SOURCE.test(name)) continue;
|
|
8683
|
-
const
|
|
8794
|
+
const path17 = join18(dir, name);
|
|
8684
8795
|
try {
|
|
8685
|
-
const entry = lstatSync4(
|
|
8796
|
+
const entry = lstatSync4(path17);
|
|
8686
8797
|
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
|
|
8687
|
-
const source = stripComments(readFileSync17(
|
|
8798
|
+
const source = stripComments(readFileSync17(path17, "utf8"));
|
|
8688
8799
|
if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
|
|
8689
8800
|
} catch {
|
|
8690
8801
|
}
|
|
@@ -8705,9 +8816,9 @@ function findRulesFile(cwd) {
|
|
|
8705
8816
|
} catch {
|
|
8706
8817
|
return { kind: "none" };
|
|
8707
8818
|
}
|
|
8708
|
-
const contained = (
|
|
8819
|
+
const contained = (path17) => {
|
|
8709
8820
|
try {
|
|
8710
|
-
const real = realpathSync7(
|
|
8821
|
+
const real = realpathSync7(path17);
|
|
8711
8822
|
return real === root || real.startsWith(root + sep3);
|
|
8712
8823
|
} catch {
|
|
8713
8824
|
return false;
|
|
@@ -8723,25 +8834,25 @@ function findRulesFile(cwd) {
|
|
|
8723
8834
|
}
|
|
8724
8835
|
for (const name of entries) {
|
|
8725
8836
|
if (name.startsWith(".")) continue;
|
|
8726
|
-
const
|
|
8837
|
+
const path17 = join18(dir, name);
|
|
8727
8838
|
let entry;
|
|
8728
8839
|
try {
|
|
8729
|
-
entry = lstatSync4(
|
|
8840
|
+
entry = lstatSync4(path17);
|
|
8730
8841
|
} catch {
|
|
8731
8842
|
continue;
|
|
8732
8843
|
}
|
|
8733
8844
|
if (entry.isSymbolicLink()) continue;
|
|
8734
8845
|
if (entry.isDirectory()) {
|
|
8735
|
-
if (!SKIP_DIRECTORIES.has(name)) walk2(
|
|
8846
|
+
if (!SKIP_DIRECTORIES.has(name)) walk2(path17, depth + 1);
|
|
8736
8847
|
continue;
|
|
8737
8848
|
}
|
|
8738
8849
|
if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
|
|
8739
|
-
if (!contained(
|
|
8850
|
+
if (!contained(path17)) continue;
|
|
8740
8851
|
if (!guardImportsRulesFile(dir, name)) continue;
|
|
8741
8852
|
try {
|
|
8742
|
-
if (isRuleBundle(JSON.parse(readFileSync17(
|
|
8853
|
+
if (isRuleBundle(JSON.parse(readFileSync17(path17, "utf8")))) found.push(path17);
|
|
8743
8854
|
} catch {
|
|
8744
|
-
unreadable.push(
|
|
8855
|
+
unreadable.push(path17);
|
|
8745
8856
|
}
|
|
8746
8857
|
}
|
|
8747
8858
|
};
|
|
@@ -8821,7 +8932,7 @@ function isPreBundleBuildHook(env = process.env) {
|
|
|
8821
8932
|
return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
|
|
8822
8933
|
}
|
|
8823
8934
|
function undeliveredReportLines(err, config, cwd) {
|
|
8824
|
-
const lines = [`patchstack:
|
|
8935
|
+
const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
|
|
8825
8936
|
if (err.code === "UNAUTHORIZED") {
|
|
8826
8937
|
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
8827
8938
|
if (hasCredential) {
|
|
@@ -8836,7 +8947,7 @@ function undeliveredReportLines(err, config, cwd) {
|
|
|
8836
8947
|
}
|
|
8837
8948
|
}
|
|
8838
8949
|
lines.push(
|
|
8839
|
-
"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."
|
|
8840
8951
|
);
|
|
8841
8952
|
return lines;
|
|
8842
8953
|
}
|
|
@@ -8933,7 +9044,7 @@ async function runMap(flags) {
|
|
|
8933
9044
|
const json = JSON.stringify(map, null, 2);
|
|
8934
9045
|
const out = getStringFlag(flags, "out");
|
|
8935
9046
|
if (out) {
|
|
8936
|
-
atomicWriteFileSync(
|
|
9047
|
+
atomicWriteFileSync(path14.resolve(out), json, { encoding: "utf8" });
|
|
8937
9048
|
console.error(`patchstack: wrote ${out}`);
|
|
8938
9049
|
} else if (flags.get("upload") !== true) {
|
|
8939
9050
|
console.log(json);
|
|
@@ -8983,13 +9094,13 @@ async function runMap(flags) {
|
|
|
8983
9094
|
|
|
8984
9095
|
// src/setup.ts
|
|
8985
9096
|
import { readFileSync as readFileSync18 } from "fs";
|
|
8986
|
-
import
|
|
9097
|
+
import path15 from "path";
|
|
8987
9098
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
8988
9099
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
8989
9100
|
function setupProtection(cwd) {
|
|
8990
|
-
const install = runProtect(cwd);
|
|
9101
|
+
const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
|
|
8991
9102
|
const verification = runVerify(cwd);
|
|
8992
|
-
return { install, verification };
|
|
9103
|
+
return { install, verification, log: log2 };
|
|
8993
9104
|
}
|
|
8994
9105
|
function appendHook(existing, command) {
|
|
8995
9106
|
if (existing === void 0 || existing.trim().length === 0) {
|
|
@@ -9008,7 +9119,7 @@ function prependHook(existing, command) {
|
|
|
9008
9119
|
return [command, ...remaining].join(" && ");
|
|
9009
9120
|
}
|
|
9010
9121
|
function wireBuildScripts(cwd, packageManager) {
|
|
9011
|
-
const target =
|
|
9122
|
+
const target = path15.join(cwd, "package.json");
|
|
9012
9123
|
const raw = readFileSync18(target, "utf8");
|
|
9013
9124
|
const pkg = JSON.parse(raw);
|
|
9014
9125
|
const scripts = pkg.scripts ?? {};
|
|
@@ -9082,7 +9193,7 @@ Usage:
|
|
|
9082
9193
|
If no UUID is configured, the server
|
|
9083
9194
|
provisions one and we persist it. After a
|
|
9084
9195
|
successful post it also adds/updates the
|
|
9085
|
-
|
|
9196
|
+
Patchstack Connector <script> tag in the root
|
|
9086
9197
|
HTML shell (index.html, public/index.html,
|
|
9087
9198
|
or src/app.html) \u2014 opt out with
|
|
9088
9199
|
"widget": false in .patchstackrc.json.
|
|
@@ -9191,6 +9302,8 @@ Usage:
|
|
|
9191
9302
|
Global:
|
|
9192
9303
|
--version Print the installed version of this package and exit
|
|
9193
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
|
|
9194
9307
|
|
|
9195
9308
|
Options (for scan, setup, status, and uninstall):
|
|
9196
9309
|
--site-uuid <uuid> Override the configured site UUID
|
|
@@ -9246,6 +9359,13 @@ Examples:
|
|
|
9246
9359
|
npx @patchstack/connect demo-guide node-serialize
|
|
9247
9360
|
`;
|
|
9248
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
|
+
}
|
|
9249
9369
|
function parseArgs(argv) {
|
|
9250
9370
|
const args = argv.slice(2);
|
|
9251
9371
|
const positional = [];
|
|
@@ -9276,6 +9396,20 @@ function parseArgs(argv) {
|
|
|
9276
9396
|
flags
|
|
9277
9397
|
};
|
|
9278
9398
|
}
|
|
9399
|
+
function resolveCliConfig(args, extra = {}) {
|
|
9400
|
+
return resolveConfig({
|
|
9401
|
+
cwd: process.cwd(),
|
|
9402
|
+
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
9403
|
+
cliEndpoint: getStringFlag(args.flags, "endpoint"),
|
|
9404
|
+
...extra
|
|
9405
|
+
});
|
|
9406
|
+
}
|
|
9407
|
+
function runningInCi() {
|
|
9408
|
+
return process.env.CI !== void 0 && process.env.CI !== "" && process.env.CI !== "false";
|
|
9409
|
+
}
|
|
9410
|
+
function gitignoreOutcomeLine(ignore) {
|
|
9411
|
+
return ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`;
|
|
9412
|
+
}
|
|
9279
9413
|
async function runInit(args) {
|
|
9280
9414
|
const uuid = args.positional[0];
|
|
9281
9415
|
if (!uuid) {
|
|
@@ -9283,26 +9417,22 @@ async function runInit(args) {
|
|
|
9283
9417
|
console.error("Usage: patchstack-connect init <site-uuid>");
|
|
9284
9418
|
return 1;
|
|
9285
9419
|
}
|
|
9286
|
-
if (
|
|
9420
|
+
if (!isCanonicalUuid(uuid)) {
|
|
9287
9421
|
console.error(`Error: "${uuid}" does not look like a valid UUID.`);
|
|
9288
9422
|
return 1;
|
|
9289
9423
|
}
|
|
9290
9424
|
const target = await writeConfigFile(process.cwd(), { siteUuid: uuid });
|
|
9291
9425
|
console.log(`Wrote ${target}`);
|
|
9292
9426
|
console.log("");
|
|
9293
|
-
console.log("Next: run `npx @patchstack/connect scan
|
|
9427
|
+
console.log("Next: run `npx @patchstack/connect scan`.");
|
|
9294
9428
|
return 0;
|
|
9295
9429
|
}
|
|
9296
9430
|
async function runClaim(args) {
|
|
9297
|
-
if (
|
|
9431
|
+
if (runningInCi()) {
|
|
9298
9432
|
console.error("`claim` is interactive and cannot run in CI. Claim the site from a developer machine.");
|
|
9299
9433
|
return 1;
|
|
9300
9434
|
}
|
|
9301
|
-
const config = await
|
|
9302
|
-
cwd: process.cwd(),
|
|
9303
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
9304
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
9305
|
-
});
|
|
9435
|
+
const config = await resolveCliConfig(args);
|
|
9306
9436
|
const siteUuid = config.siteUuid;
|
|
9307
9437
|
if (siteUuid === null) {
|
|
9308
9438
|
console.error("\n No site to claim yet \u2014 run `npx @patchstack/connect scan` first.\n");
|
|
@@ -9316,10 +9446,8 @@ async function runClaim(args) {
|
|
|
9316
9446
|
console.log(` ${result2.dashboardUrl ?? buildClaimUrl(config.endpoint, siteUuid)}`);
|
|
9317
9447
|
if (result2.credentialSaved === true) {
|
|
9318
9448
|
const ignore = await secretFileIgnored(process.cwd());
|
|
9319
|
-
console.log(`
|
|
9320
|
-
console.log(
|
|
9321
|
-
ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
|
|
9322
|
-
);
|
|
9449
|
+
console.log(` Saved a new credential to ${SECRET_CONFIG_FILENAME}.`);
|
|
9450
|
+
console.log(gitignoreOutcomeLine(ignore));
|
|
9323
9451
|
}
|
|
9324
9452
|
console.log("");
|
|
9325
9453
|
return 0;
|
|
@@ -9329,8 +9457,7 @@ async function runClaim(args) {
|
|
|
9329
9457
|
Your code: ${userCode}`);
|
|
9330
9458
|
console.log(` Claim at: ${verificationUri}
|
|
9331
9459
|
`);
|
|
9332
|
-
console.log(" Open
|
|
9333
|
-
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");
|
|
9334
9461
|
};
|
|
9335
9462
|
if (args.flags.has("wait")) {
|
|
9336
9463
|
const pending = readPendingClaim(siteUuid);
|
|
@@ -9374,8 +9501,7 @@ async function runClaim(args) {
|
|
|
9374
9501
|
return 1;
|
|
9375
9502
|
}
|
|
9376
9503
|
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
9377
|
-
console.log(" Give
|
|
9378
|
-
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");
|
|
9379
9505
|
return 0;
|
|
9380
9506
|
}
|
|
9381
9507
|
const result = await claim2(config, (userCode, verificationUri) => {
|
|
@@ -9395,24 +9521,17 @@ async function runClaim(args) {
|
|
|
9395
9521
|
return 1;
|
|
9396
9522
|
}
|
|
9397
9523
|
async function runLogin(args) {
|
|
9398
|
-
if (
|
|
9524
|
+
if (runningInCi()) {
|
|
9399
9525
|
console.error("`login` is interactive and cannot run in CI. Set PATCHSTACK_PULSE_AUTH instead.");
|
|
9400
9526
|
return 1;
|
|
9401
9527
|
}
|
|
9402
|
-
const config = await
|
|
9403
|
-
cwd: process.cwd(),
|
|
9404
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
9405
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
9406
|
-
});
|
|
9528
|
+
const config = await resolveCliConfig(args);
|
|
9407
9529
|
const approved = async () => {
|
|
9408
9530
|
const ignore = await secretFileIgnored(process.cwd());
|
|
9409
9531
|
console.log(`
|
|
9410
9532
|
\u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
9411
|
-
console.log(
|
|
9412
|
-
|
|
9413
|
-
);
|
|
9414
|
-
console.log(" The previous credential no longer works. Update it anywhere else it was set:");
|
|
9415
|
-
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
9533
|
+
console.log(gitignoreOutcomeLine(ignore));
|
|
9534
|
+
console.log(" The old credential no longer works. Update it in CI, hosting and other checkouts.\n");
|
|
9416
9535
|
return 0;
|
|
9417
9536
|
};
|
|
9418
9537
|
if (args.flags.has("wait")) {
|
|
@@ -9433,9 +9552,8 @@ async function runLogin(args) {
|
|
|
9433
9552
|
Your code: ${userCode}`);
|
|
9434
9553
|
console.log(` Approve at: ${verificationUri}
|
|
9435
9554
|
`);
|
|
9436
|
-
console.log(" Open
|
|
9437
|
-
console.log("
|
|
9438
|
-
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");
|
|
9439
9557
|
};
|
|
9440
9558
|
if (process.stdout.isTTY !== true) {
|
|
9441
9559
|
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
@@ -9460,8 +9578,7 @@ async function runLogin(args) {
|
|
|
9460
9578
|
return 1;
|
|
9461
9579
|
}
|
|
9462
9580
|
prompt(started.pending.userCode, started.pending.verificationUri);
|
|
9463
|
-
console.log(" Give
|
|
9464
|
-
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");
|
|
9465
9582
|
return 0;
|
|
9466
9583
|
}
|
|
9467
9584
|
const result = await login(config, (userCode, verificationUri) => {
|
|
@@ -9479,8 +9596,8 @@ var MAX_RETRY_TIMEOUT_MS = 18e4;
|
|
|
9479
9596
|
async function postManifestAccepted(config, payload) {
|
|
9480
9597
|
const { response, environmentUsed } = await postManifestWithEnvironmentFallback(config, payload);
|
|
9481
9598
|
if (environmentUsed !== config.environment) {
|
|
9482
|
-
|
|
9483
|
-
`
|
|
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.`
|
|
9484
9601
|
);
|
|
9485
9602
|
}
|
|
9486
9603
|
return response;
|
|
@@ -9496,75 +9613,69 @@ async function postManifestWithPatience(config, payload) {
|
|
|
9496
9613
|
if (!(err instanceof PatchstackError) || err.code !== "NETWORK_TIMEOUT") throw err;
|
|
9497
9614
|
const timeoutMs = Math.min(config.timeoutMs * RETRY_TIMEOUT_FACTOR, MAX_RETRY_TIMEOUT_MS);
|
|
9498
9615
|
if (timeoutMs <= config.timeoutMs) throw err;
|
|
9499
|
-
console.warn(
|
|
9500
|
-
|
|
9501
|
-
);
|
|
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.`);
|
|
9502
9618
|
const response = await postManifestAccepted({ ...config, timeoutMs }, payload);
|
|
9503
9619
|
try {
|
|
9504
9620
|
const target = await persistTimeout(process.cwd(), timeoutMs);
|
|
9505
|
-
|
|
9621
|
+
detail(`Saved a ${timeoutMs}ms request timeout to ${target}, so builds inherit it.`);
|
|
9506
9622
|
} catch {
|
|
9507
|
-
console.warn(
|
|
9508
|
-
`patchstack: could not save the timeout; set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where builds run.`
|
|
9509
|
-
);
|
|
9623
|
+
console.warn(`Could not save the longer wait. Set PATCHSTACK_TIMEOUT_MS=${timeoutMs} where your builds run.`);
|
|
9510
9624
|
}
|
|
9511
9625
|
return response;
|
|
9512
9626
|
}
|
|
9513
9627
|
}
|
|
9628
|
+
function plural(count, one, many) {
|
|
9629
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
9630
|
+
}
|
|
9514
9631
|
async function runScan(args, options = {}) {
|
|
9515
9632
|
const dryRun = args.flags.get("dry-run") === true;
|
|
9516
|
-
const
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9633
|
+
const say = (line) => {
|
|
9634
|
+
if (verbose || dryRun) console.log(line);
|
|
9635
|
+
};
|
|
9636
|
+
const report = options.report ?? emptyReport();
|
|
9637
|
+
const config = await resolveCliConfig(args, {
|
|
9520
9638
|
cliClaimToken: getStringFlag(args.flags, "claim-token"),
|
|
9521
9639
|
// The one command that reports them, so the one command that resolves them.
|
|
9522
9640
|
detectSiteIdentity: true
|
|
9523
9641
|
});
|
|
9524
9642
|
const manifest = await scanLockfile(process.cwd());
|
|
9525
9643
|
for (const warning of manifest.warnings ?? []) {
|
|
9526
|
-
console.warn(`patchstack: ${warning}`);
|
|
9644
|
+
if (dryRun) console.warn(`patchstack: ${warning}`);
|
|
9645
|
+
else report.missing.push({ text: warning });
|
|
9527
9646
|
}
|
|
9528
9647
|
const installPaths = args.flags.get("install-paths") === true;
|
|
9529
9648
|
const { payload, stats } = buildWirePayload(manifest, { installPaths });
|
|
9530
|
-
|
|
9531
|
-
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
9532
|
-
);
|
|
9649
|
+
say(`Found ${payload.packages.length} package versions across ${stats.uniqueNames} packages (${manifest.ecosystem}).`);
|
|
9533
9650
|
if (installPaths) {
|
|
9534
9651
|
const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
|
|
9535
9652
|
console.log(
|
|
9536
|
-
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".`
|
|
9537
9654
|
);
|
|
9538
9655
|
}
|
|
9539
9656
|
if (config.environment === "local") {
|
|
9540
|
-
|
|
9541
|
-
"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."
|
|
9542
|
-
);
|
|
9657
|
+
say("Environment: local (this machine). Set PATCHSTACK_ENVIRONMENT=production on a live build your host does not identify.");
|
|
9543
9658
|
} else {
|
|
9544
9659
|
const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
|
|
9545
|
-
|
|
9660
|
+
say(`Environment: ${config.environment}${because}.`);
|
|
9546
9661
|
}
|
|
9547
9662
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9548
|
-
|
|
9549
|
-
`Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
|
|
9550
|
-
);
|
|
9663
|
+
say(`Endpoint override: ${config.endpoint}`);
|
|
9551
9664
|
}
|
|
9552
9665
|
if (stats.duplicateNames.length > 0) {
|
|
9553
9666
|
const sample = stats.duplicateNames.slice(0, 10).join(", ");
|
|
9554
9667
|
const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
|
|
9555
|
-
|
|
9556
|
-
`${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
|
|
9557
|
-
);
|
|
9668
|
+
say(`${stats.duplicateNames.length} package(s) at more than one version: ${sample}${more}`);
|
|
9558
9669
|
}
|
|
9559
9670
|
const body = buildManifestBody(config, payload);
|
|
9560
9671
|
if (typeof body.url === "string") {
|
|
9561
|
-
|
|
9672
|
+
say(`Reporting app address: ${body.url}`);
|
|
9562
9673
|
}
|
|
9563
9674
|
if (typeof body.name === "string") {
|
|
9564
|
-
|
|
9675
|
+
say(`Reporting app name: "${body.name}"`);
|
|
9565
9676
|
}
|
|
9566
9677
|
if (typeof config.claimToken === "string" && config.claimToken !== "") {
|
|
9567
|
-
|
|
9678
|
+
say("Claim token set. The site will join the Patchstack account that issued it.");
|
|
9568
9679
|
}
|
|
9569
9680
|
if (dryRun) {
|
|
9570
9681
|
console.log("");
|
|
@@ -9583,15 +9694,15 @@ async function runScan(args, options = {}) {
|
|
|
9583
9694
|
}
|
|
9584
9695
|
if (isPreBundleBuildHook()) {
|
|
9585
9696
|
const cleared = applyBuildStamp(process.cwd(), null);
|
|
9586
|
-
if (cleared.kind === "cleared")
|
|
9697
|
+
if (cleared.kind === "cleared") say(`Removed the previous map binding from ${cleared.file}.`);
|
|
9587
9698
|
}
|
|
9588
9699
|
const shellFramework = detectStack(payload.packages).framework;
|
|
9589
9700
|
if (config.widget) {
|
|
9590
|
-
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages));
|
|
9701
|
+
reportSourceMarker(shellFramework, computeManifestChecksum(payload.packages), report);
|
|
9591
9702
|
}
|
|
9592
9703
|
const provisioning = config.siteUuid === null;
|
|
9593
9704
|
if (provisioning) {
|
|
9594
|
-
|
|
9705
|
+
say("No site yet. Creating one on Patchstack\u2026");
|
|
9595
9706
|
}
|
|
9596
9707
|
let response;
|
|
9597
9708
|
try {
|
|
@@ -9601,113 +9712,147 @@ async function runScan(args, options = {}) {
|
|
|
9601
9712
|
for (const line of undeliveredReportLines(err, config, process.cwd())) console.error(line);
|
|
9602
9713
|
return 0;
|
|
9603
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
|
+
}
|
|
9604
9728
|
if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
|
|
9605
9729
|
const target = await persistSiteUuid(process.cwd(), response.uuid);
|
|
9606
|
-
|
|
9730
|
+
report.done.push("Added this project to Patchstack");
|
|
9731
|
+
say(`Created site ${response.uuid}. Saved to ${target}.`);
|
|
9607
9732
|
}
|
|
9608
9733
|
if (typeof response.api_key === "string" && response.api_key.length > 0) {
|
|
9609
9734
|
const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
|
|
9610
9735
|
const saved = await persistApiKey(process.cwd(), response.api_key);
|
|
9611
|
-
|
|
9612
|
-
|
|
9613
|
-
|
|
9614
|
-
|
|
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
|
+
}
|
|
9615
9746
|
if (hadCredentialInConfig) {
|
|
9616
|
-
|
|
9617
|
-
"
|
|
9618
|
-
|
|
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
|
+
});
|
|
9619
9753
|
}
|
|
9620
9754
|
}
|
|
9621
|
-
|
|
9622
|
-
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
9627
|
-
}
|
|
9628
|
-
const claimLines = claimOutcomeLines(response.claim, config);
|
|
9629
|
-
if (claimLines.length > 0) {
|
|
9630
|
-
console.log("");
|
|
9631
|
-
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]));
|
|
9632
9760
|
}
|
|
9633
9761
|
const connected = response.claim?.state === "claimed" || response.claim?.state === "owned-by-you";
|
|
9634
9762
|
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
9635
9763
|
if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
|
|
9636
|
-
reportSourceWidget(effectiveUuid, shellFramework);
|
|
9637
|
-
}
|
|
9638
|
-
const
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
console.log(" 3. From this terminal: npx @patchstack/connect claim");
|
|
9648
|
-
} else {
|
|
9649
|
-
console.log(" 1. Open this dashboard link in a browser:");
|
|
9650
|
-
console.log(` ${buildClaimUrl(config.endpoint, linkUuid)}`);
|
|
9651
|
-
console.log(" 2. From this terminal: npx @patchstack/connect claim");
|
|
9652
|
-
}
|
|
9653
|
-
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
9654
|
-
console.log(" (this URL inherits the endpoint override above)");
|
|
9655
|
-
}
|
|
9656
|
-
}
|
|
9657
|
-
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) {
|
|
9658
9775
|
try {
|
|
9659
|
-
|
|
9660
|
-
const remaining = countRemainingSteps(state);
|
|
9661
|
-
if (remaining > 0) {
|
|
9662
|
-
const hooksMissing = !(state.prebuildWired && state.postbuildWired);
|
|
9663
|
-
console.log("");
|
|
9664
|
-
console.log(
|
|
9665
|
-
`Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
|
|
9666
|
-
);
|
|
9667
|
-
console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
|
|
9668
|
-
}
|
|
9776
|
+
await printScanReport(config, effectiveUuid, outcome, report, options.showRemainingSetup !== false);
|
|
9669
9777
|
} catch {
|
|
9670
9778
|
}
|
|
9671
9779
|
}
|
|
9672
9780
|
return 0;
|
|
9673
9781
|
}
|
|
9674
|
-
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
|
+
};
|
|
9675
9826
|
try {
|
|
9676
9827
|
const hint = hasJsxShell(framework) ? resolveWidgetFileHint(process.cwd(), framework) : null;
|
|
9677
9828
|
const jsxShell = hint !== null && !hint.toLowerCase().endsWith(".html") ? hint : null;
|
|
9678
9829
|
const result = ensureSourceWidget(process.cwd(), siteUuid, jsxShell);
|
|
9679
9830
|
switch (result.action) {
|
|
9680
9831
|
case "added":
|
|
9681
|
-
|
|
9682
|
-
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}`);
|
|
9683
9833
|
break;
|
|
9684
9834
|
case "updated":
|
|
9685
|
-
|
|
9835
|
+
report.done.push(`Updated the Patchstack widget in ${result.shell}`);
|
|
9686
9836
|
break;
|
|
9687
9837
|
case "unchanged":
|
|
9688
|
-
|
|
9838
|
+
report.done.push(`The Patchstack widget is already in ${result.shell}`);
|
|
9689
9839
|
break;
|
|
9690
9840
|
case "manual":
|
|
9691
|
-
|
|
9841
|
+
report.done.push(`Left your own Patchstack widget in ${result.shell} as it is`);
|
|
9692
9842
|
break;
|
|
9693
9843
|
case "no-body":
|
|
9694
|
-
|
|
9695
|
-
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
9844
|
+
handAdd(`${result.shell} has no </body>. Add this to your main page layout, just before </body>:`);
|
|
9696
9845
|
break;
|
|
9697
9846
|
case "no-shell":
|
|
9698
|
-
|
|
9699
|
-
console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
|
|
9700
|
-
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
9847
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
9701
9848
|
break;
|
|
9702
9849
|
}
|
|
9703
|
-
if (result.action === "added" || result.action === "updated") {
|
|
9704
|
-
console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
|
|
9705
|
-
}
|
|
9706
9850
|
} catch (err) {
|
|
9707
|
-
|
|
9851
|
+
handAdd("Add this to your main page layout, just before </body>:");
|
|
9852
|
+
detail(`Widget: skipped (${err.message}).`);
|
|
9708
9853
|
}
|
|
9709
9854
|
}
|
|
9710
|
-
function reportSourceMarker(framework, checksum
|
|
9855
|
+
function reportSourceMarker(framework, checksum, report) {
|
|
9711
9856
|
try {
|
|
9712
9857
|
const shell = resolveWidgetFileHint(process.cwd(), framework);
|
|
9713
9858
|
if (shell === null || shell.toLowerCase().endsWith(".html")) {
|
|
@@ -9716,30 +9861,25 @@ function reportSourceMarker(framework, checksum = null) {
|
|
|
9716
9861
|
const result = ensureSourceMarker(process.cwd(), shell, framework, checksum);
|
|
9717
9862
|
switch (result.action) {
|
|
9718
9863
|
case "added":
|
|
9719
|
-
|
|
9720
|
-
|
|
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)}).`);
|
|
9721
9866
|
break;
|
|
9722
9867
|
case "manual":
|
|
9723
|
-
|
|
9868
|
+
detail(`Production marker: already in ${shell}.`);
|
|
9724
9869
|
break;
|
|
9725
9870
|
case "no-anchor":
|
|
9726
9871
|
case "unsupported":
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
} else {
|
|
9735
|
-
console.log(
|
|
9736
|
-
` Emit <script>window.__PATCHSTACK_PROD__=true;</script> only when ${productionGate(framework)}.`
|
|
9737
|
-
);
|
|
9738
|
-
}
|
|
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
|
+
});
|
|
9739
9879
|
break;
|
|
9740
9880
|
}
|
|
9741
9881
|
} catch (err) {
|
|
9742
|
-
|
|
9882
|
+
detail(`Production marker: skipped (${err.message}).`);
|
|
9743
9883
|
}
|
|
9744
9884
|
}
|
|
9745
9885
|
async function runProtectCommand(args) {
|
|
@@ -9788,12 +9928,7 @@ async function runDemoCommand(args) {
|
|
|
9788
9928
|
try {
|
|
9789
9929
|
const scenario = resolveDemoScenario(args.positional[0]);
|
|
9790
9930
|
const cwd = process.cwd();
|
|
9791
|
-
const config = await
|
|
9792
|
-
cwd,
|
|
9793
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
9794
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint"),
|
|
9795
|
-
requireSiteUuid: true
|
|
9796
|
-
});
|
|
9931
|
+
const config = await resolveCliConfig(args, { requireSiteUuid: true });
|
|
9797
9932
|
if (config.environment !== "production") {
|
|
9798
9933
|
throw new DemoError(
|
|
9799
9934
|
"The production-backed demo requires PATCHSTACK_ENVIRONMENT=production. Unset the sandbox override and try again."
|
|
@@ -9884,8 +10019,7 @@ async function runGuide(args) {
|
|
|
9884
10019
|
let allDone = false;
|
|
9885
10020
|
try {
|
|
9886
10021
|
const state = await collectGuideState(process.cwd());
|
|
9887
|
-
|
|
9888
|
-
console.log(renderGuideChecklist(state, useColor));
|
|
10022
|
+
console.log(renderGuideChecklist(state, useColor(), {}, { verbose }));
|
|
9889
10023
|
allDone = countRemainingSteps(state) === 0;
|
|
9890
10024
|
} catch {
|
|
9891
10025
|
}
|
|
@@ -9903,102 +10037,87 @@ async function runGuide(args) {
|
|
|
9903
10037
|
console.log(readFileSync19(guidePath, "utf8"));
|
|
9904
10038
|
return 0;
|
|
9905
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
|
+
}
|
|
9906
10073
|
async function runSetup(args) {
|
|
9907
10074
|
if (args.flags.get("dry-run") === true) {
|
|
9908
|
-
console.error("
|
|
10075
|
+
console.error("Setup cannot run as a preview. To see what would be sent, run: npx @patchstack/connect scan --dry-run");
|
|
9909
10076
|
return 1;
|
|
9910
10077
|
}
|
|
9911
10078
|
const before = await collectGuideState(process.cwd());
|
|
9912
10079
|
if (!before.hasPackageJson) {
|
|
9913
|
-
console.error("
|
|
10080
|
+
console.error("No package.json here. Run setup from the folder that has your package.json.");
|
|
9914
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.");
|
|
9915
10082
|
return 1;
|
|
9916
10083
|
}
|
|
9917
10084
|
if (before.installed === null) {
|
|
9918
10085
|
console.error(
|
|
9919
|
-
`
|
|
10086
|
+
`The Patchstack connector is not installed in this project yet.
|
|
9920
10087
|
Run: ${installCommand(before.packageManager)}`
|
|
9921
10088
|
);
|
|
9922
10089
|
return 1;
|
|
9923
10090
|
}
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
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
|
+
});
|
|
9927
10099
|
if (scanCode !== 0) {
|
|
9928
10100
|
return scanCode;
|
|
9929
10101
|
}
|
|
9930
|
-
|
|
9931
|
-
console.log(" 2. Install and verify runtime protection");
|
|
9932
|
-
const protection = setupProtection(process.cwd());
|
|
9933
|
-
if (protection.install.status === "not-applicable") {
|
|
9934
|
-
console.log("Runtime protection: not applicable to this project \u2014 nothing installed.");
|
|
9935
|
-
console.log(` ${protection.install.reason}`);
|
|
9936
|
-
if (protection.install.leftovers.length > 0) {
|
|
9937
|
-
console.log(" An earlier run scaffolded a guard here before that was established. These files do");
|
|
9938
|
-
console.log(" nothing on this project and can be deleted:");
|
|
9939
|
-
for (const file of protection.install.leftovers) console.log(` ${file}`);
|
|
9940
|
-
}
|
|
9941
|
-
} else if (protection.verification.wired) {
|
|
9942
|
-
console.log(`Runtime protection: wired (${protection.verification.stack}).`);
|
|
9943
|
-
} else {
|
|
9944
|
-
console.log(`Runtime protection: manual wiring remains (${protection.verification.stack}):`);
|
|
9945
|
-
for (const check of protection.verification.checks) {
|
|
9946
|
-
console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
|
|
9947
|
-
}
|
|
9948
|
-
console.log("Run `npx @patchstack/connect protect --check` after completing the failed checks.");
|
|
9949
|
-
}
|
|
9950
|
-
console.log("");
|
|
9951
|
-
console.log(" 3. Wire dependency-install and production-build scans into package.json");
|
|
10102
|
+
reportProtection(setupProtection(process.cwd()), report);
|
|
9952
10103
|
const wired = wireBuildScripts(process.cwd(), before.packageManager);
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
10104
|
+
report.done.push(buildStepsLine(wired));
|
|
10105
|
+
detail(`Build hooks: ${wired.detail}`);
|
|
10106
|
+
const config = await resolveCliConfig(args);
|
|
9956
10107
|
const after = await collectGuideState(process.cwd());
|
|
9957
|
-
const
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
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);
|
|
9963
10116
|
}
|
|
9964
|
-
const outcome = setupOutcome(after, protection, wired);
|
|
9965
|
-
console.log("");
|
|
9966
|
-
console.log("Outcome \u2014 relay this to the user as it is:");
|
|
9967
|
-
for (const [label, value] of outcome) console.log(` ${label}: ${value}`);
|
|
9968
10117
|
return 0;
|
|
9969
10118
|
}
|
|
9970
|
-
function setupOutcome(state, protection, wired) {
|
|
9971
|
-
const remaining = countRemainingSteps(state);
|
|
9972
|
-
const lines = [];
|
|
9973
|
-
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)`]);
|
|
9974
|
-
lines.push(["Monitoring", "starts with the first build that runs on your hosting platform"]);
|
|
9975
|
-
if (protection.install.status === "not-applicable") {
|
|
9976
|
-
lines.push(["Runtime protection", "not applicable \u2014 this project has no request path (static build)"]);
|
|
9977
|
-
} else if (protection.verification.wired) {
|
|
9978
|
-
lines.push(["Runtime protection", `wired (${protection.verification.stack}) \u2014 local wiring verified; verify against the live site after deploy with \`protect --check --runtime\``]);
|
|
9979
|
-
} else {
|
|
9980
|
-
lines.push(["Runtime protection", `not wired yet (${protection.verification.stack}) \u2014 finish the checks above, then \`npx @patchstack/connect protect --check\``]);
|
|
9981
|
-
}
|
|
9982
|
-
lines.push(["Next steps", [
|
|
9983
|
-
"add PATCHSTACK_API_KEY (from .patchstackrc.local.json) to your hosting platform's environment variables",
|
|
9984
|
-
"commit .patchstackrc.json, package.json and the widget change (never .patchstackrc.local.json)",
|
|
9985
|
-
'deploy, then check the site in the Patchstack dashboard \u2014 it reads "Deployed" once the live site is seen'
|
|
9986
|
-
].join("; ")]);
|
|
9987
|
-
const warnings = [];
|
|
9988
|
-
if (!wired.changed && wired.strategy === "postinstall-only") warnings.push("no build script, so only dependency installs are scanned");
|
|
9989
|
-
if (protection.install.status === "not-applicable" && protection.install.leftovers.length > 0) {
|
|
9990
|
-
warnings.push(`earlier guard scaffold does nothing here and can be deleted: ${protection.install.leftovers.join(", ")}`);
|
|
9991
|
-
}
|
|
9992
|
-
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');
|
|
9993
|
-
if (warnings.length > 0) lines.push(["Warnings", warnings.join("; ")]);
|
|
9994
|
-
return lines;
|
|
9995
|
-
}
|
|
9996
10119
|
async function runStatus(args) {
|
|
9997
|
-
const config = await
|
|
9998
|
-
cwd: process.cwd(),
|
|
9999
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
10000
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
10001
|
-
});
|
|
10120
|
+
const config = await resolveCliConfig(args);
|
|
10002
10121
|
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
10003
10122
|
console.log(
|
|
10004
10123
|
`Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? "" : " (override)"}`
|
|
@@ -10007,24 +10126,14 @@ async function runStatus(args) {
|
|
|
10007
10126
|
console.log(`Environment: ${config.environment}`);
|
|
10008
10127
|
if (config.siteUuid !== null) {
|
|
10009
10128
|
console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
|
|
10010
|
-
console.log(
|
|
10011
|
-
console.log(' website" panel on the preview, open the link above, or run');
|
|
10012
|
-
console.log(" `npx @patchstack/connect claim`.");
|
|
10129
|
+
console.log(" Not connected yet? Open the link above or run `npx @patchstack/connect claim`.");
|
|
10013
10130
|
switch (await fetchSiteStatus(config)) {
|
|
10014
10131
|
case "active":
|
|
10015
10132
|
console.log("Site status: active on Patchstack");
|
|
10016
10133
|
break;
|
|
10017
10134
|
case "removed":
|
|
10018
10135
|
console.log("Site status: removed from Patchstack");
|
|
10019
|
-
console.log(
|
|
10020
|
-
" The site record no longer exists (deleted from the dashboard or via the"
|
|
10021
|
-
);
|
|
10022
|
-
console.log(
|
|
10023
|
-
" widget uninstall flow). The local integration files are still in this"
|
|
10024
|
-
);
|
|
10025
|
-
console.log(
|
|
10026
|
-
' project \u2014 see "Uninstalling" in AGENT-INSTALL.md to remove them.'
|
|
10027
|
-
);
|
|
10136
|
+
console.log(' The local files are still here. See "Uninstalling" in AGENT-INSTALL.md to remove them.');
|
|
10028
10137
|
break;
|
|
10029
10138
|
case "unknown":
|
|
10030
10139
|
console.log("Site status: could not be verified (Patchstack unreachable)");
|
|
@@ -10034,11 +10143,7 @@ async function runStatus(args) {
|
|
|
10034
10143
|
return 0;
|
|
10035
10144
|
}
|
|
10036
10145
|
async function runUninstall(args) {
|
|
10037
|
-
const config = await
|
|
10038
|
-
cwd: process.cwd(),
|
|
10039
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
10040
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
10041
|
-
});
|
|
10146
|
+
const config = await resolveCliConfig(args);
|
|
10042
10147
|
if (config.siteUuid === null) {
|
|
10043
10148
|
console.log("No site UUID configured \u2014 there is no site record to signal about.");
|
|
10044
10149
|
console.log('Continue with the local removal steps in AGENT-INSTALL.md ("Uninstalling").');
|
|
@@ -10063,8 +10168,7 @@ async function runUninstall(args) {
|
|
|
10063
10168
|
break;
|
|
10064
10169
|
}
|
|
10065
10170
|
console.log("");
|
|
10066
|
-
console.log(
|
|
10067
|
-
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.');
|
|
10068
10172
|
return 0;
|
|
10069
10173
|
}
|
|
10070
10174
|
function describeStack(stack) {
|
|
@@ -10078,7 +10182,7 @@ function describeStack(stack) {
|
|
|
10078
10182
|
}
|
|
10079
10183
|
function readBuildScript(cwd) {
|
|
10080
10184
|
try {
|
|
10081
|
-
const pkg = JSON.parse(readFileSync19(
|
|
10185
|
+
const pkg = JSON.parse(readFileSync19(path16.join(cwd, "package.json"), "utf8"));
|
|
10082
10186
|
return pkg.scripts?.build;
|
|
10083
10187
|
} catch {
|
|
10084
10188
|
return void 0;
|
|
@@ -10097,7 +10201,7 @@ async function reportBuildStamp(config, payload, marker) {
|
|
|
10097
10201
|
await postManifestWithEnvironmentFallback(bounded, payload, marker);
|
|
10098
10202
|
} catch (err) {
|
|
10099
10203
|
console.warn(
|
|
10100
|
-
`mark-build:
|
|
10204
|
+
`mark-build: could not tell Patchstack about this build (${err.message}). The pages were still marked.`
|
|
10101
10205
|
);
|
|
10102
10206
|
}
|
|
10103
10207
|
}
|
|
@@ -10159,22 +10263,14 @@ async function runMarkBuild(args) {
|
|
|
10159
10263
|
const files = findHtmlFiles(dir);
|
|
10160
10264
|
if (files.length === 0) {
|
|
10161
10265
|
console.warn(`mark-build: found ${dir} but no HTML files in it.`);
|
|
10162
|
-
console.warn(
|
|
10163
|
-
"mark-build: this build looks server-rendered, so the production flag has nothing to stamp."
|
|
10164
|
-
);
|
|
10165
|
-
console.warn(
|
|
10166
|
-
"mark-build: add the marker to your root shell instead \u2014 run `patchstack-connect guide` for the snippet."
|
|
10167
|
-
);
|
|
10266
|
+
console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
|
|
10168
10267
|
await reportBuildStamp(reported, wirePayload, "no-pages");
|
|
10169
10268
|
return 0;
|
|
10170
10269
|
}
|
|
10171
10270
|
const staleBy = staleBuildAge(files, startedAt);
|
|
10172
10271
|
if (staleBy !== null) {
|
|
10173
10272
|
console.warn(
|
|
10174
|
-
`mark-build: nothing in ${dir}
|
|
10175
|
-
);
|
|
10176
|
-
console.warn(
|
|
10177
|
-
"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).`
|
|
10178
10274
|
);
|
|
10179
10275
|
}
|
|
10180
10276
|
const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
|
|
@@ -10197,7 +10293,8 @@ async function runMarkBuild(args) {
|
|
|
10197
10293
|
}
|
|
10198
10294
|
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
10199
10295
|
if (published) {
|
|
10200
|
-
console.log(
|
|
10296
|
+
console.log(`Patchstack: marked ${plural(files.length, "page", "pages")} as your live app.`);
|
|
10297
|
+
detail(
|
|
10201
10298
|
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
10202
10299
|
);
|
|
10203
10300
|
await reportBuildStamp(reported, wirePayload, "stamped");
|
|
@@ -10205,13 +10302,10 @@ async function runMarkBuild(args) {
|
|
|
10205
10302
|
}
|
|
10206
10303
|
const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
|
|
10207
10304
|
console.log(
|
|
10208
|
-
`
|
|
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.`
|
|
10209
10306
|
);
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
);
|
|
10213
|
-
console.log(
|
|
10214
|
-
"mark-build: publishing this directory by hand? Re-run with --production (or set PATCHSTACK_ENVIRONMENT=production) so the deployed pages carry it."
|
|
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}]` : ""}.`
|
|
10215
10309
|
);
|
|
10216
10310
|
await reportBuildStamp(reported, wirePayload, "withheld");
|
|
10217
10311
|
return 0;
|
|
@@ -10225,8 +10319,28 @@ function packageVersion() {
|
|
|
10225
10319
|
return "unknown";
|
|
10226
10320
|
}
|
|
10227
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
|
+
}
|
|
10228
10341
|
async function main() {
|
|
10229
10342
|
const args = parseArgs(process.argv);
|
|
10343
|
+
verbose = args.flags.get("verbose") === true;
|
|
10230
10344
|
if (args.flags.has("version") || args.command === "version") {
|
|
10231
10345
|
console.log(packageVersion());
|
|
10232
10346
|
return 0;
|
|
@@ -10271,7 +10385,9 @@ async function main() {
|
|
|
10271
10385
|
}
|
|
10272
10386
|
main().then((code) => process.exit(code)).catch((err) => {
|
|
10273
10387
|
if (err instanceof PatchstackError) {
|
|
10274
|
-
|
|
10388
|
+
const plain = plainErrorMessage(err);
|
|
10389
|
+
console.error(`${plain ?? err.message} (${err.code})`);
|
|
10390
|
+
if (plain !== null && verbose) console.error(`Detail: ${err.message}`);
|
|
10275
10391
|
process.exit(1);
|
|
10276
10392
|
}
|
|
10277
10393
|
console.error("Unexpected error:", err);
|