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