@patchstack/connect 0.3.2 → 0.3.4
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 +75 -0
- package/README.md +14 -1
- package/dist/cli.js +116 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +92 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +42 -1
- package/dist/index.d.ts +42 -1
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/AGENT-INSTALL.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Patchstack — Setup Guide for AI Coding Agents
|
|
2
|
+
|
|
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
|
+
|
|
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). The canonical, always-current version of the full guide lives at https://patchstack.com/install.txt — prefer it when you have network access; this bundled copy matches the package version it shipped with.
|
|
6
|
+
|
|
7
|
+
## Verify before you trust (do this, it's quick)
|
|
8
|
+
|
|
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
|
+
- 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 **lockfile only** (`package-lock.json`, `pnpm-lock.yaml`, or `yarn.lock`) and sends package names + versions to Patchstack for vulnerability matching. No source code, no env vars, no file paths, no git history.
|
|
12
|
+
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
13
|
+
|
|
14
|
+
## Before you start — never install twice
|
|
15
|
+
|
|
16
|
+
- If `.patchstackrc.json` exists, the project is already provisioned. Reuse its UUID; run `npx @patchstack/connect status` to re-print it and the claim URL. **Do not run `scan` to provision a second site.**
|
|
17
|
+
- If `@patchstack/connect` is already in `devDependencies`, skip the install command.
|
|
18
|
+
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
|
|
19
|
+
|
|
20
|
+
## Install steps (run in order, don't pause between them unless one fails)
|
|
21
|
+
|
|
22
|
+
1. **Install** (skip if already present), matching the project's package manager:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
npm install --save-dev @patchstack/connect # package-lock.json
|
|
26
|
+
pnpm add -D @patchstack/connect # pnpm-lock.yaml
|
|
27
|
+
yarn add -D @patchstack/connect # yarn.lock
|
|
28
|
+
bun add -d @patchstack/connect # bun.lock / bun-managed platforms
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. **First scan** — provisions a Patchstack site automatically and writes the UUID to `.patchstackrc.json`. No signup, dashboard step, or UUID is needed up front:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npx @patchstack/connect scan
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
|
|
38
|
+
|
|
39
|
+
3. **Wire builds** in `package.json` (chain with `&&` if hooks already exist):
|
|
40
|
+
|
|
41
|
+
```jsonc
|
|
42
|
+
{
|
|
43
|
+
"scripts": {
|
|
44
|
+
"prebuild": "patchstack-connect scan",
|
|
45
|
+
"postbuild": "patchstack-connect mark-build"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
4. **Install the disclosure widget** — a floating "Report a vulnerability" button. Read the UUID from `.patchstackrc.json` and use it as `userToken`. Place these two snippets via the framework's HTML/layout mechanism (never a JS entry point):
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
|
|
54
|
+
<script>
|
|
55
|
+
PatchstackWidget.init({ userToken: '<SITE_UUID>' });
|
|
56
|
+
</script>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
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
|
+
|
|
61
|
+
5. **Commit** `.patchstackrc.json` and the updated `package.json` so every developer and CI run reports to the same site.
|
|
62
|
+
|
|
63
|
+
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
|
+
|
|
65
|
+
## Rules
|
|
66
|
+
|
|
67
|
+
- Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
|
|
68
|
+
- Never fetch the claim URL programmatically, and never ask the user for Patchstack credentials.
|
|
69
|
+
- If a step fails, stop and report it. Don't proceed with placeholders.
|
|
70
|
+
- In CI where `.patchstackrc.json` can't be committed, set `PATCHSTACK_SITE_UUID` as an env var instead. Precedence: CLI flag → env var → `.patchstackrc.json`.
|
|
71
|
+
|
|
72
|
+
## Verifying the install
|
|
73
|
+
|
|
74
|
+
- `npx @patchstack/connect status` re-prints the site UUID and claim URL.
|
|
75
|
+
- Load the site in a browser — the "Report a vulnerability" button should appear.
|
package/README.md
CHANGED
|
@@ -8,7 +8,20 @@ For how this repo fits with the wider Patchstack ecosystem (`saas`, `hub`, `patc
|
|
|
8
8
|
|
|
9
9
|
Copy and paste this prompt into any AI coding assistant (Cursor, v0, Bolt, Lovable, Claude Code, etc.):
|
|
10
10
|
|
|
11
|
-
> Add
|
|
11
|
+
> Add Patchstack dependency vulnerability monitoring to this project:
|
|
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.
|
|
17
|
+
|
|
18
|
+
The shape of this prompt is deliberate — it has survived three distinct AI-agent refusal modes, and each one punished a different shortcut:
|
|
19
|
+
|
|
20
|
+
1. **"Follow the instructions at this URL" reads as remote script execution.** Agents refuse before ever fetching the doc.
|
|
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.
|
|
12
25
|
|
|
13
26
|
## Quick start (zero configuration)
|
|
14
27
|
|
package/dist/cli.js
CHANGED
|
@@ -1108,6 +1108,94 @@ function isEnvironment(value) {
|
|
|
1108
1108
|
// src/mark-build.ts
|
|
1109
1109
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1110
1110
|
import path7 from "path";
|
|
1111
|
+
|
|
1112
|
+
// src/stack.ts
|
|
1113
|
+
var STACK_RULES = [
|
|
1114
|
+
// Meta-frameworks (most specific first).
|
|
1115
|
+
{ category: "framework", pkg: "@tanstack/react-start", label: "tanstack-start" },
|
|
1116
|
+
{ category: "framework", pkg: "@tanstack/start", label: "tanstack-start" },
|
|
1117
|
+
{ category: "framework", pkg: "next", label: "next" },
|
|
1118
|
+
{ category: "framework", pkg: "nuxt", label: "nuxt" },
|
|
1119
|
+
{ category: "framework", pkg: "@remix-run/react", label: "remix" },
|
|
1120
|
+
{ category: "framework", pkg: "@remix-run/node", label: "remix" },
|
|
1121
|
+
{ category: "framework", pkg: "react-router", label: "react-router" },
|
|
1122
|
+
{ category: "framework", pkg: "astro", label: "astro" },
|
|
1123
|
+
{ category: "framework", pkg: "@sveltejs/kit", label: "sveltekit" },
|
|
1124
|
+
{ category: "framework", pkg: "@builder.io/qwik-city", label: "qwik-city" },
|
|
1125
|
+
{ category: "framework", pkg: "gatsby", label: "gatsby" },
|
|
1126
|
+
{ category: "framework", pkg: "express", label: "express" },
|
|
1127
|
+
{ category: "framework", pkg: "fastify", label: "fastify" },
|
|
1128
|
+
// UI runtimes.
|
|
1129
|
+
{ category: "ui", pkg: "@angular/core", label: "angular" },
|
|
1130
|
+
{ category: "ui", pkg: "react-dom", label: "react" },
|
|
1131
|
+
{ category: "ui", pkg: "react", label: "react" },
|
|
1132
|
+
{ category: "ui", pkg: "vue", label: "vue" },
|
|
1133
|
+
{ category: "ui", pkg: "svelte", label: "svelte" },
|
|
1134
|
+
{ category: "ui", pkg: "solid-js", label: "solid" },
|
|
1135
|
+
{ category: "ui", pkg: "preact", label: "preact" },
|
|
1136
|
+
// Bundlers / build tools.
|
|
1137
|
+
{ category: "bundler", pkg: "vite", label: "vite" },
|
|
1138
|
+
{ category: "bundler", pkg: "@rspack/core", label: "rspack" },
|
|
1139
|
+
{ category: "bundler", pkg: "webpack", label: "webpack" },
|
|
1140
|
+
{ category: "bundler", pkg: "parcel", label: "parcel" },
|
|
1141
|
+
{ category: "bundler", pkg: "rollup", label: "rollup" },
|
|
1142
|
+
{ category: "bundler", pkg: "esbuild", label: "esbuild" },
|
|
1143
|
+
// Deployment-runtime hints from build deps.
|
|
1144
|
+
{ category: "runtime", pkg: "wrangler", label: "cloudflare-workers" },
|
|
1145
|
+
{ category: "runtime", pkg: "@cloudflare/workers-types", label: "cloudflare-workers" },
|
|
1146
|
+
{ category: "runtime", pkg: "@cloudflare/vite-plugin", label: "cloudflare-workers" },
|
|
1147
|
+
{ category: "runtime", pkg: "@vercel/node", label: "vercel" },
|
|
1148
|
+
{ category: "runtime", pkg: "@netlify/functions", label: "netlify" },
|
|
1149
|
+
{ category: "runtime", pkg: "@netlify/blobs", label: "netlify" },
|
|
1150
|
+
// Vibe / builder platforms — the "learn from each platform" signal.
|
|
1151
|
+
{ category: "builder", pkg: "lovable-tagger", label: "lovable" },
|
|
1152
|
+
{ category: "builder", pkg: "@replit/vite-plugin-runtime-error-modal", label: "replit" },
|
|
1153
|
+
{ category: "builder", pkg: "@replit/vite-plugin-cartographer", label: "replit" }
|
|
1154
|
+
];
|
|
1155
|
+
var HOSTING_ENV_PATTERNS = [
|
|
1156
|
+
/^CF_/,
|
|
1157
|
+
/^CLOUDFLARE_/,
|
|
1158
|
+
/^VERCEL/,
|
|
1159
|
+
/^NETLIFY/,
|
|
1160
|
+
/^AWS_(LAMBDA|REGION|EXECUTION)/,
|
|
1161
|
+
/^FLY_/,
|
|
1162
|
+
/^RENDER/,
|
|
1163
|
+
/^RAILWAY_/,
|
|
1164
|
+
/^DENO_/,
|
|
1165
|
+
/^EDGE_RUNTIME/,
|
|
1166
|
+
/^DYNO$/,
|
|
1167
|
+
/^K_SERVICE$/,
|
|
1168
|
+
/^GAE_/,
|
|
1169
|
+
/^FUNCTION_/
|
|
1170
|
+
];
|
|
1171
|
+
function collectHostingEnvKeys(env = process.env) {
|
|
1172
|
+
return Object.keys(env).filter((key) => HOSTING_ENV_PATTERNS.some((pattern) => pattern.test(key))).sort();
|
|
1173
|
+
}
|
|
1174
|
+
function detectStack(packages, env = process.env) {
|
|
1175
|
+
const present = new Set(packages.map((pkg) => pkg.name));
|
|
1176
|
+
const firstMatch = (category) => {
|
|
1177
|
+
for (const rule of STACK_RULES) {
|
|
1178
|
+
if (rule.category === category && present.has(rule.pkg)) {
|
|
1179
|
+
return rule.label;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return null;
|
|
1183
|
+
};
|
|
1184
|
+
return {
|
|
1185
|
+
framework: firstMatch("framework"),
|
|
1186
|
+
ui: firstMatch("ui"),
|
|
1187
|
+
bundler: firstMatch("bundler"),
|
|
1188
|
+
runtime: firstMatch("runtime"),
|
|
1189
|
+
builder: firstMatch("builder"),
|
|
1190
|
+
ecosystem: "npm",
|
|
1191
|
+
hostingEnvKeys: collectHostingEnvKeys(env)
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
function isEmptyStack(stack) {
|
|
1195
|
+
return stack.framework === null && stack.ui === null && stack.bundler === null && stack.runtime === null && stack.builder === null && stack.hostingEnvKeys.length === 0;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/mark-build.ts
|
|
1111
1199
|
var MARKER_ATTR = "data-patchstack-build";
|
|
1112
1200
|
var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
|
|
1113
1201
|
function resolveBuildDir(cwd, override) {
|
|
@@ -1138,11 +1226,14 @@ function findHtmlFiles(dir) {
|
|
|
1138
1226
|
walk2(dir);
|
|
1139
1227
|
return out;
|
|
1140
1228
|
}
|
|
1141
|
-
function buildInjectionSnippet(checksum) {
|
|
1229
|
+
function buildInjectionSnippet(checksum, stack) {
|
|
1142
1230
|
const statements = ["window.__PATCHSTACK_PROD__=true;"];
|
|
1143
1231
|
if (checksum !== null && checksum !== "") {
|
|
1144
1232
|
statements.push(`window.__PATCHSTACK_BUILD__=${JSON.stringify(checksum)};`);
|
|
1145
1233
|
}
|
|
1234
|
+
if (stack != null && !isEmptyStack(stack)) {
|
|
1235
|
+
statements.push(`window.__PATCHSTACK_STACK__=${JSON.stringify(stack)};`);
|
|
1236
|
+
}
|
|
1146
1237
|
return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
|
|
1147
1238
|
}
|
|
1148
1239
|
function injectMarker(html, snippet) {
|
|
@@ -1296,6 +1387,9 @@ Usage:
|
|
|
1296
1387
|
build fingerprint (run as a postbuild step)
|
|
1297
1388
|
patchstack-connect protect Install always-on runtime protection (the
|
|
1298
1389
|
guard) into a TanStack Start + Supabase app.
|
|
1390
|
+
Covers the browser + server-function paths.
|
|
1391
|
+
patchstack-connect guide Print the full setup guide for AI coding
|
|
1392
|
+
agents (also at https://patchstack.com/install.txt)
|
|
1299
1393
|
patchstack-connect help Print this message
|
|
1300
1394
|
|
|
1301
1395
|
Options (for scan and status):
|
|
@@ -1441,6 +1535,11 @@ async function runProtectCommand(_args) {
|
|
|
1441
1535
|
}
|
|
1442
1536
|
return 0;
|
|
1443
1537
|
}
|
|
1538
|
+
async function runGuide() {
|
|
1539
|
+
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
1540
|
+
console.log(readFileSync2(guidePath, "utf8"));
|
|
1541
|
+
return 0;
|
|
1542
|
+
}
|
|
1444
1543
|
async function runStatus(args) {
|
|
1445
1544
|
const config = await resolveConfig({
|
|
1446
1545
|
cwd: process.cwd(),
|
|
@@ -1456,9 +1555,19 @@ async function runStatus(args) {
|
|
|
1456
1555
|
}
|
|
1457
1556
|
return 0;
|
|
1458
1557
|
}
|
|
1558
|
+
function describeStack(stack) {
|
|
1559
|
+
const parts = [stack.builder, stack.framework, stack.ui, stack.runtime].filter(
|
|
1560
|
+
(part) => part !== null
|
|
1561
|
+
);
|
|
1562
|
+
if (stack.hostingEnvKeys.length > 0) {
|
|
1563
|
+
parts.push(`${stack.hostingEnvKeys.length} hosting env key(s)`);
|
|
1564
|
+
}
|
|
1565
|
+
return parts.length > 0 ? parts.join(" \xB7 ") : null;
|
|
1566
|
+
}
|
|
1459
1567
|
async function runMarkBuild(args) {
|
|
1460
1568
|
const cwd = process.cwd();
|
|
1461
1569
|
let checksum = null;
|
|
1570
|
+
let stack = null;
|
|
1462
1571
|
try {
|
|
1463
1572
|
const manifest = await scanLockfile(cwd);
|
|
1464
1573
|
for (const warning of manifest.warnings ?? []) {
|
|
@@ -1466,6 +1575,7 @@ async function runMarkBuild(args) {
|
|
|
1466
1575
|
}
|
|
1467
1576
|
const { payload } = buildWirePayload(manifest);
|
|
1468
1577
|
checksum = computeManifestChecksum(payload.packages);
|
|
1578
|
+
stack = detectStack(payload.packages);
|
|
1469
1579
|
} catch (err) {
|
|
1470
1580
|
console.warn(
|
|
1471
1581
|
`mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
|
|
@@ -1483,7 +1593,7 @@ async function runMarkBuild(args) {
|
|
|
1483
1593
|
console.warn(`mark-build: no HTML files found under ${dir}. Nothing to mark.`);
|
|
1484
1594
|
return 0;
|
|
1485
1595
|
}
|
|
1486
|
-
const snippet = buildInjectionSnippet(checksum);
|
|
1596
|
+
const snippet = buildInjectionSnippet(checksum, stack);
|
|
1487
1597
|
let marked = 0;
|
|
1488
1598
|
for (const file of files) {
|
|
1489
1599
|
const before = readFileSync2(file, "utf8");
|
|
@@ -1493,8 +1603,9 @@ async function runMarkBuild(args) {
|
|
|
1493
1603
|
marked += 1;
|
|
1494
1604
|
}
|
|
1495
1605
|
}
|
|
1606
|
+
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
1496
1607
|
console.log(
|
|
1497
|
-
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
|
|
1608
|
+
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
1498
1609
|
);
|
|
1499
1610
|
return 0;
|
|
1500
1611
|
}
|
|
@@ -1515,6 +1626,8 @@ async function main() {
|
|
|
1515
1626
|
return runMarkBuild(args);
|
|
1516
1627
|
case "protect":
|
|
1517
1628
|
return runProtectCommand(args);
|
|
1629
|
+
case "guide":
|
|
1630
|
+
return runGuide();
|
|
1518
1631
|
default:
|
|
1519
1632
|
console.error(`Unknown command: ${args.command}
|
|
1520
1633
|
`);
|