@patchstack/connect 0.3.18 → 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/AGENT-INSTALL.md +33 -18
- package/README.md +28 -11
- package/dist/cli.js +1204 -1071
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/express-guard.cjs +5 -1
- package/dist/protect/templates/express-guard.js +5 -1
- package/dist/protect/templates/express-guard.ts +5 -1
- package/dist/protect/templates/fastify-plugin.cjs +46 -0
- package/dist/protect/templates/fastify-plugin.js +45 -0
- package/dist/protect/templates/generic-guard.cjs +41 -0
- package/dist/protect/templates/generic-guard.js +42 -0
- package/dist/protect/templates/generic-guard.ts +5 -1
- package/dist/protect/templates/guard.ts +5 -1
- package/dist/protect/templates/nuxt-middleware.ts +43 -0
- package/dist/protect.cjs +1692 -92
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +24 -0
- package/dist/protect.js +501 -82
- package/dist/protect.js.map +1 -1
- package/dist/refresh-manifest-XBOGN446.js +1090 -0
- package/dist/refresh-manifest-XBOGN446.js.map +1 -0
- package/package.json +1 -1
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
|
|
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/
|
|
1471
|
-
import { existsSync as existsSync2,
|
|
1472
|
-
import
|
|
1473
|
-
|
|
1474
|
-
var
|
|
1475
|
-
var
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
);
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
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
|
-
|
|
1497
|
-
|
|
1498
|
-
return
|
|
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
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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/
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
{
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
"
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
"
|
|
1568
|
-
"
|
|
1569
|
-
"
|
|
1570
|
-
"
|
|
1571
|
-
".
|
|
1572
|
-
"
|
|
1573
|
-
"
|
|
1574
|
-
"
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
"
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
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
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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
|
|
1596
|
+
function detect(cwd) {
|
|
1597
|
+
const pkgPath = join3(cwd, "package.json");
|
|
1598
|
+
if (!existsSync3(pkgPath)) return false;
|
|
1599
|
+
let pkg;
|
|
1620
1600
|
try {
|
|
1621
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
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
|
|
1627
|
+
return changed;
|
|
1695
1628
|
}
|
|
1696
|
-
|
|
1697
|
-
const
|
|
1698
|
-
const
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
|
1769
|
-
const
|
|
1770
|
-
|
|
1771
|
-
const
|
|
1772
|
-
const
|
|
1773
|
-
const
|
|
1774
|
-
|
|
1775
|
-
(
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
-
|
|
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/
|
|
1877
|
-
import {
|
|
1878
|
-
import { join as
|
|
1879
|
-
|
|
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(
|
|
1885
|
-
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }
|
|
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
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
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
|
-
|
|
1921
|
-
|
|
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
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
""
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
-
|
|
1991
|
-
if (
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
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
|
-
|
|
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
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
|
|
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(
|
|
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
|
-
|
|
2015
|
-
|
|
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
|
|
2036
|
-
|
|
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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
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
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
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
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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
|
|
2089
|
-
const
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
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
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
|
|
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(
|
|
2127
|
-
|
|
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
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
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
|
|
2172
|
-
const
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
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:
|
|
2072
|
+
wired: scaffolded && imported,
|
|
2177
2073
|
checks: [
|
|
2178
|
-
{ label: "
|
|
2179
|
-
{
|
|
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
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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/
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
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
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
2226
|
-
const
|
|
2227
|
-
const
|
|
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:
|
|
2209
|
+
wired: scaffolded && wired,
|
|
2230
2210
|
checks: [
|
|
2231
|
-
{ label: `${spec.
|
|
2232
|
-
{ label:
|
|
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/
|
|
2238
|
-
var
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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/
|
|
2269
|
-
import {
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
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
|
|
2287
|
-
|
|
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
|
|
2290
|
-
|
|
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/
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
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
|
|
2456
|
+
return "npm";
|
|
2373
2457
|
}
|
|
2374
|
-
function
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
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
|
-
|
|
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
|
|
2468
|
+
function installedVersion(cwd, declaredRange) {
|
|
2399
2469
|
try {
|
|
2400
|
-
const pkg = JSON.parse(
|
|
2401
|
-
|
|
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
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
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(
|
|
2491
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2443
2492
|
} catch {
|
|
2444
2493
|
return;
|
|
2445
2494
|
}
|
|
2446
|
-
for (const
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
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 (
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
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(
|
|
2466
|
-
return
|
|
2527
|
+
walk2(cwd, 0);
|
|
2528
|
+
return {
|
|
2529
|
+
found: sawWidget,
|
|
2530
|
+
uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
|
|
2531
|
+
};
|
|
2467
2532
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
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
|
-
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
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
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
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
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
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
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
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
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
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
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
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
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
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
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
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: "
|
|
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: "
|
|
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
|
|
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,
|
|
2737
|
-
|
|
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.
|
|
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("
|
|
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));
|