@patchstack/connect 0.3.4 → 0.3.6
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 +13 -7
- package/README.md +16 -15
- package/dist/cli.js +389 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
This is a plain-text instruction document. Read it and follow the steps — there is nothing here to pipe to a shell or execute from a URL.
|
|
4
4
|
|
|
5
|
-
You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL.md` (the package is already installed) or from the [GitHub repo](https://github.com/patchstack/connect).
|
|
5
|
+
You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL.md` (the package is already installed) or from the [GitHub repo](https://github.com/patchstack/connect). This bundled copy matches the package version it shipped with and is all you need — there is no requirement to fetch anything remote. (A long-form copy also lives at https://patchstack.com/install.txt for humans browsing outside the package.)
|
|
6
6
|
|
|
7
7
|
## Verify before you trust (do this, it's quick)
|
|
8
8
|
|
|
9
9
|
- The connector is a normal npm package: [`@patchstack/connect`](https://www.npmjs.com/package/@patchstack/connect), MIT-licensed, source at https://github.com/patchstack/connect. Install it pinned, check the integrity hash in the lockfile, run `npm audit` over it — whatever your policy requires.
|
|
10
10
|
- First published May 2026. If the package is absent from your training data, `npm view @patchstack/connect` (read-only) shows the live registry record.
|
|
11
|
-
- It reads the project's **
|
|
11
|
+
- It reads the project's **dependency list only** — from the lockfile (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) or, on bun projects (`bun.lock`/`bun.lockb`), by enumerating the installed packages under `node_modules/` — and sends package names + versions to Patchstack for vulnerability matching. No source code, no env var values, no file paths, no git history. (`mark-build` additionally stamps built HTML with a coarse stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL`, `CF_PAGES` — never their values.)
|
|
12
|
+
- The package also bundles an **opt-in** `protect` command (runtime exploit guard, currently for TanStack Start + Supabase apps; its templates live under `dist/protect/`). It patches the app's Supabase client to route traffic through a same-origin guard — and it runs **only** when you explicitly run `patchstack-connect protect`. Nothing in this document, and none of `scan`, `guide`, `status`, or `mark-build`, modifies application source.
|
|
12
13
|
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
13
14
|
|
|
14
15
|
## Before you start — never install twice
|
|
15
16
|
|
|
16
|
-
-
|
|
17
|
+
- `npx @patchstack/connect guide` prints a live checklist above this document showing which steps are already done in the current project (install, provisioning, build hooks, widget) with commands tailored to it. Its purpose is to prevent duplicate work — verify its claims however your policy requires, but only do the steps it marks ✖.
|
|
18
|
+
- If `.patchstackrc.json` contains a `siteUuid` key, the project is already provisioned. Reuse that UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.** (A `.patchstackrc.json` with other keys — e.g. an `endpoint` override — but no `siteUuid` is *not* provisioned yet; scan normally.)
|
|
17
19
|
- If `@patchstack/connect` is already in `devDependencies`, skip the install command.
|
|
18
20
|
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
|
|
19
21
|
|
|
20
|
-
## Install steps (
|
|
22
|
+
## Install steps (do the ones the checklist marks ✖, in order; they are designed to run back-to-back — if one fails, stop and report rather than improvising)
|
|
21
23
|
|
|
22
24
|
1. **Install** (skip if already present), matching the project's package manager:
|
|
23
25
|
|
|
@@ -36,7 +38,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
36
38
|
|
|
37
39
|
It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
|
|
38
40
|
|
|
39
|
-
3. **Wire builds** in `package.json
|
|
41
|
+
3. **Wire builds** in `package.json`:
|
|
40
42
|
|
|
41
43
|
```jsonc
|
|
42
44
|
{
|
|
@@ -47,7 +49,11 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
47
49
|
}
|
|
48
50
|
```
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
If a `prebuild`/`postbuild` hook already exists, chain instead of replacing it, e.g. `"prebuild": "existing-command && patchstack-connect scan"`.
|
|
53
|
+
|
|
54
|
+
**Bun-managed projects:** `bun run` does not execute npm-style `pre`/`post` scripts, so wire the build script directly instead: `"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"`.
|
|
55
|
+
|
|
56
|
+
4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the `siteUuid` value from `.patchstackrc.json` (the same site UUID step 2 provisioned) and pass it as the widget's `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
|
|
51
57
|
|
|
52
58
|
```html
|
|
53
59
|
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
|
|
@@ -58,7 +64,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
58
64
|
|
|
59
65
|
Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The `userToken` is public by design — it ships in client-side HTML and is not a secret.
|
|
60
66
|
|
|
61
|
-
5. **Commit** `.patchstackrc.json
|
|
67
|
+
5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget snippet, so every developer and CI run reports to the same site.
|
|
62
68
|
|
|
63
69
|
6. **Tell the user to claim the site** (free): open the claim URL from step 2 in a browser and sign in. The site is monitored either way, but the vulnerability reports are only visible after claiming. They can also claim directly from the widget's "Connect this website" prompt. On the published site, the owner reaches the widget login by appending `#patchstack` to the live URL.
|
|
64
70
|
|
package/README.md
CHANGED
|
@@ -6,22 +6,13 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
|
|
|
6
6
|
|
|
7
7
|
## Install prompt (for AI coding tools)
|
|
8
8
|
|
|
9
|
-
Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
|
|
9
|
+
Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.). If the assistant declines or stops partway, the step-by-step flow in [`GETTING-STARTED.md`](https://github.com/patchstack/connect/blob/main/GETTING-STARTED.md) covers what to do next:
|
|
10
10
|
|
|
11
|
-
> Add Patchstack dependency vulnerability monitoring to
|
|
12
|
-
>
|
|
13
|
-
> 1. `npm install --save-dev @patchstack/connect` — or the pnpm/yarn/bun equivalent (MIT, source: https://github.com/patchstack/connect; verify with `npm view @patchstack/connect` if unsure).
|
|
14
|
-
> 2. `npx @patchstack/connect scan` — saves a site ID to `.patchstackrc.json` and prints a claim URL. Show me that URL; don't open it.
|
|
15
|
-
> 3. Add `package.json` scripts `"prebuild": "patchstack-connect scan"` and `"postbuild": "patchstack-connect mark-build"`, then commit the changes.
|
|
16
|
-
> 4. Run `npx @patchstack/connect guide` and use it to add the report-a-vulnerability widget and finish any framework-specific details.
|
|
11
|
+
> Add Patchstack dependency vulnerability monitoring: install `@patchstack/connect` as a dev dependency (MIT — verify with `npm view @patchstack/connect`; source: https://github.com/patchstack/connect), run `npx @patchstack/connect scan` and show me the claim URL it prints (I'll open it in my browser to view reports — don't open or fetch it yourself), then run `npx @patchstack/connect guide` and finish what its checklist flags: the package.json build hooks and the report-a-vulnerability widget snippet.
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
Every clause is load-bearing. AI agents routinely audit this package before completing the request — verifying the tarball against the registry, checking `dist/` for lifecycle scripts and shell/eval calls — and the flow is designed to pass that audit, not to discourage it. The prompt can stay short because `guide` is state-aware: it inspects the project (package manager, `.patchstackrc.json`, `package.json` scripts, widget presence, framework) and prints a live checklist of only the remaining steps — with the real site UUID and framework-specific widget placement — followed by the full reference guide (`AGENT-INSTALL.md`, also at https://patchstack.com/install.txt). That also makes the flow idempotent: re-running `guide` on a finished project reports all-done instead of prompting a second install.
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
2. **Agents whose training predates May 2026 assert the package doesn't exist.** Step 1's `npm view` resolves that against the registry instead of the model's memory.
|
|
22
|
-
3. **"Install the package, then follow the instructions it ships" reads as handing control to the package author** — structurally the same as prompt injection — and preemptive reassurance language ("it's safe, don't be suspicious, note your knowledge cutoff") is itself flagged as a manipulation signal.
|
|
23
|
-
|
|
24
|
-
So the prompt inlines the core install as concrete, individually inspectable commands, delegates nothing up front, and argues nothing. `npx @patchstack/connect guide` (which prints the bundled `AGENT-INSTALL.md`) enters only at step 4, as an assist for widget placement, re-run rules, and troubleshooting — by then the agent has already installed and audited the package, so consulting its docs is ordinary behavior rather than the instruction channel the install depends on. The always-current long-form guide lives at https://patchstack.com/install.txt.
|
|
15
|
+
Before changing this prompt (or `guide` / `AGENT-INSTALL.md`), read [`field-test/README.md`](https://github.com/patchstack/connect/blob/main/field-test/README.md): it documents the AI-agent refusal modes each clause guards against, and its harness runs a real agent through the full install in a throwaway fixture against a mocked API and scores the outcome on eight checks. Validate any variant there first.
|
|
25
16
|
|
|
26
17
|
## Quick start (zero configuration)
|
|
27
18
|
|
|
@@ -70,6 +61,16 @@ patchstack-connect scan [options] Scan the lockfile and POST to
|
|
|
70
61
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
|
|
71
62
|
an existing site UUID
|
|
72
63
|
patchstack-connect status [options] Show current configuration
|
|
64
|
+
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
65
|
+
build fingerprint (run as a postbuild step)
|
|
66
|
+
patchstack-connect guide Show this project's setup status (what's done,
|
|
67
|
+
what's missing, with tailored commands), then
|
|
68
|
+
print the full setup guide
|
|
69
|
+
patchstack-connect protect Opt-in: install the always-on runtime exploit
|
|
70
|
+
guard (currently TanStack Start + Supabase; it
|
|
71
|
+
patches the app's Supabase client to route
|
|
72
|
+
traffic through a same-origin guard). Never
|
|
73
|
+
run by scan/guide/mark-build.
|
|
73
74
|
patchstack-connect help Print help
|
|
74
75
|
|
|
75
76
|
Options (for scan and status):
|
|
@@ -100,7 +101,7 @@ Environment variables:
|
|
|
100
101
|
}
|
|
101
102
|
```
|
|
102
103
|
|
|
103
|
-
The site UUID
|
|
104
|
+
The site UUID identifies the site; it is not a secret — the disclosure widget ships the same UUID in client-side HTML as its `userToken`, and committing `.patchstackrc.json` is the intended workflow so every developer and CI run reports to the same site. Possession of the UUID lets someone submit dependency manifests for that site (noise, not data access). In CI setups where the file isn't committed, set `PATCHSTACK_SITE_UUID` instead.
|
|
104
105
|
|
|
105
106
|
## Programmatic API
|
|
106
107
|
|
|
@@ -126,7 +127,7 @@ Lower-level pieces are also exported: `scanLockfile`, `buildWirePayload`, `postM
|
|
|
126
127
|
}
|
|
127
128
|
```
|
|
128
129
|
|
|
129
|
-
That's the entire payload. No source code, no environment
|
|
130
|
+
That's the entire payload. No source code, no environment variable values, no file paths — just the package names and versions from your lockfile. Duplicate names with different versions are preserved so transitive vulnerabilities aren't missed. (`mark-build` separately stamps built HTML with a stack descriptor that may include hosting-related env variable *names* — e.g. `VERCEL` — never their values.)
|
|
130
131
|
|
|
131
132
|
## Supported lockfiles
|
|
132
133
|
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
@@ -1250,14 +1250,351 @@ function injectMarker(html, snippet) {
|
|
|
1250
1250
|
return stripped + snippet;
|
|
1251
1251
|
}
|
|
1252
1252
|
|
|
1253
|
+
// src/guide.ts
|
|
1254
|
+
import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1255
|
+
import path8 from "path";
|
|
1256
|
+
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
1257
|
+
var WIDGET_NEEDLE = "patchstack-widget";
|
|
1258
|
+
var INSTALL_COMMANDS = {
|
|
1259
|
+
npm: "npm install --save-dev @patchstack/connect",
|
|
1260
|
+
pnpm: "pnpm add -D @patchstack/connect",
|
|
1261
|
+
yarn: "yarn add -D @patchstack/connect",
|
|
1262
|
+
bun: "bun add -d @patchstack/connect"
|
|
1263
|
+
};
|
|
1264
|
+
var PM_BY_LOCKFILE = [
|
|
1265
|
+
{ filename: "package-lock.json", pm: "npm" },
|
|
1266
|
+
{ filename: "bun.lock", pm: "bun" },
|
|
1267
|
+
{ filename: "bun.lockb", pm: "bun" },
|
|
1268
|
+
{ filename: "pnpm-lock.yaml", pm: "pnpm" },
|
|
1269
|
+
{ filename: "yarn.lock", pm: "yarn" }
|
|
1270
|
+
];
|
|
1271
|
+
var WIDGET_FILE_CANDIDATES = {
|
|
1272
|
+
next: [
|
|
1273
|
+
"app/layout.tsx",
|
|
1274
|
+
"app/layout.jsx",
|
|
1275
|
+
"src/app/layout.tsx",
|
|
1276
|
+
"src/app/layout.jsx",
|
|
1277
|
+
"pages/_document.tsx",
|
|
1278
|
+
"pages/_document.jsx",
|
|
1279
|
+
"src/pages/_document.tsx"
|
|
1280
|
+
],
|
|
1281
|
+
nuxt: ["app.vue", "src/app.vue", "app/app.vue"],
|
|
1282
|
+
remix: ["app/root.tsx", "app/root.jsx"],
|
|
1283
|
+
"react-router": ["app/root.tsx", "src/root.tsx"],
|
|
1284
|
+
"tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
|
|
1285
|
+
sveltekit: ["src/app.html"],
|
|
1286
|
+
astro: ["src/layouts/Layout.astro"],
|
|
1287
|
+
gatsby: ["src/html.js"]
|
|
1288
|
+
};
|
|
1289
|
+
var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
|
|
1290
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([
|
|
1291
|
+
"node_modules",
|
|
1292
|
+
".git",
|
|
1293
|
+
"dist",
|
|
1294
|
+
"build",
|
|
1295
|
+
"out",
|
|
1296
|
+
".output",
|
|
1297
|
+
".next",
|
|
1298
|
+
".nuxt",
|
|
1299
|
+
".svelte-kit",
|
|
1300
|
+
".vercel",
|
|
1301
|
+
".netlify",
|
|
1302
|
+
"coverage",
|
|
1303
|
+
"vendor"
|
|
1304
|
+
]);
|
|
1305
|
+
var WIDGET_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
1306
|
+
".html",
|
|
1307
|
+
".htm",
|
|
1308
|
+
".tsx",
|
|
1309
|
+
".jsx",
|
|
1310
|
+
".ts",
|
|
1311
|
+
".js",
|
|
1312
|
+
".mjs",
|
|
1313
|
+
".cjs",
|
|
1314
|
+
".vue",
|
|
1315
|
+
".svelte",
|
|
1316
|
+
".astro",
|
|
1317
|
+
".ejs",
|
|
1318
|
+
".hbs"
|
|
1319
|
+
]);
|
|
1320
|
+
var WIDGET_SCAN_MAX_FILES = 4e3;
|
|
1321
|
+
var WIDGET_SCAN_MAX_DEPTH = 6;
|
|
1322
|
+
var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
|
|
1323
|
+
function detectPackageManager(cwd) {
|
|
1324
|
+
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
1325
|
+
if (existsSync2(path8.join(cwd, filename))) {
|
|
1326
|
+
return pm;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return "npm";
|
|
1330
|
+
}
|
|
1331
|
+
function installCommand(pm) {
|
|
1332
|
+
return INSTALL_COMMANDS[pm];
|
|
1333
|
+
}
|
|
1334
|
+
function readPackageJson(cwd) {
|
|
1335
|
+
try {
|
|
1336
|
+
return JSON.parse(readFileSync(path8.join(cwd, "package.json"), "utf8"));
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
function installedVersion(cwd, declaredRange) {
|
|
1342
|
+
try {
|
|
1343
|
+
const pkg = JSON.parse(
|
|
1344
|
+
readFileSync(path8.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
|
|
1345
|
+
);
|
|
1346
|
+
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
1347
|
+
return pkg.version;
|
|
1348
|
+
}
|
|
1349
|
+
} catch {
|
|
1350
|
+
}
|
|
1351
|
+
return declaredRange;
|
|
1352
|
+
}
|
|
1353
|
+
function findWidgetMarker(cwd, siteUuid) {
|
|
1354
|
+
let budget = WIDGET_SCAN_MAX_FILES;
|
|
1355
|
+
let sawWidget = false;
|
|
1356
|
+
let sawTokenMatch = false;
|
|
1357
|
+
const settled = () => sawTokenMatch || sawWidget && siteUuid == null;
|
|
1358
|
+
const walk2 = (dir, depth) => {
|
|
1359
|
+
if (depth > WIDGET_SCAN_MAX_DEPTH || budget <= 0) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
let entries;
|
|
1363
|
+
try {
|
|
1364
|
+
entries = readdirSync2(dir, { withFileTypes: true });
|
|
1365
|
+
} catch {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
for (const entry of entries) {
|
|
1369
|
+
if (budget <= 0 || settled()) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
const full = path8.join(dir, entry.name);
|
|
1373
|
+
if (entry.isDirectory()) {
|
|
1374
|
+
if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
walk2(full, depth + 1);
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
budget -= 1;
|
|
1384
|
+
try {
|
|
1385
|
+
if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
const content = readFileSync(full, "utf8");
|
|
1389
|
+
if (!content.includes(WIDGET_NEEDLE)) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
sawWidget = true;
|
|
1393
|
+
if (siteUuid != null && content.includes(siteUuid)) {
|
|
1394
|
+
sawTokenMatch = true;
|
|
1395
|
+
}
|
|
1396
|
+
} catch {
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
walk2(cwd, 0);
|
|
1401
|
+
return {
|
|
1402
|
+
found: sawWidget,
|
|
1403
|
+
uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
function resolveWidgetFileHint(cwd, framework) {
|
|
1407
|
+
const candidates = [
|
|
1408
|
+
...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
|
|
1409
|
+
...GENERIC_WIDGET_FILES
|
|
1410
|
+
];
|
|
1411
|
+
for (const candidate of candidates) {
|
|
1412
|
+
if (existsSync2(path8.join(cwd, candidate))) {
|
|
1413
|
+
return candidate;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
async function collectGuideState(cwd) {
|
|
1419
|
+
const pkg = readPackageJson(cwd);
|
|
1420
|
+
const packageManager = detectPackageManager(cwd);
|
|
1421
|
+
let installed = null;
|
|
1422
|
+
if (pkg?.devDependencies?.["@patchstack/connect"] !== void 0) {
|
|
1423
|
+
installed = {
|
|
1424
|
+
version: installedVersion(cwd, pkg.devDependencies["@patchstack/connect"]),
|
|
1425
|
+
section: "devDependencies"
|
|
1426
|
+
};
|
|
1427
|
+
} else if (pkg?.dependencies?.["@patchstack/connect"] !== void 0) {
|
|
1428
|
+
installed = {
|
|
1429
|
+
version: installedVersion(cwd, pkg.dependencies["@patchstack/connect"]),
|
|
1430
|
+
section: "dependencies"
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
let siteUuid = null;
|
|
1434
|
+
let claimUrl = null;
|
|
1435
|
+
let endpointOverride = null;
|
|
1436
|
+
try {
|
|
1437
|
+
const config = await resolveConfig({ cwd });
|
|
1438
|
+
siteUuid = config.siteUuid;
|
|
1439
|
+
if (siteUuid !== null) {
|
|
1440
|
+
claimUrl = buildClaimUrl(config.endpoint, siteUuid);
|
|
1441
|
+
}
|
|
1442
|
+
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
1443
|
+
endpointOverride = config.endpoint;
|
|
1444
|
+
}
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
|
|
1448
|
+
const stack = detectStack(
|
|
1449
|
+
declaredNames.map((name) => ({ name, version: "" })),
|
|
1450
|
+
{}
|
|
1451
|
+
);
|
|
1452
|
+
const widget = findWidgetMarker(cwd, siteUuid);
|
|
1453
|
+
return {
|
|
1454
|
+
projectName: pkg?.name ?? null,
|
|
1455
|
+
hasPackageJson: pkg !== null,
|
|
1456
|
+
packageManager,
|
|
1457
|
+
installed,
|
|
1458
|
+
siteUuid,
|
|
1459
|
+
claimUrl,
|
|
1460
|
+
endpointOverride,
|
|
1461
|
+
// bun run doesn't execute npm-style pre/post scripts, so chaining inside
|
|
1462
|
+
// the build script itself also counts as wired (and is what we suggest on bun).
|
|
1463
|
+
prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
|
|
1464
|
+
postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
1465
|
+
widgetInstalled: widget.found,
|
|
1466
|
+
widgetTokenMatches: widget.uuidMatches,
|
|
1467
|
+
framework: stack.framework,
|
|
1468
|
+
widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
var ANSI = {
|
|
1472
|
+
reset: "\x1B[0m",
|
|
1473
|
+
bold: "\x1B[1m",
|
|
1474
|
+
dim: "\x1B[2m",
|
|
1475
|
+
green: "\x1B[32m",
|
|
1476
|
+
yellow: "\x1B[33m",
|
|
1477
|
+
cyan: "\x1B[36m"
|
|
1478
|
+
};
|
|
1479
|
+
function countRemainingSteps(state) {
|
|
1480
|
+
return [
|
|
1481
|
+
state.installed !== null,
|
|
1482
|
+
state.siteUuid !== null,
|
|
1483
|
+
state.prebuildWired && state.postbuildWired,
|
|
1484
|
+
state.widgetInstalled && state.widgetTokenMatches !== false
|
|
1485
|
+
].filter((step) => !step).length;
|
|
1486
|
+
}
|
|
1487
|
+
function renderGuideChecklist(state, useColor) {
|
|
1488
|
+
const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
|
|
1489
|
+
const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
|
|
1490
|
+
const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
|
|
1491
|
+
const detail = (text) => ` ${paint(ANSI.dim, text)}`;
|
|
1492
|
+
const lines = [];
|
|
1493
|
+
const headerParts = [state.framework, state.packageManager].filter(
|
|
1494
|
+
(part) => part !== null
|
|
1495
|
+
);
|
|
1496
|
+
const name = state.projectName ?? path8.basename(process.cwd());
|
|
1497
|
+
lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
|
|
1498
|
+
if (state.endpointOverride !== null) {
|
|
1499
|
+
lines.push(
|
|
1500
|
+
detail(
|
|
1501
|
+
`endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
|
|
1502
|
+
)
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
lines.push("");
|
|
1506
|
+
if (!state.hasPackageJson) {
|
|
1507
|
+
lines.push(todo("No package.json found in this directory."));
|
|
1508
|
+
lines.push(detail("Run the guide from the project root."));
|
|
1509
|
+
return lines.join("\n");
|
|
1510
|
+
}
|
|
1511
|
+
if (state.installed !== null) {
|
|
1512
|
+
lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
|
|
1513
|
+
} else {
|
|
1514
|
+
lines.push(todo("Install @patchstack/connect as a dev dependency"));
|
|
1515
|
+
lines.push(detail(`\u2192 ${installCommand(state.packageManager)}`));
|
|
1516
|
+
if (state.packageManager === "bun") {
|
|
1517
|
+
lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if (state.siteUuid !== null) {
|
|
1521
|
+
lines.push(done(`Site provisioned (${state.siteUuid})`));
|
|
1522
|
+
} else {
|
|
1523
|
+
lines.push(todo("Provision the site \u2014 run the first scan"));
|
|
1524
|
+
lines.push(detail("\u2192 npx @patchstack/connect scan"));
|
|
1525
|
+
lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
|
|
1526
|
+
lines.push(detail("and prints a claim URL \u2014 show that URL to the user; never open it yourself."));
|
|
1527
|
+
}
|
|
1528
|
+
if (state.prebuildWired && state.postbuildWired) {
|
|
1529
|
+
lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
|
|
1530
|
+
} else if (state.packageManager === "bun") {
|
|
1531
|
+
lines.push(todo("Wire builds \u2014 chain into the package.json build script (bun skips pre/post hooks)"));
|
|
1532
|
+
lines.push(detail('\u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
|
|
1533
|
+
} else {
|
|
1534
|
+
lines.push(todo("Wire builds \u2014 add to package.json scripts (chain with && if a hook exists)"));
|
|
1535
|
+
if (!state.prebuildWired) {
|
|
1536
|
+
lines.push(detail('\u2192 "prebuild": "patchstack-connect scan"'));
|
|
1537
|
+
}
|
|
1538
|
+
if (!state.postbuildWired) {
|
|
1539
|
+
lines.push(detail('\u2192 "postbuild": "patchstack-connect mark-build"'));
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
1543
|
+
if (widgetOk) {
|
|
1544
|
+
lines.push(done("Disclosure widget installed"));
|
|
1545
|
+
} else if (state.widgetInstalled) {
|
|
1546
|
+
lines.push(todo("Disclosure widget found, but its userToken doesn't match this project's site UUID"));
|
|
1547
|
+
lines.push(detail(`\u2192 a wrong userToken makes the widget silently no-op; set it to '${state.siteUuid}'`));
|
|
1548
|
+
} else {
|
|
1549
|
+
lines.push(todo('Add the "Report a vulnerability" widget'));
|
|
1550
|
+
const placement = state.widgetFileHint !== null ? `\u2192 add to ${state.widgetFileHint}, just before </body> (via the framework's HTML/layout mechanism):` : "\u2192 add just before </body> via the framework's HTML/layout mechanism (never a JS entry point):";
|
|
1551
|
+
lines.push(detail(placement));
|
|
1552
|
+
lines.push(detail(` <script src="${WIDGET_SCRIPT_URL}"></script>`));
|
|
1553
|
+
const token = state.siteUuid ?? "<SITE_UUID from .patchstackrc.json \u2014 run scan first>";
|
|
1554
|
+
lines.push(detail(` <script>PatchstackWidget.init({ userToken: '${token}' });</script>`));
|
|
1555
|
+
lines.push(detail("The userToken is public by design \u2014 it ships in client-side HTML."));
|
|
1556
|
+
}
|
|
1557
|
+
lines.push("");
|
|
1558
|
+
if (state.claimUrl !== null) {
|
|
1559
|
+
lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Claim the site (free, opens the dashboard):")}`);
|
|
1560
|
+
lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
|
|
1561
|
+
lines.push(detail("Open in a browser. AI agents: show this URL to the user verbatim."));
|
|
1562
|
+
if (state.endpointOverride !== null) {
|
|
1563
|
+
lines.push(detail("(this URL inherits the endpoint override above)"));
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
lines.push(detail("The claim URL appears after the first scan (re-print any time with `status`)."));
|
|
1567
|
+
}
|
|
1568
|
+
const remaining = countRemainingSteps(state);
|
|
1569
|
+
lines.push("");
|
|
1570
|
+
if (remaining === 0) {
|
|
1571
|
+
lines.push(
|
|
1572
|
+
done(
|
|
1573
|
+
paint(
|
|
1574
|
+
ANSI.bold,
|
|
1575
|
+
"All setup steps complete. Commit .patchstackrc.json, package.json, and the file carrying the widget snippet."
|
|
1576
|
+
)
|
|
1577
|
+
)
|
|
1578
|
+
);
|
|
1579
|
+
if (state.claimUrl !== null) {
|
|
1580
|
+
lines.push(detail("The only manual action left is claiming the site via the URL above (if not already claimed)."));
|
|
1581
|
+
}
|
|
1582
|
+
} else {
|
|
1583
|
+
lines.push(
|
|
1584
|
+
` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
return lines.join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1253
1590
|
// src/protect/install.ts
|
|
1254
|
-
import { readFileSync, writeFileSync, existsSync as
|
|
1591
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
|
|
1255
1592
|
import { join, dirname } from "path";
|
|
1256
1593
|
import { fileURLToPath } from "url";
|
|
1257
1594
|
var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
|
|
1258
1595
|
var APP = process.cwd();
|
|
1259
1596
|
var PS_DIR = join(APP, "src/integrations/patchstack");
|
|
1260
|
-
var read = (p) =>
|
|
1597
|
+
var read = (p) => readFileSync2(p, "utf8");
|
|
1261
1598
|
var log = (msg) => console.log(`patchstack protect: ${msg}`);
|
|
1262
1599
|
var CLIENT_TUNNEL = [
|
|
1263
1600
|
"",
|
|
@@ -1302,10 +1639,10 @@ var FUNCTION_MIDDLEWARE_DEF = [
|
|
|
1302
1639
|
].join("\n");
|
|
1303
1640
|
function detectSupportedStack(cwd) {
|
|
1304
1641
|
const pkgPath = join(cwd, "package.json");
|
|
1305
|
-
if (!
|
|
1642
|
+
if (!existsSync3(pkgPath)) return false;
|
|
1306
1643
|
const pkg = JSON.parse(read(pkgPath));
|
|
1307
1644
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1308
|
-
return Boolean(deps["@tanstack/react-start"]) &&
|
|
1645
|
+
return Boolean(deps["@tanstack/react-start"]) && existsSync3(join(cwd, "src/start.ts")) && existsSync3(join(cwd, "src/integrations/supabase/client.ts"));
|
|
1309
1646
|
}
|
|
1310
1647
|
function scaffold(cwd) {
|
|
1311
1648
|
const dst = join(cwd, "src/integrations/patchstack");
|
|
@@ -1388,8 +1725,11 @@ Usage:
|
|
|
1388
1725
|
patchstack-connect protect Install always-on runtime protection (the
|
|
1389
1726
|
guard) into a TanStack Start + Supabase app.
|
|
1390
1727
|
Covers the browser + server-function paths.
|
|
1391
|
-
patchstack-connect guide
|
|
1392
|
-
|
|
1728
|
+
patchstack-connect guide [--full] Show this project's setup status (what's done,
|
|
1729
|
+
what's missing, with tailored commands), then
|
|
1730
|
+
print the full setup guide. --full prints the
|
|
1731
|
+
guide even when setup is complete
|
|
1732
|
+
(also at https://patchstack.com/install.txt)
|
|
1393
1733
|
patchstack-connect help Print this message
|
|
1394
1734
|
|
|
1395
1735
|
Options (for scan and status):
|
|
@@ -1480,14 +1820,22 @@ async function runScan(args) {
|
|
|
1480
1820
|
}
|
|
1481
1821
|
const { payload, stats } = buildWirePayload(manifest);
|
|
1482
1822
|
console.log(
|
|
1483
|
-
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names
|
|
1823
|
+
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
1824
|
+
);
|
|
1825
|
+
console.log(
|
|
1826
|
+
`Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
|
|
1484
1827
|
);
|
|
1485
|
-
|
|
1828
|
+
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
1829
|
+
console.log(
|
|
1830
|
+
`Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1486
1833
|
if (stats.duplicateNames.length > 0) {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1834
|
+
const sample = stats.duplicateNames.slice(0, 10).join(", ");
|
|
1835
|
+
const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
|
|
1836
|
+
console.log(
|
|
1837
|
+
`${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
|
|
1838
|
+
);
|
|
1491
1839
|
}
|
|
1492
1840
|
if (dryRun) {
|
|
1493
1841
|
console.log("");
|
|
@@ -1524,6 +1872,9 @@ async function runScan(args) {
|
|
|
1524
1872
|
console.log("");
|
|
1525
1873
|
console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
|
|
1526
1874
|
console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
|
|
1875
|
+
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
1876
|
+
console.log(" (this URL inherits the endpoint override above)");
|
|
1877
|
+
}
|
|
1527
1878
|
}
|
|
1528
1879
|
return 0;
|
|
1529
1880
|
}
|
|
@@ -1535,9 +1886,27 @@ async function runProtectCommand(_args) {
|
|
|
1535
1886
|
}
|
|
1536
1887
|
return 0;
|
|
1537
1888
|
}
|
|
1538
|
-
async function runGuide() {
|
|
1889
|
+
async function runGuide(args) {
|
|
1890
|
+
let allDone = false;
|
|
1891
|
+
try {
|
|
1892
|
+
const state = await collectGuideState(process.cwd());
|
|
1893
|
+
const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
|
|
1894
|
+
console.log(renderGuideChecklist(state, useColor));
|
|
1895
|
+
allDone = countRemainingSteps(state) === 0;
|
|
1896
|
+
} catch {
|
|
1897
|
+
}
|
|
1898
|
+
if (allDone && args.flags.get("full") !== true) {
|
|
1899
|
+
console.log("");
|
|
1900
|
+
console.log(
|
|
1901
|
+
"Full reference guide: `npx @patchstack/connect guide --full` (or read node_modules/@patchstack/connect/AGENT-INSTALL.md)."
|
|
1902
|
+
);
|
|
1903
|
+
return 0;
|
|
1904
|
+
}
|
|
1905
|
+
console.log("");
|
|
1906
|
+
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
1907
|
+
console.log("");
|
|
1539
1908
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
1540
|
-
console.log(
|
|
1909
|
+
console.log(readFileSync3(guidePath, "utf8"));
|
|
1541
1910
|
return 0;
|
|
1542
1911
|
}
|
|
1543
1912
|
async function runStatus(args) {
|
|
@@ -1547,7 +1916,9 @@ async function runStatus(args) {
|
|
|
1547
1916
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
1548
1917
|
});
|
|
1549
1918
|
console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
|
|
1550
|
-
console.log(
|
|
1919
|
+
console.log(
|
|
1920
|
+
`Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? "" : " (override)"}`
|
|
1921
|
+
);
|
|
1551
1922
|
console.log(`Timeout: ${config.timeoutMs}ms`);
|
|
1552
1923
|
console.log(`Environment: ${config.environment}`);
|
|
1553
1924
|
if (config.siteUuid !== null) {
|
|
@@ -1596,7 +1967,7 @@ async function runMarkBuild(args) {
|
|
|
1596
1967
|
const snippet = buildInjectionSnippet(checksum, stack);
|
|
1597
1968
|
let marked = 0;
|
|
1598
1969
|
for (const file of files) {
|
|
1599
|
-
const before =
|
|
1970
|
+
const before = readFileSync3(file, "utf8");
|
|
1600
1971
|
const after = injectMarker(before, snippet);
|
|
1601
1972
|
if (after !== before) {
|
|
1602
1973
|
writeFileSync2(file, after);
|
|
@@ -1627,7 +1998,7 @@ async function main() {
|
|
|
1627
1998
|
case "protect":
|
|
1628
1999
|
return runProtectCommand(args);
|
|
1629
2000
|
case "guide":
|
|
1630
|
-
return runGuide();
|
|
2001
|
+
return runGuide(args);
|
|
1631
2002
|
default:
|
|
1632
2003
|
console.error(`Unknown command: ${args.command}
|
|
1633
2004
|
`);
|