@patchstack/connect 0.3.28 → 0.3.30
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 +37 -21
- package/README.md +24 -10
- package/dist/{chunk-MJOTFUDE.js → chunk-LLKP5EJS.js} +1 -1
- package/dist/chunk-LLKP5EJS.js.map +1 -0
- package/dist/cli.js +1267 -224
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +419 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -9
- package/dist/index.d.ts +28 -9
- package/dist/index.js +412 -97
- 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 +1039 -235
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +39 -11
- package/dist/protect.edge.js +663 -132
- package/dist/protect.edge.js.map +4 -4
- package/dist/protect.js +665 -134
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-VRBE6RH6.js → refresh-manifest-ZSP76JWQ.js} +351 -94
- package/dist/refresh-manifest-ZSP76JWQ.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
|
|
4
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync9 } 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,122 @@ 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));
|
|
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) {
|
|
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);
|
|
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);
|
|
370
|
+
});
|
|
371
|
+
return scoped.flat();
|
|
208
372
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
373
|
+
return packageAndNested(fullPath, resolved, depth, seen);
|
|
374
|
+
});
|
|
375
|
+
return perEntry.flat();
|
|
212
376
|
}
|
|
213
|
-
async function
|
|
377
|
+
async function packageAndNested(pkgDir, resolved, depth, seen) {
|
|
378
|
+
const [own, nested] = await Promise.all([readPackage(pkgDir, depth), walkNested(pkgDir, depth, seen)]);
|
|
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;
|
|
392
|
+
}
|
|
393
|
+
async function readPackage(pkgDir, depth) {
|
|
214
394
|
let raw;
|
|
215
395
|
try {
|
|
216
|
-
raw = await
|
|
396
|
+
raw = await readFile4(path3.join(pkgDir, "package.json"), "utf8");
|
|
217
397
|
} catch {
|
|
218
|
-
return;
|
|
398
|
+
return null;
|
|
219
399
|
}
|
|
220
400
|
let parsed;
|
|
221
401
|
try {
|
|
222
402
|
parsed = JSON.parse(raw);
|
|
223
403
|
} catch {
|
|
224
|
-
return;
|
|
404
|
+
return null;
|
|
225
405
|
}
|
|
226
406
|
if (typeof parsed.name !== "string" || parsed.name.length === 0) {
|
|
227
|
-
return;
|
|
407
|
+
return null;
|
|
228
408
|
}
|
|
229
409
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
230
|
-
return;
|
|
410
|
+
return null;
|
|
231
411
|
}
|
|
232
|
-
|
|
233
|
-
name: parsed.name,
|
|
234
|
-
version: parsed.version,
|
|
235
|
-
direct: depth === 0
|
|
236
|
-
});
|
|
412
|
+
return { name: parsed.name, version: parsed.version, direct: depth === 0 };
|
|
237
413
|
}
|
|
238
|
-
async function walkNested(pkgDir,
|
|
414
|
+
async function walkNested(pkgDir, depth, seen) {
|
|
239
415
|
const nested = path3.join(pkgDir, "node_modules");
|
|
240
|
-
if (
|
|
241
|
-
return;
|
|
416
|
+
if (await claim(nested, seen, depth + 1) === null) {
|
|
417
|
+
return [];
|
|
242
418
|
}
|
|
243
|
-
|
|
419
|
+
return walk(nested, depth + 1, seen);
|
|
244
420
|
}
|
|
245
|
-
async function
|
|
421
|
+
async function claim(dir, seen, depth) {
|
|
422
|
+
let resolved;
|
|
246
423
|
try {
|
|
247
424
|
const info = await lstat(dir);
|
|
248
|
-
|
|
425
|
+
if (!info.isDirectory() && !info.isSymbolicLink()) return null;
|
|
426
|
+
resolved = await realpath(dir);
|
|
427
|
+
const target = await stat(resolved);
|
|
428
|
+
if (!target.isDirectory()) return null;
|
|
249
429
|
} catch {
|
|
250
|
-
return
|
|
430
|
+
return null;
|
|
251
431
|
}
|
|
432
|
+
const claimed = seen.get(resolved);
|
|
433
|
+
if (claimed !== void 0 && claimed <= depth) return null;
|
|
434
|
+
seen.set(resolved, depth);
|
|
435
|
+
return resolved;
|
|
252
436
|
}
|
|
253
437
|
|
|
254
438
|
// src/parsers/pnpm.ts
|
|
255
|
-
import { readFile as
|
|
256
|
-
async function parsePnpmLockfile(lockfilePath) {
|
|
439
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
440
|
+
async function parsePnpmLockfile(lockfilePath, report) {
|
|
257
441
|
let raw;
|
|
258
442
|
try {
|
|
259
|
-
raw = await
|
|
443
|
+
raw = await readFile5(lockfilePath, "utf8");
|
|
260
444
|
} catch (cause) {
|
|
261
445
|
throw new PatchstackError(
|
|
262
446
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -277,6 +461,7 @@ async function parsePnpmLockfile(lockfilePath) {
|
|
|
277
461
|
for (const key of packageKeys) {
|
|
278
462
|
const parsed = parsePackageKey(key);
|
|
279
463
|
if (parsed === null) {
|
|
464
|
+
recordUnreadable(report, key);
|
|
280
465
|
continue;
|
|
281
466
|
}
|
|
282
467
|
entries.push({
|
|
@@ -504,12 +689,12 @@ function extractLeafName(trimmed) {
|
|
|
504
689
|
}
|
|
505
690
|
|
|
506
691
|
// src/parsers/yarn.ts
|
|
507
|
-
import { readFile as
|
|
692
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
508
693
|
import path4 from "path";
|
|
509
|
-
async function parseYarnLockfile(lockfilePath) {
|
|
694
|
+
async function parseYarnLockfile(lockfilePath, report) {
|
|
510
695
|
let raw;
|
|
511
696
|
try {
|
|
512
|
-
raw = await
|
|
697
|
+
raw = await readFile6(lockfilePath, "utf8");
|
|
513
698
|
} catch (cause) {
|
|
514
699
|
throw new PatchstackError(
|
|
515
700
|
`Could not read lockfile at ${lockfilePath}`,
|
|
@@ -517,7 +702,7 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
517
702
|
cause
|
|
518
703
|
);
|
|
519
704
|
}
|
|
520
|
-
const blocks = parseBlocks(raw);
|
|
705
|
+
const blocks = parseBlocks(raw, report);
|
|
521
706
|
if (blocks.length === 0) {
|
|
522
707
|
throw new PatchstackError(
|
|
523
708
|
`Lockfile at ${lockfilePath} contains no package entries`,
|
|
@@ -546,13 +731,19 @@ async function parseYarnLockfile(lockfilePath) {
|
|
|
546
731
|
}
|
|
547
732
|
return entries;
|
|
548
733
|
}
|
|
549
|
-
|
|
734
|
+
var NON_PACKAGE_KEYS = /* @__PURE__ */ new Set(["__metadata"]);
|
|
735
|
+
function parseBlocks(raw, report) {
|
|
550
736
|
const lines = raw.split(/\r?\n/);
|
|
551
737
|
const blocks = [];
|
|
552
738
|
let current = null;
|
|
553
739
|
const finalize = () => {
|
|
554
|
-
if (current
|
|
740
|
+
if (current === null) {
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
if (current.version.length > 0 && current.names.size > 0) {
|
|
555
744
|
blocks.push(current);
|
|
745
|
+
} else if (current.unreadable) {
|
|
746
|
+
recordUnreadable(report, current.key);
|
|
556
747
|
}
|
|
557
748
|
current = null;
|
|
558
749
|
};
|
|
@@ -569,13 +760,13 @@ function parseBlocks(raw) {
|
|
|
569
760
|
}
|
|
570
761
|
const keyLine = trimmed.slice(0, -1);
|
|
571
762
|
const names = /* @__PURE__ */ new Set();
|
|
763
|
+
let unreadable = false;
|
|
572
764
|
for (const spec of splitDescriptors(keyLine)) {
|
|
573
|
-
const
|
|
574
|
-
if (name !==
|
|
575
|
-
|
|
576
|
-
}
|
|
765
|
+
const classified = classifyDescriptor(spec);
|
|
766
|
+
if (classified.name !== void 0) names.add(classified.name);
|
|
767
|
+
else if (!classified.excluded) unreadable = true;
|
|
577
768
|
}
|
|
578
|
-
current = { names, version: "" };
|
|
769
|
+
current = { names, version: "", key: keyLine, unreadable: unreadable && names.size === 0 };
|
|
579
770
|
continue;
|
|
580
771
|
}
|
|
581
772
|
if (current === null) {
|
|
@@ -630,20 +821,37 @@ function splitDescriptors(keyLine) {
|
|
|
630
821
|
}
|
|
631
822
|
return parts;
|
|
632
823
|
}
|
|
633
|
-
|
|
824
|
+
var NON_REGISTRY_PROTOCOLS = /^(?:workspace|patch|virtual|portal|file|link|exec|git|github|https?|ssh):/i;
|
|
825
|
+
function classifyDescriptor(rawSpec) {
|
|
634
826
|
let s = rawSpec.trim();
|
|
635
827
|
if (s.length === 0) {
|
|
636
|
-
return
|
|
828
|
+
return { excluded: true };
|
|
637
829
|
}
|
|
638
830
|
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
639
831
|
s = s.slice(1, -1);
|
|
640
832
|
}
|
|
641
|
-
|
|
833
|
+
if (NON_PACKAGE_KEYS.has(s)) {
|
|
834
|
+
return { excluded: true };
|
|
835
|
+
}
|
|
836
|
+
const atIdx = s.indexOf("@", 1);
|
|
642
837
|
if (atIdx <= 0) {
|
|
643
|
-
return
|
|
838
|
+
return {};
|
|
644
839
|
}
|
|
645
840
|
const name = s.slice(0, atIdx);
|
|
646
|
-
|
|
841
|
+
const range = s.slice(atIdx + 1);
|
|
842
|
+
if (name.length === 0) {
|
|
843
|
+
return {};
|
|
844
|
+
}
|
|
845
|
+
if (NON_REGISTRY_PROTOCOLS.test(range)) {
|
|
846
|
+
return { excluded: true };
|
|
847
|
+
}
|
|
848
|
+
const alias = /^npm:(.+)$/i.exec(range);
|
|
849
|
+
if (alias !== null) {
|
|
850
|
+
const target = alias[1] ?? "";
|
|
851
|
+
const targetAt = target.indexOf("@", 1);
|
|
852
|
+
return { name: targetAt > 0 ? target.slice(0, targetAt) : name };
|
|
853
|
+
}
|
|
854
|
+
return { name };
|
|
647
855
|
}
|
|
648
856
|
function parseVersionField(content) {
|
|
649
857
|
if (!content.startsWith("version")) {
|
|
@@ -668,7 +876,7 @@ async function readDirectDepNames(cwd) {
|
|
|
668
876
|
const names = /* @__PURE__ */ new Set();
|
|
669
877
|
let raw;
|
|
670
878
|
try {
|
|
671
|
-
raw = await
|
|
879
|
+
raw = await readFile6(path4.join(cwd, "package.json"), "utf8");
|
|
672
880
|
} catch {
|
|
673
881
|
return names;
|
|
674
882
|
}
|
|
@@ -697,7 +905,10 @@ async function readDirectDepNames(cwd) {
|
|
|
697
905
|
// src/parsers/index.ts
|
|
698
906
|
var LOCKFILE_CANDIDATES = [
|
|
699
907
|
{ filename: "package-lock.json", strategy: "npm-lockfile" },
|
|
700
|
-
|
|
908
|
+
// Read directly, not walked. Bun's isolated layout puts packages under a dot-directory and makes the
|
|
909
|
+
// top level symlinks into it, so a `node_modules` walk finds nothing there — and isolated is the default
|
|
910
|
+
// for new workspaces.
|
|
911
|
+
{ filename: "bun.lock", strategy: "bun-lockfile" },
|
|
701
912
|
{ filename: "bun.lockb", strategy: "node-modules-walk" },
|
|
702
913
|
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
703
914
|
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
@@ -732,33 +943,57 @@ async function scanLockfile(cwd) {
|
|
|
732
943
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
733
944
|
const warnings = [];
|
|
734
945
|
let firstParsed = null;
|
|
735
|
-
let
|
|
946
|
+
let walked = null;
|
|
947
|
+
let walkWasCandidate = false;
|
|
948
|
+
const parsedSources = [];
|
|
736
949
|
for (const candidate of candidates) {
|
|
737
950
|
let packages;
|
|
951
|
+
const report = newReport();
|
|
738
952
|
try {
|
|
739
|
-
packages = await runStrategy(candidate, cwd);
|
|
953
|
+
packages = await runStrategy(candidate, cwd, report);
|
|
740
954
|
} catch {
|
|
741
955
|
continue;
|
|
742
956
|
}
|
|
743
|
-
|
|
957
|
+
const unreadable = unreadableWarning(candidate.filename, report);
|
|
958
|
+
if (unreadable !== null) warnings.push(unreadable);
|
|
959
|
+
if (candidate.strategy === "node-modules-walk") {
|
|
960
|
+
walked = packages;
|
|
961
|
+
walkWasCandidate = true;
|
|
962
|
+
}
|
|
744
963
|
firstParsed ??= { packages, filename: candidate.filename };
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
964
|
+
parsedSources.push({ packages, filename: candidate.filename });
|
|
965
|
+
}
|
|
966
|
+
for (const source of parsedSources) {
|
|
967
|
+
const missing = missingDependencies(declared, source.packages);
|
|
968
|
+
if (missing.length > 0) {
|
|
969
|
+
warnings.push(staleWarning(source.filename, missing));
|
|
970
|
+
continue;
|
|
748
971
|
}
|
|
749
|
-
|
|
972
|
+
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
973
|
+
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
974
|
+
);
|
|
975
|
+
if (conflicts.length === 0) {
|
|
976
|
+
return manifestWith(source.packages, warnings);
|
|
977
|
+
}
|
|
978
|
+
warnings.push(conflictWarning(source.filename, conflicts));
|
|
979
|
+
break;
|
|
750
980
|
}
|
|
751
|
-
if (
|
|
981
|
+
if (walked === null) {
|
|
752
982
|
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);
|
|
983
|
+
walked = await walkNodeModules(cwd);
|
|
760
984
|
} catch {
|
|
985
|
+
walked = null;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (walked !== null) {
|
|
989
|
+
const missing = missingDependencies(declared, walked);
|
|
990
|
+
if (missing.length > 0) {
|
|
991
|
+
warnings.push(staleWarning("node_modules/", missing));
|
|
761
992
|
}
|
|
993
|
+
warnings.push(
|
|
994
|
+
walkWasCandidate ? "Reporting node_modules/, which is what the build loads." : "Scanned node_modules/ instead. Delete the stale lockfile to silence this warning."
|
|
995
|
+
);
|
|
996
|
+
return manifestWith(walked, warnings);
|
|
762
997
|
}
|
|
763
998
|
if (firstParsed === null) {
|
|
764
999
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
@@ -775,6 +1010,15 @@ async function scanLockfile(cwd) {
|
|
|
775
1010
|
function manifestWith(packages, warnings) {
|
|
776
1011
|
return warnings.length > 0 ? { ecosystem: "npm", packages, warnings } : { ecosystem: "npm", packages };
|
|
777
1012
|
}
|
|
1013
|
+
function otherCompleteSources(all, current, declared) {
|
|
1014
|
+
return all.filter(
|
|
1015
|
+
(source) => source.filename !== current.filename && missingDependencies(declared, source.packages).length === 0
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
function conflictWarning(filename, conflicts) {
|
|
1019
|
+
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
1020
|
+
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.`;
|
|
1021
|
+
}
|
|
778
1022
|
function staleWarning(source, missing) {
|
|
779
1023
|
const sample = missing.slice(0, 3).join(", ");
|
|
780
1024
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
@@ -789,14 +1033,18 @@ async function presentLockfiles(cwd) {
|
|
|
789
1033
|
);
|
|
790
1034
|
return probed.filter((candidate) => candidate.present).map(({ filename, strategy, filePath }) => ({ ecosystem: "npm", filename, strategy, filePath }));
|
|
791
1035
|
}
|
|
792
|
-
async function runStrategy(detected, cwd) {
|
|
1036
|
+
async function runStrategy(detected, cwd, report) {
|
|
793
1037
|
switch (detected.strategy) {
|
|
794
1038
|
case "npm-lockfile":
|
|
795
1039
|
return parseNpmLockfile(detected.filePath);
|
|
1040
|
+
case "bun-lockfile":
|
|
1041
|
+
return parseBunLockfile(detected.filePath, report);
|
|
1042
|
+
// The hand-written scanners. Each meets entries it was not written for, and each is asked to say how
|
|
1043
|
+
// many it dropped, because a manifest short a package reads exactly like a project without it.
|
|
796
1044
|
case "pnpm-lockfile":
|
|
797
|
-
return parsePnpmLockfile(detected.filePath);
|
|
1045
|
+
return parsePnpmLockfile(detected.filePath, report);
|
|
798
1046
|
case "yarn-lockfile":
|
|
799
|
-
return parseYarnLockfile(detected.filePath);
|
|
1047
|
+
return parseYarnLockfile(detected.filePath, report);
|
|
800
1048
|
case "node-modules-walk":
|
|
801
1049
|
return walkNodeModules(cwd);
|
|
802
1050
|
}
|
|
@@ -1228,7 +1476,7 @@ function isTimeoutError(cause) {
|
|
|
1228
1476
|
}
|
|
1229
1477
|
|
|
1230
1478
|
// src/demo.ts
|
|
1231
|
-
import { readFile as
|
|
1479
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1232
1480
|
import { join } from "path";
|
|
1233
1481
|
var NODE_SERIALIZE_DEMO = {
|
|
1234
1482
|
name: "node-serialize",
|
|
@@ -1298,7 +1546,7 @@ Add this deliberately vulnerable package first: npm install --save-exact ${scena
|
|
|
1298
1546
|
}
|
|
1299
1547
|
async function readPersistedSiteUuid(cwd) {
|
|
1300
1548
|
try {
|
|
1301
|
-
const value = JSON.parse(await
|
|
1549
|
+
const value = JSON.parse(await readFile7(join(cwd, ".patchstackrc.json"), "utf8")).siteUuid;
|
|
1302
1550
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1303
1551
|
} catch {
|
|
1304
1552
|
return null;
|
|
@@ -1383,7 +1631,7 @@ async function waitForDemoRule(manifestEndpoint, siteUuid, scenario, options = {
|
|
|
1383
1631
|
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
1384
1632
|
const requestTimeoutMs = options.requestTimeoutMs ?? 1e4;
|
|
1385
1633
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1386
|
-
const sleep = options.sleep ?? ((ms) => new Promise((
|
|
1634
|
+
const sleep = options.sleep ?? ((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)));
|
|
1387
1635
|
const now = options.now ?? Date.now;
|
|
1388
1636
|
const started = now();
|
|
1389
1637
|
const rulesUrl = buildRulesUrl(manifestEndpoint, siteUuid);
|
|
@@ -1432,12 +1680,14 @@ function renderDemoTestCommands(url, scenario) {
|
|
|
1432
1680
|
}
|
|
1433
1681
|
|
|
1434
1682
|
// src/config.ts
|
|
1435
|
-
import { readFile as
|
|
1683
|
+
import { readFile as readFile8, writeFile, chmod } from "fs/promises";
|
|
1436
1684
|
import path6 from "path";
|
|
1437
1685
|
var CONFIG_FILENAME = ".patchstackrc.json";
|
|
1686
|
+
var SECRET_FILENAME = ".patchstackrc.local.json";
|
|
1438
1687
|
var DEFAULT_ENVIRONMENT = "production";
|
|
1439
1688
|
async function resolveConfig(options) {
|
|
1440
1689
|
const fromFile = await readConfigFile(options.cwd);
|
|
1690
|
+
const fromSecretFile = await readSecretFile(options.cwd);
|
|
1441
1691
|
const fromEnv = readEnv();
|
|
1442
1692
|
const siteUuid = options.cliSiteUuid ?? fromEnv.siteUuid ?? fromFile.siteUuid ?? null;
|
|
1443
1693
|
const endpoint = options.cliEndpoint ?? fromEnv.endpoint ?? fromFile.endpoint ?? DEFAULT_ENDPOINT;
|
|
@@ -1462,8 +1712,8 @@ async function resolveConfig(options) {
|
|
|
1462
1712
|
"CONFIG_MISSING"
|
|
1463
1713
|
);
|
|
1464
1714
|
}
|
|
1465
|
-
const apiKeyRaw = fromEnv.apiKey ?? fromFile.apiKey ?? null;
|
|
1466
|
-
const pulseAuthRaw = fromEnv.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1715
|
+
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
1716
|
+
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
1467
1717
|
return {
|
|
1468
1718
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
1469
1719
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
@@ -1480,19 +1730,102 @@ async function writeConfigFile(cwd, config) {
|
|
|
1480
1730
|
await writeFile(target, content, "utf8");
|
|
1481
1731
|
return target;
|
|
1482
1732
|
}
|
|
1733
|
+
var SECRET_CONFIG_FILENAME = SECRET_FILENAME;
|
|
1734
|
+
async function writeSecretFile(cwd, secrets) {
|
|
1735
|
+
const target = path6.join(cwd, SECRET_FILENAME);
|
|
1736
|
+
await writeFile(target, JSON.stringify(secrets, null, 2) + "\n", { encoding: "utf8", mode: 384 });
|
|
1737
|
+
try {
|
|
1738
|
+
await chmod(target, 384);
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1741
|
+
const ignore = await ensureIgnored(cwd, SECRET_FILENAME);
|
|
1742
|
+
return { path: target, ...ignore };
|
|
1743
|
+
}
|
|
1744
|
+
async function ensureIgnored(cwd, entry) {
|
|
1745
|
+
const target = path6.join(cwd, ".gitignore");
|
|
1746
|
+
let existing = "";
|
|
1747
|
+
try {
|
|
1748
|
+
existing = await readFile8(target, "utf8");
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
const code = err.code;
|
|
1751
|
+
if (code !== "ENOENT") {
|
|
1752
|
+
return { ignored: false, reason: `.gitignore could not be read (${code ?? "unknown error"})` };
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (ignoresEntry(existing, entry)) return { ignored: true };
|
|
1756
|
+
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
1757
|
+
const block = `${separator}
|
|
1758
|
+
# Patchstack credential \u2014 never commit this
|
|
1759
|
+
${entry}
|
|
1760
|
+
`;
|
|
1761
|
+
try {
|
|
1762
|
+
await writeFile(target, existing + block, "utf8");
|
|
1763
|
+
} catch (err) {
|
|
1764
|
+
const code = err.code;
|
|
1765
|
+
return { ignored: false, reason: `.gitignore could not be written (${code ?? "unknown error"})` };
|
|
1766
|
+
}
|
|
1767
|
+
try {
|
|
1768
|
+
const written = await readFile8(target, "utf8");
|
|
1769
|
+
if (ignoresEntry(written, entry)) return { ignored: true };
|
|
1770
|
+
return { ignored: false, reason: ".gitignore does not cover the file after writing it" };
|
|
1771
|
+
} catch (err) {
|
|
1772
|
+
const code = err.code;
|
|
1773
|
+
return { ignored: false, reason: `.gitignore could not be read back (${code ?? "unknown error"})` };
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
async function secretFileIgnored(cwd) {
|
|
1777
|
+
try {
|
|
1778
|
+
const contents = await readFile8(path6.join(cwd, ".gitignore"), "utf8");
|
|
1779
|
+
if (ignoresEntry(contents, SECRET_FILENAME)) return { ignored: true };
|
|
1780
|
+
return { ignored: false, reason: `.gitignore does not cover ${SECRET_FILENAME}` };
|
|
1781
|
+
} catch (err) {
|
|
1782
|
+
const code = err.code;
|
|
1783
|
+
return {
|
|
1784
|
+
ignored: false,
|
|
1785
|
+
reason: code === "ENOENT" ? "this project has no .gitignore" : `.gitignore could not be read (${code ?? "unknown error"})`
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
function ignoresEntry(contents, entry) {
|
|
1790
|
+
let ignored = false;
|
|
1791
|
+
for (const raw of contents.split("\n")) {
|
|
1792
|
+
const line = raw.trim();
|
|
1793
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
1794
|
+
const negated = line.startsWith("!");
|
|
1795
|
+
const pattern = negated ? line.slice(1).trim() : line;
|
|
1796
|
+
if (pattern === entry || pattern === `/${entry}`) ignored = !negated;
|
|
1797
|
+
}
|
|
1798
|
+
return ignored;
|
|
1799
|
+
}
|
|
1483
1800
|
async function persistSiteUuid(cwd, siteUuid) {
|
|
1484
1801
|
const existing = await readConfigFile(cwd);
|
|
1485
1802
|
return writeConfigFile(cwd, { ...existing, siteUuid });
|
|
1486
1803
|
}
|
|
1487
1804
|
async function persistApiKey(cwd, apiKey) {
|
|
1488
|
-
const { pulseAuth:
|
|
1489
|
-
|
|
1805
|
+
const { apiKey: _movedKey, pulseAuth: _movedPulse, ...publicConfig } = await readConfigFile(cwd);
|
|
1806
|
+
const existingSecrets = await readSecretFile(cwd);
|
|
1807
|
+
const target = await writeSecretFile(cwd, { ...existingSecrets, apiKey, pulseAuth: void 0 });
|
|
1808
|
+
if (_movedKey !== void 0 || _movedPulse !== void 0) {
|
|
1809
|
+
await writeConfigFile(cwd, publicConfig);
|
|
1810
|
+
}
|
|
1811
|
+
return target;
|
|
1812
|
+
}
|
|
1813
|
+
async function credentialInCommittedConfig(cwd) {
|
|
1814
|
+
const config = await readConfigFile(cwd);
|
|
1815
|
+
return config.apiKey !== void 0 || config.pulseAuth !== void 0;
|
|
1816
|
+
}
|
|
1817
|
+
async function readSecretFile(cwd) {
|
|
1818
|
+
const file = await readJsonFile(cwd, SECRET_FILENAME);
|
|
1819
|
+
return { apiKey: file.apiKey, pulseAuth: file.pulseAuth };
|
|
1490
1820
|
}
|
|
1491
1821
|
async function readConfigFile(cwd) {
|
|
1492
|
-
|
|
1822
|
+
return readJsonFile(cwd, CONFIG_FILENAME);
|
|
1823
|
+
}
|
|
1824
|
+
async function readJsonFile(cwd, filename) {
|
|
1825
|
+
const target = path6.join(cwd, filename);
|
|
1493
1826
|
let raw;
|
|
1494
1827
|
try {
|
|
1495
|
-
raw = await
|
|
1828
|
+
raw = await readFile8(target, "utf8");
|
|
1496
1829
|
} catch (err) {
|
|
1497
1830
|
if (err.code === "ENOENT") {
|
|
1498
1831
|
return {};
|
|
@@ -1544,7 +1877,7 @@ function isEnvironment(value) {
|
|
|
1544
1877
|
}
|
|
1545
1878
|
|
|
1546
1879
|
// src/mark-build.ts
|
|
1547
|
-
import { existsSync, readdirSync, statSync } from "fs";
|
|
1880
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1548
1881
|
import path7 from "path";
|
|
1549
1882
|
|
|
1550
1883
|
// src/stack.ts
|
|
@@ -1687,16 +2020,94 @@ function injectMarker(html, snippet) {
|
|
|
1687
2020
|
}
|
|
1688
2021
|
return stripped + snippet;
|
|
1689
2022
|
}
|
|
2023
|
+
var JSX_SHELL_FRAMEWORKS = /* @__PURE__ */ new Set([
|
|
2024
|
+
"next",
|
|
2025
|
+
"remix",
|
|
2026
|
+
"react-router",
|
|
2027
|
+
"tanstack-start",
|
|
2028
|
+
"gatsby"
|
|
2029
|
+
]);
|
|
2030
|
+
var PRODUCTION_GATES = {
|
|
2031
|
+
next: "process.env.NODE_ENV === 'production'",
|
|
2032
|
+
gatsby: "process.env.NODE_ENV === 'production'",
|
|
2033
|
+
nuxt: "!import.meta.dev"
|
|
2034
|
+
};
|
|
2035
|
+
var DEFAULT_PRODUCTION_GATE = "import.meta.env.PROD";
|
|
2036
|
+
function productionGate(framework) {
|
|
2037
|
+
const mapped = framework !== null ? PRODUCTION_GATES[framework] : void 0;
|
|
2038
|
+
return mapped ?? DEFAULT_PRODUCTION_GATE;
|
|
2039
|
+
}
|
|
2040
|
+
function hasJsxShell(framework) {
|
|
2041
|
+
return framework !== null && JSX_SHELL_FRAMEWORKS.has(framework);
|
|
2042
|
+
}
|
|
2043
|
+
function buildSourceMarkerSnippet(framework) {
|
|
2044
|
+
const gate = productionGate(framework);
|
|
2045
|
+
return `{${gate} && (
|
|
2046
|
+
<script
|
|
2047
|
+
${MARKER_ATTR}="true"
|
|
2048
|
+
dangerouslySetInnerHTML={{ __html: 'window.__PATCHSTACK_PROD__=true;' }}
|
|
2049
|
+
/>
|
|
2050
|
+
)}`;
|
|
2051
|
+
}
|
|
2052
|
+
var PROD_MARKER_GLOBAL = "__PATCHSTACK_PROD__";
|
|
2053
|
+
var REGION_OPEN = "{/* #region patchstack (managed by patchstack-connect \u2014 do not edit) */}";
|
|
2054
|
+
var REGION_CLOSE = "{/* #endregion patchstack */}";
|
|
2055
|
+
var REGION_RE = /[ \t]*\{\/\* #region patchstack[\s\S]*?#endregion patchstack \*\/\}\n?/g;
|
|
2056
|
+
function ensureMarkerInJsxShell(source, framework) {
|
|
2057
|
+
if (!hasJsxShell(framework)) {
|
|
2058
|
+
return { source, action: "unsupported" };
|
|
2059
|
+
}
|
|
2060
|
+
const stripped = source.replace(REGION_RE, "");
|
|
2061
|
+
if (stripped.includes(PROD_MARKER_GLOBAL)) {
|
|
2062
|
+
return { source, action: "manual" };
|
|
2063
|
+
}
|
|
2064
|
+
const block = (indent) => [REGION_OPEN, ...buildSourceMarkerSnippet(framework).split("\n"), REGION_CLOSE].map((line) => `${indent}${line}`).join("\n");
|
|
2065
|
+
const widget = stripped.match(/^([ \t]*).*patchstack-widget.*$/m);
|
|
2066
|
+
if (widget?.index !== void 0) {
|
|
2067
|
+
const indent = widget[1] ?? "";
|
|
2068
|
+
return {
|
|
2069
|
+
source: `${stripped.slice(0, widget.index)}${block(indent)}
|
|
2070
|
+
${stripped.slice(widget.index)}`,
|
|
2071
|
+
action: "added"
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
for (const anchor of [/^([ \t]*)<head>[ \t]*$/m, /^([ \t]*)<body>[ \t]*$/m]) {
|
|
2075
|
+
const match = stripped.match(anchor);
|
|
2076
|
+
if (match?.index === void 0) {
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
const end = match.index + match[0].length;
|
|
2080
|
+
const indent = `${match[1] ?? ""} `;
|
|
2081
|
+
return {
|
|
2082
|
+
source: `${stripped.slice(0, end)}
|
|
2083
|
+
${block(indent)}${stripped.slice(end)}`,
|
|
2084
|
+
action: "added"
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
return { source, action: "no-anchor" };
|
|
2088
|
+
}
|
|
2089
|
+
function ensureSourceMarker(cwd, shell, framework) {
|
|
2090
|
+
if (shell === null) {
|
|
2091
|
+
return { shell: null, source: "", action: "no-anchor" };
|
|
2092
|
+
}
|
|
2093
|
+
const file = path7.resolve(cwd, shell);
|
|
2094
|
+
const before = readFileSync(file, "utf8");
|
|
2095
|
+
const result = ensureMarkerInJsxShell(before, framework);
|
|
2096
|
+
if (result.source !== before && result.action === "added") {
|
|
2097
|
+
writeFileSync(file, result.source);
|
|
2098
|
+
}
|
|
2099
|
+
return { ...result, shell };
|
|
2100
|
+
}
|
|
1690
2101
|
|
|
1691
2102
|
// src/guide.ts
|
|
1692
|
-
import { existsSync as
|
|
2103
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
1693
2104
|
import path9 from "path";
|
|
1694
2105
|
|
|
1695
2106
|
// src/protect/install/util.ts
|
|
1696
|
-
import { readFileSync, existsSync as existsSync2, writeFileSync } from "fs";
|
|
2107
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1697
2108
|
import { join as join2, dirname } from "path";
|
|
1698
2109
|
import { fileURLToPath } from "url";
|
|
1699
|
-
var read = (p) =>
|
|
2110
|
+
var read = (p) => readFileSync2(p, "utf8");
|
|
1700
2111
|
var log = (msg) => console.log(`patchstack protect: ${msg}`);
|
|
1701
2112
|
function hasDependency(cwd, name) {
|
|
1702
2113
|
try {
|
|
@@ -1706,6 +2117,20 @@ function hasDependency(cwd, name) {
|
|
|
1706
2117
|
return false;
|
|
1707
2118
|
}
|
|
1708
2119
|
}
|
|
2120
|
+
function hasResolvableCredential(cwd) {
|
|
2121
|
+
if ((process.env.PATCHSTACK_API_KEY ?? "") !== "" || (process.env.PATCHSTACK_PULSE_AUTH ?? "") !== "") {
|
|
2122
|
+
return true;
|
|
2123
|
+
}
|
|
2124
|
+
for (const file of [".patchstackrc.local.json", ".patchstackrc.json"]) {
|
|
2125
|
+
try {
|
|
2126
|
+
const parsed = JSON.parse(read(join2(cwd, file)));
|
|
2127
|
+
if (typeof parsed.apiKey === "string" && parsed.apiKey !== "") return true;
|
|
2128
|
+
if (typeof parsed.pulseAuth === "string" && parsed.pulseAuth !== "") return true;
|
|
2129
|
+
} catch {
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
return false;
|
|
2133
|
+
}
|
|
1709
2134
|
var SITE_UUID_PLACEHOLDER = "__PATCHSTACK_SITE_UUID__";
|
|
1710
2135
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
1711
2136
|
function bakeSiteUuid(cwd, guardRelPath) {
|
|
@@ -1732,7 +2157,7 @@ function bakeSiteUuid(cwd, guardRelPath) {
|
|
|
1732
2157
|
log(`${guardRelPath} site UUID already baked`);
|
|
1733
2158
|
return false;
|
|
1734
2159
|
}
|
|
1735
|
-
|
|
2160
|
+
writeFileSync2(guardPath, source.replace(SITE_UUID_PLACEHOLDER, uuid));
|
|
1736
2161
|
log(`baked site UUID into ${guardRelPath} \u2014 live rules from the Patchstack API`);
|
|
1737
2162
|
return true;
|
|
1738
2163
|
}
|
|
@@ -1749,7 +2174,7 @@ function templatesDir() {
|
|
|
1749
2174
|
}
|
|
1750
2175
|
|
|
1751
2176
|
// src/protect/install/adapters/tanstack-supabase.ts
|
|
1752
|
-
import { writeFileSync as
|
|
2177
|
+
import { writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync, copyFileSync } from "fs";
|
|
1753
2178
|
import { join as join3 } from "path";
|
|
1754
2179
|
var CLIENT_TUNNEL = [
|
|
1755
2180
|
"",
|
|
@@ -1780,7 +2205,8 @@ var REQUEST_MIDDLEWARE_BLOCK = [
|
|
|
1780
2205
|
" if (blocked) return blocked;",
|
|
1781
2206
|
" }",
|
|
1782
2207
|
" }",
|
|
1783
|
-
"
|
|
2208
|
+
" // The request is passed so route/method-scoped response rules can apply their scope.",
|
|
2209
|
+
" return screenResponse(await next(), request);",
|
|
1784
2210
|
"});",
|
|
1785
2211
|
"// #endregion patchstack-guard"
|
|
1786
2212
|
].join("\n");
|
|
@@ -1863,7 +2289,7 @@ function patchClient(cwd) {
|
|
|
1863
2289
|
log("client.ts anchor not found \u2014 skipping (template changed?)");
|
|
1864
2290
|
return false;
|
|
1865
2291
|
}
|
|
1866
|
-
|
|
2292
|
+
writeFileSync3(p, s.replace(anchor, anchor + "\n" + CLIENT_TUNNEL));
|
|
1867
2293
|
log("patched client.ts (tunnel Supabase through the guard)");
|
|
1868
2294
|
return true;
|
|
1869
2295
|
}
|
|
@@ -1900,7 +2326,7 @@ function patchStart(cwd) {
|
|
|
1900
2326
|
log("start.ts already wired");
|
|
1901
2327
|
return false;
|
|
1902
2328
|
}
|
|
1903
|
-
|
|
2329
|
+
writeFileSync3(p, s);
|
|
1904
2330
|
log("patched start.ts (guard registered as request + function middleware)");
|
|
1905
2331
|
return true;
|
|
1906
2332
|
}
|
|
@@ -2151,7 +2577,7 @@ var nuxtAdapter = {
|
|
|
2151
2577
|
};
|
|
2152
2578
|
|
|
2153
2579
|
// src/protect/install/find-app.ts
|
|
2154
|
-
import { readFileSync as
|
|
2580
|
+
import { readFileSync as readFileSync3, existsSync as existsSync6, readdirSync as readdirSync2, lstatSync } from "fs";
|
|
2155
2581
|
import { join as join8, relative, dirname as dirname3 } from "path";
|
|
2156
2582
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2157
2583
|
"node_modules",
|
|
@@ -2174,9 +2600,9 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
2174
2600
|
var MAX_DEPTH = 8;
|
|
2175
2601
|
function findAppInstance(cwd, re) {
|
|
2176
2602
|
const root = existsSync6(join8(cwd, "src")) ? join8(cwd, "src") : cwd;
|
|
2177
|
-
|
|
2603
|
+
const found = [];
|
|
2178
2604
|
const walk2 = (dir, depth) => {
|
|
2179
|
-
if (
|
|
2605
|
+
if (depth > MAX_DEPTH) return;
|
|
2180
2606
|
let entries;
|
|
2181
2607
|
try {
|
|
2182
2608
|
entries = readdirSync2(dir).sort();
|
|
@@ -2184,7 +2610,6 @@ function findAppInstance(cwd, re) {
|
|
|
2184
2610
|
return;
|
|
2185
2611
|
}
|
|
2186
2612
|
for (const name of entries) {
|
|
2187
|
-
if (hit) return;
|
|
2188
2613
|
if (SKIP_DIRS.has(name) || name.startsWith(".")) continue;
|
|
2189
2614
|
const p = join8(dir, name);
|
|
2190
2615
|
let st;
|
|
@@ -2198,17 +2623,70 @@ function findAppInstance(cwd, re) {
|
|
|
2198
2623
|
else if (/\.(?:ts|js|mjs|cjs)$/.test(name)) {
|
|
2199
2624
|
let src;
|
|
2200
2625
|
try {
|
|
2201
|
-
src =
|
|
2626
|
+
src = readFileSync3(p, "utf8");
|
|
2202
2627
|
} catch {
|
|
2203
2628
|
continue;
|
|
2204
2629
|
}
|
|
2205
2630
|
const m = re.exec(src);
|
|
2206
|
-
if (m)
|
|
2631
|
+
if (!m) continue;
|
|
2632
|
+
const appVar = m[1];
|
|
2633
|
+
found.push({
|
|
2634
|
+
relPath: relative(cwd, p).replace(/\\/g, "/"),
|
|
2635
|
+
appVar,
|
|
2636
|
+
listens: listensOn(src, appVar)
|
|
2637
|
+
});
|
|
2207
2638
|
}
|
|
2208
2639
|
}
|
|
2209
2640
|
};
|
|
2210
2641
|
walk2(root, 0);
|
|
2211
|
-
return
|
|
2642
|
+
if (found.length === 0) return null;
|
|
2643
|
+
const declared = declaredEntries(cwd);
|
|
2644
|
+
const chosen = found.slice().sort((a, b) => rankEntry({ ...a, declared: declared.has(a.relPath) }, { ...b, declared: declared.has(b.relPath) }))[0];
|
|
2645
|
+
return {
|
|
2646
|
+
relPath: chosen.relPath,
|
|
2647
|
+
appVar: chosen.appVar,
|
|
2648
|
+
others: found.filter((f) => f !== chosen && f.listens).map((f) => ({ relPath: f.relPath, appVar: f.appVar }))
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
function listensOn(source, appVar) {
|
|
2652
|
+
return new RegExp(`\\b${appVar}\\s*\\.\\s*listen\\s*\\(`).test(source);
|
|
2653
|
+
}
|
|
2654
|
+
var ENTRY_NAMES = ["server", "index", "main", "app"];
|
|
2655
|
+
function rankEntry(a, b) {
|
|
2656
|
+
if (a.declared !== b.declared) return a.declared ? -1 : 1;
|
|
2657
|
+
if (a.listens !== b.listens) return a.listens ? -1 : 1;
|
|
2658
|
+
const named = entryRank(a.relPath) - entryRank(b.relPath);
|
|
2659
|
+
if (named !== 0) return named;
|
|
2660
|
+
const depth = a.relPath.split("/").length - b.relPath.split("/").length;
|
|
2661
|
+
if (depth !== 0) return depth;
|
|
2662
|
+
return a.relPath < b.relPath ? -1 : 1;
|
|
2663
|
+
}
|
|
2664
|
+
function entryRank(relPath) {
|
|
2665
|
+
const base = (relPath.split("/").pop() ?? "").replace(/\.(?:ts|js|mjs|cjs)$/, "");
|
|
2666
|
+
const index = ENTRY_NAMES.indexOf(base);
|
|
2667
|
+
return index === -1 ? ENTRY_NAMES.length : index;
|
|
2668
|
+
}
|
|
2669
|
+
function declaredEntries(cwd) {
|
|
2670
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2671
|
+
let pkg;
|
|
2672
|
+
try {
|
|
2673
|
+
pkg = JSON.parse(readFileSync3(join8(cwd, "package.json"), "utf8"));
|
|
2674
|
+
} catch {
|
|
2675
|
+
return paths;
|
|
2676
|
+
}
|
|
2677
|
+
const add = (value) => {
|
|
2678
|
+
if (typeof value !== "string") return;
|
|
2679
|
+
for (const match of value.matchAll(/[\w./@-]+\.(?:ts|js|mjs|cjs)/g)) {
|
|
2680
|
+
paths.add(match[0].replace(/^\.\//, ""));
|
|
2681
|
+
}
|
|
2682
|
+
};
|
|
2683
|
+
add(pkg.main);
|
|
2684
|
+
add(pkg.module);
|
|
2685
|
+
const scripts = pkg.scripts;
|
|
2686
|
+
if (typeof scripts === "object" && scripts !== null) {
|
|
2687
|
+
for (const name of ["start", "serve", "dev"]) add(scripts[name]);
|
|
2688
|
+
}
|
|
2689
|
+
return paths;
|
|
2212
2690
|
}
|
|
2213
2691
|
function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
2214
2692
|
let spec = relative(dirname3(fromRel), toRel).replace(/\\/g, "/");
|
|
@@ -2218,28 +2696,252 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
|
2218
2696
|
}
|
|
2219
2697
|
|
|
2220
2698
|
// src/protect/install/register.ts
|
|
2221
|
-
import { writeFileSync as
|
|
2222
|
-
import { join as
|
|
2699
|
+
import { writeFileSync as writeFileSync5, existsSync as existsSync9 } from "fs";
|
|
2700
|
+
import { join as join11, resolve as resolve2 } from "path";
|
|
2223
2701
|
|
|
2224
2702
|
// src/protect/install/generic.ts
|
|
2225
|
-
import { readFileSync as
|
|
2226
|
-
import { join as
|
|
2227
|
-
|
|
2703
|
+
import { readFileSync as readFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync4, copyFileSync as copyFileSync4, readdirSync as readdirSync3, lstatSync as lstatSync2 } from "fs";
|
|
2704
|
+
import { join as join10, dirname as dirname5 } from "path";
|
|
2705
|
+
|
|
2706
|
+
// src/protect/install/source-scope.ts
|
|
2707
|
+
import { execFileSync } from "child_process";
|
|
2708
|
+
import { existsSync as existsSync7 } from "fs";
|
|
2709
|
+
import { dirname as dirname4, join as join9, resolve } from "path";
|
|
2710
|
+
var IMPORT_LINE = /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+[^=]+=\s*require\()/;
|
|
2711
|
+
function lastTopLevelImportLine(source) {
|
|
2712
|
+
const lines = source.split("\n");
|
|
2713
|
+
let depth = 0;
|
|
2714
|
+
let last = -1;
|
|
2715
|
+
let inBlockComment = false;
|
|
2716
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2717
|
+
const line = lines[i] ?? "";
|
|
2718
|
+
if (!inBlockComment && depth === 0 && IMPORT_LINE.test(line)) last = i;
|
|
2719
|
+
inBlockComment = advanceDepth(line, inBlockComment, (delta) => {
|
|
2720
|
+
depth += delta;
|
|
2721
|
+
});
|
|
2722
|
+
if (depth < 0) return -1;
|
|
2723
|
+
}
|
|
2724
|
+
return depth === 0 ? last : -1;
|
|
2725
|
+
}
|
|
2726
|
+
function isTopLevelLine(source, index) {
|
|
2727
|
+
const lines = source.split("\n");
|
|
2728
|
+
let depth = 0;
|
|
2729
|
+
let inBlockComment = false;
|
|
2730
|
+
for (let i = 0; i < lines.length && i < index; i++) {
|
|
2731
|
+
inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
|
|
2732
|
+
depth += delta;
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
return depth === 0;
|
|
2736
|
+
}
|
|
2737
|
+
function inSameBlockAfter(source, appIndex, callIndex) {
|
|
2738
|
+
if (appIndex < 0 || callIndex <= appIndex) return false;
|
|
2739
|
+
const lines = source.split("\n");
|
|
2740
|
+
let depth = 0;
|
|
2741
|
+
let inBlockComment = false;
|
|
2742
|
+
for (let i = appIndex; i < callIndex; i++) {
|
|
2743
|
+
inBlockComment = advanceDepth(lines[i] ?? "", inBlockComment, (delta) => {
|
|
2744
|
+
depth += delta;
|
|
2745
|
+
});
|
|
2746
|
+
if (depth < 0) return false;
|
|
2747
|
+
}
|
|
2748
|
+
return depth === 0;
|
|
2749
|
+
}
|
|
2750
|
+
function advanceDepth(line, inBlockComment, add) {
|
|
2751
|
+
let quote = null;
|
|
2752
|
+
for (let i = 0; i < line.length; i++) {
|
|
2753
|
+
const ch = line[i];
|
|
2754
|
+
const next = line[i + 1];
|
|
2755
|
+
if (inBlockComment) {
|
|
2756
|
+
if (ch === "*" && next === "/") {
|
|
2757
|
+
inBlockComment = false;
|
|
2758
|
+
i++;
|
|
2759
|
+
}
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
if (quote !== null) {
|
|
2763
|
+
if (ch === "\\") {
|
|
2764
|
+
i++;
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
if (ch === quote) quote = null;
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
if (ch === "/" && next === "/") return inBlockComment;
|
|
2771
|
+
if (ch === "/" && next === "*") {
|
|
2772
|
+
inBlockComment = true;
|
|
2773
|
+
i++;
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2777
|
+
quote = ch;
|
|
2778
|
+
continue;
|
|
2779
|
+
}
|
|
2780
|
+
if (ch === "{" || ch === "(" || ch === "[") add(1);
|
|
2781
|
+
else if (ch === "}" || ch === ")" || ch === "]") add(-1);
|
|
2782
|
+
}
|
|
2783
|
+
return inBlockComment;
|
|
2784
|
+
}
|
|
2785
|
+
function parses(filePath) {
|
|
2786
|
+
if (!/\.(?:js|cjs|mjs)$/.test(filePath)) return null;
|
|
2787
|
+
try {
|
|
2788
|
+
execFileSync(process.execPath, ["--check", filePath], { stdio: "pipe" });
|
|
2789
|
+
return true;
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
return isSyntaxError(error) ? false : null;
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
function isSyntaxError(error) {
|
|
2795
|
+
const status = error?.status;
|
|
2796
|
+
return typeof status === "number" && status !== 0;
|
|
2797
|
+
}
|
|
2798
|
+
function stripComments(source) {
|
|
2799
|
+
let out = "";
|
|
2800
|
+
let quote = null;
|
|
2801
|
+
let inLine = false;
|
|
2802
|
+
let inBlock = false;
|
|
2803
|
+
for (let i = 0; i < source.length; i++) {
|
|
2804
|
+
const ch = source[i];
|
|
2805
|
+
const next = source[i + 1];
|
|
2806
|
+
if (inLine) {
|
|
2807
|
+
if (ch === "\n") {
|
|
2808
|
+
inLine = false;
|
|
2809
|
+
out += ch;
|
|
2810
|
+
}
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
if (inBlock) {
|
|
2814
|
+
if (ch === "*" && next === "/") {
|
|
2815
|
+
inBlock = false;
|
|
2816
|
+
i++;
|
|
2817
|
+
} else if (ch === "\n") {
|
|
2818
|
+
out += ch;
|
|
2819
|
+
}
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
if (quote !== null) {
|
|
2823
|
+
out += ch;
|
|
2824
|
+
if (ch === "\\") {
|
|
2825
|
+
if (next !== void 0) {
|
|
2826
|
+
out += next;
|
|
2827
|
+
i++;
|
|
2828
|
+
}
|
|
2829
|
+
} else if (ch === quote) {
|
|
2830
|
+
quote = null;
|
|
2831
|
+
}
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2835
|
+
quote = ch;
|
|
2836
|
+
out += ch;
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
if (ch === "/" && next === "/") {
|
|
2840
|
+
inLine = true;
|
|
2841
|
+
i++;
|
|
2842
|
+
continue;
|
|
2843
|
+
}
|
|
2844
|
+
if (ch === "/" && next === "*") {
|
|
2845
|
+
inBlock = true;
|
|
2846
|
+
i++;
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
out += ch;
|
|
2850
|
+
}
|
|
2851
|
+
return out;
|
|
2852
|
+
}
|
|
2853
|
+
var MASKED_CHAR = "";
|
|
2854
|
+
function maskStringContents(source) {
|
|
2855
|
+
let out = "";
|
|
2856
|
+
let quote = null;
|
|
2857
|
+
for (let i = 0; i < source.length; i++) {
|
|
2858
|
+
const ch = source[i];
|
|
2859
|
+
if (quote === null) {
|
|
2860
|
+
out += ch;
|
|
2861
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
if (ch === "\\") {
|
|
2865
|
+
out += MASKED_CHAR;
|
|
2866
|
+
if (i + 1 < source.length) {
|
|
2867
|
+
out += source[i + 1] === "\n" ? "\n" : MASKED_CHAR;
|
|
2868
|
+
i++;
|
|
2869
|
+
}
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
if (ch === quote) {
|
|
2873
|
+
out += ch;
|
|
2874
|
+
quote = null;
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
out += ch === "\n" ? "\n" : MASKED_CHAR;
|
|
2878
|
+
}
|
|
2879
|
+
return out;
|
|
2880
|
+
}
|
|
2881
|
+
var GUARD_FILENAMES = ["guard.ts", "guard.js", "guard.mjs", "guard.cjs"];
|
|
2882
|
+
function moduleKindOf(filePath, packageIsModule) {
|
|
2883
|
+
if (/\.(?:ts|tsx|mts|cts|jsx)$/.test(filePath)) return "ts";
|
|
2884
|
+
if (/\.mjs$/.test(filePath)) return "esm";
|
|
2885
|
+
if (/\.cjs$/.test(filePath)) return "cjs";
|
|
2886
|
+
return packageIsModule ? "esm" : "cjs";
|
|
2887
|
+
}
|
|
2888
|
+
function resolvesToGuardModule(specifier, query) {
|
|
2889
|
+
const resolved = resolve(dirname4(query.fromFile), specifier);
|
|
2890
|
+
if (withoutModuleExtension(resolved) !== join9(query.guardDir, "guard")) return false;
|
|
2891
|
+
const existing = GUARD_FILENAMES.filter((name) => existsSync7(join9(query.guardDir, name)));
|
|
2892
|
+
if (existing.length === 0) return false;
|
|
2893
|
+
const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
|
|
2894
|
+
if (!query.strictExtensions) return true;
|
|
2895
|
+
if (extension === "") {
|
|
2896
|
+
if (query.kind === "esm") return false;
|
|
2897
|
+
if (query.kind === "cjs") return existing.includes("guard.js");
|
|
2898
|
+
return true;
|
|
2899
|
+
}
|
|
2900
|
+
if (existsSync7(resolved)) return true;
|
|
2901
|
+
return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
|
|
2902
|
+
}
|
|
2903
|
+
function withoutModuleExtension(filePath) {
|
|
2904
|
+
return filePath.replace(/\.(?:ts|tsx|js|jsx|mjs|cjs)$/, "");
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
// src/protect/install/generic.ts
|
|
2908
|
+
function packageIsEsm(cwd) {
|
|
2909
|
+
try {
|
|
2910
|
+
return JSON.parse(read(join10(cwd, "package.json"))).type === "module";
|
|
2911
|
+
} catch {
|
|
2912
|
+
return false;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2228
2915
|
function genericDir(cwd) {
|
|
2229
|
-
return
|
|
2916
|
+
return existsSync8(join10(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
2230
2917
|
}
|
|
2231
|
-
function
|
|
2918
|
+
function hasTypeScript(cwd) {
|
|
2919
|
+
return existsSync8(join10(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
|
|
2920
|
+
}
|
|
2921
|
+
function genericGuardTarget(cwd) {
|
|
2922
|
+
if (hasTypeScript(cwd)) {
|
|
2923
|
+
return { template: "generic-guard.ts", file: "guard.ts", specifier: "guard" };
|
|
2924
|
+
}
|
|
2925
|
+
if (packageIsEsm(cwd)) {
|
|
2926
|
+
return { template: "generic-guard.js", file: "guard.js", specifier: "guard.js" };
|
|
2927
|
+
}
|
|
2928
|
+
return { template: "generic-guard.cjs", file: "guard.cjs", specifier: "guard.cjs" };
|
|
2929
|
+
}
|
|
2930
|
+
function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
2931
|
+
const target = genericGuardTarget(cwd);
|
|
2932
|
+
guardTemplate ??= target.template;
|
|
2933
|
+
guardFile ??= target.file;
|
|
2232
2934
|
const templates = templatesDir();
|
|
2233
2935
|
const dir = genericDir(cwd);
|
|
2234
|
-
const dst =
|
|
2936
|
+
const dst = join10(cwd, dir);
|
|
2235
2937
|
mkdirSync4(dst, { recursive: true });
|
|
2236
|
-
copyFileSync4(
|
|
2938
|
+
copyFileSync4(join10(templates, guardTemplate), join10(dst, guardFile));
|
|
2237
2939
|
const guardRel = `${dir}/${guardFile}`;
|
|
2238
2940
|
const changed = [guardRel];
|
|
2239
2941
|
if (!opts.demo) bakeSiteUuid(cwd, guardRel);
|
|
2240
|
-
const rulesDst =
|
|
2241
|
-
if (opts.demo || !
|
|
2242
|
-
copyFileSync4(
|
|
2942
|
+
const rulesDst = join10(dst, "rules.json");
|
|
2943
|
+
if (opts.demo || !existsSync8(rulesDst)) {
|
|
2944
|
+
copyFileSync4(join10(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
2243
2945
|
changed.push(`${dir}/rules.json`);
|
|
2244
2946
|
}
|
|
2245
2947
|
return { changed, dir };
|
|
@@ -2260,17 +2962,17 @@ function candidateEntries(cwd) {
|
|
|
2260
2962
|
"app.ts",
|
|
2261
2963
|
"app.js"
|
|
2262
2964
|
];
|
|
2263
|
-
const hits = names.filter((n) =>
|
|
2965
|
+
const hits = names.filter((n) => existsSync8(join10(cwd, n)));
|
|
2264
2966
|
try {
|
|
2265
|
-
const pkg = JSON.parse(read(
|
|
2266
|
-
if (typeof pkg.main === "string" &&
|
|
2967
|
+
const pkg = JSON.parse(read(join10(cwd, "package.json")));
|
|
2968
|
+
if (typeof pkg.main === "string" && existsSync8(join10(cwd, pkg.main))) hits.push(pkg.main);
|
|
2267
2969
|
} catch {
|
|
2268
2970
|
}
|
|
2269
2971
|
return [...new Set(hits)];
|
|
2270
2972
|
}
|
|
2271
2973
|
function usesExpress(cwd) {
|
|
2272
2974
|
try {
|
|
2273
|
-
const pkg = JSON.parse(read(
|
|
2975
|
+
const pkg = JSON.parse(read(join10(cwd, "package.json")));
|
|
2274
2976
|
return Boolean({ ...pkg.dependencies, ...pkg.devDependencies }.express);
|
|
2275
2977
|
} catch {
|
|
2276
2978
|
return false;
|
|
@@ -2279,34 +2981,140 @@ function usesExpress(cwd) {
|
|
|
2279
2981
|
function wiringPlan(cwd, dir) {
|
|
2280
2982
|
const entries = candidateEntries(cwd);
|
|
2281
2983
|
const express = usesExpress(cwd);
|
|
2984
|
+
const target = genericGuardTarget(cwd);
|
|
2282
2985
|
const lines = [
|
|
2283
|
-
`no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}
|
|
2986
|
+
`no built-in adapter matched this stack \u2014 scaffolded a generic guard at ${dir}/${target.file} + ${dir}/rules.json.`,
|
|
2284
2987
|
"Finish by wiring it into your server (pick the one that fits):",
|
|
2285
|
-
|
|
2286
|
-
|
|
2988
|
+
// The specifier is the one this project's loader resolves — a bare path is right for a compiler and
|
|
2989
|
+
// wrong for `require()`, which never guesses `.cjs`.
|
|
2990
|
+
` \u2022 Web-Fetch entry: export default { fetch: protectFetch(yourHandler) } // import from "${dir}/${target.specifier}"`,
|
|
2991
|
+
` \u2022 Node / Connect: app.use(patchstackMiddleware) // before any body parser`,
|
|
2287
2992
|
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.",
|
|
2288
2993
|
"Then confirm it is hooked up: npx patchstack-connect protect --check"
|
|
2289
2994
|
];
|
|
2290
2995
|
return lines.join("\n");
|
|
2291
2996
|
}
|
|
2997
|
+
var GUARD_EXPORTS = ["protectFetch", "patchstackMiddleware"];
|
|
2998
|
+
function importsAndUsesGuard(source, sourcePath, guardDir, packageIsModule) {
|
|
2999
|
+
const code = stripComments(source);
|
|
3000
|
+
const masked = maskStringContents(code);
|
|
3001
|
+
const declarations = guardImportDeclarations(code, masked, {
|
|
3002
|
+
fromFile: sourcePath,
|
|
3003
|
+
guardDir,
|
|
3004
|
+
kind: moduleKindOf(sourcePath, packageIsModule),
|
|
3005
|
+
// The scaffold now matches the project's module format and the printed plan names the specifier that
|
|
3006
|
+
// format resolves, so there is nothing left to excuse: a specifier this file's loader would not find is
|
|
3007
|
+
// not a wired guard here either.
|
|
3008
|
+
strictExtensions: true
|
|
3009
|
+
});
|
|
3010
|
+
if (declarations.length === 0) return false;
|
|
3011
|
+
const direct = /* @__PURE__ */ new Set();
|
|
3012
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
3013
|
+
for (const declaration of declarations) {
|
|
3014
|
+
const bound = boundNames(declaration.text);
|
|
3015
|
+
for (const name of bound.direct) direct.add(name);
|
|
3016
|
+
for (const name of bound.namespaces) namespaces.add(name);
|
|
3017
|
+
}
|
|
3018
|
+
if (direct.size === 0 && namespaces.size === 0) return false;
|
|
3019
|
+
let body = masked;
|
|
3020
|
+
for (const declaration of declarations) {
|
|
3021
|
+
body = body.slice(0, declaration.start) + " ".repeat(declaration.text.length) + body.slice(declaration.start + declaration.text.length);
|
|
3022
|
+
}
|
|
3023
|
+
const members = GUARD_EXPORTS.map(escapeForRegExp).join("|");
|
|
3024
|
+
return [...direct].some((name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*[(),]`).test(body)) || [...namespaces].some(
|
|
3025
|
+
(name) => new RegExp(`\\b${escapeForRegExp(name)}\\s*\\.\\s*(?:${members})\\s*[(),]`).test(body)
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
function guardImportDeclarations(code, masked, query) {
|
|
3029
|
+
const pattern = /import\s+[^;]*?from\s*(['"`])[^'"`]*\1|(?:const|let|var)\s+[^;=]+=\s*require\(\s*(['"`])[^'"`]*\2\s*\)/g;
|
|
3030
|
+
const found = [];
|
|
3031
|
+
for (const match of masked.matchAll(pattern)) {
|
|
3032
|
+
const start = match.index ?? -1;
|
|
3033
|
+
if (start < 0) continue;
|
|
3034
|
+
const text = code.slice(start, start + match[0].length);
|
|
3035
|
+
const specifier = specifierOf(text);
|
|
3036
|
+
if (specifier !== null && resolvesToGuardModule(specifier, query)) found.push({ text, start });
|
|
3037
|
+
}
|
|
3038
|
+
return found;
|
|
3039
|
+
}
|
|
3040
|
+
function specifierOf(declaration) {
|
|
3041
|
+
return /(['"`])([^'"`]*)\1/.exec(declaration)?.[2] ?? null;
|
|
3042
|
+
}
|
|
3043
|
+
function boundNames(declaration) {
|
|
3044
|
+
const direct = [];
|
|
3045
|
+
const namespaces = [];
|
|
3046
|
+
const clause = /^import\s+([^]*?)\s+from\b/.exec(declaration)?.[1] ?? /^(?:const|let|var)\s+([^]*?)=\s*require\b/.exec(declaration)?.[1] ?? "";
|
|
3047
|
+
const braced = /\{([^}]*)\}/.exec(clause)?.[1];
|
|
3048
|
+
if (braced !== void 0) {
|
|
3049
|
+
for (const part of braced.split(",")) {
|
|
3050
|
+
const [exported, local] = splitBinding(part);
|
|
3051
|
+
if (!isGuardExport(exported)) continue;
|
|
3052
|
+
if (/^[A-Za-z_$][\w$]*$/.test(local)) direct.push(local);
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
const bare = clause.replace(/\{[^}]*\}/g, "").replace(/^\*\s*as\s*/, "").replace(/,/g, " ").trim();
|
|
3056
|
+
for (const token of bare.split(/\s+/)) {
|
|
3057
|
+
if (/^[A-Za-z_$][\w$]*$/.test(token) && token !== "as") namespaces.push(token);
|
|
3058
|
+
}
|
|
3059
|
+
return { direct, namespaces };
|
|
3060
|
+
}
|
|
3061
|
+
function splitBinding(part) {
|
|
3062
|
+
const separator = part.includes(" as ") ? " as " : part.includes(":") ? ":" : null;
|
|
3063
|
+
if (separator === null) {
|
|
3064
|
+
const name = part.trim();
|
|
3065
|
+
return [name, name];
|
|
3066
|
+
}
|
|
3067
|
+
const [exported, ...rest] = part.split(separator);
|
|
3068
|
+
return [(exported ?? "").trim(), (rest.join(separator) ?? "").trim()];
|
|
3069
|
+
}
|
|
3070
|
+
function isGuardExport(name) {
|
|
3071
|
+
return GUARD_EXPORTS.includes(name);
|
|
3072
|
+
}
|
|
3073
|
+
function escapeForRegExp(value) {
|
|
3074
|
+
return value.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
3075
|
+
}
|
|
2292
3076
|
function genericVerify(cwd) {
|
|
2293
3077
|
const dir = genericDir(cwd);
|
|
2294
|
-
const
|
|
2295
|
-
const
|
|
3078
|
+
const target = genericGuardTarget(cwd);
|
|
3079
|
+
const present = GUARD_FILENAMES.find((name) => existsSync8(join10(cwd, dir, name)));
|
|
3080
|
+
const scaffolded = present !== void 0;
|
|
3081
|
+
const imported = scaffolded && guardIsImported(cwd, join10(cwd, dir, present));
|
|
2296
3082
|
return {
|
|
2297
3083
|
wired: scaffolded && imported,
|
|
2298
3084
|
checks: [
|
|
2299
3085
|
{ label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
2300
3086
|
{
|
|
2301
|
-
label: "guard imported
|
|
3087
|
+
label: "guard imported and called in a server entry",
|
|
2302
3088
|
ok: imported,
|
|
2303
|
-
hint: `import { protectFetch } (or patchstackMiddleware) from "${dir}
|
|
3089
|
+
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`
|
|
2304
3090
|
}
|
|
2305
3091
|
]
|
|
2306
3092
|
};
|
|
2307
3093
|
}
|
|
3094
|
+
var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
|
|
3095
|
+
"node_modules",
|
|
3096
|
+
"patchstack",
|
|
3097
|
+
"dist",
|
|
3098
|
+
"build",
|
|
3099
|
+
"out",
|
|
3100
|
+
"coverage",
|
|
3101
|
+
".next",
|
|
3102
|
+
".output",
|
|
3103
|
+
".svelte-kit",
|
|
3104
|
+
"test",
|
|
3105
|
+
"tests",
|
|
3106
|
+
"__tests__",
|
|
3107
|
+
"e2e",
|
|
3108
|
+
"examples",
|
|
3109
|
+
"fixtures",
|
|
3110
|
+
"__fixtures__",
|
|
3111
|
+
"stories",
|
|
3112
|
+
"__mocks__"
|
|
3113
|
+
]);
|
|
2308
3114
|
function guardIsImported(cwd, guardPath) {
|
|
2309
|
-
const
|
|
3115
|
+
const guardDir = dirname5(guardPath);
|
|
3116
|
+
const packageIsModule = packageIsEsm(cwd);
|
|
3117
|
+
const root = existsSync8(join10(cwd, "src")) ? join10(cwd, "src") : cwd;
|
|
2310
3118
|
let found = false;
|
|
2311
3119
|
const walk2 = (d, depth) => {
|
|
2312
3120
|
if (found || depth > 8) return;
|
|
@@ -2318,8 +3126,8 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2318
3126
|
}
|
|
2319
3127
|
for (const name of entries) {
|
|
2320
3128
|
if (found) return;
|
|
2321
|
-
if (name
|
|
2322
|
-
const p =
|
|
3129
|
+
if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
|
|
3130
|
+
const p = join10(d, name);
|
|
2323
3131
|
let st;
|
|
2324
3132
|
try {
|
|
2325
3133
|
st = lstatSync2(p);
|
|
@@ -2330,7 +3138,7 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2330
3138
|
if (st.isDirectory()) walk2(p, depth + 1);
|
|
2331
3139
|
else if (/\.(?:ts|tsx|js|jsx|mjs|cjs)$/.test(name) && p !== guardPath) {
|
|
2332
3140
|
try {
|
|
2333
|
-
if (
|
|
3141
|
+
if (importsAndUsesGuard(readFileSync4(p, "utf8"), p, guardDir, packageIsModule)) found = true;
|
|
2334
3142
|
} catch {
|
|
2335
3143
|
}
|
|
2336
3144
|
}
|
|
@@ -2341,6 +3149,15 @@ function guardIsImported(cwd, guardPath) {
|
|
|
2341
3149
|
}
|
|
2342
3150
|
|
|
2343
3151
|
// src/protect/install/register.ts
|
|
3152
|
+
var routeRegistration = (appVar) => new RegExp(
|
|
3153
|
+
`^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*['"\`]`,
|
|
3154
|
+
"m"
|
|
3155
|
+
);
|
|
3156
|
+
function routesBefore(source, appVar, guardIndex) {
|
|
3157
|
+
if (guardIndex < 0) return [];
|
|
3158
|
+
const pattern = routeRegistration(appVar);
|
|
3159
|
+
return source.split("\n").map((line, index) => ({ line, index })).filter(({ line, index }) => index < guardIndex && pattern.test(line)).map(({ index }) => index + 1);
|
|
3160
|
+
}
|
|
2344
3161
|
var REGION = "// #region patchstack (managed by patchstack-connect protect \u2014 do not edit)";
|
|
2345
3162
|
function guardTarget(cwd, entryRel, spec) {
|
|
2346
3163
|
if (/\.cjs$/.test(entryRel) || /\.js$/.test(entryRel) && packageType(cwd) !== "module") {
|
|
@@ -2369,11 +3186,48 @@ function guardTarget(cwd, entryRel, spec) {
|
|
|
2369
3186
|
}
|
|
2370
3187
|
function packageType(cwd) {
|
|
2371
3188
|
try {
|
|
2372
|
-
return JSON.parse(read(
|
|
3189
|
+
return JSON.parse(read(join11(cwd, "package.json"))).type;
|
|
2373
3190
|
} catch {
|
|
2374
3191
|
return void 0;
|
|
2375
3192
|
}
|
|
2376
3193
|
}
|
|
3194
|
+
function wiringState(source, spec, appVar, fileRel, guard) {
|
|
3195
|
+
const code = stripComments(source);
|
|
3196
|
+
const masked = maskStringContents(code);
|
|
3197
|
+
const lines = code.split("\n");
|
|
3198
|
+
const maskedLines = masked.split("\n");
|
|
3199
|
+
const call = spec.call(appVar);
|
|
3200
|
+
const callIndex = maskedLines.findIndex((line) => line.includes(call));
|
|
3201
|
+
const importIndex = maskedLines.findIndex(
|
|
3202
|
+
(line, index) => bindsGuard(lines[index] ?? "", line, spec.importName, call, fileRel, guard)
|
|
3203
|
+
);
|
|
3204
|
+
const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
|
|
3205
|
+
const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
|
|
3206
|
+
const anchorIndex = anchor ? maskedLines.findIndex((line) => anchor.test(line)) : -1;
|
|
3207
|
+
return {
|
|
3208
|
+
importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
|
|
3209
|
+
callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
|
|
3210
|
+
ordered: anchor ? anchorIndex !== -1 && callIndex > anchorIndex : callIndex !== -1,
|
|
3211
|
+
callIndex,
|
|
3212
|
+
appIndex
|
|
3213
|
+
};
|
|
3214
|
+
}
|
|
3215
|
+
function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
|
|
3216
|
+
if (!maskedLine.includes(importName) || maskedLine.includes(call)) return false;
|
|
3217
|
+
if (/^\s*export\b[^=]*\bfrom\b/.test(maskedLine)) return false;
|
|
3218
|
+
if (!/^\s*(?:import\b|(?:export\s+)?(?:const|let|var)\s+[^=]+=\s*require\()/.test(maskedLine)) return false;
|
|
3219
|
+
const specifier = /(['"`])([^'"`]*)\1/.exec(line)?.[2];
|
|
3220
|
+
if (specifier === void 0 || specifier === "") return false;
|
|
3221
|
+
return namesGuardModule(specifier, fileRel, guard);
|
|
3222
|
+
}
|
|
3223
|
+
function namesGuardModule(specifier, fileRel, guard) {
|
|
3224
|
+
return resolvesToGuardModule(specifier, {
|
|
3225
|
+
fromFile: join11(guard.cwd, fileRel),
|
|
3226
|
+
guardDir: resolve2(join11(guard.cwd, guard.guardDir)),
|
|
3227
|
+
kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
|
|
3228
|
+
strictExtensions: true
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
2377
3231
|
function wireRegister(cwd, opts, spec) {
|
|
2378
3232
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
2379
3233
|
if (!entry) {
|
|
@@ -2381,14 +3235,27 @@ function wireRegister(cwd, opts, spec) {
|
|
|
2381
3235
|
log(`${spec.label} not located \u2014 scaffolded guard; ${spec.manualHint}`);
|
|
2382
3236
|
return { ok: true, changed: changed2 };
|
|
2383
3237
|
}
|
|
3238
|
+
if (entry.others.length > 0) {
|
|
3239
|
+
log(
|
|
3240
|
+
`${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`
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
2384
3243
|
const target = guardTarget(cwd, entry.relPath, spec);
|
|
2385
3244
|
const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
|
|
2386
|
-
const p =
|
|
3245
|
+
const p = join11(cwd, entry.relPath);
|
|
2387
3246
|
const s = read(p);
|
|
2388
|
-
|
|
2389
|
-
|
|
3247
|
+
const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
|
|
3248
|
+
if (state.importAtTopLevel && state.callInAppScope) {
|
|
3249
|
+
log(
|
|
3250
|
+
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`
|
|
3251
|
+
);
|
|
2390
3252
|
return { ok: true, changed };
|
|
2391
3253
|
}
|
|
3254
|
+
if (state.importAtTopLevel || state.callInAppScope) {
|
|
3255
|
+
log(
|
|
3256
|
+
`${entry.relPath} has the guard ${state.importAtTopLevel ? "imported but not registered" : "registered but not imported"} \u2014 completing it`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
2392
3259
|
const sourceLines = s.split("\n");
|
|
2393
3260
|
const sourceAppIdx = sourceLines.findIndex((line) => spec.appRe.test(line));
|
|
2394
3261
|
const requiredAnchor = spec.callAfter?.(entry.appVar);
|
|
@@ -2401,40 +3268,85 @@ function wireRegister(cwd, opts, spec) {
|
|
|
2401
3268
|
importSpecifier(entry.relPath, `${dir}/${target.file}`, target.preserveExtension)
|
|
2402
3269
|
);
|
|
2403
3270
|
const lines = s.split("\n");
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
3271
|
+
if (!state.importAtTopLevel) {
|
|
3272
|
+
const lastImport = lastTopLevelImportLine(s);
|
|
3273
|
+
const importIdx = lastImport === -1 ? firstStatementLine(lines) : lastImport + 1;
|
|
3274
|
+
lines.splice(importIdx, 0, importLine);
|
|
2407
3275
|
}
|
|
2408
|
-
lines.splice(lastImport + 1, 0, importLine);
|
|
2409
3276
|
const appIdx = lines.findIndex((l) => spec.appRe.test(l));
|
|
2410
|
-
if (appIdx !== -1) {
|
|
3277
|
+
if (appIdx !== -1 && !state.callInAppScope) {
|
|
2411
3278
|
const preferred = spec.callAfter?.(entry.appVar);
|
|
2412
3279
|
const preferredIdx = preferred ? lines.findIndex((line, index) => index > appIdx && preferred.test(line)) : -1;
|
|
2413
3280
|
const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
|
|
2414
3281
|
lines.splice(callIdx + 1, 0, REGION, spec.call(entry.appVar), "// #endregion patchstack");
|
|
2415
3282
|
}
|
|
2416
|
-
|
|
3283
|
+
const patched = lines.join("\n");
|
|
3284
|
+
writeFileSync5(p, patched);
|
|
3285
|
+
if (spec.callAfter) {
|
|
3286
|
+
const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
|
|
3287
|
+
const early = routesBefore(patched, entry.appVar, guardIndex);
|
|
3288
|
+
if (early.length > 0) {
|
|
3289
|
+
log(
|
|
3290
|
+
`${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.`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
const parsed = parses(p);
|
|
3295
|
+
if (parsed === false) {
|
|
3296
|
+
writeFileSync5(p, s);
|
|
3297
|
+
log(`${entry.relPath} would not parse after patching \u2014 reverted; ${spec.manualHint}`);
|
|
3298
|
+
return { ok: true, changed };
|
|
3299
|
+
}
|
|
2417
3300
|
changed.push(entry.relPath);
|
|
2418
3301
|
log(`patched ${entry.relPath} (${spec.call(entry.appVar).trim()})`);
|
|
2419
3302
|
return { ok: true, changed: [...new Set(changed)] };
|
|
2420
3303
|
}
|
|
3304
|
+
function firstStatementLine(lines) {
|
|
3305
|
+
let i = 0;
|
|
3306
|
+
if ((lines[0] ?? "").startsWith("#!")) i = 1;
|
|
3307
|
+
while (i < lines.length) {
|
|
3308
|
+
const line = (lines[i] ?? "").trim();
|
|
3309
|
+
if (line !== "" && !line.startsWith("//") && !line.startsWith("/*") && !line.startsWith("*")) break;
|
|
3310
|
+
i++;
|
|
3311
|
+
}
|
|
3312
|
+
return i;
|
|
3313
|
+
}
|
|
3314
|
+
function serverIsGuarded(cwd, spec, server, guardDir) {
|
|
3315
|
+
const source = read(join11(cwd, server.relPath));
|
|
3316
|
+
const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
|
|
3317
|
+
if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
|
|
3318
|
+
return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
|
|
3319
|
+
}
|
|
2421
3320
|
function verifyRegister(cwd, spec) {
|
|
2422
|
-
const dir =
|
|
3321
|
+
const dir = existsSync9(join11(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
2423
3322
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
2424
3323
|
const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
|
|
2425
|
-
const scaffolded = target ?
|
|
2426
|
-
const entrySource = entry ? read(
|
|
2427
|
-
const
|
|
2428
|
-
const
|
|
2429
|
-
const
|
|
2430
|
-
const
|
|
2431
|
-
const
|
|
2432
|
-
const wired = entrySource.includes(spec.importName) && ordered;
|
|
3324
|
+
const scaffolded = target ? existsSync9(join11(cwd, dir, target.file)) : false;
|
|
3325
|
+
const entrySource = entry ? read(join11(cwd, entry.relPath)) : "";
|
|
3326
|
+
const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
|
|
3327
|
+
const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
|
|
3328
|
+
const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
|
|
3329
|
+
const noEarlyRoutes = early.length === 0;
|
|
3330
|
+
const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
|
|
2433
3331
|
return {
|
|
2434
|
-
wired: scaffolded && wired,
|
|
3332
|
+
wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
|
|
2435
3333
|
checks: [
|
|
2436
3334
|
{ label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
2437
|
-
{ label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint }
|
|
3335
|
+
{ label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
|
|
3336
|
+
...entry && entry.others.length > 0 ? [
|
|
3337
|
+
{
|
|
3338
|
+
label: "every server in this project has a guard",
|
|
3339
|
+
ok: unguarded.length === 0,
|
|
3340
|
+
hint: unguarded.length === 0 ? "nothing to do" : `${unguarded.join(", ")} start${unguarded.length === 1 ? "s" : ""} a server without one \u2014 ${spec.manualHint} there`
|
|
3341
|
+
}
|
|
3342
|
+
] : [],
|
|
3343
|
+
...spec.callAfter ? [
|
|
3344
|
+
{
|
|
3345
|
+
label: "every route registered after the guard",
|
|
3346
|
+
ok: noEarlyRoutes,
|
|
3347
|
+
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`
|
|
3348
|
+
}
|
|
3349
|
+
] : []
|
|
2438
3350
|
]
|
|
2439
3351
|
};
|
|
2440
3352
|
}
|
|
@@ -2531,18 +3443,35 @@ function runProtect(cwd, opts = {}) {
|
|
|
2531
3443
|
return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
|
|
2532
3444
|
}
|
|
2533
3445
|
}
|
|
3446
|
+
var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["next", "sveltekit", "astro", "nuxt", "generic"]);
|
|
3447
|
+
function credentialNote(cwd, adapterName) {
|
|
3448
|
+
if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
|
|
3449
|
+
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";
|
|
3450
|
+
return [
|
|
3451
|
+
{
|
|
3452
|
+
label: "live rule updates need PATCHSTACK_API_KEY in the deployment environment",
|
|
3453
|
+
ok: true,
|
|
3454
|
+
unverifiable: true,
|
|
3455
|
+
hint: `this runtime cannot read .patchstackrc.local.json \u2014 ${localHint}. Without it the guard keeps running on the rules it shipped with.`
|
|
3456
|
+
}
|
|
3457
|
+
];
|
|
3458
|
+
}
|
|
2534
3459
|
function runVerify(cwd) {
|
|
2535
3460
|
try {
|
|
2536
3461
|
const adapter = ADAPTERS.find((a) => a.detect(cwd));
|
|
2537
|
-
if (adapter)
|
|
2538
|
-
|
|
3462
|
+
if (adapter) {
|
|
3463
|
+
const result = adapter.verify(cwd);
|
|
3464
|
+
return { stack: adapter.label, ...result, checks: [...result.checks, ...credentialNote(cwd, adapter.name)] };
|
|
3465
|
+
}
|
|
3466
|
+
const generic = genericVerify(cwd);
|
|
3467
|
+
return { stack: "generic", ...generic, checks: [...generic.checks, ...credentialNote(cwd, "generic")] };
|
|
2539
3468
|
} catch (err) {
|
|
2540
3469
|
return { stack: "unknown", wired: false, checks: [{ label: "verification failed", ok: false, hint: String(err?.message ?? err) }] };
|
|
2541
3470
|
}
|
|
2542
3471
|
}
|
|
2543
3472
|
|
|
2544
3473
|
// src/widget.ts
|
|
2545
|
-
import { existsSync as
|
|
3474
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
2546
3475
|
import path8 from "path";
|
|
2547
3476
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
2548
3477
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
@@ -2581,7 +3510,7 @@ ${indent}</body>`;
|
|
|
2581
3510
|
}
|
|
2582
3511
|
function findSourceShell(cwd) {
|
|
2583
3512
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
2584
|
-
if (
|
|
3513
|
+
if (existsSync10(path8.join(cwd, candidate))) {
|
|
2585
3514
|
return candidate;
|
|
2586
3515
|
}
|
|
2587
3516
|
}
|
|
@@ -2593,15 +3522,16 @@ function ensureSourceWidget(cwd, siteUuid) {
|
|
|
2593
3522
|
return { shell: null, action: "no-shell" };
|
|
2594
3523
|
}
|
|
2595
3524
|
const file = path8.join(cwd, shell);
|
|
2596
|
-
const before =
|
|
3525
|
+
const before = readFileSync5(file, "utf8");
|
|
2597
3526
|
const { html, action } = ensureWidgetInHtml(before, siteUuid);
|
|
2598
3527
|
if (html !== before) {
|
|
2599
|
-
|
|
3528
|
+
writeFileSync6(file, html);
|
|
2600
3529
|
}
|
|
2601
3530
|
return { shell, action };
|
|
2602
3531
|
}
|
|
2603
3532
|
|
|
2604
3533
|
// src/guide.ts
|
|
3534
|
+
var PROD_MARKER_NEEDLE = "__PATCHSTACK_PROD__";
|
|
2605
3535
|
var WIDGET_NEEDLE2 = "patchstack-widget";
|
|
2606
3536
|
var INSTALL_COMMANDS = {
|
|
2607
3537
|
npm: "npm install --save @patchstack/connect",
|
|
@@ -2674,7 +3604,7 @@ function detectPackageManager(cwd) {
|
|
|
2674
3604
|
return declared;
|
|
2675
3605
|
}
|
|
2676
3606
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
2677
|
-
if (
|
|
3607
|
+
if (existsSync11(path9.join(cwd, filename))) {
|
|
2678
3608
|
return pm;
|
|
2679
3609
|
}
|
|
2680
3610
|
}
|
|
@@ -2685,7 +3615,7 @@ function installCommand(pm) {
|
|
|
2685
3615
|
}
|
|
2686
3616
|
function readPackageJson(cwd) {
|
|
2687
3617
|
try {
|
|
2688
|
-
return JSON.parse(
|
|
3618
|
+
return JSON.parse(readFileSync6(path9.join(cwd, "package.json"), "utf8"));
|
|
2689
3619
|
} catch {
|
|
2690
3620
|
return null;
|
|
2691
3621
|
}
|
|
@@ -2693,7 +3623,7 @@ function readPackageJson(cwd) {
|
|
|
2693
3623
|
function installedVersion(cwd, declaredRange) {
|
|
2694
3624
|
try {
|
|
2695
3625
|
const pkg = JSON.parse(
|
|
2696
|
-
|
|
3626
|
+
readFileSync6(path9.join(cwd, "node_modules", "@patchstack", "connect", "package.json"), "utf8")
|
|
2697
3627
|
);
|
|
2698
3628
|
if (typeof pkg.version === "string" && pkg.version.length > 0) {
|
|
2699
3629
|
return pkg.version;
|
|
@@ -2737,7 +3667,7 @@ function findWidgetMarker(cwd, siteUuid) {
|
|
|
2737
3667
|
if (statSync2(full).size > WIDGET_SCAN_MAX_BYTES) {
|
|
2738
3668
|
continue;
|
|
2739
3669
|
}
|
|
2740
|
-
const content =
|
|
3670
|
+
const content = readFileSync6(full, "utf8");
|
|
2741
3671
|
if (!content.includes(WIDGET_NEEDLE2)) {
|
|
2742
3672
|
continue;
|
|
2743
3673
|
}
|
|
@@ -2755,13 +3685,23 @@ function findWidgetMarker(cwd, siteUuid) {
|
|
|
2755
3685
|
uuidMatches: sawWidget && siteUuid != null ? sawTokenMatch : null
|
|
2756
3686
|
};
|
|
2757
3687
|
}
|
|
3688
|
+
function findProductionMarker(cwd, widgetFileHint) {
|
|
3689
|
+
if (widgetFileHint === null) {
|
|
3690
|
+
return false;
|
|
3691
|
+
}
|
|
3692
|
+
try {
|
|
3693
|
+
return readFileSync6(path9.join(cwd, widgetFileHint), "utf8").includes(PROD_MARKER_NEEDLE);
|
|
3694
|
+
} catch {
|
|
3695
|
+
return false;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
2758
3698
|
function resolveWidgetFileHint(cwd, framework) {
|
|
2759
3699
|
const candidates = [
|
|
2760
3700
|
...framework !== null ? WIDGET_FILE_CANDIDATES[framework] ?? [] : [],
|
|
2761
3701
|
...GENERIC_WIDGET_FILES
|
|
2762
3702
|
];
|
|
2763
3703
|
for (const candidate of candidates) {
|
|
2764
|
-
if (
|
|
3704
|
+
if (existsSync11(path9.join(cwd, candidate))) {
|
|
2765
3705
|
return candidate;
|
|
2766
3706
|
}
|
|
2767
3707
|
}
|
|
@@ -2804,6 +3744,7 @@ async function collectGuideState(cwd) {
|
|
|
2804
3744
|
{}
|
|
2805
3745
|
);
|
|
2806
3746
|
const widget = findWidgetMarker(cwd, siteUuid);
|
|
3747
|
+
const widgetFileHint = resolveWidgetFileHint(cwd, stack.framework);
|
|
2807
3748
|
const protection = runVerify(cwd);
|
|
2808
3749
|
return {
|
|
2809
3750
|
projectName: pkg?.name ?? null,
|
|
@@ -2823,7 +3764,8 @@ async function collectGuideState(cwd) {
|
|
|
2823
3764
|
widgetTokenMatches: widget.uuidMatches,
|
|
2824
3765
|
widgetOptOut,
|
|
2825
3766
|
framework: stack.framework,
|
|
2826
|
-
widgetFileHint
|
|
3767
|
+
widgetFileHint,
|
|
3768
|
+
productionMarkerWired: findProductionMarker(cwd, widgetFileHint),
|
|
2827
3769
|
protectionWired: protection.wired,
|
|
2828
3770
|
protectionStack: protection.stack,
|
|
2829
3771
|
protectionChecks: protection.checks
|
|
@@ -2837,6 +3779,12 @@ var ANSI = {
|
|
|
2837
3779
|
yellow: "\x1B[33m",
|
|
2838
3780
|
cyan: "\x1B[36m"
|
|
2839
3781
|
};
|
|
3782
|
+
function needsSourceProductionMarker(state) {
|
|
3783
|
+
if (state.siteUuid === null || state.widgetOptOut || state.widgetFileHint === null) {
|
|
3784
|
+
return false;
|
|
3785
|
+
}
|
|
3786
|
+
return !state.widgetFileHint.toLowerCase().endsWith(".html");
|
|
3787
|
+
}
|
|
2840
3788
|
function countRemainingSteps(state) {
|
|
2841
3789
|
return [
|
|
2842
3790
|
state.installed?.section === "dependencies",
|
|
@@ -2844,6 +3792,7 @@ function countRemainingSteps(state) {
|
|
|
2844
3792
|
state.installScanWired,
|
|
2845
3793
|
!state.hasBuildScript || state.prebuildWired && state.postbuildWired,
|
|
2846
3794
|
state.widgetOptOut || state.widgetInstalled && state.widgetTokenMatches !== false,
|
|
3795
|
+
!needsSourceProductionMarker(state) || state.productionMarkerWired,
|
|
2847
3796
|
state.protectionWired
|
|
2848
3797
|
].filter((step) => !step).length;
|
|
2849
3798
|
}
|
|
@@ -2934,6 +3883,34 @@ function renderGuideChecklist(state, useColor) {
|
|
|
2934
3883
|
lines.push(detail(` ${buildWidgetTag(state.siteUuid)}`));
|
|
2935
3884
|
lines.push(detail("The site UUID is public by design \u2014 it ships in client-side HTML."));
|
|
2936
3885
|
}
|
|
3886
|
+
if (needsSourceProductionMarker(state)) {
|
|
3887
|
+
if (state.productionMarkerWired) {
|
|
3888
|
+
lines.push(done("Production marker wired (widget switches to report mode on the published site)"));
|
|
3889
|
+
} else {
|
|
3890
|
+
lines.push(todo("Add the production marker \u2014 this root is server-rendered, so mark-build cannot stamp it"));
|
|
3891
|
+
lines.push(
|
|
3892
|
+
detail(
|
|
3893
|
+
"Without it the widget treats the published site as build mode and shows the claim flow to visitors."
|
|
3894
|
+
)
|
|
3895
|
+
);
|
|
3896
|
+
const gate = productionGate(state.framework);
|
|
3897
|
+
if (hasJsxShell(state.framework)) {
|
|
3898
|
+
lines.push(detail(`Run \u2192 npx @patchstack/connect scan (adds it to ${state.widgetFileHint} automatically)`));
|
|
3899
|
+
lines.push(detail("Or add it by hand, in <head> above the widget tag:"));
|
|
3900
|
+
for (const snippetLine of buildSourceMarkerSnippet(state.framework).split("\n")) {
|
|
3901
|
+
lines.push(detail(` ${snippetLine}`));
|
|
3902
|
+
}
|
|
3903
|
+
} else {
|
|
3904
|
+
lines.push(
|
|
3905
|
+
detail(
|
|
3906
|
+
`Edit ${state.widgetFileHint} \u2192 using the framework's head mechanism, emit an inline`
|
|
3907
|
+
)
|
|
3908
|
+
);
|
|
3909
|
+
lines.push(detail(` <script>window.__PATCHSTACK_PROD__=true;</script> only when ${gate}.`));
|
|
3910
|
+
}
|
|
3911
|
+
lines.push(detail(`The ${gate} guard is required \u2014 an ungated marker also hides the claim flow in preview.`));
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
2937
3914
|
if (state.protectionWired) {
|
|
2938
3915
|
lines.push(done(`Runtime protection wired (${state.protectionStack})`));
|
|
2939
3916
|
} else {
|
|
@@ -2961,7 +3938,7 @@ function renderGuideChecklist(state, useColor) {
|
|
|
2961
3938
|
done(
|
|
2962
3939
|
paint(
|
|
2963
3940
|
ANSI.bold,
|
|
2964
|
-
"All setup steps complete. Commit .patchstackrc.json, package.json, the runtime guard changes, and the file carrying the widget snippet."
|
|
3941
|
+
"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."
|
|
2965
3942
|
)
|
|
2966
3943
|
)
|
|
2967
3944
|
);
|
|
@@ -2977,7 +3954,7 @@ function renderGuideChecklist(state, useColor) {
|
|
|
2977
3954
|
}
|
|
2978
3955
|
|
|
2979
3956
|
// src/login.ts
|
|
2980
|
-
import { mkdtempSync, readFileSync as
|
|
3957
|
+
import { mkdtempSync, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync7 } from "fs";
|
|
2981
3958
|
import { tmpdir } from "os";
|
|
2982
3959
|
import path10 from "path";
|
|
2983
3960
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2994,11 +3971,11 @@ function pendingPath(siteUuid) {
|
|
|
2994
3971
|
return path10.join(tmpdir(), `patchstack-login-${key}.json`);
|
|
2995
3972
|
}
|
|
2996
3973
|
function savePendingLogin(siteUuid, pending) {
|
|
2997
|
-
|
|
3974
|
+
writeFileSync7(pendingPath(siteUuid), JSON.stringify(pending), { encoding: "utf8", mode: 384 });
|
|
2998
3975
|
}
|
|
2999
3976
|
function readPendingLogin(siteUuid) {
|
|
3000
3977
|
try {
|
|
3001
|
-
return JSON.parse(
|
|
3978
|
+
return JSON.parse(readFileSync7(pendingPath(siteUuid), "utf8"));
|
|
3002
3979
|
} catch {
|
|
3003
3980
|
return null;
|
|
3004
3981
|
}
|
|
@@ -3104,10 +4081,10 @@ async function login(config, onPrompt, deps = {}) {
|
|
|
3104
4081
|
// src/map/ts-loader.ts
|
|
3105
4082
|
import { createRequire } from "module";
|
|
3106
4083
|
import { pathToFileURL } from "url";
|
|
3107
|
-
import { join as
|
|
4084
|
+
import { join as join12 } from "path";
|
|
3108
4085
|
async function loadTypeScript(cwd) {
|
|
3109
4086
|
try {
|
|
3110
|
-
const req = createRequire(pathToFileURL(
|
|
4087
|
+
const req = createRequire(pathToFileURL(join12(cwd, "package.json")));
|
|
3111
4088
|
const resolved = req.resolve("typescript");
|
|
3112
4089
|
const mod = await import(pathToFileURL(resolved).href);
|
|
3113
4090
|
return mod.default ?? mod;
|
|
@@ -3122,7 +4099,7 @@ async function loadTypeScript(cwd) {
|
|
|
3122
4099
|
}
|
|
3123
4100
|
|
|
3124
4101
|
// src/map/extract.ts
|
|
3125
|
-
import { readFileSync as
|
|
4102
|
+
import { readFileSync as readFileSync12, realpathSync as realpathSync3 } from "fs";
|
|
3126
4103
|
import { createHash as createHash4 } from "crypto";
|
|
3127
4104
|
import { relative as relative4 } from "path";
|
|
3128
4105
|
|
|
@@ -3378,8 +4355,8 @@ function npmPackageOf(spec) {
|
|
|
3378
4355
|
}
|
|
3379
4356
|
|
|
3380
4357
|
// src/map/sources.ts
|
|
3381
|
-
import { readFileSync as
|
|
3382
|
-
import { join as
|
|
4358
|
+
import { readFileSync as readFileSync8, readdirSync as readdirSync5, realpathSync, statSync as statSync3 } from "fs";
|
|
4359
|
+
import { join as join13, relative as relative2, isAbsolute } from "path";
|
|
3383
4360
|
|
|
3384
4361
|
// src/map/routes.ts
|
|
3385
4362
|
var ROUTE_REGISTER_NAMES = ["get", "post", "put", "patch", "delete", "options", "all", "head", "use"];
|
|
@@ -3463,7 +4440,7 @@ function hasEntrySignal(text) {
|
|
|
3463
4440
|
}
|
|
3464
4441
|
function detectFramework(cwd) {
|
|
3465
4442
|
try {
|
|
3466
|
-
const pkg = JSON.parse(
|
|
4443
|
+
const pkg = JSON.parse(readFileSync8(join13(cwd, "package.json"), "utf8"));
|
|
3467
4444
|
const d = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
3468
4445
|
if (d["@tanstack/react-start"] || d["@tanstack/start"] || d["@tanstack/solid-start"]) return "tanstack-start";
|
|
3469
4446
|
if (d["next"]) return "next";
|
|
@@ -3475,11 +4452,11 @@ function detectFramework(cwd) {
|
|
|
3475
4452
|
} catch {
|
|
3476
4453
|
}
|
|
3477
4454
|
try {
|
|
3478
|
-
if (statSync3(
|
|
4455
|
+
if (statSync3(join13(cwd, "supabase", "functions")).isDirectory()) return "supabase-functions";
|
|
3479
4456
|
} catch {
|
|
3480
4457
|
}
|
|
3481
4458
|
try {
|
|
3482
|
-
if (statSync3(
|
|
4459
|
+
if (statSync3(join13(cwd, "functions")).isDirectory()) return "deno-functions";
|
|
3483
4460
|
} catch {
|
|
3484
4461
|
}
|
|
3485
4462
|
return "unknown";
|
|
@@ -3532,7 +4509,7 @@ function collectSources(dir, boundary, opts, out = [], seen = /* @__PURE__ */ ne
|
|
|
3532
4509
|
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
3533
4510
|
for (const e of entries) {
|
|
3534
4511
|
if (SKIP_DIRS2.has(e.name) || e.name.startsWith(".") && e.name !== ".") continue;
|
|
3535
|
-
const full =
|
|
4512
|
+
const full = join13(dir, e.name);
|
|
3536
4513
|
if (e.isDirectory()) collectSources(full, boundary, opts, out, seen, stats);
|
|
3537
4514
|
else if (e.isSymbolicLink()) {
|
|
3538
4515
|
let st, real;
|
|
@@ -3602,7 +4579,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3602
4579
|
const found = [];
|
|
3603
4580
|
for (const candidate of DEPLOYMENT_SHAPES) {
|
|
3604
4581
|
for (const file of candidate.files ?? []) {
|
|
3605
|
-
const full =
|
|
4582
|
+
const full = join13(cwd, file);
|
|
3606
4583
|
try {
|
|
3607
4584
|
if (statSync3(full).isFile() && inProject(full)) {
|
|
3608
4585
|
found.push({ shape: candidate.shape, source: file, evidence: candidate.evidence });
|
|
@@ -3612,7 +4589,7 @@ function detectDeploymentShapes(cwd, opts = {}) {
|
|
|
3612
4589
|
}
|
|
3613
4590
|
}
|
|
3614
4591
|
for (const dir of candidate.dirs ?? []) {
|
|
3615
|
-
const full =
|
|
4592
|
+
const full = join13(cwd, dir);
|
|
3616
4593
|
try {
|
|
3617
4594
|
if (!statSync3(full).isDirectory() || !inProject(full)) continue;
|
|
3618
4595
|
} catch {
|
|
@@ -3631,7 +4608,7 @@ function holdsFunctionEntry(dir) {
|
|
|
3631
4608
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
3632
4609
|
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
3633
4610
|
try {
|
|
3634
|
-
for (const nested of readdirSync5(
|
|
4611
|
+
for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
|
|
3635
4612
|
if (nested.isFile() && /^index\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(nested.name)) return true;
|
|
3636
4613
|
}
|
|
3637
4614
|
} catch {
|
|
@@ -3647,7 +4624,7 @@ function holdsSourceFile(dir) {
|
|
|
3647
4624
|
if (entry.isFile() && isSourceFile(entry.name)) return true;
|
|
3648
4625
|
if (entry.isDirectory()) {
|
|
3649
4626
|
try {
|
|
3650
|
-
for (const nested of readdirSync5(
|
|
4627
|
+
for (const nested of readdirSync5(join13(dir, entry.name), { withFileTypes: true })) {
|
|
3651
4628
|
if (nested.isFile() && isSourceFile(nested.name)) return true;
|
|
3652
4629
|
}
|
|
3653
4630
|
} catch {
|
|
@@ -3660,8 +4637,8 @@ function holdsSourceFile(dir) {
|
|
|
3660
4637
|
}
|
|
3661
4638
|
|
|
3662
4639
|
// src/map/surface.ts
|
|
3663
|
-
import { readFileSync as
|
|
3664
|
-
import { join as
|
|
4640
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
4641
|
+
import { join as join14 } from "path";
|
|
3665
4642
|
var STATIC_GENERATORS = [
|
|
3666
4643
|
{ dep: "vite", label: "vite" },
|
|
3667
4644
|
{ dep: "react-scripts", label: "create-react-app" },
|
|
@@ -3699,7 +4676,7 @@ var SHAPE_SIGNAL = {
|
|
|
3699
4676
|
};
|
|
3700
4677
|
function readManifest(cwd) {
|
|
3701
4678
|
try {
|
|
3702
|
-
const parsed = JSON.parse(
|
|
4679
|
+
const parsed = JSON.parse(readFileSync9(join14(cwd, "package.json"), "utf8"));
|
|
3703
4680
|
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
3704
4681
|
} catch {
|
|
3705
4682
|
return null;
|
|
@@ -3712,7 +4689,7 @@ function nextExportsStatically(cwd, manifest, ts) {
|
|
|
3712
4689
|
for (const file of ["next.config.js", "next.config.mjs", "next.config.ts", "next.config.cjs"]) {
|
|
3713
4690
|
let text;
|
|
3714
4691
|
try {
|
|
3715
|
-
text =
|
|
4692
|
+
text = readFileSync9(join14(cwd, file), "utf8");
|
|
3716
4693
|
} catch {
|
|
3717
4694
|
continue;
|
|
3718
4695
|
}
|
|
@@ -4112,8 +5089,8 @@ function dedupeSinks(sinks, owner) {
|
|
|
4112
5089
|
}
|
|
4113
5090
|
|
|
4114
5091
|
// src/map/module-graph.ts
|
|
4115
|
-
import { readFileSync as
|
|
4116
|
-
import { dirname as
|
|
5092
|
+
import { readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
5093
|
+
import { dirname as dirname7, join as join15, relative as relative3, resolve as resolvePath } from "path";
|
|
4117
5094
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
4118
5095
|
function createModuleGraph(ts, opts) {
|
|
4119
5096
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -4121,7 +5098,7 @@ function createModuleGraph(ts, opts) {
|
|
|
4121
5098
|
if (cache.has(file)) return cache.get(file) ?? null;
|
|
4122
5099
|
let entry = null;
|
|
4123
5100
|
try {
|
|
4124
|
-
const text =
|
|
5101
|
+
const text = readFileSync10(file, "utf8");
|
|
4125
5102
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
4126
5103
|
const bindings = buildModuleBindings(sf, ts);
|
|
4127
5104
|
entry = { fnSinks: collectLocalSinks(sf, ts, bindings), calleesOf: collectCallees(sf, ts), bindings };
|
|
@@ -4181,7 +5158,7 @@ function createModuleGraph(ts, opts) {
|
|
|
4181
5158
|
}
|
|
4182
5159
|
function resolveRelativeModule(fromFile, spec) {
|
|
4183
5160
|
if (!spec.startsWith(".")) return void 0;
|
|
4184
|
-
const dir =
|
|
5161
|
+
const dir = dirname7(fromFile);
|
|
4185
5162
|
const base = resolvePath(dir, spec);
|
|
4186
5163
|
const candidates = [];
|
|
4187
5164
|
const jsLike = /\.(js|jsx|mjs|cjs)$/.exec(base);
|
|
@@ -4191,7 +5168,7 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
4191
5168
|
}
|
|
4192
5169
|
candidates.push(base);
|
|
4193
5170
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
4194
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
5171
|
+
for (const e of RESOLVE_EXTS) candidates.push(join15(base, "index" + e));
|
|
4195
5172
|
for (const c of candidates) {
|
|
4196
5173
|
try {
|
|
4197
5174
|
if (statSync4(c).isFile()) return c;
|
|
@@ -4863,8 +5840,8 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
4863
5840
|
}
|
|
4864
5841
|
|
|
4865
5842
|
// src/map/imports.ts
|
|
4866
|
-
import { readFileSync as
|
|
4867
|
-
import { join as
|
|
5843
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
5844
|
+
import { join as join16 } from "path";
|
|
4868
5845
|
var MAX_SITES = 5;
|
|
4869
5846
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
4870
5847
|
function isNpmPackageName(pkg) {
|
|
@@ -4875,7 +5852,7 @@ function readPathAliases(cwd) {
|
|
|
4875
5852
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
4876
5853
|
let paths;
|
|
4877
5854
|
try {
|
|
4878
|
-
const raw =
|
|
5855
|
+
const raw = readFileSync11(join16(cwd, name), "utf8");
|
|
4879
5856
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
4880
5857
|
paths = parsed.compilerOptions?.paths;
|
|
4881
5858
|
} catch {
|
|
@@ -5254,7 +6231,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5254
6231
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
5255
6232
|
for (const file of files) {
|
|
5256
6233
|
try {
|
|
5257
|
-
const text =
|
|
6234
|
+
const text = readFileSync12(file, "utf8");
|
|
5258
6235
|
const relFile = relative4(cwd, file);
|
|
5259
6236
|
sourceBytes += text.length;
|
|
5260
6237
|
if (!hasEntrySignal(text)) {
|
|
@@ -5309,7 +6286,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
5309
6286
|
for (const target of hopTargets) {
|
|
5310
6287
|
if (entryFiles.has(target)) continue;
|
|
5311
6288
|
try {
|
|
5312
|
-
const text =
|
|
6289
|
+
const text = readFileSync12(target, "utf8");
|
|
5313
6290
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
5314
6291
|
const bindings = buildModuleBindings(sf, ts);
|
|
5315
6292
|
const called = collectInvocations(sf, ts, bindings, { file: target, owner: relative4(cwd, target), graph });
|
|
@@ -5440,7 +6417,7 @@ async function buildInputMap(cwd, options = {}) {
|
|
|
5440
6417
|
}
|
|
5441
6418
|
|
|
5442
6419
|
// src/setup.ts
|
|
5443
|
-
import { readFileSync as
|
|
6420
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "fs";
|
|
5444
6421
|
import path11 from "path";
|
|
5445
6422
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
5446
6423
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
@@ -5460,7 +6437,7 @@ function appendHook(existing, command) {
|
|
|
5460
6437
|
}
|
|
5461
6438
|
function wireBuildScripts(cwd, packageManager) {
|
|
5462
6439
|
const target = path11.join(cwd, "package.json");
|
|
5463
|
-
const raw =
|
|
6440
|
+
const raw = readFileSync13(target, "utf8");
|
|
5464
6441
|
const pkg = JSON.parse(raw);
|
|
5465
6442
|
const scripts = pkg.scripts ?? {};
|
|
5466
6443
|
const build = scripts.build;
|
|
@@ -5509,7 +6486,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
5509
6486
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
5510
6487
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
5511
6488
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
5512
|
-
|
|
6489
|
+
writeFileSync8(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
5513
6490
|
if (build === void 0 || build.trim().length === 0) {
|
|
5514
6491
|
return {
|
|
5515
6492
|
changed: true,
|
|
@@ -5588,7 +6565,9 @@ Usage:
|
|
|
5588
6565
|
print the full setup guide. --full prints the
|
|
5589
6566
|
guide even when setup is complete
|
|
5590
6567
|
patchstack-connect login [--wait] Recover this site's credential when
|
|
5591
|
-
.patchstackrc.json has been lost.
|
|
6568
|
+
.patchstackrc.local.json has been lost. Needs the
|
|
6569
|
+
site UUID, which lives in the committed
|
|
6570
|
+
.patchstackrc.json. Prints a link
|
|
5592
6571
|
for the site's OWNER to approve in the dashboard.
|
|
5593
6572
|
In a terminal it then waits. When the output is
|
|
5594
6573
|
piped or captured \u2014 an assistant running it \u2014 it
|
|
@@ -5629,7 +6608,8 @@ Environment:
|
|
|
5629
6608
|
PATCHSTACK_MODE (protect) Runtime guard mode: block (default) | dry-run
|
|
5630
6609
|
PATCHSTACK_ROUTE_WAF (protect) Set to 1 to also screen every request at the route level (opt-in)
|
|
5631
6610
|
|
|
5632
|
-
Precedence: CLI flag > environment variable > .patchstackrc.json.
|
|
6611
|
+
Precedence, site UUID and settings: CLI flag > environment variable > .patchstackrc.json (committed).
|
|
6612
|
+
Precedence, API key: environment variable > .patchstackrc.local.json (git-ignored) > .patchstackrc.json.
|
|
5633
6613
|
|
|
5634
6614
|
Examples:
|
|
5635
6615
|
npx @patchstack/connect setup
|
|
@@ -5702,8 +6682,13 @@ async function runLogin(args) {
|
|
|
5702
6682
|
cliSiteUuid: getStringFlag(args.flags, "site-uuid"),
|
|
5703
6683
|
cliEndpoint: getStringFlag(args.flags, "endpoint")
|
|
5704
6684
|
});
|
|
5705
|
-
const approved = () => {
|
|
5706
|
-
|
|
6685
|
+
const approved = async () => {
|
|
6686
|
+
const ignore = await secretFileIgnored(process.cwd());
|
|
6687
|
+
console.log(`
|
|
6688
|
+
\u2713 Credential restored and saved to ${SECRET_CONFIG_FILENAME}.`);
|
|
6689
|
+
console.log(
|
|
6690
|
+
ignore.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${ignore.reason ?? "unknown reason"}. Add it to .gitignore yourself before committing.`
|
|
6691
|
+
);
|
|
5707
6692
|
console.log(" The previous credential no longer works. Update it anywhere else it was set:");
|
|
5708
6693
|
console.log(" CI secrets, hosting env vars, preview environments, other checkouts.\n");
|
|
5709
6694
|
return 0;
|
|
@@ -5715,7 +6700,7 @@ async function runLogin(args) {
|
|
|
5715
6700
|
return 1;
|
|
5716
6701
|
}
|
|
5717
6702
|
const resumed = await waitForApproval(config, pending);
|
|
5718
|
-
if (resumed.status === "approved") return approved();
|
|
6703
|
+
if (resumed.status === "approved") return await approved();
|
|
5719
6704
|
console.error(`
|
|
5720
6705
|
${resumed.message ?? "Login failed."}
|
|
5721
6706
|
`);
|
|
@@ -5734,7 +6719,7 @@ async function runLogin(args) {
|
|
|
5734
6719
|
const existing = config.siteUuid === null ? null : readPendingLogin(config.siteUuid);
|
|
5735
6720
|
if (existing !== null && Date.now() < existing.expiresAt) {
|
|
5736
6721
|
const outcome = await redeemIfApproved(config, existing);
|
|
5737
|
-
if (outcome === "approved") return approved();
|
|
6722
|
+
if (outcome === "approved") return await approved();
|
|
5738
6723
|
if (outcome === "pending") {
|
|
5739
6724
|
const secondsLeft = Math.round((existing.expiresAt - Date.now()) / 1e3);
|
|
5740
6725
|
console.log(`
|
|
@@ -5761,7 +6746,7 @@ async function runLogin(args) {
|
|
|
5761
6746
|
prompt(userCode, verificationUri);
|
|
5762
6747
|
console.log(" Waiting for approval (the code expires in 10 minutes)\u2026");
|
|
5763
6748
|
});
|
|
5764
|
-
if (result.status === "approved") return approved();
|
|
6749
|
+
if (result.status === "approved") return await approved();
|
|
5765
6750
|
console.error(`
|
|
5766
6751
|
${result.message ?? "Login failed."}
|
|
5767
6752
|
`);
|
|
@@ -5810,7 +6795,7 @@ async function runMap(args) {
|
|
|
5810
6795
|
const json = JSON.stringify(map, null, 2);
|
|
5811
6796
|
const out = getStringFlag(args.flags, "out");
|
|
5812
6797
|
if (out) {
|
|
5813
|
-
|
|
6798
|
+
writeFileSync9(out, json);
|
|
5814
6799
|
console.error(`patchstack: wrote ${out}`);
|
|
5815
6800
|
} else if (args.flags.get("upload") !== true) {
|
|
5816
6801
|
console.log(json);
|
|
@@ -5883,6 +6868,9 @@ async function runScan(args, options = {}) {
|
|
|
5883
6868
|
}
|
|
5884
6869
|
return 0;
|
|
5885
6870
|
}
|
|
6871
|
+
if (config.widget) {
|
|
6872
|
+
reportSourceMarker(detectStack(payload.packages).framework);
|
|
6873
|
+
}
|
|
5886
6874
|
const provisioning = config.siteUuid === null;
|
|
5887
6875
|
if (provisioning) {
|
|
5888
6876
|
console.log("No site UUID configured \u2014 provisioning a new Patchstack site from this manifest\u2026");
|
|
@@ -5893,8 +6881,17 @@ async function runScan(args, options = {}) {
|
|
|
5893
6881
|
console.log(`Provisioned site ${response.uuid}. Saved UUID to ${target}.`);
|
|
5894
6882
|
}
|
|
5895
6883
|
if (typeof response.api_key === "string" && response.api_key.length > 0) {
|
|
5896
|
-
const
|
|
5897
|
-
|
|
6884
|
+
const hadCredentialInConfig = await credentialInCommittedConfig(process.cwd());
|
|
6885
|
+
const saved = await persistApiKey(process.cwd(), response.api_key);
|
|
6886
|
+
console.log(`Saved API key to ${saved.path}. Do not commit it.`);
|
|
6887
|
+
console.log(
|
|
6888
|
+
saved.ignored ? " Added to .gitignore." : ` NOT ignored by git \u2014 ${saved.reason ?? "unknown reason"}. Add \`${SECRET_CONFIG_FILENAME}\` to .gitignore yourself before committing.`
|
|
6889
|
+
);
|
|
6890
|
+
if (hadCredentialInConfig) {
|
|
6891
|
+
console.log(
|
|
6892
|
+
"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."
|
|
6893
|
+
);
|
|
6894
|
+
}
|
|
5898
6895
|
}
|
|
5899
6896
|
if (response.stored) {
|
|
5900
6897
|
console.log(`Stored manifest #${response.manifest_id} (checksum ${response.checksum}).`);
|
|
@@ -5965,14 +6962,54 @@ function reportSourceWidget(siteUuid) {
|
|
|
5965
6962
|
console.warn(`Widget: skipped (${err.message}).`);
|
|
5966
6963
|
}
|
|
5967
6964
|
}
|
|
6965
|
+
function reportSourceMarker(framework) {
|
|
6966
|
+
try {
|
|
6967
|
+
const shell = resolveWidgetFileHint(process.cwd(), framework);
|
|
6968
|
+
if (shell === null || shell.toLowerCase().endsWith(".html")) {
|
|
6969
|
+
return;
|
|
6970
|
+
}
|
|
6971
|
+
const result = ensureSourceMarker(process.cwd(), shell, framework);
|
|
6972
|
+
switch (result.action) {
|
|
6973
|
+
case "added":
|
|
6974
|
+
console.log(`Production marker: added to ${shell} (guarded by ${productionGate(framework)}).`);
|
|
6975
|
+
console.log(" This root is server-rendered, so the marker ships in source rather than built HTML.");
|
|
6976
|
+
break;
|
|
6977
|
+
case "manual":
|
|
6978
|
+
console.log(`Production marker: already set in ${shell} \u2014 left untouched.`);
|
|
6979
|
+
break;
|
|
6980
|
+
case "no-anchor":
|
|
6981
|
+
case "unsupported":
|
|
6982
|
+
console.log(
|
|
6983
|
+
`Production marker: ${shell} needs it by hand \u2014 without it the widget reads the published site as build mode.`
|
|
6984
|
+
);
|
|
6985
|
+
if (hasJsxShell(framework)) {
|
|
6986
|
+
for (const line of buildSourceMarkerSnippet(framework).split("\n")) {
|
|
6987
|
+
console.log(` ${line}`);
|
|
6988
|
+
}
|
|
6989
|
+
} else {
|
|
6990
|
+
console.log(
|
|
6991
|
+
` Emit <script>window.__PATCHSTACK_PROD__=true;</script> only when ${productionGate(framework)}.`
|
|
6992
|
+
);
|
|
6993
|
+
}
|
|
6994
|
+
break;
|
|
6995
|
+
}
|
|
6996
|
+
} catch (err) {
|
|
6997
|
+
console.warn(`Production marker: skipped (${err.message}).`);
|
|
6998
|
+
}
|
|
6999
|
+
}
|
|
5968
7000
|
async function runProtectCommand(args) {
|
|
5969
7001
|
if (args.flags.get("check") === true) {
|
|
5970
7002
|
const report = runVerify(process.cwd());
|
|
5971
7003
|
console.log(`patchstack protect --check (${report.stack}):`);
|
|
5972
7004
|
for (const c of report.checks) {
|
|
5973
|
-
|
|
7005
|
+
const mark = c.unverifiable ? "?" : c.ok ? "\u2713" : "\u2717";
|
|
7006
|
+
const note = c.unverifiable || !c.ok ? c.hint : void 0;
|
|
7007
|
+
console.log(` ${mark} ${c.label}${note ? ` \u2014 ${note}` : ""}`);
|
|
5974
7008
|
}
|
|
5975
7009
|
console.log(report.wired ? "guard is wired \u2713" : "guard is NOT fully wired \u2717");
|
|
7010
|
+
if (report.checks.some((c) => c.unverifiable)) {
|
|
7011
|
+
console.log("One or more checks could not be answered from here \u2014 see the `?` lines above.");
|
|
7012
|
+
}
|
|
5976
7013
|
return report.wired ? 0 : 1;
|
|
5977
7014
|
}
|
|
5978
7015
|
const demo = args.flags.get("demo") === true;
|
|
@@ -6096,7 +7133,7 @@ async function runGuide(args) {
|
|
|
6096
7133
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
6097
7134
|
console.log("");
|
|
6098
7135
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
6099
|
-
console.log(
|
|
7136
|
+
console.log(readFileSync14(guidePath, "utf8"));
|
|
6100
7137
|
return 0;
|
|
6101
7138
|
}
|
|
6102
7139
|
async function runSetup(args) {
|
|
@@ -6270,14 +7307,20 @@ async function runMarkBuild(args) {
|
|
|
6270
7307
|
}
|
|
6271
7308
|
const files = findHtmlFiles(dir);
|
|
6272
7309
|
if (files.length === 0) {
|
|
6273
|
-
console.warn(`mark-build:
|
|
7310
|
+
console.warn(`mark-build: found ${dir} but no HTML files in it.`);
|
|
7311
|
+
console.warn(
|
|
7312
|
+
"mark-build: this build looks server-rendered, so the production flag has nothing to stamp."
|
|
7313
|
+
);
|
|
7314
|
+
console.warn(
|
|
7315
|
+
"mark-build: add the marker to your root shell instead \u2014 run `patchstack-connect guide` for the snippet."
|
|
7316
|
+
);
|
|
6274
7317
|
return 0;
|
|
6275
7318
|
}
|
|
6276
7319
|
const snippet = buildInjectionSnippet(checksum, stack);
|
|
6277
7320
|
let marked = 0;
|
|
6278
7321
|
let widgetTouched = 0;
|
|
6279
7322
|
for (const file of files) {
|
|
6280
|
-
const before =
|
|
7323
|
+
const before = readFileSync14(file, "utf8");
|
|
6281
7324
|
let after = injectMarker(before, snippet);
|
|
6282
7325
|
if (widgetUuid !== null) {
|
|
6283
7326
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -6287,7 +7330,7 @@ async function runMarkBuild(args) {
|
|
|
6287
7330
|
after = ensured.html;
|
|
6288
7331
|
}
|
|
6289
7332
|
if (after !== before) {
|
|
6290
|
-
|
|
7333
|
+
writeFileSync9(file, after);
|
|
6291
7334
|
marked += 1;
|
|
6292
7335
|
}
|
|
6293
7336
|
}
|