@patchstack/connect 0.3.3 → 0.3.5

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/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 readFileSync2, writeFileSync as writeFileSync2 } from "fs";
4
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -1250,15 +1250,339 @@ function injectMarker(html, snippet) {
1250
1250
  return stripped + snippet;
1251
1251
  }
1252
1252
 
1253
+ // src/guide.ts
1254
+ import { existsSync as existsSync2, readFileSync, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1255
+ import path8 from "path";
1256
+ var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
1257
+ var WIDGET_NEEDLE = "patchstack-widget";
1258
+ var INSTALL_COMMANDS = {
1259
+ npm: "npm install --save-dev @patchstack/connect",
1260
+ pnpm: "pnpm add -D @patchstack/connect",
1261
+ yarn: "yarn add -D @patchstack/connect",
1262
+ bun: "bun add -d @patchstack/connect"
1263
+ };
1264
+ var PM_BY_LOCKFILE = [
1265
+ { filename: "package-lock.json", pm: "npm" },
1266
+ { filename: "bun.lock", pm: "bun" },
1267
+ { filename: "bun.lockb", pm: "bun" },
1268
+ { filename: "pnpm-lock.yaml", pm: "pnpm" },
1269
+ { filename: "yarn.lock", pm: "yarn" }
1270
+ ];
1271
+ var WIDGET_FILE_CANDIDATES = {
1272
+ next: [
1273
+ "app/layout.tsx",
1274
+ "app/layout.jsx",
1275
+ "src/app/layout.tsx",
1276
+ "src/app/layout.jsx",
1277
+ "pages/_document.tsx",
1278
+ "pages/_document.jsx",
1279
+ "src/pages/_document.tsx"
1280
+ ],
1281
+ nuxt: ["app.vue", "src/app.vue", "app/app.vue"],
1282
+ remix: ["app/root.tsx", "app/root.jsx"],
1283
+ "react-router": ["app/root.tsx", "src/root.tsx"],
1284
+ "tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
1285
+ sveltekit: ["src/app.html"],
1286
+ astro: ["src/layouts/Layout.astro"],
1287
+ gatsby: ["src/html.js"]
1288
+ };
1289
+ var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
1290
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
1291
+ "node_modules",
1292
+ ".git",
1293
+ "dist",
1294
+ "build",
1295
+ "out",
1296
+ ".output",
1297
+ ".next",
1298
+ ".nuxt",
1299
+ ".svelte-kit",
1300
+ ".vercel",
1301
+ ".netlify",
1302
+ "coverage",
1303
+ "vendor"
1304
+ ]);
1305
+ var WIDGET_EXTENSIONS = /* @__PURE__ */ new Set([
1306
+ ".html",
1307
+ ".htm",
1308
+ ".tsx",
1309
+ ".jsx",
1310
+ ".ts",
1311
+ ".js",
1312
+ ".mjs",
1313
+ ".cjs",
1314
+ ".vue",
1315
+ ".svelte",
1316
+ ".astro",
1317
+ ".ejs",
1318
+ ".hbs"
1319
+ ]);
1320
+ var WIDGET_SCAN_MAX_FILES = 4e3;
1321
+ var WIDGET_SCAN_MAX_DEPTH = 6;
1322
+ var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
1323
+ function detectPackageManager(cwd) {
1324
+ for (const { filename, pm } of PM_BY_LOCKFILE) {
1325
+ if (existsSync2(path8.join(cwd, filename))) {
1326
+ return pm;
1327
+ }
1328
+ }
1329
+ return "npm";
1330
+ }
1331
+ function installCommand(pm) {
1332
+ return INSTALL_COMMANDS[pm];
1333
+ }
1334
+ function readPackageJson(cwd) {
1335
+ try {
1336
+ return JSON.parse(readFileSync(path8.join(cwd, "package.json"), "utf8"));
1337
+ } catch {
1338
+ return null;
1339
+ }
1340
+ }
1341
+ function installedVersion(cwd, declaredRange) {
1342
+ try {
1343
+ const pkg = JSON.parse(
1344
+ readFileSync(path8.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
1345
+ );
1346
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
1347
+ return pkg.version;
1348
+ }
1349
+ } catch {
1350
+ }
1351
+ return declaredRange;
1352
+ }
1353
+ function findWidgetMarker(cwd, siteUuid) {
1354
+ let budget = WIDGET_SCAN_MAX_FILES;
1355
+ let sawWidget = false;
1356
+ let sawTokenMatch = false;
1357
+ const settled = () => sawTokenMatch || sawWidget && siteUuid == null;
1358
+ const walk2 = (dir, depth) => {
1359
+ if (depth > WIDGET_SCAN_MAX_DEPTH || budget <= 0) {
1360
+ return;
1361
+ }
1362
+ let entries;
1363
+ try {
1364
+ entries = readdirSync2(dir, { withFileTypes: true });
1365
+ } catch {
1366
+ return;
1367
+ }
1368
+ for (const entry of entries) {
1369
+ if (budget <= 0 || settled()) {
1370
+ return;
1371
+ }
1372
+ const full = path8.join(dir, entry.name);
1373
+ if (entry.isDirectory()) {
1374
+ if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
1375
+ continue;
1376
+ }
1377
+ walk2(full, depth + 1);
1378
+ continue;
1379
+ }
1380
+ if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path8.extname(entry.name).toLowerCase())) {
1381
+ continue;
1382
+ }
1383
+ budget -= 1;
1384
+ try {
1385
+ if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
1386
+ continue;
1387
+ }
1388
+ const content = readFileSync(full, "utf8");
1389
+ if (!content.includes(WIDGET_NEEDLE)) {
1390
+ continue;
1391
+ }
1392
+ sawWidget = true;
1393
+ if (siteUuid != null && content.includes(siteUuid)) {
1394
+ sawTokenMatch = true;
1395
+ }
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ };
1400
+ walk2(cwd, 0);
1401
+ return {
1402
+ found: sawWidget,
1403
+ uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
1404
+ };
1405
+ }
1406
+ function resolveWidgetFileHint(cwd, framework) {
1407
+ const candidates = [
1408
+ ...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
1409
+ ...GENERIC_WIDGET_FILES
1410
+ ];
1411
+ for (const candidate of candidates) {
1412
+ if (existsSync2(path8.join(cwd, candidate))) {
1413
+ return candidate;
1414
+ }
1415
+ }
1416
+ return null;
1417
+ }
1418
+ async function collectGuideState(cwd) {
1419
+ const pkg = readPackageJson(cwd);
1420
+ const packageManager = detectPackageManager(cwd);
1421
+ let installed = null;
1422
+ if (pkg?.devDependencies?.["@patchstack/connect"] !== void 0) {
1423
+ installed = {
1424
+ version: installedVersion(cwd, pkg.devDependencies["@patchstack/connect"]),
1425
+ section: "devDependencies"
1426
+ };
1427
+ } else if (pkg?.dependencies?.["@patchstack/connect"] !== void 0) {
1428
+ installed = {
1429
+ version: installedVersion(cwd, pkg.dependencies["@patchstack/connect"]),
1430
+ section: "dependencies"
1431
+ };
1432
+ }
1433
+ let siteUuid = null;
1434
+ let claimUrl = null;
1435
+ let endpointOverride = null;
1436
+ try {
1437
+ const config = await resolveConfig({ cwd });
1438
+ siteUuid = config.siteUuid;
1439
+ if (siteUuid !== null) {
1440
+ claimUrl = buildClaimUrl(config.endpoint, siteUuid);
1441
+ }
1442
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1443
+ endpointOverride = config.endpoint;
1444
+ }
1445
+ } catch {
1446
+ }
1447
+ const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
1448
+ const stack = detectStack(
1449
+ declaredNames.map((name) => ({ name, version: "" })),
1450
+ {}
1451
+ );
1452
+ const widget = findWidgetMarker(cwd, siteUuid);
1453
+ return {
1454
+ projectName: pkg?.name ?? null,
1455
+ hasPackageJson: pkg !== null,
1456
+ packageManager,
1457
+ installed,
1458
+ siteUuid,
1459
+ claimUrl,
1460
+ endpointOverride,
1461
+ prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan"),
1462
+ postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build"),
1463
+ widgetInstalled: widget.found,
1464
+ widgetTokenMatches: widget.uuidMatches,
1465
+ framework: stack.framework,
1466
+ widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
1467
+ };
1468
+ }
1469
+ var ANSI = {
1470
+ reset: "\x1B[0m",
1471
+ bold: "\x1B[1m",
1472
+ dim: "\x1B[2m",
1473
+ green: "\x1B[32m",
1474
+ yellow: "\x1B[33m",
1475
+ cyan: "\x1B[36m"
1476
+ };
1477
+ function renderGuideChecklist(state, useColor) {
1478
+ const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
1479
+ const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
1480
+ const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
1481
+ const detail = (text) => ` ${paint(ANSI.dim, text)}`;
1482
+ const lines = [];
1483
+ const headerParts = [state.framework, state.packageManager].filter(
1484
+ (part) => part !== null
1485
+ );
1486
+ const name = state.projectName ?? path8.basename(process.cwd());
1487
+ lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
1488
+ if (state.endpointOverride !== null) {
1489
+ lines.push(
1490
+ detail(
1491
+ `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
1492
+ )
1493
+ );
1494
+ }
1495
+ lines.push("");
1496
+ if (!state.hasPackageJson) {
1497
+ lines.push(todo("No package.json found in this directory."));
1498
+ lines.push(detail("Run the guide from the project root."));
1499
+ return lines.join("\n");
1500
+ }
1501
+ if (state.installed !== null) {
1502
+ lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
1503
+ } else {
1504
+ lines.push(todo("Install @patchstack/connect as a dev dependency"));
1505
+ lines.push(detail(`\u2192 ${installCommand(state.packageManager)}`));
1506
+ if (state.packageManager === "bun") {
1507
+ lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
1508
+ }
1509
+ }
1510
+ if (state.siteUuid !== null) {
1511
+ lines.push(done(`Site provisioned (${state.siteUuid})`));
1512
+ } else {
1513
+ lines.push(todo("Provision the site \u2014 run the first scan"));
1514
+ lines.push(detail("\u2192 npx @patchstack/connect scan"));
1515
+ lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
1516
+ lines.push(detail("and prints a claim URL \u2014 show that URL to the user; never open it yourself."));
1517
+ }
1518
+ if (state.prebuildWired && state.postbuildWired) {
1519
+ lines.push(done("Build hooks wired (prebuild scan, postbuild mark-build)"));
1520
+ } else {
1521
+ lines.push(todo("Wire builds \u2014 add to package.json scripts (chain with && if a hook exists)"));
1522
+ if (!state.prebuildWired) {
1523
+ lines.push(detail('\u2192 "prebuild": "patchstack-connect scan"'));
1524
+ }
1525
+ if (!state.postbuildWired) {
1526
+ lines.push(detail('\u2192 "postbuild": "patchstack-connect mark-build"'));
1527
+ }
1528
+ }
1529
+ const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
1530
+ if (widgetOk) {
1531
+ lines.push(done("Disclosure widget installed"));
1532
+ } else if (state.widgetInstalled) {
1533
+ lines.push(todo("Disclosure widget found, but its userToken doesn't match this project's site UUID"));
1534
+ lines.push(detail(`\u2192 a wrong userToken makes the widget silently no-op; set it to '${state.siteUuid}'`));
1535
+ } else {
1536
+ lines.push(todo('Add the "Report a vulnerability" widget'));
1537
+ const placement = state.widgetFileHint !== null ? `\u2192 add to ${state.widgetFileHint} (via the framework's HTML/layout mechanism):` : "\u2192 add via the framework's HTML/layout mechanism (never a JS entry point):";
1538
+ lines.push(detail(placement));
1539
+ lines.push(detail(` <script src="${WIDGET_SCRIPT_URL}"></script>`));
1540
+ const token = state.siteUuid ?? "<SITE_UUID from .patchstackrc.json \u2014 run scan first>";
1541
+ lines.push(detail(` <script>PatchstackWidget.init({ userToken: '${token}' });</script>`));
1542
+ lines.push(detail("The userToken is public by design \u2014 it ships in client-side HTML."));
1543
+ }
1544
+ lines.push("");
1545
+ if (state.claimUrl !== null) {
1546
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Claim the site (free, opens the dashboard):")}`);
1547
+ lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
1548
+ lines.push(detail("Open in a browser. AI agents: show this URL to the user verbatim."));
1549
+ if (state.endpointOverride !== null) {
1550
+ lines.push(detail("(this URL inherits the endpoint override above)"));
1551
+ }
1552
+ } else {
1553
+ lines.push(detail("The claim URL appears after the first scan (re-print any time with `status`)."));
1554
+ }
1555
+ const remaining = [
1556
+ state.installed !== null,
1557
+ state.siteUuid !== null,
1558
+ state.prebuildWired && state.postbuildWired,
1559
+ widgetOk
1560
+ ].filter((step) => !step).length;
1561
+ lines.push("");
1562
+ lines.push(
1563
+ remaining === 0 ? done(
1564
+ paint(
1565
+ ANSI.bold,
1566
+ "All setup steps complete. Commit .patchstackrc.json, package.json, and the file carrying the widget snippet."
1567
+ )
1568
+ ) : ` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
1569
+ );
1570
+ return lines.join("\n");
1571
+ }
1572
+
1253
1573
  // src/protect/install.ts
1254
- import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
1255
- import { join } from "path";
1574
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
1575
+ import { join, dirname } from "path";
1256
1576
  import { fileURLToPath } from "url";
1257
- var TEMPLATES = fileURLToPath(new URL("./protect/templates/", import.meta.url));
1577
+ var TEMPLATES = join(dirname(fileURLToPath(import.meta.url)), "protect", "templates");
1578
+ var APP = process.cwd();
1579
+ var PS_DIR = join(APP, "src/integrations/patchstack");
1580
+ var read = (p) => readFileSync2(p, "utf8");
1581
+ var log = (msg) => console.log(`patchstack protect: ${msg}`);
1258
1582
  var CLIENT_TUNNEL = [
1259
1583
  "",
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.",
1584
+ " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
1585
+ " // (same-origin) so payloads are inspected before they reach Supabase.",
1262
1586
  " if (typeof window !== 'undefined') {",
1263
1587
  " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
1264
1588
  " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
@@ -1270,11 +1594,11 @@ var CLIENT_TUNNEL = [
1270
1594
  ].join("\n");
1271
1595
  var START_IMPORTS = [
1272
1596
  'import { getRequest } from "@tanstack/react-start/server";',
1273
- 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";'
1597
+ 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1274
1598
  ].join("\n");
1275
- var START_MIDDLEWARE = [
1599
+ var REQUEST_MIDDLEWARE_DEF = [
1276
1600
  "",
1277
- "// Patchstack auto-guard: intercept the tunneled data traffic before anything else runs.",
1601
+ "// Patchstack guard (browser tunnel): intercept tunneled Supabase traffic before anything else.",
1278
1602
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1279
1603
  " const request = getRequest();",
1280
1604
  " if (request) {",
@@ -1285,113 +1609,87 @@ var START_MIDDLEWARE = [
1285
1609
  "});",
1286
1610
  ""
1287
1611
  ].join("\n");
1288
- var PS_DIR_REL = "src/integrations/patchstack";
1289
- function log(msg) {
1290
- console.log(`patchstack protect: ${msg}`);
1291
- }
1612
+ var FUNCTION_MIDDLEWARE_DEF = [
1613
+ "",
1614
+ "// Patchstack guard (server functions): inspect server-fn args before they reach the database,",
1615
+ "// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).",
1616
+ 'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1617
+ " const blocked = await inspectServerFn(data);",
1618
+ " if (blocked) throw new Error(blocked.message);",
1619
+ " return next();",
1620
+ "});",
1621
+ ""
1622
+ ].join("\n");
1292
1623
  function detectSupportedStack(cwd) {
1293
1624
  const pkgPath = join(cwd, "package.json");
1294
- if (!existsSync2(pkgPath)) return false;
1295
- const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
1296
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1297
- return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
1298
- }
1299
- function generateManifest(cwd) {
1300
- const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
1625
+ if (!existsSync3(pkgPath)) return false;
1626
+ const pkg = JSON.parse(read(pkgPath));
1301
1627
  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)`);
1628
+ return Boolean(deps["@tanstack/react-start"]) && existsSync3(join(cwd, "src/start.ts")) && existsSync3(join(cwd, "src/integrations/supabase/client.ts"));
1318
1629
  }
1319
1630
  function scaffold(cwd) {
1320
- const dst = join(cwd, PS_DIR_REL);
1631
+ const dst = join(cwd, "src/integrations/patchstack");
1321
1632
  mkdirSync(dst, { recursive: true });
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"));
1325
- copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1326
1633
  copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1327
- log("scaffolded engine.js (+types), rules.json, guard.ts");
1634
+ copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1635
+ log("scaffolded guard.ts + rules.json");
1328
1636
  }
1329
1637
  function patchClient(cwd) {
1330
1638
  const p = join(cwd, "src/integrations/supabase/client.ts");
1331
- let s = readFileSync(p, "utf8");
1332
- if (s.includes("x-ps-target")) {
1333
- log("client.ts already wired");
1334
- return;
1335
- }
1639
+ let s = read(p);
1640
+ if (s.includes("x-ps-target")) return log("client.ts already wired");
1336
1641
  const anchor = "headers.set('apikey', supabaseKey);";
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);
1642
+ if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
1643
+ writeFileSync(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1343
1644
  log("patched client.ts (tunnel Supabase through the guard)");
1344
1645
  }
1345
1646
  function patchStart(cwd) {
1346
1647
  const p = join(cwd, "src/start.ts");
1347
- let s = readFileSync(p, "utf8");
1348
- if (s.includes("patchstackGuard")) {
1349
- log("start.ts already wired");
1350
- return;
1351
- }
1648
+ let s = read(p);
1352
1649
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1353
1650
  const exportAnchor = "export const startInstance";
1354
1651
  const rmAnchor = "requestMiddleware: [";
1355
- if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
1356
- log("start.ts anchors not found (template changed?) \u2014 skipping start patch");
1357
- return;
1652
+ if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
1653
+ return log("start.ts anchors not found \u2014 skipping (template changed?)");
1654
+ }
1655
+ const original = s;
1656
+ if (!s.includes("@/integrations/patchstack/guard")) {
1657
+ s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1658
+ } else if (!s.includes("inspectServerFn")) {
1659
+ s = s.replace(
1660
+ 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";',
1661
+ 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1662
+ );
1358
1663
  }
1359
- s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1360
- s = s.replace(exportAnchor, START_MIDDLEWARE + "\n" + exportAnchor);
1361
- s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1362
- writeFileSync(p, s);
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;
1664
+ if (!s.includes("const patchstackGuard =")) {
1665
+ s = s.replace(exportAnchor, REQUEST_MIDDLEWARE_DEF + "\n" + exportAnchor);
1374
1666
  }
1375
- pkg.scripts.prebuild = prebuild ? prebuild + " && " + step : step;
1376
- writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
1377
- log("wired manifest refresh into prebuild");
1378
- }
1379
- function runProtect(cwd, opts = {}) {
1380
- if (opts.manifestOnly) {
1381
- generateManifest(cwd);
1382
- return;
1667
+ if (!s.includes("const patchstackFunctionGuard =")) {
1668
+ s = s.replace(exportAnchor, FUNCTION_MIDDLEWARE_DEF + "\n" + exportAnchor);
1383
1669
  }
1670
+ if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
1671
+ s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1672
+ }
1673
+ if (!s.includes("functionMiddleware: [patchstackFunctionGuard")) {
1674
+ const fmAnchor = "functionMiddleware: [";
1675
+ if (s.includes(fmAnchor)) {
1676
+ s = s.replace(fmAnchor, fmAnchor + "patchstackFunctionGuard, ");
1677
+ } else if (s.includes(rmAnchor)) {
1678
+ s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1679
+ }
1680
+ }
1681
+ if (s === original) return log("start.ts already wired");
1682
+ writeFileSync(p, s);
1683
+ log("patched start.ts (guard registered as request + function middleware)");
1684
+ }
1685
+ function runProtect(cwd) {
1384
1686
  if (!detectSupportedStack(cwd)) {
1385
- log(
1386
- "runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping."
1387
- );
1687
+ log("runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping.");
1388
1688
  return;
1389
1689
  }
1390
1690
  scaffold(cwd);
1391
1691
  patchClient(cwd);
1392
1692
  patchStart(cwd);
1393
- generateManifest(cwd);
1394
- wirePrebuild(cwd);
1395
1693
  log("done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only.");
1396
1694
  }
1397
1695
 
@@ -1407,11 +1705,13 @@ Usage:
1407
1705
  patchstack-connect status [options] Show current configuration
1408
1706
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1409
1707
  build fingerprint (run as a postbuild step)
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)
1708
+ patchstack-connect protect Install always-on runtime protection (the
1709
+ guard) into a TanStack Start + Supabase app.
1710
+ Covers the browser + server-function paths.
1711
+ patchstack-connect guide Show this project's setup status (what's done,
1712
+ what's missing, with tailored commands), then
1713
+ print the full setup guide
1714
+ (also at https://patchstack.com/install.txt)
1415
1715
  patchstack-connect help Print this message
1416
1716
 
1417
1717
  Options (for scan and status):
@@ -1502,14 +1802,22 @@ async function runScan(args) {
1502
1802
  }
1503
1803
  const { payload, stats } = buildWirePayload(manifest);
1504
1804
  console.log(
1505
- `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
1805
+ `Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
1806
+ );
1807
+ console.log(
1808
+ `Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
1506
1809
  );
1507
- console.log(`Reporting under the ${config.environment} environment.`);
1810
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1811
+ console.log(
1812
+ `Using endpoint override: ${config.endpoint} (set via --endpoint, PATCHSTACK_ENDPOINT, or .patchstackrc.json).`
1813
+ );
1814
+ }
1508
1815
  if (stats.duplicateNames.length > 0) {
1509
- console.log(`${stats.duplicateNames.length} package(s) appear at multiple versions:`);
1510
- if (stats.duplicateNames.length <= 10) {
1511
- console.log(` ${stats.duplicateNames.join(", ")}`);
1512
- }
1816
+ const sample = stats.duplicateNames.slice(0, 10).join(", ");
1817
+ const more = stats.duplicateNames.length > 10 ? `, +${stats.duplicateNames.length - 10} more` : "";
1818
+ console.log(
1819
+ `${stats.duplicateNames.length} package(s) appear at multiple versions: ${sample}${more}`
1820
+ );
1513
1821
  }
1514
1822
  if (dryRun) {
1515
1823
  console.log("");
@@ -1546,20 +1854,32 @@ async function runScan(args) {
1546
1854
  console.log("");
1547
1855
  console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
1548
1856
  console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
1857
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
1858
+ console.log(" (this URL inherits the endpoint override above)");
1859
+ }
1549
1860
  }
1550
1861
  return 0;
1551
1862
  }
1552
- async function runProtectCommand(args) {
1863
+ async function runProtectCommand(_args) {
1553
1864
  try {
1554
- runProtect(process.cwd(), { manifestOnly: args.flags.get("manifest") === true });
1865
+ runProtect(process.cwd());
1555
1866
  } catch (err) {
1556
1867
  console.warn(`patchstack protect: skipped (${err.message}).`);
1557
1868
  }
1558
1869
  return 0;
1559
1870
  }
1560
1871
  async function runGuide() {
1872
+ try {
1873
+ const state = await collectGuideState(process.cwd());
1874
+ const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
1875
+ console.log(renderGuideChecklist(state, useColor));
1876
+ console.log("");
1877
+ console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
1878
+ console.log("");
1879
+ } catch {
1880
+ }
1561
1881
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
1562
- console.log(readFileSync2(guidePath, "utf8"));
1882
+ console.log(readFileSync3(guidePath, "utf8"));
1563
1883
  return 0;
1564
1884
  }
1565
1885
  async function runStatus(args) {
@@ -1569,7 +1889,9 @@ async function runStatus(args) {
1569
1889
  cliEndpoint: getStringFlag(args.flags, "endpoint")
1570
1890
  });
1571
1891
  console.log(`Site UUID: ${config.siteUuid ?? "(none yet \u2014 the next `scan` will provision one)"}`);
1572
- console.log(`Endpoint: ${config.endpoint}`);
1892
+ console.log(
1893
+ `Endpoint: ${config.endpoint}${config.endpoint === DEFAULT_ENDPOINT ? "" : " (override)"}`
1894
+ );
1573
1895
  console.log(`Timeout: ${config.timeoutMs}ms`);
1574
1896
  console.log(`Environment: ${config.environment}`);
1575
1897
  if (config.siteUuid !== null) {
@@ -1618,7 +1940,7 @@ async function runMarkBuild(args) {
1618
1940
  const snippet = buildInjectionSnippet(checksum, stack);
1619
1941
  let marked = 0;
1620
1942
  for (const file of files) {
1621
- const before = readFileSync2(file, "utf8");
1943
+ const before = readFileSync3(file, "utf8");
1622
1944
  const after = injectMarker(before, snippet);
1623
1945
  if (after !== before) {
1624
1946
  writeFileSync2(file, after);