@patchstack/connect 0.2.7 → 0.2.9
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/README.md +23 -1
- package/dist/cli.js +297 -34
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +133 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +126 -27
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/engine.d.ts +57 -0
- package/dist/protect/templates/engine.js +137 -0
- package/dist/protect/templates/guard.ts +100 -0
- package/dist/protect/templates/manifest.d.ts +1 -0
- package/dist/protect/templates/rules.json +22 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -17,9 +17,11 @@ npm install --save-dev @patchstack/connect
|
|
|
17
17
|
npx @patchstack/connect scan
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
+
> **Use your project's own package manager.** On bun-managed projects (Lovable, Bolt, most vibe-coding platforms) install with `bun add -d @patchstack/connect` instead — running `npm install` there plants a `package-lock.json` that the platform's native dependency flow never updates again, leaving a stale lockfile next to the live one. The connector detects and works around that (see *Stale lockfiles* below), but not creating the fossil is better.
|
|
21
|
+
|
|
20
22
|
That's it. The first `scan`:
|
|
21
23
|
|
|
22
|
-
1. Reads your
|
|
24
|
+
1. Reads your lockfile (see *Supported lockfiles*).
|
|
23
25
|
2. POSTs the package list to Patchstack with **no** UUID.
|
|
24
26
|
3. Patchstack provisions a fresh site and returns its UUID.
|
|
25
27
|
4. The connector writes the UUID to `.patchstackrc.json` so the next `scan` targets the same site.
|
|
@@ -123,6 +125,10 @@ That's the entire payload. No source code, no environment variables, no file pat
|
|
|
123
125
|
|
|
124
126
|
If both a Bun lockfile and `node_modules/` are present, the connector walks `node_modules/` to enumerate the installed packages. Run `bun install` (or `npm install`) before scanning so the directory is populated.
|
|
125
127
|
|
|
128
|
+
### Stale lockfiles
|
|
129
|
+
|
|
130
|
+
Every scanned source is validated against `package.json`: if the chosen lockfile is missing dependencies that `package.json` declares, it is treated as a fossil (e.g. a `package-lock.json` created by a one-off `npm install` in a bun-managed project) and the connector falls through to the next source — ultimately walking `node_modules/`, the installed truth — and prints a warning naming the stale file. Delete the stale lockfile to silence the warning. Without this, the manifest and the build fingerprint would silently freeze while the real dependency set drifts.
|
|
131
|
+
|
|
126
132
|
## Development
|
|
127
133
|
|
|
128
134
|
```bash
|
|
@@ -132,6 +138,22 @@ npm test
|
|
|
132
138
|
npm run build
|
|
133
139
|
```
|
|
134
140
|
|
|
141
|
+
### Manifest endpoint testing
|
|
142
|
+
|
|
143
|
+
To post the current lockfile manifest to a local Patchstack API endpoint and provision a new site:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The response should include the new site UUID. To re-test an existing site, pass that UUID explicitly:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
bun run test:manifest -- --endpoint http://localhost:8000/monitor/pulse/manifest --site-uuid YOUR_REAL_UUID
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Use `--dry-run` to preview the payload without posting.
|
|
156
|
+
|
|
135
157
|
## Release process
|
|
136
158
|
|
|
137
159
|
Pull requests run typecheck, tests, build, package verification, and a production dependency audit in GitHub Actions.
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
8
|
-
import
|
|
8
|
+
import path5 from "path";
|
|
9
9
|
|
|
10
10
|
// src/types.ts
|
|
11
11
|
var PatchstackError = class extends Error {
|
|
@@ -19,13 +19,52 @@ var PatchstackError = class extends Error {
|
|
|
19
19
|
cause;
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
// src/parsers/
|
|
22
|
+
// src/parsers/consistency.ts
|
|
23
23
|
import { readFile } from "fs/promises";
|
|
24
24
|
import path from "path";
|
|
25
|
+
async function readDeclaredDependencyNames(cwd) {
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = await readFile(path.join(cwd, "package.json"), "utf8");
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(raw);
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const names = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const group of ["dependencies", "devDependencies"]) {
|
|
43
|
+
const deps = parsed[group];
|
|
44
|
+
if (typeof deps !== "object" || deps === null) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
48
|
+
if (typeof spec === "string" && /^(file|link|workspace|portal):/.test(spec)) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
names.add(name);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [...names];
|
|
55
|
+
}
|
|
56
|
+
function missingDependencies(declared, packages) {
|
|
57
|
+
const present = new Set(packages.map((entry) => entry.name));
|
|
58
|
+
return declared.filter((name) => !present.has(name));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/parsers/npm.ts
|
|
62
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
63
|
+
import path2 from "path";
|
|
25
64
|
async function parseNpmLockfile(lockfilePath) {
|
|
26
65
|
let raw;
|
|
27
66
|
try {
|
|
28
|
-
raw = await
|
|
67
|
+
raw = await readFile2(lockfilePath, "utf8");
|
|
29
68
|
} catch (cause) {
|
|
30
69
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
31
70
|
}
|
|
@@ -83,7 +122,7 @@ function extractFromV1(deps, acc = [], depth = 0) {
|
|
|
83
122
|
return acc;
|
|
84
123
|
}
|
|
85
124
|
function extractNameFromPath(pkgPath) {
|
|
86
|
-
const segments = pkgPath.split("node_modules" +
|
|
125
|
+
const segments = pkgPath.split("node_modules" + path2.sep === pkgPath ? path2.sep : "/");
|
|
87
126
|
const parts = pkgPath.split("/");
|
|
88
127
|
const nmIndex = parts.lastIndexOf("node_modules");
|
|
89
128
|
if (nmIndex === -1 || nmIndex >= parts.length - 1) {
|
|
@@ -106,10 +145,10 @@ function isDirectV2(pkgPath) {
|
|
|
106
145
|
}
|
|
107
146
|
|
|
108
147
|
// src/parsers/node_modules.ts
|
|
109
|
-
import { lstat, readFile as
|
|
110
|
-
import
|
|
148
|
+
import { lstat, readFile as readFile3, readdir, stat } from "fs/promises";
|
|
149
|
+
import path3 from "path";
|
|
111
150
|
async function walkNodeModules(cwd) {
|
|
112
|
-
const root =
|
|
151
|
+
const root = path3.join(cwd, "node_modules");
|
|
113
152
|
try {
|
|
114
153
|
const info = await stat(root);
|
|
115
154
|
if (!info.isDirectory()) {
|
|
@@ -143,7 +182,7 @@ async function walk(dir, acc, depth) {
|
|
|
143
182
|
if (name.startsWith(".")) {
|
|
144
183
|
continue;
|
|
145
184
|
}
|
|
146
|
-
const fullPath =
|
|
185
|
+
const fullPath = path3.join(dir, name);
|
|
147
186
|
if (!await isPlainDirectory(fullPath)) {
|
|
148
187
|
continue;
|
|
149
188
|
}
|
|
@@ -158,7 +197,7 @@ async function walk(dir, acc, depth) {
|
|
|
158
197
|
if (sub.startsWith(".")) {
|
|
159
198
|
continue;
|
|
160
199
|
}
|
|
161
|
-
const scopedDir =
|
|
200
|
+
const scopedDir = path3.join(fullPath, sub);
|
|
162
201
|
if (!await isPlainDirectory(scopedDir)) {
|
|
163
202
|
continue;
|
|
164
203
|
}
|
|
@@ -174,7 +213,7 @@ async function walk(dir, acc, depth) {
|
|
|
174
213
|
async function readPackage(pkgDir, depth, acc) {
|
|
175
214
|
let raw;
|
|
176
215
|
try {
|
|
177
|
-
raw = await
|
|
216
|
+
raw = await readFile3(path3.join(pkgDir, "package.json"), "utf8");
|
|
178
217
|
} catch {
|
|
179
218
|
return;
|
|
180
219
|
}
|
|
@@ -197,7 +236,7 @@ async function readPackage(pkgDir, depth, acc) {
|
|
|
197
236
|
});
|
|
198
237
|
}
|
|
199
238
|
async function walkNested(pkgDir, acc, depth) {
|
|
200
|
-
const nested =
|
|
239
|
+
const nested = path3.join(pkgDir, "node_modules");
|
|
201
240
|
if (!await isPlainDirectory(nested)) {
|
|
202
241
|
return;
|
|
203
242
|
}
|
|
@@ -213,11 +252,11 @@ async function isPlainDirectory(dir) {
|
|
|
213
252
|
}
|
|
214
253
|
|
|
215
254
|
// src/parsers/pnpm.ts
|
|
216
|
-
import { readFile as
|
|
255
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
217
256
|
async function parsePnpmLockfile(lockfilePath) {
|
|
218
257
|
let raw;
|
|
219
258
|
try {
|
|
220
|
-
raw = await
|
|
259
|
+
raw = await readFile4(lockfilePath, "utf8");
|
|
221
260
|
} catch (cause) {
|
|
222
261
|
throw new PatchstackError(
|
|
223
262
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -465,12 +504,12 @@ function extractLeafName(trimmed) {
|
|
|
465
504
|
}
|
|
466
505
|
|
|
467
506
|
// src/parsers/yarn.ts
|
|
468
|
-
import { readFile as
|
|
469
|
-
import
|
|
507
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
508
|
+
import path4 from "path";
|
|
470
509
|
async function parseYarnLockfile(lockfilePath) {
|
|
471
510
|
let raw;
|
|
472
511
|
try {
|
|
473
|
-
raw = await
|
|
512
|
+
raw = await readFile5(lockfilePath, "utf8");
|
|
474
513
|
} catch (cause) {
|
|
475
514
|
throw new PatchstackError(
|
|
476
515
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -485,7 +524,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
485
524
|
"LOCKFILE_PARSE_ERROR"
|
|
486
525
|
);
|
|
487
526
|
}
|
|
488
|
-
const directNames = await readDirectDepNames(
|
|
527
|
+
const directNames = await readDirectDepNames(path4.dirname(lockfilePath));
|
|
489
528
|
const entries = [];
|
|
490
529
|
const seen = /* @__PURE__ */ new Set();
|
|
491
530
|
for (const block of blocks) {
|
|
@@ -629,7 +668,7 @@ async function readDirectDepNames(cwd) {
|
|
|
629
668
|
const names = /* @__PURE__ */ new Set();
|
|
630
669
|
let raw;
|
|
631
670
|
try {
|
|
632
|
-
raw = await
|
|
671
|
+
raw = await readFile5(path4.join(cwd, "package.json"), "utf8");
|
|
633
672
|
} catch {
|
|
634
673
|
return names;
|
|
635
674
|
}
|
|
@@ -666,7 +705,7 @@ var LOCKFILE_CANDIDATES = [
|
|
|
666
705
|
async function detectLockfile(cwd) {
|
|
667
706
|
const probed = await Promise.all(
|
|
668
707
|
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
669
|
-
const filePath =
|
|
708
|
+
const filePath = path5.join(cwd, candidate.filename);
|
|
670
709
|
return { ...candidate, filePath, present: await exists(filePath) };
|
|
671
710
|
})
|
|
672
711
|
);
|
|
@@ -686,9 +725,69 @@ async function detectLockfile(cwd) {
|
|
|
686
725
|
);
|
|
687
726
|
}
|
|
688
727
|
async function scanLockfile(cwd) {
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
728
|
+
const candidates = await presentLockfiles(cwd);
|
|
729
|
+
if (candidates.length === 0) {
|
|
730
|
+
await detectLockfile(cwd);
|
|
731
|
+
}
|
|
732
|
+
const declared = await readDeclaredDependencyNames(cwd);
|
|
733
|
+
const warnings = [];
|
|
734
|
+
let firstParsed = null;
|
|
735
|
+
let walkTried = false;
|
|
736
|
+
for (const candidate of candidates) {
|
|
737
|
+
let packages;
|
|
738
|
+
try {
|
|
739
|
+
packages = await runStrategy(candidate, cwd);
|
|
740
|
+
} catch {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
walkTried ||= candidate.strategy === "node-modules-walk";
|
|
744
|
+
firstParsed ??= { packages, filename: candidate.filename };
|
|
745
|
+
const missing = missingDependencies(declared, packages);
|
|
746
|
+
if (missing.length === 0) {
|
|
747
|
+
return manifestWith(packages, warnings);
|
|
748
|
+
}
|
|
749
|
+
warnings.push(staleWarning(candidate.filename, missing));
|
|
750
|
+
}
|
|
751
|
+
if (!walkTried) {
|
|
752
|
+
try {
|
|
753
|
+
const packages = await walkNodeModules(cwd);
|
|
754
|
+
const missing = missingDependencies(declared, packages);
|
|
755
|
+
if (missing.length > 0) {
|
|
756
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
757
|
+
}
|
|
758
|
+
warnings.push("Scanned node_modules/ instead. Delete the stale lockfile to silence this warning.");
|
|
759
|
+
return manifestWith(packages, warnings);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (firstParsed === null) {
|
|
764
|
+
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
765
|
+
throw new PatchstackError(
|
|
766
|
+
`No readable lockfile found in ${cwd}. Expected one of: ${expected}.`,
|
|
767
|
+
"LOCKFILE_NOT_FOUND"
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
warnings.push(
|
|
771
|
+
`No fully-consistent source found; reporting ${firstParsed.filename}. The manifest may understate the real dependency set.`
|
|
772
|
+
);
|
|
773
|
+
return manifestWith(firstParsed.packages, warnings);
|
|
774
|
+
}
|
|
775
|
+
function manifestWith(packages, warnings) {
|
|
776
|
+
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
777
|
+
}
|
|
778
|
+
function staleWarning(source, missing) {
|
|
779
|
+
const sample = missing.slice(0, 3).join(", ");
|
|
780
|
+
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
781
|
+
return `${source} looks stale: package.json declares ${missing.length} dependenc${missing.length === 1 ? "y" : "ies"} it does not contain (${sample}${suffix}).`;
|
|
782
|
+
}
|
|
783
|
+
async function presentLockfiles(cwd) {
|
|
784
|
+
const probed = await Promise.all(
|
|
785
|
+
LOCKFILE_CANDIDATES.map(async (candidate) => {
|
|
786
|
+
const filePath = path5.join(cwd, candidate.filename);
|
|
787
|
+
return { ...candidate, filePath, present: await exists(filePath) };
|
|
788
|
+
})
|
|
789
|
+
);
|
|
790
|
+
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
692
791
|
}
|
|
693
792
|
async function runStrategy(detected, cwd) {
|
|
694
793
|
switch (detected.strategy) {
|
|
@@ -906,8 +1005,8 @@ function isTimeoutError(cause) {
|
|
|
906
1005
|
}
|
|
907
1006
|
|
|
908
1007
|
// src/config.ts
|
|
909
|
-
import { readFile as
|
|
910
|
-
import
|
|
1008
|
+
import { readFile as readFile6, writeFile } from "fs/promises";
|
|
1009
|
+
import path6 from "path";
|
|
911
1010
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
912
1011
|
var DEFAULT_ENVIRONMENT = "production";
|
|
913
1012
|
async function resolveConfig(options) {
|
|
@@ -944,7 +1043,7 @@ async function resolveConfig(options) {
|
|
|
944
1043
|
};
|
|
945
1044
|
}
|
|
946
1045
|
async function writeConfigFile(cwd, config) {
|
|
947
|
-
const target =
|
|
1046
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
948
1047
|
const content = JSON.stringify(config, null, 2) + "\n";
|
|
949
1048
|
await writeFile(target, content, "utf8");
|
|
950
1049
|
return target;
|
|
@@ -954,10 +1053,10 @@ async function persistSiteUuid(cwd, siteUuid) {
|
|
|
954
1053
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
955
1054
|
}
|
|
956
1055
|
async function readConfigFile(cwd) {
|
|
957
|
-
const target =
|
|
1056
|
+
const target = path6.join(cwd, CONFIG_FILENAME);
|
|
958
1057
|
let raw;
|
|
959
1058
|
try {
|
|
960
|
-
raw = await
|
|
1059
|
+
raw = await readFile6(target, "utf8");
|
|
961
1060
|
} catch (err) {
|
|
962
1061
|
if (err.code === "ENOENT") {
|
|
963
1062
|
return {};
|
|
@@ -1008,16 +1107,16 @@ function isEnvironment(value) {
|
|
|
1008
1107
|
|
|
1009
1108
|
// src/mark-build.ts
|
|
1010
1109
|
import { existsSync, readdirSync, statSync } from "fs";
|
|
1011
|
-
import
|
|
1110
|
+
import path7 from "path";
|
|
1012
1111
|
var MARKER_ATTR = "data-patchstack-build";
|
|
1013
1112
|
var BUILD_DIR_CANDIDATES = ["dist", "build", "out", ".output/public"];
|
|
1014
1113
|
function resolveBuildDir(cwd, override) {
|
|
1015
1114
|
if (override !== void 0 && override !== "") {
|
|
1016
|
-
const abs =
|
|
1115
|
+
const abs = path7.resolve(cwd, override);
|
|
1017
1116
|
return existsSync(abs) && statSync(abs).isDirectory() ? abs : null;
|
|
1018
1117
|
}
|
|
1019
1118
|
for (const candidate of BUILD_DIR_CANDIDATES) {
|
|
1020
|
-
const abs =
|
|
1119
|
+
const abs = path7.resolve(cwd, candidate);
|
|
1021
1120
|
if (existsSync(abs) && statSync(abs).isDirectory()) {
|
|
1022
1121
|
return abs;
|
|
1023
1122
|
}
|
|
@@ -1028,7 +1127,7 @@ function findHtmlFiles(dir) {
|
|
|
1028
1127
|
const out = [];
|
|
1029
1128
|
const walk2 = (current) => {
|
|
1030
1129
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
1031
|
-
const full =
|
|
1130
|
+
const full = path7.join(current, entry.name);
|
|
1032
1131
|
if (entry.isDirectory()) {
|
|
1033
1132
|
walk2(full);
|
|
1034
1133
|
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
|
|
@@ -1060,6 +1159,151 @@ function injectMarker(html, snippet) {
|
|
|
1060
1159
|
return stripped + snippet;
|
|
1061
1160
|
}
|
|
1062
1161
|
|
|
1162
|
+
// src/protect/install.ts
|
|
1163
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync, copyFileSync } from "fs";
|
|
1164
|
+
import { join } from "path";
|
|
1165
|
+
import { fileURLToPath } from "url";
|
|
1166
|
+
var TEMPLATES = fileURLToPath(new URL("./protect/templates/", import.meta.url));
|
|
1167
|
+
var CLIENT_TUNNEL = [
|
|
1168
|
+
"",
|
|
1169
|
+
" // PATCHSTACK auto-guard: in the browser, tunnel Supabase traffic through the app's own",
|
|
1170
|
+
" // server guard (same-origin) so payloads are inspected before they reach Supabase.",
|
|
1171
|
+
" if (typeof window !== 'undefined') {",
|
|
1172
|
+
" const target = typeof input === 'string' ? input : input instanceof Request ? input.url : String(input);",
|
|
1173
|
+
" const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
|
|
1174
|
+
" headers.set('x-ps-target', target);",
|
|
1175
|
+
" const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
|
|
1176
|
+
" return fetch(guardUrl, { ...init, method, headers });",
|
|
1177
|
+
" }",
|
|
1178
|
+
""
|
|
1179
|
+
].join("\n");
|
|
1180
|
+
var START_IMPORTS = [
|
|
1181
|
+
'import { getRequest } from "@tanstack/react-start/server";',
|
|
1182
|
+
'import { GUARD_PATH, handleGuardRequest } from "@/integrations/patchstack/guard";'
|
|
1183
|
+
].join("\n");
|
|
1184
|
+
var START_MIDDLEWARE = [
|
|
1185
|
+
"",
|
|
1186
|
+
"// Patchstack auto-guard: intercept the tunneled data traffic before anything else runs.",
|
|
1187
|
+
"const patchstackGuard = createMiddleware().server(async ({ next }) => {",
|
|
1188
|
+
" const request = getRequest();",
|
|
1189
|
+
" if (request) {",
|
|
1190
|
+
" const { pathname } = new URL(request.url);",
|
|
1191
|
+
" if (pathname === GUARD_PATH) return handleGuardRequest(request);",
|
|
1192
|
+
" }",
|
|
1193
|
+
" return next();",
|
|
1194
|
+
"});",
|
|
1195
|
+
""
|
|
1196
|
+
].join("\n");
|
|
1197
|
+
var PS_DIR_REL = "src/integrations/patchstack";
|
|
1198
|
+
function log(msg) {
|
|
1199
|
+
console.log(`patchstack protect: ${msg}`);
|
|
1200
|
+
}
|
|
1201
|
+
function detectSupportedStack(cwd) {
|
|
1202
|
+
const pkgPath = join(cwd, "package.json");
|
|
1203
|
+
if (!existsSync2(pkgPath)) return false;
|
|
1204
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
1205
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1206
|
+
return Boolean(deps["@tanstack/react-start"]) && existsSync2(join(cwd, "src/start.ts")) && existsSync2(join(cwd, "src/integrations/supabase/client.ts"));
|
|
1207
|
+
}
|
|
1208
|
+
function generateManifest(cwd) {
|
|
1209
|
+
const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
|
|
1210
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1211
|
+
const packages = {};
|
|
1212
|
+
for (const name of Object.keys(deps)) {
|
|
1213
|
+
let version = String(deps[name]).replace(/^[^\d]*/, "");
|
|
1214
|
+
const installed = join(cwd, "node_modules", name, "package.json");
|
|
1215
|
+
if (existsSync2(installed)) {
|
|
1216
|
+
try {
|
|
1217
|
+
version = JSON.parse(readFileSync(installed, "utf8")).version;
|
|
1218
|
+
} catch {
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
packages[name] = version;
|
|
1222
|
+
}
|
|
1223
|
+
mkdirSync(join(cwd, PS_DIR_REL), { recursive: true });
|
|
1224
|
+
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";
|
|
1225
|
+
writeFileSync(join(cwd, PS_DIR_REL, "manifest.js"), body);
|
|
1226
|
+
log(`wrote manifest.js (${Object.keys(packages).length} packages)`);
|
|
1227
|
+
}
|
|
1228
|
+
function scaffold(cwd) {
|
|
1229
|
+
const dst = join(cwd, PS_DIR_REL);
|
|
1230
|
+
mkdirSync(dst, { recursive: true });
|
|
1231
|
+
copyFileSync(join(TEMPLATES, "engine.js"), join(dst, "engine.js"));
|
|
1232
|
+
copyFileSync(join(TEMPLATES, "engine.d.ts"), join(dst, "engine.d.ts"));
|
|
1233
|
+
copyFileSync(join(TEMPLATES, "manifest.d.ts"), join(dst, "manifest.d.ts"));
|
|
1234
|
+
copyFileSync(join(TEMPLATES, "rules.json"), join(dst, "rules.json"));
|
|
1235
|
+
copyFileSync(join(TEMPLATES, "guard.ts"), join(dst, "guard.ts"));
|
|
1236
|
+
log("scaffolded engine.js (+types), rules.json, guard.ts");
|
|
1237
|
+
}
|
|
1238
|
+
function patchClient(cwd) {
|
|
1239
|
+
const p = join(cwd, "src/integrations/supabase/client.ts");
|
|
1240
|
+
let s = readFileSync(p, "utf8");
|
|
1241
|
+
if (s.includes("x-ps-target")) {
|
|
1242
|
+
log("client.ts already wired");
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const anchor = "headers.set('apikey', supabaseKey);";
|
|
1246
|
+
if (!s.includes(anchor)) {
|
|
1247
|
+
log("client.ts anchor not found (template changed?) \u2014 skipping client patch");
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
s = s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL);
|
|
1251
|
+
writeFileSync(p, s);
|
|
1252
|
+
log("patched client.ts (tunnel Supabase through the guard)");
|
|
1253
|
+
}
|
|
1254
|
+
function patchStart(cwd) {
|
|
1255
|
+
const p = join(cwd, "src/start.ts");
|
|
1256
|
+
let s = readFileSync(p, "utf8");
|
|
1257
|
+
if (s.includes("patchstackGuard")) {
|
|
1258
|
+
log("start.ts already wired");
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
|
|
1262
|
+
const exportAnchor = "export const startInstance";
|
|
1263
|
+
const rmAnchor = "requestMiddleware: [";
|
|
1264
|
+
if (!s.includes(importAnchor) || !s.includes(exportAnchor) || !s.includes(rmAnchor)) {
|
|
1265
|
+
log("start.ts anchors not found (template changed?) \u2014 skipping start patch");
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
s = s.replace(importAnchor, importAnchor + "\n" + START_IMPORTS);
|
|
1269
|
+
s = s.replace(exportAnchor, START_MIDDLEWARE + "\n" + exportAnchor);
|
|
1270
|
+
s = s.replace(rmAnchor, rmAnchor + "patchstackGuard, ");
|
|
1271
|
+
writeFileSync(p, s);
|
|
1272
|
+
log("patched start.ts (registered the guard as request middleware)");
|
|
1273
|
+
}
|
|
1274
|
+
function wirePrebuild(cwd) {
|
|
1275
|
+
const p = join(cwd, "package.json");
|
|
1276
|
+
const pkg = JSON.parse(readFileSync(p, "utf8"));
|
|
1277
|
+
pkg.scripts = pkg.scripts || {};
|
|
1278
|
+
const step = "patchstack-connect protect --manifest";
|
|
1279
|
+
const prebuild = pkg.scripts.prebuild || "";
|
|
1280
|
+
if (prebuild.includes(step)) {
|
|
1281
|
+
log("prebuild already refreshes the manifest");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
pkg.scripts.prebuild = prebuild ? prebuild + " && " + step : step;
|
|
1285
|
+
writeFileSync(p, JSON.stringify(pkg, null, 2) + "\n");
|
|
1286
|
+
log("wired manifest refresh into prebuild");
|
|
1287
|
+
}
|
|
1288
|
+
function runProtect(cwd, opts = {}) {
|
|
1289
|
+
if (opts.manifestOnly) {
|
|
1290
|
+
generateManifest(cwd);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
if (!detectSupportedStack(cwd)) {
|
|
1294
|
+
log(
|
|
1295
|
+
"runtime protection currently supports TanStack Start + Supabase apps; stack not detected \u2014 skipping."
|
|
1296
|
+
);
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
scaffold(cwd);
|
|
1300
|
+
patchClient(cwd);
|
|
1301
|
+
patchStart(cwd);
|
|
1302
|
+
generateManifest(cwd);
|
|
1303
|
+
wirePrebuild(cwd);
|
|
1304
|
+
log("done \u2014 guard wired. Set PATCHSTACK_MODE=block to enforce (default dry-run).");
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1063
1307
|
// src/cli.ts
|
|
1064
1308
|
var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
|
|
1065
1309
|
|
|
@@ -1072,6 +1316,9 @@ Usage:
|
|
|
1072
1316
|
patchstack-connect status [options] Show current configuration
|
|
1073
1317
|
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
1074
1318
|
build fingerprint (run as a postbuild step)
|
|
1319
|
+
patchstack-connect protect [--manifest] Install runtime protection (the guard) into
|
|
1320
|
+
a server-side JS app. --manifest only
|
|
1321
|
+
regenerates the package manifest (prebuild).
|
|
1075
1322
|
patchstack-connect help Print this message
|
|
1076
1323
|
|
|
1077
1324
|
Options (for scan and status):
|
|
@@ -1157,6 +1404,9 @@ async function runScan(args) {
|
|
|
1157
1404
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
1158
1405
|
});
|
|
1159
1406
|
const manifest = await scanLockfile(process.cwd());
|
|
1407
|
+
for (const warning of manifest.warnings ?? []) {
|
|
1408
|
+
console.warn(`patchstack: ${warning}`);
|
|
1409
|
+
}
|
|
1160
1410
|
const { payload, stats } = buildWirePayload(manifest);
|
|
1161
1411
|
console.log(
|
|
1162
1412
|
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names in ${manifest.ecosystem} lockfile.`
|
|
@@ -1206,6 +1456,14 @@ async function runScan(args) {
|
|
|
1206
1456
|
}
|
|
1207
1457
|
return 0;
|
|
1208
1458
|
}
|
|
1459
|
+
async function runProtectCommand(args) {
|
|
1460
|
+
try {
|
|
1461
|
+
runProtect(process.cwd(), { manifestOnly: args.flags.get("manifest") === true });
|
|
1462
|
+
} catch (err) {
|
|
1463
|
+
console.warn(`patchstack protect: skipped (${err.message}).`);
|
|
1464
|
+
}
|
|
1465
|
+
return 0;
|
|
1466
|
+
}
|
|
1209
1467
|
async function runStatus(args) {
|
|
1210
1468
|
const config = await resolveConfig({
|
|
1211
1469
|
cwd: process.cwd(),
|
|
@@ -1226,6 +1484,9 @@ async function runMarkBuild(args) {
|
|
|
1226
1484
|
let checksum = null;
|
|
1227
1485
|
try {
|
|
1228
1486
|
const manifest = await scanLockfile(cwd);
|
|
1487
|
+
for (const warning of manifest.warnings ?? []) {
|
|
1488
|
+
console.warn(`mark-build: ${warning}`);
|
|
1489
|
+
}
|
|
1229
1490
|
const { payload } = buildWirePayload(manifest);
|
|
1230
1491
|
checksum = computeManifestChecksum(payload.packages);
|
|
1231
1492
|
} catch (err) {
|
|
@@ -1248,10 +1509,10 @@ async function runMarkBuild(args) {
|
|
|
1248
1509
|
const snippet = buildInjectionSnippet(checksum);
|
|
1249
1510
|
let marked = 0;
|
|
1250
1511
|
for (const file of files) {
|
|
1251
|
-
const before =
|
|
1512
|
+
const before = readFileSync2(file, "utf8");
|
|
1252
1513
|
const after = injectMarker(before, snippet);
|
|
1253
1514
|
if (after !== before) {
|
|
1254
|
-
|
|
1515
|
+
writeFileSync2(file, after);
|
|
1255
1516
|
marked += 1;
|
|
1256
1517
|
}
|
|
1257
1518
|
}
|
|
@@ -1275,6 +1536,8 @@ async function main() {
|
|
|
1275
1536
|
return runStatus(args);
|
|
1276
1537
|
case "mark-build":
|
|
1277
1538
|
return runMarkBuild(args);
|
|
1539
|
+
case "protect":
|
|
1540
|
+
return runProtectCommand(args);
|
|
1278
1541
|
default:
|
|
1279
1542
|
console.error(`Unknown command: ${args.command}
|
|
1280
1543
|
`);
|