@patchstack/connect 0.3.15 → 0.3.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
4
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
5
5
 
6
6
  // src/parsers/index.ts
7
7
  import { access } from "fs/promises";
@@ -1324,11 +1324,11 @@ var INSTALL_COMMANDS = {
1324
1324
  bun: "bun add -d @patchstack/connect"
1325
1325
  };
1326
1326
  var PM_BY_LOCKFILE = [
1327
- { filename: "package-lock.json", pm: "npm" },
1328
1327
  { filename: "bun.lock", pm: "bun" },
1329
1328
  { filename: "bun.lockb", pm: "bun" },
1330
1329
  { filename: "pnpm-lock.yaml", pm: "pnpm" },
1331
- { filename: "yarn.lock", pm: "yarn" }
1330
+ { filename: "yarn.lock", pm: "yarn" },
1331
+ { filename: "package-lock.json", pm: "npm" }
1332
1332
  ];
1333
1333
  var WIDGET_FILE_CANDIDATES = {
1334
1334
  next: [
@@ -1383,6 +1383,10 @@ var WIDGET_SCAN_MAX_FILES = 4e3;
1383
1383
  var WIDGET_SCAN_MAX_DEPTH = 6;
1384
1384
  var WIDGET_SCAN_MAX_BYTES = 512 * 1024;
1385
1385
  function detectPackageManager(cwd) {
1386
+ const declared = readPackageJson(cwd)?.packageManager?.split("@")[0];
1387
+ if (declared === "npm" || declared === "pnpm" || declared === "yarn" || declared === "bun") {
1388
+ return declared;
1389
+ }
1386
1390
  for (const { filename, pm } of PM_BY_LOCKFILE) {
1387
1391
  if (existsSync3(path9.join(cwd, filename))) {
1388
1392
  return pm;
@@ -1587,8 +1591,8 @@ function renderGuideChecklist(state, useColor) {
1587
1591
  } else {
1588
1592
  lines.push(todo("Provision the site \u2014 run the first scan"));
1589
1593
  lines.push(detail("Run \u2192 npx @patchstack/connect scan"));
1590
- lines.push(detail("reads the lockfile, registers the project, writes .patchstackrc.json,"));
1591
- lines.push(detail("and prints a claim URL \u2014 show that URL to the user; never open it yourself."));
1594
+ lines.push(detail("Reads the lockfile, registers the project, writes .patchstackrc.json,"));
1595
+ lines.push(detail("and prints a dashboard link. The CLI prints the link but never opens it."));
1592
1596
  }
1593
1597
  if (state.prebuildWired && state.postbuildWired) {
1594
1598
  lines.push(done("Build hooks wired (scan before builds, mark-build after)"));
@@ -1626,14 +1630,14 @@ function renderGuideChecklist(state, useColor) {
1626
1630
  }
1627
1631
  lines.push("");
1628
1632
  if (state.claimUrl !== null) {
1629
- lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Claim the site (free, opens the dashboard):")}`);
1633
+ lines.push(` ${paint(ANSI.cyan, "\u279C")} ${paint(ANSI.bold, "Dashboard link (open to view reports):")}`);
1630
1634
  lines.push(` ${paint(ANSI.cyan, state.claimUrl)}`);
1631
- lines.push(detail("Open in a browser. AI agents: show this URL to the user verbatim."));
1635
+ lines.push(detail("Open this link in a browser. The CLI never opens it."));
1632
1636
  if (state.endpointOverride !== null) {
1633
1637
  lines.push(detail("(this URL inherits the endpoint override above)"));
1634
1638
  }
1635
1639
  } else {
1636
- lines.push(detail("The claim URL appears after the first scan (re-print any time with `status`)."));
1640
+ lines.push(detail("The dashboard link appears after the first scan (re-print any time with `status`)."));
1637
1641
  }
1638
1642
  const remaining = countRemainingSteps(state);
1639
1643
  lines.push("");
@@ -1647,7 +1651,7 @@ function renderGuideChecklist(state, useColor) {
1647
1651
  )
1648
1652
  );
1649
1653
  if (state.claimUrl !== null) {
1650
- lines.push(detail("The only manual action left is claiming the site via the URL above (if not already claimed)."));
1654
+ lines.push(detail("The only manual action left is opening the dashboard link above (if not already connected)."));
1651
1655
  }
1652
1656
  } else {
1653
1657
  lines.push(
@@ -1657,16 +1661,65 @@ function renderGuideChecklist(state, useColor) {
1657
1661
  return lines.join("\n");
1658
1662
  }
1659
1663
 
1660
- // src/protect/install.ts
1661
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync, copyFileSync } from "fs";
1664
+ // src/protect/install/util.ts
1665
+ import { readFileSync as readFileSync3, existsSync as existsSync4, writeFileSync as writeFileSync2 } from "fs";
1662
1666
  import { join, dirname } from "path";
1663
1667
  import { fileURLToPath } from "url";
1664
- var HERE = dirname(fileURLToPath(import.meta.url));
1665
- var TEMPLATES = [join(HERE, "protect", "templates"), join(HERE, "templates")].find((p) => existsSync4(p)) ?? join(HERE, "protect", "templates");
1666
- var APP = process.cwd();
1667
- var PS_DIR = join(APP, "src/integrations/patchstack");
1668
1668
  var read = (p) => readFileSync3(p, "utf8");
1669
1669
  var log = (msg) => console.log(`patchstack protect: ${msg}`);
1670
+ function hasDependency(cwd, name) {
1671
+ try {
1672
+ const pkg = JSON.parse(read(join(cwd, "package.json")));
1673
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }[name]);
1674
+ } catch {
1675
+ return false;
1676
+ }
1677
+ }
1678
+ var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
1679
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1680
+ function bakeSiteUuid(cwd, guardRelPath) {
1681
+ const rc = join(cwd, ".patchstackrc.json");
1682
+ if (!existsSync4(rc)) {
1683
+ log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1684
+ return false;
1685
+ }
1686
+ let uuid;
1687
+ try {
1688
+ uuid = JSON.parse(read(rc)).siteUuid;
1689
+ } catch {
1690
+ log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
1691
+ return false;
1692
+ }
1693
+ if (typeof uuid !== "string" || !UUID_RE.test(uuid)) {
1694
+ log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1695
+ return false;
1696
+ }
1697
+ const guardPath = join(cwd, guardRelPath);
1698
+ if (!existsSync4(guardPath)) return false;
1699
+ const source = read(guardPath);
1700
+ if (!source.includes(SITE_UUID_PLACEHOLDER)) {
1701
+ log(`${guardRelPath} site UUID already baked`);
1702
+ return false;
1703
+ }
1704
+ writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
1705
+ log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
1706
+ return true;
1707
+ }
1708
+ var HERE = dirname(fileURLToPath(import.meta.url));
1709
+ function templatesDir() {
1710
+ const builtLayout = join(HERE, "protect", "templates");
1711
+ const candidates = [
1712
+ builtLayout,
1713
+ join(HERE, "..", "templates"),
1714
+ // source: src/protect/install/ → src/protect/templates
1715
+ join(HERE, "templates")
1716
+ ];
1717
+ return candidates.find((p) => existsSync4(p)) ?? builtLayout;
1718
+ }
1719
+
1720
+ // src/protect/install/adapters/tanstack-supabase.ts
1721
+ import { writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync, copyFileSync } from "fs";
1722
+ import { join as join2 } from "path";
1670
1723
  var CLIENT_TUNNEL = [
1671
1724
  "",
1672
1725
  " // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
@@ -1680,101 +1733,127 @@ var CLIENT_TUNNEL = [
1680
1733
  " }",
1681
1734
  ""
1682
1735
  ].join("\n");
1683
- var START_IMPORTS = [
1684
- 'import { getRequest } from "@tanstack/react-start/server";',
1685
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse } from "@/integrations/patchstack/guard";'
1686
- ].join("\n");
1687
- var REQUEST_MIDDLEWARE_DEF = [
1688
- "",
1689
- "// Patchstack guard (browser tunnel): intercept tunneled Supabase traffic before anything else,",
1690
- "// then screen the outgoing response (SSR HTML / data) for leaked secrets & PII.",
1736
+ var GUARD_IMPORT = 'import { GUARD_PATH, handleGuardRequest, inspectServerFn, screenResponse, guardRequest } from "@/integrations/patchstack/guard";';
1737
+ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard";/;
1738
+ var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
1739
+ var REQUEST_MIDDLEWARE_BLOCK = [
1740
+ "// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
1741
+ "// Browser\u2192Supabase tunnel + response screening; optional route WAF via PATCHSTACK_ROUTE_WAF=1.",
1691
1742
  "const patchstackGuard = createMiddleware().server(async ({ next }) => {",
1692
1743
  " const request = getRequest();",
1693
1744
  " if (request) {",
1694
1745
  " const { pathname } = new URL(request.url);",
1695
1746
  " if (pathname === GUARD_PATH) return handleGuardRequest(request);",
1747
+ ' if (process.env.PATCHSTACK_ROUTE_WAF === "1") {',
1748
+ " const blocked = await guardRequest(request);",
1749
+ " if (blocked) return blocked;",
1750
+ " }",
1696
1751
  " }",
1697
1752
  " return screenResponse(await next());",
1698
1753
  "});",
1699
- ""
1754
+ "// #endregion patchstack-guard"
1700
1755
  ].join("\n");
1701
- var FUNCTION_MIDDLEWARE_DEF = [
1702
- "",
1703
- "// Patchstack guard (server functions): inspect server-fn args before they reach the database,",
1704
- "// covering apps that mutate via TanStack server functions (which bypass the browser tunnel).",
1756
+ var FUNCTION_MIDDLEWARE_BLOCK = [
1757
+ "// #region patchstack-function-guard (managed by patchstack-connect protect \u2014 do not edit)",
1758
+ "// Inspect server-function args before they reach the database.",
1705
1759
  'const patchstackFunctionGuard = createMiddleware({ type: "function" }).server(async ({ next, data }) => {',
1706
1760
  " const blocked = await inspectServerFn(data);",
1707
1761
  " if (blocked) throw new Error(blocked.message);",
1708
1762
  " return next();",
1709
1763
  "});",
1710
- ""
1764
+ "// #endregion patchstack-function-guard"
1711
1765
  ].join("\n");
1712
- function detectSupportedStack(cwd) {
1713
- const pkgPath = join(cwd, "package.json");
1714
- if (!existsSync4(pkgPath)) return false;
1715
- const pkg = JSON.parse(read(pkgPath));
1716
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1717
- return Boolean(deps["@tanstack/react-start"]) && existsSync4(join(cwd, "src/start.ts")) && existsSync4(join(cwd, "src/integrations/supabase/client.ts"));
1718
- }
1719
- function scaffold(cwd) {
1720
- const dst = join(cwd, "src/integrations/patchstack");
1721
- mkdirSync(dst, { recursive: true });
1722
- copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
1723
- copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
1724
- log("scaffolded guard.ts + rules.json");
1725
- }
1726
- function bakeSiteUuid(cwd) {
1727
- const rc = join(cwd, ".patchstackrc.json");
1728
- if (!existsSync4(rc)) return log("no .patchstackrc.json \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1729
- let uuid;
1766
+ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
1767
+ const lines = s.split("\n");
1768
+ const startMarker = `// #region ${region} `;
1769
+ const endMarker = `// #endregion ${region}`;
1770
+ const si = lines.findIndex((l) => l.includes(startMarker));
1771
+ if (si !== -1) {
1772
+ const ei = lines.findIndex((l, i) => i > si && l.trim() === endMarker);
1773
+ if (ei !== -1) {
1774
+ lines.splice(si, ei - si + 1, ...block.split("\n"));
1775
+ return lines.join("\n");
1776
+ }
1777
+ }
1778
+ const ci = lines.findIndex((l) => l.includes(legacyConst));
1779
+ if (ci !== -1) {
1780
+ const close = lines.findIndex((l, i) => i >= ci && l.trim() === "});");
1781
+ if (close !== -1) {
1782
+ let start = ci;
1783
+ while (start > 0 && (lines[start - 1] ?? "").trim().startsWith("//")) start--;
1784
+ lines.splice(start, close - start + 1, ...block.split("\n"));
1785
+ return lines.join("\n");
1786
+ }
1787
+ }
1788
+ return s.replace(insertBefore, block + "\n\n" + insertBefore);
1789
+ }
1790
+ function detect(cwd) {
1791
+ const pkgPath = join2(cwd, "package.json");
1792
+ if (!existsSync5(pkgPath)) return false;
1793
+ let pkg;
1730
1794
  try {
1731
- uuid = JSON.parse(read(rc)).siteUuid;
1795
+ pkg = JSON.parse(read(pkgPath));
1732
1796
  } catch {
1733
- return log(".patchstackrc.json unreadable \u2014 skipping site-UUID bake");
1797
+ return false;
1734
1798
  }
1735
- if (!uuid || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid)) {
1736
- return log(".patchstackrc.json siteUuid missing or malformed \u2014 guard uses PATCHSTACK_SITE_UUID env or the bundled fallback");
1799
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1800
+ return Boolean(deps["@tanstack/react-start"]) && existsSync5(join2(cwd, "src/start.ts")) && existsSync5(join2(cwd, "src/integrations/supabase/client.ts"));
1801
+ }
1802
+ var GUARD_FILE = "src/integrations/patchstack/guard.ts";
1803
+ function scaffold(cwd, opts) {
1804
+ const templates = templatesDir();
1805
+ const dst = join2(cwd, "src/integrations/patchstack");
1806
+ mkdirSync(dst, { recursive: true });
1807
+ copyFileSync(join2(templates, "guard.ts"), join2(dst, "guard.ts"));
1808
+ const changed = [GUARD_FILE];
1809
+ const rulesDst = join2(dst, "rules.json");
1810
+ if (opts.demo) {
1811
+ copyFileSync(join2(templates, "demo-rules.json"), rulesDst);
1812
+ changed.push("src/integrations/patchstack/rules.json");
1813
+ log("scaffolded guard.ts + rules.json (demo sample rule set)");
1814
+ } else if (!existsSync5(rulesDst)) {
1815
+ copyFileSync(join2(templates, "rules.json"), rulesDst);
1816
+ changed.push("src/integrations/patchstack/rules.json");
1817
+ log("scaffolded guard.ts + rules.json (starter rules)");
1818
+ } else {
1819
+ log("scaffolded guard.ts (kept existing rules.json)");
1737
1820
  }
1738
- const p = join(cwd, "src/integrations/patchstack/guard.ts");
1739
- if (!existsSync4(p)) return;
1740
- const s = read(p);
1741
- if (!s.includes("__PATCHSTACK_SITE_UUID__")) return log("guard.ts site UUID already baked");
1742
- writeFileSync2(p, s.replace("__PATCHSTACK_SITE_UUID__", uuid));
1743
- log("baked site UUID into guard.ts \u2014 live rules from the Patchstack API");
1821
+ return changed;
1744
1822
  }
1745
1823
  function patchClient(cwd) {
1746
- const p = join(cwd, "src/integrations/supabase/client.ts");
1747
- let s = read(p);
1748
- if (s.includes("x-ps-target")) return log("client.ts already wired");
1824
+ const p = join2(cwd, "src/integrations/supabase/client.ts");
1825
+ const s = read(p);
1826
+ if (s.includes("x-ps-target")) {
1827
+ log("client.ts already wired");
1828
+ return false;
1829
+ }
1749
1830
  const anchor = "headers.set('apikey', supabaseKey);";
1750
- if (!s.includes(anchor)) return log("client.ts anchor not found \u2014 skipping (template changed?)");
1751
- writeFileSync2(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1831
+ if (!s.includes(anchor)) {
1832
+ log("client.ts anchor not found \u2014 skipping (template changed?)");
1833
+ return false;
1834
+ }
1835
+ writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
1752
1836
  log("patched client.ts (tunnel Supabase through the guard)");
1837
+ return true;
1753
1838
  }
1754
1839
  function patchStart(cwd) {
1755
- const p = join(cwd, "src/start.ts");
1840
+ const p = join2(cwd, "src/start.ts");
1756
1841
  let s = read(p);
1757
1842
  const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
1758
1843
  const exportAnchor = "export const startInstance";
1759
1844
  const rmAnchor = "requestMiddleware: [";
1760
1845
  if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
1761
- return log("start.ts anchors not found \u2014 skipping (template changed?)");
1846
+ log("start.ts anchors not found \u2014 skipping (template changed?)");
1847
+ return false;
1762
1848
  }
1763
1849
  const original = s;
1764
- if (!s.includes("@/integrations/patchstack/guard")) {
1850
+ if (GUARD_IMPORT_RE.test(s)) {
1851
+ s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
1852
+ } else {
1765
1853
  s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
1766
- } else if (!s.includes("inspectServerFn")) {
1767
- s = s.replace(
1768
- 'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";',
1769
- 'import { GUARD_PATH, handleGuardRequest, inspectServerFn } from "@/integrations/patchstack/guard";'
1770
- );
1771
- }
1772
- if (!s.includes("const patchstackGuard =")) {
1773
- s = s.replace(exportAnchor, REQUEST_MIDDLEWARE_DEF + "\n" + exportAnchor);
1774
- }
1775
- if (!s.includes("const patchstackFunctionGuard =")) {
1776
- s = s.replace(exportAnchor, FUNCTION_MIDDLEWARE_DEF + "\n" + exportAnchor);
1777
1854
  }
1855
+ s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
1856
+ s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
1778
1857
  if (s.includes(rmAnchor) && !s.includes("requestMiddleware: [patchstackGuard")) {
1779
1858
  s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
1780
1859
  }
@@ -1786,20 +1865,584 @@ function patchStart(cwd) {
1786
1865
  s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
1787
1866
  }
1788
1867
  }
1789
- if (s === original) return log("start.ts already wired");
1790
- writeFileSync2(p, s);
1868
+ if (s === original) {
1869
+ log("start.ts already wired");
1870
+ return false;
1871
+ }
1872
+ writeFileSync3(p, s);
1791
1873
  log("patched start.ts (guard registered as request + function middleware)");
1874
+ return true;
1875
+ }
1876
+ function wire(cwd, opts) {
1877
+ const changed = scaffold(cwd, opts);
1878
+ if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
1879
+ if (patchClient(cwd)) changed.push("src/integrations/supabase/client.ts");
1880
+ if (patchStart(cwd)) changed.push("src/start.ts");
1881
+ log(
1882
+ 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."
1883
+ );
1884
+ return { ok: true, changed: [...new Set(changed)] };
1885
+ }
1886
+ function verify(cwd) {
1887
+ const guardPath = join2(cwd, GUARD_FILE);
1888
+ const clientPath = join2(cwd, "src/integrations/supabase/client.ts");
1889
+ const startPath = join2(cwd, "src/start.ts");
1890
+ const guard = existsSync5(guardPath) ? read(guardPath) : "";
1891
+ const client = existsSync5(clientPath) ? read(clientPath) : "";
1892
+ const start = existsSync5(startPath) ? read(startPath) : "";
1893
+ const checks = [
1894
+ { label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
1895
+ { 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" },
1896
+ { 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" },
1897
+ { 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" }
1898
+ ];
1899
+ return { wired: checks.every((c) => c.ok), checks };
1900
+ }
1901
+ var tanstackSupabaseAdapter = {
1902
+ name: "tanstack-supabase",
1903
+ label: "TanStack Start + Supabase",
1904
+ detect,
1905
+ wire,
1906
+ verify
1907
+ };
1908
+
1909
+ // src/protect/install/adapters/next.ts
1910
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, copyFileSync as copyFileSync2 } from "fs";
1911
+ import { join as join3 } from "path";
1912
+ function hasNextDep(cwd) {
1913
+ try {
1914
+ const pkg = JSON.parse(read(join3(cwd, "package.json")));
1915
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.next);
1916
+ } catch {
1917
+ return false;
1918
+ }
1792
1919
  }
1793
- function runProtect(cwd) {
1794
- if (!detectSupportedStack(cwd)) {
1795
- log("runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping.");
1796
- return;
1920
+ function middlewareInfo(cwd) {
1921
+ const candidates = ["middleware.ts", "middleware.js", "src/middleware.ts", "src/middleware.js"];
1922
+ for (const rel of candidates) {
1923
+ if (existsSync6(join3(cwd, rel))) return { relDir: rel.includes("/") ? "src" : ".", relFile: rel, exists: true };
1924
+ }
1925
+ const useSrc = existsSync6(join3(cwd, "src"));
1926
+ return { relDir: useSrc ? "src" : ".", relFile: useSrc ? "src/middleware.ts" : "middleware.ts", exists: false };
1927
+ }
1928
+ function detect2(cwd) {
1929
+ return hasNextDep(cwd);
1930
+ }
1931
+ function rulesFile(relDir) {
1932
+ return relDir === "." ? "patchstack.rules.json" : `${relDir}/patchstack.rules.json`;
1933
+ }
1934
+ function wire2(cwd, opts) {
1935
+ const templates = templatesDir();
1936
+ const mw = middlewareInfo(cwd);
1937
+ const dir = join3(cwd, mw.relDir === "." ? "" : mw.relDir);
1938
+ mkdirSync2(dir, { recursive: true });
1939
+ const rulesDst = join3(dir, "patchstack.rules.json");
1940
+ const changed = [];
1941
+ if (opts.demo || !existsSync6(rulesDst)) {
1942
+ copyFileSync2(join3(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1943
+ changed.push(rulesFile(mw.relDir));
1944
+ }
1945
+ const mwPath = join3(cwd, mw.relFile);
1946
+ const existing = mw.exists ? read(mwPath) : "";
1947
+ if (mw.exists && !existing.includes("patchstack-next")) {
1948
+ log(
1949
+ `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`
1950
+ );
1951
+ return { ok: true, changed };
1952
+ }
1953
+ copyFileSync2(join3(templates, "next-middleware.ts"), mwPath);
1954
+ if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
1955
+ changed.push(mw.relFile);
1956
+ log(mw.exists ? `refreshed ${mw.relFile} (Patchstack middleware)` : `scaffolded ${mw.relFile} (Patchstack middleware)`);
1957
+ return { ok: true, changed: [...new Set(changed)] };
1958
+ }
1959
+ function verify2(cwd) {
1960
+ const mw = middlewareInfo(cwd);
1961
+ const present = mw.exists && read(join3(cwd, mw.relFile)).includes("patchstack-next");
1962
+ const rulesPresent = existsSync6(join3(cwd, rulesFile(mw.relDir)));
1963
+ return {
1964
+ wired: present && rulesPresent,
1965
+ checks: [
1966
+ { label: "Patchstack middleware present", ok: present, hint: `run \`patchstack-connect protect\` (writes ${mw.relFile})` },
1967
+ { label: "rules co-located with the middleware", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
1968
+ ]
1969
+ };
1970
+ }
1971
+ var nextAdapter = {
1972
+ name: "nextjs",
1973
+ label: "Next.js",
1974
+ detect: detect2,
1975
+ wire: wire2,
1976
+ verify: verify2
1977
+ };
1978
+
1979
+ // src/protect/install/adapters/sveltekit.ts
1980
+ import { join as join5 } from "path";
1981
+
1982
+ // src/protect/install/seam.ts
1983
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, copyFileSync as copyFileSync3 } from "fs";
1984
+ import { join as join4, dirname as dirname2 } from "path";
1985
+ function rulesRel(seamRel) {
1986
+ const d = dirname2(seamRel);
1987
+ return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
1988
+ }
1989
+ function wireSeam(cwd, opts, spec) {
1990
+ const templates = templatesDir();
1991
+ const existing = spec.candidates.find((c) => existsSync7(join4(cwd, c)));
1992
+ const seamRel = existing ?? spec.target;
1993
+ mkdirSync3(dirname2(join4(cwd, seamRel)), { recursive: true });
1994
+ const rulesDst = join4(cwd, rulesRel(seamRel));
1995
+ const changed = [];
1996
+ if (opts.demo || !existsSync7(rulesDst)) {
1997
+ copyFileSync3(join4(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
1998
+ changed.push(rulesRel(seamRel));
1999
+ }
2000
+ const current = existing ? read(join4(cwd, existing)) : "";
2001
+ if (existing && !current.includes(spec.marker)) {
2002
+ log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
2003
+ return { ok: true, changed };
2004
+ }
2005
+ copyFileSync3(join4(templates, spec.templateName), join4(cwd, seamRel));
2006
+ if (!opts.demo) bakeSiteUuid(cwd, seamRel);
2007
+ changed.push(seamRel);
2008
+ log(existing ? `refreshed ${seamRel}` : `scaffolded ${seamRel}`);
2009
+ return { ok: true, changed: [...new Set(changed)] };
2010
+ }
2011
+ function verifySeam(cwd, spec) {
2012
+ const existing = spec.candidates.find((c) => existsSync7(join4(cwd, c)));
2013
+ const seamRel = existing ?? spec.target;
2014
+ const present = existing ? read(join4(cwd, existing)).includes(spec.marker) : false;
2015
+ const rulesPresent = existsSync7(join4(cwd, rulesRel(seamRel)));
2016
+ return {
2017
+ wired: present && rulesPresent,
2018
+ checks: [
2019
+ { label: `${spec.seamLabel} present`, ok: present, hint: `run \`patchstack-connect protect\` (writes ${spec.target})` },
2020
+ { label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
2021
+ ]
2022
+ };
2023
+ }
2024
+
2025
+ // src/protect/install/adapters/sveltekit.ts
2026
+ var SPEC = {
2027
+ templateName: "sveltekit-hooks.ts",
2028
+ candidates: ["src/hooks.server.ts", "src/hooks.server.js"],
2029
+ target: "src/hooks.server.ts",
2030
+ marker: "patchstack-sveltekit",
2031
+ planHint: 'add the guard to your `handle` (compose with `sequence()` from "@sveltejs/kit/hooks"), then: npx patchstack-connect protect --check',
2032
+ seamLabel: "SvelteKit server hook"
2033
+ };
2034
+ function detect3(cwd) {
2035
+ try {
2036
+ const pkg = JSON.parse(read(join5(cwd, "package.json")));
2037
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }["@sveltejs/kit"]);
2038
+ } catch {
2039
+ return false;
2040
+ }
2041
+ }
2042
+ function wire3(cwd, opts) {
2043
+ return wireSeam(cwd, opts, SPEC);
2044
+ }
2045
+ function verify3(cwd) {
2046
+ return verifySeam(cwd, SPEC);
2047
+ }
2048
+ var sveltekitAdapter = {
2049
+ name: "sveltekit",
2050
+ label: "SvelteKit",
2051
+ detect: detect3,
2052
+ wire: wire3,
2053
+ verify: verify3
2054
+ };
2055
+
2056
+ // src/protect/install/adapters/astro.ts
2057
+ import { join as join6 } from "path";
2058
+ var SPEC2 = {
2059
+ templateName: "astro-middleware.ts",
2060
+ candidates: ["src/middleware.ts", "src/middleware.js", "src/middleware/index.ts", "src/middleware/index.js"],
2061
+ target: "src/middleware.ts",
2062
+ marker: "patchstack-astro",
2063
+ planHint: 'add the guard to your `onRequest` (compose with `sequence()` from "astro:middleware"), then: npx patchstack-connect protect --check',
2064
+ seamLabel: "Astro middleware"
2065
+ };
2066
+ function detect4(cwd) {
2067
+ try {
2068
+ const pkg = JSON.parse(read(join6(cwd, "package.json")));
2069
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.astro);
2070
+ } catch {
2071
+ return false;
1797
2072
  }
1798
- scaffold(cwd);
1799
- bakeSiteUuid(cwd);
1800
- patchClient(cwd);
1801
- patchStart(cwd);
1802
- log("done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only.");
2073
+ }
2074
+ function wire4(cwd, opts) {
2075
+ return wireSeam(cwd, opts, SPEC2);
2076
+ }
2077
+ function verify4(cwd) {
2078
+ return verifySeam(cwd, SPEC2);
2079
+ }
2080
+ var astroAdapter = {
2081
+ name: "astro",
2082
+ label: "Astro",
2083
+ detect: detect4,
2084
+ wire: wire4,
2085
+ verify: verify4
2086
+ };
2087
+
2088
+ // src/protect/install/find-app.ts
2089
+ import { readFileSync as readFileSync4, existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "fs";
2090
+ import { join as join7, relative, dirname as dirname3 } from "path";
2091
+ function findAppInstance(cwd, re) {
2092
+ const root = existsSync8(join7(cwd, "src")) ? join7(cwd, "src") : cwd;
2093
+ let hit = null;
2094
+ const walk2 = (dir) => {
2095
+ if (hit) return;
2096
+ let entries;
2097
+ try {
2098
+ entries = readdirSync3(dir);
2099
+ } catch {
2100
+ return;
2101
+ }
2102
+ for (const name of entries) {
2103
+ if (hit) return;
2104
+ if (name === "node_modules" || name === "patchstack" || name.startsWith(".")) continue;
2105
+ const p = join7(dir, name);
2106
+ let st;
2107
+ try {
2108
+ st = statSync3(p);
2109
+ } catch {
2110
+ continue;
2111
+ }
2112
+ if (st.isDirectory()) walk2(p);
2113
+ else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
2114
+ let src;
2115
+ try {
2116
+ src = readFileSync4(p, "utf8");
2117
+ } catch {
2118
+ continue;
2119
+ }
2120
+ const m = re.exec(src);
2121
+ if (m) hit = { relPath: relative(cwd, p).replace(/\\/g, "/"), appVar: m[1] };
2122
+ }
2123
+ }
2124
+ };
2125
+ walk2(root);
2126
+ return hit;
2127
+ }
2128
+ function importSpecifier(fromRel, toRel) {
2129
+ let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/").replace(/\.(?:ts|js)$/, "");
2130
+ if (!spec.startsWith(".")) spec = `./${spec}`;
2131
+ return spec;
2132
+ }
2133
+
2134
+ // src/protect/install/register.ts
2135
+ import { writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
2136
+ import { join as join9 } from "path";
2137
+
2138
+ // src/protect/install/generic.ts
2139
+ import { readFileSync as readFileSync5, existsSync as existsSync9, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
2140
+ import { join as join8 } from "path";
2141
+ var GUARD_MARKER = "patchstack/guard";
2142
+ function genericDir(cwd) {
2143
+ return existsSync9(join8(cwd, "src")) ? "src/patchstack" : "patchstack";
2144
+ }
2145
+ function scaffoldGeneric(cwd, opts, guardTemplate = "generic-guard.ts") {
2146
+ const templates = templatesDir();
2147
+ const dir = genericDir(cwd);
2148
+ const dst = join8(cwd, dir);
2149
+ mkdirSync4(dst, { recursive: true });
2150
+ copyFileSync4(join8(templates, guardTemplate), join8(dst, "guard.ts"));
2151
+ const changed = [`${dir}/guard.ts`];
2152
+ if (!opts.demo) bakeSiteUuid(cwd, `${dir}/guard.ts`);
2153
+ const rulesDst = join8(dst, "rules.json");
2154
+ if (opts.demo || !existsSync9(rulesDst)) {
2155
+ copyFileSync4(join8(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
2156
+ changed.push(`${dir}/rules.json`);
2157
+ }
2158
+ return { changed, dir };
2159
+ }
2160
+ function candidateEntries(cwd) {
2161
+ const names = [
2162
+ "src/server.ts",
2163
+ "src/server.js",
2164
+ "src/index.ts",
2165
+ "src/index.js",
2166
+ "src/app.ts",
2167
+ "src/app.js",
2168
+ "src/main.ts",
2169
+ "server.ts",
2170
+ "server.js",
2171
+ "index.ts",
2172
+ "index.js",
2173
+ "app.ts",
2174
+ "app.js"
2175
+ ];
2176
+ const hits = names.filter((n) => existsSync9(join8(cwd, n)));
2177
+ try {
2178
+ const pkg = JSON.parse(read(join8(cwd, "package.json")));
2179
+ if (typeof pkg.main === "string" && existsSync9(join8(cwd, pkg.main))) hits.push(pkg.main);
2180
+ } catch {
2181
+ }
2182
+ return [...new Set(hits)];
2183
+ }
2184
+ function usesExpress(cwd) {
2185
+ try {
2186
+ const pkg = JSON.parse(read(join8(cwd, "package.json")));
2187
+ return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
2188
+ } catch {
2189
+ return false;
2190
+ }
2191
+ }
2192
+ function wiringPlan(cwd, dir) {
2193
+ const entries = candidateEntries(cwd);
2194
+ const express = usesExpress(cwd);
2195
+ const lines = [
2196
+ `no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/guard.ts + ${dir}/rules.json.`,
2197
+ "Finish by wiring it into your server (pick the one that fits):",
2198
+ ` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/guard"`,
2199
+ ` \u2022 Node / Express: app.use(patchstackMiddleware) // add before your routes`,
2200
+ 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.",
2201
+ "Then confirm it is hooked up: npx patchstack-connect protect --check"
2202
+ ];
2203
+ return lines.join("\n");
2204
+ }
2205
+ function genericVerify(cwd) {
2206
+ const dir = genericDir(cwd);
2207
+ const scaffolded = existsSync9(join8(cwd, dir, "guard.ts"));
2208
+ const imported = scaffolded && guardIsImported(cwd, join8(cwd, dir, "guard.ts"));
2209
+ return {
2210
+ wired: scaffolded && imported,
2211
+ checks: [
2212
+ { label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
2213
+ {
2214
+ label: "guard imported into a server entry",
2215
+ ok: imported,
2216
+ hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/guard" and wire it into your request path`
2217
+ }
2218
+ ]
2219
+ };
2220
+ }
2221
+ function guardIsImported(cwd, guardPath) {
2222
+ const root = existsSync9(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
2223
+ let found = false;
2224
+ const walk2 = (d) => {
2225
+ if (found) return;
2226
+ let entries;
2227
+ try {
2228
+ entries = readdirSync4(d);
2229
+ } catch {
2230
+ return;
2231
+ }
2232
+ for (const name of entries) {
2233
+ if (found) return;
2234
+ if (name === "node_modules" || name.startsWith(".")) continue;
2235
+ const p = join8(d, name);
2236
+ let st;
2237
+ try {
2238
+ st = statSync4(p);
2239
+ } catch {
2240
+ continue;
2241
+ }
2242
+ if (st.isDirectory()) walk2(p);
2243
+ else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
2244
+ try {
2245
+ if (readFileSync5(p, "utf8").includes(GUARD_MARKER)) found = true;
2246
+ } catch {
2247
+ }
2248
+ }
2249
+ }
2250
+ };
2251
+ walk2(root);
2252
+ return found;
2253
+ }
2254
+
2255
+ // src/protect/install/register.ts
2256
+ var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
2257
+ function wireRegister(cwd, opts, spec) {
2258
+ const { changed, dir } = scaffoldGeneric(cwd, opts, spec.guardTemplate);
2259
+ const entry = findAppInstance(cwd, spec.appRe);
2260
+ if (!entry) {
2261
+ log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
2262
+ return { ok: true, changed };
2263
+ }
2264
+ const p = join9(cwd, entry.relPath);
2265
+ const s = read(p);
2266
+ if (s.includes(spec.importName)) {
2267
+ log(`${entry.relPath} already wired`);
2268
+ return { ok: true, changed };
2269
+ }
2270
+ const importLine = `import { ${spec.importName} } from "${importSpecifier(entry.relPath, `${dir}/guard`)}";`;
2271
+ const lines = s.split("\n");
2272
+ let lastImport = -1;
2273
+ for (let i = 0; i < lines.length; i++) if (/^\s*import\b/.test(lines[i] ?? "")) lastImport = i;
2274
+ lines.splice(lastImport + 1, 0, importLine);
2275
+ const appIdx = lines.findIndex((l) => spec.appRe.test(l));
2276
+ if (appIdx !== -1) {
2277
+ lines.splice(appIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
2278
+ }
2279
+ writeFileSync5(p, lines.join("\n"));
2280
+ changed.push(entry.relPath);
2281
+ log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
2282
+ return { ok: true, changed: [...new Set(changed)] };
2283
+ }
2284
+ function verifyRegister(cwd, spec) {
2285
+ const dir = existsSync10(join9(cwd, "src")) ? "src/patchstack" : "patchstack";
2286
+ const scaffolded = existsSync10(join9(cwd, dir, "guard.ts"));
2287
+ const entry = findAppInstance(cwd, spec.appRe);
2288
+ const wired = entry ? read(join9(cwd, entry.relPath)).includes(spec.importName) : false;
2289
+ return {
2290
+ wired: scaffolded && wired,
2291
+ checks: [
2292
+ { label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
2293
+ { label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
2294
+ ]
2295
+ };
2296
+ }
2297
+
2298
+ // src/protect/install/adapters/nestjs.ts
2299
+ var SPEC3 = {
2300
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+NestFactory\.create/,
2301
+ guardTemplate: "generic-guard.ts",
2302
+ importName: "patchstackMiddleware",
2303
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2304
+ label: "NestJS app",
2305
+ manualHint: "add `app.use(patchstackMiddleware)` right after your NestFactory.create(...) call"
2306
+ };
2307
+ var nestjsAdapter = {
2308
+ name: "nestjs",
2309
+ label: "NestJS",
2310
+ detect: (cwd) => hasDependency(cwd, "@nestjs/core") && findAppInstance(cwd, SPEC3.appRe) !== null,
2311
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC3),
2312
+ verify: (cwd) => verifyRegister(cwd, SPEC3)
2313
+ };
2314
+
2315
+ // src/protect/install/adapters/fastify.ts
2316
+ var SPEC4 = {
2317
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:fastify|Fastify)\(/,
2318
+ guardTemplate: "fastify-plugin.ts",
2319
+ importName: "patchstackFastify",
2320
+ call: (v) => `${v}.register(patchstackFastify);`,
2321
+ label: "Fastify app",
2322
+ manualHint: "add `app.register(patchstackFastify)` right after you create your fastify() app"
2323
+ };
2324
+ var fastifyAdapter = {
2325
+ name: "fastify",
2326
+ label: "Fastify (Node)",
2327
+ detect: (cwd) => hasDependency(cwd, "fastify") && findAppInstance(cwd, SPEC4.appRe) !== null,
2328
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC4),
2329
+ verify: (cwd) => verifyRegister(cwd, SPEC4)
2330
+ };
2331
+
2332
+ // src/protect/install/adapters/express.ts
2333
+ var SPEC5 = {
2334
+ appRe: /(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*express\(\)/,
2335
+ guardTemplate: "generic-guard.ts",
2336
+ importName: "patchstackMiddleware",
2337
+ call: (v) => `${v}.use(patchstackMiddleware);`,
2338
+ label: "Express app",
2339
+ manualHint: "add `app.use(patchstackMiddleware)` right after you create your express() app"
2340
+ };
2341
+ var expressAdapter = {
2342
+ name: "express",
2343
+ label: "Express (Node)",
2344
+ detect: (cwd) => hasDependency(cwd, "express") && findAppInstance(cwd, SPEC5.appRe) !== null,
2345
+ wire: (cwd, opts) => wireRegister(cwd, opts, SPEC5),
2346
+ verify: (cwd) => verifyRegister(cwd, SPEC5)
2347
+ };
2348
+
2349
+ // src/protect/install/index.ts
2350
+ var ADAPTERS = [
2351
+ tanstackSupabaseAdapter,
2352
+ nextAdapter,
2353
+ sveltekitAdapter,
2354
+ astroAdapter,
2355
+ nestjsAdapter,
2356
+ fastifyAdapter,
2357
+ expressAdapter
2358
+ ];
2359
+ function runProtect(cwd, opts = {}) {
2360
+ const adapter = ADAPTERS.find((a) => a.detect(cwd));
2361
+ if (adapter) {
2362
+ const result = adapter.wire(cwd, opts);
2363
+ return { status: "wired", adapter: adapter.name, changed: result.changed };
2364
+ }
2365
+ const { changed, dir } = scaffoldGeneric(cwd, opts);
2366
+ const plan = wiringPlan(cwd, dir);
2367
+ log(plan);
2368
+ return { status: "scaffolded", adapter: "generic", changed, plan };
2369
+ }
2370
+ function runVerify(cwd) {
2371
+ const adapter = ADAPTERS.find((a) => a.detect(cwd));
2372
+ if (adapter) return { stack: adapter.label, ...adapter.verify(cwd) };
2373
+ return { stack: "generic", ...genericVerify(cwd) };
2374
+ }
2375
+
2376
+ // src/setup.ts
2377
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
2378
+ import path10 from "path";
2379
+ var SCAN_COMMAND = "patchstack-connect scan";
2380
+ var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
2381
+ function appendHook(existing, command) {
2382
+ if (existing === void 0 || existing.trim().length === 0) {
2383
+ return command;
2384
+ }
2385
+ if (existing.includes(command)) {
2386
+ return existing;
2387
+ }
2388
+ return `${existing} && ${command}`;
2389
+ }
2390
+ function wireBuildScripts(cwd, packageManager) {
2391
+ const target = path10.join(cwd, "package.json");
2392
+ const raw = readFileSync6(target, "utf8");
2393
+ const pkg = JSON.parse(raw);
2394
+ const scripts = pkg.scripts ?? {};
2395
+ const build = scripts.build;
2396
+ if (build === void 0 || build.trim().length === 0) {
2397
+ return {
2398
+ changed: false,
2399
+ strategy: "skipped",
2400
+ detail: "package.json has no build script; no build integration was added."
2401
+ };
2402
+ }
2403
+ if (packageManager === "bun") {
2404
+ let nextBuild = build;
2405
+ if (!nextBuild.includes(SCAN_COMMAND)) {
2406
+ nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
2407
+ }
2408
+ if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
2409
+ nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
2410
+ }
2411
+ if (nextBuild === build) {
2412
+ return {
2413
+ changed: false,
2414
+ strategy: "build-chain",
2415
+ detail: "build script already runs scan and mark-build."
2416
+ };
2417
+ }
2418
+ scripts.build = nextBuild;
2419
+ } else {
2420
+ const prebuild = appendHook(scripts.prebuild, SCAN_COMMAND);
2421
+ const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
2422
+ if (prebuild === scripts.prebuild && postbuild === scripts.postbuild) {
2423
+ return {
2424
+ changed: false,
2425
+ strategy: "lifecycle-hooks",
2426
+ detail: "prebuild and postbuild hooks are already wired."
2427
+ };
2428
+ }
2429
+ scripts.prebuild = prebuild;
2430
+ scripts.postbuild = postbuild;
2431
+ }
2432
+ pkg.scripts = scripts;
2433
+ const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
2434
+ const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
2435
+ const trailingNewline = raw.endsWith("\n") ? "\n" : "";
2436
+ writeFileSync6(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
2437
+ return packageManager === "bun" ? {
2438
+ changed: true,
2439
+ strategy: "build-chain",
2440
+ detail: "chained scan and mark-build around the existing build command."
2441
+ } : {
2442
+ changed: true,
2443
+ strategy: "lifecycle-hooks",
2444
+ detail: "added scan to prebuild and mark-build to postbuild."
2445
+ };
1803
2446
  }
1804
2447
 
1805
2448
  // src/cli.ts
@@ -1814,22 +2457,30 @@ Usage:
1814
2457
  HTML shell (index.html, public/index.html,
1815
2458
  or src/app.html) \u2014 opt out with
1816
2459
  "widget": false in .patchstackrc.json
2460
+ patchstack-connect setup [options] Finish the bounded project setup: run scan,
2461
+ manage the widget, and wire package.json build
2462
+ scripts. Never runs a build or protect
1817
2463
  patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
1818
2464
  with an existing site UUID
1819
2465
  patchstack-connect status [options] Show current configuration
1820
2466
  patchstack-connect mark-build [options] Stamp built HTML with a production flag +
1821
2467
  build fingerprint, and ensure the widget
1822
2468
  tag in built pages (run as a postbuild step)
1823
- patchstack-connect protect Install always-on runtime protection (the
1824
- guard) into a TanStack Start + Supabase app.
1825
- Covers the browser + server-function paths.
2469
+ patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
2470
+ guard). Auto-wires known stacks (TanStack
2471
+ Start + Supabase); for others it scaffolds a
2472
+ generic guard + prints a wiring plan.
2473
+ --demo seeds a broad sample rule set (for
2474
+ demonstrations, not production).
2475
+ --check verifies the guard is wired (exit 1
2476
+ if not) \u2014 for the wire-then-verify loop.
1826
2477
  patchstack-connect guide [--full] Show this project's setup status (what's done,
1827
2478
  what's missing, with tailored commands), then
1828
2479
  print the full setup guide. --full prints the
1829
2480
  guide even when setup is complete
1830
2481
  patchstack-connect help Print this message
1831
2482
 
1832
- Options (for scan and status):
2483
+ Options (for scan, setup, and status):
1833
2484
  --site-uuid <uuid> Override the configured site UUID
1834
2485
  --endpoint <url> Override the API endpoint
1835
2486
  --dry-run (scan only) Show the payload without posting
@@ -1843,10 +2494,13 @@ Environment:
1843
2494
  PATCHSTACK_ENDPOINT API endpoint (default: https://api.patchstack.com/monitor/pulse/manifest)
1844
2495
  PATCHSTACK_TIMEOUT_MS Request timeout in ms (default: 30000)
1845
2496
  PATCHSTACK_ENVIRONMENT Manifest environment: production | sandbox (default: production)
2497
+ PATCHSTACK_MODE (protect) Runtime guard mode: block (default) | dry-run
2498
+ PATCHSTACK_ROUTE_WAF (protect) Set to 1 to also screen every request at the route level (opt-in)
1846
2499
 
1847
2500
  Precedence: CLI flag > environment variable > .patchstackrc.json.
1848
2501
 
1849
2502
  Examples:
2503
+ npx @patchstack/connect setup
1850
2504
  npx @patchstack/connect scan
1851
2505
  npx @patchstack/connect scan --dry-run
1852
2506
  npx @patchstack/connect init 550e8400-e29b-41d4-a716-446655440000
@@ -1904,7 +2558,7 @@ async function runInit(args) {
1904
2558
  console.log("Next: run `npx @patchstack/connect scan` to send your first manifest.");
1905
2559
  return 0;
1906
2560
  }
1907
- async function runScan(args) {
2561
+ async function runScan(args, options = {}) {
1908
2562
  const dryRun = args.flags.get("dry-run") === true;
1909
2563
  const config = await resolveConfig({
1910
2564
  cwd: process.cwd(),
@@ -1971,24 +2625,26 @@ async function runScan(args) {
1971
2625
  }
1972
2626
  if (provisioning && response.uuid !== void 0 && response.uuid.length > 0) {
1973
2627
  console.log("");
1974
- console.log("Claim this site to view vulnerability reports in your Patchstack dashboard:");
2628
+ console.log("Open this dashboard link to view vulnerability reports:");
1975
2629
  console.log(` ${buildClaimUrl(config.endpoint, response.uuid)}`);
1976
2630
  if (config.endpoint !== DEFAULT_ENDPOINT) {
1977
2631
  console.log(" (this URL inherits the endpoint override above)");
1978
2632
  }
1979
2633
  }
1980
- try {
1981
- const state = await collectGuideState(process.cwd());
1982
- const remaining = countRemainingSteps(state);
1983
- if (remaining > 0) {
1984
- const hooksMissing = !(state.prebuildWired && state.postbuildWired);
1985
- console.log("");
1986
- console.log(
1987
- `Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
1988
- );
1989
- console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
2634
+ if (options.showRemainingSetup !== false) {
2635
+ try {
2636
+ const state = await collectGuideState(process.cwd());
2637
+ const remaining = countRemainingSteps(state);
2638
+ if (remaining > 0) {
2639
+ const hooksMissing = !(state.prebuildWired && state.postbuildWired);
2640
+ console.log("");
2641
+ console.log(
2642
+ `Setup not complete \u2014 ${remaining} step(s) remaining${hooksMissing ? ", including the package.json build hooks (which scan can't wire)" : ""}.`
2643
+ );
2644
+ console.log("Run `npx @patchstack/connect guide` for the exact steps to finish for this project.");
2645
+ }
2646
+ } catch {
1990
2647
  }
1991
- } catch {
1992
2648
  }
1993
2649
  return 0;
1994
2650
  }
@@ -2025,9 +2681,19 @@ function reportSourceWidget(siteUuid) {
2025
2681
  console.warn(`Widget: skipped (${err.message}).`);
2026
2682
  }
2027
2683
  }
2028
- async function runProtectCommand(_args) {
2684
+ async function runProtectCommand(args) {
2685
+ if (args.flags.get("check") === true) {
2686
+ const report = runVerify(process.cwd());
2687
+ console.log(`patchstack protect --check (${report.stack}):`);
2688
+ for (const c of report.checks) {
2689
+ console.log(` ${c.ok ? "\u2713" : "\u2717"} ${c.label}${!c.ok && c.hint ? ` \u2014 ${c.hint}` : ""}`);
2690
+ }
2691
+ console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
2692
+ return report.wired ? 0 : 1;
2693
+ }
2694
+ const demo = args.flags.get("demo") === true;
2029
2695
  try {
2030
- runProtect(process.cwd());
2696
+ runProtect(process.cwd(), { demo });
2031
2697
  } catch (err) {
2032
2698
  console.warn(`patchstack protect: skipped (${err.message}).`);
2033
2699
  }
@@ -2053,7 +2719,46 @@ async function runGuide(args) {
2053
2719
  console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
2054
2720
  console.log("");
2055
2721
  const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
2056
- console.log(readFileSync4(guidePath, "utf8"));
2722
+ console.log(readFileSync7(guidePath, "utf8"));
2723
+ return 0;
2724
+ }
2725
+ async function runSetup(args) {
2726
+ if (args.flags.get("dry-run") === true) {
2727
+ console.error("Error: setup does not support --dry-run. Use `scan --dry-run` to preview the manifest.");
2728
+ return 1;
2729
+ }
2730
+ const before = await collectGuideState(process.cwd());
2731
+ if (!before.hasPackageJson) {
2732
+ console.error("Error: no package.json found. Run setup from the project root.");
2733
+ return 1;
2734
+ }
2735
+ if (before.installed === null) {
2736
+ console.error(
2737
+ `Error: @patchstack/connect is not declared in package.json.
2738
+ Run: ${installCommand(before.packageManager)}`
2739
+ );
2740
+ return 1;
2741
+ }
2742
+ console.log("Patchstack setup \u2014 applying bounded project changes");
2743
+ console.log(" 1. Scan dependencies, provision/reuse the site, and manage the source widget");
2744
+ const scanCode = await runScan(args, { showRemainingSetup: false });
2745
+ if (scanCode !== 0) {
2746
+ return scanCode;
2747
+ }
2748
+ console.log("");
2749
+ console.log(" 2. Wire scan and mark-build into package.json");
2750
+ const wired = wireBuildScripts(process.cwd(), before.packageManager);
2751
+ console.log(`Build integration: ${wired.detail}`);
2752
+ console.log("");
2753
+ console.log(" 3. Verify setup status");
2754
+ const after = await collectGuideState(process.cwd());
2755
+ const useColor = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
2756
+ console.log(renderGuideChecklist(after, useColor));
2757
+ const remaining = countRemainingSteps(after);
2758
+ if (remaining > 0) {
2759
+ console.log("");
2760
+ console.log(`Setup applied its bounded changes; ${remaining} manual step(s) remain above.`);
2761
+ }
2057
2762
  return 0;
2058
2763
  }
2059
2764
  async function runStatus(args) {
@@ -2069,7 +2774,7 @@ async function runStatus(args) {
2069
2774
  console.log(`Timeout: ${config.timeoutMs}ms`);
2070
2775
  console.log(`Environment: ${config.environment}`);
2071
2776
  if (config.siteUuid !== null) {
2072
- console.log(`Claim URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
2777
+ console.log(`Dashboard URL: ${buildClaimUrl(config.endpoint, config.siteUuid)}`);
2073
2778
  }
2074
2779
  return 0;
2075
2780
  }
@@ -2129,7 +2834,7 @@ async function runMarkBuild(args) {
2129
2834
  let marked = 0;
2130
2835
  let widgetTouched = 0;
2131
2836
  for (const file of files) {
2132
- const before = readFileSync4(file, "utf8");
2837
+ const before = readFileSync7(file, "utf8");
2133
2838
  let after = injectMarker(before, snippet);
2134
2839
  if (widgetUuid !== null) {
2135
2840
  const ensured = ensureWidgetInHtml(after, widgetUuid);
@@ -2139,7 +2844,7 @@ async function runMarkBuild(args) {
2139
2844
  after = ensured.html;
2140
2845
  }
2141
2846
  if (after !== before) {
2142
- writeFileSync3(file, after);
2847
+ writeFileSync7(file, after);
2143
2848
  marked += 1;
2144
2849
  }
2145
2850
  }
@@ -2168,6 +2873,8 @@ async function main() {
2168
2873
  return runProtectCommand(args);
2169
2874
  case "guide":
2170
2875
  return runGuide(args);
2876
+ case "setup":
2877
+ return runSetup(args);
2171
2878
  default:
2172
2879
  console.error(`Unknown command: ${args.command}
2173
2880
  `);