@patchstack/connect 0.3.19 → 0.3.20

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
@@ -1464,1188 +1464,1291 @@ function injectMarker(html, snippet) {
1464
1464
  }
1465
1465
 
1466
1466
  // src/guide.ts
1467
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
1467
+ import { existsSync as existsSync10, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
1468
1468
  import path9 from "path";
1469
1469
 
1470
- // src/widget.ts
1471
- import { existsSync as existsSync2, readFileSync, writeFileSync } from "fs";
1472
- import path8 from "path";
1473
- var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
1474
- var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
1475
- var WIDGET_NEEDLE = "patchstack-widget";
1476
- var SOURCE_SHELL_CANDIDATES = ["index.html", "public/index.html", "src/app.html"];
1477
- function buildWidgetTag(siteUuid) {
1478
- return `<script src="${WIDGET_SCRIPT_URL}" data-site-uuid="${siteUuid}" defer ${WIDGET_MARKER_ATTR}="true"></script>`;
1470
+ // src/protect/install/util.ts
1471
+ import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
1472
+ import { join as join2, dirname } from "path";
1473
+ import { fileURLToPath } from "url";
1474
+ var read = (p) => readFileSync(p, "utf8");
1475
+ var log = (msg) => console.log(`patchstack protect: ${msg}`);
1476
+ function hasDependency(cwd, name) {
1477
+ try {
1478
+ const pkg = JSON.parse(read(join2(cwd, "package.json")));
1479
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
1480
+ } catch {
1481
+ return false;
1482
+ }
1479
1483
  }
1480
- var MANAGED_TAG_RE = new RegExp(
1481
- `<script[^>]*${WIDGET_MARKER_ATTR}[^>]*>\\s*</script>`,
1482
- "i"
1483
- );
1484
- function ensureWidgetInHtml(html, siteUuid) {
1485
- const tag = buildWidgetTag(siteUuid);
1486
- const managed = html.match(MANAGED_TAG_RE);
1487
- if (managed !== null) {
1488
- if (managed[0].includes(`data-site-uuid="${siteUuid}"`)) {
1489
- return { html, action: "unchanged" };
1490
- }
1491
- return { html: html.replace(MANAGED_TAG_RE, tag), action: "updated" };
1484
+ var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
1485
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1486
+ function bakeSiteUuid(cwd, guardRelPath) {
1487
+ const rc = join2(cwd, ".patchstackrc.json");
1488
+ if (!existsSync2(rc)) {
1489
+ log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1490
+ return false;
1492
1491
  }
1493
- if (html.includes(WIDGET_NEEDLE)) {
1494
- return { html, action: "manual" };
1492
+ let uuid;
1493
+ try {
1494
+ uuid = JSON.parse(read(rc)).siteUuid;
1495
+ } catch {
1496
+ log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
1497
+ return false;
1495
1498
  }
1496
- const bodyClose = html.match(/([ \t]*)<\/body>/i);
1497
- if (bodyClose === null || bodyClose.index === void 0) {
1498
- return { html, action: "no-body" };
1499
+ if (typeof uuid !== "string" || !UUID_RE.test(uuid)) {
1500
+ log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1501
+ return false;
1499
1502
  }
1500
- const indent = bodyClose[1] ?? "";
1501
- const insertion = `${indent} ${tag}
1502
- ${indent}</body>`;
1503
- return {
1504
- html: html.slice(0, bodyClose.index) + insertion + html.slice(bodyClose.index + bodyClose[0].length),
1505
- action: "added"
1506
- };
1507
- }
1508
- function findSourceShell(cwd) {
1509
- for (const candidate of SOURCE_SHELL_CANDIDATES) {
1510
- if (existsSync2(path8.join(cwd, candidate))) {
1511
- return candidate;
1512
- }
1503
+ const guardPath = join2(cwd, guardRelPath);
1504
+ if (!existsSync2(guardPath)) return false;
1505
+ const source = read(guardPath);
1506
+ if (!source.includes(SITE_UUID_PLACEHOLDER)) {
1507
+ log(`${guardRelPath} site UUID already baked`);
1508
+ return false;
1513
1509
  }
1514
- return null;
1510
+ writeFileSync(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
1511
+ log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
1512
+ return true;
1515
1513
  }
1516
- function ensureSourceWidget(cwd, siteUuid) {
1517
- const shell = findSourceShell(cwd);
1518
- if (shell === null) {
1519
- return { shell: null, action: "no-shell" };
1520
- }
1521
- const file = path8.join(cwd, shell);
1522
- const before = readFileSync(file, "utf8");
1523
- const { html, action } = ensureWidgetInHtml(before, siteUuid);
1524
- if (html !== before) {
1525
- writeFileSync(file, html);
1526
- }
1527
- return { shell, action };
1514
+ var HERE = dirname(fileURLToPath(import.meta.url));
1515
+ function templatesDir() {
1516
+ const builtLayout = join2(HERE, "protect", "templates");
1517
+ const candidates = [
1518
+ builtLayout,
1519
+ join2(HERE, "..", "templates"),
1520
+ // source: src/protect/install/ → src/protect/templates
1521
+ join2(HERE, "templates")
1522
+ ];
1523
+ return candidates.find((p) => existsSync2(p)) ?? builtLayout;
1528
1524
  }
1529
1525
 
1530
- // src/guide.ts
1531
- var WIDGET_NEEDLE2 = "patchstack-widget";
1532
- var INSTALL_COMMANDS = {
1533
- npm: "npm install --save-dev @patchstack/connect",
1534
- pnpm: "pnpm add -D @patchstack/connect",
1535
- yarn: "yarn add -D @patchstack/connect",
1536
- bun: "bun add -d @patchstack/connect"
1537
- };
1538
- var PM_BY_LOCKFILE = [
1539
- { filename: "bun.lock", pm: "bun" },
1540
- { filename: "bun.lockb", pm: "bun" },
1541
- { filename: "pnpm-lock.yaml", pm: "pnpm" },
1542
- { filename: "yarn.lock", pm: "yarn" },
1543
- { filename: "package-lock.json", pm: "npm" }
1544
- ];
1545
- var WIDGET_FILE_CANDIDATES = {
1546
- next: [
1547
- "app/layout.tsx",
1548
- "app/layout.jsx",
1549
- "src/app/layout.tsx",
1550
- "src/app/layout.jsx",
1551
- "pages/_document.tsx",
1552
- "pages/_document.jsx",
1553
- "src/pages/_document.tsx"
1554
- ],
1555
- nuxt: ["app.vue", "src/app.vue", "app/app.vue"],
1556
- remix: ["app/root.tsx", "app/root.jsx"],
1557
- "react-router": ["app/root.tsx", "src/root.tsx"],
1558
- "tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
1559
- sveltekit: ["src/app.html"],
1560
- astro: ["src/layouts/Layout.astro"],
1561
- gatsby: ["src/html.js"]
1562
- };
1563
- var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
1564
- var SKIPPED_DIRS = /* @__PURE__ */ new Set([
1565
- "node_modules",
1566
- ".git",
1567
- "dist",
1568
- "build",
1569
- "out",
1570
- ".output",
1571
- ".next",
1572
- ".nuxt",
1573
- ".svelte-kit",
1574
- ".vercel",
1575
- ".netlify",
1576
- "coverage",
1577
- "vendor"
1578
- ]);
1579
- var WIDGET_EXTENSIONS = /* @__PURE__ */ new Set([
1580
- ".html",
1581
- ".htm",
1582
- ".tsx",
1583
- ".jsx",
1584
- ".ts",
1585
- ".js",
1586
- ".mjs",
1587
- ".cjs",
1588
- ".vue",
1589
- ".svelte",
1590
- ".astro",
1591
- ".ejs",
1592
- ".hbs"
1593
- ]);
1594
- var WIDGET_SCAN_MAX_FILES = 4e3;
1595
- var WIDGET_SCAN_MAX_DEPTH = 6;
1596
- var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
1597
- function detectPackageManager(cwd) {
1598
- const declared = readPackageJson(cwd)?.packageManager?.split("@")[0];
1599
- if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
1600
- return declared;
1601
- }
1602
- for (const { filename, pm } of PM_BY_LOCKFILE) {
1603
- if (existsSync3(path9.join(cwd, filename))) {
1604
- return pm;
1526
+ // src/protect/install/adapters/tanstack-supabase.ts
1527
+ import { writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
1528
+ import { join as join3 } from "path";
1529
+ var CLIENT_TUNNEL = [
1530
+ "",
1531
+ " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
1532
+ " // (same-origin) so payloads are inspected before they reach Supabase.",
1533
+ " if (typeof window !== 'undefined') {",
1534
+ " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
1535
+ " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
1536
+ " headers.set('x-ps-target', target);",
1537
+ " const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
1538
+ " return fetch(guardUrl, { ...init, method, headers });",
1539
+ " }",
1540
+ ""
1541
+ ].join("\n");
1542
+ var GUARD_IMPORT = 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse, guardRequest } from "@/integrations/patchstack/guard";';
1543
+ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard";/;
1544
+ var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
1545
+ var REQUEST_MIDDLEWARE_BLOCK = [
1546
+ "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
1547
+ "// Browser\u2192Supabase tunnel + response screening; optional route WAF via PATCHSTACK_ROUTE_WAF=1.",
1548
+ "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1549
+ " const request = getRequest();",
1550
+ " if (request) {",
1551
+ " const { pathname } = new URL(request.url);",
1552
+ " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
1553
+ ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
1554
+ " const blocked = await guardRequest(request);",
1555
+ " if (blocked) return blocked;",
1556
+ " }",
1557
+ " }",
1558
+ " return screenResponse(await next());",
1559
+ "});",
1560
+ "// #endregion patchstack-guard"
1561
+ ].join("\n");
1562
+ var FUNCTION_MIDDLEWARE_BLOCK = [
1563
+ "// #region patchstack-function-guard (managed by patchstack-connect protect \u2014 do not edit)",
1564
+ "// Inspect server-function args before they reach the database.",
1565
+ 'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1566
+ " const blocked = await inspectServerFn(data);",
1567
+ " if (blocked) throw new Error(blocked.message);",
1568
+ " return next();",
1569
+ "});",
1570
+ "// #endregion patchstack-function-guard"
1571
+ ].join("\n");
1572
+ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
1573
+ const lines = s.split("\n");
1574
+ const startMarker = `// #region ${region} `;
1575
+ const endMarker = `// #endregion ${region}`;
1576
+ const si = lines.findIndex((l) => l.includes(startMarker));
1577
+ if (si !== -1) {
1578
+ const ei = lines.findIndex((l, i) => i > si && l.trim() === endMarker);
1579
+ if (ei !== -1) {
1580
+ lines.splice(si, ei - si + 1, ...block.split("\n"));
1581
+ return lines.join("\n");
1605
1582
  }
1606
1583
  }
1607
- return "npm";
1608
- }
1609
- function installCommand(pm) {
1610
- return INSTALL_COMMANDS[pm];
1611
- }
1612
- function readPackageJson(cwd) {
1613
- try {
1614
- return JSON.parse(readFileSync2(path9.join(cwd, "package.json"), "utf8"));
1615
- } catch {
1616
- return null;
1584
+ const ci = lines.findIndex((l) => l.includes(legacyConst));
1585
+ if (ci !== -1) {
1586
+ const close = lines.findIndex((l, i) => i >= ci && l.trim() === "});");
1587
+ if (close !== -1) {
1588
+ let start = ci;
1589
+ while (start > 0 && (lines[start - 1] ?? "").trim().startsWith("//")) start--;
1590
+ lines.splice(start, close - start + 1, ...block.split("\n"));
1591
+ return lines.join("\n");
1592
+ }
1617
1593
  }
1594
+ return s.replace(insertBefore, block + "\n\n" + insertBefore);
1618
1595
  }
1619
- function installedVersion(cwd, declaredRange) {
1596
+ function detect(cwd) {
1597
+ const pkgPath = join3(cwd, "package.json");
1598
+ if (!existsSync3(pkgPath)) return false;
1599
+ let pkg;
1620
1600
  try {
1621
- const pkg = JSON.parse(
1622
- readFileSync2(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
1623
- );
1624
- if (typeof pkg.version === "string" && pkg.version.length > 0) {
1625
- return pkg.version;
1626
- }
1601
+ pkg = JSON.parse(read(pkgPath));
1627
1602
  } catch {
1603
+ return false;
1628
1604
  }
1629
- return declaredRange;
1605
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1606
+ return Boolean(deps["@tanstack/react-start"]) && existsSync3(join3(cwd, "src/start.ts")) && existsSync3(join3(cwd, "src/integrations/supabase/client.ts"));
1630
1607
  }
1631
- function findWidgetMarker(cwd, siteUuid) {
1632
- let budget = WIDGET_SCAN_MAX_FILES;
1633
- let sawWidget = false;
1634
- let sawTokenMatch = false;
1635
- const settled = () => sawTokenMatch || sawWidget && siteUuid == null;
1636
- const walk2 = (dir, depth) => {
1637
- if (depth > WIDGET_SCAN_MAX_DEPTH || budget <= 0) {
1638
- return;
1639
- }
1640
- let entries;
1641
- try {
1642
- entries = readdirSync2(dir, { withFileTypes: true });
1643
- } catch {
1644
- return;
1645
- }
1646
- for (const entry of entries) {
1647
- if (budget <= 0 || settled()) {
1648
- return;
1649
- }
1650
- const full = path9.join(dir, entry.name);
1651
- if (entry.isDirectory()) {
1652
- if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
1653
- continue;
1654
- }
1655
- walk2(full, depth + 1);
1656
- continue;
1657
- }
1658
- if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())) {
1659
- continue;
1660
- }
1661
- budget -= 1;
1662
- try {
1663
- if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
1664
- continue;
1665
- }
1666
- const content = readFileSync2(full, "utf8");
1667
- if (!content.includes(WIDGET_NEEDLE2)) {
1668
- continue;
1669
- }
1670
- sawWidget = true;
1671
- if (siteUuid != null && content.includes(siteUuid)) {
1672
- sawTokenMatch = true;
1673
- }
1674
- } catch {
1675
- }
1676
- }
1677
- };
1678
- walk2(cwd, 0);
1679
- return {
1680
- found: sawWidget,
1681
- uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
1682
- };
1683
- }
1684
- function resolveWidgetFileHint(cwd, framework) {
1685
- const candidates = [
1686
- ...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
1687
- ...GENERIC_WIDGET_FILES
1688
- ];
1689
- for (const candidate of candidates) {
1690
- if (existsSync3(path9.join(cwd, candidate))) {
1691
- return candidate;
1692
- }
1608
+ var GUARD_FILE = "src/integrations/patchstack/guard.ts";
1609
+ function scaffold(cwd, opts) {
1610
+ const templates = templatesDir();
1611
+ const dst = join3(cwd, "src/integrations/patchstack");
1612
+ mkdirSync(dst, { recursive: true });
1613
+ copyFileSync(join3(templates, "guard.ts"), join3(dst, "guard.ts"));
1614
+ const changed = [GUARD_FILE];
1615
+ const rulesDst = join3(dst, "rules.json");
1616
+ if (opts.demo) {
1617
+ copyFileSync(join3(templates, "demo-rules.json"), rulesDst);
1618
+ changed.push("src/integrations/patchstack/rules.json");
1619
+ log("scaffolded guard.ts + rules.json (demo sample rule set)");
1620
+ } else if (!existsSync3(rulesDst)) {
1621
+ copyFileSync(join3(templates, "rules.json"), rulesDst);
1622
+ changed.push("src/integrations/patchstack/rules.json");
1623
+ log("scaffolded guard.ts + rules.json (starter rules)");
1624
+ } else {
1625
+ log("scaffolded guard.ts (kept existing rules.json)");
1693
1626
  }
1694
- return null;
1627
+ return changed;
1695
1628
  }
1696
- async function collectGuideState(cwd) {
1697
- const pkg = readPackageJson(cwd);
1698
- const packageManager = detectPackageManager(cwd);
1699
- let installed = null;
1700
- if (pkg?.devDependencies?.["@patchstack/connect"] !== void 0) {
1701
- installed = {
1702
- version: installedVersion(cwd, pkg.devDependencies["@patchstack/connect"]),
1703
- section: "devDependencies"
1704
- };
1705
- } else if (pkg?.dependencies?.["@patchstack/connect"] !== void 0) {
1706
- installed = {
1707
- version: installedVersion(cwd, pkg.dependencies["@patchstack/connect"]),
1708
- section: "dependencies"
1709
- };
1629
+ function patchClient(cwd) {
1630
+ const p = join3(cwd, "src/integrations/supabase/client.ts");
1631
+ const s = read(p);
1632
+ if (s.includes("x-ps-target")) {
1633
+ log("client.ts already wired");
1634
+ return false;
1710
1635
  }
1711
- let siteUuid = null;
1712
- let claimUrl = null;
1713
- let endpointOverride = null;
1714
- let widgetOptOut = false;
1715
- try {
1716
- const config = await resolveConfig({ cwd });
1717
- siteUuid = config.siteUuid;
1718
- if (siteUuid !== null) {
1719
- claimUrl = buildClaimUrl(config.endpoint, siteUuid);
1720
- }
1721
- if (config.endpoint !== DEFAULT_ENDPOINT) {
1722
- endpointOverride = config.endpoint;
1723
- }
1724
- widgetOptOut = !config.widget;
1725
- } catch {
1636
+ const anchor = "headers.set('apikey', supabaseKey);";
1637
+ if (!s.includes(anchor)) {
1638
+ log("client.ts anchor not found \u2014 skipping (template changed?)");
1639
+ return false;
1726
1640
  }
1727
- const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
1728
- const stack = detectStack(
1729
- declaredNames.map((name) => ({ name, version: "" })),
1730
- {}
1731
- );
1732
- const widget = findWidgetMarker(cwd, siteUuid);
1733
- return {
1734
- projectName: pkg?.name ?? null,
1735
- hasPackageJson: pkg !== null,
1736
- packageManager,
1737
- installed,
1738
- siteUuid,
1739
- claimUrl,
1740
- endpointOverride,
1741
- // bun run doesn't execute npm-style pre/post scripts, so chaining inside
1742
- // the build script itself also counts as wired (and is what we suggest on bun).
1743
- prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
1744
- postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
1745
- widgetInstalled: widget.found,
1746
- widgetTokenMatches: widget.uuidMatches,
1747
- widgetOptOut,
1748
- framework: stack.framework,
1749
- widgetFileHint: resolveWidgetFileHint(cwd, stack.framework)
1750
- };
1751
- }
1752
- var ANSI = {
1753
- reset: "\x1B[0m",
1754
- bold: "\x1B[1m",
1755
- dim: "\x1B[2m",
1756
- green: "\x1B[32m",
1757
- yellow: "\x1B[33m",
1758
- cyan: "\x1B[36m"
1759
- };
1760
- function countRemainingSteps(state) {
1761
- return [
1762
- state.installed !== null,
1763
- state.siteUuid !== null,
1764
- state.prebuildWired && state.postbuildWired,
1765
- state.widgetOptOut || state.widgetInstalled && state.widgetTokenMatches !== false
1766
- ].filter((step) => !step).length;
1641
+ writeFileSync2(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1642
+ log("patched client.ts (tunnel Supabase through the guard)");
1643
+ return true;
1767
1644
  }
1768
- function renderGuideChecklist(state, useColor) {
1769
- const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
1770
- const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
1771
- const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
1772
- const detail = (text) => ` ${paint(ANSI.dim, text)}`;
1773
- const lines = [];
1774
- const headerParts = [state.framework, state.packageManager].filter(
1775
- (part) => part !== null
1776
- );
1777
- const name = state.projectName ?? path9.basename(process.cwd());
1778
- lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
1779
- if (state.endpointOverride !== null) {
1780
- lines.push(
1781
- detail(
1782
- `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
1783
- )
1784
- );
1785
- }
1786
- lines.push("");
1787
- if (!state.hasPackageJson) {
1788
- lines.push(todo("No package.json found in this directory."));
1789
- lines.push(detail("Run the guide from the project root."));
1790
- return lines.join("\n");
1791
- }
1792
- if (state.installed !== null) {
1793
- lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
1794
- } else {
1795
- lines.push(todo("Install @patchstack/connect as a dev dependency"));
1796
- lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
1797
- if (state.packageManager === "bun") {
1798
- lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
1799
- }
1800
- }
1801
- if (state.siteUuid !== null) {
1802
- lines.push(done(`Site provisioned (${state.siteUuid})`));
1803
- } else {
1804
- lines.push(todo("Provision the site \u2014 run the first scan"));
1805
- lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
1806
- lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
1807
- lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
1645
+ function patchStart(cwd) {
1646
+ const p = join3(cwd, "src/start.ts");
1647
+ let s = read(p);
1648
+ const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1649
+ const exportAnchor = "export const startInstance";
1650
+ const rmAnchor = "requestMiddleware: [";
1651
+ if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
1652
+ log("start.ts anchors not found \u2014 skipping (template changed?)");
1653
+ return false;
1808
1654
  }
1809
- if (state.prebuildWired && state.postbuildWired) {
1810
- lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
1811
- } else if (state.packageManager === "bun") {
1812
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
1813
- lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
1655
+ const original = s;
1656
+ if (GUARD_IMPORT_RE.test(s)) {
1657
+ s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
1814
1658
  } else {
1815
- lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
1816
- if (!state.prebuildWired) {
1817
- lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
1818
- }
1819
- if (!state.postbuildWired) {
1820
- lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
1821
- }
1659
+ s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1822
1660
  }
1823
- const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
1824
- if (state.widgetOptOut && !widgetOk) {
1825
- lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
1826
- } else if (widgetOk) {
1827
- lines.push(done("Disclosure widget installed"));
1828
- } else if (state.widgetInstalled) {
1829
- lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
1830
- lines.push(detail(`Edit the widget tag \u2192 set data-site-uuid (or userToken) to '${state.siteUuid}' (a wrong UUID makes the widget silently no-op)`));
1831
- } else if (state.siteUuid === null) {
1832
- lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
1833
- lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
1834
- lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
1835
- } else {
1836
- lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
1837
- lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
1838
- const placement = state.widgetFileHint !== null ? `Edit ${state.widgetFileHint} \u2192 put it just before </body>:` : "Edit your root layout \u2192 put it just before </body> (the framework's HTML/layout mechanism, never a JS entry point):";
1839
- lines.push(detail(placement));
1840
- lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
1841
- lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
1661
+ s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
1662
+ s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
1663
+ if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
1664
+ s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1842
1665
  }
1843
- lines.push("");
1844
- if (state.claimUrl !== null) {
1845
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
1846
- lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
1847
- lines.push(detail("Open this link in a browser. The CLI never opens it."));
1848
- if (state.endpointOverride !== null) {
1849
- lines.push(detail("(this URL inherits the endpoint override above)"));
1666
+ if (!s.includes("functionMiddleware: [patchstackFunctionGuard")) {
1667
+ const fmAnchor = "functionMiddleware: [";
1668
+ if (s.includes(fmAnchor)) {
1669
+ s = s.replace(fmAnchor, fmAnchor + "patchstackFunctionGuard, ");
1670
+ } else if (s.includes(rmAnchor)) {
1671
+ s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1850
1672
  }
1851
- } else {
1852
- lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
1853
1673
  }
1854
- const remaining = countRemainingSteps(state);
1855
- lines.push("");
1856
- if (remaining === 0) {
1857
- lines.push(
1858
- done(
1859
- paint(
1860
- ANSI.bold,
1861
- "All setup steps complete. Commit .patchstackrc.json, package.json, and the file carrying the widget snippet."
1862
- )
1863
- )
1864
- );
1865
- if (state.claimUrl !== null) {
1866
- lines.push(detail("The only manual action left is opening the dashboard link above (if not already connected)."));
1867
- }
1868
- } else {
1869
- lines.push(
1870
- ` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
1871
- );
1674
+ if (s === original) {
1675
+ log("start.ts already wired");
1676
+ return false;
1872
1677
  }
1873
- return lines.join("\n");
1678
+ writeFileSync2(p, s);
1679
+ log("patched start.ts (guard registered as request + function middleware)");
1680
+ return true;
1681
+ }
1682
+ function wire(cwd, opts) {
1683
+ const changed = scaffold(cwd, opts);
1684
+ if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
1685
+ if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
1686
+ if (patchStart(cwd)) changed.push("src/start.ts");
1687
+ log(
1688
+ opts.demo ? "done \u2014 guard wired with the demo sample rules (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only." : "done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only."
1689
+ );
1690
+ return { ok: true, changed: [...new Set(changed)] };
1874
1691
  }
1692
+ function verify(cwd) {
1693
+ const guardPath = join3(cwd, GUARD_FILE);
1694
+ const clientPath = join3(cwd, "src/integrations/supabase/client.ts");
1695
+ const startPath = join3(cwd, "src/start.ts");
1696
+ const guard = existsSync3(guardPath) ? read(guardPath) : "";
1697
+ const client = existsSync3(clientPath) ? read(clientPath) : "";
1698
+ const start = existsSync3(startPath) ? read(startPath) : "";
1699
+ const checks = [
1700
+ { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
1701
+ { label: "Supabase client tunnels through the guard", ok: client.includes("x-ps-target"), hint: "run `patchstack-connect protect` to re-patch src/integrations/supabase/client.ts" },
1702
+ { label: "request middleware defined + registered", ok: start.includes("const patchstackGuard =") && start.includes("requestMiddleware: [patchstackGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" },
1703
+ { label: "server-function middleware defined + registered", ok: start.includes("const patchstackFunctionGuard =") && start.includes("functionMiddleware: [patchstackFunctionGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" }
1704
+ ];
1705
+ return { wired: checks.every((c) => c.ok), checks };
1706
+ }
1707
+ var tanstackSupabaseAdapter = {
1708
+ name: "tanstack-supabase",
1709
+ label: "TanStack Start + Supabase",
1710
+ detect,
1711
+ wire,
1712
+ verify
1713
+ };
1875
1714
 
1876
- // src/protect/install/util.ts
1877
- import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
1878
- import { join as join2, dirname } from "path";
1879
- import { fileURLToPath } from "url";
1880
- var read = (p) => readFileSync3(p, "utf8");
1881
- var log = (msg) => console.log(`patchstack protect: ${msg}`);
1882
- function hasDependency(cwd, name) {
1715
+ // src/protect/install/adapters/next.ts
1716
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
1717
+ import { join as join4 } from "path";
1718
+ function hasNextDep(cwd) {
1883
1719
  try {
1884
- const pkg = JSON.parse(read(join2(cwd, "package.json")));
1885
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
1720
+ const pkg = JSON.parse(read(join4(cwd, "package.json")));
1721
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
1886
1722
  } catch {
1887
1723
  return false;
1888
1724
  }
1889
1725
  }
1890
- var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
1891
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1892
- function bakeSiteUuid(cwd, guardRelPath) {
1893
- const rc = join2(cwd, ".patchstackrc.json");
1894
- if (!existsSync4(rc)) {
1895
- log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1896
- return false;
1897
- }
1898
- let uuid;
1899
- try {
1900
- uuid = JSON.parse(read(rc)).siteUuid;
1901
- } catch {
1902
- log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
1903
- return false;
1904
- }
1905
- if (typeof uuid !== "string" || !UUID_RE.test(uuid)) {
1906
- log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1907
- return false;
1908
- }
1909
- const guardPath = join2(cwd, guardRelPath);
1910
- if (!existsSync4(guardPath)) return false;
1911
- const source = read(guardPath);
1912
- if (!source.includes(SITE_UUID_PLACEHOLDER)) {
1913
- log(`${guardRelPath} site UUID already baked`);
1914
- return false;
1726
+ function middlewareInfo(cwd) {
1727
+ const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
1728
+ for (const rel of candidates) {
1729
+ if (existsSync4(join4(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
1915
1730
  }
1916
- writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
1917
- log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
1918
- return true;
1731
+ const useSrc = existsSync4(join4(cwd, "src"));
1732
+ return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
1919
1733
  }
1920
- var HERE = dirname(fileURLToPath(import.meta.url));
1921
- function templatesDir() {
1922
- const builtLayout = join2(HERE, "protect", "templates");
1923
- const candidates = [
1924
- builtLayout,
1925
- join2(HERE, "..", "templates"),
1926
- // source: src/protect/install/ → src/protect/templates
1927
- join2(HERE, "templates")
1928
- ];
1929
- return candidates.find((p) => existsSync4(p)) ?? builtLayout;
1734
+ function detect2(cwd) {
1735
+ return hasNextDep(cwd);
1930
1736
  }
1931
-
1932
- // src/protect/install/adapters/tanstack-supabase.ts
1933
- import { writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync, copyFileSync } from "fs";
1934
- import { join as join3 } from "path";
1935
- var CLIENT_TUNNEL = [
1936
- "",
1937
- " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
1938
- " // (same-origin) so payloads are inspected before they reach Supabase.",
1939
- " if (typeof window !== 'undefined') {",
1940
- " const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
1941
- " const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
1942
- " headers.set('x-ps-target', target);",
1943
- " const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
1944
- " return fetch(guardUrl, { ...init, method, headers });",
1945
- " }",
1946
- ""
1947
- ].join("\n");
1948
- var GUARD_IMPORT = 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse, guardRequest } from "@/integrations/patchstack/guard";';
1949
- var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard";/;
1950
- var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
1951
- var REQUEST_MIDDLEWARE_BLOCK = [
1952
- "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
1953
- "// Browser\u2192Supabase tunnel + response screening; optional route WAF via PATCHSTACK_ROUTE_WAF=1.",
1954
- "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1955
- " const request = getRequest();",
1956
- " if (request) {",
1957
- " const { pathname } = new URL(request.url);",
1958
- " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
1959
- ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
1960
- " const blocked = await guardRequest(request);",
1961
- " if (blocked) return blocked;",
1962
- " }",
1963
- " }",
1964
- " return screenResponse(await next());",
1965
- "});",
1966
- "// #endregion patchstack-guard"
1967
- ].join("\n");
1968
- var FUNCTION_MIDDLEWARE_BLOCK = [
1969
- "// #region patchstack-function-guard (managed by patchstack-connect protect \u2014 do not edit)",
1970
- "// Inspect server-function args before they reach the database.",
1971
- 'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1972
- " const blocked = await inspectServerFn(data);",
1973
- " if (blocked) throw new Error(blocked.message);",
1974
- " return next();",
1975
- "});",
1976
- "// #endregion patchstack-function-guard"
1977
- ].join("\n");
1978
- function reconcileBlock(s, region, block, legacyConst, insertBefore) {
1979
- const lines = s.split("\n");
1980
- const startMarker = `// #region ${region} `;
1981
- const endMarker = `// #endregion ${region}`;
1982
- const si = lines.findIndex((l) => l.includes(startMarker));
1983
- if (si !== -1) {
1984
- const ei = lines.findIndex((l, i) => i > si && l.trim() === endMarker);
1985
- if (ei !== -1) {
1986
- lines.splice(si, ei - si + 1, ...block.split("\n"));
1987
- return lines.join("\n");
1988
- }
1737
+ function rulesFile(relDir) {
1738
+ return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
1739
+ }
1740
+ function wire2(cwd, opts) {
1741
+ const templates = templatesDir();
1742
+ const mw = middlewareInfo(cwd);
1743
+ const dir = join4(cwd, mw.relDir === "." ? "" : mw.relDir);
1744
+ mkdirSync2(dir, { recursive: true });
1745
+ const rulesDst = join4(dir, "patchstack.rules.json");
1746
+ const changed = [];
1747
+ if (opts.demo || !existsSync4(rulesDst)) {
1748
+ copyFileSync2(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1749
+ changed.push(rulesFile(mw.relDir));
1750
+ }
1751
+ const mwPath = join4(cwd, mw.relFile);
1752
+ const existing = mw.exists ? read(mwPath) : "";
1753
+ if (mw.exists && existing.includes("patchstack-next")) {
1754
+ log(`${mw.relFile} already has the Patchstack middleware \u2014 left as-is`);
1755
+ return { ok: true, changed };
1756
+ }
1757
+ if (mw.exists) {
1758
+ log(
1759
+ `existing ${mw.relFile} left untouched \u2014 scaffolded ${rulesFile(mw.relDir)}; add the guard to your middleware: import { createProtection } from "@patchstack/connect/protect", run fetchGuard() on the request, and return the block Response. Then: npx patchstack-connect protect --check`
1760
+ );
1761
+ return { ok: true, changed };
1989
1762
  }
1990
- const ci = lines.findIndex((l) => l.includes(legacyConst));
1991
- if (ci !== -1) {
1992
- const close = lines.findIndex((l, i) => i >= ci && l.trim() === "});");
1993
- if (close !== -1) {
1994
- let start = ci;
1995
- while (start > 0 && (lines[start - 1] ?? "").trim().startsWith("//")) start--;
1996
- lines.splice(start, close - start + 1, ...block.split("\n"));
1997
- return lines.join("\n");
1998
- }
1763
+ copyFileSync2(join4(templates, "next-middleware.ts"), mwPath);
1764
+ if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
1765
+ changed.push(mw.relFile);
1766
+ log(`scaffolded ${mw.relFile} (Patchstack middleware)`);
1767
+ return { ok: true, changed: [...new Set(changed)] };
1768
+ }
1769
+ function verify2(cwd) {
1770
+ const mw = middlewareInfo(cwd);
1771
+ const present = mw.exists && read(join4(cwd, mw.relFile)).includes("patchstack-next");
1772
+ const rulesPresent = existsSync4(join4(cwd, rulesFile(mw.relDir)));
1773
+ return {
1774
+ wired: present && rulesPresent,
1775
+ checks: [
1776
+ { label: "Patchstack middleware present", ok: present, hint: `run \`patchstack-connect protect\` (writes ${mw.relFile})` },
1777
+ { label: "rules co-located with the middleware", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
1778
+ ]
1779
+ };
1780
+ }
1781
+ var nextAdapter = {
1782
+ name: "nextjs",
1783
+ label: "Next.js",
1784
+ detect: detect2,
1785
+ wire: wire2,
1786
+ verify: verify2
1787
+ };
1788
+
1789
+ // src/protect/install/adapters/sveltekit.ts
1790
+ import { join as join6 } from "path";
1791
+
1792
+ // src/protect/install/seam.ts
1793
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
1794
+ import { join as join5, dirname as dirname2 } from "path";
1795
+ function rulesRel(seamRel) {
1796
+ const d = dirname2(seamRel);
1797
+ return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
1798
+ }
1799
+ function wireSeam(cwd, opts, spec) {
1800
+ const templates = templatesDir();
1801
+ const existing = spec.candidates.find((c) => existsSync5(join5(cwd, c)));
1802
+ const seamRel = existing ?? spec.target;
1803
+ mkdirSync3(dirname2(join5(cwd, seamRel)), { recursive: true });
1804
+ const rulesDst = join5(cwd, rulesRel(seamRel));
1805
+ const changed = [];
1806
+ if (opts.demo || !existsSync5(rulesDst)) {
1807
+ copyFileSync3(join5(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1808
+ changed.push(rulesRel(seamRel));
1999
1809
  }
2000
- return s.replace(insertBefore, block + "\n\n" + insertBefore);
1810
+ const current = existing ? read(join5(cwd, existing)) : "";
1811
+ if (existing && current.includes(spec.marker)) {
1812
+ log(`${existing} already has the Patchstack guard \u2014 left as-is`);
1813
+ return { ok: true, changed };
1814
+ }
1815
+ if (existing) {
1816
+ log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
1817
+ return { ok: true, changed };
1818
+ }
1819
+ copyFileSync3(join5(templates, spec.templateName), join5(cwd, seamRel));
1820
+ if (!opts.demo) bakeSiteUuid(cwd, seamRel);
1821
+ changed.push(seamRel);
1822
+ log(`scaffolded ${seamRel}`);
1823
+ return { ok: true, changed: [...new Set(changed)] };
2001
1824
  }
2002
- function detect(cwd) {
2003
- const pkgPath = join3(cwd, "package.json");
2004
- if (!existsSync5(pkgPath)) return false;
2005
- let pkg;
1825
+ function verifySeam(cwd, spec) {
1826
+ const existing = spec.candidates.find((c) => existsSync5(join5(cwd, c)));
1827
+ const seamRel = existing ?? spec.target;
1828
+ const present = existing ? read(join5(cwd, existing)).includes(spec.marker) : false;
1829
+ const rulesPresent = existsSync5(join5(cwd, rulesRel(seamRel)));
1830
+ return {
1831
+ wired: present && rulesPresent,
1832
+ checks: [
1833
+ { label: `${spec.seamLabel} present`, ok: present, hint: `run \`patchstack-connect protect\` (writes ${spec.target})` },
1834
+ { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
1835
+ ]
1836
+ };
1837
+ }
1838
+
1839
+ // src/protect/install/adapters/sveltekit.ts
1840
+ var SPEC = {
1841
+ templateName: "sveltekit-hooks.ts",
1842
+ candidates: ["src/hooks.server.ts", "src/hooks.server.js"],
1843
+ target: "src/hooks.server.ts",
1844
+ marker: "patchstack-sveltekit",
1845
+ planHint: 'add the guard to your `handle` (compose with `sequence()` from "@sveltejs/kit/hooks"), then: npx patchstack-connect protect --check',
1846
+ seamLabel: "SvelteKit server hook"
1847
+ };
1848
+ function detect3(cwd) {
2006
1849
  try {
2007
- pkg = JSON.parse(read(pkgPath));
1850
+ const pkg = JSON.parse(read(join6(cwd, "package.json")));
1851
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
2008
1852
  } catch {
2009
1853
  return false;
2010
1854
  }
2011
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
2012
- return Boolean(deps["@tanstack/react-start"]) && existsSync5(join3(cwd, "src/start.ts")) && existsSync5(join3(cwd, "src/integrations/supabase/client.ts"));
2013
1855
  }
2014
- var GUARD_FILE = "src/integrations/patchstack/guard.ts";
2015
- function scaffold(cwd, opts) {
2016
- const templates = templatesDir();
2017
- const dst = join3(cwd, "src/integrations/patchstack");
2018
- mkdirSync(dst, { recursive: true });
2019
- copyFileSync(join3(templates, "guard.ts"), join3(dst, "guard.ts"));
2020
- const changed = [GUARD_FILE];
2021
- const rulesDst = join3(dst, "rules.json");
2022
- if (opts.demo) {
2023
- copyFileSync(join3(templates, "demo-rules.json"), rulesDst);
2024
- changed.push("src/integrations/patchstack/rules.json");
2025
- log("scaffolded guard.ts + rules.json (demo sample rule set)");
2026
- } else if (!existsSync5(rulesDst)) {
2027
- copyFileSync(join3(templates, "rules.json"), rulesDst);
2028
- changed.push("src/integrations/patchstack/rules.json");
2029
- log("scaffolded guard.ts + rules.json (starter rules)");
2030
- } else {
2031
- log("scaffolded guard.ts (kept existing rules.json)");
2032
- }
2033
- return changed;
1856
+ function wire3(cwd, opts) {
1857
+ return wireSeam(cwd, opts, SPEC);
2034
1858
  }
2035
- function patchClient(cwd) {
2036
- const p = join3(cwd, "src/integrations/supabase/client.ts");
2037
- const s = read(p);
2038
- if (s.includes("x-ps-target")) {
2039
- log("client.ts already wired");
2040
- return false;
2041
- }
2042
- const anchor = "headers.set('apikey', supabaseKey);";
2043
- if (!s.includes(anchor)) {
2044
- log("client.ts anchor not found \u2014 skipping (template changed?)");
2045
- return false;
2046
- }
2047
- writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
2048
- log("patched client.ts (tunnel Supabase through the guard)");
2049
- return true;
1859
+ function verify3(cwd) {
1860
+ return verifySeam(cwd, SPEC);
2050
1861
  }
2051
- function patchStart(cwd) {
2052
- const p = join3(cwd, "src/start.ts");
2053
- let s = read(p);
2054
- const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
2055
- const exportAnchor = "export const startInstance";
2056
- const rmAnchor = "requestMiddleware: [";
2057
- if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
2058
- log("start.ts anchors not found \u2014 skipping (template changed?)");
1862
+ var sveltekitAdapter = {
1863
+ name: "sveltekit",
1864
+ label: "SvelteKit",
1865
+ detect: detect3,
1866
+ wire: wire3,
1867
+ verify: verify3
1868
+ };
1869
+
1870
+ // src/protect/install/adapters/astro.ts
1871
+ import { join as join7 } from "path";
1872
+ var SPEC2 = {
1873
+ templateName: "astro-middleware.ts",
1874
+ candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
1875
+ target: "src/middleware.ts",
1876
+ marker: "patchstack-astro",
1877
+ planHint: 'add the guard to your `onRequest` (compose with `sequence()` from "astro:middleware"), then: npx patchstack-connect protect --check',
1878
+ seamLabel: "Astro middleware"
1879
+ };
1880
+ function detect4(cwd) {
1881
+ try {
1882
+ const pkg = JSON.parse(read(join7(cwd, "package.json")));
1883
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
1884
+ } catch {
2059
1885
  return false;
2060
1886
  }
2061
- const original = s;
2062
- if (GUARD_IMPORT_RE.test(s)) {
2063
- s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
2064
- } else {
2065
- s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
2066
- }
2067
- s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
2068
- s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
2069
- if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
2070
- s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
2071
- }
2072
- if (!s.includes("functionMiddleware: [patchstackFunctionGuard")) {
2073
- const fmAnchor = "functionMiddleware: [";
2074
- if (s.includes(fmAnchor)) {
2075
- s = s.replace(fmAnchor, fmAnchor + "patchstackFunctionGuard, ");
2076
- } else if (s.includes(rmAnchor)) {
2077
- s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1887
+ }
1888
+ function wire4(cwd, opts) {
1889
+ return wireSeam(cwd, opts, SPEC2);
1890
+ }
1891
+ function verify4(cwd) {
1892
+ return verifySeam(cwd, SPEC2);
1893
+ }
1894
+ var astroAdapter = {
1895
+ name: "astro",
1896
+ label: "Astro",
1897
+ detect: detect4,
1898
+ wire: wire4,
1899
+ verify: verify4
1900
+ };
1901
+
1902
+ // src/protect/install/adapters/nuxt.ts
1903
+ var SPEC3 = {
1904
+ templateName: "nuxt-middleware.ts",
1905
+ candidates: ["server/middleware/patchstack.ts", "server/middleware/patchstack.js"],
1906
+ target: "server/middleware/patchstack.ts",
1907
+ marker: "patchstack-nuxt",
1908
+ planHint: "add a `server/middleware/` handler that runs the guard, then: npx patchstack-connect protect --check",
1909
+ seamLabel: "Nuxt server middleware"
1910
+ };
1911
+ function detect5(cwd) {
1912
+ return hasDependency(cwd, "nuxt");
1913
+ }
1914
+ function wire5(cwd, opts) {
1915
+ return wireSeam(cwd, opts, SPEC3);
1916
+ }
1917
+ function verify5(cwd) {
1918
+ return verifySeam(cwd, SPEC3);
1919
+ }
1920
+ var nuxtAdapter = {
1921
+ name: "nuxt",
1922
+ label: "Nuxt",
1923
+ detect: detect5,
1924
+ wire: wire5,
1925
+ verify: verify5
1926
+ };
1927
+
1928
+ // src/protect/install/find-app.ts
1929
+ import { readFileSync as readFileSync2, existsSync as existsSync6, readdirSync as readdirSync2, lstatSync } from "fs";
1930
+ import { join as join8, relative, dirname as dirname3 } from "path";
1931
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1932
+ "node_modules",
1933
+ "patchstack",
1934
+ "dist",
1935
+ "build",
1936
+ "out",
1937
+ "coverage",
1938
+ ".next",
1939
+ ".output",
1940
+ ".svelte-kit",
1941
+ "test",
1942
+ "tests",
1943
+ "__tests__",
1944
+ "e2e",
1945
+ "examples",
1946
+ "fixtures",
1947
+ "__fixtures__"
1948
+ ]);
1949
+ var MAX_DEPTH = 8;
1950
+ function findAppInstance(cwd, re) {
1951
+ const root = existsSync6(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
1952
+ let hit = null;
1953
+ const walk2 = (dir, depth) => {
1954
+ if (hit || depth > MAX_DEPTH) return;
1955
+ let entries;
1956
+ try {
1957
+ entries = readdirSync2(dir).sort();
1958
+ } catch {
1959
+ return;
2078
1960
  }
2079
- }
2080
- if (s === original) {
2081
- log("start.ts already wired");
2082
- return false;
2083
- }
2084
- writeFileSync3(p, s);
2085
- log("patched start.ts (guard registered as request + function middleware)");
2086
- return true;
1961
+ for (const name of entries) {
1962
+ if (hit) return;
1963
+ if (SKIP_DIRS.has(name) || name.startsWith(".")) continue;
1964
+ const p = join8(dir, name);
1965
+ let st;
1966
+ try {
1967
+ st = lstatSync(p);
1968
+ } catch {
1969
+ continue;
1970
+ }
1971
+ if (st.isSymbolicLink()) continue;
1972
+ if (st.isDirectory()) walk2(p, depth + 1);
1973
+ else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
1974
+ let src;
1975
+ try {
1976
+ src = readFileSync2(p, "utf8");
1977
+ } catch {
1978
+ continue;
1979
+ }
1980
+ const m = re.exec(src);
1981
+ if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, "/"), appVar: m[1] };
1982
+ }
1983
+ }
1984
+ };
1985
+ walk2(root, 0);
1986
+ return hit;
1987
+ }
1988
+ function importSpecifier(fromRel, toRel, preserveExtension = false) {
1989
+ let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
1990
+ if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
1991
+ if (!spec.startsWith(".")) spec = `./${spec}`;
1992
+ return spec;
1993
+ }
1994
+
1995
+ // src/protect/install/register.ts
1996
+ import { writeFileSync as writeFileSync4, existsSync as existsSync8 } from "fs";
1997
+ import { join as join10 } from "path";
1998
+
1999
+ // src/protect/install/generic.ts
2000
+ import { readFileSync as readFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
2001
+ import { join as join9 } from "path";
2002
+ var GUARD_MARKER = "patchstack/guard";
2003
+ function genericDir(cwd) {
2004
+ return existsSync7(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2087
2005
  }
2088
- function wire(cwd, opts) {
2089
- const changed = scaffold(cwd, opts);
2090
- if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
2091
- if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
2092
- if (patchStart(cwd)) changed.push("src/start.ts");
2093
- log(
2094
- opts.demo ? "done \u2014 guard wired with the demo sample rules (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only." : "done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only."
2095
- );
2096
- return { ok: true, changed: [...new Set(changed)] };
2006
+ function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts", guardFile = "guard.ts") {
2007
+ const templates = templatesDir();
2008
+ const dir = genericDir(cwd);
2009
+ const dst = join9(cwd, dir);
2010
+ mkdirSync4(dst, { recursive: true });
2011
+ copyFileSync4(join9(templates, guardTemplate), join9(dst, guardFile));
2012
+ const guardRel = `${dir}/${guardFile}`;
2013
+ const changed = [guardRel];
2014
+ if (!opts.demo) bakeSiteUuid(cwd, guardRel);
2015
+ const rulesDst = join9(dst, "rules.json");
2016
+ if (opts.demo || !existsSync7(rulesDst)) {
2017
+ copyFileSync4(join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2018
+ changed.push(`${dir}/rules.json`);
2019
+ }
2020
+ return { changed, dir };
2097
2021
  }
2098
- function verify(cwd) {
2099
- const guardPath = join3(cwd, GUARD_FILE);
2100
- const clientPath = join3(cwd, "src/integrations/supabase/client.ts");
2101
- const startPath = join3(cwd, "src/start.ts");
2102
- const guard = existsSync5(guardPath) ? read(guardPath) : "";
2103
- const client = existsSync5(clientPath) ? read(clientPath) : "";
2104
- const start = existsSync5(startPath) ? read(startPath) : "";
2105
- const checks = [
2106
- { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
2107
- { label: "Supabase client tunnels through the guard", ok: client.includes("x-ps-target"), hint: "run `patchstack-connect protect` to re-patch src/integrations/supabase/client.ts" },
2108
- { label: "request middleware defined + registered", ok: start.includes("const patchstackGuard =") && start.includes("requestMiddleware: [patchstackGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" },
2109
- { label: "server-function middleware defined + registered", ok: start.includes("const patchstackFunctionGuard =") && start.includes("functionMiddleware: [patchstackFunctionGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" }
2022
+ function candidateEntries(cwd) {
2023
+ const names = [
2024
+ "src/server.ts",
2025
+ "src/server.js",
2026
+ "src/index.ts",
2027
+ "src/index.js",
2028
+ "src/app.ts",
2029
+ "src/app.js",
2030
+ "src/main.ts",
2031
+ "server.ts",
2032
+ "server.js",
2033
+ "index.ts",
2034
+ "index.js",
2035
+ "app.ts",
2036
+ "app.js"
2110
2037
  ];
2111
- return { wired: checks.every((c) => c.ok), checks };
2112
- }
2113
- var tanstackSupabaseAdapter = {
2114
- name: "tanstack-supabase",
2115
- label: "TanStack Start + Supabase",
2116
- detect,
2117
- wire,
2118
- verify
2119
- };
2120
-
2121
- // src/protect/install/adapters/next.ts
2122
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
2123
- import { join as join4 } from "path";
2124
- function hasNextDep(cwd) {
2038
+ const hits = names.filter((n) => existsSync7(join9(cwd, n)));
2125
2039
  try {
2126
- const pkg = JSON.parse(read(join4(cwd, "package.json")));
2127
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
2040
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
2041
+ if (typeof pkg.main === "string" && existsSync7(join9(cwd, pkg.main))) hits.push(pkg.main);
2128
2042
  } catch {
2129
- return false;
2130
2043
  }
2044
+ return [...new Set(hits)];
2131
2045
  }
2132
- function middlewareInfo(cwd) {
2133
- const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
2134
- for (const rel of candidates) {
2135
- if (existsSync6(join4(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
2046
+ function usesExpress(cwd) {
2047
+ try {
2048
+ const pkg = JSON.parse(read(join9(cwd, "package.json")));
2049
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2050
+ } catch {
2051
+ return false;
2136
2052
  }
2137
- const useSrc = existsSync6(join4(cwd, "src"));
2138
- return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
2139
- }
2140
- function detect2(cwd) {
2141
- return hasNextDep(cwd);
2142
2053
  }
2143
- function rulesFile(relDir) {
2144
- return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
2145
- }
2146
- function wire2(cwd, opts) {
2147
- const templates = templatesDir();
2148
- const mw = middlewareInfo(cwd);
2149
- const dir = join4(cwd, mw.relDir === "." ? "" : mw.relDir);
2150
- mkdirSync2(dir, { recursive: true });
2151
- const rulesDst = join4(dir, "patchstack.rules.json");
2152
- const changed = [];
2153
- if (opts.demo || !existsSync6(rulesDst)) {
2154
- copyFileSync2(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2155
- changed.push(rulesFile(mw.relDir));
2156
- }
2157
- const mwPath = join4(cwd, mw.relFile);
2158
- const existing = mw.exists ? read(mwPath) : "";
2159
- if (mw.exists && !existing.includes("patchstack-next")) {
2160
- log(
2161
- `existing ${mw.relFile} left untouched \u2014 scaffolded ${rulesFile(mw.relDir)}; add the guard to your middleware: import { createProtection } from "@patchstack/connect/protect", run fetchGuard() on the request, and return the block Response. Then: npx patchstack-connect protect --check`
2162
- );
2163
- return { ok: true, changed };
2164
- }
2165
- copyFileSync2(join4(templates, "next-middleware.ts"), mwPath);
2166
- if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
2167
- changed.push(mw.relFile);
2168
- log(mw.exists ? `refreshed ${mw.relFile} (Patchstack middleware)` : `scaffolded ${mw.relFile} (Patchstack middleware)`);
2169
- return { ok: true, changed: [...new Set(changed)] };
2054
+ function wiringPlan(cwd, dir) {
2055
+ const entries = candidateEntries(cwd);
2056
+ const express = usesExpress(cwd);
2057
+ const lines = [
2058
+ `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/guard.ts + ${dir}/rules.json.`,
2059
+ "Finish by wiring it into your server (pick the one that fits):",
2060
+ ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/guard"`,
2061
+ ` \u2022 Node / Express: app.use(patchstackMiddleware) // add before your routes`,
2062
+ entries.length ? `Likely server ${entries.length === 1 ? "entry" : "entries"}: ${entries.join(", ")}${express ? " (Express detected)" : ""}.` : "Could not locate a server entry \u2014 wire it wherever requests enter your app.",
2063
+ "Then confirm it is hooked up: npx patchstack-connect protect --check"
2064
+ ];
2065
+ return lines.join("\n");
2170
2066
  }
2171
- function verify2(cwd) {
2172
- const mw = middlewareInfo(cwd);
2173
- const present = mw.exists && read(join4(cwd, mw.relFile)).includes("patchstack-next");
2174
- const rulesPresent = existsSync6(join4(cwd, rulesFile(mw.relDir)));
2067
+ function genericVerify(cwd) {
2068
+ const dir = genericDir(cwd);
2069
+ const scaffolded = existsSync7(join9(cwd, dir, "guard.ts"));
2070
+ const imported = scaffolded && guardIsImported(cwd, join9(cwd, dir, "guard.ts"));
2175
2071
  return {
2176
- wired: present && rulesPresent,
2072
+ wired: scaffolded && imported,
2177
2073
  checks: [
2178
- { label: "Patchstack middleware present", ok: present, hint: `run \`patchstack-connect protect\` (writes ${mw.relFile})` },
2179
- { label: "rules co-located with the middleware", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
2074
+ { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
2075
+ {
2076
+ label: "guard imported into a server entry",
2077
+ ok: imported,
2078
+ hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/guard" and wire it into your request path`
2079
+ }
2180
2080
  ]
2181
2081
  };
2182
2082
  }
2183
- var nextAdapter = {
2184
- name: "nextjs",
2185
- label: "Next.js",
2186
- detect: detect2,
2187
- wire: wire2,
2188
- verify: verify2
2189
- };
2190
-
2191
- // src/protect/install/adapters/sveltekit.ts
2192
- import { join as join6 } from "path";
2083
+ function guardIsImported(cwd, guardPath) {
2084
+ const root = existsSync7(join9(cwd, "src")) ? join9(cwd, "src") : cwd;
2085
+ let found = false;
2086
+ const walk2 = (d, depth) => {
2087
+ if (found || depth > 8) return;
2088
+ let entries;
2089
+ try {
2090
+ entries = readdirSync3(d);
2091
+ } catch {
2092
+ return;
2093
+ }
2094
+ for (const name of entries) {
2095
+ if (found) return;
2096
+ if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2097
+ const p = join9(d, name);
2098
+ let st;
2099
+ try {
2100
+ st = lstatSync2(p);
2101
+ } catch {
2102
+ continue;
2103
+ }
2104
+ if (st.isSymbolicLink()) continue;
2105
+ if (st.isDirectory()) walk2(p, depth + 1);
2106
+ else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
2107
+ try {
2108
+ if (readFileSync3(p, "utf8").includes(GUARD_MARKER)) found = true;
2109
+ } catch {
2110
+ }
2111
+ }
2112
+ }
2113
+ };
2114
+ walk2(root, 0);
2115
+ return found;
2116
+ }
2193
2117
 
2194
- // src/protect/install/seam.ts
2195
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
2196
- import { join as join5, dirname as dirname2 } from "path";
2197
- function rulesRel(seamRel) {
2198
- const d = dirname2(seamRel);
2199
- return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
2118
+ // src/protect/install/register.ts
2119
+ var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
2120
+ function guardTarget(cwd, entryRel, spec) {
2121
+ if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
2122
+ if (spec.guardTemplateCjs) {
2123
+ return {
2124
+ template: spec.guardTemplateCjs,
2125
+ file: "guard.cjs",
2126
+ importLine: (name, target) => `const { ${name} } = require("${target}");`,
2127
+ preserveExtension: true
2128
+ };
2129
+ }
2130
+ } else if (/\.(?:js|mjs)$/.test(entryRel) && spec.guardTemplateEsm) {
2131
+ return {
2132
+ template: spec.guardTemplateEsm,
2133
+ file: entryRel.endsWith(".mjs") ? "guard.mjs" : "guard.js",
2134
+ importLine: (name, target) => `import { ${name} } from "${target}";`,
2135
+ preserveExtension: true
2136
+ };
2137
+ }
2138
+ return {
2139
+ template: spec.guardTemplate,
2140
+ file: "guard.ts",
2141
+ importLine: (name, target) => `import { ${name} } from "${target}";`,
2142
+ preserveExtension: false
2143
+ };
2200
2144
  }
2201
- function wireSeam(cwd, opts, spec) {
2202
- const templates = templatesDir();
2203
- const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
2204
- const seamRel = existing ?? spec.target;
2205
- mkdirSync3(dirname2(join5(cwd, seamRel)), { recursive: true });
2206
- const rulesDst = join5(cwd, rulesRel(seamRel));
2207
- const changed = [];
2208
- if (opts.demo || !existsSync7(rulesDst)) {
2209
- copyFileSync3(join5(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2210
- changed.push(rulesRel(seamRel));
2145
+ function packageType(cwd) {
2146
+ try {
2147
+ return JSON.parse(read(join10(cwd, "package.json"))).type;
2148
+ } catch {
2149
+ return void 0;
2211
2150
  }
2212
- const current = existing ? read(join5(cwd, existing)) : "";
2213
- if (existing && !current.includes(spec.marker)) {
2214
- log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
2151
+ }
2152
+ function wireRegister(cwd, opts, spec) {
2153
+ const entry = findAppInstance(cwd, spec.appRe);
2154
+ if (!entry) {
2155
+ const { changed: changed2 } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2156
+ log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2157
+ return { ok: true, changed: changed2 };
2158
+ }
2159
+ const target = guardTarget(cwd, entry.relPath, spec);
2160
+ const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
2161
+ const p = join10(cwd, entry.relPath);
2162
+ const s = read(p);
2163
+ if (s.includes(spec.importName)) {
2164
+ log(`${entry.relPath} already wired`);
2215
2165
  return { ok: true, changed };
2216
2166
  }
2217
- copyFileSync3(join5(templates, spec.templateName), join5(cwd, seamRel));
2218
- if (!opts.demo) bakeSiteUuid(cwd, seamRel);
2219
- changed.push(seamRel);
2220
- log(existing ? `refreshed ${seamRel}` : `scaffolded ${seamRel}`);
2167
+ const sourceLines = s.split("\n");
2168
+ const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
2169
+ const requiredAnchor = spec.callAfter?.(entry.appVar);
2170
+ if (spec.requireCallAfter && requiredAnchor && !sourceLines.some((line, index) => index > sourceAppIdx && requiredAnchor.test(line))) {
2171
+ log(`${entry.relPath} body-parser anchor not found \u2014 scaffolded guard; ${spec.manualHint}`);
2172
+ return { ok: true, changed };
2173
+ }
2174
+ const importLine = target.importLine(
2175
+ spec.importName,
2176
+ importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
2177
+ );
2178
+ const lines = s.split("\n");
2179
+ let lastImport = -1;
2180
+ for (let i = 0; i < lines.length; i++) {
2181
+ if (/^\s*(?:import\b|(?:const|let|var)\s+.+?=\s*require\()/.test(lines[i] ?? "")) lastImport = i;
2182
+ }
2183
+ lines.splice(lastImport + 1, 0, importLine);
2184
+ const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2185
+ if (appIdx !== -1) {
2186
+ const preferred = spec.callAfter?.(entry.appVar);
2187
+ const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
2188
+ const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
2189
+ lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2190
+ }
2191
+ writeFileSync4(p, lines.join("\n"));
2192
+ changed.push(entry.relPath);
2193
+ log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
2221
2194
  return { ok: true, changed: [...new Set(changed)] };
2222
2195
  }
2223
- function verifySeam(cwd, spec) {
2224
- const existing = spec.candidates.find((c) => existsSync7(join5(cwd, c)));
2225
- const seamRel = existing ?? spec.target;
2226
- const present = existing ? read(join5(cwd, existing)).includes(spec.marker) : false;
2227
- const rulesPresent = existsSync7(join5(cwd, rulesRel(seamRel)));
2196
+ function verifyRegister(cwd, spec) {
2197
+ const dir = existsSync8(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
2198
+ const entry = findAppInstance(cwd, spec.appRe);
2199
+ const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
2200
+ const scaffolded = target ? existsSync8(join10(cwd, dir, target.file)) : false;
2201
+ const entrySource = entry ? read(join10(cwd, entry.relPath)) : "";
2202
+ const entryLines = entrySource.split("\n");
2203
+ const anchor = entry && spec.callAfter ? spec.callAfter(entry.appVar) : null;
2204
+ const anchorIndex = anchor ? entryLines.findIndex((line) => anchor.test(line)) : -1;
2205
+ const callIndex = entry ? entryLines.findIndex((line) => line.includes(spec.call(entry.appVar))) : -1;
2206
+ const ordered = anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1;
2207
+ const wired = entrySource.includes(spec.importName) && ordered;
2228
2208
  return {
2229
- wired: present && rulesPresent,
2209
+ wired: scaffolded && wired,
2230
2210
  checks: [
2231
- { label: `${spec.seamLabel} present`, ok: present, hint: `run \`patchstack-connect protect\` (writes ${spec.target})` },
2232
- { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
2211
+ { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
2212
+ { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
2233
2213
  ]
2234
2214
  };
2235
2215
  }
2236
2216
 
2237
- // src/protect/install/adapters/sveltekit.ts
2238
- var SPEC = {
2239
- templateName: "sveltekit-hooks.ts",
2240
- candidates: ["src/hooks.server.ts", "src/hooks.server.js"],
2241
- target: "src/hooks.server.ts",
2242
- marker: "patchstack-sveltekit",
2243
- planHint: 'add the guard to your `handle` (compose with `sequence()` from "@sveltejs/kit/hooks"), then: npx patchstack-connect protect --check',
2244
- seamLabel: "SvelteKit server hook"
2217
+ // src/protect/install/adapters/nestjs.ts
2218
+ var SPEC4 = {
2219
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+NestFactory\.create/,
2220
+ guardTemplate: "generic-guard.ts",
2221
+ guardTemplateEsm: "generic-guard.js",
2222
+ guardTemplateCjs: "generic-guard.cjs",
2223
+ importName: "patchstackMiddleware",
2224
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2225
+ label: "NestJS app",
2226
+ manualHint: "add `app.use(patchstackMiddleware)` right after your NestFactory.create(...) call"
2245
2227
  };
2246
- function detect3(cwd) {
2228
+ var nestjsAdapter = {
2229
+ name: "nestjs",
2230
+ label: "NestJS",
2231
+ detect: (cwd) => hasDependency(cwd, "@nestjs/core") && findAppInstance(cwd, SPEC4.appRe) !== null,
2232
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC4),
2233
+ verify: (cwd) => verifyRegister(cwd, SPEC4)
2234
+ };
2235
+
2236
+ // src/protect/install/adapters/fastify.ts
2237
+ var SPEC5 = {
2238
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:fastify|Fastify)\(/,
2239
+ guardTemplate: "fastify-plugin.ts",
2240
+ guardTemplateEsm: "fastify-plugin.js",
2241
+ guardTemplateCjs: "fastify-plugin.cjs",
2242
+ importName: "patchstackFastify",
2243
+ call: (v) => `${v}.register(patchstackFastify);`,
2244
+ label: "Fastify app",
2245
+ manualHint: "add `app.register(patchstackFastify)` right after you create your fastify() app"
2246
+ };
2247
+ var fastifyAdapter = {
2248
+ name: "fastify",
2249
+ label: "Fastify (Node)",
2250
+ detect: (cwd) => hasDependency(cwd, "fastify") && findAppInstance(cwd, SPEC5.appRe) !== null,
2251
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC5),
2252
+ verify: (cwd) => verifyRegister(cwd, SPEC5)
2253
+ };
2254
+
2255
+ // src/protect/install/adapters/express.ts
2256
+ var bodyParserRe = (appVar) => new RegExp(`^\\s*${appVar}\\.use\\(\\s*(?:[A-Za-z_$][\\w$]*\\.)?(?:json|urlencoded)\\(`, "m");
2257
+ var SPEC6 = {
2258
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/,
2259
+ guardTemplate: "express-guard.ts",
2260
+ guardTemplateEsm: "express-guard.js",
2261
+ guardTemplateCjs: "express-guard.cjs",
2262
+ importName: "patchstackMiddleware",
2263
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2264
+ callAfter: bodyParserRe,
2265
+ requireCallAfter: true,
2266
+ label: "Express app",
2267
+ manualHint: "add `app.use(patchstackMiddleware)` after your body parser (express.json/urlencoded or body-parser) and before the routes"
2268
+ };
2269
+ var expressAdapter = {
2270
+ name: "express",
2271
+ label: "Express (Node)",
2272
+ detect: (cwd) => hasDependency(cwd, "express") && findAppInstance(cwd, SPEC6.appRe) !== null,
2273
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC6),
2274
+ verify: (cwd) => verifyRegister(cwd, SPEC6)
2275
+ };
2276
+
2277
+ // src/protect/install/index.ts
2278
+ var ADAPTERS = [
2279
+ tanstackSupabaseAdapter,
2280
+ nextAdapter,
2281
+ sveltekitAdapter,
2282
+ astroAdapter,
2283
+ nuxtAdapter,
2284
+ nestjsAdapter,
2285
+ fastifyAdapter,
2286
+ expressAdapter
2287
+ ];
2288
+ function runProtect(cwd, opts = {}) {
2289
+ let adapter;
2247
2290
  try {
2248
- const pkg = JSON.parse(read(join6(cwd, "package.json")));
2249
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
2250
- } catch {
2251
- return false;
2291
+ adapter = ADAPTERS.find((a) => a.detect(cwd));
2292
+ if (adapter) {
2293
+ const result = adapter.wire(cwd, opts);
2294
+ return { status: "wired", adapter: adapter.name, changed: result.changed };
2295
+ }
2296
+ } catch (err) {
2297
+ log(`adapter ${adapter?.name ?? ""} failed (${err?.message ?? err}); falling back to a generic guard`);
2298
+ }
2299
+ try {
2300
+ const { changed, dir } = scaffoldGeneric(cwd, opts);
2301
+ const plan = wiringPlan(cwd, dir);
2302
+ log(plan);
2303
+ return { status: "scaffolded", adapter: "generic", changed, plan };
2304
+ } catch (err) {
2305
+ log(`could not scaffold the guard (${err?.message ?? err})`);
2306
+ return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
2252
2307
  }
2253
2308
  }
2254
- function wire3(cwd, opts) {
2255
- return wireSeam(cwd, opts, SPEC);
2256
- }
2257
- function verify3(cwd) {
2258
- return verifySeam(cwd, SPEC);
2309
+ function runVerify(cwd) {
2310
+ try {
2311
+ const adapter = ADAPTERS.find((a) => a.detect(cwd));
2312
+ if (adapter) return { stack: adapter.label, ...adapter.verify(cwd) };
2313
+ return { stack: "generic", ...genericVerify(cwd) };
2314
+ } catch (err) {
2315
+ return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
2316
+ }
2259
2317
  }
2260
- var sveltekitAdapter = {
2261
- name: "sveltekit",
2262
- label: "SvelteKit",
2263
- detect: detect3,
2264
- wire: wire3,
2265
- verify: verify3
2266
- };
2267
2318
 
2268
- // src/protect/install/adapters/astro.ts
2269
- import { join as join7 } from "path";
2270
- var SPEC2 = {
2271
- templateName: "astro-middleware.ts",
2272
- candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
2273
- target: "src/middleware.ts",
2274
- marker: "patchstack-astro",
2275
- planHint: 'add the guard to your `onRequest` (compose with `sequence()` from "astro:middleware"), then: npx patchstack-connect protect --check',
2276
- seamLabel: "Astro middleware"
2277
- };
2278
- function detect4(cwd) {
2279
- try {
2280
- const pkg = JSON.parse(read(join7(cwd, "package.json")));
2281
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
2282
- } catch {
2283
- return false;
2319
+ // src/widget.ts
2320
+ import { existsSync as existsSync9, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2321
+ import path8 from "path";
2322
+ var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
2323
+ var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
2324
+ var WIDGET_NEEDLE = "patchstack-widget";
2325
+ var SOURCE_SHELL_CANDIDATES = ["index.html", "public/index.html", "src/app.html"];
2326
+ function buildWidgetTag(siteUuid) {
2327
+ return `<script src="${WIDGET_SCRIPT_URL}" data-site-uuid="${siteUuid}" defer ${WIDGET_MARKER_ATTR}="true"></script>`;
2328
+ }
2329
+ var MANAGED_TAG_RE = new RegExp(
2330
+ `<script[^>]*${WIDGET_MARKER_ATTR}[^>]*>\\s*</script>`,
2331
+ "i"
2332
+ );
2333
+ function ensureWidgetInHtml(html, siteUuid) {
2334
+ const tag = buildWidgetTag(siteUuid);
2335
+ const managed = html.match(MANAGED_TAG_RE);
2336
+ if (managed !== null) {
2337
+ if (managed[0].includes(`data-site-uuid="${siteUuid}"`)) {
2338
+ return { html, action: "unchanged" };
2339
+ }
2340
+ return { html: html.replace(MANAGED_TAG_RE, tag), action: "updated" };
2341
+ }
2342
+ if (html.includes(WIDGET_NEEDLE)) {
2343
+ return { html, action: "manual" };
2344
+ }
2345
+ const bodyClose = html.match(/([ \t]*)<\/body>/i);
2346
+ if (bodyClose === null || bodyClose.index === void 0) {
2347
+ return { html, action: "no-body" };
2284
2348
  }
2349
+ const indent = bodyClose[1] ?? "";
2350
+ const insertion = `${indent} ${tag}
2351
+ ${indent}</body>`;
2352
+ return {
2353
+ html: html.slice(0, bodyClose.index) + insertion + html.slice(bodyClose.index + bodyClose[0].length),
2354
+ action: "added"
2355
+ };
2285
2356
  }
2286
- function wire4(cwd, opts) {
2287
- return wireSeam(cwd, opts, SPEC2);
2357
+ function findSourceShell(cwd) {
2358
+ for (const candidate of SOURCE_SHELL_CANDIDATES) {
2359
+ if (existsSync9(path8.join(cwd, candidate))) {
2360
+ return candidate;
2361
+ }
2362
+ }
2363
+ return null;
2288
2364
  }
2289
- function verify4(cwd) {
2290
- return verifySeam(cwd, SPEC2);
2365
+ function ensureSourceWidget(cwd, siteUuid) {
2366
+ const shell = findSourceShell(cwd);
2367
+ if (shell === null) {
2368
+ return { shell: null, action: "no-shell" };
2369
+ }
2370
+ const file = path8.join(cwd, shell);
2371
+ const before = readFileSync4(file, "utf8");
2372
+ const { html, action } = ensureWidgetInHtml(before, siteUuid);
2373
+ if (html !== before) {
2374
+ writeFileSync5(file, html);
2375
+ }
2376
+ return { shell, action };
2291
2377
  }
2292
- var astroAdapter = {
2293
- name: "astro",
2294
- label: "Astro",
2295
- detect: detect4,
2296
- wire: wire4,
2297
- verify: verify4
2298
- };
2299
2378
 
2300
- // src/protect/install/find-app.ts
2301
- import { readFileSync as readFileSync4, existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2302
- import { join as join8, relative, dirname as dirname3 } from "path";
2303
- function findAppInstance(cwd, re) {
2304
- const root = existsSync8(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2305
- let hit = null;
2306
- const walk2 = (dir) => {
2307
- if (hit) return;
2308
- let entries;
2309
- try {
2310
- entries = readdirSync3(dir);
2311
- } catch {
2312
- return;
2313
- }
2314
- for (const name of entries) {
2315
- if (hit) return;
2316
- if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2317
- const p = join8(dir, name);
2318
- let st;
2319
- try {
2320
- st = statSync3(p);
2321
- } catch {
2322
- continue;
2323
- }
2324
- if (st.isDirectory()) walk2(p);
2325
- else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
2326
- let src;
2327
- try {
2328
- src = readFileSync4(p, "utf8");
2329
- } catch {
2330
- continue;
2331
- }
2332
- const m = re.exec(src);
2333
- if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, "/"), appVar: m[1] };
2334
- }
2379
+ // src/guide.ts
2380
+ var WIDGET_NEEDLE2 = "patchstack-widget";
2381
+ var INSTALL_COMMANDS = {
2382
+ npm: "npm install --save @patchstack/connect",
2383
+ pnpm: "pnpm add @patchstack/connect",
2384
+ yarn: "yarn add @patchstack/connect",
2385
+ bun: "bun add @patchstack/connect"
2386
+ };
2387
+ var PM_BY_LOCKFILE = [
2388
+ { filename: "bun.lock", pm: "bun" },
2389
+ { filename: "bun.lockb", pm: "bun" },
2390
+ { filename: "pnpm-lock.yaml", pm: "pnpm" },
2391
+ { filename: "yarn.lock", pm: "yarn" },
2392
+ { filename: "package-lock.json", pm: "npm" }
2393
+ ];
2394
+ var WIDGET_FILE_CANDIDATES = {
2395
+ next: [
2396
+ "app/layout.tsx",
2397
+ "app/layout.jsx",
2398
+ "src/app/layout.tsx",
2399
+ "src/app/layout.jsx",
2400
+ "pages/_document.tsx",
2401
+ "pages/_document.jsx",
2402
+ "src/pages/_document.tsx"
2403
+ ],
2404
+ nuxt: ["app.vue", "src/app.vue", "app/app.vue"],
2405
+ remix: ["app/root.tsx", "app/root.jsx"],
2406
+ "react-router": ["app/root.tsx", "src/root.tsx"],
2407
+ "tanstack-start": ["src/routes/__root.tsx", "app/routes/__root.tsx"],
2408
+ sveltekit: ["src/app.html"],
2409
+ astro: ["src/layouts/Layout.astro"],
2410
+ gatsby: ["src/html.js"]
2411
+ };
2412
+ var GENERIC_WIDGET_FILES = ["index.html", "public/index.html"];
2413
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set([
2414
+ "node_modules",
2415
+ ".git",
2416
+ "dist",
2417
+ "build",
2418
+ "out",
2419
+ ".output",
2420
+ ".next",
2421
+ ".nuxt",
2422
+ ".svelte-kit",
2423
+ ".vercel",
2424
+ ".netlify",
2425
+ "coverage",
2426
+ "vendor"
2427
+ ]);
2428
+ var WIDGET_EXTENSIONS = /* @__PURE__ */ new Set([
2429
+ ".html",
2430
+ ".htm",
2431
+ ".tsx",
2432
+ ".jsx",
2433
+ ".ts",
2434
+ ".js",
2435
+ ".mjs",
2436
+ ".cjs",
2437
+ ".vue",
2438
+ ".svelte",
2439
+ ".astro",
2440
+ ".ejs",
2441
+ ".hbs"
2442
+ ]);
2443
+ var WIDGET_SCAN_MAX_FILES = 4e3;
2444
+ var WIDGET_SCAN_MAX_DEPTH = 6;
2445
+ var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
2446
+ function detectPackageManager(cwd) {
2447
+ const declared = readPackageJson(cwd)?.packageManager?.split("@")[0];
2448
+ if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
2449
+ return declared;
2450
+ }
2451
+ for (const { filename, pm } of PM_BY_LOCKFILE) {
2452
+ if (existsSync10(path9.join(cwd, filename))) {
2453
+ return pm;
2335
2454
  }
2336
- };
2337
- walk2(root);
2338
- return hit;
2339
- }
2340
- function importSpecifier(fromRel, toRel, preserveExtension = false) {
2341
- let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
2342
- if (!preserveExtension) spec = spec.replace(/\.(?:ts|js)$/, "");
2343
- if (!spec.startsWith(".")) spec = `./${spec}`;
2344
- return spec;
2345
- }
2346
-
2347
- // src/protect/install/register.ts
2348
- import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
2349
- import { join as join10 } from "path";
2350
-
2351
- // src/protect/install/generic.ts
2352
- import { readFileSync as readFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2353
- import { join as join9 } from "path";
2354
- var GUARD_MARKER = "patchstack/guard";
2355
- function genericDir(cwd) {
2356
- return existsSync9(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2357
- }
2358
- function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts", guardFile = "guard.ts") {
2359
- const templates = templatesDir();
2360
- const dir = genericDir(cwd);
2361
- const dst = join9(cwd, dir);
2362
- mkdirSync4(dst, { recursive: true });
2363
- copyFileSync4(join9(templates, guardTemplate), join9(dst, guardFile));
2364
- const guardRel = `${dir}/${guardFile}`;
2365
- const changed = [guardRel];
2366
- if (!opts.demo) bakeSiteUuid(cwd, guardRel);
2367
- const rulesDst = join9(dst, "rules.json");
2368
- if (opts.demo || !existsSync9(rulesDst)) {
2369
- copyFileSync4(join9(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2370
- changed.push(`${dir}/rules.json`);
2371
2455
  }
2372
- return { changed, dir };
2456
+ return "npm";
2373
2457
  }
2374
- function candidateEntries(cwd) {
2375
- const names = [
2376
- "src/server.ts",
2377
- "src/server.js",
2378
- "src/index.ts",
2379
- "src/index.js",
2380
- "src/app.ts",
2381
- "src/app.js",
2382
- "src/main.ts",
2383
- "server.ts",
2384
- "server.js",
2385
- "index.ts",
2386
- "index.js",
2387
- "app.ts",
2388
- "app.js"
2389
- ];
2390
- const hits = names.filter((n) => existsSync9(join9(cwd, n)));
2458
+ function installCommand(pm) {
2459
+ return INSTALL_COMMANDS[pm];
2460
+ }
2461
+ function readPackageJson(cwd) {
2391
2462
  try {
2392
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
2393
- if (typeof pkg.main === "string" && existsSync9(join9(cwd, pkg.main))) hits.push(pkg.main);
2463
+ return JSON.parse(readFileSync5(path9.join(cwd, "package.json"), "utf8"));
2394
2464
  } catch {
2465
+ return null;
2395
2466
  }
2396
- return [...new Set(hits)];
2397
2467
  }
2398
- function usesExpress(cwd) {
2468
+ function installedVersion(cwd, declaredRange) {
2399
2469
  try {
2400
- const pkg = JSON.parse(read(join9(cwd, "package.json")));
2401
- return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2470
+ const pkg = JSON.parse(
2471
+ readFileSync5(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
2472
+ );
2473
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
2474
+ return pkg.version;
2475
+ }
2402
2476
  } catch {
2403
- return false;
2404
2477
  }
2478
+ return declaredRange;
2405
2479
  }
2406
- function wiringPlan(cwd, dir) {
2407
- const entries = candidateEntries(cwd);
2408
- const express = usesExpress(cwd);
2409
- const lines = [
2410
- `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/guard.ts + ${dir}/rules.json.`,
2411
- "Finish by wiring it into your server (pick the one that fits):",
2412
- ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/guard"`,
2413
- ` \u2022 Node / Express: app.use(patchstackMiddleware) // add before your routes`,
2414
- entries.length ? `Likely server ${entries.length === 1 ? "entry" : "entries"}: ${entries.join(", ")}${express ? " (Express detected)" : ""}.` : "Could not locate a server entry \u2014 wire it wherever requests enter your app.",
2415
- "Then confirm it is hooked up: npx patchstack-connect protect --check"
2416
- ];
2417
- return lines.join("\n");
2418
- }
2419
- function genericVerify(cwd) {
2420
- const dir = genericDir(cwd);
2421
- const scaffolded = existsSync9(join9(cwd, dir, "guard.ts"));
2422
- const imported = scaffolded && guardIsImported(cwd, join9(cwd, dir, "guard.ts"));
2423
- return {
2424
- wired: scaffolded && imported,
2425
- checks: [
2426
- { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
2427
- {
2428
- label: "guard imported into a server entry",
2429
- ok: imported,
2430
- hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/guard" and wire it into your request path`
2431
- }
2432
- ]
2433
- };
2434
- }
2435
- function guardIsImported(cwd, guardPath) {
2436
- const root = existsSync9(join9(cwd, "src")) ? join9(cwd, "src") : cwd;
2437
- let found = false;
2438
- const walk2 = (d) => {
2439
- if (found) return;
2480
+ function findWidgetMarker(cwd, siteUuid) {
2481
+ let budget = WIDGET_SCAN_MAX_FILES;
2482
+ let sawWidget = false;
2483
+ let sawTokenMatch = false;
2484
+ const settled = () => sawTokenMatch || sawWidget && siteUuid == null;
2485
+ const walk2 = (dir, depth) => {
2486
+ if (depth > WIDGET_SCAN_MAX_DEPTH || budget <= 0) {
2487
+ return;
2488
+ }
2440
2489
  let entries;
2441
2490
  try {
2442
- entries = readdirSync4(d);
2491
+ entries = readdirSync4(dir, { withFileTypes: true });
2443
2492
  } catch {
2444
2493
  return;
2445
2494
  }
2446
- for (const name of entries) {
2447
- if (found) return;
2448
- if (name === "node_modules" || name.startsWith(".")) continue;
2449
- const p = join9(d, name);
2450
- let st;
2451
- try {
2452
- st = statSync4(p);
2453
- } catch {
2495
+ for (const entry of entries) {
2496
+ if (budget <= 0 || settled()) {
2497
+ return;
2498
+ }
2499
+ const full = path9.join(dir, entry.name);
2500
+ if (entry.isDirectory()) {
2501
+ if (SKIPPED_DIRS.has(entry.name) || entry.name.startsWith(".")) {
2502
+ continue;
2503
+ }
2504
+ walk2(full, depth + 1);
2454
2505
  continue;
2455
2506
  }
2456
- if (st.isDirectory()) walk2(p);
2457
- else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
2458
- try {
2459
- if (readFileSync5(p, "utf8").includes(GUARD_MARKER)) found = true;
2460
- } catch {
2507
+ if (!entry.isFile() || !WIDGET_EXTENSIONS.has(path9.extname(entry.name).toLowerCase())) {
2508
+ continue;
2509
+ }
2510
+ budget -= 1;
2511
+ try {
2512
+ if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
2513
+ continue;
2514
+ }
2515
+ const content = readFileSync5(full, "utf8");
2516
+ if (!content.includes(WIDGET_NEEDLE2)) {
2517
+ continue;
2518
+ }
2519
+ sawWidget = true;
2520
+ if (siteUuid != null && content.includes(siteUuid)) {
2521
+ sawTokenMatch = true;
2461
2522
  }
2523
+ } catch {
2462
2524
  }
2463
2525
  }
2464
2526
  };
2465
- walk2(root);
2466
- return found;
2527
+ walk2(cwd, 0);
2528
+ return {
2529
+ found: sawWidget,
2530
+ uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
2531
+ };
2467
2532
  }
2468
-
2469
- // src/protect/install/register.ts
2470
- var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
2471
- function guardTarget(cwd, entryRel, spec) {
2472
- if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
2473
- if (spec.guardTemplateCjs) {
2474
- return {
2475
- template: spec.guardTemplateCjs,
2476
- file: "guard.cjs",
2477
- importLine: (name, target) => `const { ${name} } = require("${target}");`,
2478
- preserveExtension: true
2479
- };
2533
+ function resolveWidgetFileHint(cwd, framework) {
2534
+ const candidates = [
2535
+ ...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
2536
+ ...GENERIC_WIDGET_FILES
2537
+ ];
2538
+ for (const candidate of candidates) {
2539
+ if (existsSync10(path9.join(cwd, candidate))) {
2540
+ return candidate;
2480
2541
  }
2481
- } else if (/\.(?:js|mjs)$/.test(entryRel) && spec.guardTemplateEsm) {
2482
- return {
2483
- template: spec.guardTemplateEsm,
2484
- file: entryRel.endsWith(".mjs") ? "guard.mjs" : "guard.js",
2485
- importLine: (name, target) => `import { ${name} } from "${target}";`,
2486
- preserveExtension: true
2542
+ }
2543
+ return null;
2544
+ }
2545
+ async function collectGuideState(cwd) {
2546
+ const pkg = readPackageJson(cwd);
2547
+ const packageManager = detectPackageManager(cwd);
2548
+ let installed = null;
2549
+ if (pkg?.devDependencies?.["@patchstack/connect"] !== void 0) {
2550
+ installed = {
2551
+ version: installedVersion(cwd, pkg.devDependencies["@patchstack/connect"]),
2552
+ section: "devDependencies"
2553
+ };
2554
+ } else if (pkg?.dependencies?.["@patchstack/connect"] !== void 0) {
2555
+ installed = {
2556
+ version: installedVersion(cwd, pkg.dependencies["@patchstack/connect"]),
2557
+ section: "dependencies"
2487
2558
  };
2488
2559
  }
2560
+ let siteUuid = null;
2561
+ let claimUrl = null;
2562
+ let endpointOverride = null;
2563
+ let widgetOptOut = false;
2564
+ try {
2565
+ const config = await resolveConfig({ cwd });
2566
+ siteUuid = config.siteUuid;
2567
+ if (siteUuid !== null) {
2568
+ claimUrl = buildClaimUrl(config.endpoint, siteUuid);
2569
+ }
2570
+ if (config.endpoint !== DEFAULT_ENDPOINT) {
2571
+ endpointOverride = config.endpoint;
2572
+ }
2573
+ widgetOptOut = !config.widget;
2574
+ } catch {
2575
+ }
2576
+ const declaredNames = Object.keys({ ...pkg?.dependencies, ...pkg?.devDependencies });
2577
+ const stack = detectStack(
2578
+ declaredNames.map((name) => ({ name, version: "" })),
2579
+ {}
2580
+ );
2581
+ const widget = findWidgetMarker(cwd, siteUuid);
2582
+ const protection = runVerify(cwd);
2489
2583
  return {
2490
- template: spec.guardTemplate,
2491
- file: "guard.ts",
2492
- importLine: (name, target) => `import { ${name} } from "${target}";`,
2493
- preserveExtension: false
2584
+ projectName: pkg?.name ?? null,
2585
+ hasPackageJson: pkg !== null,
2586
+ packageManager,
2587
+ installed,
2588
+ siteUuid,
2589
+ claimUrl,
2590
+ endpointOverride,
2591
+ hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
2592
+ installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
2593
+ // bun run doesn't execute npm-style pre/post scripts, so chaining inside
2594
+ // the build script itself also counts as wired (and is what we suggest on bun).
2595
+ prebuildWired: (pkg?.scripts?.prebuild ?? "").includes("patchstack-connect scan") || (pkg?.scripts?.build ?? "").includes("patchstack-connect scan"),
2596
+ postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
2597
+ widgetInstalled: widget.found,
2598
+ widgetTokenMatches: widget.uuidMatches,
2599
+ widgetOptOut,
2600
+ framework: stack.framework,
2601
+ widgetFileHint: resolveWidgetFileHint(cwd, stack.framework),
2602
+ protectionWired: protection.wired,
2603
+ protectionStack: protection.stack,
2604
+ protectionChecks: protection.checks
2494
2605
  };
2495
2606
  }
2496
- function packageType(cwd) {
2497
- try {
2498
- return JSON.parse(read(join10(cwd, "package.json"))).type;
2499
- } catch {
2500
- return void 0;
2501
- }
2607
+ var ANSI = {
2608
+ reset: "\x1B[0m",
2609
+ bold: "\x1B[1m",
2610
+ dim: "\x1B[2m",
2611
+ green: "\x1B[32m",
2612
+ yellow: "\x1B[33m",
2613
+ cyan: "\x1B[36m"
2614
+ };
2615
+ function countRemainingSteps(state) {
2616
+ return [
2617
+ state.installed?.section === "dependencies",
2618
+ state.siteUuid !== null,
2619
+ state.installScanWired,
2620
+ !state.hasBuildScript || state.prebuildWired && state.postbuildWired,
2621
+ state.widgetOptOut || state.widgetInstalled && state.widgetTokenMatches !== false,
2622
+ state.protectionWired
2623
+ ].filter((step) => !step).length;
2502
2624
  }
2503
- function wireRegister(cwd, opts, spec) {
2504
- const entry = findAppInstance(cwd, spec.appRe);
2505
- if (!entry) {
2506
- const { changed: changed2 } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2507
- log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2508
- return { ok: true, changed: changed2 };
2625
+ function renderGuideChecklist(state, useColor) {
2626
+ const paint = (code, text) => useColor ? `${code}${text}${ANSI.reset}` : text;
2627
+ const done = (text) => ` ${paint(ANSI.green, "\u2714")} ${text}`;
2628
+ const todo = (text) => ` ${paint(ANSI.yellow, "\u2716")} ${paint(ANSI.bold, text)}`;
2629
+ const detail = (text) => ` ${paint(ANSI.dim, text)}`;
2630
+ const lines = [];
2631
+ const headerParts = [state.framework, state.packageManager].filter(
2632
+ (part) => part !== null
2633
+ );
2634
+ const name = state.projectName ?? path9.basename(process.cwd());
2635
+ lines.push(paint(ANSI.bold, `Patchstack setup status \u2014 ${name} (${headerParts.join(" \xB7 ")})`));
2636
+ if (state.endpointOverride !== null) {
2637
+ lines.push(
2638
+ detail(
2639
+ `endpoint override in effect: ${state.endpointOverride} (set via .patchstackrc.json, PATCHSTACK_ENDPOINT, or --endpoint)`
2640
+ )
2641
+ );
2509
2642
  }
2510
- const target = guardTarget(cwd, entry.relPath, spec);
2511
- const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
2512
- const p = join10(cwd, entry.relPath);
2513
- const s = read(p);
2514
- if (s.includes(spec.importName)) {
2515
- log(`${entry.relPath} already wired`);
2516
- return { ok: true, changed };
2643
+ lines.push("");
2644
+ if (!state.hasPackageJson) {
2645
+ lines.push(todo("No package.json found in this directory."));
2646
+ lines.push(detail("Run the guide from the project root."));
2647
+ return lines.join("\n");
2517
2648
  }
2518
- const sourceLines = s.split("\n");
2519
- const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
2520
- const requiredAnchor = spec.callAfter?.(entry.appVar);
2521
- if (spec.requireCallAfter && requiredAnchor && !sourceLines.some((line, index) => index > sourceAppIdx && requiredAnchor.test(line))) {
2522
- log(`${entry.relPath} body-parser anchor not found \u2014 scaffolded guard; ${spec.manualHint}`);
2523
- return { ok: true, changed };
2649
+ if (state.installed?.section === "dependencies") {
2650
+ lines.push(done(`@patchstack/connect installed (${state.installed.version}, ${state.installed.section})`));
2651
+ } else if (state.installed !== null) {
2652
+ lines.push(todo(`Move @patchstack/connect to runtime dependencies (currently ${state.installed.section})`));
2653
+ lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
2654
+ lines.push(detail("The generated guard imports @patchstack/connect/protect at runtime."));
2655
+ } else {
2656
+ lines.push(todo("Install @patchstack/connect as a runtime dependency"));
2657
+ lines.push(detail(`Run \u2192 ${installCommand(state.packageManager)}`));
2658
+ if (state.packageManager === "bun") {
2659
+ lines.push(detail(`(if bun isn't available here, ${INSTALL_COMMANDS.npm} works too)`));
2660
+ }
2524
2661
  }
2525
- const importLine = target.importLine(
2526
- spec.importName,
2527
- importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
2528
- );
2529
- const lines = s.split("\n");
2530
- let lastImport = -1;
2531
- for (let i = 0; i < lines.length; i++) {
2532
- if (/^\s*(?:import\b|(?:const|let|var)\s+.+?=\s*require\()/.test(lines[i] ?? "")) lastImport = i;
2662
+ if (state.siteUuid !== null) {
2663
+ lines.push(done(`Site provisioned (${state.siteUuid})`));
2664
+ } else {
2665
+ lines.push(todo("Provision the site \u2014 run the first scan"));
2666
+ lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
2667
+ lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
2668
+ lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
2533
2669
  }
2534
- lines.splice(lastImport + 1, 0, importLine);
2535
- const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2536
- if (appIdx !== -1) {
2537
- const preferred = spec.callAfter?.(entry.appVar);
2538
- const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
2539
- const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
2540
- lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2670
+ if (state.installScanWired) {
2671
+ lines.push(done("Dependency-install scan wired (postinstall)"));
2672
+ } else {
2673
+ lines.push(todo("Scan again whenever dependencies are installed"));
2674
+ lines.push(detail('Edit package.json \u2192 "postinstall": "patchstack-connect scan"'));
2541
2675
  }
2542
- writeFileSync5(p, lines.join("\n"));
2543
- changed.push(entry.relPath);
2544
- log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
2545
- return { ok: true, changed: [...new Set(changed)] };
2546
- }
2547
- function verifyRegister(cwd, spec) {
2548
- const dir = existsSync10(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
2549
- const entry = findAppInstance(cwd, spec.appRe);
2550
- const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
2551
- const scaffolded = target ? existsSync10(join10(cwd, dir, target.file)) : false;
2552
- const entrySource = entry ? read(join10(cwd, entry.relPath)) : "";
2553
- const entryLines = entrySource.split("\n");
2554
- const anchor = entry && spec.callAfter ? spec.callAfter(entry.appVar) : null;
2555
- const anchorIndex = anchor ? entryLines.findIndex((line) => anchor.test(line)) : -1;
2556
- const callIndex = entry ? entryLines.findIndex((line) => line.includes(spec.call(entry.appVar))) : -1;
2557
- const ordered = anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1;
2558
- const wired = entrySource.includes(spec.importName) && ordered;
2559
- return {
2560
- wired: scaffolded && wired,
2561
- checks: [
2562
- { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
2563
- { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
2564
- ]
2565
- };
2566
- }
2567
-
2568
- // src/protect/install/adapters/nestjs.ts
2569
- var SPEC3 = {
2570
- appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+NestFactory\.create/,
2571
- guardTemplate: "generic-guard.ts",
2572
- importName: "patchstackMiddleware",
2573
- call: (v) => `${v}.use(patchstackMiddleware);`,
2574
- label: "NestJS app",
2575
- manualHint: "add `app.use(patchstackMiddleware)` right after your NestFactory.create(...) call"
2576
- };
2577
- var nestjsAdapter = {
2578
- name: "nestjs",
2579
- label: "NestJS",
2580
- detect: (cwd) => hasDependency(cwd, "@nestjs/core") && findAppInstance(cwd, SPEC3.appRe) !== null,
2581
- wire: (cwd, opts) => wireRegister(cwd, opts, SPEC3),
2582
- verify: (cwd) => verifyRegister(cwd, SPEC3)
2583
- };
2584
-
2585
- // src/protect/install/adapters/fastify.ts
2586
- var SPEC4 = {
2587
- appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:fastify|Fastify)\(/,
2588
- guardTemplate: "fastify-plugin.ts",
2589
- importName: "patchstackFastify",
2590
- call: (v) => `${v}.register(patchstackFastify);`,
2591
- label: "Fastify app",
2592
- manualHint: "add `app.register(patchstackFastify)` right after you create your fastify() app"
2593
- };
2594
- var fastifyAdapter = {
2595
- name: "fastify",
2596
- label: "Fastify (Node)",
2597
- detect: (cwd) => hasDependency(cwd, "fastify") && findAppInstance(cwd, SPEC4.appRe) !== null,
2598
- wire: (cwd, opts) => wireRegister(cwd, opts, SPEC4),
2599
- verify: (cwd) => verifyRegister(cwd, SPEC4)
2600
- };
2601
-
2602
- // src/protect/install/adapters/express.ts
2603
- var jsonParserRe = (appVar) => new RegExp(`^\\s*${appVar}\\.use\\(\\s*express\\.json\\(`, "m");
2604
- var SPEC5 = {
2605
- appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/,
2606
- guardTemplate: "express-guard.ts",
2607
- guardTemplateEsm: "express-guard.js",
2608
- guardTemplateCjs: "express-guard.cjs",
2609
- importName: "patchstackMiddleware",
2610
- call: (v) => `${v}.use(patchstackMiddleware);`,
2611
- callAfter: jsonParserRe,
2612
- requireCallAfter: true,
2613
- label: "Express app",
2614
- manualHint: "add `app.use(patchstackMiddleware)` after your JSON body parser and before the routes"
2615
- };
2616
- var expressAdapter = {
2617
- name: "express",
2618
- label: "Express (Node)",
2619
- detect: (cwd) => hasDependency(cwd, "express") && findAppInstance(cwd, SPEC5.appRe) !== null,
2620
- wire: (cwd, opts) => wireRegister(cwd, opts, SPEC5),
2621
- verify: (cwd) => verifyRegister(cwd, SPEC5)
2622
- };
2623
-
2624
- // src/protect/install/index.ts
2625
- var ADAPTERS = [
2626
- tanstackSupabaseAdapter,
2627
- nextAdapter,
2628
- sveltekitAdapter,
2629
- astroAdapter,
2630
- nestjsAdapter,
2631
- fastifyAdapter,
2632
- expressAdapter
2633
- ];
2634
- function runProtect(cwd, opts = {}) {
2635
- const adapter = ADAPTERS.find((a) => a.detect(cwd));
2636
- if (adapter) {
2637
- const result = adapter.wire(cwd, opts);
2638
- return { status: "wired", adapter: adapter.name, changed: result.changed };
2676
+ if (!state.hasBuildScript) {
2677
+ lines.push(done("No build script to integrate (postinstall covers dependency changes)"));
2678
+ } else if (state.prebuildWired && state.postbuildWired) {
2679
+ lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
2680
+ } else if (state.packageManager === "bun") {
2681
+ lines.push(todo('Wire the build hooks yourself \u2014 edit package.json (bun skips pre/post hooks, so chain inside "build")'));
2682
+ lines.push(detail('Edit package.json \u2192 "build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"'));
2683
+ } else {
2684
+ lines.push(todo('Wire the build hooks yourself \u2014 edit package.json "scripts" (chain with && if a hook already exists)'));
2685
+ if (!state.prebuildWired) {
2686
+ lines.push(detail('Edit package.json \u2192 "prebuild": "patchstack-connect scan"'));
2687
+ }
2688
+ if (!state.postbuildWired) {
2689
+ lines.push(detail('Edit package.json \u2192 "postbuild": "patchstack-connect mark-build"'));
2690
+ }
2639
2691
  }
2640
- const { changed, dir } = scaffoldGeneric(cwd, opts);
2641
- const plan = wiringPlan(cwd, dir);
2642
- log(plan);
2643
- return { status: "scaffolded", adapter: "generic", changed, plan };
2644
- }
2645
- function runVerify(cwd) {
2646
- const adapter = ADAPTERS.find((a) => a.detect(cwd));
2647
- if (adapter) return { stack: adapter.label, ...adapter.verify(cwd) };
2648
- return { stack: "generic", ...genericVerify(cwd) };
2692
+ const widgetOk = state.widgetInstalled && state.widgetTokenMatches !== false;
2693
+ if (state.widgetOptOut && !widgetOk) {
2694
+ lines.push(done('Disclosure widget disabled by config ("widget": false in .patchstackrc.json)'));
2695
+ } else if (widgetOk) {
2696
+ lines.push(done("Disclosure widget installed"));
2697
+ } else if (state.widgetInstalled) {
2698
+ lines.push(todo("Fix the disclosure widget yourself \u2014 its site UUID doesn't match this project's"));
2699
+ lines.push(detail(`Edit the widget tag \u2192 set data-site-uuid (or userToken) to '${state.siteUuid}' (a wrong UUID makes the widget silently no-op)`));
2700
+ } else if (state.siteUuid === null) {
2701
+ lines.push(todo('Add the "Report a vulnerability" widget \u2014 the first scan does this for you'));
2702
+ lines.push(detail("Run \u2192 npx @patchstack/connect scan (provisions the site and adds the widget tag"));
2703
+ lines.push(detail(" to the root HTML shell: index.html / public/index.html / src/app.html)"));
2704
+ } else {
2705
+ lines.push(todo('Add the "Report a vulnerability" widget yourself \u2014 this root is code, not a plain HTML shell'));
2706
+ lines.push(detail("Note \u2192 a normal `scan` adds this tag to a plain HTML shell automatically; add it by hand here:"));
2707
+ const placement = state.widgetFileHint !== null ? `Edit ${state.widgetFileHint} \u2192 put it just before </body>:` : "Edit your root layout \u2192 put it just before </body> (the framework's HTML/layout mechanism, never a JS entry point):";
2708
+ lines.push(detail(placement));
2709
+ lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
2710
+ lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
2711
+ }
2712
+ if (state.protectionWired) {
2713
+ lines.push(done(`Runtime protection wired (${state.protectionStack})`));
2714
+ } else {
2715
+ lines.push(todo(`Finish runtime protection (${state.protectionStack})`));
2716
+ for (const check of state.protectionChecks.filter((item) => !item.ok)) {
2717
+ lines.push(detail(`${check.label}${check.hint ? ` \u2014 ${check.hint}` : ""}`));
2718
+ }
2719
+ lines.push(detail("Verify \u2192 npx @patchstack/connect protect --check"));
2720
+ }
2721
+ lines.push("");
2722
+ if (state.claimUrl !== null) {
2723
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
2724
+ lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
2725
+ lines.push(detail("Open this link in a browser. The CLI never opens it."));
2726
+ if (state.endpointOverride !== null) {
2727
+ lines.push(detail("(this URL inherits the endpoint override above)"));
2728
+ }
2729
+ } else {
2730
+ lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
2731
+ }
2732
+ const remaining = countRemainingSteps(state);
2733
+ lines.push("");
2734
+ if (remaining === 0) {
2735
+ lines.push(
2736
+ done(
2737
+ paint(
2738
+ ANSI.bold,
2739
+ "All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet."
2740
+ )
2741
+ )
2742
+ );
2743
+ if (state.claimUrl !== null) {
2744
+ lines.push(detail("The only manual action left is opening the dashboard link above (if not already connected)."));
2745
+ }
2746
+ } else {
2747
+ lines.push(
2748
+ ` ${paint(ANSI.yellow, String(remaining))} step(s) remaining \u2014 details in the reference guide below.`
2749
+ );
2750
+ }
2751
+ return lines.join("\n");
2649
2752
  }
2650
2753
 
2651
2754
  // src/setup.ts
@@ -2653,6 +2756,11 @@ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "
2653
2756
  import path10 from "path";
2654
2757
  var SCAN_COMMAND = "patchstack-connect scan";
2655
2758
  var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
2759
+ function setupProtection(cwd) {
2760
+ const install = runProtect(cwd);
2761
+ const verification = runVerify(cwd);
2762
+ return { install, verification };
2763
+ }
2656
2764
  function appendHook(existing, command) {
2657
2765
  if (existing === void 0 || existing.trim().length === 0) {
2658
2766
  return command;
@@ -2668,14 +2776,17 @@ function wireBuildScripts(cwd, packageManager) {
2668
2776
  const pkg = JSON.parse(raw);
2669
2777
  const scripts = pkg.scripts ?? {};
2670
2778
  const build = scripts.build;
2779
+ const postinstall = appendHook(scripts.postinstall, SCAN_COMMAND);
2671
2780
  if (build === void 0 || build.trim().length === 0) {
2672
- return {
2673
- changed: false,
2674
- strategy: "skipped",
2675
- detail: "package.json has no build script; no build integration was added."
2676
- };
2677
- }
2678
- if (packageManager === "bun") {
2781
+ if (postinstall === scripts.postinstall) {
2782
+ return {
2783
+ changed: false,
2784
+ strategy: "postinstall-only",
2785
+ detail: "dependency-install scan already wired; no build script to integrate."
2786
+ };
2787
+ }
2788
+ scripts.postinstall = postinstall;
2789
+ } else if (packageManager === "bun") {
2679
2790
  let nextBuild = build;
2680
2791
  if (!nextBuild.includes(SCAN_COMMAND)) {
2681
2792
  nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
@@ -2683,24 +2794,26 @@ function wireBuildScripts(cwd, packageManager) {
2683
2794
  if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
2684
2795
  nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
2685
2796
  }
2686
- if (nextBuild === build) {
2797
+ if (nextBuild === build && postinstall === scripts.postinstall) {
2687
2798
  return {
2688
2799
  changed: false,
2689
2800
  strategy: "build-chain",
2690
- detail: "build script already runs scan and mark-build."
2801
+ detail: "dependency-install scan and build chain already wired."
2691
2802
  };
2692
2803
  }
2804
+ scripts.postinstall = postinstall;
2693
2805
  scripts.build = nextBuild;
2694
2806
  } else {
2695
2807
  const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
2696
2808
  const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
2697
- if (prebuild === scripts.prebuild && postbuild === scripts.postbuild) {
2809
+ if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
2698
2810
  return {
2699
2811
  changed: false,
2700
2812
  strategy: "lifecycle-hooks",
2701
- detail: "prebuild and postbuild hooks are already wired."
2813
+ detail: "dependency-install, prebuild, and postbuild hooks are already wired."
2702
2814
  };
2703
2815
  }
2816
+ scripts.postinstall = postinstall;
2704
2817
  scripts.prebuild = prebuild;
2705
2818
  scripts.postbuild = postbuild;
2706
2819
  }
@@ -2709,14 +2822,21 @@ function wireBuildScripts(cwd, packageManager) {
2709
2822
  const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
2710
2823
  const trailingNewline = raw.endsWith("\n") ? "\n" : "";
2711
2824
  writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
2825
+ if (build === void 0 || build.trim().length === 0) {
2826
+ return {
2827
+ changed: true,
2828
+ strategy: "postinstall-only",
2829
+ detail: "added a dependency-install scan; package.json has no build script."
2830
+ };
2831
+ }
2712
2832
  return packageManager === "bun" ? {
2713
2833
  changed: true,
2714
2834
  strategy: "build-chain",
2715
- detail: "chained scan and mark-build around the existing build command."
2835
+ detail: "added a dependency-install scan and chained scan/mark-build around the build."
2716
2836
  } : {
2717
2837
  changed: true,
2718
2838
  strategy: "lifecycle-hooks",
2719
- detail: "added scan to prebuild and mark-build to postbuild."
2839
+ detail: "added scans to postinstall/prebuild and mark-build to postbuild."
2720
2840
  };
2721
2841
  }
2722
2842
 
@@ -2733,8 +2853,9 @@ Usage:
2733
2853
  or src/app.html) \u2014 opt out with
2734
2854
  "widget": false in .patchstackrc.json
2735
2855
  patchstack-connect setup [options] Finish the bounded project setup: run scan,
2736
- manage the widget, and wire package.json build
2737
- scripts. Never runs a build or protect
2856
+ manage the widget, install + verify runtime
2857
+ protection, and wire dependency/build scans.
2858
+ Never runs the project build
2738
2859
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
2739
2860
  with an existing site UUID
2740
2861
  patchstack-connect status [options] Show current configuration
@@ -3128,11 +3249,23 @@ Run: ${installCommand(before.packageManager)}`
3128
3249
  return scanCode;
3129
3250
  }
3130
3251
  console.log("");
3131
- console.log(" 2. Wire scan and mark-build into package.json");
3252
+ console.log(" 2. Install and verify runtime protection");
3253
+ const protection = setupProtection(process.cwd());
3254
+ if (protection.verification.wired) {
3255
+ console.log(`Runtime protection: wired (${protection.verification.stack}).`);
3256
+ } else {
3257
+ console.log(`Runtime protection: manual wiring remains (${protection.verification.stack}):`);
3258
+ for (const check of protection.verification.checks) {
3259
+ console.log(` ${check.ok ? "\u2713" : "\u2717"} ${check.label}${!check.ok && check.hint ? ` \u2014 ${check.hint}` : ""}`);
3260
+ }
3261
+ console.log("Run `npx @patchstack/connect protect --check` after completing the failed checks.");
3262
+ }
3263
+ console.log("");
3264
+ console.log(" 3. Wire dependency-install and production-build scans into package.json");
3132
3265
  const wired = wireBuildScripts(process.cwd(), before.packageManager);
3133
3266
  console.log(`Build integration: ${wired.detail}`);
3134
3267
  console.log("");
3135
- console.log(" 3. Verify setup status");
3268
+ console.log(" 4. Verify setup status");
3136
3269
  const after = await collectGuideState(process.cwd());
3137
3270
  const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
3138
3271
  console.log(renderGuideChecklist(after, useColor));