@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.
@@ -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 `@patchstack/connect` to this project as a dev dependency, then add `"prebuild": "patchstack-connect scan"` to `package.json` `scripts`. The first time the script runs, Patchstack provisions a site automatically from the lockfile and saves the new site UUID to `.patchstackrc.json` — no dashboard signup or manual UUID required. Commit `.patchstackrc.json` so every developer / CI run targets the same site.
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
  `);