@patchstack/connect 0.3.29 → 0.3.31
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 +42 -23
- package/README.md +40 -11
- package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
- package/dist/chunk-LLKP5EJS.js.map +1 -0
- package/dist/cli.js +1265 -287
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +456 -111
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -10
- package/dist/index.d.ts +76 -10
- package/dist/index.js +449 -104
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +33 -13
- package/dist/protect/templates/demo-rules.json +2 -3
- package/dist/protect/templates/express-guard.cjs +26 -15
- package/dist/protect/templates/express-guard.js +26 -15
- package/dist/protect/templates/express-guard.ts +33 -16
- package/dist/protect/templates/fastify-plugin.cjs +33 -11
- package/dist/protect/templates/fastify-plugin.js +33 -11
- package/dist/protect/templates/fastify-plugin.ts +40 -12
- package/dist/protect/templates/generic-guard.cjs +28 -12
- package/dist/protect/templates/generic-guard.js +28 -13
- package/dist/protect/templates/generic-guard.ts +41 -20
- package/dist/protect/templates/guard.ts +47 -27
- package/dist/protect/templates/next-middleware.ts +29 -12
- package/dist/protect/templates/nuxt-middleware.ts +29 -12
- package/dist/protect/templates/rules.json +2 -2
- package/dist/protect/templates/sveltekit-hooks.ts +33 -13
- package/dist/protect.cjs +1204 -252
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +39 -11
- package/dist/protect.edge.js +792 -143
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +794 -145
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-KNZQBC4V.js} +387 -100
- package/dist/refresh-manifest-KNZQBC4V.js.map +1 -0
- package/package.json +5 -2
- package/dist/chunk-MJOTFUDE.js.map +0 -1
- package/dist/refresh-manifest-VRBE6RH6.js.map +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 readFileSync14, writeFileSync as
|
|
4
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
5
5
|
|
|
6
6
|
// src/parsers/index.ts
|
|
7
7
|
import { access } from "fs/promises";
|
|
@@ -39,7 +39,7 @@ async function readDeclaredDependencyNames(cwd) {
|
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
const names = /* @__PURE__ */ new Set();
|
|
42
|
-
for (const group of ["dependencies", "devDependencies"]) {
|
|
42
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
43
43
|
const deps = parsed[group];
|
|
44
44
|
if (typeof deps !== "object" || deps === null) {
|
|
45
45
|
continue;
|
|
@@ -57,14 +57,171 @@ function missingDependencies(declared, packages) {
|
|
|
57
57
|
const present = new Set(packages.map((entry) => entry.name));
|
|
58
58
|
return declared.filter((name) => !present.has(name));
|
|
59
59
|
}
|
|
60
|
+
function disagreements(packages, other) {
|
|
61
|
+
const otherVersions = /* @__PURE__ */ new Map();
|
|
62
|
+
for (const entry of other) {
|
|
63
|
+
if (!otherVersions.has(entry.name)) otherVersions.set(entry.name, entry.version);
|
|
64
|
+
}
|
|
65
|
+
const seen = /* @__PURE__ */ new Set();
|
|
66
|
+
const conflicts = [];
|
|
67
|
+
for (const entry of packages) {
|
|
68
|
+
if (seen.has(entry.name)) continue;
|
|
69
|
+
seen.add(entry.name);
|
|
70
|
+
const otherVersion = otherVersions.get(entry.name);
|
|
71
|
+
if (otherVersion !== void 0 && otherVersion !== entry.version) {
|
|
72
|
+
conflicts.push({ name: entry.name, version: entry.version, otherVersion });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return conflicts;
|
|
76
|
+
}
|
|
60
77
|
|
|
61
|
-
// src/parsers/
|
|
78
|
+
// src/parsers/bun.ts
|
|
62
79
|
import { readFile as readFile2 } from "fs/promises";
|
|
80
|
+
|
|
81
|
+
// src/parsers/report.ts
|
|
82
|
+
var MAX_SAMPLES = 3;
|
|
83
|
+
function newReport() {
|
|
84
|
+
return { unreadable: 0, samples: [] };
|
|
85
|
+
}
|
|
86
|
+
function recordUnreadable(report, sample) {
|
|
87
|
+
if (!report) return;
|
|
88
|
+
report.unreadable++;
|
|
89
|
+
if (report.samples.length < MAX_SAMPLES) report.samples.push(sample);
|
|
90
|
+
}
|
|
91
|
+
function unreadableWarning(filename, report) {
|
|
92
|
+
if (report.unreadable === 0) return null;
|
|
93
|
+
const shown = report.samples.join(", ");
|
|
94
|
+
const one = report.unreadable === 1;
|
|
95
|
+
return `${filename}: ${report.unreadable} entr${one ? "y was" : "ies were"} not in a form this scanner reads (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "it is" : "they are"} absent from this manifest. Anything installed only through ${one ? "that entry" : "those entries"} is not being checked.`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// src/parsers/bun.ts
|
|
99
|
+
async function parseBunLockfile(lockfilePath, report) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = await readFile2(lockfilePath, "utf8");
|
|
103
|
+
} catch (cause) {
|
|
104
|
+
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
105
|
+
}
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(stripTrailingCommas(raw));
|
|
109
|
+
} catch (cause) {
|
|
110
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} is not valid JSON`, "LOCKFILE_PARSE_ERROR", cause);
|
|
111
|
+
}
|
|
112
|
+
const packages = parsed?.packages;
|
|
113
|
+
if (typeof packages !== "object" || packages === null) {
|
|
114
|
+
throw new PatchstackError(`Lockfile at ${lockfilePath} has no "packages" entries`, "LOCKFILE_PARSE_ERROR");
|
|
115
|
+
}
|
|
116
|
+
const direct = directDependencyNames(parsed);
|
|
117
|
+
const entries = [];
|
|
118
|
+
let skipped = 0;
|
|
119
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
120
|
+
const descriptor = Array.isArray(value) ? value[0] : value;
|
|
121
|
+
if (typeof descriptor !== "string") {
|
|
122
|
+
skipped++;
|
|
123
|
+
recordUnreadable(report, key);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const split = splitDescriptor(descriptor);
|
|
127
|
+
if (split === null) {
|
|
128
|
+
skipped++;
|
|
129
|
+
if (!isDeliberatelyExcluded(descriptor)) recordUnreadable(report, key);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
entries.push({
|
|
133
|
+
name: split.name,
|
|
134
|
+
version: split.version,
|
|
135
|
+
...direct.has(split.name) ? { direct: true } : {},
|
|
136
|
+
...key === split.name ? {} : { path: key }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (entries.length === 0) {
|
|
140
|
+
throw new PatchstackError(
|
|
141
|
+
`Lockfile at ${lockfilePath} lists no registry packages${skipped > 0 ? ` (${skipped} non-registry entr${skipped === 1 ? "y" : "ies"} skipped)` : ""}`,
|
|
142
|
+
"LOCKFILE_PARSE_ERROR"
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return entries;
|
|
146
|
+
}
|
|
147
|
+
function splitDescriptor(descriptor) {
|
|
148
|
+
const at = descriptor.lastIndexOf("@");
|
|
149
|
+
if (at <= 0) return null;
|
|
150
|
+
const name = descriptor.slice(0, at);
|
|
151
|
+
const version = descriptor.slice(at + 1);
|
|
152
|
+
if (name === "" || version === "") return null;
|
|
153
|
+
if (NON_REGISTRY_VERSION.test(version)) return null;
|
|
154
|
+
return { name, version };
|
|
155
|
+
}
|
|
156
|
+
var NON_REGISTRY_VERSION = /^[a-z][a-z0-9+.-]*:|\//i;
|
|
157
|
+
function isDeliberatelyExcluded(descriptor) {
|
|
158
|
+
const at = descriptor.lastIndexOf("@");
|
|
159
|
+
if (at <= 0) return false;
|
|
160
|
+
return NON_REGISTRY_VERSION.test(descriptor.slice(at + 1));
|
|
161
|
+
}
|
|
162
|
+
function directDependencyNames(parsed) {
|
|
163
|
+
const names = /* @__PURE__ */ new Set();
|
|
164
|
+
const workspaces = parsed?.workspaces;
|
|
165
|
+
if (typeof workspaces !== "object" || workspaces === null) return names;
|
|
166
|
+
for (const workspace of Object.values(workspaces)) {
|
|
167
|
+
if (typeof workspace !== "object" || workspace === null) continue;
|
|
168
|
+
for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) {
|
|
169
|
+
const deps = workspace[group];
|
|
170
|
+
if (typeof deps !== "object" || deps === null) continue;
|
|
171
|
+
for (const name of Object.keys(deps)) names.add(name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return names;
|
|
175
|
+
}
|
|
176
|
+
function stripTrailingCommas(source) {
|
|
177
|
+
const out = [];
|
|
178
|
+
let inString = false;
|
|
179
|
+
let pendingComma = -1;
|
|
180
|
+
for (let i = 0; i < source.length; i++) {
|
|
181
|
+
const ch = source[i];
|
|
182
|
+
if (inString) {
|
|
183
|
+
out.push(ch);
|
|
184
|
+
if (ch === "\\") {
|
|
185
|
+
const next = source[i + 1];
|
|
186
|
+
if (next !== void 0) {
|
|
187
|
+
out.push(next);
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
} else if (ch === '"') {
|
|
191
|
+
inString = false;
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '"') {
|
|
196
|
+
inString = true;
|
|
197
|
+
pendingComma = -1;
|
|
198
|
+
out.push(ch);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (ch === ",") {
|
|
202
|
+
pendingComma = out.length;
|
|
203
|
+
out.push(ch);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (ch === "}" || ch === "]") {
|
|
207
|
+
if (pendingComma !== -1) out[pendingComma] = " ";
|
|
208
|
+
pendingComma = -1;
|
|
209
|
+
out.push(ch);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!/\s/.test(ch)) pendingComma = -1;
|
|
213
|
+
out.push(ch);
|
|
214
|
+
}
|
|
215
|
+
return out.join("");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/parsers/npm.ts
|
|
219
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
63
220
|
import path2 from "path";
|
|
64
221
|
async function parseNpmLockfile(lockfilePath) {
|
|
65
222
|
let raw;
|
|
66
223
|
try {
|
|
67
|
-
raw = await
|
|
224
|
+
raw = await readFile3(lockfilePath, "utf8");
|
|
68
225
|
} catch (cause) {
|
|
69
226
|
throw new PatchstackError(`Could not read lockfile at ${lockfilePath}`, "LOCKFILE_NOT_FOUND", cause);
|
|
70
227
|
}
|
|
@@ -145,8 +302,9 @@ function isDirectV2(pkgPath) {
|
|
|
145
302
|
}
|
|
146
303
|
|
|
147
304
|
// src/parsers/node_modules.ts
|
|
148
|
-
import { lstat, readFile as
|
|
305
|
+
import { lstat, readFile as readFile4, readdir, realpath, stat } from "fs/promises";
|
|
149
306
|
import path3 from "path";
|
|
307
|
+
var PACKAGE_STORE_DIRS = /* @__PURE__ */ new Set([".bun", ".pnpm", ".store"]);
|
|
150
308
|
async function walkNodeModules(cwd) {
|
|
151
309
|
const root = path3.join(cwd, "node_modules");
|
|
152
310
|
try {
|
|
@@ -167,96 +325,125 @@ async function walkNodeModules(cwd) {
|
|
|
167
325
|
cause
|
|
168
326
|
);
|
|
169
327
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
328
|
+
const seen = /* @__PURE__ */ new Map();
|
|
329
|
+
try {
|
|
330
|
+
seen.set(await realpath(cwd), -1);
|
|
331
|
+
} catch {
|
|
332
|
+
}
|
|
333
|
+
return dedupeByShallowest(await walk(root, 0, seen, cwd));
|
|
334
|
+
}
|
|
335
|
+
function dedupeByShallowest(found) {
|
|
336
|
+
const best = /* @__PURE__ */ new Map();
|
|
337
|
+
for (const item of found) {
|
|
338
|
+
const current = best.get(item.realpath);
|
|
339
|
+
if (current === void 0 || item.depth < current.depth) best.set(item.realpath, item);
|
|
340
|
+
}
|
|
341
|
+
return [...best.values()].map(({ entry }) => entry);
|
|
173
342
|
}
|
|
174
|
-
|
|
343
|
+
var CONCURRENCY = 32;
|
|
344
|
+
async function walk(dir, depth, seen, root) {
|
|
175
345
|
let names;
|
|
176
346
|
try {
|
|
177
347
|
names = await readdir(dir);
|
|
178
348
|
} catch {
|
|
179
|
-
return;
|
|
349
|
+
return [];
|
|
180
350
|
}
|
|
181
|
-
|
|
182
|
-
if (name.startsWith("."))
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
351
|
+
const perEntry = await mapLimit(names, async (name) => {
|
|
352
|
+
if (name.startsWith(".") && !PACKAGE_STORE_DIRS.has(name)) return [];
|
|
185
353
|
const fullPath = path3.join(dir, name);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
354
|
+
const resolved = await claim(fullPath, seen, depth);
|
|
355
|
+
if (resolved === null) return [];
|
|
356
|
+
if (PACKAGE_STORE_DIRS.has(name)) return walk(fullPath, depth, seen, root);
|
|
189
357
|
if (name.startsWith("@")) {
|
|
190
358
|
let subNames;
|
|
191
359
|
try {
|
|
192
360
|
subNames = await readdir(fullPath);
|
|
193
361
|
} catch {
|
|
194
|
-
|
|
362
|
+
return [];
|
|
195
363
|
}
|
|
196
|
-
|
|
197
|
-
if (sub.startsWith("."))
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
364
|
+
const scoped = await mapLimit(subNames, async (sub) => {
|
|
365
|
+
if (sub.startsWith(".")) return [];
|
|
200
366
|
const scopedDir = path3.join(fullPath, sub);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
continue;
|
|
367
|
+
const scopedReal = await claim(scopedDir, seen, depth);
|
|
368
|
+
if (scopedReal === null) return [];
|
|
369
|
+
return packageAndNested(scopedDir, scopedReal, depth, seen, root);
|
|
370
|
+
});
|
|
371
|
+
return scoped.flat();
|
|
208
372
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
373
|
+
return packageAndNested(fullPath, resolved, depth, seen, root);
|
|
374
|
+
});
|
|
375
|
+
return perEntry.flat();
|
|
376
|
+
}
|
|
377
|
+
async function packageAndNested(pkgDir, resolved, depth, seen, root) {
|
|
378
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth, root), walkNested(pkgDir, depth, seen, root)]);
|
|
379
|
+
return own === null ? nested : [{ entry: own, realpath: resolved, depth }, ...nested];
|
|
380
|
+
}
|
|
381
|
+
async function mapLimit(items, fn) {
|
|
382
|
+
const results = new Array(items.length);
|
|
383
|
+
let next = 0;
|
|
384
|
+
const workers = Array.from({ length: Math.min(CONCURRENCY, items.length) }, async () => {
|
|
385
|
+
while (next < items.length) {
|
|
386
|
+
const index = next++;
|
|
387
|
+
results[index] = await fn(items[index]);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
await Promise.all(workers);
|
|
391
|
+
return results;
|
|
212
392
|
}
|
|
213
|
-
|
|
393
|
+
function installLocation(root, pkgDir, impl = path3) {
|
|
394
|
+
return impl.relative(root, pkgDir).split(impl.sep).join("/");
|
|
395
|
+
}
|
|
396
|
+
async function readPackage(pkgDir, depth, root) {
|
|
214
397
|
let raw;
|
|
215
398
|
try {
|
|
216
|
-
raw = await
|
|
399
|
+
raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
|
|
217
400
|
} catch {
|
|
218
|
-
return;
|
|
401
|
+
return null;
|
|
219
402
|
}
|
|
220
403
|
let parsed;
|
|
221
404
|
try {
|
|
222
405
|
parsed = JSON.parse(raw);
|
|
223
406
|
} catch {
|
|
224
|
-
return;
|
|
407
|
+
return null;
|
|
225
408
|
}
|
|
226
409
|
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
227
|
-
return;
|
|
410
|
+
return null;
|
|
228
411
|
}
|
|
229
412
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
230
|
-
return;
|
|
413
|
+
return null;
|
|
231
414
|
}
|
|
232
|
-
|
|
233
|
-
name: parsed.name,
|
|
234
|
-
version: parsed.version,
|
|
235
|
-
direct: depth === 0
|
|
236
|
-
});
|
|
415
|
+
return { name: parsed.name, version: parsed.version, path: installLocation(root, pkgDir), direct: depth === 0 };
|
|
237
416
|
}
|
|
238
|
-
async function walkNested(pkgDir,
|
|
417
|
+
async function walkNested(pkgDir, depth, seen, root) {
|
|
239
418
|
const nested = path3.join(pkgDir, "node_modules");
|
|
240
|
-
if (
|
|
241
|
-
return;
|
|
419
|
+
if (await claim(nested, seen, depth + 1) === null) {
|
|
420
|
+
return [];
|
|
242
421
|
}
|
|
243
|
-
|
|
422
|
+
return walk(nested, depth + 1, seen, root);
|
|
244
423
|
}
|
|
245
|
-
async function
|
|
424
|
+
async function claim(dir, seen, depth) {
|
|
425
|
+
let resolved;
|
|
246
426
|
try {
|
|
247
427
|
const info = await lstat(dir);
|
|
248
|
-
|
|
428
|
+
if (!info.isDirectory() && !info.isSymbolicLink()) return null;
|
|
429
|
+
resolved = await realpath(dir);
|
|
430
|
+
const target = await stat(resolved);
|
|
431
|
+
if (!target.isDirectory()) return null;
|
|
249
432
|
} catch {
|
|
250
|
-
return
|
|
433
|
+
return null;
|
|
251
434
|
}
|
|
435
|
+
const claimed = seen.get(resolved);
|
|
436
|
+
if (claimed !== void 0 && claimed <= depth) return null;
|
|
437
|
+
seen.set(resolved, depth);
|
|
438
|
+
return resolved;
|
|
252
439
|
}
|
|
253
440
|
|
|
254
441
|
// src/parsers/pnpm.ts
|
|
255
|
-
import { readFile as
|
|
256
|
-
async function parsePnpmLockfile(lockfilePath) {
|
|
442
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
443
|
+
async function parsePnpmLockfile(lockfilePath, report) {
|
|
257
444
|
let raw;
|
|
258
445
|
try {
|
|
259
|
-
raw = await
|
|
446
|
+
raw = await readFile5(lockfilePath, "utf8");
|
|
260
447
|
} catch (cause) {
|
|
261
448
|
throw new PatchstackError(
|
|
262
449
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -277,6 +464,7 @@ async function parsePnpmLockfile(lockfilePath) {
|
|
|
277
464
|
for (const key of packageKeys) {
|
|
278
465
|
const parsed = parsePackageKey(key);
|
|
279
466
|
if (parsed === null) {
|
|
467
|
+
recordUnreadable(report, key);
|
|
280
468
|
continue;
|
|
281
469
|
}
|
|
282
470
|
entries.push({
|
|
@@ -504,12 +692,12 @@ function extractLeafName(trimmed) {
|
|
|
504
692
|
}
|
|
505
693
|
|
|
506
694
|
// src/parsers/yarn.ts
|
|
507
|
-
import { readFile as
|
|
695
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
508
696
|
import path4 from "path";
|
|
509
|
-
async function parseYarnLockfile(lockfilePath) {
|
|
697
|
+
async function parseYarnLockfile(lockfilePath, report) {
|
|
510
698
|
let raw;
|
|
511
699
|
try {
|
|
512
|
-
raw = await
|
|
700
|
+
raw = await readFile6(lockfilePath, "utf8");
|
|
513
701
|
} catch (cause) {
|
|
514
702
|
throw new PatchstackError(
|
|
515
703
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -517,7 +705,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
517
705
|
cause
|
|
518
706
|
);
|
|
519
707
|
}
|
|
520
|
-
const blocks = parseBlocks(raw);
|
|
708
|
+
const blocks = parseBlocks(raw, report);
|
|
521
709
|
if (blocks.length === 0) {
|
|
522
710
|
throw new PatchstackError(
|
|
523
711
|
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
@@ -546,13 +734,19 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
546
734
|
}
|
|
547
735
|
return entries;
|
|
548
736
|
}
|
|
549
|
-
|
|
737
|
+
var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
|
|
738
|
+
function parseBlocks(raw, report) {
|
|
550
739
|
const lines = raw.split(/\r?\n/);
|
|
551
740
|
const blocks = [];
|
|
552
741
|
let current = null;
|
|
553
742
|
const finalize = () => {
|
|
554
|
-
if (current
|
|
743
|
+
if (current === null) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (current.version.length > 0 && current.names.size > 0) {
|
|
555
747
|
blocks.push(current);
|
|
748
|
+
} else if (current.unreadable) {
|
|
749
|
+
recordUnreadable(report, current.key);
|
|
556
750
|
}
|
|
557
751
|
current = null;
|
|
558
752
|
};
|
|
@@ -569,13 +763,13 @@ function parseBlocks(raw) {
|
|
|
569
763
|
}
|
|
570
764
|
const keyLine = trimmed.slice(0, -1);
|
|
571
765
|
const names = /* @__PURE__ */ new Set();
|
|
766
|
+
let unreadable = false;
|
|
572
767
|
for (const spec of splitDescriptors(keyLine)) {
|
|
573
|
-
const
|
|
574
|
-
if (name !==
|
|
575
|
-
|
|
576
|
-
}
|
|
768
|
+
const classified = classifyDescriptor(spec);
|
|
769
|
+
if (classified.name !== void 0) names.add(classified.name);
|
|
770
|
+
else if (!classified.excluded) unreadable = true;
|
|
577
771
|
}
|
|
578
|
-
current = { names, version: "" };
|
|
772
|
+
current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
|
|
579
773
|
continue;
|
|
580
774
|
}
|
|
581
775
|
if (current === null) {
|
|
@@ -630,20 +824,37 @@ function splitDescriptors(keyLine) {
|
|
|
630
824
|
}
|
|
631
825
|
return parts;
|
|
632
826
|
}
|
|
633
|
-
|
|
827
|
+
var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
|
|
828
|
+
function classifyDescriptor(rawSpec) {
|
|
634
829
|
let s = rawSpec.trim();
|
|
635
830
|
if (s.length === 0) {
|
|
636
|
-
return
|
|
831
|
+
return { excluded: true };
|
|
637
832
|
}
|
|
638
833
|
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
639
834
|
s = s.slice(1, -1);
|
|
640
835
|
}
|
|
641
|
-
|
|
836
|
+
if (NON_PACKAGE_KEYS.has(s)) {
|
|
837
|
+
return { excluded: true };
|
|
838
|
+
}
|
|
839
|
+
const atIdx = s.indexOf("@", 1);
|
|
642
840
|
if (atIdx <= 0) {
|
|
643
|
-
return
|
|
841
|
+
return {};
|
|
644
842
|
}
|
|
645
843
|
const name = s.slice(0, atIdx);
|
|
646
|
-
|
|
844
|
+
const range = s.slice(atIdx + 1);
|
|
845
|
+
if (name.length === 0) {
|
|
846
|
+
return {};
|
|
847
|
+
}
|
|
848
|
+
if (NON_REGISTRY_PROTOCOLS.test(range)) {
|
|
849
|
+
return { excluded: true };
|
|
850
|
+
}
|
|
851
|
+
const alias = /^npm:(.+)$/i.exec(range);
|
|
852
|
+
if (alias !== null) {
|
|
853
|
+
const target = alias[1] ?? "";
|
|
854
|
+
const targetAt = target.indexOf("@", 1);
|
|
855
|
+
return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
|
|
856
|
+
}
|
|
857
|
+
return { name };
|
|
647
858
|
}
|
|
648
859
|
function parseVersionField(content) {
|
|
649
860
|
if (!content.startsWith("version")) {
|
|
@@ -668,7 +879,7 @@ async function readDirectDepNames(cwd) {
|
|
|
668
879
|
const names = /* @__PURE__ */ new Set();
|
|
669
880
|
let raw;
|
|
670
881
|
try {
|
|
671
|
-
raw = await
|
|
882
|
+
raw = await readFile6(path4.join(cwd, "package.json"), "utf8");
|
|
672
883
|
} catch {
|
|
673
884
|
return names;
|
|
674
885
|
}
|
|
@@ -697,7 +908,10 @@ async function readDirectDepNames(cwd) {
|
|
|
697
908
|
// src/parsers/index.ts
|
|
698
909
|
var LOCKFILE_CANDIDATES = [
|
|
699
910
|
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
700
|
-
|
|
911
|
+
// Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
|
|
912
|
+
// top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
|
|
913
|
+
// for new workspaces.
|
|
914
|
+
{ filename: "bun.lock", strategy: "bun-lockfile" },
|
|
701
915
|
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
702
916
|
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
703
917
|
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
@@ -732,34 +946,58 @@ async function scanLockfile(cwd) {
|
|
|
732
946
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
733
947
|
const warnings = [];
|
|
734
948
|
let firstParsed = null;
|
|
735
|
-
let
|
|
949
|
+
let walked = null;
|
|
950
|
+
let walkWasCandidate = false;
|
|
951
|
+
const parsedSources = [];
|
|
736
952
|
for (const candidate of candidates) {
|
|
737
953
|
let packages;
|
|
954
|
+
const report = newReport();
|
|
738
955
|
try {
|
|
739
|
-
packages = await runStrategy(candidate, cwd);
|
|
956
|
+
packages = await runStrategy(candidate, cwd, report);
|
|
740
957
|
} catch {
|
|
741
958
|
continue;
|
|
742
959
|
}
|
|
743
|
-
|
|
960
|
+
const unreadable = unreadableWarning(candidate.filename, report);
|
|
961
|
+
if (unreadable !== null) warnings.push(unreadable);
|
|
962
|
+
if (candidate.strategy === "node-modules-walk") {
|
|
963
|
+
walked = packages;
|
|
964
|
+
walkWasCandidate = true;
|
|
965
|
+
}
|
|
744
966
|
firstParsed ??= { packages, filename: candidate.filename };
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
967
|
+
parsedSources.push({ packages, filename: candidate.filename });
|
|
968
|
+
}
|
|
969
|
+
for (const source of parsedSources) {
|
|
970
|
+
const missing = missingDependencies(declared, source.packages);
|
|
971
|
+
if (missing.length > 0) {
|
|
972
|
+
warnings.push(staleWarning(source.filename, missing));
|
|
973
|
+
continue;
|
|
748
974
|
}
|
|
749
|
-
|
|
975
|
+
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
976
|
+
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
977
|
+
);
|
|
978
|
+
if (conflicts.length === 0) {
|
|
979
|
+
return manifestWith(source.packages, warnings);
|
|
980
|
+
}
|
|
981
|
+
warnings.push(conflictWarning(source.filename, conflicts));
|
|
982
|
+
break;
|
|
750
983
|
}
|
|
751
|
-
if (
|
|
984
|
+
if (walked === null) {
|
|
752
985
|
try {
|
|
753
|
-
|
|
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);
|
|
986
|
+
walked = await walkNodeModules(cwd);
|
|
760
987
|
} catch {
|
|
988
|
+
walked = null;
|
|
761
989
|
}
|
|
762
990
|
}
|
|
991
|
+
if (walked !== null) {
|
|
992
|
+
const missing = missingDependencies(declared, walked);
|
|
993
|
+
if (missing.length > 0) {
|
|
994
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
995
|
+
}
|
|
996
|
+
warnings.push(
|
|
997
|
+
walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
|
|
998
|
+
);
|
|
999
|
+
return manifestWith(walked, warnings);
|
|
1000
|
+
}
|
|
763
1001
|
if (firstParsed === null) {
|
|
764
1002
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
765
1003
|
throw new PatchstackError(
|
|
@@ -775,6 +1013,15 @@ async function scanLockfile(cwd) {
|
|
|
775
1013
|
function manifestWith(packages, warnings) {
|
|
776
1014
|
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
777
1015
|
}
|
|
1016
|
+
function otherCompleteSources(all, current, declared) {
|
|
1017
|
+
return all.filter(
|
|
1018
|
+
(source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
function conflictWarning(filename, conflicts) {
|
|
1022
|
+
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
1023
|
+
return `${filename} and another lockfile disagree about installed versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Scanned node_modules/ instead. Remove whichever lockfile your package manager does not maintain.`;
|
|
1024
|
+
}
|
|
778
1025
|
function staleWarning(source, missing) {
|
|
779
1026
|
const sample = missing.slice(0, 3).join(", ");
|
|
780
1027
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
@@ -789,14 +1036,18 @@ async function presentLockfiles(cwd) {
|
|
|
789
1036
|
);
|
|
790
1037
|
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
791
1038
|
}
|
|
792
|
-
async function runStrategy(detected, cwd) {
|
|
1039
|
+
async function runStrategy(detected, cwd, report) {
|
|
793
1040
|
switch (detected.strategy) {
|
|
794
1041
|
case "npm-lockfile":
|
|
795
1042
|
return parseNpmLockfile(detected.filePath);
|
|
1043
|
+
case "bun-lockfile":
|
|
1044
|
+
return parseBunLockfile(detected.filePath, report);
|
|
1045
|
+
// The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
|
|
1046
|
+
// many it dropped, because a manifest short a package reads exactly like a project without it.
|
|
796
1047
|
case "pnpm-lockfile":
|
|
797
|
-
return parsePnpmLockfile(detected.filePath);
|
|
1048
|
+
return parsePnpmLockfile(detected.filePath, report);
|
|
798
1049
|
case "yarn-lockfile":
|
|
799
|
-
return parseYarnLockfile(detected.filePath);
|
|
1050
|
+
return parseYarnLockfile(detected.filePath, report);
|
|
800
1051
|
case "node-modules-walk":
|
|
801
1052
|
return walkNodeModules(cwd);
|
|
802
1053
|
}
|
|
@@ -811,21 +1062,33 @@ async function exists(filePath) {
|
|
|
811
1062
|
}
|
|
812
1063
|
|
|
813
1064
|
// src/normalize.ts
|
|
814
|
-
function buildWirePayload(manifest) {
|
|
1065
|
+
function buildWirePayload(manifest, options = {}) {
|
|
1066
|
+
const withPaths = options.installPaths === true;
|
|
815
1067
|
const seen = /* @__PURE__ */ new Map();
|
|
816
1068
|
const wirePackages = [];
|
|
1069
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
1070
|
+
let entriesWithoutPath = 0;
|
|
817
1071
|
for (const entry of manifest.packages) {
|
|
1072
|
+
const identity = `${entry.name}@${entry.version}`;
|
|
1073
|
+
const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
|
|
1074
|
+
if (location === void 0) entriesWithoutPath++;
|
|
1075
|
+
const existing = byIdentity.get(identity);
|
|
1076
|
+
if (existing) {
|
|
1077
|
+
if (location !== void 0) addPath(existing, location);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
818
1080
|
const versions = seen.get(entry.name);
|
|
819
1081
|
if (versions) {
|
|
820
|
-
if (versions.has(entry.version)) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
1082
|
versions.add(entry.version);
|
|
824
1083
|
} else {
|
|
825
1084
|
seen.set(entry.name, /* @__PURE__ */ new Set([entry.version]));
|
|
826
1085
|
}
|
|
827
|
-
|
|
1086
|
+
const wire6 = { name: entry.name, version: entry.version };
|
|
1087
|
+
if (location !== void 0) addPath(wire6, location);
|
|
1088
|
+
byIdentity.set(identity, wire6);
|
|
1089
|
+
wirePackages.push(wire6);
|
|
828
1090
|
}
|
|
1091
|
+
for (const wire6 of wirePackages) wire6.paths?.sort();
|
|
829
1092
|
wirePackages.sort((a, b) => {
|
|
830
1093
|
if (a.name === b.name) {
|
|
831
1094
|
return compareVersions(a.version, b.version);
|
|
@@ -839,7 +1102,18 @@ function buildWirePayload(manifest) {
|
|
|
839
1102
|
}
|
|
840
1103
|
}
|
|
841
1104
|
return {
|
|
842
|
-
payload: {
|
|
1105
|
+
payload: {
|
|
1106
|
+
ecosystem: manifest.ecosystem,
|
|
1107
|
+
packages: wirePackages,
|
|
1108
|
+
// Every entry had a location, so a missing path anywhere means the package is not installed there.
|
|
1109
|
+
// One entry without one forfeits that for the whole payload: a consumer reading a short `paths` has
|
|
1110
|
+
// no way to tell which entry was the incomplete one.
|
|
1111
|
+
//
|
|
1112
|
+
// No separate check for "locations were not requested": with them off every entry counts as without
|
|
1113
|
+
// one, so this is already false — which is the default, and the safe direction. An explicit
|
|
1114
|
+
// `withPaths &&` here was a guard no test could distinguish from its absence.
|
|
1115
|
+
installPathsComplete: entriesWithoutPath === 0 && manifest.packages.length > 0
|
|
1116
|
+
},
|
|
843
1117
|
stats: {
|
|
844
1118
|
uniqueNames: seen.size,
|
|
845
1119
|
duplicateNames,
|
|
@@ -847,6 +1121,10 @@ function buildWirePayload(manifest) {
|
|
|
847
1121
|
}
|
|
848
1122
|
};
|
|
849
1123
|
}
|
|
1124
|
+
function addPath(wire6, installPath) {
|
|
1125
|
+
if (wire6.paths === void 0) wire6.paths = [installPath];
|
|
1126
|
+
else if (!wire6.paths.includes(installPath)) wire6.paths.push(installPath);
|
|
1127
|
+
}
|
|
850
1128
|
function compareVersions(a, b) {
|
|
851
1129
|
if (a === b) {
|
|
852
1130
|
return 0;
|
|
@@ -1228,7 +1506,7 @@ function isTimeoutError(cause) {
|
|
|
1228
1506
|
}
|
|
1229
1507
|
|
|
1230
1508
|
// src/demo.ts
|
|
1231
|
-
import { readFile as
|
|
1509
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1232
1510
|
import { join } from "path";
|
|
1233
1511
|
var NODE_SERIALIZE_DEMO = {
|
|
1234
1512
|
name: "node-serialize",
|
|
@@ -1298,7 +1576,7 @@ Add this deliberately vulnerable package first: npm install --save-exact ${scena
|
|
|
1298
1576
|
}
|
|
1299
1577
|
async function readPersistedSiteUuid(cwd) {
|
|
1300
1578
|
try {
|
|
1301
|
-
const value = JSON.parse(await
|
|
1579
|
+
const value = JSON.parse(await readFile7(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
|
|
1302
1580
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1303
1581
|
} catch {
|
|
1304
1582
|
return null;
|
|
@@ -1383,7 +1661,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
|
|
|
1383
1661
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
1384
1662
|
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
1385
1663
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1386
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
1664
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
1387
1665
|
const now = options.now ?? Date.now;
|
|
1388
1666
|
const started = now();
|
|
1389
1667
|
const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
|
|
@@ -1432,12 +1710,14 @@ function renderDemoTestCommands(url, scenario) {
|
|
|
1432
1710
|
}
|
|
1433
1711
|
|
|
1434
1712
|
// src/config.ts
|
|
1435
|
-
import { readFile as
|
|
1713
|
+
import { readFile as readFile8, writeFile, chmod } from "fs/promises";
|
|
1436
1714
|
import path6 from "path";
|
|
1437
1715
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
1716
|
+
var SECRET_FILENAME = ".patchstackrc.local.json";
|
|
1438
1717
|
var DEFAULT_ENVIRONMENT = "production";
|
|
1439
1718
|
async function resolveConfig(options) {
|
|
1440
1719
|
const fromFile = await readConfigFile(options.cwd);
|
|
1720
|
+
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1441
1721
|
const fromEnv = readEnv();
|
|
1442
1722
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1443
1723
|
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
@@ -1462,8 +1742,8 @@ async function resolveConfig(options) {
|
|
|
1462
1742
|
"CONFIG_MISSING"
|
|
1463
1743
|
);
|
|
1464
1744
|
}
|
|
1465
|
-
const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
|
|
1466
|
-
const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1745
|
+
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1746
|
+
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1467
1747
|
return {
|
|
1468
1748
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1469
1749
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1480,19 +1760,102 @@ async function writeConfigFile(cwd, config) {
|
|
|
1480
1760
|
await writeFile(target, content, "utf8");
|
|
1481
1761
|
return target;
|
|
1482
1762
|
}
|
|
1763
|
+
var SECRET_CONFIG_FILENAME = SECRET_FILENAME;
|
|
1764
|
+
async function writeSecretFile(cwd, secrets) {
|
|
1765
|
+
const target = path6.join(cwd, SECRET_FILENAME);
|
|
1766
|
+
await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1767
|
+
try {
|
|
1768
|
+
await chmod(target, 384);
|
|
1769
|
+
} catch {
|
|
1770
|
+
}
|
|
1771
|
+
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1772
|
+
return { path: target, ...ignore };
|
|
1773
|
+
}
|
|
1774
|
+
async function ensureIgnored(cwd, entry) {
|
|
1775
|
+
const target = path6.join(cwd, ".gitignore");
|
|
1776
|
+
let existing = "";
|
|
1777
|
+
try {
|
|
1778
|
+
existing = await readFile8(target, "utf8");
|
|
1779
|
+
} catch (err) {
|
|
1780
|
+
const code = err.code;
|
|
1781
|
+
if (code !== "ENOENT") {
|
|
1782
|
+
return { ignored: false, reason: `.gitignore could not be read (${code ?? "unknown error"})` };
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
if (ignoresEntry(existing, entry)) return { ignored: true };
|
|
1786
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1787
|
+
const block = `${separator}
|
|
1788
|
+
# Patchstack credential \u2014 never commit this
|
|
1789
|
+
${entry}
|
|
1790
|
+
`;
|
|
1791
|
+
try {
|
|
1792
|
+
await writeFile(target, existing + block, "utf8");
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
const code = err.code;
|
|
1795
|
+
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
const written = await readFile8(target, "utf8");
|
|
1799
|
+
if (ignoresEntry(written, entry)) return { ignored: true };
|
|
1800
|
+
return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
const code = err.code;
|
|
1803
|
+
return { ignored: false, reason: `.gitignore could not be read back (${code ?? "unknown error"})` };
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function secretFileIgnored(cwd) {
|
|
1807
|
+
try {
|
|
1808
|
+
const contents = await readFile8(path6.join(cwd, ".gitignore"), "utf8");
|
|
1809
|
+
if (ignoresEntry(contents, SECRET_FILENAME)) return { ignored: true };
|
|
1810
|
+
return { ignored: false, reason: `.gitignore does not cover ${SECRET_FILENAME}` };
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
const code = err.code;
|
|
1813
|
+
return {
|
|
1814
|
+
ignored: false,
|
|
1815
|
+
reason: code === "ENOENT" ? "this project has no .gitignore" : `.gitignore could not be read (${code ?? "unknown error"})`
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function ignoresEntry(contents, entry) {
|
|
1820
|
+
let ignored = false;
|
|
1821
|
+
for (const raw of contents.split("\n")) {
|
|
1822
|
+
const line = raw.trim();
|
|
1823
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1824
|
+
const negated = line.startsWith("!");
|
|
1825
|
+
const pattern = negated ? line.slice(1).trim() : line;
|
|
1826
|
+
if (pattern === entry || pattern === `/${entry}`) ignored = !negated;
|
|
1827
|
+
}
|
|
1828
|
+
return ignored;
|
|
1829
|
+
}
|
|
1483
1830
|
async function persistSiteUuid(cwd, siteUuid) {
|
|
1484
1831
|
const existing = await readConfigFile(cwd);
|
|
1485
1832
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
1486
1833
|
}
|
|
1487
1834
|
async function persistApiKey(cwd, apiKey) {
|
|
1488
|
-
const { pulseAuth:
|
|
1489
|
-
|
|
1835
|
+
const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
|
|
1836
|
+
const existingSecrets = await readSecretFile(cwd);
|
|
1837
|
+
const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
|
|
1838
|
+
if (_movedKey !== void 0 || _movedPulse !== void 0) {
|
|
1839
|
+
await writeConfigFile(cwd, publicConfig);
|
|
1840
|
+
}
|
|
1841
|
+
return target;
|
|
1842
|
+
}
|
|
1843
|
+
async function credentialInCommittedConfig(cwd) {
|
|
1844
|
+
const config = await readConfigFile(cwd);
|
|
1845
|
+
return config.apiKey !== void 0 || config.pulseAuth !== void 0;
|
|
1846
|
+
}
|
|
1847
|
+
async function readSecretFile(cwd) {
|
|
1848
|
+
const file = await readJsonFile(cwd, SECRET_FILENAME);
|
|
1849
|
+
return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
|
|
1490
1850
|
}
|
|
1491
1851
|
async function readConfigFile(cwd) {
|
|
1492
|
-
|
|
1852
|
+
return readJsonFile(cwd, CONFIG_FILENAME);
|
|
1853
|
+
}
|
|
1854
|
+
async function readJsonFile(cwd, filename) {
|
|
1855
|
+
const target = path6.join(cwd, filename);
|
|
1493
1856
|
let raw;
|
|
1494
1857
|
try {
|
|
1495
|
-
raw = await
|
|
1858
|
+
raw = await readFile8(target, "utf8");
|
|
1496
1859
|
} catch (err) {
|
|
1497
1860
|
if (err.code === "ENOENT") {
|
|
1498
1861
|
return {};
|
|
@@ -1767,7 +2130,7 @@ function ensureSourceMarker(cwd, shell, framework) {
|
|
|
1767
2130
|
}
|
|
1768
2131
|
|
|
1769
2132
|
// src/guide.ts
|
|
1770
|
-
import { existsSync as
|
|
2133
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
1771
2134
|
import path9 from "path";
|
|
1772
2135
|
|
|
1773
2136
|
// src/protect/install/util.ts
|
|
@@ -1784,6 +2147,20 @@ function hasDependency(cwd, name) {
|
|
|
1784
2147
|
return false;
|
|
1785
2148
|
}
|
|
1786
2149
|
}
|
|
2150
|
+
function hasResolvableCredential(cwd) {
|
|
2151
|
+
if ((process.env.PATCHSTACK_API_KEY ?? "") !== "" || (process.env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
|
|
2152
|
+
return true;
|
|
2153
|
+
}
|
|
2154
|
+
for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
|
|
2155
|
+
try {
|
|
2156
|
+
const parsed = JSON.parse(read(join2(cwd, file)));
|
|
2157
|
+
if (typeof parsed.apiKey === "string" && parsed.apiKey !== "") return true;
|
|
2158
|
+
if (typeof parsed.pulseAuth === "string" && parsed.pulseAuth !== "") return true;
|
|
2159
|
+
} catch {
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
1787
2164
|
var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
|
|
1788
2165
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1789
2166
|
function bakeSiteUuid(cwd, guardRelPath) {
|
|
@@ -1858,7 +2235,8 @@ var REQUEST_MIDDLEWARE_BLOCK = [
|
|
|
1858
2235
|
" if (blocked) return blocked;",
|
|
1859
2236
|
" }",
|
|
1860
2237
|
" }",
|
|
1861
|
-
"
|
|
2238
|
+
" // The request is passed so route/method-scoped response rules can apply their scope.",
|
|
2239
|
+
" return screenResponse(await next(), request);",
|
|
1862
2240
|
"});",
|
|
1863
2241
|
"// #endregion patchstack-guard"
|
|
1864
2242
|
].join("\n");
|
|
@@ -2252,9 +2630,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
2252
2630
|
var MAX_DEPTH = 8;
|
|
2253
2631
|
function findAppInstance(cwd, re) {
|
|
2254
2632
|
const root = existsSync6(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
|
|
2255
|
-
|
|
2633
|
+
const found = [];
|
|
2256
2634
|
const walk2 = (dir, depth) => {
|
|
2257
|
-
if (
|
|
2635
|
+
if (depth > MAX_DEPTH) return;
|
|
2258
2636
|
let entries;
|
|
2259
2637
|
try {
|
|
2260
2638
|
entries = readdirSync2(dir).sort();
|
|
@@ -2262,7 +2640,6 @@ function findAppInstance(cwd, re) {
|
|
|
2262
2640
|
return;
|
|
2263
2641
|
}
|
|
2264
2642
|
for (const name of entries) {
|
|
2265
|
-
if (hit) return;
|
|
2266
2643
|
if (SKIP_DIRS.has(name) || name.startsWith(".")) continue;
|
|
2267
2644
|
const p = join8(dir, name);
|
|
2268
2645
|
let st;
|
|
@@ -2281,12 +2658,65 @@ function findAppInstance(cwd, re) {
|
|
|
2281
2658
|
continue;
|
|
2282
2659
|
}
|
|
2283
2660
|
const m = re.exec(src);
|
|
2284
|
-
if (m)
|
|
2661
|
+
if (!m) continue;
|
|
2662
|
+
const appVar = m[1];
|
|
2663
|
+
found.push({
|
|
2664
|
+
relPath: relative(cwd, p).replace(/\\/g, "/"),
|
|
2665
|
+
appVar,
|
|
2666
|
+
listens: listensOn(src, appVar)
|
|
2667
|
+
});
|
|
2285
2668
|
}
|
|
2286
2669
|
}
|
|
2287
2670
|
};
|
|
2288
2671
|
walk2(root, 0);
|
|
2289
|
-
return
|
|
2672
|
+
if (found.length === 0) return null;
|
|
2673
|
+
const declared = declaredEntries(cwd);
|
|
2674
|
+
const chosen = found.slice().sort((a, b) => rankEntry({ ...a, declared: declared.has(a.relPath) }, { ...b, declared: declared.has(b.relPath) }))[0];
|
|
2675
|
+
return {
|
|
2676
|
+
relPath: chosen.relPath,
|
|
2677
|
+
appVar: chosen.appVar,
|
|
2678
|
+
others: found.filter((f) => f !== chosen && f.listens).map((f) => ({ relPath: f.relPath, appVar: f.appVar }))
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
function listensOn(source, appVar) {
|
|
2682
|
+
return new RegExp(`\\b${appVar}\\s*\\.\\s*listen\\s*\\(`).test(source);
|
|
2683
|
+
}
|
|
2684
|
+
var ENTRY_NAMES = ["server", "index", "main", "app"];
|
|
2685
|
+
function rankEntry(a, b) {
|
|
2686
|
+
if (a.declared !== b.declared) return a.declared ? -1 : 1;
|
|
2687
|
+
if (a.listens !== b.listens) return a.listens ? -1 : 1;
|
|
2688
|
+
const named = entryRank(a.relPath) - entryRank(b.relPath);
|
|
2689
|
+
if (named !== 0) return named;
|
|
2690
|
+
const depth = a.relPath.split("/").length - b.relPath.split("/").length;
|
|
2691
|
+
if (depth !== 0) return depth;
|
|
2692
|
+
return a.relPath < b.relPath ? -1 : 1;
|
|
2693
|
+
}
|
|
2694
|
+
function entryRank(relPath) {
|
|
2695
|
+
const base = (relPath.split("/").pop() ?? "").replace(/\.(?:ts|js|mjs|cjs)$/, "");
|
|
2696
|
+
const index = ENTRY_NAMES.indexOf(base);
|
|
2697
|
+
return index === -1 ? ENTRY_NAMES.length : index;
|
|
2698
|
+
}
|
|
2699
|
+
function declaredEntries(cwd) {
|
|
2700
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2701
|
+
let pkg;
|
|
2702
|
+
try {
|
|
2703
|
+
pkg = JSON.parse(readFileSync3(join8(cwd, "package.json"), "utf8"));
|
|
2704
|
+
} catch {
|
|
2705
|
+
return paths;
|
|
2706
|
+
}
|
|
2707
|
+
const add = (value) => {
|
|
2708
|
+
if (typeof value !== "string") return;
|
|
2709
|
+
for (const match of value.matchAll(/[\w./@-]+\.(?:ts|js|mjs|cjs)/g)) {
|
|
2710
|
+
paths.add(match[0].replace(/^\.\//, ""));
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
add(pkg.main);
|
|
2714
|
+
add(pkg.module);
|
|
2715
|
+
const scripts = pkg.scripts;
|
|
2716
|
+
if (typeof scripts === "object" && scripts !== null) {
|
|
2717
|
+
for (const name of ["start", "serve", "dev"]) add(scripts[name]);
|
|
2718
|
+
}
|
|
2719
|
+
return paths;
|
|
2290
2720
|
}
|
|
2291
2721
|
function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
2292
2722
|
let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
|
|
@@ -2296,28 +2726,252 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
|
2296
2726
|
}
|
|
2297
2727
|
|
|
2298
2728
|
// src/protect/install/register.ts
|
|
2299
|
-
import { writeFileSync as writeFileSync5, existsSync as
|
|
2300
|
-
import { join as
|
|
2729
|
+
import { writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
|
|
2730
|
+
import { join as join11, resolve as resolve2 } from "path";
|
|
2731
|
+
|
|
2732
|
+
// src/protect/install/generic.ts
|
|
2733
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
|
|
2734
|
+
import { join as join10, dirname as dirname5 } from "path";
|
|
2735
|
+
|
|
2736
|
+
// src/protect/install/source-scope.ts
|
|
2737
|
+
import { execFileSync } from "child_process";
|
|
2738
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2739
|
+
import { dirname as dirname4, join as join9, resolve } from "path";
|
|
2740
|
+
var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
|
|
2741
|
+
function lastTopLevelImportLine(source) {
|
|
2742
|
+
const lines = source.split("\n");
|
|
2743
|
+
let depth = 0;
|
|
2744
|
+
let last = -1;
|
|
2745
|
+
let inBlockComment = false;
|
|
2746
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2747
|
+
const line = lines[i] ?? "";
|
|
2748
|
+
if (!inBlockComment && depth === 0 && IMPORT_LINE.test(line)) last = i;
|
|
2749
|
+
inBlockComment = advanceDepth(line, inBlockComment, (delta) => {
|
|
2750
|
+
depth += delta;
|
|
2751
|
+
});
|
|
2752
|
+
if (depth < 0) return -1;
|
|
2753
|
+
}
|
|
2754
|
+
return depth === 0 ? last : -1;
|
|
2755
|
+
}
|
|
2756
|
+
function isTopLevelLine(source, index) {
|
|
2757
|
+
const lines = source.split("\n");
|
|
2758
|
+
let depth = 0;
|
|
2759
|
+
let inBlockComment = false;
|
|
2760
|
+
for (let i = 0; i < lines.length && i < index; i++) {
|
|
2761
|
+
inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
|
|
2762
|
+
depth += delta;
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
return depth === 0;
|
|
2766
|
+
}
|
|
2767
|
+
function inSameBlockAfter(source, appIndex, callIndex) {
|
|
2768
|
+
if (appIndex < 0 || callIndex <= appIndex) return false;
|
|
2769
|
+
const lines = source.split("\n");
|
|
2770
|
+
let depth = 0;
|
|
2771
|
+
let inBlockComment = false;
|
|
2772
|
+
for (let i = appIndex; i < callIndex; i++) {
|
|
2773
|
+
inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
|
|
2774
|
+
depth += delta;
|
|
2775
|
+
});
|
|
2776
|
+
if (depth < 0) return false;
|
|
2777
|
+
}
|
|
2778
|
+
return depth === 0;
|
|
2779
|
+
}
|
|
2780
|
+
function advanceDepth(line, inBlockComment, add) {
|
|
2781
|
+
let quote = null;
|
|
2782
|
+
for (let i = 0; i < line.length; i++) {
|
|
2783
|
+
const ch = line[i];
|
|
2784
|
+
const next = line[i + 1];
|
|
2785
|
+
if (inBlockComment) {
|
|
2786
|
+
if (ch === "*" && next === "/") {
|
|
2787
|
+
inBlockComment = false;
|
|
2788
|
+
i++;
|
|
2789
|
+
}
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
if (quote !== null) {
|
|
2793
|
+
if (ch === "\\") {
|
|
2794
|
+
i++;
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
if (ch === quote) quote = null;
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
if (ch === "/" && next === "/") return inBlockComment;
|
|
2801
|
+
if (ch === "/" && next === "*") {
|
|
2802
|
+
inBlockComment = true;
|
|
2803
|
+
i++;
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2807
|
+
quote = ch;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (ch === "{" || ch === "(" || ch === "[") add(1);
|
|
2811
|
+
else if (ch === "}" || ch === ")" || ch === "]") add(-1);
|
|
2812
|
+
}
|
|
2813
|
+
return inBlockComment;
|
|
2814
|
+
}
|
|
2815
|
+
function parses(filePath) {
|
|
2816
|
+
if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
|
|
2817
|
+
try {
|
|
2818
|
+
execFileSync(process.execPath, ["--check", filePath], { stdio: "pipe" });
|
|
2819
|
+
return true;
|
|
2820
|
+
} catch (error) {
|
|
2821
|
+
return isSyntaxError(error) ? false : null;
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function isSyntaxError(error) {
|
|
2825
|
+
const status = error?.status;
|
|
2826
|
+
return typeof status === "number" && status !== 0;
|
|
2827
|
+
}
|
|
2828
|
+
function stripComments(source) {
|
|
2829
|
+
let out = "";
|
|
2830
|
+
let quote = null;
|
|
2831
|
+
let inLine = false;
|
|
2832
|
+
let inBlock = false;
|
|
2833
|
+
for (let i = 0; i < source.length; i++) {
|
|
2834
|
+
const ch = source[i];
|
|
2835
|
+
const next = source[i + 1];
|
|
2836
|
+
if (inLine) {
|
|
2837
|
+
if (ch === "\n") {
|
|
2838
|
+
inLine = false;
|
|
2839
|
+
out += ch;
|
|
2840
|
+
}
|
|
2841
|
+
continue;
|
|
2842
|
+
}
|
|
2843
|
+
if (inBlock) {
|
|
2844
|
+
if (ch === "*" && next === "/") {
|
|
2845
|
+
inBlock = false;
|
|
2846
|
+
i++;
|
|
2847
|
+
} else if (ch === "\n") {
|
|
2848
|
+
out += ch;
|
|
2849
|
+
}
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
if (quote !== null) {
|
|
2853
|
+
out += ch;
|
|
2854
|
+
if (ch === "\\") {
|
|
2855
|
+
if (next !== void 0) {
|
|
2856
|
+
out += next;
|
|
2857
|
+
i++;
|
|
2858
|
+
}
|
|
2859
|
+
} else if (ch === quote) {
|
|
2860
|
+
quote = null;
|
|
2861
|
+
}
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2865
|
+
quote = ch;
|
|
2866
|
+
out += ch;
|
|
2867
|
+
continue;
|
|
2868
|
+
}
|
|
2869
|
+
if (ch === "/" && next === "/") {
|
|
2870
|
+
inLine = true;
|
|
2871
|
+
i++;
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
if (ch === "/" && next === "*") {
|
|
2875
|
+
inBlock = true;
|
|
2876
|
+
i++;
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
out += ch;
|
|
2880
|
+
}
|
|
2881
|
+
return out;
|
|
2882
|
+
}
|
|
2883
|
+
var MASKED_CHAR = "";
|
|
2884
|
+
function maskStringContents(source) {
|
|
2885
|
+
let out = "";
|
|
2886
|
+
let quote = null;
|
|
2887
|
+
for (let i = 0; i < source.length; i++) {
|
|
2888
|
+
const ch = source[i];
|
|
2889
|
+
if (quote === null) {
|
|
2890
|
+
out += ch;
|
|
2891
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
2892
|
+
continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (ch === "\\") {
|
|
2895
|
+
out += MASKED_CHAR;
|
|
2896
|
+
if (i + 1 < source.length) {
|
|
2897
|
+
out += source[i + 1] === "\n" ? "\n" : MASKED_CHAR;
|
|
2898
|
+
i++;
|
|
2899
|
+
}
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
if (ch === quote) {
|
|
2903
|
+
out += ch;
|
|
2904
|
+
quote = null;
|
|
2905
|
+
continue;
|
|
2906
|
+
}
|
|
2907
|
+
out += ch === "\n" ? "\n" : MASKED_CHAR;
|
|
2908
|
+
}
|
|
2909
|
+
return out;
|
|
2910
|
+
}
|
|
2911
|
+
var GUARD_FILENAMES = ["guard.ts", "guard.js", "guard.mjs", "guard.cjs"];
|
|
2912
|
+
function moduleKindOf(filePath, packageIsModule) {
|
|
2913
|
+
if (/\.(?:ts|tsx|mts|cts|jsx)$/.test(filePath)) return "ts";
|
|
2914
|
+
if (/\.mjs$/.test(filePath)) return "esm";
|
|
2915
|
+
if (/\.cjs$/.test(filePath)) return "cjs";
|
|
2916
|
+
return packageIsModule ? "esm" : "cjs";
|
|
2917
|
+
}
|
|
2918
|
+
function resolvesToGuardModule(specifier, query) {
|
|
2919
|
+
const resolved = resolve(dirname4(query.fromFile), specifier);
|
|
2920
|
+
if (withoutModuleExtension(resolved) !== join9(query.guardDir, "guard")) return false;
|
|
2921
|
+
const existing = GUARD_FILENAMES.filter((name) => existsSync7(join9(query.guardDir, name)));
|
|
2922
|
+
if (existing.length === 0) return false;
|
|
2923
|
+
const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
|
|
2924
|
+
if (!query.strictExtensions) return true;
|
|
2925
|
+
if (extension === "") {
|
|
2926
|
+
if (query.kind === "esm") return false;
|
|
2927
|
+
if (query.kind === "cjs") return existing.includes("guard.js");
|
|
2928
|
+
return true;
|
|
2929
|
+
}
|
|
2930
|
+
if (existsSync7(resolved)) return true;
|
|
2931
|
+
return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
|
|
2932
|
+
}
|
|
2933
|
+
function withoutModuleExtension(filePath) {
|
|
2934
|
+
return filePath.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
2935
|
+
}
|
|
2301
2936
|
|
|
2302
2937
|
// src/protect/install/generic.ts
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2938
|
+
function packageIsEsm(cwd) {
|
|
2939
|
+
try {
|
|
2940
|
+
return JSON.parse(read(join10(cwd, "package.json"))).type === "module";
|
|
2941
|
+
} catch {
|
|
2942
|
+
return false;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2306
2945
|
function genericDir(cwd) {
|
|
2307
|
-
return
|
|
2946
|
+
return existsSync8(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
2947
|
+
}
|
|
2948
|
+
function hasTypeScript(cwd) {
|
|
2949
|
+
return existsSync8(join10(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
|
|
2950
|
+
}
|
|
2951
|
+
function genericGuardTarget(cwd) {
|
|
2952
|
+
if (hasTypeScript(cwd)) {
|
|
2953
|
+
return { template: "generic-guard.ts", file: "guard.ts", specifier: "guard" };
|
|
2954
|
+
}
|
|
2955
|
+
if (packageIsEsm(cwd)) {
|
|
2956
|
+
return { template: "generic-guard.js", file: "guard.js", specifier: "guard.js" };
|
|
2957
|
+
}
|
|
2958
|
+
return { template: "generic-guard.cjs", file: "guard.cjs", specifier: "guard.cjs" };
|
|
2308
2959
|
}
|
|
2309
|
-
function scaffoldGeneric(cwd, opts, guardTemplate
|
|
2960
|
+
function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
2961
|
+
const target = genericGuardTarget(cwd);
|
|
2962
|
+
guardTemplate ??= target.template;
|
|
2963
|
+
guardFile ??= target.file;
|
|
2310
2964
|
const templates = templatesDir();
|
|
2311
2965
|
const dir = genericDir(cwd);
|
|
2312
|
-
const dst =
|
|
2966
|
+
const dst = join10(cwd, dir);
|
|
2313
2967
|
mkdirSync4(dst, { recursive: true });
|
|
2314
|
-
copyFileSync4(
|
|
2968
|
+
copyFileSync4(join10(templates, guardTemplate), join10(dst, guardFile));
|
|
2315
2969
|
const guardRel = `${dir}/${guardFile}`;
|
|
2316
2970
|
const changed = [guardRel];
|
|
2317
2971
|
if (!opts.demo) bakeSiteUuid(cwd, guardRel);
|
|
2318
|
-
const rulesDst =
|
|
2319
|
-
if (opts.demo || !
|
|
2320
|
-
copyFileSync4(
|
|
2972
|
+
const rulesDst = join10(dst, "rules.json");
|
|
2973
|
+
if (opts.demo || !existsSync8(rulesDst)) {
|
|
2974
|
+
copyFileSync4(join10(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
2321
2975
|
changed.push(`${dir}/rules.json`);
|
|
2322
2976
|
}
|
|
2323
2977
|
return { changed, dir };
|
|
@@ -2338,17 +2992,17 @@ function candidateEntries(cwd) {
|
|
|
2338
2992
|
"app.ts",
|
|
2339
2993
|
"app.js"
|
|
2340
2994
|
];
|
|
2341
|
-
const hits = names.filter((n) =>
|
|
2995
|
+
const hits = names.filter((n) => existsSync8(join10(cwd, n)));
|
|
2342
2996
|
try {
|
|
2343
|
-
const pkg = JSON.parse(read(
|
|
2344
|
-
if (typeof pkg.main === "string" &&
|
|
2997
|
+
const pkg = JSON.parse(read(join10(cwd, "package.json")));
|
|
2998
|
+
if (typeof pkg.main === "string" && existsSync8(join10(cwd, pkg.main))) hits.push(pkg.main);
|
|
2345
2999
|
} catch {
|
|
2346
3000
|
}
|
|
2347
3001
|
return [...new Set(hits)];
|
|
2348
3002
|
}
|
|
2349
3003
|
function usesExpress(cwd) {
|
|
2350
3004
|
try {
|
|
2351
|
-
const pkg = JSON.parse(read(
|
|
3005
|
+
const pkg = JSON.parse(read(join10(cwd, "package.json")));
|
|
2352
3006
|
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
|
|
2353
3007
|
} catch {
|
|
2354
3008
|
return false;
|
|
@@ -2357,34 +3011,140 @@ function usesExpress(cwd) {
|
|
|
2357
3011
|
function wiringPlan(cwd, dir) {
|
|
2358
3012
|
const entries = candidateEntries(cwd);
|
|
2359
3013
|
const express = usesExpress(cwd);
|
|
3014
|
+
const target = genericGuardTarget(cwd);
|
|
2360
3015
|
const lines = [
|
|
2361
|
-
`no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}
|
|
3016
|
+
`no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/${target.file} + ${dir}/rules.json.`,
|
|
2362
3017
|
"Finish by wiring it into your server (pick the one that fits):",
|
|
2363
|
-
|
|
2364
|
-
|
|
3018
|
+
// The specifier is the one this project's loader resolves — a bare path is right for a compiler and
|
|
3019
|
+
// wrong for `require()`, which never guesses `.cjs`.
|
|
3020
|
+
` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/${target.specifier}"`,
|
|
3021
|
+
` \u2022 Node / Connect: app.use(patchstackMiddleware) // before any body parser`,
|
|
2365
3022
|
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.",
|
|
2366
3023
|
"Then confirm it is hooked up: npx patchstack-connect protect --check"
|
|
2367
3024
|
];
|
|
2368
3025
|
return lines.join("\n");
|
|
2369
3026
|
}
|
|
3027
|
+
var GUARD_EXPORTS = ["protectFetch", "patchstackMiddleware"];
|
|
3028
|
+
function importsAndUsesGuard(source, sourcePath, guardDir, packageIsModule) {
|
|
3029
|
+
const code = stripComments(source);
|
|
3030
|
+
const masked = maskStringContents(code);
|
|
3031
|
+
const declarations = guardImportDeclarations(code, masked, {
|
|
3032
|
+
fromFile: sourcePath,
|
|
3033
|
+
guardDir,
|
|
3034
|
+
kind: moduleKindOf(sourcePath, packageIsModule),
|
|
3035
|
+
// The scaffold now matches the project's module format and the printed plan names the specifier that
|
|
3036
|
+
// format resolves, so there is nothing left to excuse: a specifier this file's loader would not find is
|
|
3037
|
+
// not a wired guard here either.
|
|
3038
|
+
strictExtensions: true
|
|
3039
|
+
});
|
|
3040
|
+
if (declarations.length === 0) return false;
|
|
3041
|
+
const direct = /* @__PURE__ */ new Set();
|
|
3042
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
3043
|
+
for (const declaration of declarations) {
|
|
3044
|
+
const bound = boundNames(declaration.text);
|
|
3045
|
+
for (const name of bound.direct) direct.add(name);
|
|
3046
|
+
for (const name of bound.namespaces) namespaces.add(name);
|
|
3047
|
+
}
|
|
3048
|
+
if (direct.size === 0 && namespaces.size === 0) return false;
|
|
3049
|
+
let body = masked;
|
|
3050
|
+
for (const declaration of declarations) {
|
|
3051
|
+
body = body.slice(0, declaration.start) + " ".repeat(declaration.text.length) + body.slice(declaration.start + declaration.text.length);
|
|
3052
|
+
}
|
|
3053
|
+
const members = GUARD_EXPORTS.map(escapeForRegExp).join("|");
|
|
3054
|
+
return [...direct].some((name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*[(),]`).test(body)) || [...namespaces].some(
|
|
3055
|
+
(name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*\\.\\s*(?:${members})\\s*[(),]`).test(body)
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
function guardImportDeclarations(code, masked, query) {
|
|
3059
|
+
const pattern = /import\s+[^;]*?from\s*(['"`])[^'"`]*\1|(?:const|let|var)\s+[^;=]+=\s*require\(\s*(['"`])[^'"`]*\2\s*\)/g;
|
|
3060
|
+
const found = [];
|
|
3061
|
+
for (const match of masked.matchAll(pattern)) {
|
|
3062
|
+
const start = match.index ?? -1;
|
|
3063
|
+
if (start < 0) continue;
|
|
3064
|
+
const text = code.slice(start, start + match[0].length);
|
|
3065
|
+
const specifier = specifierOf(text);
|
|
3066
|
+
if (specifier !== null && resolvesToGuardModule(specifier, query)) found.push({ text, start });
|
|
3067
|
+
}
|
|
3068
|
+
return found;
|
|
3069
|
+
}
|
|
3070
|
+
function specifierOf(declaration) {
|
|
3071
|
+
return /(['"`])([^'"`]*)\1/.exec(declaration)?.[2] ?? null;
|
|
3072
|
+
}
|
|
3073
|
+
function boundNames(declaration) {
|
|
3074
|
+
const direct = [];
|
|
3075
|
+
const namespaces = [];
|
|
3076
|
+
const clause = /^import\s+([^]*?)\s+from\b/.exec(declaration)?.[1] ?? /^(?:const|let|var)\s+([^]*?)=\s*require\b/.exec(declaration)?.[1] ?? "";
|
|
3077
|
+
const braced = /\{([^}]*)\}/.exec(clause)?.[1];
|
|
3078
|
+
if (braced !== void 0) {
|
|
3079
|
+
for (const part of braced.split(",")) {
|
|
3080
|
+
const [exported, local] = splitBinding(part);
|
|
3081
|
+
if (!isGuardExport(exported)) continue;
|
|
3082
|
+
if (/^[A-Za-z_$][\w$]*$/.test(local)) direct.push(local);
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
const bare = clause.replace(/\{[^}]*\}/g, "").replace(/^\*\s*as\s*/, "").replace(/,/g, " ").trim();
|
|
3086
|
+
for (const token of bare.split(/\s+/)) {
|
|
3087
|
+
if (/^[A-Za-z_$][\w$]*$/.test(token) && token !== "as") namespaces.push(token);
|
|
3088
|
+
}
|
|
3089
|
+
return { direct, namespaces };
|
|
3090
|
+
}
|
|
3091
|
+
function splitBinding(part) {
|
|
3092
|
+
const separator = part.includes(" as ") ? " as " : part.includes(":") ? ":" : null;
|
|
3093
|
+
if (separator === null) {
|
|
3094
|
+
const name = part.trim();
|
|
3095
|
+
return [name, name];
|
|
3096
|
+
}
|
|
3097
|
+
const [exported, ...rest] = part.split(separator);
|
|
3098
|
+
return [(exported ?? "").trim(), (rest.join(separator) ?? "").trim()];
|
|
3099
|
+
}
|
|
3100
|
+
function isGuardExport(name) {
|
|
3101
|
+
return GUARD_EXPORTS.includes(name);
|
|
3102
|
+
}
|
|
3103
|
+
function escapeForRegExp(value) {
|
|
3104
|
+
return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
3105
|
+
}
|
|
2370
3106
|
function genericVerify(cwd) {
|
|
2371
3107
|
const dir = genericDir(cwd);
|
|
2372
|
-
const
|
|
2373
|
-
const
|
|
3108
|
+
const target = genericGuardTarget(cwd);
|
|
3109
|
+
const present = GUARD_FILENAMES.find((name) => existsSync8(join10(cwd, dir, name)));
|
|
3110
|
+
const scaffolded = present !== void 0;
|
|
3111
|
+
const imported = scaffolded && guardIsImported(cwd, join10(cwd, dir, present));
|
|
2374
3112
|
return {
|
|
2375
3113
|
wired: scaffolded && imported,
|
|
2376
3114
|
checks: [
|
|
2377
3115
|
{ label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
2378
3116
|
{
|
|
2379
|
-
label: "guard imported
|
|
3117
|
+
label: "guard imported and called in a server entry",
|
|
2380
3118
|
ok: imported,
|
|
2381
|
-
hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}
|
|
3119
|
+
hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}/${present ?? target.specifier}" and wire it into your request path \u2014 an import that is never called screens nothing`
|
|
2382
3120
|
}
|
|
2383
3121
|
]
|
|
2384
3122
|
};
|
|
2385
3123
|
}
|
|
3124
|
+
var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
|
|
3125
|
+
"node_modules",
|
|
3126
|
+
"patchstack",
|
|
3127
|
+
"dist",
|
|
3128
|
+
"build",
|
|
3129
|
+
"out",
|
|
3130
|
+
"coverage",
|
|
3131
|
+
".next",
|
|
3132
|
+
".output",
|
|
3133
|
+
".svelte-kit",
|
|
3134
|
+
"test",
|
|
3135
|
+
"tests",
|
|
3136
|
+
"__tests__",
|
|
3137
|
+
"e2e",
|
|
3138
|
+
"examples",
|
|
3139
|
+
"fixtures",
|
|
3140
|
+
"__fixtures__",
|
|
3141
|
+
"stories",
|
|
3142
|
+
"__mocks__"
|
|
3143
|
+
]);
|
|
2386
3144
|
function guardIsImported(cwd, guardPath) {
|
|
2387
|
-
const
|
|
3145
|
+
const guardDir = dirname5(guardPath);
|
|
3146
|
+
const packageIsModule = packageIsEsm(cwd);
|
|
3147
|
+
const root = existsSync8(join10(cwd, "src")) ? join10(cwd, "src") : cwd;
|
|
2388
3148
|
let found = false;
|
|
2389
3149
|
const walk2 = (d, depth) => {
|
|
2390
3150
|
if (found || depth > 8) return;
|
|
@@ -2396,8 +3156,8 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2396
3156
|
}
|
|
2397
3157
|
for (const name of entries) {
|
|
2398
3158
|
if (found) return;
|
|
2399
|
-
if (name
|
|
2400
|
-
const p =
|
|
3159
|
+
if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
|
|
3160
|
+
const p = join10(d, name);
|
|
2401
3161
|
let st;
|
|
2402
3162
|
try {
|
|
2403
3163
|
st = lstatSync2(p);
|
|
@@ -2408,7 +3168,7 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2408
3168
|
if (st.isDirectory()) walk2(p, depth + 1);
|
|
2409
3169
|
else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
|
|
2410
3170
|
try {
|
|
2411
|
-
if (readFileSync4(p, "utf8")
|
|
3171
|
+
if (importsAndUsesGuard(readFileSync4(p, "utf8"), p, guardDir, packageIsModule)) found = true;
|
|
2412
3172
|
} catch {
|
|
2413
3173
|
}
|
|
2414
3174
|
}
|
|
@@ -2419,6 +3179,15 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2419
3179
|
}
|
|
2420
3180
|
|
|
2421
3181
|
// src/protect/install/register.ts
|
|
3182
|
+
var routeRegistration = (appVar) => new RegExp(
|
|
3183
|
+
`^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*['"\`]`,
|
|
3184
|
+
"m"
|
|
3185
|
+
);
|
|
3186
|
+
function routesBefore(source, appVar, guardIndex) {
|
|
3187
|
+
if (guardIndex < 0) return [];
|
|
3188
|
+
const pattern = routeRegistration(appVar);
|
|
3189
|
+
return source.split("\n").map((line, index) => ({ line, index })).filter(({ line, index }) => index < guardIndex && pattern.test(line)).map(({ index }) => index + 1);
|
|
3190
|
+
}
|
|
2422
3191
|
var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
|
|
2423
3192
|
function guardTarget(cwd, entryRel, spec) {
|
|
2424
3193
|
if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
|
|
@@ -2447,11 +3216,48 @@ function guardTarget(cwd, entryRel, spec) {
|
|
|
2447
3216
|
}
|
|
2448
3217
|
function packageType(cwd) {
|
|
2449
3218
|
try {
|
|
2450
|
-
return JSON.parse(read(
|
|
3219
|
+
return JSON.parse(read(join11(cwd, "package.json"))).type;
|
|
2451
3220
|
} catch {
|
|
2452
3221
|
return void 0;
|
|
2453
3222
|
}
|
|
2454
3223
|
}
|
|
3224
|
+
function wiringState(source, spec, appVar, fileRel, guard) {
|
|
3225
|
+
const code = stripComments(source);
|
|
3226
|
+
const masked = maskStringContents(code);
|
|
3227
|
+
const lines = code.split("\n");
|
|
3228
|
+
const maskedLines = masked.split("\n");
|
|
3229
|
+
const call = spec.call(appVar);
|
|
3230
|
+
const callIndex = maskedLines.findIndex((line) => line.includes(call));
|
|
3231
|
+
const importIndex = maskedLines.findIndex(
|
|
3232
|
+
(line, index) => bindsGuard(lines[index] ?? "", line, spec.importName, call, fileRel, guard)
|
|
3233
|
+
);
|
|
3234
|
+
const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
|
|
3235
|
+
const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
|
|
3236
|
+
const anchorIndex = anchor ? maskedLines.findIndex((line) => anchor.test(line)) : -1;
|
|
3237
|
+
return {
|
|
3238
|
+
importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
|
|
3239
|
+
callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
|
|
3240
|
+
ordered: anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1,
|
|
3241
|
+
callIndex,
|
|
3242
|
+
appIndex
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
|
|
3246
|
+
if (!maskedLine.includes(importName) || maskedLine.includes(call)) return false;
|
|
3247
|
+
if (/^\s*export\b[^=]*\bfrom\b/.test(maskedLine)) return false;
|
|
3248
|
+
if (!/^\s*(?:import\b|(?:export\s+)?(?:const|let|var)\s+[^=]+=\s*require\()/.test(maskedLine)) return false;
|
|
3249
|
+
const specifier = /(['"`])([^'"`]*)\1/.exec(line)?.[2];
|
|
3250
|
+
if (specifier === void 0 || specifier === "") return false;
|
|
3251
|
+
return namesGuardModule(specifier, fileRel, guard);
|
|
3252
|
+
}
|
|
3253
|
+
function namesGuardModule(specifier, fileRel, guard) {
|
|
3254
|
+
return resolvesToGuardModule(specifier, {
|
|
3255
|
+
fromFile: join11(guard.cwd, fileRel),
|
|
3256
|
+
guardDir: resolve2(join11(guard.cwd, guard.guardDir)),
|
|
3257
|
+
kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
|
|
3258
|
+
strictExtensions: true
|
|
3259
|
+
});
|
|
3260
|
+
}
|
|
2455
3261
|
function wireRegister(cwd, opts, spec) {
|
|
2456
3262
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
2457
3263
|
if (!entry) {
|
|
@@ -2459,14 +3265,27 @@ function wireRegister(cwd, opts, spec) {
|
|
|
2459
3265
|
log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
|
|
2460
3266
|
return { ok: true, changed: changed2 };
|
|
2461
3267
|
}
|
|
3268
|
+
if (entry.others.length > 0) {
|
|
3269
|
+
log(
|
|
3270
|
+
`${entry.others.map((other) => other.relPath).join(", ")} also start${entry.others.length === 1 ? "s" : ""} a server \u2014 wired ${entry.relPath} only; ${spec.manualHint} there too`
|
|
3271
|
+
);
|
|
3272
|
+
}
|
|
2462
3273
|
const target = guardTarget(cwd, entry.relPath, spec);
|
|
2463
3274
|
const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
|
|
2464
|
-
const p =
|
|
3275
|
+
const p = join11(cwd, entry.relPath);
|
|
2465
3276
|
const s = read(p);
|
|
2466
|
-
|
|
2467
|
-
|
|
3277
|
+
const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
|
|
3278
|
+
if (state.importAtTopLevel && state.callInAppScope) {
|
|
3279
|
+
log(
|
|
3280
|
+
state.ordered ? `${entry.relPath} already wired` : `${entry.relPath} has the guard registered before the body parser, where it cannot read the body \u2014 move \`${spec.call(entry.appVar).trim()}\` below the parser`
|
|
3281
|
+
);
|
|
2468
3282
|
return { ok: true, changed };
|
|
2469
3283
|
}
|
|
3284
|
+
if (state.importAtTopLevel || state.callInAppScope) {
|
|
3285
|
+
log(
|
|
3286
|
+
`${entry.relPath} has the guard ${state.importAtTopLevel ? "imported but not registered" : "registered but not imported"} \u2014 completing it`
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
2470
3289
|
const sourceLines = s.split("\n");
|
|
2471
3290
|
const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
|
|
2472
3291
|
const requiredAnchor = spec.callAfter?.(entry.appVar);
|
|
@@ -2479,40 +3298,85 @@ function wireRegister(cwd, opts, spec) {
|
|
|
2479
3298
|
importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
|
|
2480
3299
|
);
|
|
2481
3300
|
const lines = s.split("\n");
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
3301
|
+
if (!state.importAtTopLevel) {
|
|
3302
|
+
const lastImport = lastTopLevelImportLine(s);
|
|
3303
|
+
const importIdx = lastImport === -1 ? firstStatementLine(lines) : lastImport + 1;
|
|
3304
|
+
lines.splice(importIdx, 0, importLine);
|
|
2485
3305
|
}
|
|
2486
|
-
lines.splice(lastImport + 1, 0, importLine);
|
|
2487
3306
|
const appIdx = lines.findIndex((l) => spec.appRe.test(l));
|
|
2488
|
-
if (appIdx !== -1) {
|
|
3307
|
+
if (appIdx !== -1 && !state.callInAppScope) {
|
|
2489
3308
|
const preferred = spec.callAfter?.(entry.appVar);
|
|
2490
3309
|
const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
|
|
2491
3310
|
const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
|
|
2492
3311
|
lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
|
|
2493
3312
|
}
|
|
2494
|
-
|
|
3313
|
+
const patched = lines.join("\n");
|
|
3314
|
+
writeFileSync5(p, patched);
|
|
3315
|
+
if (spec.callAfter) {
|
|
3316
|
+
const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
|
|
3317
|
+
const early = routesBefore(patched, entry.appVar, guardIndex);
|
|
3318
|
+
if (early.length > 0) {
|
|
3319
|
+
log(
|
|
3320
|
+
`${entry.relPath}: ${early.length} route${early.length === 1 ? "" : "s"} registered before the guard (line${early.length === 1 ? "" : "s"} ${early.join(", ")}) \u2014 those are not screened. Move them below the guard, or move your body parser above them.`
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
const parsed = parses(p);
|
|
3325
|
+
if (parsed === false) {
|
|
3326
|
+
writeFileSync5(p, s);
|
|
3327
|
+
log(`${entry.relPath} would not parse after patching \u2014 reverted; ${spec.manualHint}`);
|
|
3328
|
+
return { ok: true, changed };
|
|
3329
|
+
}
|
|
2495
3330
|
changed.push(entry.relPath);
|
|
2496
3331
|
log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
|
|
2497
3332
|
return { ok: true, changed: [...new Set(changed)] };
|
|
2498
3333
|
}
|
|
3334
|
+
function firstStatementLine(lines) {
|
|
3335
|
+
let i = 0;
|
|
3336
|
+
if ((lines[0] ?? "").startsWith("#!")) i = 1;
|
|
3337
|
+
while (i < lines.length) {
|
|
3338
|
+
const line = (lines[i] ?? "").trim();
|
|
3339
|
+
if (line !== "" && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) break;
|
|
3340
|
+
i++;
|
|
3341
|
+
}
|
|
3342
|
+
return i;
|
|
3343
|
+
}
|
|
3344
|
+
function serverIsGuarded(cwd, spec, server, guardDir) {
|
|
3345
|
+
const source = read(join11(cwd, server.relPath));
|
|
3346
|
+
const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
|
|
3347
|
+
if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
|
|
3348
|
+
return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
|
|
3349
|
+
}
|
|
2499
3350
|
function verifyRegister(cwd, spec) {
|
|
2500
|
-
const dir =
|
|
3351
|
+
const dir = existsSync9(join11(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
2501
3352
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
2502
3353
|
const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
|
|
2503
|
-
const scaffolded = target ?
|
|
2504
|
-
const entrySource = entry ? read(
|
|
2505
|
-
const
|
|
2506
|
-
const
|
|
2507
|
-
const
|
|
2508
|
-
const
|
|
2509
|
-
const
|
|
2510
|
-
const wired = entrySource.includes(spec.importName) && ordered;
|
|
3354
|
+
const scaffolded = target ? existsSync9(join11(cwd, dir, target.file)) : false;
|
|
3355
|
+
const entrySource = entry ? read(join11(cwd, entry.relPath)) : "";
|
|
3356
|
+
const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
|
|
3357
|
+
const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
|
|
3358
|
+
const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
|
|
3359
|
+
const noEarlyRoutes = early.length === 0;
|
|
3360
|
+
const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
|
|
2511
3361
|
return {
|
|
2512
|
-
wired: scaffolded && wired,
|
|
3362
|
+
wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
|
|
2513
3363
|
checks: [
|
|
2514
3364
|
{ label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
2515
|
-
{ label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
|
|
3365
|
+
{ label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
|
|
3366
|
+
...entry && entry.others.length > 0 ? [
|
|
3367
|
+
{
|
|
3368
|
+
label: "every server in this project has a guard",
|
|
3369
|
+
ok: unguarded.length === 0,
|
|
3370
|
+
hint: unguarded.length === 0 ? "nothing to do" : `${unguarded.join(", ")} start${unguarded.length === 1 ? "s" : ""} a server without one \u2014 ${spec.manualHint} there`
|
|
3371
|
+
}
|
|
3372
|
+
] : [],
|
|
3373
|
+
...spec.callAfter ? [
|
|
3374
|
+
{
|
|
3375
|
+
label: "every route registered after the guard",
|
|
3376
|
+
ok: noEarlyRoutes,
|
|
3377
|
+
hint: noEarlyRoutes ? "nothing to do" : `move the route${early.length === 1 ? "" : "s"} on line${early.length === 1 ? "" : "s"} ${early.join(", ")} below the guard, or move your body parser above them`
|
|
3378
|
+
}
|
|
3379
|
+
] : []
|
|
2516
3380
|
]
|
|
2517
3381
|
};
|
|
2518
3382
|
}
|
|
@@ -2609,18 +3473,35 @@ function runProtect(cwd, opts = {}) {
|
|
|
2609
3473
|
return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
|
|
2610
3474
|
}
|
|
2611
3475
|
}
|
|
3476
|
+
var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["next", "sveltekit", "astro", "nuxt", "generic"]);
|
|
3477
|
+
function credentialNote(cwd, adapterName) {
|
|
3478
|
+
if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
|
|
3479
|
+
const localHint = hasResolvableCredential(cwd) ? "a credential is configured here; set the same one in your deployment" : "no credential found here either \u2014 run `npx @patchstack/connect scan` to provision one";
|
|
3480
|
+
return [
|
|
3481
|
+
{
|
|
3482
|
+
label: "live rule updates need PATCHSTACK_API_KEY in the deployment environment",
|
|
3483
|
+
ok: true,
|
|
3484
|
+
unverifiable: true,
|
|
3485
|
+
hint: `this runtime cannot read .patchstackrc.local.json \u2014 ${localHint}. Without it the guard keeps running on the rules it shipped with.`
|
|
3486
|
+
}
|
|
3487
|
+
];
|
|
3488
|
+
}
|
|
2612
3489
|
function runVerify(cwd) {
|
|
2613
3490
|
try {
|
|
2614
3491
|
const adapter = ADAPTERS.find((a) => a.detect(cwd));
|
|
2615
|
-
if (adapter)
|
|
2616
|
-
|
|
3492
|
+
if (adapter) {
|
|
3493
|
+
const result = adapter.verify(cwd);
|
|
3494
|
+
return { stack: adapter.label, ...result, checks: [...result.checks, ...credentialNote(cwd, adapter.name)] };
|
|
3495
|
+
}
|
|
3496
|
+
const generic = genericVerify(cwd);
|
|
3497
|
+
return { stack: "generic", ...generic, checks: [...generic.checks, ...credentialNote(cwd, "generic")] };
|
|
2617
3498
|
} catch (err) {
|
|
2618
3499
|
return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
|
|
2619
3500
|
}
|
|
2620
3501
|
}
|
|
2621
3502
|
|
|
2622
3503
|
// src/widget.ts
|
|
2623
|
-
import { existsSync as
|
|
3504
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2624
3505
|
import path8 from "path";
|
|
2625
3506
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
2626
3507
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
@@ -2659,7 +3540,7 @@ ${indent}</body>`;
|
|
|
2659
3540
|
}
|
|
2660
3541
|
function findSourceShell(cwd) {
|
|
2661
3542
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
2662
|
-
if (
|
|
3543
|
+
if (existsSync10(path8.join(cwd, candidate))) {
|
|
2663
3544
|
return candidate;
|
|
2664
3545
|
}
|
|
2665
3546
|
}
|
|
@@ -2753,7 +3634,7 @@ function detectPackageManager(cwd) {
|
|
|
2753
3634
|
return declared;
|
|
2754
3635
|
}
|
|
2755
3636
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
2756
|
-
if (
|
|
3637
|
+
if (existsSync11(path9.join(cwd, filename))) {
|
|
2757
3638
|
return pm;
|
|
2758
3639
|
}
|
|
2759
3640
|
}
|
|
@@ -2850,7 +3731,7 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
2850
3731
|
...GENERIC_WIDGET_FILES
|
|
2851
3732
|
];
|
|
2852
3733
|
for (const candidate of candidates) {
|
|
2853
|
-
if (
|
|
3734
|
+
if (existsSync11(path9.join(cwd, candidate))) {
|
|
2854
3735
|
return candidate;
|
|
2855
3736
|
}
|
|
2856
3737
|
}
|
|
@@ -3087,7 +3968,7 @@ function renderGuideChecklist(state, useColor) {
|
|
|
3087
3968
|
done(
|
|
3088
3969
|
paint(
|
|
3089
3970
|
ANSI.bold,
|
|
3090
|
-
"All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet."
|
|
3971
|
+
"All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet. Never commit .patchstackrc.local.json \u2014 it holds the API key, and setup has already added it to .gitignore."
|
|
3091
3972
|
)
|
|
3092
3973
|
)
|
|
3093
3974
|
);
|
|
@@ -3227,13 +4108,16 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
3227
4108
|
return waitForApproval(config, started.pending, deps);
|
|
3228
4109
|
}
|
|
3229
4110
|
|
|
4111
|
+
// src/map-command.ts
|
|
4112
|
+
import { writeFileSync as writeFileSync8 } from "fs";
|
|
4113
|
+
|
|
3230
4114
|
// src/map/ts-loader.ts
|
|
3231
4115
|
import { createRequire } from "module";
|
|
3232
4116
|
import { pathToFileURL } from "url";
|
|
3233
|
-
import { join as
|
|
4117
|
+
import { join as join12 } from "path";
|
|
3234
4118
|
async function loadTypeScript(cwd) {
|
|
3235
4119
|
try {
|
|
3236
|
-
const req = createRequire(pathToFileURL(
|
|
4120
|
+
const req = createRequire(pathToFileURL(join12(cwd, "package.json")));
|
|
3237
4121
|
const resolved = req.resolve("typescript");
|
|
3238
4122
|
const mod = await import(pathToFileURL(resolved).href);
|
|
3239
4123
|
return mod.default ?? mod;
|
|
@@ -3505,7 +4389,7 @@ function npmPackageOf(spec) {
|
|
|
3505
4389
|
|
|
3506
4390
|
// src/map/sources.ts
|
|
3507
4391
|
import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
3508
|
-
import { join as
|
|
4392
|
+
import { join as join13, relative as relative2, isAbsolute } from "path";
|
|
3509
4393
|
|
|
3510
4394
|
// src/map/routes.ts
|
|
3511
4395
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -3589,7 +4473,7 @@ function hasEntrySignal(text) {
|
|
|
3589
4473
|
}
|
|
3590
4474
|
function detectFramework(cwd) {
|
|
3591
4475
|
try {
|
|
3592
|
-
const pkg = JSON.parse(readFileSync8(
|
|
4476
|
+
const pkg = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
|
|
3593
4477
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3594
4478
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
3595
4479
|
if (d["next"]) return "next";
|
|
@@ -3601,11 +4485,11 @@ function detectFramework(cwd) {
|
|
|
3601
4485
|
} catch {
|
|
3602
4486
|
}
|
|
3603
4487
|
try {
|
|
3604
|
-
if (statSync3(
|
|
4488
|
+
if (statSync3(join13(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
3605
4489
|
} catch {
|
|
3606
4490
|
}
|
|
3607
4491
|
try {
|
|
3608
|
-
if (statSync3(
|
|
4492
|
+
if (statSync3(join13(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
3609
4493
|
} catch {
|
|
3610
4494
|
}
|
|
3611
4495
|
return "unknown";
|
|
@@ -3634,7 +4518,30 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
|
3634
4518
|
"temp",
|
|
3635
4519
|
"__pycache__"
|
|
3636
4520
|
]);
|
|
3637
|
-
|
|
4521
|
+
var SKIP_IS_DEFINITIONAL = /* @__PURE__ */ new Set(["node_modules", ".git"]);
|
|
4522
|
+
var PROBE_BUDGET = 4096;
|
|
4523
|
+
function probeForSource(dir, budget) {
|
|
4524
|
+
let entries;
|
|
4525
|
+
try {
|
|
4526
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
4527
|
+
} catch {
|
|
4528
|
+
return "unknown";
|
|
4529
|
+
}
|
|
4530
|
+
const dirs = [];
|
|
4531
|
+
for (const e of entries) {
|
|
4532
|
+
if (budget.left-- <= 0) return "unknown";
|
|
4533
|
+
if (e.isDirectory()) {
|
|
4534
|
+
if (!SKIP_IS_DEFINITIONAL.has(e.name)) dirs.push(join13(dir, e.name));
|
|
4535
|
+
} else if (isSourceFile(e.name)) return true;
|
|
4536
|
+
else if (e.isSymbolicLink()) return "unknown";
|
|
4537
|
+
}
|
|
4538
|
+
for (const sub of dirs) {
|
|
4539
|
+
const found = probeForSource(sub, budget);
|
|
4540
|
+
if (found !== false) return found;
|
|
4541
|
+
}
|
|
4542
|
+
return false;
|
|
4543
|
+
}
|
|
4544
|
+
function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ new Set(), stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 }, probeBudget = { left: PROBE_BUDGET }) {
|
|
3638
4545
|
let key;
|
|
3639
4546
|
try {
|
|
3640
4547
|
key = realpathSync(dir);
|
|
@@ -3657,9 +4564,18 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
3657
4564
|
}
|
|
3658
4565
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
3659
4566
|
for (const e of entries) {
|
|
3660
|
-
if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".")
|
|
3661
|
-
|
|
3662
|
-
|
|
4567
|
+
if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") {
|
|
4568
|
+
const probeable = !SKIP_IS_DEFINITIONAL.has(e.name);
|
|
4569
|
+
if (probeable && !e.isDirectory() && e.isSymbolicLink()) stats.skippedUnknown++;
|
|
4570
|
+
else if (probeable && e.isDirectory()) {
|
|
4571
|
+
const held = probeForSource(join13(dir, e.name), probeBudget);
|
|
4572
|
+
if (held === true) stats.skippedWithSource.push(join13(dir, e.name));
|
|
4573
|
+
else if (held === "unknown") stats.skippedUnknown++;
|
|
4574
|
+
}
|
|
4575
|
+
continue;
|
|
4576
|
+
}
|
|
4577
|
+
const full = join13(dir, e.name);
|
|
4578
|
+
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
3663
4579
|
else if (e.isSymbolicLink()) {
|
|
3664
4580
|
let st, real;
|
|
3665
4581
|
try {
|
|
@@ -3673,7 +4589,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
3673
4589
|
if (st.isDirectory() || isSourceFile(e.name)) stats.unwalked++;
|
|
3674
4590
|
continue;
|
|
3675
4591
|
}
|
|
3676
|
-
if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
|
|
4592
|
+
if (st.isDirectory()) collectSources(full, boundary, opts, out, seen, stats, probeBudget);
|
|
3677
4593
|
else if (st.isFile() && isSourceFile(e.name)) {
|
|
3678
4594
|
out.push(full);
|
|
3679
4595
|
stats.discovered++;
|
|
@@ -3728,7 +4644,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3728
4644
|
const found = [];
|
|
3729
4645
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
3730
4646
|
for (const file of candidate.files ?? []) {
|
|
3731
|
-
const full =
|
|
4647
|
+
const full = join13(cwd, file);
|
|
3732
4648
|
try {
|
|
3733
4649
|
if (statSync3(full).isFile() && inProject(full)) {
|
|
3734
4650
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
@@ -3738,7 +4654,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3738
4654
|
}
|
|
3739
4655
|
}
|
|
3740
4656
|
for (const dir of candidate.dirs ?? []) {
|
|
3741
|
-
const full =
|
|
4657
|
+
const full = join13(cwd, dir);
|
|
3742
4658
|
try {
|
|
3743
4659
|
if (!statSync3(full).isDirectory() || !inProject(full)) continue;
|
|
3744
4660
|
} catch {
|
|
@@ -3757,7 +4673,7 @@ function holdsFunctionEntry(dir) {
|
|
|
3757
4673
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
3758
4674
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3759
4675
|
try {
|
|
3760
|
-
for (const nested of readdirSync5(
|
|
4676
|
+
for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
|
|
3761
4677
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
3762
4678
|
}
|
|
3763
4679
|
} catch {
|
|
@@ -3773,7 +4689,7 @@ function holdsSourceFile(dir) {
|
|
|
3773
4689
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
3774
4690
|
if (entry.isDirectory()) {
|
|
3775
4691
|
try {
|
|
3776
|
-
for (const nested of readdirSync5(
|
|
4692
|
+
for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
|
|
3777
4693
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
3778
4694
|
}
|
|
3779
4695
|
} catch {
|
|
@@ -3787,7 +4703,7 @@ function holdsSourceFile(dir) {
|
|
|
3787
4703
|
|
|
3788
4704
|
// src/map/surface.ts
|
|
3789
4705
|
import { readFileSync as readFileSync9 } from "fs";
|
|
3790
|
-
import { join as
|
|
4706
|
+
import { join as join14 } from "path";
|
|
3791
4707
|
var STATIC_GENERATORS = [
|
|
3792
4708
|
{ dep: "vite", label: "vite" },
|
|
3793
4709
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -3825,7 +4741,7 @@ var SHAPE_SIGNAL = {
|
|
|
3825
4741
|
};
|
|
3826
4742
|
function readManifest(cwd) {
|
|
3827
4743
|
try {
|
|
3828
|
-
const parsed = JSON.parse(readFileSync9(
|
|
4744
|
+
const parsed = JSON.parse(readFileSync9(join14(cwd, "package.json"), "utf8"));
|
|
3829
4745
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
3830
4746
|
} catch {
|
|
3831
4747
|
return null;
|
|
@@ -3838,7 +4754,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
3838
4754
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
3839
4755
|
let text;
|
|
3840
4756
|
try {
|
|
3841
|
-
text = readFileSync9(
|
|
4757
|
+
text = readFileSync9(join14(cwd, file), "utf8");
|
|
3842
4758
|
} catch {
|
|
3843
4759
|
continue;
|
|
3844
4760
|
}
|
|
@@ -4239,7 +5155,7 @@ function dedupeSinks(sinks, owner) {
|
|
|
4239
5155
|
|
|
4240
5156
|
// src/map/module-graph.ts
|
|
4241
5157
|
import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
4242
|
-
import { dirname as
|
|
5158
|
+
import { dirname as dirname7, join as join15, relative as relative3, resolve as resolvePath } from "path";
|
|
4243
5159
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
4244
5160
|
function createModuleGraph(ts, opts) {
|
|
4245
5161
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -4307,7 +5223,7 @@ function createModuleGraph(ts, opts) {
|
|
|
4307
5223
|
}
|
|
4308
5224
|
function resolveRelativeModule(fromFile, spec) {
|
|
4309
5225
|
if (!spec.startsWith(".")) return void 0;
|
|
4310
|
-
const dir =
|
|
5226
|
+
const dir = dirname7(fromFile);
|
|
4311
5227
|
const base = resolvePath(dir, spec);
|
|
4312
5228
|
const candidates = [];
|
|
4313
5229
|
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
@@ -4317,7 +5233,7 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
4317
5233
|
}
|
|
4318
5234
|
candidates.push(base);
|
|
4319
5235
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
4320
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
5236
|
+
for (const e of RESOLVE_EXTS) candidates.push(join15(base, "index" + e));
|
|
4321
5237
|
for (const c of candidates) {
|
|
4322
5238
|
try {
|
|
4323
5239
|
if (statSync4(c).isFile()) return c;
|
|
@@ -4498,7 +5414,7 @@ function zodShape(node, ts) {
|
|
|
4498
5414
|
}
|
|
4499
5415
|
return shape;
|
|
4500
5416
|
}
|
|
4501
|
-
var REQ_SOURCES = ["body", "query", "params"];
|
|
5417
|
+
var REQ_SOURCES = ["body", "query", "params", "headers", "cookies", "files"];
|
|
4502
5418
|
function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
4503
5419
|
if (!body) return [];
|
|
4504
5420
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -4535,6 +5451,12 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
4535
5451
|
if (ts.isPropertyAccessExpression(n) && isReqSourceExpr(n.expression)) {
|
|
4536
5452
|
record(n.name.text, sourceOfExpr(n.expression));
|
|
4537
5453
|
}
|
|
5454
|
+
if (ts.isElementAccessExpression(n) && isReqSourceExpr(n.expression) && n.argumentExpression && ts.isStringLiteral(n.argumentExpression)) {
|
|
5455
|
+
record(n.argumentExpression.text, sourceOfExpr(n.expression));
|
|
5456
|
+
}
|
|
5457
|
+
if (ts.isCallExpression(n) && ts.isPropertyAccessExpression(n.expression) && n.expression.name.text === "get" && isReqSourceExpr(n.expression.expression) && n.arguments.length === 1 && n.arguments[0] && ts.isStringLiteral(n.arguments[0])) {
|
|
5458
|
+
record(n.arguments[0].text, sourceOfExpr(n.expression.expression));
|
|
5459
|
+
}
|
|
4538
5460
|
if (ts.isVariableDeclaration(n) && n.initializer) {
|
|
4539
5461
|
const init = unwrap(n.initializer);
|
|
4540
5462
|
if (ts.isIdentifier(n.name) && isBodyReadCall(n.initializer)) sourceNames.set(n.name.text, bodyReadSource(n.initializer));
|
|
@@ -4559,9 +5481,8 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
4559
5481
|
function sourceOfExpr(e) {
|
|
4560
5482
|
if (isPayloadExpr(e)) return "server-fn-data";
|
|
4561
5483
|
if (ts.isPropertyAccessExpression(e)) {
|
|
4562
|
-
|
|
4563
|
-
if (e.name.text
|
|
4564
|
-
if (e.name.text === "body") return "body";
|
|
5484
|
+
const named = namespaceSource(e.name.text);
|
|
5485
|
+
if (REQ_SOURCES.includes(e.name.text)) return named;
|
|
4565
5486
|
}
|
|
4566
5487
|
if (ts.isIdentifier(e)) {
|
|
4567
5488
|
const recorded = sourceNames.get(e.text);
|
|
@@ -4572,6 +5493,9 @@ function requestMemberAccesses(params, body, ts, opts = {}) {
|
|
|
4572
5493
|
function namespaceSource(key) {
|
|
4573
5494
|
if (key === "query") return "query";
|
|
4574
5495
|
if (key === "params") return "route-param";
|
|
5496
|
+
if (key === "headers") return "header";
|
|
5497
|
+
if (key === "cookies") return "cookie";
|
|
5498
|
+
if (key === "files") return "file";
|
|
4575
5499
|
return "body";
|
|
4576
5500
|
}
|
|
4577
5501
|
function bodyReadSource(init) {
|
|
@@ -4586,6 +5510,9 @@ function spaceOfKey(key) {
|
|
|
4586
5510
|
if (key === "params") return "route-param";
|
|
4587
5511
|
if (key === "body") return "post";
|
|
4588
5512
|
if (key === "data" || key === "input" || key === "payload") return "post";
|
|
5513
|
+
if (key === "headers") return "server";
|
|
5514
|
+
if (key === "cookies") return "cookie";
|
|
5515
|
+
if (key === "files") return "files";
|
|
4589
5516
|
return void 0;
|
|
4590
5517
|
}
|
|
4591
5518
|
function linkedFlows(body, params, inputs, sinks, ts) {
|
|
@@ -4990,7 +5917,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
4990
5917
|
|
|
4991
5918
|
// src/map/imports.ts
|
|
4992
5919
|
import { readFileSync as readFileSync11 } from "fs";
|
|
4993
|
-
import { join as
|
|
5920
|
+
import { join as join16 } from "path";
|
|
4994
5921
|
var MAX_SITES = 5;
|
|
4995
5922
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
4996
5923
|
function isNpmPackageName(pkg) {
|
|
@@ -5001,7 +5928,7 @@ function readPathAliases(cwd) {
|
|
|
5001
5928
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
5002
5929
|
let paths;
|
|
5003
5930
|
try {
|
|
5004
|
-
const raw = readFileSync11(
|
|
5931
|
+
const raw = readFileSync11(join16(cwd, name), "utf8");
|
|
5005
5932
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
5006
5933
|
paths = parsed.compilerOptions?.paths;
|
|
5007
5934
|
} catch {
|
|
@@ -5365,7 +6292,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5365
6292
|
} catch {
|
|
5366
6293
|
}
|
|
5367
6294
|
const graph = createModuleGraph(ts, { cwd, boundary, followOutside: options.followSymlinks });
|
|
5368
|
-
const stats = { discovered: 0, unwalked: 0 };
|
|
6295
|
+
const stats = { discovered: 0, unwalked: 0, skippedWithSource: [], skippedUnknown: 0 };
|
|
5369
6296
|
const files = collectSources(cwd, boundary, { followOutside: options.followSymlinks }, [], /* @__PURE__ */ new Set(), stats);
|
|
5370
6297
|
const imports = createImportInventory(readPathAliases(cwd));
|
|
5371
6298
|
const invocations = createInvocationInventory();
|
|
@@ -5470,19 +6397,31 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5470
6397
|
if (endpoints.length === 0) notes.push("No recognized server-side entry points found under the analyzed roots.");
|
|
5471
6398
|
const importList = imports.list();
|
|
5472
6399
|
const unmodelled = importList.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6400
|
+
const skippedDirsWithSource = stats.skippedWithSource.map((d) => relative4(cwd, d));
|
|
5473
6401
|
const importCoverageGaps = {
|
|
5474
6402
|
unreadableFiles: failed.length,
|
|
5475
6403
|
unscannableFiles: importScanFailures,
|
|
5476
6404
|
unwalkedPaths: stats.unwalked,
|
|
5477
|
-
unresolvableImports
|
|
6405
|
+
unresolvableImports,
|
|
6406
|
+
// A directory skipped by NAME that turned out to hold source. It moved no other counter, so before
|
|
6407
|
+
// this an app whose server lived under `vendor/` was indistinguishable from one with no such
|
|
6408
|
+
// directory — and the inventory still claimed to be complete.
|
|
6409
|
+
skippedDirsWithSource,
|
|
6410
|
+
skippedDirsUnsettled: stats.skippedUnknown
|
|
5478
6411
|
};
|
|
5479
|
-
const environmentalGaps = failed.length + importScanFailures + stats.unwalked;
|
|
6412
|
+
const environmentalGaps = failed.length + importScanFailures + stats.unwalked + stats.skippedWithSource.length + stats.skippedUnknown;
|
|
5480
6413
|
const importsComplete = environmentalGaps === 0 && unresolvableImports === 0;
|
|
5481
6414
|
notes.push("`imports` lists every package the app imports, from ALL source files \u2014 not only files holding an entry point. Absence of a SINK for a package is never evidence; absence of the PACKAGE is evidence only when coverage.importsComplete is true.");
|
|
5482
6415
|
notes.push(`${unmodelled} of ${importList.length} imported package(s) have no recognized sink family (recognizedSinkKinds: []). The extractor models a small set of API families, so for those packages it cannot tell whether input reaches them: a vulnerability in one must stay "needs review" and can never be closed as unreachable using this map.`);
|
|
5483
6416
|
if (environmentalGaps > 0) {
|
|
5484
6417
|
notes.push(`The import inventory is INCOMPLETE: ${failed.length} file(s) could not be read, ${importScanFailures} could not be scanned, and ${stats.unwalked} path(s) could not be walked at all (unreadable directory, broken link, or a symlink leaving the project). A package may therefore be imported without appearing in \`imports\`. Do not read a package's absence as "not imported" while coverage.importsComplete is false.`);
|
|
5485
6418
|
}
|
|
6419
|
+
if (stats.skippedWithSource.length > 0) {
|
|
6420
|
+
notes.push(`${stats.skippedWithSource.length} director(ies) skipped by name hold source that was NOT analyzed: ${skippedDirsWithSource.slice(0, 10).join(", ")}${skippedDirsWithSource.length > 10 ? ", \u2026" : ""}. The walk excludes these names because they are usually generated or vendored, which is a guess about where app source lives \u2014 and here the guess was wrong. Code in them can import, and serve, anything. Point the scan at the directory, or move the app source out of it, before reading any package's absence as evidence.`);
|
|
6421
|
+
}
|
|
6422
|
+
if (stats.skippedUnknown > 0) {
|
|
6423
|
+
notes.push(`${stats.skippedUnknown} skipped director(ies) could not be settled either way \u2014 the probe for source hit its budget or the directory could not be read. Counted against completeness deliberately: "we stopped looking" is not the same answer as "nothing there".`);
|
|
6424
|
+
}
|
|
5486
6425
|
if (unresolvableImports > 0) {
|
|
5487
6426
|
notes.push(`The import inventory is INCOMPLETE for a reason no re-run can fix: ${unresolvableImports} import(s) do not name a resolvable module \u2014 either the specifier is computed at runtime (\`require(REGISTRY[kind])\`) or the loader itself was aliased (\`const r = require\`), so which package is loaded is not knowable from the source. Those imports appear NOWHERE in \`imports\` \u2014 not as an unresolved entry, simply absent \u2014 so a package's absence is not evidence of it being unused. This is a property of the application's code, not a scan failure. Reported conservatively: an aliased loader counts even if every call through it uses a literal.`);
|
|
5488
6427
|
}
|
|
@@ -5565,8 +6504,88 @@ async function buildInputMap(cwd, options = {}) {
|
|
|
5565
6504
|
return { map };
|
|
5566
6505
|
}
|
|
5567
6506
|
|
|
6507
|
+
// src/flags.ts
|
|
6508
|
+
function getStringFlag(flags, name) {
|
|
6509
|
+
const value = flags.get(name);
|
|
6510
|
+
return typeof value === "string" ? value : void 0;
|
|
6511
|
+
}
|
|
6512
|
+
|
|
6513
|
+
// src/map-command.ts
|
|
6514
|
+
async function runMap(flags) {
|
|
6515
|
+
const cwd = getStringFlag(flags, "dir") ?? process.cwd();
|
|
6516
|
+
const { map, error } = await buildInputMap(cwd, {
|
|
6517
|
+
followSymlinks: flags.get("follow-symlinks") === true
|
|
6518
|
+
});
|
|
6519
|
+
if (!map) {
|
|
6520
|
+
console.error(`patchstack: ${error}`);
|
|
6521
|
+
return 1;
|
|
6522
|
+
}
|
|
6523
|
+
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
|
|
6524
|
+
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
|
|
6525
|
+
const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
|
|
6526
|
+
const c = map.coverage;
|
|
6527
|
+
console.error(
|
|
6528
|
+
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
|
|
6529
|
+
);
|
|
6530
|
+
console.error(
|
|
6531
|
+
// All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
|
|
6532
|
+
// simply contain no server entry point (most of a project is client code). Only `skipped` is a
|
|
6533
|
+
// failure to analyse.
|
|
6534
|
+
`patchstack: ${c.filesDiscovered} file(s) found \u2014 ${c.filesParsed} analysed, ${c.filesPreFiltered} skipped (no server entry point)` + (c.filesSkipped ? `, ${c.filesSkipped} could not be analysed` : "") + `. DETECTED surface only \u2014 static analysis is best-effort; every flow carries the tier it was established at ("exact-local" and "transformed-local" are proven; "imported", "heuristic" and "unknown" are not).`
|
|
6535
|
+
);
|
|
6536
|
+
const invoked = map.apiInvocations ?? [];
|
|
6537
|
+
if (invoked.length > 0) {
|
|
6538
|
+
const c2 = map.coverage;
|
|
6539
|
+
const dependency = c2.callsDependency ?? 0;
|
|
6540
|
+
const ambiguous = c2.callsAmbiguous ?? 0;
|
|
6541
|
+
const denominator = dependency + ambiguous;
|
|
6542
|
+
const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
|
|
6543
|
+
console.error(
|
|
6544
|
+
`patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
|
|
6545
|
+
);
|
|
6546
|
+
}
|
|
6547
|
+
const imported = map.imports ?? [];
|
|
6548
|
+
if (imported.length > 0) {
|
|
6549
|
+
const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
6550
|
+
console.error(
|
|
6551
|
+
`patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
|
|
6552
|
+
);
|
|
6553
|
+
}
|
|
6554
|
+
const json = JSON.stringify(map, null, 2);
|
|
6555
|
+
const out = getStringFlag(flags, "out");
|
|
6556
|
+
if (out) {
|
|
6557
|
+
writeFileSync8(out, json);
|
|
6558
|
+
console.error(`patchstack: wrote ${out}`);
|
|
6559
|
+
} else if (flags.get("upload") !== true) {
|
|
6560
|
+
console.log(json);
|
|
6561
|
+
}
|
|
6562
|
+
if (flags.get("upload") === true) {
|
|
6563
|
+
if (map.endpoints.length === 0) {
|
|
6564
|
+
console.error(
|
|
6565
|
+
"patchstack: no server entry points were recognized \u2014 uploading the import inventory and coverage notes anyway, so a vulnerability can still be judged imported or not. Nothing here can decide whether a request reaches it."
|
|
6566
|
+
);
|
|
6567
|
+
}
|
|
6568
|
+
const config = await resolveConfig({
|
|
6569
|
+
cwd,
|
|
6570
|
+
cliSiteUuid: getStringFlag(flags, "site-uuid"),
|
|
6571
|
+
cliEndpoint: getStringFlag(flags, "endpoint")
|
|
6572
|
+
});
|
|
6573
|
+
const outcome = await postInputMap(config, map);
|
|
6574
|
+
if (outcome.result === "stored") {
|
|
6575
|
+
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
6576
|
+
} else if (outcome.result === "unchanged") {
|
|
6577
|
+
console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
|
|
6578
|
+
} else if (outcome.result === "skipped") {
|
|
6579
|
+
console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
|
|
6580
|
+
} else {
|
|
6581
|
+
console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
|
|
6582
|
+
}
|
|
6583
|
+
}
|
|
6584
|
+
return 0;
|
|
6585
|
+
}
|
|
6586
|
+
|
|
5568
6587
|
// src/setup.ts
|
|
5569
|
-
import { readFileSync as readFileSync13, writeFileSync as
|
|
6588
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
|
|
5570
6589
|
import path11 from "path";
|
|
5571
6590
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
5572
6591
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
@@ -5635,7 +6654,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
5635
6654
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
5636
6655
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
5637
6656
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
5638
|
-
|
|
6657
|
+
writeFileSync9(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
5639
6658
|
if (build === void 0 || build.trim().length === 0) {
|
|
5640
6659
|
return {
|
|
5641
6660
|
changed: true,
|
|
@@ -5714,7 +6733,9 @@ Usage:
|
|
|
5714
6733
|
print the full setup guide. --full prints the
|
|
5715
6734
|
guide even when setup is complete
|
|
5716
6735
|
patchstack-connect login [--wait] Recover this site's credential when
|
|
5717
|
-
.patchstackrc.json has been lost.
|
|
6736
|
+
.patchstackrc.local.json has been lost. Needs the
|
|
6737
|
+
site UUID, which lives in the committed
|
|
6738
|
+
.patchstackrc.json. Prints a link
|
|
5718
6739
|
for the site's OWNER to approve in the dashboard.
|
|
5719
6740
|
In a terminal it then waits. When the output is
|
|
5720
6741
|
piped or captured \u2014 an assistant running it \u2014 it
|
|
@@ -5734,6 +6755,11 @@ Options (for scan, setup, status, and uninstall):
|
|
|
5734
6755
|
--site-uuid <uuid> Override the configured site UUID
|
|
5735
6756
|
--endpoint <url> Override the API endpoint
|
|
5736
6757
|
--dry-run (scan only) Show the payload without posting
|
|
6758
|
+
--install-paths (scan only) Also send where each package is installed
|
|
6759
|
+
in the dependency tree (node_modules/lodash, and the
|
|
6760
|
+
same under a workspace that pins its own copy), so an
|
|
6761
|
+
advisory can be matched to the copy your code actually
|
|
6762
|
+
loads. Off by default; never source file paths
|
|
5737
6763
|
|
|
5738
6764
|
Options (for mark-build):
|
|
5739
6765
|
--dir <path> Build output directory (default: auto-detect
|
|
@@ -5755,7 +6781,8 @@ Environment:
|
|
|
5755
6781
|
PATCHSTACK_MODE (protect) Runtime guard mode: block (default) | dry-run
|
|
5756
6782
|
PATCHSTACK_ROUTE_WAF (protect) Set to 1 to also screen every request at the route level (opt-in)
|
|
5757
6783
|
|
|
5758
|
-
Precedence: CLI flag > environment variable > .patchstackrc.json.
|
|
6784
|
+
Precedence, site UUID and settings: CLI flag > environment variable > .patchstackrc.json (committed).
|
|
6785
|
+
Precedence, API key: environment variable > .patchstackrc.local.json (git-ignored) > .patchstackrc.json.
|
|
5759
6786
|
|
|
5760
6787
|
Examples:
|
|
5761
6788
|
npx @patchstack/connect setup
|
|
@@ -5797,10 +6824,6 @@ function parseArgs(argv) {
|
|
|
5797
6824
|
flags
|
|
5798
6825
|
};
|
|
5799
6826
|
}
|
|
5800
|
-
function getStringFlag(flags, name) {
|
|
5801
|
-
const value = flags.get(name);
|
|
5802
|
-
return typeof value === "string" ? value : void 0;
|
|
5803
|
-
}
|
|
5804
6827
|
async function runInit(args) {
|
|
5805
6828
|
const uuid = args.positional[0];
|
|
5806
6829
|
if (!uuid) {
|
|
@@ -5828,8 +6851,13 @@ async function runLogin(args) {
|
|
|
5828
6851
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
5829
6852
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
5830
6853
|
});
|
|
5831
|
-
const approved = () => {
|
|
5832
|
-
|
|
6854
|
+
const approved = async () => {
|
|
6855
|
+
const ignore = await secretFileIgnored(process.cwd());
|
|
6856
|
+
console.log(`
|
|
6857
|
+
\u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
6858
|
+
console.log(
|
|
6859
|
+
ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
|
|
6860
|
+
);
|
|
5833
6861
|
console.log(" The previous credential no longer works. Update it anywhere else it was set:");
|
|
5834
6862
|
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
5835
6863
|
return 0;
|
|
@@ -5841,7 +6869,7 @@ async function runLogin(args) {
|
|
|
5841
6869
|
return 1;
|
|
5842
6870
|
}
|
|
5843
6871
|
const resumed = await waitForApproval(config, pending);
|
|
5844
|
-
if (resumed.status === "approved") return approved();
|
|
6872
|
+
if (resumed.status === "approved") return await approved();
|
|
5845
6873
|
console.error(`
|
|
5846
6874
|
${resumed.message ?? "Login failed."}
|
|
5847
6875
|
`);
|
|
@@ -5860,7 +6888,7 @@ async function runLogin(args) {
|
|
|
5860
6888
|
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
5861
6889
|
if (existing !== null && Date.now() < existing.expiresAt) {
|
|
5862
6890
|
const outcome = await redeemIfApproved(config, existing);
|
|
5863
|
-
if (outcome === "approved") return approved();
|
|
6891
|
+
if (outcome === "approved") return await approved();
|
|
5864
6892
|
if (outcome === "pending") {
|
|
5865
6893
|
const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
|
|
5866
6894
|
console.log(`
|
|
@@ -5887,83 +6915,12 @@ async function runLogin(args) {
|
|
|
5887
6915
|
prompt(userCode, verificationUri);
|
|
5888
6916
|
console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
|
|
5889
6917
|
});
|
|
5890
|
-
if (result.status === "approved") return approved();
|
|
6918
|
+
if (result.status === "approved") return await approved();
|
|
5891
6919
|
console.error(`
|
|
5892
6920
|
${result.message ?? "Login failed."}
|
|
5893
6921
|
`);
|
|
5894
6922
|
return 1;
|
|
5895
6923
|
}
|
|
5896
|
-
async function runMap(args) {
|
|
5897
|
-
const cwd = getStringFlag(args.flags, "dir") ?? process.cwd();
|
|
5898
|
-
const { map, error } = await buildInputMap(cwd, {
|
|
5899
|
-
followSymlinks: args.flags.get("follow-symlinks") === true
|
|
5900
|
-
});
|
|
5901
|
-
if (!map) {
|
|
5902
|
-
console.error(`patchstack: ${error}`);
|
|
5903
|
-
return 1;
|
|
5904
|
-
}
|
|
5905
|
-
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
|
|
5906
|
-
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
|
|
5907
|
-
const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
|
|
5908
|
-
const c = map.coverage;
|
|
5909
|
-
console.error(
|
|
5910
|
-
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
|
|
5911
|
-
);
|
|
5912
|
-
console.error(
|
|
5913
|
-
// All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
|
|
5914
|
-
// simply contain no server entry point (most of a project is client code). Only `skipped` is a
|
|
5915
|
-
// failure to analyse.
|
|
5916
|
-
`patchstack: ${c.filesDiscovered} file(s) found \u2014 ${c.filesParsed} analysed, ${c.filesPreFiltered} skipped (no server entry point)` + (c.filesSkipped ? `, ${c.filesSkipped} could not be analysed` : "") + `. DETECTED surface only \u2014 static analysis is best-effort; every flow carries the tier it was established at ("exact-local" and "transformed-local" are proven; "imported", "heuristic" and "unknown" are not).`
|
|
5917
|
-
);
|
|
5918
|
-
const invoked = map.apiInvocations ?? [];
|
|
5919
|
-
if (invoked.length > 0) {
|
|
5920
|
-
const c2 = map.coverage;
|
|
5921
|
-
const dependency = c2.callsDependency ?? 0;
|
|
5922
|
-
const ambiguous = c2.callsAmbiguous ?? 0;
|
|
5923
|
-
const denominator = dependency + ambiguous;
|
|
5924
|
-
const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
|
|
5925
|
-
console.error(
|
|
5926
|
-
`patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
|
|
5927
|
-
);
|
|
5928
|
-
}
|
|
5929
|
-
const imported = map.imports ?? [];
|
|
5930
|
-
if (imported.length > 0) {
|
|
5931
|
-
const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
5932
|
-
console.error(
|
|
5933
|
-
`patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
|
|
5934
|
-
);
|
|
5935
|
-
}
|
|
5936
|
-
const json = JSON.stringify(map, null, 2);
|
|
5937
|
-
const out = getStringFlag(args.flags, "out");
|
|
5938
|
-
if (out) {
|
|
5939
|
-
writeFileSync9(out, json);
|
|
5940
|
-
console.error(`patchstack: wrote ${out}`);
|
|
5941
|
-
} else if (args.flags.get("upload") !== true) {
|
|
5942
|
-
console.log(json);
|
|
5943
|
-
}
|
|
5944
|
-
if (args.flags.get("upload") === true) {
|
|
5945
|
-
if (map.endpoints.length === 0) {
|
|
5946
|
-
console.error("patchstack: nothing to upload \u2014 no server entry points were detected.");
|
|
5947
|
-
return 0;
|
|
5948
|
-
}
|
|
5949
|
-
const config = await resolveConfig({
|
|
5950
|
-
cwd,
|
|
5951
|
-
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
5952
|
-
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
5953
|
-
});
|
|
5954
|
-
const outcome = await postInputMap(config, map);
|
|
5955
|
-
if (outcome.result === "stored") {
|
|
5956
|
-
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
5957
|
-
} else if (outcome.result === "unchanged") {
|
|
5958
|
-
console.error(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
|
|
5959
|
-
} else if (outcome.result === "skipped") {
|
|
5960
|
-
console.error(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
|
|
5961
|
-
} else {
|
|
5962
|
-
console.error(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
|
|
5963
|
-
}
|
|
5964
|
-
}
|
|
5965
|
-
return 0;
|
|
5966
|
-
}
|
|
5967
6924
|
async function runScan(args, options = {}) {
|
|
5968
6925
|
const dryRun = args.flags.get("dry-run") === true;
|
|
5969
6926
|
const config = await resolveConfig({
|
|
@@ -5975,10 +6932,17 @@ async function runScan(args, options = {}) {
|
|
|
5975
6932
|
for (const warning of manifest.warnings ?? []) {
|
|
5976
6933
|
console.warn(`patchstack: ${warning}`);
|
|
5977
6934
|
}
|
|
5978
|
-
const
|
|
6935
|
+
const installPaths = args.flags.get("install-paths") === true;
|
|
6936
|
+
const { payload, stats } = buildWirePayload(manifest, { installPaths });
|
|
5979
6937
|
console.log(
|
|
5980
6938
|
`Found ${payload.packages.length} unique package versions across ${stats.uniqueNames} package names (${manifest.ecosystem} ecosystem).`
|
|
5981
6939
|
);
|
|
6940
|
+
if (installPaths) {
|
|
6941
|
+
const located = payload.packages.filter((pkg) => pkg.paths !== void 0).length;
|
|
6942
|
+
console.log(
|
|
6943
|
+
payload.installPathsComplete ? `Including each package's install location in the dependency tree (--install-paths), for all ${located}.` : `Including install locations (--install-paths), for ${located} of ${payload.packages.length} \u2014 this lockfile format does not record them for the rest, which will be reported as "not recorded" rather than "not installed there".`
|
|
6944
|
+
);
|
|
6945
|
+
}
|
|
5982
6946
|
console.log(
|
|
5983
6947
|
`Reporting under the ${config.environment} environment (override with PATCHSTACK_ENVIRONMENT).`
|
|
5984
6948
|
);
|
|
@@ -6022,8 +6986,17 @@ async function runScan(args, options = {}) {
|
|
|
6022
6986
|
console.log(`Provisioned site ${response.uuid}. Saved UUID to ${target}.`);
|
|
6023
6987
|
}
|
|
6024
6988
|
if (typeof response.api_key === "string" && response.api_key.length > 0) {
|
|
6025
|
-
const
|
|
6026
|
-
|
|
6989
|
+
const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
|
|
6990
|
+
const saved = await persistApiKey(process.cwd(), response.api_key);
|
|
6991
|
+
console.log(`Saved API key to ${saved.path}. Do not commit it.`);
|
|
6992
|
+
console.log(
|
|
6993
|
+
saved.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${saved.reason ?? "unknown reason"}. Add \`${SECRET_CONFIG_FILENAME}\` to .gitignore yourself before committing.`
|
|
6994
|
+
);
|
|
6995
|
+
if (hadCredentialInConfig) {
|
|
6996
|
+
console.log(
|
|
6997
|
+
"A credential was also present in .patchstackrc.json and has been removed from it. If that file was ever committed, rotate the credential from the dashboard."
|
|
6998
|
+
);
|
|
6999
|
+
}
|
|
6027
7000
|
}
|
|
6028
7001
|
if (response.stored) {
|
|
6029
7002
|
console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
|
|
@@ -6134,9 +7107,14 @@ async function runProtectCommand(args) {
|
|
|
6134
7107
|
const report = runVerify(process.cwd());
|
|
6135
7108
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
6136
7109
|
for (const c of report.checks) {
|
|
6137
|
-
|
|
7110
|
+
const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
|
|
7111
|
+
const note = c.unverifiable || !c.ok ? c.hint : void 0;
|
|
7112
|
+
console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
|
|
6138
7113
|
}
|
|
6139
7114
|
console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
|
|
7115
|
+
if (report.checks.some((c) => c.unverifiable)) {
|
|
7116
|
+
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7117
|
+
}
|
|
6140
7118
|
return report.wired ? 0 : 1;
|
|
6141
7119
|
}
|
|
6142
7120
|
const demo = args.flags.get("demo") === true;
|
|
@@ -6457,7 +7435,7 @@ async function runMarkBuild(args) {
|
|
|
6457
7435
|
after = ensured.html;
|
|
6458
7436
|
}
|
|
6459
7437
|
if (after !== before) {
|
|
6460
|
-
|
|
7438
|
+
writeFileSync10(file, after);
|
|
6461
7439
|
marked += 1;
|
|
6462
7440
|
}
|
|
6463
7441
|
}
|
|
@@ -6495,7 +7473,7 @@ async function main() {
|
|
|
6495
7473
|
case "setup":
|
|
6496
7474
|
return runSetup(args);
|
|
6497
7475
|
case "map":
|
|
6498
|
-
return runMap(args);
|
|
7476
|
+
return runMap(args.flags);
|
|
6499
7477
|
case "login":
|
|
6500
7478
|
return runLogin(args);
|
|
6501
7479
|
default:
|