@patchstack/connect 0.3.7 → 0.3.9
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 +9 -11
- package/README.md +29 -5
- package/dist/cli.js +169 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +74 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +46 -1
- package/dist/index.d.ts +46 -1
- package/dist/index.js +68 -1
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +311 -18
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +26 -3
- package/dist/protect.js +311 -18
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/AGENT-INSTALL.md
CHANGED
|
@@ -9,7 +9,8 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
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
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
|
-
-
|
|
12
|
+
- **`scan` makes one source edit, and only after a successful post:** it adds (or updates) the disclosure widget's `<script>` tag in the project's root HTML shell — the first of `index.html`, `public/index.html`, or `src/app.html` that exists. It touches no other file, never edits on `--dry-run` or after a failed post, leaves any pre-existing manual widget tag untouched, and is disabled entirely by `"widget": false` in `.patchstackrc.json`. `mark-build` writes to build output only (`dist/`, `build/`, `out/`, `.output/public`), never to source. `guide`, `status`, and `init` write nothing except `init`'s own `.patchstackrc.json`.
|
|
13
|
+
- 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`; `scan`, `guide`, `status`, and `mark-build` never invoke it.
|
|
13
14
|
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
14
15
|
|
|
15
16
|
## Before you start — never install twice
|
|
@@ -17,7 +18,7 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
17
18
|
- `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
19
|
- 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.)
|
|
19
20
|
- If `@patchstack/connect` is already in `devDependencies`, skip the install command.
|
|
20
|
-
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
|
|
21
|
+
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one — `scan` also respects an existing tag: it updates its own managed tag in place and leaves a manual one untouched.
|
|
21
22
|
|
|
22
23
|
## 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)
|
|
23
24
|
|
|
@@ -30,13 +31,13 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
30
31
|
bun add -d @patchstack/connect # bun.lock / bun-managed platforms
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
2. **First scan** — provisions a Patchstack site automatically
|
|
34
|
+
2. **First scan** — provisions a Patchstack site automatically, writes the UUID to `.patchstackrc.json`, and installs the disclosure widget's `<script>` tag into the root HTML shell (`index.html`, `public/index.html`, or `src/app.html`) when one exists. No signup, dashboard step, or UUID is needed up front:
|
|
34
35
|
|
|
35
36
|
```
|
|
36
37
|
npx @patchstack/connect scan
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
|
|
40
|
+
It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser. It also prints what it did about the widget — if it added the tag, reload the preview and confirm the "Report a vulnerability" button appears.
|
|
40
41
|
|
|
41
42
|
3. **Wire builds** in `package.json`:
|
|
42
43
|
|
|
@@ -53,18 +54,15 @@ You are reading this either from `node_modules/@patchstack/connect/AGENT-INSTALL
|
|
|
53
54
|
|
|
54
55
|
**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
|
|
|
56
|
-
4. **
|
|
57
|
+
4. **Verify the disclosure widget** — a floating "Report a vulnerability" button. `scan` (step 2) installs it automatically into a plain HTML shell, and `mark-build` carries it into built HTML. Only when `scan` reported that it found no editable shell (frameworks whose root layout is code, e.g. Next.js/Nuxt/Astro) add the one-liner it printed to the root layout yourself, just before `</body>` (never a JS entry point), reading `siteUuid` from `.patchstackrc.json`:
|
|
57
58
|
|
|
58
59
|
```html
|
|
59
|
-
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
|
|
60
|
-
<script>
|
|
61
|
-
PatchstackWidget.init({ userToken: '<SITE_UUID>' });
|
|
62
|
-
</script>
|
|
60
|
+
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer></script>
|
|
63
61
|
```
|
|
64
62
|
|
|
65
|
-
Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The
|
|
63
|
+
Framework-specific placement patterns: https://cdn.patchstack.com/llm.html. The site UUID is public by design — it ships in client-side HTML and is not a secret. If the project must not carry the widget, persist `"widget": false` in `.patchstackrc.json`; otherwise the next scan re-adds it.
|
|
66
64
|
|
|
67
|
-
5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget
|
|
65
|
+
5. **Commit** `.patchstackrc.json`, the updated `package.json`, and the layout/HTML file carrying the widget tag, so every developer and CI run reports to the same site.
|
|
68
66
|
|
|
69
67
|
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.
|
|
70
68
|
|
package/README.md
CHANGED
|
@@ -29,7 +29,8 @@ That's it. The first `scan`:
|
|
|
29
29
|
2. POSTs the package list to Patchstack with **no** UUID.
|
|
30
30
|
3. Patchstack provisions a fresh site and returns its UUID.
|
|
31
31
|
4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
|
|
32
|
-
5. The connector
|
|
32
|
+
5. The connector installs the disclosure widget's `<script>` tag into your root HTML shell (see *The disclosure widget* below) so the "Report a vulnerability" button shows up on the next preview reload.
|
|
33
|
+
6. The connector prints a claim URL — open it in a browser to attach the new site to your Patchstack account. You can re-display it any time with `npx @patchstack/connect status`.
|
|
33
34
|
|
|
34
35
|
Then wire it into builds:
|
|
35
36
|
|
|
@@ -57,12 +58,16 @@ npx @patchstack/connect scan
|
|
|
57
58
|
```
|
|
58
59
|
patchstack-connect scan [options] Scan the lockfile and POST to Patchstack.
|
|
59
60
|
If no UUID is configured the server provisions
|
|
60
|
-
one and the connector persists it.
|
|
61
|
+
one and the connector persists it. After a
|
|
62
|
+
successful post, adds/updates the disclosure
|
|
63
|
+
widget tag in the root HTML shell (opt out
|
|
64
|
+
with "widget": false in .patchstackrc.json)
|
|
61
65
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json with
|
|
62
66
|
an existing site UUID
|
|
63
67
|
patchstack-connect status [options] Show current configuration
|
|
64
68
|
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
65
|
-
build fingerprint
|
|
69
|
+
build fingerprint and ensure the widget tag
|
|
70
|
+
in built pages (run as a postbuild step)
|
|
66
71
|
patchstack-connect guide Show this project's setup status (what's done,
|
|
67
72
|
what's missing, with tailored commands), then
|
|
68
73
|
print the full setup guide
|
|
@@ -97,11 +102,30 @@ Environment variables:
|
|
|
97
102
|
|
|
98
103
|
```json
|
|
99
104
|
{
|
|
100
|
-
"siteUuid": "550e8400-e29b-41d4-a716-446655440000"
|
|
105
|
+
"siteUuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
106
|
+
"widget": true
|
|
101
107
|
}
|
|
102
108
|
```
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
`"widget"` is optional and defaults to `true`; set it to `false` to stop the connector from managing the disclosure-widget tag (see *The disclosure widget*).
|
|
111
|
+
|
|
112
|
+
The site UUID identifies the site; it is not a secret — the disclosure widget ships the same UUID in client-side HTML, 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.
|
|
113
|
+
|
|
114
|
+
## The disclosure widget
|
|
115
|
+
|
|
116
|
+
The widget is a floating "Report a vulnerability" button — a disclosure channel for anyone who spots a bug on the site. The connector manages its install so the UUID never has to be copied by hand:
|
|
117
|
+
|
|
118
|
+
- **`scan`** (after a successful post) adds this managed tag to the first root HTML shell it finds — `index.html`, `public/index.html`, or `src/app.html` — immediately before `</body>`:
|
|
119
|
+
|
|
120
|
+
```html
|
|
121
|
+
<script src="https://cdn.patchstack.com/patchstack-widget.js" data-site-uuid="<SITE_UUID>" defer data-patchstack-connect-widget="true"></script>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Re-runs update the tag in place (the `data-patchstack-connect-widget` attribute marks it as connector-managed); a pre-existing manual widget tag is left untouched. `--dry-run` and failed posts never edit anything. Projects whose root layout is code rather than HTML (Next.js, Nuxt, Astro, …) get the exact snippet and target file printed instead — `guide` shows framework-specific placement.
|
|
125
|
+
|
|
126
|
+
- **`mark-build`** ensures the same tag in built HTML output, covering builds whose source shell the connector couldn't edit, and stamps `window.__PATCHSTACK_PROD__` so the widget hides the claim/login UI on the published site (owners reach it by appending `#patchstack` to the live URL).
|
|
127
|
+
|
|
128
|
+
- **Opting out:** persist `"widget": false` in `.patchstackrc.json` to disable both passes (dependency scanning only). Without it, the next successful scan re-adds the managed tag.
|
|
105
129
|
|
|
106
130
|
## Programmatic API
|
|
107
131
|
|
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 readFileSync4, writeFileSync as writeFileSync3 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
@@ -1039,7 +1039,8 @@ async function resolveConfig(options) {
|
|
|
1039
1039
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1040
1040
|
endpoint,
|
|
1041
1041
|
timeoutMs,
|
|
1042
|
-
environment
|
|
1042
|
+
environment,
|
|
1043
|
+
widget: fromFile.widget !== false
|
|
1043
1044
|
};
|
|
1044
1045
|
}
|
|
1045
1046
|
async function writeConfigFile(cwd, config) {
|
|
@@ -1251,10 +1252,71 @@ function injectMarker(html, snippet) {
|
|
|
1251
1252
|
}
|
|
1252
1253
|
|
|
1253
1254
|
// src/guide.ts
|
|
1254
|
-
import { existsSync as
|
|
1255
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1256
|
+
import path9 from "path";
|
|
1257
|
+
|
|
1258
|
+
// src/widget.ts
|
|
1259
|
+
import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
|
|
1255
1260
|
import path8 from "path";
|
|
1256
1261
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
1262
|
+
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
1257
1263
|
var WIDGET_NEEDLE = "patchstack-widget";
|
|
1264
|
+
var SOURCE_SHELL_CANDIDATES = ["index.html", "public/index.html", "src/app.html"];
|
|
1265
|
+
function buildWidgetTag(siteUuid) {
|
|
1266
|
+
return `<script src="${WIDGET_SCRIPT_URL}" data-site-uuid="${siteUuid}" defer ${WIDGET_MARKER_ATTR}="true"></script>`;
|
|
1267
|
+
}
|
|
1268
|
+
var MANAGED_TAG_RE = new RegExp(
|
|
1269
|
+
`<script[^>]*${WIDGET_MARKER_ATTR}[^>]*>\\s*</script>`,
|
|
1270
|
+
"i"
|
|
1271
|
+
);
|
|
1272
|
+
function ensureWidgetInHtml(html, siteUuid) {
|
|
1273
|
+
const tag = buildWidgetTag(siteUuid);
|
|
1274
|
+
const managed = html.match(MANAGED_TAG_RE);
|
|
1275
|
+
if (managed !== null) {
|
|
1276
|
+
if (managed[0].includes(`data-site-uuid="${siteUuid}"`)) {
|
|
1277
|
+
return { html, action: "unchanged" };
|
|
1278
|
+
}
|
|
1279
|
+
return { html: html.replace(MANAGED_TAG_RE, tag), action: "updated" };
|
|
1280
|
+
}
|
|
1281
|
+
if (html.includes(WIDGET_NEEDLE)) {
|
|
1282
|
+
return { html, action: "manual" };
|
|
1283
|
+
}
|
|
1284
|
+
const bodyClose = html.match(/([ \t]*)<\/body>/i);
|
|
1285
|
+
if (bodyClose === null || bodyClose.index === void 0) {
|
|
1286
|
+
return { html, action: "no-body" };
|
|
1287
|
+
}
|
|
1288
|
+
const indent = bodyClose[1] ?? "";
|
|
1289
|
+
const insertion = `${indent} ${tag}
|
|
1290
|
+
${indent}</body>`;
|
|
1291
|
+
return {
|
|
1292
|
+
html: html.slice(0, bodyClose.index) + insertion + html.slice(bodyClose.index + bodyClose[0].length),
|
|
1293
|
+
action: "added"
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function findSourceShell(cwd) {
|
|
1297
|
+
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
1298
|
+
if (existsSync2(path8.join(cwd, candidate))) {
|
|
1299
|
+
return candidate;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
return null;
|
|
1303
|
+
}
|
|
1304
|
+
function ensureSourceWidget(cwd, siteUuid) {
|
|
1305
|
+
const shell = findSourceShell(cwd);
|
|
1306
|
+
if (shell === null) {
|
|
1307
|
+
return { shell: null, action: "no-shell" };
|
|
1308
|
+
}
|
|
1309
|
+
const file = path8.join(cwd, shell);
|
|
1310
|
+
const before = readFileSync(file, "utf8");
|
|
1311
|
+
const { html, action } = ensureWidgetInHtml(before, siteUuid);
|
|
1312
|
+
if (html !== before) {
|
|
1313
|
+
writeFileSync(file, html);
|
|
1314
|
+
}
|
|
1315
|
+
return { shell, action };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// src/guide.ts
|
|
1319
|
+
var WIDGET_NEEDLE2 = "patchstack-widget";
|
|
1258
1320
|
var INSTALL_COMMANDS = {
|
|
1259
1321
|
npm: "npm install --save-dev @patchstack/connect",
|
|
1260
1322
|
pnpm: "pnpm add -D @patchstack/connect",
|
|
@@ -1322,7 +1384,7 @@ var WIDGET_SCAN_MAX_DEPTH = 6;
|
|
|
1322
1384
|
var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
|
|
1323
1385
|
function detectPackageManager(cwd) {
|
|
1324
1386
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
1325
|
-
if (
|
|
1387
|
+
if (existsSync3(path9.join(cwd, filename))) {
|
|
1326
1388
|
return pm;
|
|
1327
1389
|
}
|
|
1328
1390
|
}
|
|
@@ -1333,7 +1395,7 @@ function installCommand(pm) {
|
|
|
1333
1395
|
}
|
|
1334
1396
|
function readPackageJson(cwd) {
|
|
1335
1397
|
try {
|
|
1336
|
-
return JSON.parse(
|
|
1398
|
+
return JSON.parse(readFileSync2(path9.join(cwd, "package.json"), "utf8"));
|
|
1337
1399
|
} catch {
|
|
1338
1400
|
return null;
|
|
1339
1401
|
}
|
|
@@ -1341,7 +1403,7 @@ function readPackageJson(cwd) {
|
|
|
1341
1403
|
function installedVersion(cwd, declaredRange) {
|
|
1342
1404
|
try {
|
|
1343
1405
|
const pkg = JSON.parse(
|
|
1344
|
-
|
|
1406
|
+
readFileSync2(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
|
|
1345
1407
|
);
|
|
1346
1408
|
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
1347
1409
|
return pkg.version;
|
|
@@ -1369,7 +1431,7 @@ function findWidgetMarker(cwd, siteUuid) {
|
|
|
1369
1431
|
if (budget <= 0 || settled()) {
|
|
1370
1432
|
return;
|
|
1371
1433
|
}
|
|
1372
|
-
const full =
|
|
1434
|
+
const full = path9.join(dir, entry.name);
|
|
1373
1435
|
if (entry.isDirectory()) {
|
|
1374
1436
|
if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
|
|
1375
1437
|
continue;
|
|
@@ -1377,7 +1439,7 @@ function findWidgetMarker(cwd, siteUuid) {
|
|
|
1377
1439
|
walk2(full, depth + 1);
|
|
1378
1440
|
continue;
|
|
1379
1441
|
}
|
|
1380
|
-
if (!entry.isFile() || !WIDGET_EXTENSIONS.has(
|
|
1442
|
+
if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())) {
|
|
1381
1443
|
continue;
|
|
1382
1444
|
}
|
|
1383
1445
|
budget -= 1;
|
|
@@ -1385,8 +1447,8 @@ function findWidgetMarker(cwd, siteUuid) {
|
|
|
1385
1447
|
if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
|
|
1386
1448
|
continue;
|
|
1387
1449
|
}
|
|
1388
|
-
const content =
|
|
1389
|
-
if (!content.includes(
|
|
1450
|
+
const content = readFileSync2(full, "utf8");
|
|
1451
|
+
if (!content.includes(WIDGET_NEEDLE2)) {
|
|
1390
1452
|
continue;
|
|
1391
1453
|
}
|
|
1392
1454
|
sawWidget = true;
|
|
@@ -1409,7 +1471,7 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
1409
1471
|
...GENERIC_WIDGET_FILES
|
|
1410
1472
|
];
|
|
1411
1473
|
for (const candidate of candidates) {
|
|
1412
|
-
if (
|
|
1474
|
+
if (existsSync3(path9.join(cwd, candidate))) {
|
|
1413
1475
|
return candidate;
|
|
1414
1476
|
}
|
|
1415
1477
|
}
|
|
@@ -1433,6 +1495,7 @@ async function collectGuideState(cwd) {
|
|
|
1433
1495
|
let siteUuid = null;
|
|
1434
1496
|
let claimUrl = null;
|
|
1435
1497
|
let endpointOverride = null;
|
|
1498
|
+
let widgetOptOut = false;
|
|
1436
1499
|
try {
|
|
1437
1500
|
const config = await resolveConfig({ cwd });
|
|
1438
1501
|
siteUuid = config.siteUuid;
|
|
@@ -1442,6 +1505,7 @@ async function collectGuideState(cwd) {
|
|
|
1442
1505
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
1443
1506
|
endpointOverride = config.endpoint;
|
|
1444
1507
|
}
|
|
1508
|
+
widgetOptOut = !config.widget;
|
|
1445
1509
|
} catch {
|
|
1446
1510
|
}
|
|
1447
1511
|
const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
|
|
@@ -1464,6 +1528,7 @@ async function collectGuideState(cwd) {
|
|
|
1464
1528
|
postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
1465
1529
|
widgetInstalled: widget.found,
|
|
1466
1530
|
widgetTokenMatches: widget.uuidMatches,
|
|
1531
|
+
widgetOptOut,
|
|
1467
1532
|
framework: stack.framework,
|
|
1468
1533
|
widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
|
|
1469
1534
|
};
|
|
@@ -1481,7 +1546,7 @@ function countRemainingSteps(state) {
|
|
|
1481
1546
|
state.installed !== null,
|
|
1482
1547
|
state.siteUuid !== null,
|
|
1483
1548
|
state.prebuildWired && state.postbuildWired,
|
|
1484
|
-
state.widgetInstalled && state.widgetTokenMatches !== false
|
|
1549
|
+
state.widgetOptOut || state.widgetInstalled && state.widgetTokenMatches !== false
|
|
1485
1550
|
].filter((step) => !step).length;
|
|
1486
1551
|
}
|
|
1487
1552
|
function renderGuideChecklist(state, useColor) {
|
|
@@ -1493,7 +1558,7 @@ function renderGuideChecklist(state, useColor) {
|
|
|
1493
1558
|
const headerParts = [state.framework, state.packageManager].filter(
|
|
1494
1559
|
(part) => part !== null
|
|
1495
1560
|
);
|
|
1496
|
-
const name = state.projectName ??
|
|
1561
|
+
const name = state.projectName ?? path9.basename(process.cwd());
|
|
1497
1562
|
lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
|
|
1498
1563
|
if (state.endpointOverride !== null) {
|
|
1499
1564
|
lines.push(
|
|
@@ -1540,19 +1605,24 @@ function renderGuideChecklist(state, useColor) {
|
|
|
1540
1605
|
}
|
|
1541
1606
|
}
|
|
1542
1607
|
const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
|
|
1543
|
-
if (widgetOk) {
|
|
1608
|
+
if (state.widgetOptOut && !widgetOk) {
|
|
1609
|
+
lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
|
|
1610
|
+
} else if (widgetOk) {
|
|
1544
1611
|
lines.push(done("Disclosure widget installed"));
|
|
1545
1612
|
} else if (state.widgetInstalled) {
|
|
1546
|
-
lines.push(todo("Disclosure widget found, but its
|
|
1547
|
-
lines.push(detail(`\u2192 a wrong
|
|
1613
|
+
lines.push(todo("Disclosure widget found, but its site UUID doesn't match this project's"));
|
|
1614
|
+
lines.push(detail(`\u2192 a wrong site UUID makes the widget silently no-op; set data-site-uuid (or userToken) to '${state.siteUuid}'`));
|
|
1615
|
+
} else if (state.siteUuid === null) {
|
|
1616
|
+
lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
|
|
1617
|
+
lines.push(detail("\u2192 `npx @patchstack/connect scan` provisions the site and adds the widget tag"));
|
|
1618
|
+
lines.push(detail(" to the root HTML shell (index.html / public/index.html / src/app.html)."));
|
|
1548
1619
|
} else {
|
|
1549
1620
|
lines.push(todo('Add the "Report a vulnerability" widget'));
|
|
1550
|
-
|
|
1621
|
+
lines.push(detail("\u2192 a normal `scan` adds this tag to a plain HTML shell automatically;"));
|
|
1622
|
+
const placement = state.widgetFileHint !== null ? ` for this project put it in ${state.widgetFileHint}, just before </body>:` : " put it just before </body> via the framework's HTML/layout mechanism (never a JS entry point):";
|
|
1551
1623
|
lines.push(detail(placement));
|
|
1552
|
-
lines.push(detail(`
|
|
1553
|
-
|
|
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."));
|
|
1624
|
+
lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
|
|
1625
|
+
lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
|
|
1556
1626
|
}
|
|
1557
1627
|
lines.push("");
|
|
1558
1628
|
if (state.claimUrl !== null) {
|
|
@@ -1588,13 +1658,13 @@ function renderGuideChecklist(state, useColor) {
|
|
|
1588
1658
|
}
|
|
1589
1659
|
|
|
1590
1660
|
// src/protect/install.ts
|
|
1591
|
-
import { readFileSync as
|
|
1661
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
|
|
1592
1662
|
import { join, dirname } from "path";
|
|
1593
1663
|
import { fileURLToPath } from "url";
|
|
1594
1664
|
var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
|
|
1595
1665
|
var APP = process.cwd();
|
|
1596
1666
|
var PS_DIR = join(APP, "src/integrations/patchstack");
|
|
1597
|
-
var read = (p) =>
|
|
1667
|
+
var read = (p) => readFileSync3(p, "utf8");
|
|
1598
1668
|
var log = (msg) => console.log(`patchstack protect: ${msg}`);
|
|
1599
1669
|
var CLIENT_TUNNEL = [
|
|
1600
1670
|
"",
|
|
@@ -1639,10 +1709,10 @@ var FUNCTION_MIDDLEWARE_DEF = [
|
|
|
1639
1709
|
].join("\n");
|
|
1640
1710
|
function detectSupportedStack(cwd) {
|
|
1641
1711
|
const pkgPath = join(cwd, "package.json");
|
|
1642
|
-
if (!
|
|
1712
|
+
if (!existsSync4(pkgPath)) return false;
|
|
1643
1713
|
const pkg = JSON.parse(read(pkgPath));
|
|
1644
1714
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1645
|
-
return Boolean(deps["@tanstack/react-start"]) &&
|
|
1715
|
+
return Boolean(deps["@tanstack/react-start"]) && existsSync4(join(cwd, "src/start.ts")) && existsSync4(join(cwd, "src/integrations/supabase/client.ts"));
|
|
1646
1716
|
}
|
|
1647
1717
|
function scaffold(cwd) {
|
|
1648
1718
|
const dst = join(cwd, "src/integrations/patchstack");
|
|
@@ -1657,7 +1727,7 @@ function patchClient(cwd) {
|
|
|
1657
1727
|
if (s.includes("x-ps-target")) return log("client.ts already wired");
|
|
1658
1728
|
const anchor = "headers.set('apikey', supabaseKey);";
|
|
1659
1729
|
if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
|
|
1660
|
-
|
|
1730
|
+
writeFileSync2(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
|
|
1661
1731
|
log("patched client.ts (tunnel Supabase through the guard)");
|
|
1662
1732
|
}
|
|
1663
1733
|
function patchStart(cwd) {
|
|
@@ -1696,7 +1766,7 @@ function patchStart(cwd) {
|
|
|
1696
1766
|
}
|
|
1697
1767
|
}
|
|
1698
1768
|
if (s === original) return log("start.ts already wired");
|
|
1699
|
-
|
|
1769
|
+
writeFileSync2(p, s);
|
|
1700
1770
|
log("patched start.ts (guard registered as request + function middleware)");
|
|
1701
1771
|
}
|
|
1702
1772
|
function runProtect(cwd) {
|
|
@@ -1716,12 +1786,18 @@ var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to
|
|
|
1716
1786
|
Usage:
|
|
1717
1787
|
patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
|
|
1718
1788
|
If no UUID is configured, the server
|
|
1719
|
-
provisions one and we persist it.
|
|
1789
|
+
provisions one and we persist it. After a
|
|
1790
|
+
successful post it also adds/updates the
|
|
1791
|
+
disclosure-widget <script> tag in the root
|
|
1792
|
+
HTML shell (index.html, public/index.html,
|
|
1793
|
+
or src/app.html) \u2014 opt out with
|
|
1794
|
+
"widget": false in .patchstackrc.json
|
|
1720
1795
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
1721
1796
|
with an existing site UUID
|
|
1722
1797
|
patchstack-connect status [options] Show current configuration
|
|
1723
1798
|
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
1724
|
-
build fingerprint
|
|
1799
|
+
build fingerprint, and ensure the widget
|
|
1800
|
+
tag in built pages (run as a postbuild step)
|
|
1725
1801
|
patchstack-connect protect Install always-on runtime protection (the
|
|
1726
1802
|
guard) into a TanStack Start + Supabase app.
|
|
1727
1803
|
Covers the browser + server-function paths.
|
|
@@ -1868,6 +1944,10 @@ async function runScan(args) {
|
|
|
1868
1944
|
} else {
|
|
1869
1945
|
console.log(`Server response: ${response.message ?? JSON.stringify(response)}`);
|
|
1870
1946
|
}
|
|
1947
|
+
const effectiveUuid = config.siteUuid ?? response.uuid ?? null;
|
|
1948
|
+
if (config.widget && effectiveUuid !== null && effectiveUuid.length > 0) {
|
|
1949
|
+
reportSourceWidget(effectiveUuid);
|
|
1950
|
+
}
|
|
1871
1951
|
if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
|
|
1872
1952
|
console.log("");
|
|
1873
1953
|
console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
|
|
@@ -1878,6 +1958,39 @@ async function runScan(args) {
|
|
|
1878
1958
|
}
|
|
1879
1959
|
return 0;
|
|
1880
1960
|
}
|
|
1961
|
+
function reportSourceWidget(siteUuid) {
|
|
1962
|
+
try {
|
|
1963
|
+
const result = ensureSourceWidget(process.cwd(), siteUuid);
|
|
1964
|
+
switch (result.action) {
|
|
1965
|
+
case "added":
|
|
1966
|
+
console.log(`Widget: added the "Report a vulnerability" tag to ${result.shell}. Reload your preview to see it.`);
|
|
1967
|
+
break;
|
|
1968
|
+
case "updated":
|
|
1969
|
+
console.log(`Widget: updated the managed tag in ${result.shell} to site ${siteUuid}.`);
|
|
1970
|
+
break;
|
|
1971
|
+
case "unchanged":
|
|
1972
|
+
console.log(`Widget: already installed in ${result.shell}.`);
|
|
1973
|
+
break;
|
|
1974
|
+
case "manual":
|
|
1975
|
+
console.log(`Widget: found an existing (manual) install in ${result.shell} \u2014 left untouched.`);
|
|
1976
|
+
break;
|
|
1977
|
+
case "no-body":
|
|
1978
|
+
console.log(`Widget: ${result.shell} has no </body> tag to anchor on. Add this tag to your root layout manually:`);
|
|
1979
|
+
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
1980
|
+
break;
|
|
1981
|
+
case "no-shell":
|
|
1982
|
+
console.log("Widget: no plain HTML shell found (index.html / public/index.html / src/app.html).");
|
|
1983
|
+
console.log("Add this tag to your root layout before </body> (run `guide` for framework-specific placement):");
|
|
1984
|
+
console.log(` ${buildWidgetTag(siteUuid)}`);
|
|
1985
|
+
break;
|
|
1986
|
+
}
|
|
1987
|
+
if (result.action === "added" || result.action === "updated") {
|
|
1988
|
+
console.log(' (opt out any time with "widget": false in .patchstackrc.json)');
|
|
1989
|
+
}
|
|
1990
|
+
} catch (err) {
|
|
1991
|
+
console.warn(`Widget: skipped (${err.message}).`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1881
1994
|
async function runProtectCommand(_args) {
|
|
1882
1995
|
try {
|
|
1883
1996
|
runProtect(process.cwd());
|
|
@@ -1906,7 +2019,7 @@ async function runGuide(args) {
|
|
|
1906
2019
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
1907
2020
|
console.log("");
|
|
1908
2021
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
1909
|
-
console.log(
|
|
2022
|
+
console.log(readFileSync4(guidePath, "utf8"));
|
|
1910
2023
|
return 0;
|
|
1911
2024
|
}
|
|
1912
2025
|
async function runStatus(args) {
|
|
@@ -1952,6 +2065,20 @@ async function runMarkBuild(args) {
|
|
|
1952
2065
|
`mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
|
|
1953
2066
|
);
|
|
1954
2067
|
}
|
|
2068
|
+
let widgetUuid = null;
|
|
2069
|
+
try {
|
|
2070
|
+
const config = await resolveConfig({
|
|
2071
|
+
cwd,
|
|
2072
|
+
cliSiteUuid: getStringFlag(args.flags, "site-uuid")
|
|
2073
|
+
});
|
|
2074
|
+
if (config.widget) {
|
|
2075
|
+
widgetUuid = config.siteUuid;
|
|
2076
|
+
}
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
console.warn(
|
|
2079
|
+
`mark-build: could not resolve the site UUID (${err.message}). Skipping the widget pass.`
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
1955
2082
|
const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"));
|
|
1956
2083
|
if (dir === null) {
|
|
1957
2084
|
console.warn(
|
|
@@ -1966,17 +2093,25 @@ async function runMarkBuild(args) {
|
|
|
1966
2093
|
}
|
|
1967
2094
|
const snippet = buildInjectionSnippet(checksum, stack);
|
|
1968
2095
|
let marked = 0;
|
|
2096
|
+
let widgetTouched = 0;
|
|
1969
2097
|
for (const file of files) {
|
|
1970
|
-
const before =
|
|
1971
|
-
|
|
2098
|
+
const before = readFileSync4(file, "utf8");
|
|
2099
|
+
let after = injectMarker(before, snippet);
|
|
2100
|
+
if (widgetUuid !== null) {
|
|
2101
|
+
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
2102
|
+
if (ensured.action === "added" || ensured.action === "updated") {
|
|
2103
|
+
widgetTouched += 1;
|
|
2104
|
+
}
|
|
2105
|
+
after = ensured.html;
|
|
2106
|
+
}
|
|
1972
2107
|
if (after !== before) {
|
|
1973
|
-
|
|
2108
|
+
writeFileSync3(file, after);
|
|
1974
2109
|
marked += 1;
|
|
1975
2110
|
}
|
|
1976
2111
|
}
|
|
1977
2112
|
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
1978
2113
|
console.log(
|
|
1979
|
-
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
2114
|
+
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
1980
2115
|
);
|
|
1981
2116
|
return 0;
|
|
1982
2117
|
}
|