@patchstack/connect 0.3.1 → 0.3.3
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 +74 -0
- package/README.md +5 -1
- package/dist/cli.js +200 -45
- 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/dist/protect/templates/engine.d.ts +57 -0
- package/dist/protect/templates/engine.js +137 -0
- package/dist/protect/templates/guard.ts +95 -52
- package/dist/protect/templates/manifest.d.ts +1 -0
- package/dist/protect/templates/rules.json +7 -4
- package/package.json +2 -6
- package/dist/protect.cjs +0 -1240
- package/dist/protect.cjs.map +0 -1
- package/dist/protect.d.ts +0 -51
- package/dist/protect.js +0 -1210
- package/dist/protect.js.map +0 -1
package/AGENT-INSTALL.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
- 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.
|
|
11
|
+
- Patchstack is not WordPress-only. This connector monitors any JS/Node project — Vite, Next.js, plain vanilla JS, anything with a lockfile.
|
|
12
|
+
|
|
13
|
+
## Before you start — never install twice
|
|
14
|
+
|
|
15
|
+
- 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.**
|
|
16
|
+
- If `@patchstack/connect` is already in `devDependencies`, skip the install command.
|
|
17
|
+
- If the widget script tag (`cdn.patchstack.com/patchstack-widget.js`) is already in the layout, don't add a second one.
|
|
18
|
+
|
|
19
|
+
## Install steps (run in order, don't pause between them unless one fails)
|
|
20
|
+
|
|
21
|
+
1. **Install** (skip if already present), matching the project's package manager:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
npm install --save-dev @patchstack/connect # package-lock.json
|
|
25
|
+
pnpm add -D @patchstack/connect # pnpm-lock.yaml
|
|
26
|
+
yarn add -D @patchstack/connect # yarn.lock
|
|
27
|
+
bun add -d @patchstack/connect # bun.lock / bun-managed platforms
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
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:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
npx @patchstack/connect scan
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
It prints a claim URL. Surface that URL to the user verbatim — never fetch it yourself; it must be opened in their browser.
|
|
37
|
+
|
|
38
|
+
3. **Wire builds** in `package.json` (chain with `&&` if hooks already exist):
|
|
39
|
+
|
|
40
|
+
```jsonc
|
|
41
|
+
{
|
|
42
|
+
"scripts": {
|
|
43
|
+
"prebuild": "patchstack-connect scan",
|
|
44
|
+
"postbuild": "patchstack-connect mark-build"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
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):
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<script src="https://cdn.patchstack.com/patchstack-widget.js"></script>
|
|
53
|
+
<script>
|
|
54
|
+
PatchstackWidget.init({ userToken: '<SITE_UUID>' });
|
|
55
|
+
</script>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
5. **Commit** `.patchstackrc.json` and the updated `package.json` so every developer and CI run reports to the same site.
|
|
61
|
+
|
|
62
|
+
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.
|
|
63
|
+
|
|
64
|
+
## Rules
|
|
65
|
+
|
|
66
|
+
- Never invent or guess a UUID — the scan provisions it, the widget silently no-ops on a fake one.
|
|
67
|
+
- Never fetch the claim URL programmatically, and never ask the user for Patchstack credentials.
|
|
68
|
+
- If a step fails, stop and report it. Don't proceed with placeholders.
|
|
69
|
+
- 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`.
|
|
70
|
+
|
|
71
|
+
## Verifying the install
|
|
72
|
+
|
|
73
|
+
- `npx @patchstack/connect status` re-prints the site UUID and claim URL.
|
|
74
|
+
- Load the site in a browser — the "Report a vulnerability" button should appear.
|
package/README.md
CHANGED
|
@@ -8,7 +8,11 @@ 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
|
-
>
|
|
11
|
+
> Install the npm package `@patchstack/connect` as a dev dependency (MIT, source: https://github.com/patchstack/connect) to add dependency vulnerability monitoring, then run `npx @patchstack/connect guide` and follow the setup guide it prints. It only reads the lockfile — nothing is fetched from a URL or executed remotely.
|
|
12
|
+
|
|
13
|
+
The prompt is built to survive agent security filters: it names a pinnable, auditable npm package (not a remote script), the `guide` command prints instructions that ship *inside* the installed package (`AGENT-INSTALL.md`), and it preempts the "Patchstack is WordPress-only" objection. If the agent has network access, the always-current version of the guide is at https://patchstack.com/install.txt — this longer prompt uses it directly:
|
|
14
|
+
|
|
15
|
+
> Add Patchstack dependency vulnerability monitoring: install the npm package `@patchstack/connect` as a dev dependency (MIT, source: https://github.com/patchstack/connect — pin it and verify however your policy requires), then follow the plain-text setup instructions at https://patchstack.com/install.txt. That URL is documentation to read, not a script to execute — nothing gets piped to a shell. Patchstack monitors any JS/Node lockfile, not just WordPress.
|
|
12
16
|
|
|
13
17
|
## Quick start (zero configuration)
|
|
14
18
|
|
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) {
|
|
@@ -1161,17 +1252,13 @@ function injectMarker(html, snippet) {
|
|
|
1161
1252
|
|
|
1162
1253
|
// src/protect/install.ts
|
|
1163
1254
|
import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
|
|
1164
|
-
import { join
|
|
1255
|
+
import { join } from "path";
|
|
1165
1256
|
import { fileURLToPath } from "url";
|
|
1166
|
-
var TEMPLATES =
|
|
1167
|
-
var APP = process.cwd();
|
|
1168
|
-
var PS_DIR = join(APP, "src/integrations/patchstack");
|
|
1169
|
-
var read = (p) => readFileSync(p, "utf8");
|
|
1170
|
-
var log = (msg) => console.log(`patchstack protect: ${msg}`);
|
|
1257
|
+
var TEMPLATES = fileURLToPath(new URL("./protect/templates/", import.meta.url));
|
|
1171
1258
|
var CLIENT_TUNNEL = [
|
|
1172
1259
|
"",
|
|
1173
|
-
" // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own
|
|
1174
|
-
" // (same-origin) so payloads are inspected before they reach Supabase.",
|
|
1260
|
+
" // PATCHSTACK auto-guard: in the browser, tunnel Supabase traffic through the app's own",
|
|
1261
|
+
" // server guard (same-origin) so payloads are inspected before they reach Supabase.",
|
|
1175
1262
|
" if (typeof window !== 'undefined') {",
|
|
1176
1263
|
" const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
|
|
1177
1264
|
" const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
|
|
@@ -1183,11 +1270,11 @@ var CLIENT_TUNNEL = [
|
|
|
1183
1270
|
].join("\n");
|
|
1184
1271
|
var START_IMPORTS = [
|
|
1185
1272
|
'import { getRequest } from "@tanstack/react-start/server";',
|
|
1186
|
-
'import { GUARD_PATH, handleGuardRequest
|
|
1273
|
+
'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";'
|
|
1187
1274
|
].join("\n");
|
|
1188
1275
|
var START_MIDDLEWARE = [
|
|
1189
1276
|
"",
|
|
1190
|
-
"// Patchstack guard
|
|
1277
|
+
"// Patchstack auto-guard: intercept the tunneled data traffic before anything else runs.",
|
|
1191
1278
|
"const patchstackGuard = createMiddleware().server(async ({ next }) => {",
|
|
1192
1279
|
" const request = getRequest();",
|
|
1193
1280
|
" if (request) {",
|
|
@@ -1196,69 +1283,115 @@ var START_MIDDLEWARE = [
|
|
|
1196
1283
|
" }",
|
|
1197
1284
|
" return next();",
|
|
1198
1285
|
"});",
|
|
1199
|
-
"",
|
|
1200
|
-
"// Patchstack guard (server functions): inspect server-fn args before they reach the database,",
|
|
1201
|
-
"// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).",
|
|
1202
|
-
'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
|
|
1203
|
-
" const blocked = await inspectServerFn(data);",
|
|
1204
|
-
" if (blocked) throw new Error(blocked.message);",
|
|
1205
|
-
" return next();",
|
|
1206
|
-
"});",
|
|
1207
1286
|
""
|
|
1208
1287
|
].join("\n");
|
|
1288
|
+
var PS_DIR_REL = "src/integrations/patchstack";
|
|
1289
|
+
function log(msg) {
|
|
1290
|
+
console.log(`patchstack protect: ${msg}`);
|
|
1291
|
+
}
|
|
1209
1292
|
function detectSupportedStack(cwd) {
|
|
1210
1293
|
const pkgPath = join(cwd, "package.json");
|
|
1211
1294
|
if (!existsSync2(pkgPath)) return false;
|
|
1212
|
-
const pkg = JSON.parse(
|
|
1295
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1213
1296
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1214
1297
|
return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
|
|
1215
1298
|
}
|
|
1299
|
+
function generateManifest(cwd) {
|
|
1300
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
1301
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1302
|
+
const packages = {};
|
|
1303
|
+
for (const name of Object.keys(deps)) {
|
|
1304
|
+
let version = String(deps[name]).replace(/^[^\d]*/, "");
|
|
1305
|
+
const installed = join(cwd, "node_modules", name, "package.json");
|
|
1306
|
+
if (existsSync2(installed)) {
|
|
1307
|
+
try {
|
|
1308
|
+
version = JSON.parse(readFileSync(installed, "utf8")).version;
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
packages[name] = version;
|
|
1313
|
+
}
|
|
1314
|
+
mkdirSync(join(cwd, PS_DIR_REL), { recursive: true });
|
|
1315
|
+
const body = "// Patchstack auto-guard manifest \u2014 generated from the lockfile.\n// Do not edit; regenerated by `patchstack-connect protect --manifest` (wired into prebuild).\nexport const manifest = " + JSON.stringify({ packages }, null, 2) + ";\n";
|
|
1316
|
+
writeFileSync(join(cwd, PS_DIR_REL, "manifest.js"), body);
|
|
1317
|
+
log(`wrote manifest.js (${Object.keys(packages).length} packages)`);
|
|
1318
|
+
}
|
|
1216
1319
|
function scaffold(cwd) {
|
|
1217
|
-
const dst = join(cwd,
|
|
1320
|
+
const dst = join(cwd, PS_DIR_REL);
|
|
1218
1321
|
mkdirSync(dst, { recursive: true });
|
|
1219
|
-
copyFileSync(join(TEMPLATES, "
|
|
1322
|
+
copyFileSync(join(TEMPLATES, "engine.js"), join(dst, "engine.js"));
|
|
1323
|
+
copyFileSync(join(TEMPLATES, "engine.d.ts"), join(dst, "engine.d.ts"));
|
|
1324
|
+
copyFileSync(join(TEMPLATES, "manifest.d.ts"), join(dst, "manifest.d.ts"));
|
|
1220
1325
|
copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
|
|
1221
|
-
|
|
1326
|
+
copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
|
|
1327
|
+
log("scaffolded engine.js (+types), rules.json, guard.ts");
|
|
1222
1328
|
}
|
|
1223
1329
|
function patchClient(cwd) {
|
|
1224
1330
|
const p = join(cwd, "src/integrations/supabase/client.ts");
|
|
1225
|
-
let s =
|
|
1226
|
-
if (s.includes("x-ps-target"))
|
|
1331
|
+
let s = readFileSync(p, "utf8");
|
|
1332
|
+
if (s.includes("x-ps-target")) {
|
|
1333
|
+
log("client.ts already wired");
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1227
1336
|
const anchor = "headers.set('apikey', supabaseKey);";
|
|
1228
|
-
if (!s.includes(anchor))
|
|
1229
|
-
|
|
1337
|
+
if (!s.includes(anchor)) {
|
|
1338
|
+
log("client.ts anchor not found (template changed?) \u2014 skipping client patch");
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
s = s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL);
|
|
1342
|
+
writeFileSync(p, s);
|
|
1230
1343
|
log("patched client.ts (tunnel Supabase through the guard)");
|
|
1231
1344
|
}
|
|
1232
1345
|
function patchStart(cwd) {
|
|
1233
1346
|
const p = join(cwd, "src/start.ts");
|
|
1234
|
-
let s =
|
|
1235
|
-
if (s.includes("patchstackGuard"))
|
|
1347
|
+
let s = readFileSync(p, "utf8");
|
|
1348
|
+
if (s.includes("patchstackGuard")) {
|
|
1349
|
+
log("start.ts already wired");
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1236
1352
|
const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
|
|
1237
1353
|
const exportAnchor = "export const startInstance";
|
|
1238
1354
|
const rmAnchor = "requestMiddleware: [";
|
|
1239
1355
|
if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
|
|
1240
|
-
|
|
1356
|
+
log("start.ts anchors not found (template changed?) \u2014 skipping start patch");
|
|
1357
|
+
return;
|
|
1241
1358
|
}
|
|
1242
1359
|
s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
|
|
1243
1360
|
s = s.replace(exportAnchor, START_MIDDLEWARE + "\n" + exportAnchor);
|
|
1244
1361
|
s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
|
|
1245
|
-
const fmAnchor = "functionMiddleware: [";
|
|
1246
|
-
if (s.includes(fmAnchor)) {
|
|
1247
|
-
s = s.replace(fmAnchor, fmAnchor + "patchstackFunctionGuard, ");
|
|
1248
|
-
} else {
|
|
1249
|
-
s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
|
|
1250
|
-
}
|
|
1251
1362
|
writeFileSync(p, s);
|
|
1252
|
-
log("patched start.ts (guard
|
|
1363
|
+
log("patched start.ts (registered the guard as request middleware)");
|
|
1364
|
+
}
|
|
1365
|
+
function wirePrebuild(cwd) {
|
|
1366
|
+
const p = join(cwd, "package.json");
|
|
1367
|
+
const pkg = JSON.parse(readFileSync(p, "utf8"));
|
|
1368
|
+
pkg.scripts = pkg.scripts || {};
|
|
1369
|
+
const step = "patchstack-connect protect --manifest";
|
|
1370
|
+
const prebuild = pkg.scripts.prebuild || "";
|
|
1371
|
+
if (prebuild.includes(step)) {
|
|
1372
|
+
log("prebuild already refreshes the manifest");
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
pkg.scripts.prebuild = prebuild ? prebuild + " && " + step : step;
|
|
1376
|
+
writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
|
|
1377
|
+
log("wired manifest refresh into prebuild");
|
|
1253
1378
|
}
|
|
1254
|
-
function runProtect(cwd) {
|
|
1379
|
+
function runProtect(cwd, opts = {}) {
|
|
1380
|
+
if (opts.manifestOnly) {
|
|
1381
|
+
generateManifest(cwd);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1255
1384
|
if (!detectSupportedStack(cwd)) {
|
|
1256
|
-
log(
|
|
1385
|
+
log(
|
|
1386
|
+
"runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping."
|
|
1387
|
+
);
|
|
1257
1388
|
return;
|
|
1258
1389
|
}
|
|
1259
1390
|
scaffold(cwd);
|
|
1260
1391
|
patchClient(cwd);
|
|
1261
1392
|
patchStart(cwd);
|
|
1393
|
+
generateManifest(cwd);
|
|
1394
|
+
wirePrebuild(cwd);
|
|
1262
1395
|
log("done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only.");
|
|
1263
1396
|
}
|
|
1264
1397
|
|
|
@@ -1274,8 +1407,11 @@ Usage:
|
|
|
1274
1407
|
patchstack-connect status [options] Show current configuration
|
|
1275
1408
|
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
1276
1409
|
build fingerprint (run as a postbuild step)
|
|
1277
|
-
patchstack-connect protect
|
|
1278
|
-
|
|
1410
|
+
patchstack-connect protect [--manifest] Install runtime protection (the guard) into
|
|
1411
|
+
a server-side JS app. --manifest only
|
|
1412
|
+
regenerates the package manifest (prebuild).
|
|
1413
|
+
patchstack-connect guide Print the full setup guide for AI coding
|
|
1414
|
+
agents (also at https://patchstack.com/install.txt)
|
|
1279
1415
|
patchstack-connect help Print this message
|
|
1280
1416
|
|
|
1281
1417
|
Options (for scan and status):
|
|
@@ -1413,14 +1549,19 @@ async function runScan(args) {
|
|
|
1413
1549
|
}
|
|
1414
1550
|
return 0;
|
|
1415
1551
|
}
|
|
1416
|
-
async function runProtectCommand(
|
|
1552
|
+
async function runProtectCommand(args) {
|
|
1417
1553
|
try {
|
|
1418
|
-
runProtect(process.cwd());
|
|
1554
|
+
runProtect(process.cwd(), { manifestOnly: args.flags.get("manifest") === true });
|
|
1419
1555
|
} catch (err) {
|
|
1420
1556
|
console.warn(`patchstack protect: skipped (${err.message}).`);
|
|
1421
1557
|
}
|
|
1422
1558
|
return 0;
|
|
1423
1559
|
}
|
|
1560
|
+
async function runGuide() {
|
|
1561
|
+
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
1562
|
+
console.log(readFileSync2(guidePath, "utf8"));
|
|
1563
|
+
return 0;
|
|
1564
|
+
}
|
|
1424
1565
|
async function runStatus(args) {
|
|
1425
1566
|
const config = await resolveConfig({
|
|
1426
1567
|
cwd: process.cwd(),
|
|
@@ -1436,9 +1577,19 @@ async function runStatus(args) {
|
|
|
1436
1577
|
}
|
|
1437
1578
|
return 0;
|
|
1438
1579
|
}
|
|
1580
|
+
function describeStack(stack) {
|
|
1581
|
+
const parts = [stack.builder, stack.framework, stack.ui, stack.runtime].filter(
|
|
1582
|
+
(part) => part !== null
|
|
1583
|
+
);
|
|
1584
|
+
if (stack.hostingEnvKeys.length > 0) {
|
|
1585
|
+
parts.push(`${stack.hostingEnvKeys.length} hosting env key(s)`);
|
|
1586
|
+
}
|
|
1587
|
+
return parts.length > 0 ? parts.join(" \xB7 ") : null;
|
|
1588
|
+
}
|
|
1439
1589
|
async function runMarkBuild(args) {
|
|
1440
1590
|
const cwd = process.cwd();
|
|
1441
1591
|
let checksum = null;
|
|
1592
|
+
let stack = null;
|
|
1442
1593
|
try {
|
|
1443
1594
|
const manifest = await scanLockfile(cwd);
|
|
1444
1595
|
for (const warning of manifest.warnings ?? []) {
|
|
@@ -1446,6 +1597,7 @@ async function runMarkBuild(args) {
|
|
|
1446
1597
|
}
|
|
1447
1598
|
const { payload } = buildWirePayload(manifest);
|
|
1448
1599
|
checksum = computeManifestChecksum(payload.packages);
|
|
1600
|
+
stack = detectStack(payload.packages);
|
|
1449
1601
|
} catch (err) {
|
|
1450
1602
|
console.warn(
|
|
1451
1603
|
`mark-build: could not compute the build fingerprint (${err.message}). Stamping the production flag only.`
|
|
@@ -1463,7 +1615,7 @@ async function runMarkBuild(args) {
|
|
|
1463
1615
|
console.warn(`mark-build: no HTML files found under ${dir}. Nothing to mark.`);
|
|
1464
1616
|
return 0;
|
|
1465
1617
|
}
|
|
1466
|
-
const snippet = buildInjectionSnippet(checksum);
|
|
1618
|
+
const snippet = buildInjectionSnippet(checksum, stack);
|
|
1467
1619
|
let marked = 0;
|
|
1468
1620
|
for (const file of files) {
|
|
1469
1621
|
const before = readFileSync2(file, "utf8");
|
|
@@ -1473,8 +1625,9 @@ async function runMarkBuild(args) {
|
|
|
1473
1625
|
marked += 1;
|
|
1474
1626
|
}
|
|
1475
1627
|
}
|
|
1628
|
+
const stackSummary = stack !== null ? describeStack(stack) : null;
|
|
1476
1629
|
console.log(
|
|
1477
|
-
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}.`
|
|
1630
|
+
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
1478
1631
|
);
|
|
1479
1632
|
return 0;
|
|
1480
1633
|
}
|
|
@@ -1495,6 +1648,8 @@ async function main() {
|
|
|
1495
1648
|
return runMarkBuild(args);
|
|
1496
1649
|
case "protect":
|
|
1497
1650
|
return runProtectCommand(args);
|
|
1651
|
+
case "guide":
|
|
1652
|
+
return runGuide();
|
|
1498
1653
|
default:
|
|
1499
1654
|
console.error(`Unknown command: ${args.command}
|
|
1500
1655
|
`);
|