@patchstack/connect 0.5.27 → 0.5.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 +34 -19
- package/README.md +28 -14
- package/dist/{chunk-YG5C7HYB.js → chunk-52IV7LN7.js} +1 -1
- package/dist/{chunk-YG5C7HYB.js.map → chunk-52IV7LN7.js.map} +1 -1
- package/dist/cli.js +2735 -369
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +10 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +5 -4
- package/dist/protect/templates/express-guard.cjs +5 -8
- package/dist/protect/templates/express-guard.js +5 -8
- package/dist/protect/templates/express-guard.ts +5 -8
- package/dist/protect/templates/fastify-plugin.cjs +52 -6
- package/dist/protect/templates/fastify-plugin.js +52 -6
- package/dist/protect/templates/fastify-plugin.ts +53 -7
- package/dist/protect/templates/fetch-guard.ts +52 -0
- package/dist/protect/templates/generic-guard.cjs +6 -5
- package/dist/protect/templates/generic-guard.js +6 -5
- package/dist/protect/templates/generic-guard.ts +5 -8
- package/dist/protect/templates/guard.ts +4 -7
- package/dist/protect/templates/next-guard.ts +5 -4
- package/dist/protect/templates/next-middleware.ts +5 -4
- package/dist/protect/templates/nuxt-middleware.ts +5 -4
- package/dist/protect/templates/sveltekit-hooks.ts +5 -4
- package/dist/protect.cjs +10 -6
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +1 -1
- package/dist/protect.d.ts +1 -1
- package/dist/protect.edge.js +2 -2
- package/dist/protect.edge.js.map +1 -1
- package/dist/protect.js +4 -4
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-THITZHBD.js → refresh-manifest-EBORKM75.js} +10 -6
- package/dist/refresh-manifest-EBORKM75.js.map +1 -0
- package/package.json +5 -2
- package/dist/refresh-manifest-THITZHBD.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync20 } from "fs";
|
|
5
5
|
import path16 from "path";
|
|
6
|
-
import { createRequire as
|
|
6
|
+
import { createRequire as createRequire6 } from "module";
|
|
7
7
|
|
|
8
8
|
// src/safe-file.ts
|
|
9
9
|
import {
|
|
@@ -1073,7 +1073,7 @@ async function scanLockfile(cwd) {
|
|
|
1073
1073
|
await detectLockfile(cwd);
|
|
1074
1074
|
}
|
|
1075
1075
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
1076
|
-
const
|
|
1076
|
+
const warnings2 = [];
|
|
1077
1077
|
let firstParsed = null;
|
|
1078
1078
|
let walked = null;
|
|
1079
1079
|
let walkWasCandidate = false;
|
|
@@ -1087,7 +1087,7 @@ async function scanLockfile(cwd) {
|
|
|
1087
1087
|
continue;
|
|
1088
1088
|
}
|
|
1089
1089
|
const unreadable = unreadableWarning(candidate.filename, report);
|
|
1090
|
-
if (unreadable !== null)
|
|
1090
|
+
if (unreadable !== null) warnings2.push(unreadable);
|
|
1091
1091
|
if (candidate.strategy === "node-modules-walk") {
|
|
1092
1092
|
walked = packages;
|
|
1093
1093
|
walkWasCandidate = true;
|
|
@@ -1098,16 +1098,16 @@ async function scanLockfile(cwd) {
|
|
|
1098
1098
|
for (const source of parsedSources) {
|
|
1099
1099
|
const missing = missingDependencies(declared, source.packages);
|
|
1100
1100
|
if (missing.length > 0) {
|
|
1101
|
-
|
|
1101
|
+
warnings2.push(staleWarning(source.filename, missing));
|
|
1102
1102
|
continue;
|
|
1103
1103
|
}
|
|
1104
1104
|
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
1105
1105
|
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
1106
1106
|
);
|
|
1107
1107
|
if (conflicts.length === 0) {
|
|
1108
|
-
return manifestWith(source.packages,
|
|
1108
|
+
return manifestWith(source.packages, warnings2);
|
|
1109
1109
|
}
|
|
1110
|
-
|
|
1110
|
+
warnings2.push(conflictWarning(source.filename, conflicts));
|
|
1111
1111
|
break;
|
|
1112
1112
|
}
|
|
1113
1113
|
if (walked === null) {
|
|
@@ -1120,12 +1120,12 @@ async function scanLockfile(cwd) {
|
|
|
1120
1120
|
if (walked !== null) {
|
|
1121
1121
|
const missing = missingDependencies(declared, walked);
|
|
1122
1122
|
if (missing.length > 0) {
|
|
1123
|
-
|
|
1123
|
+
warnings2.push(staleWarning("node_modules/", missing));
|
|
1124
1124
|
}
|
|
1125
|
-
|
|
1125
|
+
warnings2.push(
|
|
1126
1126
|
walkWasCandidate ? "Checked the installed packages in node_modules/, which is what the build loads." : "Checked the installed packages in node_modules/ instead. Delete the out-of-date package list file to stop this warning."
|
|
1127
1127
|
);
|
|
1128
|
-
return manifestWith(walked,
|
|
1128
|
+
return manifestWith(walked, warnings2);
|
|
1129
1129
|
}
|
|
1130
1130
|
if (firstParsed === null) {
|
|
1131
1131
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
@@ -1134,13 +1134,13 @@ async function scanLockfile(cwd) {
|
|
|
1134
1134
|
"LOCKFILE_NOT_FOUND"
|
|
1135
1135
|
);
|
|
1136
1136
|
}
|
|
1137
|
-
|
|
1137
|
+
warnings2.push(
|
|
1138
1138
|
`No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
|
|
1139
1139
|
);
|
|
1140
|
-
return manifestWith(firstParsed.packages,
|
|
1140
|
+
return manifestWith(firstParsed.packages, warnings2);
|
|
1141
1141
|
}
|
|
1142
|
-
function manifestWith(packages,
|
|
1143
|
-
return
|
|
1142
|
+
function manifestWith(packages, warnings2) {
|
|
1143
|
+
return warnings2.length > 0 ? { ecosystem: "npm", packages, warnings: warnings2 } : { ecosystem: "npm", packages };
|
|
1144
1144
|
}
|
|
1145
1145
|
function otherCompleteSources(all, current, declared) {
|
|
1146
1146
|
return all.filter(
|
|
@@ -1205,10 +1205,10 @@ function buildWirePayload(manifest, options = {}) {
|
|
|
1205
1205
|
const byIdentity = /* @__PURE__ */ new Map();
|
|
1206
1206
|
let entriesWithoutPath = 0;
|
|
1207
1207
|
for (const entry of manifest.packages) {
|
|
1208
|
-
const
|
|
1208
|
+
const identity2 = `${entry.name}@${entry.version}`;
|
|
1209
1209
|
const location = withPaths && entry.path !== void 0 && entry.path !== "" ? entry.path : void 0;
|
|
1210
1210
|
if (location === void 0) entriesWithoutPath++;
|
|
1211
|
-
const existing = byIdentity.get(
|
|
1211
|
+
const existing = byIdentity.get(identity2);
|
|
1212
1212
|
if (existing) {
|
|
1213
1213
|
if (location !== void 0) addPath(existing, location);
|
|
1214
1214
|
continue;
|
|
@@ -1221,7 +1221,7 @@ function buildWirePayload(manifest, options = {}) {
|
|
|
1221
1221
|
}
|
|
1222
1222
|
const wire6 = { name: entry.name, version: entry.version };
|
|
1223
1223
|
if (location !== void 0) addPath(wire6, location);
|
|
1224
|
-
byIdentity.set(
|
|
1224
|
+
byIdentity.set(identity2, wire6);
|
|
1225
1225
|
wirePackages.push(wire6);
|
|
1226
1226
|
}
|
|
1227
1227
|
for (const wire6 of wirePackages) wire6.paths?.sort();
|
|
@@ -1329,7 +1329,7 @@ function compareSegments(a, b) {
|
|
|
1329
1329
|
// src/checksum.ts
|
|
1330
1330
|
import { createHash } from "crypto";
|
|
1331
1331
|
function computeManifestChecksum(packages) {
|
|
1332
|
-
const
|
|
1332
|
+
const canonical2 = packages.map((pkg) => ({ name: pkg.name, version: pkg.version })).sort((a, b) => {
|
|
1333
1333
|
if (a.name !== b.name) {
|
|
1334
1334
|
return a.name < b.name ? -1 : 1;
|
|
1335
1335
|
}
|
|
@@ -1338,7 +1338,7 @@ function computeManifestChecksum(packages) {
|
|
|
1338
1338
|
}
|
|
1339
1339
|
return 0;
|
|
1340
1340
|
});
|
|
1341
|
-
return createHash("sha256").update(JSON.stringify(
|
|
1341
|
+
return createHash("sha256").update(JSON.stringify(canonical2)).digest("hex").slice(0, 12);
|
|
1342
1342
|
}
|
|
1343
1343
|
|
|
1344
1344
|
// src/hosting.ts
|
|
@@ -1379,6 +1379,18 @@ function isSafeOrigin(value) {
|
|
|
1379
1379
|
return false;
|
|
1380
1380
|
}
|
|
1381
1381
|
}
|
|
1382
|
+
function safeBaseUrl(candidate, fallback, label) {
|
|
1383
|
+
if (typeof candidate !== "string" || candidate === "") return fallback;
|
|
1384
|
+
if (isSafeOrigin(candidate)) return candidate;
|
|
1385
|
+
warnOnce(label, `[patchstack] ignoring unsafe ${label} override (${candidate}): must be https (or localhost). Using the default.`);
|
|
1386
|
+
return fallback;
|
|
1387
|
+
}
|
|
1388
|
+
var warned = /* @__PURE__ */ new Set();
|
|
1389
|
+
function warnOnce(key, message) {
|
|
1390
|
+
if (warned.has(key)) return;
|
|
1391
|
+
warned.add(key);
|
|
1392
|
+
console.warn(message);
|
|
1393
|
+
}
|
|
1382
1394
|
|
|
1383
1395
|
// src/endpoint-policy.ts
|
|
1384
1396
|
function assertConnectableEndpoint(config, target = config.endpoint) {
|
|
@@ -1707,7 +1719,7 @@ async function postInputMap(config, map, buildId) {
|
|
|
1707
1719
|
}
|
|
1708
1720
|
const url = buildInputMapUrl(config.endpoint, config.siteUuid);
|
|
1709
1721
|
try {
|
|
1710
|
-
const
|
|
1722
|
+
const canonical2 = canonicalBuildId(buildId);
|
|
1711
1723
|
const response = await pulseFetch(config, url, {
|
|
1712
1724
|
method: "POST",
|
|
1713
1725
|
headers: {
|
|
@@ -1720,7 +1732,7 @@ async function postInputMap(config, map, buildId) {
|
|
|
1720
1732
|
// to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
|
|
1721
1733
|
// run could not bind the map into a pre-bundle guard — an absent field is a question the server
|
|
1722
1734
|
// answers by withholding enforceability, which is the safe direction.
|
|
1723
|
-
body: JSON.stringify(
|
|
1735
|
+
body: JSON.stringify(canonical2 === null ? map : { ...map, build_id: canonical2 }),
|
|
1724
1736
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
1725
1737
|
});
|
|
1726
1738
|
if (response.status === 404) {
|
|
@@ -2627,10 +2639,13 @@ async function resolveConfig(options) {
|
|
|
2627
2639
|
"CONFIG_INVALID"
|
|
2628
2640
|
);
|
|
2629
2641
|
}
|
|
2630
|
-
const inferred =
|
|
2631
|
-
const
|
|
2632
|
-
const
|
|
2633
|
-
const
|
|
2642
|
+
const inferred = fromEnv.environment === void 0 ? inferEnvironment(process.env, detectHostedBuilder(options.cwd)) : null;
|
|
2643
|
+
const fileLabelOverruled = fromEnv.environment === void 0 && environmentRaw !== void 0 && environmentRaw !== "production" && inferred.environment === "production";
|
|
2644
|
+
const labelled = fileLabelOverruled ? void 0 : environmentRaw;
|
|
2645
|
+
const environment = labelled ?? inferred.environment;
|
|
2646
|
+
const environmentEvidence = labelled === void 0 ? inferred.evidence : [];
|
|
2647
|
+
const environmentSource = labelled !== void 0 ? "override" : inferred.source;
|
|
2648
|
+
const ignoredFileEnvironment = fileLabelOverruled ? environmentRaw : null;
|
|
2634
2649
|
if (siteUuid !== null && siteUuid.length > 0 && !isCanonicalUuid(siteUuid)) {
|
|
2635
2650
|
throw new PatchstackError(
|
|
2636
2651
|
`Site UUID "${siteUuid}" does not look like a valid UUID.`,
|
|
@@ -2645,20 +2660,21 @@ async function resolveConfig(options) {
|
|
|
2645
2660
|
}
|
|
2646
2661
|
const apiKeyRaw = fromEnv.apiKey ?? fromSecretFile.apiKey ?? fromFile.apiKey ?? null;
|
|
2647
2662
|
const pulseAuthRaw = fromEnv.pulseAuth ?? fromSecretFile.pulseAuth ?? fromFile.pulseAuth ?? apiKeyRaw;
|
|
2648
|
-
const
|
|
2663
|
+
const identity2 = options.detectSiteIdentity === true ? await resolveSiteIdentity(options.cwd, fromEnv, fromFile) : { url: null, name: null };
|
|
2649
2664
|
const claimToken = stated(options.cliClaimToken) ?? stated(process.env.PATCHSTACK_CLAIM_TOKEN);
|
|
2650
2665
|
return {
|
|
2651
2666
|
siteUuid: siteUuid === null || siteUuid.length === 0 ? null : siteUuid,
|
|
2652
2667
|
apiKey: apiKeyRaw === null || apiKeyRaw.length === 0 ? null : apiKeyRaw,
|
|
2653
2668
|
pulseAuth: pulseAuthRaw === null || pulseAuthRaw.length === 0 ? null : pulseAuthRaw,
|
|
2654
|
-
siteUrl:
|
|
2655
|
-
siteName:
|
|
2669
|
+
siteUrl: identity2.url,
|
|
2670
|
+
siteName: identity2.name,
|
|
2656
2671
|
endpoint,
|
|
2657
2672
|
endpointTrusted,
|
|
2658
2673
|
timeoutMs,
|
|
2659
2674
|
environment,
|
|
2660
2675
|
environmentEvidence,
|
|
2661
2676
|
environmentSource,
|
|
2677
|
+
ignoredFileEnvironment,
|
|
2662
2678
|
widget: fromFile.widget !== false,
|
|
2663
2679
|
claimToken
|
|
2664
2680
|
};
|
|
@@ -2713,7 +2729,7 @@ async function writeAtomicFile(target, content, mode) {
|
|
|
2713
2729
|
throw error;
|
|
2714
2730
|
}
|
|
2715
2731
|
}
|
|
2716
|
-
async function ensureIgnored(cwd, entry) {
|
|
2732
|
+
async function ensureIgnored(cwd, entry, label = "Patchstack credential \u2014 never commit this") {
|
|
2717
2733
|
const target = path8.join(cwd, ".gitignore");
|
|
2718
2734
|
let existing = "";
|
|
2719
2735
|
try {
|
|
@@ -2727,7 +2743,7 @@ async function ensureIgnored(cwd, entry) {
|
|
|
2727
2743
|
if (ignoresEntry(existing, entry)) return { ignored: true };
|
|
2728
2744
|
const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
|
|
2729
2745
|
const block = `${separator}
|
|
2730
|
-
#
|
|
2746
|
+
# ${label}
|
|
2731
2747
|
${entry}
|
|
2732
2748
|
`;
|
|
2733
2749
|
try {
|
|
@@ -3040,6 +3056,9 @@ function buildInjectionSnippet(checksum, stack) {
|
|
|
3040
3056
|
}
|
|
3041
3057
|
return `<script ${MARKER_ATTR}>${statements.join("")}</script>`;
|
|
3042
3058
|
}
|
|
3059
|
+
function buildSandboxSnippet() {
|
|
3060
|
+
return `<script ${MARKER_ATTR}>window.__PATCHSTACK_ENV__="sandbox";</script>`;
|
|
3061
|
+
}
|
|
3043
3062
|
function injectMarker(html, snippet) {
|
|
3044
3063
|
const stripped = html.replace(
|
|
3045
3064
|
new RegExp(`\\s*<script ${MARKER_ATTR}[^>]*>[\\s\\S]*?</script>`, "gi"),
|
|
@@ -3053,6 +3072,10 @@ function injectMarker(html, snippet) {
|
|
|
3053
3072
|
}
|
|
3054
3073
|
return stripped + snippet;
|
|
3055
3074
|
}
|
|
3075
|
+
var HAND_MARKER_RE = /\s*<script(?![^>]*\bsrc\s*=)[^>]*>\s*(?:window\.__PATCHSTACK_(?:PROD|BUILD|STACK)__\s*=\s*[^;<]*;?\s*)+<\/script>/gi;
|
|
3076
|
+
function removeHandMarkers(html) {
|
|
3077
|
+
return html.replace(HAND_MARKER_RE, "");
|
|
3078
|
+
}
|
|
3056
3079
|
var JSX_SHELL_FRAMEWORKS = /* @__PURE__ */ new Set([
|
|
3057
3080
|
"next",
|
|
3058
3081
|
"remix",
|
|
@@ -3196,7 +3219,7 @@ function ensureSourceMarker(cwd, shell, framework, checksum = null) {
|
|
|
3196
3219
|
}
|
|
3197
3220
|
|
|
3198
3221
|
// src/guide.ts
|
|
3199
|
-
import { existsSync as
|
|
3222
|
+
import { existsSync as existsSync15, readFileSync as readFileSync11, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
|
|
3200
3223
|
import path12 from "path";
|
|
3201
3224
|
|
|
3202
3225
|
// src/architecture.ts
|
|
@@ -4099,18 +4122,144 @@ function templatesDir() {
|
|
|
4099
4122
|
}
|
|
4100
4123
|
|
|
4101
4124
|
// src/protect/install/adapters/tanstack-supabase.ts
|
|
4102
|
-
import { existsSync as
|
|
4125
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4126
|
+
import { join as join9 } from "path";
|
|
4127
|
+
|
|
4128
|
+
// src/protect/install/syntax.ts
|
|
4129
|
+
import { createRequire } from "module";
|
|
4103
4130
|
import { join as join6 } from "path";
|
|
4131
|
+
function sourceCompiler(cwd) {
|
|
4132
|
+
for (const base of [join6(cwd, "package.json"), import.meta.url]) {
|
|
4133
|
+
try {
|
|
4134
|
+
return createRequire(base)("typescript");
|
|
4135
|
+
} catch {
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
return null;
|
|
4139
|
+
}
|
|
4140
|
+
function parsedSource(ts, file, source) {
|
|
4141
|
+
const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
4142
|
+
return tree.parseDiagnostics.length ? null : tree;
|
|
4143
|
+
}
|
|
4144
|
+
function statementEndLine(ts, file, source, line) {
|
|
4145
|
+
const tree = parsedSource(ts, file, source);
|
|
4146
|
+
if (!tree) return null;
|
|
4147
|
+
let end = null;
|
|
4148
|
+
const visit = (node) => {
|
|
4149
|
+
if ((ts.isImportDeclaration(node) || ts.isVariableStatement(node) || ts.isExpressionStatement(node)) && tree.getLineAndCharacterOfPosition(node.getStart(tree)).line === line) {
|
|
4150
|
+
const tail = source.slice(node.end).split("\n", 1)[0].trim();
|
|
4151
|
+
if (tail !== "" && !tail.startsWith("//")) return;
|
|
4152
|
+
end = tree.getLineAndCharacterOfPosition(node.end).line;
|
|
4153
|
+
return;
|
|
4154
|
+
}
|
|
4155
|
+
ts.forEachChild(node, visit);
|
|
4156
|
+
};
|
|
4157
|
+
visit(tree);
|
|
4158
|
+
return end;
|
|
4159
|
+
}
|
|
4160
|
+
function statementPosition(ts, file, source, line, matches) {
|
|
4161
|
+
const tree = parsedSource(ts, file, source);
|
|
4162
|
+
if (!tree) return null;
|
|
4163
|
+
const statements = [];
|
|
4164
|
+
const visit = (node) => {
|
|
4165
|
+
if ((ts.isVariableStatement(node) || ts.isExpressionStatement(node)) && tree.getLineAndCharacterOfPosition(node.getStart(tree)).line === line && matches.test(node.getText(tree))) statements.push({ start: node.getStart(tree), end: node.end });
|
|
4166
|
+
ts.forEachChild(node, visit);
|
|
4167
|
+
};
|
|
4168
|
+
visit(tree);
|
|
4169
|
+
return statements.length === 1 ? statements[0] : null;
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// src/protect/install/template-match.ts
|
|
4173
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4174
|
+
import { join as join7 } from "path";
|
|
4175
|
+
function matchesGuardTemplate(cwd, file, template) {
|
|
4176
|
+
if (!existsSync4(join7(cwd, file))) return false;
|
|
4177
|
+
const ts = sourceCompiler(cwd);
|
|
4178
|
+
if (!ts) return false;
|
|
4179
|
+
const actual = canonical(ts, file, read(join7(cwd, file)));
|
|
4180
|
+
const expected = canonical(ts, template, read(join7(templatesDir(), template)));
|
|
4181
|
+
return actual !== null && expected !== null && actual === expected;
|
|
4182
|
+
}
|
|
4183
|
+
function canonical(ts, file, source) {
|
|
4184
|
+
const tree = parsedSource(ts, file, source);
|
|
4185
|
+
if (!tree) return null;
|
|
4186
|
+
const normalized2 = ts.transform(tree, [(context) => (root) => {
|
|
4187
|
+
const visit = (node) => {
|
|
4188
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === "PS_SITE_UUID" && node.initializer && ts.isStringLiteral(node.initializer)) {
|
|
4189
|
+
return ts.factory.updateVariableDeclaration(
|
|
4190
|
+
node,
|
|
4191
|
+
node.name,
|
|
4192
|
+
node.exclamationToken,
|
|
4193
|
+
node.type,
|
|
4194
|
+
ts.factory.createStringLiteral("__PATCHSTACK_SITE_UUID__")
|
|
4195
|
+
);
|
|
4196
|
+
}
|
|
4197
|
+
if (ts.isStringLiteral(node)) return ts.factory.createStringLiteral(node.text);
|
|
4198
|
+
return ts.visitEachChild(node, visit, context);
|
|
4199
|
+
};
|
|
4200
|
+
return ts.visitEachChild(root, visit, context);
|
|
4201
|
+
}]);
|
|
4202
|
+
try {
|
|
4203
|
+
return ts.createPrinter({ removeComments: true }).printFile(normalized2.transformed[0]);
|
|
4204
|
+
} finally {
|
|
4205
|
+
normalized2.dispose();
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
|
|
4209
|
+
// src/protect/install/template-upgrade.ts
|
|
4210
|
+
import { createHash as createHash2 } from "crypto";
|
|
4211
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4212
|
+
import { join as join8 } from "path";
|
|
4213
|
+
var PREVIOUS = {
|
|
4214
|
+
"generic-guard.ts": "2c6bb8324b909e646049c36725704b474a8bfb1b6849c392eb3462eed83727e9",
|
|
4215
|
+
"generic-guard.js": "42357bcf0cb0541e584821d03e06ced28f98d4ef91a76a50e39008ef18c63709",
|
|
4216
|
+
"generic-guard.cjs": "8de21893474a3139eff9e8fc48a0c13e976e64421281db9e8e93b0c0549361b8",
|
|
4217
|
+
"express-guard.ts": "6c30cc82b7cbd3fee62d423fcb20b27eba1d6cc1ed054efcf9d64eb360bc7b17",
|
|
4218
|
+
"express-guard.js": "bce7c76a061297621791d4ebf838449e3880d05f333588fed7bf88431d1e712e",
|
|
4219
|
+
"express-guard.cjs": "3a6430f00fc159c28a897e700cc492c306f39b556f0f4cb4b1e718993b6c40b4",
|
|
4220
|
+
"fastify-plugin.ts": "5d606cb8f8f2e6fe35b9d377c5568c5a2090a81af9f0acb219ebc15a713792e9",
|
|
4221
|
+
"fastify-plugin.js": "03327d437513571d53554ca34c84ba170ebeebb5e89c2602ae340fb82178ab48",
|
|
4222
|
+
"fastify-plugin.cjs": "b3754bf023ebb5f2a22e0a95cfe74bb783034ec3db9ce61a3e7202237e09e1dd",
|
|
4223
|
+
"guard.ts": "270e29769f5a7b3ab70ee39970be5a4e150e1b3949877687dca1ed83596afae7",
|
|
4224
|
+
"sveltekit-hooks.ts": "0a68389ac9018dc0ab5805356ad78301f039cb4c701bf642e7f986cc959da990",
|
|
4225
|
+
"astro-middleware.ts": "f217e302fb7019de451c7538e9266a5ce38aa6a6651072b6291d4593b31f60ce",
|
|
4226
|
+
"nuxt-middleware.ts": "e377c48cc8c08d20c61c6119bb5f4c8991f0eed3ebd31310b8a0fc6eb5505e4e",
|
|
4227
|
+
"next-middleware.ts": "65c163cdb2ddda4fe53c1a3cd20aa6ab63ad7595a66985d9141d595374ecfab3",
|
|
4228
|
+
"next-guard.ts": "e37c4d67619df9d850a7d94b567bd76841b6ac8e774a5b442689482f5374c451"
|
|
4229
|
+
};
|
|
4230
|
+
var identity = /const PS_SITE_UUID = "([^"]*)";/;
|
|
4231
|
+
var normalized = (source) => source.replace(identity, 'const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";');
|
|
4232
|
+
function installTemplate(cwd, file, template) {
|
|
4233
|
+
const next = read(join8(templatesDir(), template));
|
|
4234
|
+
const target = join8(cwd, file);
|
|
4235
|
+
if (!existsSync5(target)) {
|
|
4236
|
+
writeProjectFileSync(cwd, target, next);
|
|
4237
|
+
return true;
|
|
4238
|
+
}
|
|
4239
|
+
const previous = read(target);
|
|
4240
|
+
if (normalized(previous) === normalized(next)) return false;
|
|
4241
|
+
if (createHash2("sha256").update(normalized(previous)).digest("hex") !== PREVIOUS[template]) {
|
|
4242
|
+
log(`${file} is customized or unrecognized \u2014 preserved; review its request/response wiring and rule-refresh settings manually.`);
|
|
4243
|
+
return false;
|
|
4244
|
+
}
|
|
4245
|
+
const site = identity.exec(previous)?.[1];
|
|
4246
|
+
writeProjectFileSync(cwd, target, site ? next.replace(identity, () => `const PS_SITE_UUID = ${JSON.stringify(site)};`) : next);
|
|
4247
|
+
log(`updated unchanged generated helper ${file}`);
|
|
4248
|
+
return true;
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
// src/protect/install/adapters/tanstack-supabase.ts
|
|
4104
4252
|
var CLIENT_TUNNEL = [
|
|
4105
4253
|
"",
|
|
4106
4254
|
" // PATCHSTACK: in the browser, tunnel Supabase traffic through the app's own server guard",
|
|
4107
4255
|
" // (same-origin) so payloads are inspected before they reach Supabase.",
|
|
4108
4256
|
" if (typeof window !== 'undefined') {",
|
|
4109
|
-
" const
|
|
4110
|
-
" const method = init?.method ?? (input instanceof Request ? input.method : 'GET');",
|
|
4111
|
-
" headers.set('x-ps-target', target);",
|
|
4257
|
+
" const original = new Request(input, init);",
|
|
4112
4258
|
" const guardUrl = new URL('/_patchstack/guard', window.location.origin).toString();",
|
|
4113
|
-
"
|
|
4259
|
+
" const forwarded = new Request(guardUrl, original);",
|
|
4260
|
+
" headers.forEach((value, key) => forwarded.headers.set(key, value));",
|
|
4261
|
+
" forwarded.headers.set('x-ps-target', original.url);",
|
|
4262
|
+
" return fetch(forwarded);",
|
|
4114
4263
|
" }",
|
|
4115
4264
|
""
|
|
4116
4265
|
].join("\n");
|
|
@@ -4119,19 +4268,19 @@ var GUARD_IMPORT_RE = /import \{[^}]*\} from "@\/integrations\/patchstack\/guard
|
|
|
4119
4268
|
var START_IMPORTS = ['import { getRequest } from "@tanstack/react-start/server";', GUARD_IMPORT].join("\n");
|
|
4120
4269
|
var REQUEST_MIDDLEWARE_BLOCK = [
|
|
4121
4270
|
"// #region patchstack-guard (managed by patchstack-connect protect \u2014 do not edit)",
|
|
4122
|
-
"//
|
|
4271
|
+
"// Screen native requests and responses as well as the browser\u2192Supabase tunnel.",
|
|
4123
4272
|
"const patchstackGuard = createMiddleware().server(async ({ next }) => {",
|
|
4124
4273
|
" const request = getRequest();",
|
|
4125
4274
|
" if (request) {",
|
|
4126
4275
|
" const { pathname } = new URL(request.url);",
|
|
4127
4276
|
" if (pathname === GUARD_PATH) return handleGuardRequest(request);",
|
|
4128
|
-
|
|
4129
|
-
"
|
|
4130
|
-
" if (blocked) return blocked;",
|
|
4131
|
-
" }",
|
|
4277
|
+
" const blocked = await guardRequest(request);",
|
|
4278
|
+
" if (blocked) return blocked;",
|
|
4132
4279
|
" }",
|
|
4133
4280
|
" // The request is passed so route/method-scoped response rules can apply their scope.",
|
|
4134
|
-
"
|
|
4281
|
+
" const result = await next();",
|
|
4282
|
+
" if (result instanceof Response) return screenResponse(result, request);",
|
|
4283
|
+
" return { ...result, response: await screenResponse(result.response, request) };",
|
|
4135
4284
|
"});",
|
|
4136
4285
|
"// #endregion patchstack-guard"
|
|
4137
4286
|
].join("\n");
|
|
@@ -4170,8 +4319,8 @@ function reconcileBlock(s, region, block, legacyConst, insertBefore) {
|
|
|
4170
4319
|
return s.replace(insertBefore, block + "\n\n" + insertBefore);
|
|
4171
4320
|
}
|
|
4172
4321
|
function detect(cwd) {
|
|
4173
|
-
const pkgPath =
|
|
4174
|
-
if (!
|
|
4322
|
+
const pkgPath = join9(cwd, "package.json");
|
|
4323
|
+
if (!existsSync6(pkgPath)) return false;
|
|
4175
4324
|
let pkg;
|
|
4176
4325
|
try {
|
|
4177
4326
|
pkg = JSON.parse(read(pkgPath));
|
|
@@ -4179,22 +4328,22 @@ function detect(cwd) {
|
|
|
4179
4328
|
return false;
|
|
4180
4329
|
}
|
|
4181
4330
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
4182
|
-
return Boolean(deps["@tanstack/react-start"]) &&
|
|
4331
|
+
return Boolean(deps["@tanstack/react-start"]) && existsSync6(join9(cwd, "src/start.ts")) && existsSync6(join9(cwd, "src/integrations/supabase/client.ts"));
|
|
4183
4332
|
}
|
|
4184
4333
|
var GUARD_FILE = "src/integrations/patchstack/guard.ts";
|
|
4185
4334
|
function scaffold(cwd, opts) {
|
|
4186
4335
|
const templates = templatesDir();
|
|
4187
|
-
const dst =
|
|
4336
|
+
const dst = join9(cwd, "src/integrations/patchstack");
|
|
4188
4337
|
ensureProjectDirectorySync(cwd, dst);
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
const rulesDst =
|
|
4338
|
+
const changed = [];
|
|
4339
|
+
if (installTemplate(cwd, GUARD_FILE, "guard.ts")) changed.push(GUARD_FILE);
|
|
4340
|
+
const rulesDst = join9(dst, "rules.json");
|
|
4192
4341
|
if (opts.demo) {
|
|
4193
|
-
copyProjectFileSync(cwd,
|
|
4342
|
+
copyProjectFileSync(cwd, join9(templates, "demo-rules.json"), rulesDst);
|
|
4194
4343
|
changed.push("src/integrations/patchstack/rules.json");
|
|
4195
4344
|
log("scaffolded guard.ts + rules.json (demo sample rule set)");
|
|
4196
|
-
} else if (!
|
|
4197
|
-
copyProjectFileSync(cwd,
|
|
4345
|
+
} else if (!existsSync6(rulesDst)) {
|
|
4346
|
+
copyProjectFileSync(cwd, join9(templates, "rules.json"), rulesDst);
|
|
4198
4347
|
changed.push("src/integrations/patchstack/rules.json");
|
|
4199
4348
|
log("scaffolded guard.ts + rules.json (starter rules)");
|
|
4200
4349
|
} else {
|
|
@@ -4202,37 +4351,74 @@ function scaffold(cwd, opts) {
|
|
|
4202
4351
|
}
|
|
4203
4352
|
return changed;
|
|
4204
4353
|
}
|
|
4205
|
-
function patchClient(
|
|
4206
|
-
const
|
|
4207
|
-
|
|
4354
|
+
function patchClient(ts, s) {
|
|
4355
|
+
const tree = parsedSource(ts, "client.ts", s);
|
|
4356
|
+
if (!tree) return null;
|
|
4357
|
+
const anchors = [];
|
|
4358
|
+
const visit = (node) => {
|
|
4359
|
+
if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression)) {
|
|
4360
|
+
const call = node.expression;
|
|
4361
|
+
if (call.expression.getText(tree) === "headers.set" && call.arguments[0] && ts.isStringLiteral(call.arguments[0]) && call.arguments[0].text === "apikey" && call.arguments[1]?.getText(tree) === "supabaseKey") {
|
|
4362
|
+
const block = node.parent;
|
|
4363
|
+
const fn = block.parent;
|
|
4364
|
+
if (ts.isBlock(block) && ts.isArrowFunction(fn) && fn.parameters.length === 2 && fn.parameters[0].name.getText(tree) === "input" && fn.parameters[1].name.getText(tree) === "init" && block.statements.some((statement) => ts.isVariableStatement(statement) && statement.declarationList.declarations.some((d) => ts.isIdentifier(d.name) && d.name.text === "headers" && d.initializer && ts.isNewExpression(d.initializer) && d.initializer.expression.getText(tree) === "Headers"))) anchors.push(node.end);
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
ts.forEachChild(node, visit);
|
|
4368
|
+
};
|
|
4369
|
+
visit(tree);
|
|
4370
|
+
if (anchors.length !== 1) return null;
|
|
4208
4371
|
if (s.includes("x-ps-target")) {
|
|
4209
|
-
|
|
4210
|
-
return
|
|
4211
|
-
}
|
|
4212
|
-
const anchor = "headers.set('apikey', supabaseKey);";
|
|
4213
|
-
if (!s.includes(anchor)) {
|
|
4214
|
-
log("client.ts anchor not found \u2014 skipping (template changed?)");
|
|
4215
|
-
return false;
|
|
4372
|
+
const code = ts.createPrinter({ removeComments: true }).printFile(tree);
|
|
4373
|
+
return /\.set\(['"]x-ps-target['"],/.test(code) && /return fetch\(/.test(code) ? s : null;
|
|
4216
4374
|
}
|
|
4217
|
-
|
|
4218
|
-
log("patched client.ts (tunnel Supabase through the guard)");
|
|
4219
|
-
return true;
|
|
4375
|
+
return s.slice(0, anchors[0]) + "\n" + CLIENT_TUNNEL + s.slice(anchors[0]);
|
|
4220
4376
|
}
|
|
4221
|
-
function patchStart(
|
|
4222
|
-
|
|
4223
|
-
let s = read(p);
|
|
4377
|
+
function patchStart(ts, original) {
|
|
4378
|
+
let s = original;
|
|
4224
4379
|
const importAnchor = 'import { createStart, createMiddleware } from "@tanstack/react-start";';
|
|
4225
4380
|
const exportAnchor = "export const startInstance";
|
|
4226
4381
|
const rmAnchor = "requestMiddleware: [";
|
|
4382
|
+
const tree = parsedSource(ts, "start.ts", s);
|
|
4383
|
+
if (!tree) return null;
|
|
4384
|
+
const startImport = tree.statements.find((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start" && !node.importClause?.isTypeOnly && !node.importClause?.name && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.length === 2 && node.importClause.namedBindings.elements.every((e) => !e.propertyName && !e.isTypeOnly && ["createStart", "createMiddleware"].includes(e.name.text)));
|
|
4385
|
+
if (!startImport) return null;
|
|
4386
|
+
const declaration = tree.statements.filter(ts.isVariableStatement).filter((node) => node.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)).flatMap((node) => [...node.declarationList.declarations]).find((node) => ts.isIdentifier(node.name) && node.name.text === "startInstance");
|
|
4387
|
+
const call = declaration?.initializer;
|
|
4388
|
+
if (!call || !ts.isCallExpression(call) || call.expression.getText(tree) !== "createStart" || call.arguments.length !== 1) return null;
|
|
4389
|
+
const factory = call.arguments[0];
|
|
4390
|
+
if (!ts.isArrowFunction(factory) || factory.parameters.length) return null;
|
|
4391
|
+
let config = factory.body;
|
|
4392
|
+
while (ts.isParenthesizedExpression(config)) config = config.expression;
|
|
4393
|
+
if (!ts.isObjectLiteralExpression(config)) return null;
|
|
4394
|
+
if (config.properties.some((p) => !ts.isPropertyAssignment(p) || !ts.isIdentifier(p.name))) return null;
|
|
4395
|
+
const edits = [{ start: startImport.getStart(tree), end: startImport.end, text: importAnchor }];
|
|
4396
|
+
for (const name of ["requestMiddleware", "functionMiddleware"]) {
|
|
4397
|
+
const properties = config.properties.filter((p) => p.name?.getText(tree) === name);
|
|
4398
|
+
if (properties.length > 1 || name === "requestMiddleware" && !properties.length) return null;
|
|
4399
|
+
const property = properties[0];
|
|
4400
|
+
if (!property || !ts.isPropertyAssignment(property)) continue;
|
|
4401
|
+
if (!ts.isArrayLiteralExpression(property.initializer)) return null;
|
|
4402
|
+
edits.push({ start: property.getStart(tree), end: property.initializer.getStart(tree) + 1, text: `${name}: [` });
|
|
4403
|
+
}
|
|
4404
|
+
const guardImports = tree.statements.filter((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@/integrations/patchstack/guard");
|
|
4405
|
+
if (guardImports.length > 1) return null;
|
|
4406
|
+
const guardImport = guardImports[0];
|
|
4407
|
+
if (guardImport) {
|
|
4408
|
+
if (!ts.isImportDeclaration(guardImport) || !guardImport.importClause?.namedBindings || !ts.isNamedImports(guardImport.importClause.namedBindings) || guardImport.importClause.namedBindings.elements.some((e) => e.propertyName || e.isTypeOnly || !["GUARD_PATH", "handleGuardRequest", "inspectServerFn", "screenResponse", "guardRequest"].includes(e.name.text))) return null;
|
|
4409
|
+
edits.push({ start: guardImport.getStart(tree), end: guardImport.end, text: GUARD_IMPORT });
|
|
4410
|
+
}
|
|
4411
|
+
for (const edit of edits.sort((a, b) => b.start - a.start)) s = s.slice(0, edit.start) + edit.text + s.slice(edit.end);
|
|
4227
4412
|
if (!s.includes(importAnchor) || !s.includes(exportAnchor)) {
|
|
4228
|
-
|
|
4229
|
-
return false;
|
|
4413
|
+
return null;
|
|
4230
4414
|
}
|
|
4231
|
-
|
|
4415
|
+
if (!s.includes(rmAnchor)) return null;
|
|
4232
4416
|
if (GUARD_IMPORT_RE.test(s)) {
|
|
4233
4417
|
s = s.replace(GUARD_IMPORT_RE, GUARD_IMPORT);
|
|
4234
4418
|
} else {
|
|
4235
|
-
|
|
4419
|
+
const requestImport = tree.statements.some((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start/server" && !node.importClause?.isTypeOnly && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.some((e) => !e.propertyName && !e.isTypeOnly && e.name.text === "getRequest"));
|
|
4420
|
+
if (!requestImport && /\bgetRequest\b/.test(original)) return null;
|
|
4421
|
+
s = s.replace(importAnchor, importAnchor + "\n" + (requestImport ? GUARD_IMPORT : START_IMPORTS));
|
|
4236
4422
|
}
|
|
4237
4423
|
s = reconcileBlock(s, "patchstack-guard", REQUEST_MIDDLEWARE_BLOCK, "const patchstackGuard =", exportAnchor);
|
|
4238
4424
|
s = reconcileBlock(s, "patchstack-function-guard", FUNCTION_MIDDLEWARE_BLOCK, "const patchstackFunctionGuard =", exportAnchor);
|
|
@@ -4247,34 +4433,56 @@ function patchStart(cwd) {
|
|
|
4247
4433
|
s = s.replace(rmAnchor, "functionMiddleware: [patchstackFunctionGuard],\n " + rmAnchor);
|
|
4248
4434
|
}
|
|
4249
4435
|
}
|
|
4250
|
-
|
|
4251
|
-
log("start.ts already wired");
|
|
4252
|
-
return false;
|
|
4253
|
-
}
|
|
4254
|
-
writeProjectFileSync(cwd, p, s, { encoding: "utf8" });
|
|
4255
|
-
log("patched start.ts (guard registered as request + function middleware)");
|
|
4256
|
-
return true;
|
|
4436
|
+
return parsedSource(ts, "start.ts", s) ? s : null;
|
|
4257
4437
|
}
|
|
4258
4438
|
function wire(cwd, opts) {
|
|
4439
|
+
const ts = sourceCompiler(cwd);
|
|
4440
|
+
const clientPath = join9(cwd, "src/integrations/supabase/client.ts");
|
|
4441
|
+
const startPath = join9(cwd, "src/start.ts");
|
|
4442
|
+
const oldClient = read(clientPath);
|
|
4443
|
+
const oldStart = read(startPath);
|
|
4444
|
+
const client = ts && patchClient(ts, oldClient);
|
|
4445
|
+
const start = ts && patchStart(ts, oldStart);
|
|
4446
|
+
if (!ts || !client || !start || !parsedSource(ts, "client.ts", client)) {
|
|
4447
|
+
log("TanStack wiring needs manual integration: client and server left untouched; no browser traffic was redirected.");
|
|
4448
|
+
return { ok: false, changed: [] };
|
|
4449
|
+
}
|
|
4259
4450
|
const changed = scaffold(cwd, opts);
|
|
4451
|
+
if (!matchesGuardTemplate(cwd, GUARD_FILE, "guard.ts")) {
|
|
4452
|
+
log("Custom guard helper needs manual review; client and server entries were left untouched.");
|
|
4453
|
+
return { ok: false, changed };
|
|
4454
|
+
}
|
|
4260
4455
|
if (!opts.demo && bakeSiteUuid(cwd, GUARD_FILE)) changed.push(GUARD_FILE);
|
|
4261
|
-
|
|
4262
|
-
|
|
4456
|
+
try {
|
|
4457
|
+
if (start !== oldStart) {
|
|
4458
|
+
writeProjectFileSync(cwd, startPath, start);
|
|
4459
|
+
changed.push("src/start.ts");
|
|
4460
|
+
}
|
|
4461
|
+
if (client !== oldClient) {
|
|
4462
|
+
writeProjectFileSync(cwd, clientPath, client);
|
|
4463
|
+
changed.push("src/integrations/supabase/client.ts");
|
|
4464
|
+
}
|
|
4465
|
+
} catch (error) {
|
|
4466
|
+
if (start !== oldStart) writeProjectFileSync(cwd, startPath, oldStart);
|
|
4467
|
+
throw error;
|
|
4468
|
+
}
|
|
4263
4469
|
log(
|
|
4264
4470
|
opts.demo ? "done \u2014 guard wired with the demo sample rules (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only." : "done \u2014 guard wired and always-on (blocks by default). Set PATCHSTACK_MODE=dry-run for log-only."
|
|
4265
4471
|
);
|
|
4266
4472
|
return { ok: true, changed: [...new Set(changed)] };
|
|
4267
4473
|
}
|
|
4268
4474
|
function verify(cwd) {
|
|
4269
|
-
const guardPath =
|
|
4270
|
-
const clientPath =
|
|
4271
|
-
const startPath =
|
|
4272
|
-
const guard =
|
|
4273
|
-
const client =
|
|
4274
|
-
const start =
|
|
4475
|
+
const guardPath = join9(cwd, GUARD_FILE);
|
|
4476
|
+
const clientPath = join9(cwd, "src/integrations/supabase/client.ts");
|
|
4477
|
+
const startPath = join9(cwd, "src/start.ts");
|
|
4478
|
+
const guard = existsSync6(guardPath) ? read(guardPath) : "";
|
|
4479
|
+
const client = existsSync6(clientPath) ? read(clientPath) : "";
|
|
4480
|
+
const start = existsSync6(startPath) ? read(startPath) : "";
|
|
4481
|
+
const ts = sourceCompiler(cwd);
|
|
4275
4482
|
const checks = [
|
|
4276
4483
|
{ label: "guard.ts scaffolded", ok: guard.length > 0, hint: "run `patchstack-connect protect`" },
|
|
4277
|
-
{ label: "
|
|
4484
|
+
{ label: "guard helper implementation verified", ok: matchesGuardTemplate(cwd, GUARD_FILE, "guard.ts"), hint: "preserved custom helpers require manual review before redirecting browser traffic" },
|
|
4485
|
+
{ label: "Supabase client tunnels through the guard", ok: !!ts && client.includes("x-ps-target") && patchClient(ts, client) === client, hint: "run `patchstack-connect protect` to re-patch src/integrations/supabase/client.ts" },
|
|
4278
4486
|
{ label: "request middleware defined + registered", ok: start.includes("const patchstackGuard =") && start.includes("requestMiddleware: [patchstackGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" },
|
|
4279
4487
|
{ label: "server-function middleware defined + registered", ok: start.includes("const patchstackFunctionGuard =") && start.includes("functionMiddleware: [patchstackFunctionGuard"), hint: "run `patchstack-connect protect` to re-patch src/start.ts" }
|
|
4280
4488
|
];
|
|
@@ -4288,20 +4496,189 @@ var tanstackSupabaseAdapter = {
|
|
|
4288
4496
|
verify
|
|
4289
4497
|
};
|
|
4290
4498
|
|
|
4499
|
+
// src/protect/install/adapters/tanstack.ts
|
|
4500
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4501
|
+
import { createRequire as createRequire2 } from "module";
|
|
4502
|
+
import { join as join10 } from "path";
|
|
4503
|
+
var ENTRY = "src/server.ts";
|
|
4504
|
+
var GUARD = "src/patchstack/guard.ts";
|
|
4505
|
+
var IMPORT = 'import { protectFetch } from "./patchstack/guard";';
|
|
4506
|
+
var DEFAULT = `import handler, { createServerEntry } from '@tanstack/react-start/server-entry';
|
|
4507
|
+
export default createServerEntry({ fetch: handler.fetch.bind(handler) });
|
|
4508
|
+
`;
|
|
4509
|
+
var EXTENSIONS = ["ts", "js", "mts", "mjs", "tsx", "jsx", "cts", "cjs"];
|
|
4510
|
+
function defaultEntryConfig(ts, file, source) {
|
|
4511
|
+
const tree = parsedSource(ts, file, source);
|
|
4512
|
+
if (!tree) return false;
|
|
4513
|
+
const binding = (module, name) => tree.statements.filter(ts.isImportDeclaration).filter((n) => ts.isStringLiteral(n.moduleSpecifier) && n.moduleSpecifier.text === module && !n.importClause?.isTypeOnly).flatMap((n) => n.importClause?.namedBindings && ts.isNamedImports(n.importClause.namedBindings) ? n.importClause.namedBindings.elements.filter((e) => !e.isTypeOnly && (e.propertyName ?? e.name).text === name).map((e) => e.name.text) : []);
|
|
4514
|
+
const defineConfig = binding("vite", "defineConfig");
|
|
4515
|
+
const start = binding("@tanstack/react-start/plugin/vite", "tanstackStart");
|
|
4516
|
+
const unwrap = (expression) => {
|
|
4517
|
+
while (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression) || ts.isSatisfiesExpression(expression)) expression = expression.expression;
|
|
4518
|
+
return expression;
|
|
4519
|
+
};
|
|
4520
|
+
const exports = tree.statements.filter(ts.isExportAssignment);
|
|
4521
|
+
if (exports.length !== 1 || exports[0].isExportEquals) return false;
|
|
4522
|
+
let config = unwrap(exports[0].expression);
|
|
4523
|
+
if (ts.isCallExpression(config)) {
|
|
4524
|
+
if (!ts.isIdentifier(config.expression) || !defineConfig.includes(config.expression.text) || config.arguments.length !== 1) return false;
|
|
4525
|
+
config = unwrap(config.arguments[0]);
|
|
4526
|
+
}
|
|
4527
|
+
if (!ts.isObjectLiteralExpression(config)) return false;
|
|
4528
|
+
let unsafe = false;
|
|
4529
|
+
const visit = (node) => {
|
|
4530
|
+
if (ts.isSpreadAssignment(node) || ts.isSpreadElement(node) || ts.isComputedPropertyName(node)) unsafe = true;
|
|
4531
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
4532
|
+
const names = /* @__PURE__ */ new Set();
|
|
4533
|
+
for (const property of node.properties) {
|
|
4534
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name)) {
|
|
4535
|
+
unsafe = true;
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
const name = property.name.text;
|
|
4539
|
+
if (names.has(name) || ["srcDirectory", "serverEntry", "entry", "server", "root"].includes(name)) unsafe = true;
|
|
4540
|
+
names.add(name);
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
ts.forEachChild(node, visit);
|
|
4544
|
+
};
|
|
4545
|
+
visit(config);
|
|
4546
|
+
if (unsafe) return false;
|
|
4547
|
+
const plugins = config.properties.find((p) => ts.isPropertyAssignment(p) && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) && p.name.text === "plugins");
|
|
4548
|
+
if (!plugins || !ts.isPropertyAssignment(plugins) || !ts.isArrayLiteralExpression(plugins.initializer)) return false;
|
|
4549
|
+
const calls = plugins.initializer.elements.map(unwrap).filter((n) => ts.isCallExpression(n) && ts.isIdentifier(n.expression) && start.includes(n.expression.text));
|
|
4550
|
+
if (calls.length !== 1) return false;
|
|
4551
|
+
const call = calls[0];
|
|
4552
|
+
return ts.isCallExpression(call) && (call.arguments.length === 0 || call.arguments.length === 1 && ts.isObjectLiteralExpression(unwrap(call.arguments[0])));
|
|
4553
|
+
}
|
|
4554
|
+
function entryProblem(cwd) {
|
|
4555
|
+
try {
|
|
4556
|
+
const file = createRequire2(join10(cwd, "package.json")).resolve("@tanstack/react-start/package.json");
|
|
4557
|
+
if (!JSON.parse(read(file)).exports?.["./server-entry"]) return "the installed TanStack Start version does not expose server-entry";
|
|
4558
|
+
} catch {
|
|
4559
|
+
return "the installed TanStack Start version must expose its server-entry contract";
|
|
4560
|
+
}
|
|
4561
|
+
const entries = ["src/server", "server", ...EXTENSIONS.flatMap((ext) => [`src/server.${ext}`, `server.${ext}`])];
|
|
4562
|
+
if (entries.some((file) => file !== ENTRY && existsSync7(join10(cwd, file)))) {
|
|
4563
|
+
return "non-default or competing server entries require manual integration";
|
|
4564
|
+
}
|
|
4565
|
+
const configs = ["vite", "app", "rsbuild"].flatMap((name) => EXTENSIONS.map((ext) => `${name}.config.${ext}`)).filter((file) => existsSync7(join10(cwd, file)));
|
|
4566
|
+
if (configs.length) {
|
|
4567
|
+
const ts = sourceCompiler(cwd);
|
|
4568
|
+
if (configs.length !== 1 || !configs[0].startsWith("vite.") || !ts || !defaultEntryConfig(ts, configs[0], read(join10(cwd, configs[0])))) {
|
|
4569
|
+
return "the default server entry cannot be established from this configuration; integrate it manually";
|
|
4570
|
+
}
|
|
4571
|
+
}
|
|
4572
|
+
return null;
|
|
4573
|
+
}
|
|
4574
|
+
function composeTanstackEntry(ts, source) {
|
|
4575
|
+
const tree = parsedSource(ts, ENTRY, source);
|
|
4576
|
+
if (!tree || /\bprotectFetch\b/.test(source)) return null;
|
|
4577
|
+
const imported = tree.statements.some((node) => ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@tanstack/react-start/server-entry" && !node.importClause?.isTypeOnly && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && node.importClause.namedBindings.elements.some((e) => !e.propertyName && !e.isTypeOnly && e.name.text === "createServerEntry"));
|
|
4578
|
+
if (!imported) return null;
|
|
4579
|
+
const exports = tree.statements.filter(ts.isExportAssignment);
|
|
4580
|
+
if (exports.length !== 1 || exports[0].isExportEquals) return null;
|
|
4581
|
+
const call = exports[0].expression;
|
|
4582
|
+
if (!ts.isCallExpression(call) || call.expression.getText(tree) !== "createServerEntry" || call.arguments.length !== 1) return null;
|
|
4583
|
+
const object = call.arguments[0];
|
|
4584
|
+
if (!ts.isObjectLiteralExpression(object) || object.properties.some((p) => ts.isSpreadAssignment(p) || !p.name || ts.isComputedPropertyName(p.name))) return null;
|
|
4585
|
+
const properties = object.properties.filter((p) => p.name?.getText(tree) === "fetch");
|
|
4586
|
+
if (properties.length !== 1) return null;
|
|
4587
|
+
const fetch2 = properties[0];
|
|
4588
|
+
let replacement;
|
|
4589
|
+
if (ts.isPropertyAssignment(fetch2)) replacement = `fetch: protectFetch(${fetch2.initializer.getText(tree)})`;
|
|
4590
|
+
else if (ts.isMethodDeclaration(fetch2) && fetch2.body && !fetch2.asteriskToken) {
|
|
4591
|
+
const text = fetch2.getText(tree);
|
|
4592
|
+
replacement = `fetch: protectFetch(${text.replace(/^(async\s+)?fetch\s*\(/, "$1function (")})`;
|
|
4593
|
+
} else if (ts.isShorthandPropertyAssignment(fetch2)) replacement = "fetch: protectFetch(fetch)";
|
|
4594
|
+
else return null;
|
|
4595
|
+
const patched = source.slice(0, fetch2.getStart(tree)) + replacement + source.slice(fetch2.end);
|
|
4596
|
+
const insertion = tree.statements.filter(ts.isImportDeclaration).at(-1)?.end ?? 0;
|
|
4597
|
+
const result = patched.slice(0, insertion) + "\n" + IMPORT + "\n" + patched.slice(insertion);
|
|
4598
|
+
return parsedSource(ts, ENTRY, result) ? result : null;
|
|
4599
|
+
}
|
|
4600
|
+
function wiredEntry(ts, source) {
|
|
4601
|
+
const tree = parsedSource(ts, ENTRY, source);
|
|
4602
|
+
if (!tree) return false;
|
|
4603
|
+
const imported = tree.statements.filter(ts.isImportDeclaration).filter((n) => n.getText(tree) === IMPORT);
|
|
4604
|
+
if (imported.length !== 1) return false;
|
|
4605
|
+
const wrappers = [];
|
|
4606
|
+
const visit = (node) => {
|
|
4607
|
+
if (ts.isCallExpression(node) && node.expression.getText(tree) === "protectFetch") wrappers.push(node);
|
|
4608
|
+
ts.forEachChild(node, visit);
|
|
4609
|
+
};
|
|
4610
|
+
visit(tree);
|
|
4611
|
+
if (wrappers.length !== 1 || wrappers[0].arguments.length !== 1) return false;
|
|
4612
|
+
const wrapper = wrappers[0];
|
|
4613
|
+
const plain = source.slice(0, wrapper.getStart(tree)) + wrapper.arguments[0].getText(tree) + source.slice(wrapper.end);
|
|
4614
|
+
const composed = composeTanstackEntry(ts, plain.replace(IMPORT, ""));
|
|
4615
|
+
const reparsed = composed && parsedSource(ts, ENTRY, composed);
|
|
4616
|
+
const printer = ts.createPrinter({ removeComments: true });
|
|
4617
|
+
return !!reparsed && printer.printFile(reparsed) === printer.printFile(tree);
|
|
4618
|
+
}
|
|
4619
|
+
var tanstackAdapter = {
|
|
4620
|
+
name: "tanstack-start",
|
|
4621
|
+
label: "TanStack Start",
|
|
4622
|
+
detect: (cwd) => hasDependency(cwd, "@tanstack/react-start"),
|
|
4623
|
+
wire(cwd, opts) {
|
|
4624
|
+
const problem = entryProblem(cwd);
|
|
4625
|
+
const ts = sourceCompiler(cwd);
|
|
4626
|
+
if (problem || !ts) {
|
|
4627
|
+
log(`TanStack server entry left untouched: ${problem ?? "parser unavailable"}.`);
|
|
4628
|
+
return { ok: false, changed: [] };
|
|
4629
|
+
}
|
|
4630
|
+
const previous = existsSync7(join10(cwd, ENTRY)) ? read(join10(cwd, ENTRY)) : DEFAULT;
|
|
4631
|
+
const next = wiredEntry(ts, previous) ? previous : composeTanstackEntry(ts, previous);
|
|
4632
|
+
if (!next) {
|
|
4633
|
+
log("Custom TanStack server entry left untouched; wrap its final Fetch boundary manually.");
|
|
4634
|
+
return { ok: false, changed: [] };
|
|
4635
|
+
}
|
|
4636
|
+
const changed = [];
|
|
4637
|
+
ensureProjectDirectorySync(cwd, join10(cwd, "src/patchstack"));
|
|
4638
|
+
if (installTemplate(cwd, GUARD, "fetch-guard.ts")) changed.push(GUARD);
|
|
4639
|
+
if (!matchesGuardTemplate(cwd, GUARD, "fetch-guard.ts")) {
|
|
4640
|
+
log("Custom Fetch helper needs manual review; the server entry was left untouched.");
|
|
4641
|
+
return { ok: false, changed };
|
|
4642
|
+
}
|
|
4643
|
+
const rules = "src/patchstack/rules.json";
|
|
4644
|
+
if (opts.demo || !existsSync7(join10(cwd, rules))) {
|
|
4645
|
+
copyProjectFileSync(cwd, join10(templatesDir(), opts.demo ? "demo-rules.json" : "rules.json"), join10(cwd, rules));
|
|
4646
|
+
changed.push(rules);
|
|
4647
|
+
}
|
|
4648
|
+
if (!opts.demo && bakeSiteUuid(cwd, GUARD)) changed.push(GUARD);
|
|
4649
|
+
if (next !== previous || !existsSync7(join10(cwd, ENTRY))) {
|
|
4650
|
+
writeProjectFileSync(cwd, join10(cwd, ENTRY), next);
|
|
4651
|
+
changed.push(ENTRY);
|
|
4652
|
+
}
|
|
4653
|
+
log("TanStack server entry screens requests and final Responses; browser-direct backend traffic and separately deployed functions need their own protection.");
|
|
4654
|
+
return { ok: true, changed };
|
|
4655
|
+
},
|
|
4656
|
+
verify(cwd) {
|
|
4657
|
+
const ts = sourceCompiler(cwd);
|
|
4658
|
+
const problem = entryProblem(cwd);
|
|
4659
|
+
const wired = !problem && !!ts && existsSync7(join10(cwd, ENTRY)) && wiredEntry(ts, read(join10(cwd, ENTRY))) && matchesGuardTemplate(cwd, GUARD, "fetch-guard.ts") && existsSync7(join10(cwd, "src/patchstack/rules.json"));
|
|
4660
|
+
return { wired, checks: [
|
|
4661
|
+
{ label: "TanStack server Fetch boundary wired", ok: wired, hint: problem ?? "run protect; manually review unsupported custom server entries" },
|
|
4662
|
+
{ label: "browser-direct services and separately deployed functions are not covered by this entry", ok: true, unverifiable: true, hint: "install protection at each independently exposed backend; retain backend authorization and RLS" }
|
|
4663
|
+
] };
|
|
4664
|
+
}
|
|
4665
|
+
};
|
|
4666
|
+
|
|
4291
4667
|
// src/protect/install/adapters/next.ts
|
|
4292
|
-
import { existsSync as
|
|
4293
|
-
import { dirname as dirname3, join as
|
|
4668
|
+
import { existsSync as existsSync8, lstatSync as lstatSync2, readdirSync as readdirSync3 } from "fs";
|
|
4669
|
+
import { dirname as dirname3, join as join12, relative as relative3 } from "path";
|
|
4670
|
+
import { createRequire as createRequire4 } from "module";
|
|
4294
4671
|
|
|
4295
4672
|
// src/protect/install/adapters/next-source.ts
|
|
4296
|
-
import { createRequire } from "module";
|
|
4297
|
-
import { join as
|
|
4673
|
+
import { createRequire as createRequire3 } from "module";
|
|
4674
|
+
import { join as join11 } from "path";
|
|
4298
4675
|
var NEXT_MARKER = "// #region patchstack-next-composed";
|
|
4299
4676
|
var ROUTE_MARKER = "// #region patchstack-next-route";
|
|
4300
4677
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
4301
4678
|
function nextCompiler(cwd) {
|
|
4302
|
-
for (const base of [
|
|
4679
|
+
for (const base of [join11(cwd, "package.json"), import.meta.url]) {
|
|
4303
4680
|
try {
|
|
4304
|
-
return
|
|
4681
|
+
return createRequire3(base)("typescript");
|
|
4305
4682
|
} catch {
|
|
4306
4683
|
}
|
|
4307
4684
|
}
|
|
@@ -4373,7 +4750,7 @@ function standardNextRouting(ts, file, source) {
|
|
|
4373
4750
|
return false;
|
|
4374
4751
|
};
|
|
4375
4752
|
if (!literal(target) || ![...objects.values()].every(literal)) return false;
|
|
4376
|
-
return target.properties.every((p) => p.name && !["basePath", "i18n", "skipMiddlewareUrlNormalize", "pageExtensions", "__proto__"].includes(
|
|
4753
|
+
return target.properties.every((p) => p.name && !["basePath", "i18n", "skipMiddlewareUrlNormalize", "skipProxyUrlNormalize", "pageExtensions", "__proto__"].includes(
|
|
4377
4754
|
ts.isIdentifier(p.name) || ts.isStringLiteral(p.name) ? p.name.text : ""
|
|
4378
4755
|
));
|
|
4379
4756
|
}
|
|
@@ -4437,7 +4814,7 @@ function prelude(request, marker) {
|
|
|
4437
4814
|
function composeNextMiddleware(ts, file, source, guardImport) {
|
|
4438
4815
|
const sf = parse(ts, file, source);
|
|
4439
4816
|
if (!sf || /\b(?:psProtection|psBlocked|psRequest|getPatchstackProtection)\b/.test(source)) return null;
|
|
4440
|
-
const found = handlers(ts, sf, /* @__PURE__ */ new Set(["middleware"]));
|
|
4817
|
+
const found = handlers(ts, sf, /* @__PURE__ */ new Set([/proxy\.[jt]s$/.test(file) ? "proxy" : "middleware"]));
|
|
4441
4818
|
if (found.length !== 1 || sf.statements.some((s) => ts.isExportAssignment(s) || ts.isExportDeclaration(s))) return null;
|
|
4442
4819
|
const fn = found[0];
|
|
4443
4820
|
const request = requestName(ts, fn);
|
|
@@ -4499,7 +4876,7 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
|
|
|
4499
4876
|
const imported = sf.statements.some((s) => ts.isImportDeclaration(s) && ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === guardImport && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings) && s.importClause.namedBindings.elements.some((e) => !e.propertyName && e.name.text === "getPatchstackProtection"));
|
|
4500
4877
|
if (!imported) return false;
|
|
4501
4878
|
if (route && !sf.statements.some((s) => ts.isImportDeclaration(s) && ts.isStringLiteral(s.moduleSpecifier) && s.moduleSpecifier.text === guardImport && s.importClause?.namedBindings && ts.isNamedImports(s.importClause.namedBindings) && s.importClause.namedBindings.elements.some((e) => !e.propertyName && e.name.text === "screenPatchstackResponse"))) return false;
|
|
4502
|
-
const found = handlers(ts, sf, route ? METHODS : /* @__PURE__ */ new Set(["middleware"]));
|
|
4879
|
+
const found = handlers(ts, sf, route ? METHODS : /* @__PURE__ */ new Set([/proxy\.[jt]s$/.test(file) ? "proxy" : "middleware"]));
|
|
4503
4880
|
if (!found.length || sf.statements.some((s) => ts.isExportDeclaration(s) || ts.isExportAssignment(s))) return false;
|
|
4504
4881
|
if (route && methodExportCount(ts, sf) !== found.length) return false;
|
|
4505
4882
|
const compact = (text) => text.replace(/\s+/g, "");
|
|
@@ -4534,17 +4911,48 @@ function nextSourceWired(ts, file, source, guardImport, route = false) {
|
|
|
4534
4911
|
|
|
4535
4912
|
// src/protect/install/adapters/next.ts
|
|
4536
4913
|
function middlewareInfo(cwd) {
|
|
4537
|
-
const candidates =
|
|
4538
|
-
const
|
|
4539
|
-
|
|
4914
|
+
const candidates = entryFiles(cwd);
|
|
4915
|
+
const name = (nextMajor(cwd) ?? 0) >= 16 ? "proxy" : "middleware";
|
|
4916
|
+
const relFile = candidates.find((rel) => existsSync8(join12(cwd, rel))) ?? (existsSync8(join12(cwd, "src")) ? `src/${name}.ts` : `${name}.ts`);
|
|
4917
|
+
return { relFile, relDir: dirname3(relFile), exists: existsSync8(join12(cwd, relFile)) };
|
|
4918
|
+
}
|
|
4919
|
+
function nextMajor(cwd) {
|
|
4920
|
+
try {
|
|
4921
|
+
const file = createRequire4(join12(cwd, "package.json")).resolve("next/package.json");
|
|
4922
|
+
return Number(JSON.parse(read(file)).version.split(".")[0]);
|
|
4923
|
+
} catch {
|
|
4924
|
+
try {
|
|
4925
|
+
const pkg = JSON.parse(read(join12(cwd, "package.json")));
|
|
4926
|
+
const version = pkg.dependencies?.next ?? pkg.devDependencies?.next;
|
|
4927
|
+
const match = typeof version === "string" && /^[~^]?(\d+)\./.exec(version);
|
|
4928
|
+
return match ? Number(match[1]) : null;
|
|
4929
|
+
} catch {
|
|
4930
|
+
return null;
|
|
4931
|
+
}
|
|
4932
|
+
}
|
|
4540
4933
|
}
|
|
4541
|
-
function
|
|
4542
|
-
return [
|
|
4934
|
+
function entryFiles(cwd) {
|
|
4935
|
+
return [
|
|
4936
|
+
"middleware.ts",
|
|
4937
|
+
"middleware.js",
|
|
4938
|
+
"src/middleware.ts",
|
|
4939
|
+
"src/middleware.js",
|
|
4940
|
+
"proxy.ts",
|
|
4941
|
+
"proxy.js",
|
|
4942
|
+
"src/proxy.ts",
|
|
4943
|
+
"src/proxy.js"
|
|
4944
|
+
].filter((file) => existsSync8(join12(cwd, file)));
|
|
4945
|
+
}
|
|
4946
|
+
function entryProblem2(cwd) {
|
|
4947
|
+
const files = entryFiles(cwd);
|
|
4948
|
+
if (files.length > 1) return `multiple Next.js middleware/proxy entries (${files.join(", ")}); select one manually`;
|
|
4949
|
+
if (files.some((file) => /proxy\.[jt]s$/.test(file)) && (nextMajor(cwd) ?? 0) < 16) return `${files.join(", ")} requires a verified Next.js 16+ installation`;
|
|
4950
|
+
return null;
|
|
4543
4951
|
}
|
|
4544
4952
|
function paths(cwd) {
|
|
4545
4953
|
const mw = middlewareInfo(cwd);
|
|
4546
4954
|
const ext = mw.relFile.endsWith(".js") ? "js" : "ts";
|
|
4547
|
-
return { ...mw, rules:
|
|
4955
|
+
return { ...mw, rules: join12(mw.relDir, "patchstack.rules.json"), guard: join12(mw.relDir, `patchstack.next.${ext}`) };
|
|
4548
4956
|
}
|
|
4549
4957
|
function importFrom(file, guard) {
|
|
4550
4958
|
const rel = relative3(dirname3(file), guard).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
@@ -4559,7 +4967,7 @@ function routes(cwd) {
|
|
|
4559
4967
|
incomplete = true;
|
|
4560
4968
|
return;
|
|
4561
4969
|
}
|
|
4562
|
-
const full =
|
|
4970
|
+
const full = join12(cwd, rel);
|
|
4563
4971
|
if (lstatSync2(full).isSymbolicLink()) {
|
|
4564
4972
|
incomplete = true;
|
|
4565
4973
|
return;
|
|
@@ -4570,14 +4978,14 @@ function routes(cwd) {
|
|
|
4570
4978
|
incomplete = true;
|
|
4571
4979
|
continue;
|
|
4572
4980
|
}
|
|
4573
|
-
const child =
|
|
4981
|
+
const child = join12(rel, entry.name);
|
|
4574
4982
|
if (entry.isDirectory()) walk2(child);
|
|
4575
4983
|
else if (entry.isFile() && /^route\.(?:tsx?|jsx?)$/.test(entry.name)) files.push(child);
|
|
4576
4984
|
}
|
|
4577
4985
|
};
|
|
4578
4986
|
for (const root of ["app", "src/app"]) {
|
|
4579
|
-
if (!
|
|
4580
|
-
if (root.startsWith("src/") && lstatSync2(
|
|
4987
|
+
if (!existsSync8(join12(cwd, root))) continue;
|
|
4988
|
+
if (root.startsWith("src/") && lstatSync2(join12(cwd, "src")).isSymbolicLink()) {
|
|
4581
4989
|
incomplete = true;
|
|
4582
4990
|
continue;
|
|
4583
4991
|
}
|
|
@@ -4591,53 +4999,62 @@ function routes(cwd) {
|
|
|
4591
4999
|
}
|
|
4592
5000
|
function customRouting(cwd, ts) {
|
|
4593
5001
|
return ["js", "mjs", "ts", "cjs"].some((ext) => {
|
|
4594
|
-
const file =
|
|
4595
|
-
return
|
|
5002
|
+
const file = join12(cwd, `next.config.${ext}`);
|
|
5003
|
+
return existsSync8(file) && !standardNextRouting(ts, file, read(file));
|
|
4596
5004
|
});
|
|
4597
5005
|
}
|
|
4598
5006
|
function sharedGuardPresent(file) {
|
|
4599
|
-
if (!
|
|
5007
|
+
if (!existsSync8(file)) return false;
|
|
4600
5008
|
const source = read(file);
|
|
4601
5009
|
return source.includes("export async function getPatchstackProtection") && source.includes("export async function screenPatchstackResponse") && source.includes('import { createProtection } from "@patchstack/connect/protect"');
|
|
4602
5010
|
}
|
|
4603
5011
|
function wire2(cwd, opts) {
|
|
4604
|
-
const
|
|
4605
|
-
if (
|
|
4606
|
-
log(`
|
|
5012
|
+
const problem = entryProblem2(cwd);
|
|
5013
|
+
if (problem) {
|
|
5014
|
+
log(`Next.js entries left untouched: ${problem}. Do not add middleware alongside a proxy.`);
|
|
4607
5015
|
return { ok: false, changed: [] };
|
|
4608
5016
|
}
|
|
4609
5017
|
const templates = templatesDir();
|
|
4610
5018
|
const mw = paths(cwd);
|
|
4611
|
-
ensureProjectDirectorySync(cwd,
|
|
5019
|
+
ensureProjectDirectorySync(cwd, join12(cwd, mw.relDir));
|
|
4612
5020
|
const changed = [];
|
|
4613
|
-
if (opts.demo || !
|
|
4614
|
-
copyProjectFileSync(cwd,
|
|
5021
|
+
if (opts.demo || !existsSync8(join12(cwd, mw.rules))) {
|
|
5022
|
+
copyProjectFileSync(cwd, join12(templates, opts.demo ? "demo-rules.json" : "rules.json"), join12(cwd, mw.rules));
|
|
4615
5023
|
changed.push(mw.rules);
|
|
4616
5024
|
}
|
|
4617
5025
|
const ts = nextCompiler(cwd);
|
|
4618
|
-
const guardPath =
|
|
4619
|
-
const guardConflict =
|
|
5026
|
+
const guardPath = join12(cwd, mw.guard);
|
|
5027
|
+
const guardConflict = existsSync8(guardPath) && !sharedGuardPresent(guardPath);
|
|
4620
5028
|
const ensureGuard = () => {
|
|
4621
|
-
if (
|
|
4622
|
-
|
|
5029
|
+
if (existsSync8(guardPath)) {
|
|
5030
|
+
if (mw.guard.endsWith(".ts") && installTemplate(cwd, mw.guard, "next-guard.ts")) changed.push(mw.guard);
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
5033
|
+
const source = read(join12(templates, "next-guard.ts"));
|
|
4623
5034
|
writeProjectFileSync(cwd, guardPath, mw.guard.endsWith(".js") ? ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext } }).outputText : source);
|
|
4624
5035
|
if (!opts.demo) bakeSiteUuid(cwd, mw.guard);
|
|
4625
5036
|
changed.push(mw.guard);
|
|
4626
5037
|
};
|
|
4627
|
-
const existing = mw.exists ? read(
|
|
5038
|
+
const existing = mw.exists ? read(join12(cwd, mw.relFile)) : "";
|
|
4628
5039
|
if (!mw.exists) {
|
|
4629
|
-
|
|
5040
|
+
if (/proxy\.[jt]s$/.test(mw.relFile)) {
|
|
5041
|
+
const composed = ts && !guardConflict && composeNextMiddleware(ts, mw.relFile, 'export function proxy(request: Request) {}\nexport const config = { matcher: "/:path*" };\n', importFrom(mw.relFile, mw.guard));
|
|
5042
|
+
if (!composed) return { ok: false, changed };
|
|
5043
|
+
ensureGuard();
|
|
5044
|
+
writeProjectFileSync(cwd, join12(cwd, mw.relFile), composed);
|
|
5045
|
+
} else copyProjectFileSync(cwd, join12(templates, "next-middleware.ts"), join12(cwd, mw.relFile));
|
|
4630
5046
|
if (!opts.demo) bakeSiteUuid(cwd, mw.relFile);
|
|
4631
5047
|
changed.push(mw.relFile);
|
|
4632
5048
|
log(`scaffolded ${mw.relFile} (request-phase guard)`);
|
|
4633
5049
|
} else if (existing.includes(NEXT_MARKER) || existing.includes("#region patchstack-next (")) {
|
|
5050
|
+
if (existing.includes("#region patchstack-next (") && installTemplate(cwd, mw.relFile, "next-middleware.ts")) changed.push(mw.relFile);
|
|
4634
5051
|
log(`${mw.relFile} already has a Patchstack guard \u2014 left as-is`);
|
|
4635
5052
|
if (ts && existing.includes(NEXT_MARKER)) ensureGuard();
|
|
4636
5053
|
} else {
|
|
4637
5054
|
const composed = ts && !guardConflict && !customRouting(cwd, ts) && composeNextMiddleware(ts, mw.relFile, existing, importFrom(mw.relFile, mw.guard));
|
|
4638
5055
|
if (composed) {
|
|
4639
5056
|
ensureGuard();
|
|
4640
|
-
writeProjectFileSync(cwd,
|
|
5057
|
+
writeProjectFileSync(cwd, join12(cwd, mw.relFile), composed);
|
|
4641
5058
|
changed.push(mw.relFile);
|
|
4642
5059
|
log(`composed ${mw.relFile}: request guard first; existing middleware keeps its original matcher scope`);
|
|
4643
5060
|
} else {
|
|
@@ -4646,7 +5063,7 @@ function wire2(cwd, opts) {
|
|
|
4646
5063
|
}
|
|
4647
5064
|
const inventory = routes(cwd);
|
|
4648
5065
|
for (const file of inventory.files) {
|
|
4649
|
-
const source = read(
|
|
5066
|
+
const source = read(join12(cwd, file));
|
|
4650
5067
|
if (source.includes(ROUTE_MARKER)) {
|
|
4651
5068
|
if (ts) ensureGuard();
|
|
4652
5069
|
continue;
|
|
@@ -4654,7 +5071,7 @@ function wire2(cwd, opts) {
|
|
|
4654
5071
|
const composed = ts && !guardConflict && /\.(?:ts|js)$/.test(file) && composeNextRoute(ts, file, source, importFrom(file, mw.guard));
|
|
4655
5072
|
if (composed) {
|
|
4656
5073
|
ensureGuard();
|
|
4657
|
-
writeProjectFileSync(cwd,
|
|
5074
|
+
writeProjectFileSync(cwd, join12(cwd, file), composed);
|
|
4658
5075
|
changed.push(file);
|
|
4659
5076
|
log(`wired ${file} (request checks + response filtering)`);
|
|
4660
5077
|
} else {
|
|
@@ -4667,25 +5084,21 @@ function wire2(cwd, opts) {
|
|
|
4667
5084
|
return { ok: true, changed: [...new Set(changed)] };
|
|
4668
5085
|
}
|
|
4669
5086
|
function verify2(cwd) {
|
|
4670
|
-
const
|
|
4671
|
-
if (
|
|
5087
|
+
const problem = entryProblem2(cwd);
|
|
5088
|
+
if (problem) return {
|
|
4672
5089
|
wired: false,
|
|
4673
|
-
checks: [{
|
|
4674
|
-
label: "Next.js proxy wiring requires manual verification",
|
|
4675
|
-
ok: false,
|
|
4676
|
-
hint: `review ${proxies.join(", ")}; automatic proxy integration is not supported. Next.js cannot use middleware and proxy together.`
|
|
4677
|
-
}]
|
|
5090
|
+
checks: [{ label: "Next.js request entry needs review", ok: false, hint: problem }]
|
|
4678
5091
|
};
|
|
4679
5092
|
const mw = paths(cwd);
|
|
4680
5093
|
const ts = nextCompiler(cwd);
|
|
4681
|
-
const source = mw.exists ? read(
|
|
4682
|
-
const shared = sharedGuardPresent(
|
|
5094
|
+
const source = mw.exists ? read(join12(cwd, mw.relFile)) : "";
|
|
5095
|
+
const shared = sharedGuardPresent(join12(cwd, mw.guard));
|
|
4683
5096
|
const legacy = source.includes("#region patchstack-next (") && source.includes("await protection.fetchGuard()(request)") && source.includes("if (blocked) return blocked") && source.includes('matcher: "/:path*"');
|
|
4684
5097
|
const composed = ts && shared && !customRouting(cwd, ts) && nextSourceWired(ts, mw.relFile, source, importFrom(mw.relFile, mw.guard));
|
|
4685
5098
|
const present2 = !!(legacy || composed);
|
|
4686
|
-
const rulesPresent =
|
|
5099
|
+
const rulesPresent = existsSync8(join12(cwd, mw.rules));
|
|
4687
5100
|
const inventory = routes(cwd);
|
|
4688
|
-
const missing = inventory.files.filter((file) => !(ts && shared && nextSourceWired(ts, file, read(
|
|
5101
|
+
const missing = inventory.files.filter((file) => !(ts && shared && nextSourceWired(ts, file, read(join12(cwd, file)), importFrom(file, mw.guard), true)));
|
|
4689
5102
|
return {
|
|
4690
5103
|
wired: present2 && rulesPresent && missing.length === 0 && !inventory.incomplete,
|
|
4691
5104
|
checks: [
|
|
@@ -4724,51 +5137,63 @@ var nextAdapter = {
|
|
|
4724
5137
|
};
|
|
4725
5138
|
|
|
4726
5139
|
// src/protect/install/seam.ts
|
|
4727
|
-
import { existsSync as
|
|
4728
|
-
import { join as
|
|
5140
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5141
|
+
import { join as join13, dirname as dirname4 } from "path";
|
|
4729
5142
|
function rulesRel(seamRel) {
|
|
4730
5143
|
const d = dirname4(seamRel);
|
|
4731
5144
|
return (d === "." ? "patchstack.rules.json" : `${d}/patchstack.rules.json`).replace(/\\/g, "/");
|
|
4732
5145
|
}
|
|
4733
5146
|
function wireSeam(cwd, opts, spec) {
|
|
4734
5147
|
const templates = templatesDir();
|
|
4735
|
-
const existing = spec.candidates.find((c) =>
|
|
5148
|
+
const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
|
|
4736
5149
|
const seamRel = existing ?? spec.target;
|
|
4737
|
-
ensureProjectDirectorySync(cwd, dirname4(
|
|
4738
|
-
const rulesDst =
|
|
5150
|
+
ensureProjectDirectorySync(cwd, dirname4(join13(cwd, seamRel)));
|
|
5151
|
+
const rulesDst = join13(cwd, rulesRel(seamRel));
|
|
4739
5152
|
const changed = [];
|
|
4740
|
-
if (opts.demo || !
|
|
4741
|
-
copyProjectFileSync(cwd,
|
|
5153
|
+
if (opts.demo || !existsSync9(rulesDst)) {
|
|
5154
|
+
copyProjectFileSync(cwd, join13(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
4742
5155
|
changed.push(rulesRel(seamRel));
|
|
4743
5156
|
}
|
|
4744
|
-
const current = existing ? read(
|
|
5157
|
+
const current = existing ? read(join13(cwd, existing)) : "";
|
|
4745
5158
|
if (existing && current.includes(spec.marker)) {
|
|
4746
|
-
|
|
5159
|
+
if (installTemplate(cwd, seamRel, spec.templateName)) changed.push(seamRel);
|
|
4747
5160
|
return { ok: true, changed };
|
|
4748
5161
|
}
|
|
4749
5162
|
if (existing) {
|
|
4750
5163
|
log(`existing ${existing} left untouched \u2014 scaffolded ${rulesRel(seamRel)}; ${spec.planHint}`);
|
|
4751
5164
|
return { ok: true, changed };
|
|
4752
5165
|
}
|
|
4753
|
-
copyProjectFileSync(cwd,
|
|
5166
|
+
copyProjectFileSync(cwd, join13(templates, spec.templateName), join13(cwd, seamRel));
|
|
4754
5167
|
if (!opts.demo) bakeSiteUuid(cwd, seamRel);
|
|
4755
5168
|
changed.push(seamRel);
|
|
4756
5169
|
log(`scaffolded ${seamRel}`);
|
|
4757
5170
|
return { ok: true, changed: [...new Set(changed)] };
|
|
4758
5171
|
}
|
|
4759
5172
|
function verifySeam(cwd, spec) {
|
|
4760
|
-
const existing = spec.candidates.find((c) =>
|
|
5173
|
+
const existing = spec.candidates.find((c) => existsSync9(join13(cwd, c)));
|
|
4761
5174
|
const seamRel = existing ?? spec.target;
|
|
4762
|
-
const present2 = existing ?
|
|
4763
|
-
const rulesPresent =
|
|
5175
|
+
const present2 = existing ? templateWiringPresent(cwd, existing, spec.templateName) : false;
|
|
5176
|
+
const rulesPresent = existsSync9(join13(cwd, rulesRel(seamRel)));
|
|
4764
5177
|
return {
|
|
4765
5178
|
wired: present2 && rulesPresent,
|
|
4766
5179
|
checks: [
|
|
4767
|
-
{ label: `${spec.seamLabel}
|
|
5180
|
+
{ label: `${spec.seamLabel} wiring verified`, ok: present2, hint: `run \`patchstack-connect protect\`; customized hooks need manual verification (${spec.target})` },
|
|
4768
5181
|
{ label: "rules co-located with the guard", ok: rulesPresent, hint: "run `patchstack-connect protect`" }
|
|
4769
5182
|
]
|
|
4770
5183
|
};
|
|
4771
5184
|
}
|
|
5185
|
+
function templateWiringPresent(cwd, file, template) {
|
|
5186
|
+
const ts = sourceCompiler(cwd);
|
|
5187
|
+
if (!ts) return false;
|
|
5188
|
+
const actual = parsedSource(ts, file, read(join13(cwd, file)));
|
|
5189
|
+
const expected = parsedSource(ts, template, read(join13(templatesDir(), template)));
|
|
5190
|
+
if (!actual || !expected) return false;
|
|
5191
|
+
const printer = ts.createPrinter({ removeComments: true });
|
|
5192
|
+
const emit = (node, tree) => printer.printNode(ts.EmitHint.Unspecified, node, tree);
|
|
5193
|
+
const required = expected.statements.filter((node) => ts.isExportAssignment(node) || ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) || ts.isFunctionDeclaration(node) && node.name?.text === "getProtection" || ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "@patchstack/connect/protect");
|
|
5194
|
+
const code = actual.statements.map((node) => emit(node, actual));
|
|
5195
|
+
return required.length >= 3 && required.every((node) => code.filter((text) => text === emit(node, expected)).length === 1);
|
|
5196
|
+
}
|
|
4772
5197
|
|
|
4773
5198
|
// src/protect/install/adapters/sveltekit.ts
|
|
4774
5199
|
var SPEC = {
|
|
@@ -4850,7 +5275,7 @@ var nuxtAdapter = {
|
|
|
4850
5275
|
|
|
4851
5276
|
// src/protect/install/find-app.ts
|
|
4852
5277
|
import { readFileSync as readFileSync8, readdirSync as readdirSync4, lstatSync as lstatSync3 } from "fs";
|
|
4853
|
-
import { join as
|
|
5278
|
+
import { join as join14, relative as relative4, dirname as dirname5 } from "path";
|
|
4854
5279
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
4855
5280
|
"node_modules",
|
|
4856
5281
|
"patchstack",
|
|
@@ -4882,7 +5307,7 @@ function findAppInstance(cwd, re) {
|
|
|
4882
5307
|
}
|
|
4883
5308
|
for (const name of entries) {
|
|
4884
5309
|
if (SKIP_DIRS2.has(name) || name.startsWith(".")) continue;
|
|
4885
|
-
const p =
|
|
5310
|
+
const p = join14(dir, name);
|
|
4886
5311
|
let st;
|
|
4887
5312
|
try {
|
|
4888
5313
|
st = lstatSync3(p);
|
|
@@ -4941,7 +5366,7 @@ function declaredEntries(cwd) {
|
|
|
4941
5366
|
const paths2 = /* @__PURE__ */ new Set();
|
|
4942
5367
|
let pkg;
|
|
4943
5368
|
try {
|
|
4944
|
-
pkg = JSON.parse(readFileSync8(
|
|
5369
|
+
pkg = JSON.parse(readFileSync8(join14(cwd, "package.json"), "utf8"));
|
|
4945
5370
|
} catch {
|
|
4946
5371
|
return paths2;
|
|
4947
5372
|
}
|
|
@@ -4967,17 +5392,17 @@ function importSpecifier(fromRel, toRel, preserveExtension = false) {
|
|
|
4967
5392
|
}
|
|
4968
5393
|
|
|
4969
5394
|
// src/protect/install/register.ts
|
|
4970
|
-
import { existsSync as
|
|
4971
|
-
import { join as
|
|
5395
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5396
|
+
import { join as join17, resolve as resolve3 } from "path";
|
|
4972
5397
|
|
|
4973
5398
|
// src/protect/install/generic.ts
|
|
4974
|
-
import { readFileSync as readFileSync9, existsSync as
|
|
4975
|
-
import { join as
|
|
5399
|
+
import { readFileSync as readFileSync9, existsSync as existsSync11, readdirSync as readdirSync5, lstatSync as lstatSync4 } from "fs";
|
|
5400
|
+
import { join as join16, dirname as dirname7 } from "path";
|
|
4976
5401
|
|
|
4977
5402
|
// src/protect/install/source-scope.ts
|
|
4978
5403
|
import { execFileSync } from "child_process";
|
|
4979
|
-
import { existsSync as
|
|
4980
|
-
import { dirname as dirname6, join as
|
|
5404
|
+
import { existsSync as existsSync10 } from "fs";
|
|
5405
|
+
import { dirname as dirname6, join as join15, resolve as resolve2 } from "path";
|
|
4981
5406
|
|
|
4982
5407
|
// src/protect/install/node-flags.ts
|
|
4983
5408
|
var flagName = (token) => token.split("=")[0];
|
|
@@ -5267,8 +5692,8 @@ function moduleKindOf(filePath, packageIsModule) {
|
|
|
5267
5692
|
}
|
|
5268
5693
|
function resolvesToGuardModule(specifier, query) {
|
|
5269
5694
|
const resolved = resolve2(dirname6(query.fromFile), specifier);
|
|
5270
|
-
if (withoutModuleExtension(resolved) !==
|
|
5271
|
-
const existing = GUARD_FILENAMES.filter((name) =>
|
|
5695
|
+
if (withoutModuleExtension(resolved) !== join15(query.guardDir, "guard")) return false;
|
|
5696
|
+
const existing = GUARD_FILENAMES.filter((name) => existsSync10(join15(query.guardDir, name)));
|
|
5272
5697
|
if (existing.length === 0) return false;
|
|
5273
5698
|
const extension = /(\.[A-Za-z0-9]+)$/.exec(specifier)?.[1] ?? "";
|
|
5274
5699
|
if (!query.strictExtensions) return true;
|
|
@@ -5277,7 +5702,7 @@ function resolvesToGuardModule(specifier, query) {
|
|
|
5277
5702
|
if (query.kind === "cjs") return existing.includes("guard.js");
|
|
5278
5703
|
return true;
|
|
5279
5704
|
}
|
|
5280
|
-
if (
|
|
5705
|
+
if (existsSync10(resolved)) return true;
|
|
5281
5706
|
return query.kind === "ts" && extension === ".js" && existing.includes("guard.ts");
|
|
5282
5707
|
}
|
|
5283
5708
|
function withoutModuleExtension(filePath) {
|
|
@@ -5287,16 +5712,16 @@ function withoutModuleExtension(filePath) {
|
|
|
5287
5712
|
// src/protect/install/generic.ts
|
|
5288
5713
|
function packageIsEsm(cwd) {
|
|
5289
5714
|
try {
|
|
5290
|
-
return JSON.parse(read(
|
|
5715
|
+
return JSON.parse(read(join16(cwd, "package.json"))).type === "module";
|
|
5291
5716
|
} catch {
|
|
5292
5717
|
return false;
|
|
5293
5718
|
}
|
|
5294
5719
|
}
|
|
5295
5720
|
function genericDir(cwd) {
|
|
5296
|
-
return
|
|
5721
|
+
return existsSync11(join16(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
5297
5722
|
}
|
|
5298
5723
|
function hasTypeScript(cwd) {
|
|
5299
|
-
return
|
|
5724
|
+
return existsSync11(join16(cwd, "tsconfig.json")) || candidateEntries(cwd).some((entry) => /\.tsx?$/.test(entry));
|
|
5300
5725
|
}
|
|
5301
5726
|
function genericGuardTarget(cwd) {
|
|
5302
5727
|
if (hasTypeScript(cwd)) {
|
|
@@ -5310,7 +5735,7 @@ function genericGuardTarget(cwd) {
|
|
|
5310
5735
|
function genericScaffoldFiles(cwd) {
|
|
5311
5736
|
const dir = genericDir(cwd);
|
|
5312
5737
|
const candidates = ["guard.ts", "guard.js", "guard.cjs", "rules.json"];
|
|
5313
|
-
return candidates.map((file) => `${dir}/${file}`).filter((relative9) =>
|
|
5738
|
+
return candidates.map((file) => `${dir}/${file}`).filter((relative9) => existsSync11(join16(cwd, relative9)));
|
|
5314
5739
|
}
|
|
5315
5740
|
function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
5316
5741
|
const target = genericGuardTarget(cwd);
|
|
@@ -5318,15 +5743,15 @@ function scaffoldGeneric(cwd, opts, guardTemplate, guardFile) {
|
|
|
5318
5743
|
guardFile ??= target.file;
|
|
5319
5744
|
const templates = templatesDir();
|
|
5320
5745
|
const dir = genericDir(cwd);
|
|
5321
|
-
const dst =
|
|
5746
|
+
const dst = join16(cwd, dir);
|
|
5322
5747
|
ensureProjectDirectorySync(cwd, dst);
|
|
5323
|
-
copyProjectFileSync(cwd, join12(templates, guardTemplate), join12(dst, guardFile));
|
|
5324
5748
|
const guardRel = `${dir}/${guardFile}`;
|
|
5325
|
-
const changed = [
|
|
5326
|
-
if (
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5749
|
+
const changed = [];
|
|
5750
|
+
if (installTemplate(cwd, guardRel, guardTemplate)) changed.push(guardRel);
|
|
5751
|
+
if (!opts.demo && matchesGuardTemplate(cwd, guardRel, guardTemplate)) bakeSiteUuid(cwd, guardRel);
|
|
5752
|
+
const rulesDst = join16(dst, "rules.json");
|
|
5753
|
+
if (opts.demo || !existsSync11(rulesDst)) {
|
|
5754
|
+
copyProjectFileSync(cwd, join16(templates, opts.demo ? "demo-rules.json" : "rules.json"), rulesDst);
|
|
5330
5755
|
changed.push(`${dir}/rules.json`);
|
|
5331
5756
|
}
|
|
5332
5757
|
return { changed, dir };
|
|
@@ -5347,10 +5772,10 @@ function candidateEntries(cwd) {
|
|
|
5347
5772
|
"app.ts",
|
|
5348
5773
|
"app.js"
|
|
5349
5774
|
];
|
|
5350
|
-
const hits = names.filter((n) =>
|
|
5775
|
+
const hits = names.filter((n) => existsSync11(join16(cwd, n)));
|
|
5351
5776
|
try {
|
|
5352
|
-
const pkg = JSON.parse(read(
|
|
5353
|
-
if (typeof pkg.main === "string" &&
|
|
5777
|
+
const pkg = JSON.parse(read(join16(cwd, "package.json")));
|
|
5778
|
+
if (typeof pkg.main === "string" && existsSync11(join16(cwd, pkg.main))) hits.push(pkg.main);
|
|
5354
5779
|
} catch {
|
|
5355
5780
|
}
|
|
5356
5781
|
return [...new Set(hits)];
|
|
@@ -5453,13 +5878,16 @@ function escapeForRegExp(value) {
|
|
|
5453
5878
|
function genericVerify(cwd) {
|
|
5454
5879
|
const dir = genericDir(cwd);
|
|
5455
5880
|
const target = genericGuardTarget(cwd);
|
|
5456
|
-
const present2 = GUARD_FILENAMES.find((name) =>
|
|
5881
|
+
const present2 = GUARD_FILENAMES.find((name) => existsSync11(join16(cwd, dir, name)));
|
|
5457
5882
|
const scaffolded = present2 !== void 0;
|
|
5458
|
-
const
|
|
5883
|
+
const template = present2?.endsWith(".ts") ? "generic-guard.ts" : present2?.endsWith(".cjs") ? "generic-guard.cjs" : "generic-guard.js";
|
|
5884
|
+
const helperVerified = !!present2 && matchesGuardTemplate(cwd, `${dir}/${present2}`, template);
|
|
5885
|
+
const imported = scaffolded && guardIsImported(cwd, join16(cwd, dir, present2));
|
|
5459
5886
|
return {
|
|
5460
|
-
wired: scaffolded && imported,
|
|
5887
|
+
wired: scaffolded && helperVerified && imported,
|
|
5461
5888
|
checks: [
|
|
5462
5889
|
{ label: "generic guard scaffolded", ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
5890
|
+
{ label: "guard helper implementation verified", ok: helperVerified, hint: "preserved custom helpers require manual review of their exports and request/response screening" },
|
|
5463
5891
|
{
|
|
5464
5892
|
label: "guard imported and called in a server entry",
|
|
5465
5893
|
ok: imported,
|
|
@@ -5491,7 +5919,7 @@ var NON_RUNTIME_DIRS = /* @__PURE__ */ new Set([
|
|
|
5491
5919
|
function guardIsImported(cwd, guardPath) {
|
|
5492
5920
|
const guardDir = dirname7(guardPath);
|
|
5493
5921
|
const packageIsModule = packageIsEsm(cwd);
|
|
5494
|
-
const root =
|
|
5922
|
+
const root = existsSync11(join16(cwd, "src")) ? join16(cwd, "src") : cwd;
|
|
5495
5923
|
let found = false;
|
|
5496
5924
|
const walk2 = (d, depth) => {
|
|
5497
5925
|
if (found || depth > 8) return;
|
|
@@ -5504,7 +5932,7 @@ function guardIsImported(cwd, guardPath) {
|
|
|
5504
5932
|
for (const name of entries) {
|
|
5505
5933
|
if (found) return;
|
|
5506
5934
|
if (NON_RUNTIME_DIRS.has(name) || name.startsWith(".")) continue;
|
|
5507
|
-
const p =
|
|
5935
|
+
const p = join16(d, name);
|
|
5508
5936
|
let st;
|
|
5509
5937
|
try {
|
|
5510
5938
|
st = lstatSync4(p);
|
|
@@ -5527,7 +5955,7 @@ function guardIsImported(cwd, guardPath) {
|
|
|
5527
5955
|
|
|
5528
5956
|
// src/protect/install/register.ts
|
|
5529
5957
|
var routeRegistration = (appVar) => new RegExp(
|
|
5530
|
-
`^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*['"\`]`,
|
|
5958
|
+
`^\\s*${appVar}\\.(?:get|post|put|patch|delete|head|options|all|route)\\(|^\\s*${appVar}\\.use\\(\\s*(?:['"\`]|[\\w$]+(?:\\.[\\w$]+)*\\s*[,\\)])`,
|
|
5531
5959
|
"m"
|
|
5532
5960
|
);
|
|
5533
5961
|
function routesBefore(source, appVar, guardIndex) {
|
|
@@ -5563,7 +5991,7 @@ function guardTarget(cwd, entryRel, spec) {
|
|
|
5563
5991
|
}
|
|
5564
5992
|
function packageType(cwd) {
|
|
5565
5993
|
try {
|
|
5566
|
-
return JSON.parse(read(
|
|
5994
|
+
return JSON.parse(read(join17(cwd, "package.json"))).type;
|
|
5567
5995
|
} catch {
|
|
5568
5996
|
return void 0;
|
|
5569
5997
|
}
|
|
@@ -5580,7 +6008,7 @@ function wiringState(source, spec, appVar, fileRel, guard) {
|
|
|
5580
6008
|
);
|
|
5581
6009
|
const appIndex = maskedLines.findIndex((line) => spec.appRe.test(line));
|
|
5582
6010
|
const anchor = spec.callAfter ? spec.callAfter(appVar) : null;
|
|
5583
|
-
const anchorIndex = anchor ? maskedLines.
|
|
6011
|
+
const anchorIndex = anchor ? maskedLines.reduce((last, line, index) => anchor.test(line) ? index : last, -1) : -1;
|
|
5584
6012
|
return {
|
|
5585
6013
|
importAtTopLevel: importIndex !== -1 && isTopLevelLine(code, importIndex),
|
|
5586
6014
|
callInAppScope: callIndex !== -1 && inSameBlockAfter(code, appIndex, callIndex),
|
|
@@ -5599,8 +6027,8 @@ function bindsGuard(line, maskedLine, importName, call, fileRel, guard) {
|
|
|
5599
6027
|
}
|
|
5600
6028
|
function namesGuardModule(specifier, fileRel, guard) {
|
|
5601
6029
|
return resolvesToGuardModule(specifier, {
|
|
5602
|
-
fromFile:
|
|
5603
|
-
guardDir: resolve3(
|
|
6030
|
+
fromFile: join17(guard.cwd, fileRel),
|
|
6031
|
+
guardDir: resolve3(join17(guard.cwd, guard.guardDir)),
|
|
5604
6032
|
kind: moduleKindOf(fileRel, packageType(guard.cwd) === "module"),
|
|
5605
6033
|
strictExtensions: true
|
|
5606
6034
|
});
|
|
@@ -5619,8 +6047,17 @@ function wireRegister(cwd, opts, spec) {
|
|
|
5619
6047
|
}
|
|
5620
6048
|
const target = guardTarget(cwd, entry.relPath, spec);
|
|
5621
6049
|
const { changed, dir } = scaffoldGeneric(cwd, opts, target.template, target.file);
|
|
5622
|
-
|
|
6050
|
+
if (!matchesGuardTemplate(cwd, `${dir}/${target.file}`, target.template)) {
|
|
6051
|
+
log(`${dir}/${target.file} needs manual review; the existing helper and ${entry.relPath} were left untouched.`);
|
|
6052
|
+
return { ok: false, changed };
|
|
6053
|
+
}
|
|
6054
|
+
const p = join17(cwd, entry.relPath);
|
|
5623
6055
|
const s = read(p);
|
|
6056
|
+
const compiler = sourceCompiler(cwd);
|
|
6057
|
+
if (!compiler || !parsedSource(compiler, entry.relPath, s)) {
|
|
6058
|
+
log(`${entry.relPath} left untouched: a working TypeScript parser is required for source edits; ${spec.manualHint}`);
|
|
6059
|
+
return { ok: false, changed };
|
|
6060
|
+
}
|
|
5624
6061
|
const state = wiringState(s, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir });
|
|
5625
6062
|
if (state.importAtTopLevel && state.callInAppScope) {
|
|
5626
6063
|
log(
|
|
@@ -5647,17 +6084,35 @@ function wireRegister(cwd, opts, spec) {
|
|
|
5647
6084
|
const lines = s.split("\n");
|
|
5648
6085
|
if (!state.importAtTopLevel) {
|
|
5649
6086
|
const lastImport = lastTopLevelImportLine(s);
|
|
5650
|
-
const
|
|
6087
|
+
const importEnd = lastImport === -1 ? -1 : statementEndLine(compiler, entry.relPath, s, lastImport);
|
|
6088
|
+
if (importEnd === null) return { ok: false, changed };
|
|
6089
|
+
const importIdx = lastImport === -1 ? firstStatementLine(lines) : importEnd + 1;
|
|
5651
6090
|
lines.splice(importIdx, 0, importLine);
|
|
5652
6091
|
}
|
|
5653
6092
|
const appIdx = lines.findIndex((l) => spec.appRe.test(l));
|
|
5654
6093
|
if (appIdx !== -1 && !state.callInAppScope) {
|
|
5655
6094
|
const preferred = spec.callAfter?.(entry.appVar);
|
|
5656
|
-
const preferredIdx = preferred ? lines.
|
|
6095
|
+
const preferredIdx = preferred ? lines.reduce((last, line, index) => index > appIdx && preferred.test(line) && inSameBlockAfter(lines.join("\n"), appIdx, index) ? index : last, -1) : -1;
|
|
5657
6096
|
const callIdx = preferredIdx === -1 ? appIdx : preferredIdx;
|
|
5658
|
-
|
|
6097
|
+
if (spec.requireCallAfter && preferredIdx === -1) return { ok: false, changed };
|
|
6098
|
+
const current = lines.join("\n");
|
|
6099
|
+
const statement = statementPosition(compiler, entry.relPath, current, callIdx, preferredIdx === -1 ? spec.appRe : preferred);
|
|
6100
|
+
if (statement === null) return { ok: false, changed };
|
|
6101
|
+
const { start, end } = statement;
|
|
6102
|
+
const tail = current.slice(end);
|
|
6103
|
+
const trailingComment = /^[ \t]*\/\//.test(tail);
|
|
6104
|
+
const insertion = trailingComment ? current.indexOf("\n", end) : end;
|
|
6105
|
+
const position = insertion === -1 ? current.length : insertion;
|
|
6106
|
+
const separator = current.slice(0, end).trimEnd().endsWith(";") ? "" : ";";
|
|
6107
|
+
const prefix = current.slice(current.lastIndexOf("\n", start - 1) + 1, start).trim();
|
|
6108
|
+
const updated = current.slice(0, start) + (prefix ? "\n" : "") + current.slice(start, end) + separator + current.slice(end, position) + "\n" + REGION + "\n" + spec.call(entry.appVar) + "\n// #endregion patchstack\n" + current.slice(position);
|
|
6109
|
+
lines.splice(0, lines.length, ...updated.split("\n"));
|
|
5659
6110
|
}
|
|
5660
6111
|
const patched = lines.join("\n");
|
|
6112
|
+
if (!parsedSource(compiler, entry.relPath, patched)) {
|
|
6113
|
+
log(`${entry.relPath} would not parse after patching \u2014 left untouched; ${spec.manualHint}`);
|
|
6114
|
+
return { ok: false, changed };
|
|
6115
|
+
}
|
|
5661
6116
|
writeProjectFileSync(cwd, p, patched, { encoding: "utf8" });
|
|
5662
6117
|
if (spec.callAfter) {
|
|
5663
6118
|
const guardIndex = lines.findIndex((line) => line.includes(spec.call(entry.appVar)));
|
|
@@ -5689,26 +6144,30 @@ function firstStatementLine(lines) {
|
|
|
5689
6144
|
return i;
|
|
5690
6145
|
}
|
|
5691
6146
|
function serverIsGuarded(cwd, spec, server, guardDir) {
|
|
5692
|
-
const
|
|
6147
|
+
const target = guardTarget(cwd, server.relPath, spec);
|
|
6148
|
+
if (!matchesGuardTemplate(cwd, `${guardDir}/${target.file}`, target.template)) return false;
|
|
6149
|
+
const source = read(join17(cwd, server.relPath));
|
|
5693
6150
|
const state = wiringState(source, spec, server.appVar, server.relPath, { cwd, guardDir });
|
|
5694
6151
|
if (!state.importAtTopLevel || !state.callInAppScope || !state.ordered) return false;
|
|
5695
6152
|
return !spec.callAfter || routesBefore(source, server.appVar, state.callIndex).length === 0;
|
|
5696
6153
|
}
|
|
5697
6154
|
function verifyRegister(cwd, spec) {
|
|
5698
|
-
const dir =
|
|
6155
|
+
const dir = existsSync12(join17(cwd, "src")) ? "src/patchstack" : "patchstack";
|
|
5699
6156
|
const entry = findAppInstance(cwd, spec.appRe);
|
|
5700
6157
|
const target = entry ? guardTarget(cwd, entry.relPath, spec) : null;
|
|
5701
|
-
const scaffolded = target ?
|
|
5702
|
-
const
|
|
6158
|
+
const scaffolded = target ? existsSync12(join17(cwd, dir, target.file)) : false;
|
|
6159
|
+
const helperVerified = !!target && matchesGuardTemplate(cwd, `${dir}/${target.file}`, target.template);
|
|
6160
|
+
const entrySource = entry ? read(join17(cwd, entry.relPath)) : "";
|
|
5703
6161
|
const state = entry ? wiringState(entrySource, spec, entry.appVar, entry.relPath, { cwd, guardDir: dir }) : { importAtTopLevel: false, callInAppScope: false, ordered: false, callIndex: -1, appIndex: -1 };
|
|
5704
6162
|
const wired = state.importAtTopLevel && state.callInAppScope && state.ordered;
|
|
5705
6163
|
const early = spec.callAfter && entry ? routesBefore(entrySource, entry.appVar, state.callIndex) : [];
|
|
5706
6164
|
const noEarlyRoutes = early.length === 0;
|
|
5707
6165
|
const unguarded = (entry?.others ?? []).filter((other) => !serverIsGuarded(cwd, spec, other, dir)).map((other) => other.relPath);
|
|
5708
6166
|
return {
|
|
5709
|
-
wired: scaffolded && wired && noEarlyRoutes && unguarded.length === 0,
|
|
6167
|
+
wired: scaffolded && helperVerified && wired && noEarlyRoutes && unguarded.length === 0,
|
|
5710
6168
|
checks: [
|
|
5711
6169
|
{ label: `${spec.label} guard scaffolded`, ok: scaffolded, hint: "run `patchstack-connect protect`" },
|
|
6170
|
+
{ label: "guard helper implementation verified", ok: helperVerified, hint: "preserved custom helpers require manual review of their exports and request/response screening" },
|
|
5712
6171
|
{ label: `guard registered on the ${spec.label}`, ok: wired, hint: spec.manualHint },
|
|
5713
6172
|
...entry && entry.others.length > 0 ? [
|
|
5714
6173
|
{
|
|
@@ -5789,8 +6248,8 @@ var expressAdapter = {
|
|
|
5789
6248
|
};
|
|
5790
6249
|
|
|
5791
6250
|
// src/protect/install/reporting.ts
|
|
5792
|
-
import { existsSync as
|
|
5793
|
-
import { join as
|
|
6251
|
+
import { existsSync as existsSync13 } from "fs";
|
|
6252
|
+
import { join as join18 } from "path";
|
|
5794
6253
|
|
|
5795
6254
|
// src/protect/reporting-state.js
|
|
5796
6255
|
var REPORTING_STATES = Object.freeze([
|
|
@@ -5848,9 +6307,9 @@ function explainReportingState(state) {
|
|
|
5848
6307
|
|
|
5849
6308
|
// src/protect/install/reporting.ts
|
|
5850
6309
|
function siteIdentity(cwd, env) {
|
|
5851
|
-
const rc =
|
|
6310
|
+
const rc = join18(cwd, ".patchstackrc.json");
|
|
5852
6311
|
let recorded;
|
|
5853
|
-
if (
|
|
6312
|
+
if (existsSync13(rc)) {
|
|
5854
6313
|
try {
|
|
5855
6314
|
recorded = JSON.parse(read(rc)).siteUuid;
|
|
5856
6315
|
} catch {
|
|
@@ -5906,6 +6365,7 @@ function reportingChecks(cwd, env = process.env) {
|
|
|
5906
6365
|
// src/protect/install/index.ts
|
|
5907
6366
|
var ADAPTERS = [
|
|
5908
6367
|
tanstackSupabaseAdapter,
|
|
6368
|
+
tanstackAdapter,
|
|
5909
6369
|
nextAdapter,
|
|
5910
6370
|
sveltekitAdapter,
|
|
5911
6371
|
astroAdapter,
|
|
@@ -5930,7 +6390,10 @@ function runProtect(cwd, opts = {}) {
|
|
|
5930
6390
|
adapter = ADAPTERS.find((a) => a.detect(cwd));
|
|
5931
6391
|
if (adapter) {
|
|
5932
6392
|
const result = adapter.wire(cwd, opts);
|
|
5933
|
-
|
|
6393
|
+
const verification = adapter.verify(cwd);
|
|
6394
|
+
if (result.ok && verification.wired) return { status: "wired", adapter: adapter.name, changed: result.changed };
|
|
6395
|
+
const plan = verification.checks.filter((check) => !check.ok).map((check) => check.hint ?? check.label).join("\n");
|
|
6396
|
+
return { status: "scaffolded", adapter: adapter.name, changed: result.changed, plan };
|
|
5934
6397
|
}
|
|
5935
6398
|
} catch (err) {
|
|
5936
6399
|
log(`adapter ${adapter?.name ?? ""} failed (${err?.message ?? err}); falling back to a generic guard`);
|
|
@@ -5945,7 +6408,7 @@ function runProtect(cwd, opts = {}) {
|
|
|
5945
6408
|
return { status: "scaffolded", adapter: "generic", changed: [], plan: "" };
|
|
5946
6409
|
}
|
|
5947
6410
|
}
|
|
5948
|
-
var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["nextjs", "sveltekit", "astro", "nuxt", "generic"]);
|
|
6411
|
+
var RUNTIMES_WITHOUT_CONFIG_FILE = /* @__PURE__ */ new Set(["tanstack-start", "nextjs", "sveltekit", "astro", "nuxt", "generic"]);
|
|
5949
6412
|
function credentialNote(cwd, adapterName) {
|
|
5950
6413
|
if (!RUNTIMES_WITHOUT_CONFIG_FILE.has(adapterName)) return [];
|
|
5951
6414
|
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";
|
|
@@ -5999,7 +6462,7 @@ function runVerify(cwd) {
|
|
|
5999
6462
|
}
|
|
6000
6463
|
|
|
6001
6464
|
// src/widget.ts
|
|
6002
|
-
import { existsSync as
|
|
6465
|
+
import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
|
|
6003
6466
|
import path11 from "path";
|
|
6004
6467
|
var WIDGET_SCRIPT_URL = "https://cdn.patchstack.com/patchstack-widget.js";
|
|
6005
6468
|
var WIDGET_MARKER_ATTR = "data-patchstack-connect-widget";
|
|
@@ -6038,7 +6501,7 @@ ${indent}</body>`;
|
|
|
6038
6501
|
}
|
|
6039
6502
|
function findSourceShell(cwd) {
|
|
6040
6503
|
for (const candidate of SOURCE_SHELL_CANDIDATES) {
|
|
6041
|
-
if (
|
|
6504
|
+
if (existsSync14(path11.join(cwd, candidate))) {
|
|
6042
6505
|
return candidate;
|
|
6043
6506
|
}
|
|
6044
6507
|
}
|
|
@@ -6050,7 +6513,7 @@ function ensureSourceWidget(cwd, siteUuid, jsxShell = null) {
|
|
|
6050
6513
|
return { shell: null, action: "no-shell" };
|
|
6051
6514
|
}
|
|
6052
6515
|
const file = path11.join(cwd, shell);
|
|
6053
|
-
if (!
|
|
6516
|
+
if (!existsSync14(file)) {
|
|
6054
6517
|
return { shell: null, action: "no-shell" };
|
|
6055
6518
|
}
|
|
6056
6519
|
const before = readFileSync10(file, "utf8");
|
|
@@ -6276,7 +6739,7 @@ function detectPackageManager(cwd) {
|
|
|
6276
6739
|
return declared;
|
|
6277
6740
|
}
|
|
6278
6741
|
for (const { filename, pm } of PM_BY_LOCKFILE) {
|
|
6279
|
-
if (
|
|
6742
|
+
if (existsSync15(path12.join(cwd, filename))) {
|
|
6280
6743
|
return pm;
|
|
6281
6744
|
}
|
|
6282
6745
|
}
|
|
@@ -6373,7 +6836,7 @@ function resolveWidgetFileHint(cwd, framework) {
|
|
|
6373
6836
|
...GENERIC_WIDGET_FILES
|
|
6374
6837
|
];
|
|
6375
6838
|
for (const candidate of candidates) {
|
|
6376
|
-
if (
|
|
6839
|
+
if (existsSync15(path12.join(cwd, candidate))) {
|
|
6377
6840
|
return candidate;
|
|
6378
6841
|
}
|
|
6379
6842
|
}
|
|
@@ -6453,9 +6916,9 @@ async function collectGuideState(cwd) {
|
|
|
6453
6916
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
6454
6917
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
6455
6918
|
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
6456
|
-
// must retain.
|
|
6457
|
-
prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
|
|
6458
|
-
postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
6919
|
+
// must retain. Non-npm managers use explicit chains independent of lifecycle settings.
|
|
6920
|
+
prebuildWired: packageManager === "npm" && /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
|
|
6921
|
+
postbuildWired: packageManager === "npm" && (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
6459
6922
|
widgetInstalled: widget.found,
|
|
6460
6923
|
widgetTokenMatches: widget.uuidMatches,
|
|
6461
6924
|
widgetOptOut,
|
|
@@ -6525,10 +6988,10 @@ function buildScriptLines(state) {
|
|
|
6525
6988
|
const lines = [];
|
|
6526
6989
|
if (!state.installScanWired) lines.push('"postinstall": "patchstack-connect scan"');
|
|
6527
6990
|
if (state.hasBuildScript && !(state.prebuildWired && state.postbuildWired)) {
|
|
6528
|
-
if (state.packageManager
|
|
6529
|
-
lines.push('"build": "patchstack-connect scan && <existing build command> && patchstack-connect mark-build"');
|
|
6991
|
+
if (state.packageManager !== "npm") {
|
|
6992
|
+
lines.push('"build": "patchstack-connect scan && patchstack-connect map --upload && <existing build command> && patchstack-connect mark-build"');
|
|
6530
6993
|
} else {
|
|
6531
|
-
if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan"');
|
|
6994
|
+
if (!state.prebuildWired) lines.push('"prebuild": "patchstack-connect scan && patchstack-connect map --upload"');
|
|
6532
6995
|
if (!state.postbuildWired) lines.push('"postbuild": "patchstack-connect mark-build"');
|
|
6533
6996
|
}
|
|
6534
6997
|
}
|
|
@@ -6646,7 +7109,7 @@ import {
|
|
|
6646
7109
|
} from "fs";
|
|
6647
7110
|
import { tmpdir } from "os";
|
|
6648
7111
|
import path13 from "path";
|
|
6649
|
-
import { createHash as
|
|
7112
|
+
import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
|
|
6650
7113
|
function baseFrom(manifestEndpoint) {
|
|
6651
7114
|
const url = new URL(manifestEndpoint);
|
|
6652
7115
|
const p = url.pathname.replace(/\/$/, "");
|
|
@@ -6656,7 +7119,7 @@ function baseFrom(manifestEndpoint) {
|
|
|
6656
7119
|
return url.toString().replace(/\/$/, "");
|
|
6657
7120
|
}
|
|
6658
7121
|
function pendingPath(siteUuid, intent) {
|
|
6659
|
-
const key =
|
|
7122
|
+
const key = createHash3("sha256").update(siteUuid).digest("hex").slice(0, 16);
|
|
6660
7123
|
return path13.join(tmpdir(), `patchstack-${intent}-${key}.json`);
|
|
6661
7124
|
}
|
|
6662
7125
|
function savePending(siteUuid, intent, pending) {
|
|
@@ -6932,8 +7395,8 @@ async function claim2(config, onPrompt, deps = {}) {
|
|
|
6932
7395
|
import { relative as relative5 } from "path";
|
|
6933
7396
|
|
|
6934
7397
|
// src/protect/install/runtime/entry.ts
|
|
6935
|
-
import { existsSync as
|
|
6936
|
-
import { join as
|
|
7398
|
+
import { existsSync as existsSync16, readFileSync as readFileSync13, realpathSync as realpathSync4, statSync as statSync6 } from "fs";
|
|
7399
|
+
import { join as join19, resolve as resolve4, sep as sep2 } from "path";
|
|
6937
7400
|
var CONVENTIONAL = [
|
|
6938
7401
|
"server.js",
|
|
6939
7402
|
"server.mjs",
|
|
@@ -6959,7 +7422,7 @@ var CONVENTIONAL = [
|
|
|
6959
7422
|
var LOADABLE = /\.(?:js|mjs|cjs)$/;
|
|
6960
7423
|
function readPackage2(cwd) {
|
|
6961
7424
|
try {
|
|
6962
|
-
const parsed = JSON.parse(readFileSync13(
|
|
7425
|
+
const parsed = JSON.parse(readFileSync13(join19(cwd, "package.json"), "utf8"));
|
|
6963
7426
|
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
6964
7427
|
} catch {
|
|
6965
7428
|
return null;
|
|
@@ -7029,14 +7492,14 @@ function resolveEntry(cwd) {
|
|
|
7029
7492
|
}
|
|
7030
7493
|
return {
|
|
7031
7494
|
kind: "unavailable",
|
|
7032
|
-
reason:
|
|
7495
|
+
reason: existsSync16(join19(cwd, "package.json")) ? 'no directly loadable entry was found \u2014 declare one as a "start" script of the form `node <file>`, or run the check against a built output' : "there is no package.json here"
|
|
7033
7496
|
};
|
|
7034
7497
|
}
|
|
7035
7498
|
|
|
7036
7499
|
// src/protect/install/runtime/probe.ts
|
|
7037
7500
|
import { spawn } from "child_process";
|
|
7038
|
-
import { createHash as
|
|
7039
|
-
import { existsSync as
|
|
7501
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
7502
|
+
import { existsSync as existsSync17 } from "fs";
|
|
7040
7503
|
import { request as httpRequest } from "http";
|
|
7041
7504
|
import { request as httpsRequest, Agent as HttpsAgent } from "https";
|
|
7042
7505
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -7053,7 +7516,7 @@ var ANSWER_LIMIT = 8192;
|
|
|
7053
7516
|
var LOOPBACK = ["127.0.0.1", "::1"];
|
|
7054
7517
|
var ENDING_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP", "SIGQUIT"];
|
|
7055
7518
|
function expectedAnswerFor(challenge) {
|
|
7056
|
-
return
|
|
7519
|
+
return createHash4("sha256").update(`patchstack-verify:${challenge}`).digest("hex");
|
|
7057
7520
|
}
|
|
7058
7521
|
function preloadPath() {
|
|
7059
7522
|
const candidates = [
|
|
@@ -7064,7 +7527,7 @@ function preloadPath() {
|
|
|
7064
7527
|
];
|
|
7065
7528
|
for (const candidate of candidates) {
|
|
7066
7529
|
const path17 = fileURLToPath2(candidate);
|
|
7067
|
-
if (
|
|
7530
|
+
if (existsSync17(path17)) return path17;
|
|
7068
7531
|
}
|
|
7069
7532
|
return null;
|
|
7070
7533
|
}
|
|
@@ -7414,12 +7877,12 @@ function formatRuntimeCheck(report) {
|
|
|
7414
7877
|
import path14 from "path";
|
|
7415
7878
|
|
|
7416
7879
|
// src/map/ts-loader.ts
|
|
7417
|
-
import { createRequire as
|
|
7880
|
+
import { createRequire as createRequire5 } from "module";
|
|
7418
7881
|
import { pathToFileURL } from "url";
|
|
7419
|
-
import { join as
|
|
7882
|
+
import { join as join20 } from "path";
|
|
7420
7883
|
async function loadTypeScript(cwd) {
|
|
7421
7884
|
try {
|
|
7422
|
-
const req =
|
|
7885
|
+
const req = createRequire5(pathToFileURL(join20(cwd, "package.json")));
|
|
7423
7886
|
const resolved = req.resolve("typescript");
|
|
7424
7887
|
const mod = await import(pathToFileURL(resolved).href);
|
|
7425
7888
|
return mod.default ?? mod;
|
|
@@ -7435,7 +7898,7 @@ async function loadTypeScript(cwd) {
|
|
|
7435
7898
|
|
|
7436
7899
|
// src/map/extract.ts
|
|
7437
7900
|
import { readFileSync as readFileSync16, realpathSync as realpathSync6 } from "fs";
|
|
7438
|
-
import { createHash as
|
|
7901
|
+
import { createHash as createHash6 } from "crypto";
|
|
7439
7902
|
import { relative as relative7 } from "path";
|
|
7440
7903
|
|
|
7441
7904
|
// src/map/bindings.ts
|
|
@@ -7565,7 +8028,7 @@ function npmPackageOf(spec) {
|
|
|
7565
8028
|
}
|
|
7566
8029
|
|
|
7567
8030
|
// src/map/sinks.ts
|
|
7568
|
-
import { createHash as
|
|
8031
|
+
import { createHash as createHash5 } from "crypto";
|
|
7569
8032
|
|
|
7570
8033
|
// src/map/capabilities.ts
|
|
7571
8034
|
var PACKAGE_SINK_MODELS = [
|
|
@@ -7964,7 +8427,7 @@ function argumentRoleOf(sinkKind, method, index, total = 0) {
|
|
|
7964
8427
|
return table?.[index] ?? "unknown";
|
|
7965
8428
|
}
|
|
7966
8429
|
function sinkId(s, owner) {
|
|
7967
|
-
return
|
|
8430
|
+
return createHash5("sha256").update([s.kind, s.provider, s.package, s.table, s.op, s.file ?? owner, s.start, s.end].join("|")).digest("hex").slice(0, 12);
|
|
7968
8431
|
}
|
|
7969
8432
|
function dedupeSinks(sinks, owner) {
|
|
7970
8433
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -8986,11 +9449,11 @@ function capEndpoint(e, limits) {
|
|
|
8986
9449
|
drop((f) => kept.has(byIndex.get(f)));
|
|
8987
9450
|
}
|
|
8988
9451
|
const ranked = [...flows].sort((a, b) => keepPriority(b) - keepPriority(a));
|
|
8989
|
-
const pick = (items, limit,
|
|
9452
|
+
const pick = (items, limit, idOf2) => {
|
|
8990
9453
|
if (items.length <= limit) return { kept: items, omitted: 0 };
|
|
8991
9454
|
const wanted = /* @__PURE__ */ new Set();
|
|
8992
9455
|
for (const f of ranked) {
|
|
8993
|
-
const id =
|
|
9456
|
+
const id = idOf2(f);
|
|
8994
9457
|
if (id !== void 0 && wanted.size < limit) wanted.add(id);
|
|
8995
9458
|
}
|
|
8996
9459
|
const chosen = new Set(items.filter((it) => it.id !== void 0 && wanted.has(it.id)));
|
|
@@ -9090,7 +9553,7 @@ function withNote(doc) {
|
|
|
9090
9553
|
|
|
9091
9554
|
// src/map/module-graph.ts
|
|
9092
9555
|
import { readFileSync as readFileSync14, realpathSync as realpathSync5, statSync as statSync7 } from "fs";
|
|
9093
|
-
import { dirname as dirname9, join as
|
|
9556
|
+
import { dirname as dirname9, join as join21, relative as relative6, resolve as resolvePath } from "path";
|
|
9094
9557
|
var RESOLVE_EXTS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
|
|
9095
9558
|
function createModuleGraph(ts, opts) {
|
|
9096
9559
|
const cache = /* @__PURE__ */ new Map();
|
|
@@ -9168,7 +9631,7 @@ function resolveRelativeModule(fromFile, spec) {
|
|
|
9168
9631
|
}
|
|
9169
9632
|
candidates.push(base);
|
|
9170
9633
|
for (const e of RESOLVE_EXTS) candidates.push(base + e);
|
|
9171
|
-
for (const e of RESOLVE_EXTS) candidates.push(
|
|
9634
|
+
for (const e of RESOLVE_EXTS) candidates.push(join21(base, "index" + e));
|
|
9172
9635
|
for (const c of candidates) {
|
|
9173
9636
|
try {
|
|
9174
9637
|
if (statSync7(c).isFile()) return c;
|
|
@@ -9458,7 +9921,7 @@ function handlerEntry(name, kindLabel, params, body, ts, localSinks, bindings, c
|
|
|
9458
9921
|
|
|
9459
9922
|
// src/map/imports.ts
|
|
9460
9923
|
import { readFileSync as readFileSync15 } from "fs";
|
|
9461
|
-
import { join as
|
|
9924
|
+
import { join as join23 } from "path";
|
|
9462
9925
|
var MAX_SITES2 = 5;
|
|
9463
9926
|
var NPM_NAME = /^(?:@[^/@\s~][^/@\s]*\/)?[^/@\s.~][^/@\s]*$/;
|
|
9464
9927
|
function isNpmPackageName(pkg) {
|
|
@@ -9469,7 +9932,7 @@ function readPathAliases(cwd) {
|
|
|
9469
9932
|
for (const name of ["tsconfig.json", "jsconfig.json"]) {
|
|
9470
9933
|
let paths2;
|
|
9471
9934
|
try {
|
|
9472
|
-
const raw = readFileSync15(
|
|
9935
|
+
const raw = readFileSync15(join23(cwd, name), "utf8");
|
|
9473
9936
|
const parsed = JSON.parse(stripJsonComments(raw));
|
|
9474
9937
|
paths2 = parsed.compilerOptions?.paths;
|
|
9475
9938
|
} catch {
|
|
@@ -9736,7 +10199,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
9736
10199
|
let sourceBytes = 0;
|
|
9737
10200
|
const calls = { total: 0, dependency: 0, local: 0, ambiguous: 0 };
|
|
9738
10201
|
const startedAt = Date.now();
|
|
9739
|
-
const
|
|
10202
|
+
const entryFiles2 = /* @__PURE__ */ new Set();
|
|
9740
10203
|
const hopTargets = /* @__PURE__ */ new Set();
|
|
9741
10204
|
for (const file of files) {
|
|
9742
10205
|
try {
|
|
@@ -9762,8 +10225,8 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
9762
10225
|
continue;
|
|
9763
10226
|
}
|
|
9764
10227
|
parsed++;
|
|
9765
|
-
|
|
9766
|
-
const fingerprint =
|
|
10228
|
+
entryFiles2.add(file);
|
|
10229
|
+
const fingerprint = createHash6("sha256").update(text).digest("hex").slice(0, 16);
|
|
9767
10230
|
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, file));
|
|
9768
10231
|
const bindings = buildModuleBindings(sf, ts);
|
|
9769
10232
|
for (const specifier of bindings.imports) {
|
|
@@ -9816,7 +10279,7 @@ async function extractInputMap(cwd, ts, options = {}) {
|
|
|
9816
10279
|
let hopParsed = 0;
|
|
9817
10280
|
let hopFailures = 0;
|
|
9818
10281
|
for (const target of hopTargets) {
|
|
9819
|
-
if (
|
|
10282
|
+
if (entryFiles2.has(target)) continue;
|
|
9820
10283
|
try {
|
|
9821
10284
|
const text = readFileSync16(target, "utf8");
|
|
9822
10285
|
const sf = ts.createSourceFile(target, text, ts.ScriptTarget.Latest, true, guessScriptKind(ts, target));
|
|
@@ -9982,7 +10445,7 @@ function getStringFlag(flags, name) {
|
|
|
9982
10445
|
|
|
9983
10446
|
// src/build-stamp.ts
|
|
9984
10447
|
import { lstatSync as lstatSync5, readFileSync as readFileSync17, readdirSync as readdirSync7, realpathSync as realpathSync7 } from "fs";
|
|
9985
|
-
import { join as
|
|
10448
|
+
import { join as join24, relative as relative8, sep as sep3 } from "path";
|
|
9986
10449
|
var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
|
|
9987
10450
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
|
|
9988
10451
|
var MAX_DEPTH2 = 6;
|
|
@@ -10000,7 +10463,7 @@ function guardImportsRulesFile(dir, rulesName) {
|
|
|
10000
10463
|
}
|
|
10001
10464
|
for (const name of siblings) {
|
|
10002
10465
|
if (!GUARD_SOURCE.test(name)) continue;
|
|
10003
|
-
const path17 =
|
|
10466
|
+
const path17 = join24(dir, name);
|
|
10004
10467
|
try {
|
|
10005
10468
|
const entry = lstatSync5(path17);
|
|
10006
10469
|
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
|
|
@@ -10043,7 +10506,7 @@ function findRulesFile(cwd) {
|
|
|
10043
10506
|
}
|
|
10044
10507
|
for (const name of entries) {
|
|
10045
10508
|
if (name.startsWith(".")) continue;
|
|
10046
|
-
const path17 =
|
|
10509
|
+
const path17 = join24(dir, name);
|
|
10047
10510
|
let entry;
|
|
10048
10511
|
try {
|
|
10049
10512
|
entry = lstatSync5(path17);
|
|
@@ -10121,6 +10584,8 @@ function applyBuildStamp(cwd, id) {
|
|
|
10121
10584
|
}
|
|
10122
10585
|
|
|
10123
10586
|
// src/build-hook.ts
|
|
10587
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
10588
|
+
import { join as join25 } from "path";
|
|
10124
10589
|
var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
|
|
10125
10590
|
"preinstall",
|
|
10126
10591
|
"install",
|
|
@@ -10134,11 +10599,16 @@ function isInstallOrBuildHook(env = process.env) {
|
|
|
10134
10599
|
const event = env.npm_lifecycle_event;
|
|
10135
10600
|
return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
|
|
10136
10601
|
}
|
|
10137
|
-
function isPreBundleBuildHook(env = process.env) {
|
|
10602
|
+
function isPreBundleBuildHook(env = process.env, cwd = process.cwd()) {
|
|
10138
10603
|
const event = env.npm_lifecycle_event;
|
|
10139
10604
|
if (event === "prebuild") return true;
|
|
10140
10605
|
if (event !== "build") return false;
|
|
10141
|
-
|
|
10606
|
+
try {
|
|
10607
|
+
const pkg = JSON.parse(readFileSync18(join25(cwd, "package.json"), "utf8"));
|
|
10608
|
+
return typeof pkg.scripts?.build === "string" && /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg.scripts.build);
|
|
10609
|
+
} catch {
|
|
10610
|
+
return false;
|
|
10611
|
+
}
|
|
10142
10612
|
}
|
|
10143
10613
|
function undeliveredReportLines(err, config, cwd) {
|
|
10144
10614
|
const lines = [`patchstack: could not send the package list to Patchstack \u2014 ${err.message}`];
|
|
@@ -10162,7 +10632,7 @@ function undeliveredReportLines(err, config, cwd) {
|
|
|
10162
10632
|
}
|
|
10163
10633
|
|
|
10164
10634
|
// src/input-map-id.ts
|
|
10165
|
-
import { createHash as
|
|
10635
|
+
import { createHash as createHash7 } from "crypto";
|
|
10166
10636
|
var NonCanonicalInputMap = class extends Error {
|
|
10167
10637
|
};
|
|
10168
10638
|
function canonicalString(value) {
|
|
@@ -10206,27 +10676,32 @@ function inputMapBuildId(map) {
|
|
|
10206
10676
|
material = { ...mapDocument, coverage: stableCoverage };
|
|
10207
10677
|
} else material = mapDocument;
|
|
10208
10678
|
}
|
|
10209
|
-
return
|
|
10679
|
+
return createHash7("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
|
|
10210
10680
|
}
|
|
10211
10681
|
|
|
10212
10682
|
// src/map-command.ts
|
|
10213
10683
|
async function runMap(flags) {
|
|
10684
|
+
return (await runMapDetailed(flags)).code;
|
|
10685
|
+
}
|
|
10686
|
+
async function runMapDetailed(flags, options = {}) {
|
|
10687
|
+
const log2 = options.log ?? console.error;
|
|
10214
10688
|
const cwd = getStringFlag(flags, "dir") ?? process.cwd();
|
|
10689
|
+
if (options.setup) applyBuildStamp(cwd, null);
|
|
10215
10690
|
const { map, error } = await buildInputMap(cwd, {
|
|
10216
10691
|
followSymlinks: flags.get("follow-symlinks") === true
|
|
10217
10692
|
});
|
|
10218
10693
|
if (!map) {
|
|
10219
|
-
|
|
10220
|
-
return 1;
|
|
10694
|
+
log2(`patchstack: ${error}`);
|
|
10695
|
+
return { code: isPreBundleBuildHook() ? 0 : 1, error: error ?? "could not analyse the project" };
|
|
10221
10696
|
}
|
|
10222
10697
|
const inputs = map.endpoints.reduce((n, e) => n + e.inputs.length, 0);
|
|
10223
10698
|
const sinks = map.endpoints.reduce((n, e) => n + e.sinks.length, 0);
|
|
10224
10699
|
const proven = map.endpoints.reduce((n, e) => n + e.flows.filter((f) => isProvenFlow(f.confidence)).length, 0);
|
|
10225
10700
|
const c = map.coverage;
|
|
10226
|
-
|
|
10701
|
+
log2(
|
|
10227
10702
|
`patchstack: ${map.endpoints.length} entry point(s), ${inputs} input(s), ${sinks} sink(s), ${proven} proven input\u2192sink flow(s) [${map.framework}].`
|
|
10228
10703
|
);
|
|
10229
|
-
|
|
10704
|
+
log2(
|
|
10230
10705
|
// All three buckets, explicitly: "6/66 parsed" reads as "91% unanalysed" when the other 60 files
|
|
10231
10706
|
// simply contain no server entry point (most of a project is client code). Only `skipped` is a
|
|
10232
10707
|
// failure to analyse.
|
|
@@ -10239,14 +10714,14 @@ async function runMap(flags) {
|
|
|
10239
10714
|
const ambiguous = c2.callsAmbiguous ?? 0;
|
|
10240
10715
|
const denominator = dependency + ambiguous;
|
|
10241
10716
|
const quality = denominator > 0 ? Math.round(100 * dependency / denominator) : 100;
|
|
10242
|
-
|
|
10717
|
+
log2(
|
|
10243
10718
|
`patchstack: ${invoked.length} dependency API call(s) resolved across ${new Set(invoked.map((i) => i.package)).size} package(s) from ${c2.callsTotal ?? 0} call site(s) \u2014 ${quality}% of dependency-candidate receivers resolved (${c2.callsLocal ?? 0} local, ${ambiguous} ambiguous). Positive evidence only: absence here never means an API is not called.`
|
|
10244
10719
|
);
|
|
10245
10720
|
}
|
|
10246
10721
|
const imported = map.imports ?? [];
|
|
10247
10722
|
if (imported.length > 0) {
|
|
10248
10723
|
const unmodelled = imported.filter((d) => d.recognizedSinkKinds.length === 0).length;
|
|
10249
|
-
|
|
10724
|
+
log2(
|
|
10250
10725
|
`patchstack: ${imported.length} package(s) imported \u2014 ${unmodelled} with no recognized sink family, so a vulnerability in those cannot be judged reachable or unreachable from this map.`
|
|
10251
10726
|
);
|
|
10252
10727
|
}
|
|
@@ -10254,62 +10729,64 @@ async function runMap(flags) {
|
|
|
10254
10729
|
const out = getStringFlag(flags, "out");
|
|
10255
10730
|
if (out) {
|
|
10256
10731
|
atomicWriteFileSync(path14.resolve(out), json, { encoding: "utf8" });
|
|
10257
|
-
|
|
10732
|
+
log2(`patchstack: wrote ${out}`);
|
|
10258
10733
|
} else if (flags.get("upload") !== true) {
|
|
10259
10734
|
console.log(json);
|
|
10260
10735
|
}
|
|
10261
10736
|
if (flags.get("upload") === true) {
|
|
10262
10737
|
const problems = ingestProblems(map);
|
|
10263
10738
|
if (problems.length > 0) {
|
|
10264
|
-
|
|
10265
|
-
return 0;
|
|
10739
|
+
log2(`patchstack: did not upload the attack surface \u2014 it cannot be fitted to the size Patchstack accepts (${problems.join("; ")}).`);
|
|
10740
|
+
return { code: 0, endpoints: map.endpoints.length, error: problems.join("; ") };
|
|
10266
10741
|
}
|
|
10267
10742
|
if (map.endpoints.length === 0) {
|
|
10268
|
-
|
|
10743
|
+
log2(
|
|
10269
10744
|
"patchstack: no server entry points were recognized \u2014 uploading the import inventory and coverage notes anyway, so a vulnerability can still be judged imported or not. Nothing here can decide whether a request reaches it."
|
|
10270
10745
|
);
|
|
10271
10746
|
}
|
|
10272
|
-
const config = await resolveConfig({
|
|
10747
|
+
const config = options.config ?? await resolveConfig({
|
|
10273
10748
|
cwd,
|
|
10274
10749
|
cliSiteUuid: getStringFlag(flags, "site-uuid"),
|
|
10275
10750
|
cliEndpoint: getStringFlag(flags, "endpoint")
|
|
10276
10751
|
});
|
|
10277
10752
|
let buildId = null;
|
|
10278
|
-
if (isPreBundleBuildHook()) {
|
|
10753
|
+
if (options.setup || isPreBundleBuildHook()) {
|
|
10279
10754
|
const candidate = inputMapBuildId(map);
|
|
10280
10755
|
const stamp = applyBuildStamp(cwd, candidate);
|
|
10281
10756
|
if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
|
|
10282
10757
|
buildId = candidate;
|
|
10283
|
-
|
|
10758
|
+
log2(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
|
|
10284
10759
|
} else {
|
|
10285
10760
|
const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
|
|
10286
|
-
|
|
10761
|
+
log2(
|
|
10287
10762
|
`patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
|
|
10288
10763
|
);
|
|
10289
10764
|
}
|
|
10290
10765
|
} else {
|
|
10291
|
-
|
|
10766
|
+
log2(
|
|
10292
10767
|
"patchstack: no runtime binding recorded \u2014 run `map --upload` in a prebuild hook before the bundler, so rules generated from these coordinates can be tied to the runtime guard. Until then they detect only, not block."
|
|
10293
10768
|
);
|
|
10294
10769
|
}
|
|
10295
10770
|
const outcome = await postInputMap(config, map, buildId);
|
|
10296
10771
|
if (outcome.result === "stored") {
|
|
10297
|
-
|
|
10772
|
+
log2(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
10298
10773
|
} else if (outcome.result === "unchanged") {
|
|
10299
|
-
|
|
10774
|
+
log2(`patchstack: attack surface unchanged since revision ${outcome.revision} \u2014 nothing to store.`);
|
|
10300
10775
|
} else if (outcome.result === "skipped") {
|
|
10301
|
-
|
|
10776
|
+
log2(`patchstack: did not upload the attack surface \u2014 ${outcome.message}`);
|
|
10302
10777
|
} else {
|
|
10303
|
-
|
|
10778
|
+
log2(`patchstack: could not upload the attack surface \u2014 ${outcome.message}`);
|
|
10304
10779
|
}
|
|
10780
|
+
return { code: 0, endpoints: map.endpoints.length, buildId, upload: outcome };
|
|
10305
10781
|
}
|
|
10306
|
-
return 0;
|
|
10782
|
+
return { code: 0, endpoints: map.endpoints.length };
|
|
10307
10783
|
}
|
|
10308
10784
|
|
|
10309
10785
|
// src/setup.ts
|
|
10310
|
-
import { readFileSync as
|
|
10786
|
+
import { readFileSync as readFileSync19 } from "fs";
|
|
10311
10787
|
import path15 from "path";
|
|
10312
10788
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
10789
|
+
var MAP_COMMAND = "patchstack-connect map --upload";
|
|
10313
10790
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
10314
10791
|
function setupProtection(cwd) {
|
|
10315
10792
|
const { result: install, lines: log2 } = collectProtectLog(() => runProtect(cwd));
|
|
@@ -10329,12 +10806,65 @@ function prependHook(existing, command) {
|
|
|
10329
10806
|
if (existing === void 0 || existing.trim().length === 0) return command;
|
|
10330
10807
|
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
10331
10808
|
if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
|
|
10332
|
-
const remaining = existing
|
|
10809
|
+
const remaining = splitAndChain(existing).filter((part) => part.trim() !== command && part.trim().length > 0);
|
|
10333
10810
|
return [command, ...remaining].join(" && ");
|
|
10334
10811
|
}
|
|
10812
|
+
function splitAndChain(source) {
|
|
10813
|
+
const parts = [];
|
|
10814
|
+
let start = 0;
|
|
10815
|
+
let quote = "";
|
|
10816
|
+
const groups = [];
|
|
10817
|
+
for (let i = 0; i < source.length; i++) {
|
|
10818
|
+
const char = source[i];
|
|
10819
|
+
if (char === "\\" && quote !== "'") {
|
|
10820
|
+
i++;
|
|
10821
|
+
continue;
|
|
10822
|
+
}
|
|
10823
|
+
if (quote === "'" || quote === "`") {
|
|
10824
|
+
if (char === quote) quote = quote === "`" ? groups.pop().quote : "";
|
|
10825
|
+
continue;
|
|
10826
|
+
}
|
|
10827
|
+
if (char === "`") {
|
|
10828
|
+
groups.push({ close: "`", quote });
|
|
10829
|
+
quote = "`";
|
|
10830
|
+
continue;
|
|
10831
|
+
}
|
|
10832
|
+
if (char === "$" && (source[i + 1] === "(" || source[i + 1] === "{")) {
|
|
10833
|
+
groups.push({ close: source[++i] === "(" ? ")" : "}", quote });
|
|
10834
|
+
quote = "";
|
|
10835
|
+
continue;
|
|
10836
|
+
}
|
|
10837
|
+
if (quote) {
|
|
10838
|
+
if (char === quote) quote = "";
|
|
10839
|
+
continue;
|
|
10840
|
+
}
|
|
10841
|
+
if (char === '"' || char === "'") {
|
|
10842
|
+
quote = char;
|
|
10843
|
+
continue;
|
|
10844
|
+
}
|
|
10845
|
+
if (char === "#" && (i === 0 || /\s/.test(source[i - 1]))) {
|
|
10846
|
+
const newline = source.indexOf("\n", i);
|
|
10847
|
+
if (newline === -1) break;
|
|
10848
|
+
i = newline;
|
|
10849
|
+
continue;
|
|
10850
|
+
}
|
|
10851
|
+
if (char === "(" || char === "{") groups.push({ close: char === "(" ? ")" : "}", quote: "" });
|
|
10852
|
+
if (char === groups.at(-1)?.close) quote = groups.pop().quote;
|
|
10853
|
+
if (groups.length === 0 && char === "&" && source[i + 1] === "&") {
|
|
10854
|
+
parts.push(source.slice(start, i).trim());
|
|
10855
|
+
start = ++i + 1;
|
|
10856
|
+
}
|
|
10857
|
+
}
|
|
10858
|
+
parts.push(source.slice(start).trim());
|
|
10859
|
+
return parts;
|
|
10860
|
+
}
|
|
10861
|
+
function finishWithMap(existing) {
|
|
10862
|
+
if (/(?:^|&&|;)\s*patchstack-connect map --upload\s*$/.test(existing)) return existing;
|
|
10863
|
+
return `${existing} && ${MAP_COMMAND}`;
|
|
10864
|
+
}
|
|
10335
10865
|
function wireBuildScripts(cwd, packageManager) {
|
|
10336
10866
|
const target = path15.join(cwd, "package.json");
|
|
10337
|
-
const raw =
|
|
10867
|
+
const raw = readFileSync19(target, "utf8");
|
|
10338
10868
|
const pkg = JSON.parse(raw);
|
|
10339
10869
|
const scripts = pkg.scripts ?? {};
|
|
10340
10870
|
const build = scripts.build;
|
|
@@ -10348,8 +10878,11 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
10348
10878
|
};
|
|
10349
10879
|
}
|
|
10350
10880
|
scripts.postinstall = postinstall;
|
|
10351
|
-
} else if (packageManager
|
|
10881
|
+
} else if (packageManager !== "npm") {
|
|
10352
10882
|
let nextBuild = prependHook(build, SCAN_COMMAND);
|
|
10883
|
+
if (!/^\s*patchstack-connect scan\s*&&\s*patchstack-connect map --upload(?:\s*(?:&&|;)|\s*$)/.test(nextBuild)) {
|
|
10884
|
+
nextBuild = nextBuild.replace(SCAN_COMMAND, `${SCAN_COMMAND} && ${MAP_COMMAND}`);
|
|
10885
|
+
}
|
|
10353
10886
|
if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
|
|
10354
10887
|
nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
|
|
10355
10888
|
}
|
|
@@ -10363,7 +10896,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
10363
10896
|
scripts.postinstall = postinstall;
|
|
10364
10897
|
scripts.build = nextBuild;
|
|
10365
10898
|
} else {
|
|
10366
|
-
const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
|
|
10899
|
+
const prebuild = finishWithMap(prependHook(scripts.prebuild, SCAN_COMMAND));
|
|
10367
10900
|
const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
|
|
10368
10901
|
if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
|
|
10369
10902
|
return {
|
|
@@ -10388,100 +10921,1894 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
10388
10921
|
detail: "added a dependency-install scan; package.json has no build script."
|
|
10389
10922
|
};
|
|
10390
10923
|
}
|
|
10391
|
-
return packageManager
|
|
10924
|
+
return packageManager !== "npm" ? {
|
|
10392
10925
|
changed: true,
|
|
10393
10926
|
strategy: "build-chain",
|
|
10394
|
-
detail: "added a dependency-install scan and chained scan/mark-build around the build."
|
|
10927
|
+
detail: "added a dependency-install scan and chained scan/map-upload/mark-build around the build."
|
|
10395
10928
|
} : {
|
|
10396
10929
|
changed: true,
|
|
10397
10930
|
strategy: "lifecycle-hooks",
|
|
10398
|
-
detail: "added scans to postinstall/prebuild and mark-build to postbuild."
|
|
10931
|
+
detail: "added scans to postinstall/prebuild, map upload before bundling, and mark-build to postbuild."
|
|
10399
10932
|
};
|
|
10400
10933
|
}
|
|
10401
10934
|
|
|
10402
|
-
// src/
|
|
10403
|
-
|
|
10935
|
+
// src/setup-sync.ts
|
|
10936
|
+
import { join as join26 } from "path";
|
|
10404
10937
|
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
10409
|
-
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
|
|
10439
|
-
|
|
10440
|
-
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
10447
|
-
|
|
10448
|
-
|
|
10449
|
-
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
10454
|
-
|
|
10455
|
-
|
|
10456
|
-
|
|
10457
|
-
|
|
10458
|
-
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
|
|
10465
|
-
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10938
|
+
// src/protect/rules/contract.js
|
|
10939
|
+
var SOURCES = Object.freeze({
|
|
10940
|
+
raw: Object.freeze({ keyed: false }),
|
|
10941
|
+
all: Object.freeze({ keyed: false }),
|
|
10942
|
+
false: Object.freeze({ keyed: false }),
|
|
10943
|
+
rules: Object.freeze({ keyed: false, group: true }),
|
|
10944
|
+
get: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
10945
|
+
post: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
10946
|
+
request: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
10947
|
+
cookie: Object.freeze({ keyed: true, keys: "any", wildcard: true }),
|
|
10948
|
+
// A bare `files.<name>` resolves to the filename; an optional trailing attribute selects another part
|
|
10949
|
+
// of the upload. Optional, not required — filename-scoped rules use the bare form.
|
|
10950
|
+
files: Object.freeze({
|
|
10951
|
+
keyed: true,
|
|
10952
|
+
keys: "any",
|
|
10953
|
+
wildcard: true,
|
|
10954
|
+
optional_key_suffixes: Object.freeze(["content", "filename", "type"])
|
|
10955
|
+
}),
|
|
10956
|
+
server: Object.freeze({
|
|
10957
|
+
keyed: true,
|
|
10958
|
+
keys: Object.freeze([
|
|
10959
|
+
"REQUEST_URI",
|
|
10960
|
+
"REQUEST_METHOD",
|
|
10961
|
+
"HTTP_USER_AGENT",
|
|
10962
|
+
"HTTP_REFERER",
|
|
10963
|
+
"HTTP_HOST",
|
|
10964
|
+
"REMOTE_ADDR",
|
|
10965
|
+
"ip",
|
|
10966
|
+
"CONTENT_TYPE",
|
|
10967
|
+
"CONTENT_LENGTH"
|
|
10968
|
+
]),
|
|
10969
|
+
key_prefixes: Object.freeze(["HTTP_"])
|
|
10970
|
+
}),
|
|
10971
|
+
response: Object.freeze({
|
|
10972
|
+
keyed: true,
|
|
10973
|
+
keys: Object.freeze(["status", "body", "headers"]),
|
|
10974
|
+
key_prefixes: Object.freeze(["header."])
|
|
10975
|
+
}),
|
|
10976
|
+
egress: Object.freeze({ keyed: true, keys: Object.freeze(["url", "host", "method"]) })
|
|
10977
|
+
});
|
|
10978
|
+
var MATCH_TYPES = Object.freeze([
|
|
10979
|
+
"array_in_array",
|
|
10980
|
+
"array_key_value",
|
|
10981
|
+
"contains",
|
|
10982
|
+
"cors_reflected",
|
|
10983
|
+
"cross_origin",
|
|
10984
|
+
"ctype_alnum",
|
|
10985
|
+
"ctype_digit",
|
|
10986
|
+
"ctype_special",
|
|
10987
|
+
"equals",
|
|
10988
|
+
"equals_strict",
|
|
10989
|
+
"hostname",
|
|
10990
|
+
"in_array",
|
|
10991
|
+
"inline_js_xss",
|
|
10992
|
+
"inline_xss",
|
|
10993
|
+
"internal_host",
|
|
10994
|
+
"is_numeric",
|
|
10995
|
+
"isset",
|
|
10996
|
+
"jwt_claim_equals",
|
|
10997
|
+
"less_than",
|
|
10998
|
+
"more_than",
|
|
10999
|
+
"not_contains",
|
|
11000
|
+
"not_in_array",
|
|
11001
|
+
"off_origin",
|
|
11002
|
+
"quotes",
|
|
11003
|
+
"regex",
|
|
11004
|
+
"stripos"
|
|
11005
|
+
]);
|
|
11006
|
+
var PARAMETERLESS_MATCH_TYPES = Object.freeze(["cross_origin", "off_origin", "cors_reflected"]);
|
|
11007
|
+
var OPERANDLESS_MATCH_TYPES = Object.freeze([
|
|
11008
|
+
"isset",
|
|
11009
|
+
"quotes",
|
|
11010
|
+
"is_numeric",
|
|
11011
|
+
"ctype_alnum",
|
|
11012
|
+
"ctype_digit",
|
|
11013
|
+
"ctype_special",
|
|
11014
|
+
"inline_xss",
|
|
11015
|
+
"inline_js_xss",
|
|
11016
|
+
"internal_host",
|
|
11017
|
+
"cross_origin",
|
|
11018
|
+
"off_origin",
|
|
11019
|
+
"cors_reflected"
|
|
11020
|
+
]);
|
|
11021
|
+
var OPERAND_SHAPES = Object.freeze([
|
|
11022
|
+
"positive-number",
|
|
11023
|
+
"boolean",
|
|
11024
|
+
"non-empty-string",
|
|
11025
|
+
"regex-literal",
|
|
11026
|
+
"string-or-string-list",
|
|
11027
|
+
"scalar",
|
|
11028
|
+
"string",
|
|
11029
|
+
"numeric",
|
|
11030
|
+
"scalar-or-non-empty-list",
|
|
11031
|
+
"non-empty-list",
|
|
11032
|
+
"object",
|
|
11033
|
+
"non-empty-object",
|
|
11034
|
+
"non-empty-string-list",
|
|
11035
|
+
"header-name-map",
|
|
11036
|
+
"header-name-list",
|
|
11037
|
+
"cookie-flags"
|
|
11038
|
+
]);
|
|
11039
|
+
var COOKIE_FLAGS = Object.freeze(["httpOnly", "secure", "sameSite"]);
|
|
11040
|
+
var SAME_SITE_VALUES = Object.freeze(["Strict", "Lax", "None"]);
|
|
11041
|
+
var HEADER_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
11042
|
+
var REGEX_LITERAL = /^\/(.+?)\/([gimsuy]*)$/s;
|
|
11043
|
+
var MATCH_OPERANDS = Object.freeze({
|
|
11044
|
+
array_key_value: Object.freeze({
|
|
11045
|
+
required: Object.freeze(["key", "match"]),
|
|
11046
|
+
shapes: Object.freeze({ key: "string-or-string-list", match: "object" })
|
|
11047
|
+
}),
|
|
11048
|
+
// Decides on a JWT's DECODED payload rather than its shape, and yields the matching token spans so
|
|
11049
|
+
// `redact` masks only those tokens. `claim` names an own, top-level claim; `value` is compared with
|
|
11050
|
+
// exact string equality. Anything not positively identified — undecodable, not JSON, not an object,
|
|
11051
|
+
// oversized, claim missing, claim not a string, or a different value — is a no-match.
|
|
11052
|
+
jwt_claim_equals: Object.freeze({
|
|
11053
|
+
required: Object.freeze(["claim", "value"]),
|
|
11054
|
+
shapes: Object.freeze({ claim: "non-empty-string", value: "non-empty-string" })
|
|
11055
|
+
}),
|
|
11056
|
+
regex: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "regex-literal" }) }),
|
|
11057
|
+
contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
11058
|
+
stripos: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
11059
|
+
not_contains: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
11060
|
+
// A hostname is compared to `new URL(value).hostname`, so an empty one only ever equals the empty
|
|
11061
|
+
// hostname of an opaque-scheme URL (`file:`, `mailto:`, `data:`) — never a host, and a false positive
|
|
11062
|
+
// on those. The other three are the list-membership family: an empty list makes `in_array` and
|
|
11063
|
+
// `array_in_array` inert and `not_in_array` true for every value.
|
|
11064
|
+
hostname: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-string" }) }),
|
|
11065
|
+
in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
11066
|
+
not_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar-or-non-empty-list" }) }),
|
|
11067
|
+
array_in_array: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "non-empty-list" }) }),
|
|
11068
|
+
// The comparison family. `equals` coerces, so any scalar can match. `equals_strict` does not, and its
|
|
11069
|
+
// subject is always a string by the time it is compared — so a number operand is unconditionally
|
|
11070
|
+
// inert, which is the trap the engine's own note about `'1' !== 1` describes.
|
|
11071
|
+
equals: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "scalar" }) }),
|
|
11072
|
+
equals_strict: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "string" }) }),
|
|
11073
|
+
less_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) }),
|
|
11074
|
+
more_than: Object.freeze({ required: Object.freeze(["value"]), shapes: Object.freeze({ value: "numeric" }) })
|
|
11075
|
+
});
|
|
11076
|
+
function operandShapeProblem(shape, value) {
|
|
11077
|
+
const isFilledString = (v) => typeof v === "string" && v.trim() !== "";
|
|
11078
|
+
const isScalar = (v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean";
|
|
11079
|
+
const isHeaderName = (v) => typeof v === "string" && HEADER_NAME.test(v);
|
|
11080
|
+
const stringLimit = (v) => typeof v === "string" && v.length > LIMITS.maxValueLength ? `must not be longer than ${LIMITS.maxValueLength} chars` : null;
|
|
11081
|
+
const listLimit = (v) => Array.isArray(v) && v.length > LIMITS.maxOperandItems ? `must not have more than ${LIMITS.maxOperandItems} entries` : null;
|
|
11082
|
+
const tooLong = stringLimit(value);
|
|
11083
|
+
if (tooLong) return tooLong;
|
|
11084
|
+
const tooMany = listLimit(value);
|
|
11085
|
+
if (tooMany) return tooMany;
|
|
11086
|
+
if (Array.isArray(value)) {
|
|
11087
|
+
const longMember = value.find((member) => stringLimit(member) !== null);
|
|
11088
|
+
if (longMember !== void 0) return `contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
11089
|
+
}
|
|
11090
|
+
switch (shape) {
|
|
11091
|
+
case "positive-number":
|
|
11092
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? null : "must be a finite positive number";
|
|
11093
|
+
case "boolean":
|
|
11094
|
+
return typeof value === "boolean" ? null : "must be true or false";
|
|
11095
|
+
case "non-empty-string":
|
|
11096
|
+
return isFilledString(value) ? null : "must be a non-empty string";
|
|
11097
|
+
case "regex-literal":
|
|
11098
|
+
if (!isFilledString(value)) return "must be a non-empty string";
|
|
11099
|
+
return REGEX_LITERAL.test(value) ? null : "must be a delimited pattern, as in /pattern/i";
|
|
11100
|
+
case "string-or-string-list":
|
|
11101
|
+
if (isFilledString(value)) return null;
|
|
11102
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isFilledString)) return null;
|
|
11103
|
+
return "must be a non-empty string, or a non-empty list of them";
|
|
11104
|
+
case "scalar":
|
|
11105
|
+
return isScalar(value) ? null : "must be a string, number or boolean";
|
|
11106
|
+
case "string":
|
|
11107
|
+
return typeof value === "string" ? null : "must be a string (this comparison is type-strict)";
|
|
11108
|
+
case "numeric":
|
|
11109
|
+
return isScalar(value) && Number.isFinite(Number(value)) ? null : "must be a number";
|
|
11110
|
+
case "scalar-or-non-empty-list":
|
|
11111
|
+
if (isScalar(value)) return null;
|
|
11112
|
+
if (Array.isArray(value) && value.length > 0 && value.every(isScalar)) return null;
|
|
11113
|
+
return "must be a scalar, or a non-empty list of them";
|
|
11114
|
+
case "non-empty-list":
|
|
11115
|
+
return Array.isArray(value) && value.length > 0 && value.every(isScalar) ? null : "must be a non-empty list of scalars";
|
|
11116
|
+
case "header-name-map": {
|
|
11117
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
11118
|
+
const names = Object.keys(value);
|
|
11119
|
+
if (names.length === 0) return "must be an object with at least one entry";
|
|
11120
|
+
if (names.length > LIMITS.maxMapEntries) return `must not have more than ${LIMITS.maxMapEntries} entries`;
|
|
11121
|
+
const bad = names.find((name) => !isHeaderName(name));
|
|
11122
|
+
if (bad !== void 0) return `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
11123
|
+
const badValue = Object.values(value).find((entry) => typeof entry !== "string" || entry.length > LIMITS.maxValueLength);
|
|
11124
|
+
return badValue === void 0 ? null : `values must be strings no longer than ${LIMITS.maxValueLength} chars`;
|
|
11125
|
+
}
|
|
11126
|
+
case "header-name-list": {
|
|
11127
|
+
if (!Array.isArray(value) || value.length === 0) return "must be a non-empty list of HTTP header names";
|
|
11128
|
+
const bad = value.find((name) => !isHeaderName(name));
|
|
11129
|
+
return bad === void 0 ? null : `names an invalid HTTP header, ${JSON.stringify(bad)}`;
|
|
11130
|
+
}
|
|
11131
|
+
case "cookie-flags": {
|
|
11132
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return "must be an object";
|
|
11133
|
+
const unknown = Object.keys(value).filter((key) => !COOKIE_FLAGS.includes(key));
|
|
11134
|
+
if (unknown.length > 0) return `has no flag named ${unknown.map((k) => JSON.stringify(k)).join(", ")}`;
|
|
11135
|
+
for (const flag of ["httpOnly", "secure"]) {
|
|
11136
|
+
if (value[flag] !== void 0 && typeof value[flag] !== "boolean") {
|
|
11137
|
+
return `flag "${flag}" must be true or false`;
|
|
11138
|
+
}
|
|
11139
|
+
}
|
|
11140
|
+
const sameSite = value.sameSite;
|
|
11141
|
+
const sameSiteOff = sameSite === false;
|
|
11142
|
+
if (sameSite !== void 0 && !sameSiteOff) {
|
|
11143
|
+
if (typeof sameSite !== "string" || !SAME_SITE_VALUES.some((v) => v.toLowerCase() === sameSite.toLowerCase())) {
|
|
11144
|
+
return `flag "sameSite" must be false, or one of ${SAME_SITE_VALUES.join(", ")}`;
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
if (value.httpOnly === false && value.secure === false && sameSiteOff) {
|
|
11148
|
+
return "turns off every flag, so it would harden nothing";
|
|
11149
|
+
}
|
|
11150
|
+
return null;
|
|
11151
|
+
}
|
|
11152
|
+
case "object":
|
|
11153
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? null : "must be an object";
|
|
11154
|
+
case "non-empty-object":
|
|
11155
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? null : "must be an object with at least one entry";
|
|
11156
|
+
case "non-empty-string-list":
|
|
11157
|
+
return Array.isArray(value) && value.length > 0 && value.every(isFilledString) ? null : "must be a non-empty list of non-empty strings";
|
|
11158
|
+
default:
|
|
11159
|
+
return null;
|
|
11160
|
+
}
|
|
11161
|
+
}
|
|
11162
|
+
var MUTATIONS = Object.freeze([
|
|
11163
|
+
"base64_decode",
|
|
11164
|
+
"getArrayValues",
|
|
11165
|
+
"htmlentitydecode",
|
|
11166
|
+
"intval",
|
|
11167
|
+
"json_decode",
|
|
11168
|
+
"json_encode",
|
|
11169
|
+
"urldecode"
|
|
11170
|
+
]);
|
|
11171
|
+
var PHASES = Object.freeze(["request", "response", "egress"]);
|
|
11172
|
+
var ACTIONS = Object.freeze([
|
|
11173
|
+
"block",
|
|
11174
|
+
"redact",
|
|
11175
|
+
"encode",
|
|
11176
|
+
"set-header",
|
|
11177
|
+
"remove-header",
|
|
11178
|
+
"harden-cookie"
|
|
11179
|
+
]);
|
|
11180
|
+
var RULE_PROPERTY_SHAPES = Object.freeze({
|
|
11181
|
+
max_bytes: "positive-number",
|
|
11182
|
+
bypass_limit: "boolean",
|
|
11183
|
+
ensure: "boolean",
|
|
11184
|
+
prefilter: "non-empty-string-list"
|
|
11185
|
+
});
|
|
11186
|
+
var ENFORCEMENT_VALUES = Object.freeze(["dry-run"]);
|
|
11187
|
+
var WHEN_KEYS = Object.freeze(["path", "method"]);
|
|
11188
|
+
var BUILD_SCOPE_PROPERTY = "build_scope";
|
|
11189
|
+
var BUILD_SCOPE = Object.freeze({
|
|
11190
|
+
applies_to: Object.freeze(["firewall"]),
|
|
11191
|
+
usable: Object.freeze({
|
|
11192
|
+
type: "string",
|
|
11193
|
+
trim: true,
|
|
11194
|
+
pattern: "^[0-9a-fA-F]{64}$",
|
|
11195
|
+
canonical: "lowercase"
|
|
11196
|
+
}),
|
|
11197
|
+
unreadable: "the rule remains accepted and detects only by default",
|
|
11198
|
+
local_override: "trustLocalRuleScope may explicitly enforce a caller-supplied rule"
|
|
11199
|
+
});
|
|
11200
|
+
var RULE_PROPERTIES = Object.freeze([
|
|
11201
|
+
"id",
|
|
11202
|
+
"rule_id",
|
|
11203
|
+
"title",
|
|
11204
|
+
"category",
|
|
11205
|
+
"phase",
|
|
11206
|
+
"action",
|
|
11207
|
+
"rule_v2",
|
|
11208
|
+
"when",
|
|
11209
|
+
"message",
|
|
11210
|
+
"enforcement",
|
|
11211
|
+
"source_revision",
|
|
11212
|
+
"prefilter",
|
|
11213
|
+
"max_bytes",
|
|
11214
|
+
"bypass_limit",
|
|
11215
|
+
"set_headers",
|
|
11216
|
+
"remove_headers",
|
|
11217
|
+
"cookie_flags",
|
|
11218
|
+
"ensure",
|
|
11219
|
+
"capture",
|
|
11220
|
+
BUILD_SCOPE_PROPERTY
|
|
11221
|
+
]);
|
|
11222
|
+
var CAPTURE_KEYS = Object.freeze(["version", "raw_chars"]);
|
|
11223
|
+
var NULL_EXEMPT_PROPERTIES = Object.freeze(["capture", BUILD_SCOPE_PROPERTY]);
|
|
11224
|
+
var ACTION_PROPERTIES = Object.freeze({
|
|
11225
|
+
block: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["request", "response", "egress"]) }),
|
|
11226
|
+
redact: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
11227
|
+
encode: Object.freeze({ required: Object.freeze([]), defaulted: Object.freeze([]), phases: Object.freeze(["response"]) }),
|
|
11228
|
+
"set-header": Object.freeze({
|
|
11229
|
+
required: Object.freeze(["set_headers"]),
|
|
11230
|
+
defaulted: Object.freeze(["ensure"]),
|
|
11231
|
+
phases: Object.freeze(["response"]),
|
|
11232
|
+
shapes: Object.freeze({ set_headers: "header-name-map" })
|
|
11233
|
+
}),
|
|
11234
|
+
"remove-header": Object.freeze({
|
|
11235
|
+
required: Object.freeze(["remove_headers"]),
|
|
11236
|
+
defaulted: Object.freeze([]),
|
|
11237
|
+
phases: Object.freeze(["response"]),
|
|
11238
|
+
shapes: Object.freeze({ remove_headers: "header-name-list" })
|
|
11239
|
+
}),
|
|
11240
|
+
"harden-cookie": Object.freeze({
|
|
11241
|
+
required: Object.freeze([]),
|
|
11242
|
+
defaulted: Object.freeze(["cookie_flags"]),
|
|
11243
|
+
phases: Object.freeze(["response"]),
|
|
11244
|
+
shapes: Object.freeze({ cookie_flags: "cookie-flags" })
|
|
11245
|
+
})
|
|
11246
|
+
});
|
|
11247
|
+
var LIMITS = Object.freeze({
|
|
11248
|
+
maxRules: 5e3,
|
|
11249
|
+
maxWhitelists: 2e3,
|
|
11250
|
+
maxConditionsPerRule: 250,
|
|
11251
|
+
maxConditionNodesPerRule: 1e3,
|
|
11252
|
+
maxConditionNodesPerBundle: 25e3,
|
|
11253
|
+
maxNestingDepth: 12,
|
|
11254
|
+
maxRegexLength: 1e3,
|
|
11255
|
+
maxValueLength: 8192,
|
|
11256
|
+
maxParameterLength: 512,
|
|
11257
|
+
maxParameterItems: 64,
|
|
11258
|
+
maxOperandItems: 256,
|
|
11259
|
+
maxMapEntries: 128,
|
|
11260
|
+
maxPathSegments: 64
|
|
11261
|
+
});
|
|
11262
|
+
var GROUP_PARAMETER = "rules";
|
|
11263
|
+
function isGroup(condition) {
|
|
11264
|
+
return condition?.parameter === GROUP_PARAMETER && Array.isArray(condition?.rules);
|
|
11265
|
+
}
|
|
11266
|
+
function conditionShapeProblem(condition) {
|
|
11267
|
+
if (!condition || typeof condition !== "object") return "condition is not an object";
|
|
11268
|
+
const named = condition.parameter === GROUP_PARAMETER;
|
|
11269
|
+
const nested = Array.isArray(condition.rules);
|
|
11270
|
+
if (named && !nested) {
|
|
11271
|
+
return `parameter "${GROUP_PARAMETER}" is a group and needs a "rules" array`;
|
|
11272
|
+
}
|
|
11273
|
+
if (nested && !named) {
|
|
11274
|
+
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
11275
|
+
}
|
|
11276
|
+
if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
|
|
11277
|
+
return "inclusive must be true or false";
|
|
11278
|
+
}
|
|
11279
|
+
if (named && nested) {
|
|
11280
|
+
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
11281
|
+
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
11282
|
+
if (condition.rules.length === 0) return "group has no conditions (would never match)";
|
|
11283
|
+
}
|
|
11284
|
+
return null;
|
|
11285
|
+
}
|
|
11286
|
+
function parameterProblem(parameter) {
|
|
11287
|
+
if (parameter === void 0 || parameter === null) return null;
|
|
11288
|
+
if (Array.isArray(parameter)) {
|
|
11289
|
+
if (parameter.length === 0) return "parameter list is empty";
|
|
11290
|
+
if (parameter.length > LIMITS.maxParameterItems) {
|
|
11291
|
+
return `parameter list has more than ${LIMITS.maxParameterItems} entries`;
|
|
11292
|
+
}
|
|
11293
|
+
for (const member of parameter) {
|
|
11294
|
+
if (Array.isArray(member)) return "a parameter list holds parameters, not more lists";
|
|
11295
|
+
if (member === void 0 || member === null) return "parameter list members must be non-empty strings";
|
|
11296
|
+
const problem = parameterProblem(member);
|
|
11297
|
+
if (problem !== null) return problem;
|
|
11298
|
+
}
|
|
11299
|
+
return null;
|
|
11300
|
+
}
|
|
11301
|
+
if (typeof parameter !== "string" || parameter === "") return "parameter must be a non-empty string";
|
|
11302
|
+
if (parameter.length > LIMITS.maxParameterLength) {
|
|
11303
|
+
return `parameter is longer than ${LIMITS.maxParameterLength} chars`;
|
|
11304
|
+
}
|
|
11305
|
+
if (parameter === GROUP_PARAMETER) {
|
|
11306
|
+
return `"${GROUP_PARAMETER}" is the group source and is only valid as a whole group, not as a parameter`;
|
|
11307
|
+
}
|
|
11308
|
+
const dot = parameter.indexOf(".");
|
|
11309
|
+
const name = dot === -1 ? parameter : parameter.slice(0, dot);
|
|
11310
|
+
const source = SOURCES[name];
|
|
11311
|
+
if (source === void 0) return `unknown parameter source "${name}"`;
|
|
11312
|
+
if (dot === -1) {
|
|
11313
|
+
return source.keyed === true ? `source "${name}" needs a key, e.g. "${name}.fieldname"` : null;
|
|
11314
|
+
}
|
|
11315
|
+
if (source.keyed !== true) return `source "${name}" takes no key`;
|
|
11316
|
+
const key = parameter.slice(dot + 1);
|
|
11317
|
+
if (key === "") return `source "${name}" needs a key after the dot`;
|
|
11318
|
+
return keyProblem(name, source, key);
|
|
11319
|
+
}
|
|
11320
|
+
function keyProblem(name, source, key) {
|
|
11321
|
+
if (key.endsWith("*") && source.wildcard !== true) {
|
|
11322
|
+
return `"${name}" does not fan out over a wildcard key`;
|
|
11323
|
+
}
|
|
11324
|
+
const bare = key.endsWith("*") ? key.slice(0, -1) : key;
|
|
11325
|
+
if (source.keys === "any") return null;
|
|
11326
|
+
if (Array.isArray(source.keys) && source.keys.includes(bare)) return null;
|
|
11327
|
+
for (const prefix of source.key_prefixes ?? []) {
|
|
11328
|
+
if (bare.startsWith(prefix) && bare.length > prefix.length) return null;
|
|
11329
|
+
}
|
|
11330
|
+
const accepted = [
|
|
11331
|
+
...Array.isArray(source.keys) ? source.keys : [],
|
|
11332
|
+
...(source.key_prefixes ?? []).map((p) => `${p}\u2026`)
|
|
11333
|
+
].join(", ");
|
|
11334
|
+
return `"${name}" has no key "${key}" (expected one of: ${accepted})`;
|
|
11335
|
+
}
|
|
11336
|
+
function matchProblem(match, parameter) {
|
|
11337
|
+
if (!match || typeof match !== "object") return "condition has no match object";
|
|
11338
|
+
if (typeof match.type !== "string" || match.type === "") return "match.type must be a non-empty string";
|
|
11339
|
+
if (!MATCH_TYPES.includes(match.type)) return `unknown match type "${match.type}"`;
|
|
11340
|
+
for (const operand of ["value", "key", "claim"]) {
|
|
11341
|
+
const value = match[operand];
|
|
11342
|
+
if (typeof value === "string" && value.length > LIMITS.maxValueLength) {
|
|
11343
|
+
return `match operand "${operand}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
11344
|
+
}
|
|
11345
|
+
if (Array.isArray(value)) {
|
|
11346
|
+
if (value.length > LIMITS.maxOperandItems) {
|
|
11347
|
+
return `match operand "${operand}" has more than ${LIMITS.maxOperandItems} entries`;
|
|
11348
|
+
}
|
|
11349
|
+
if (value.some((entry) => typeof entry === "string" && entry.length > LIMITS.maxValueLength)) {
|
|
11350
|
+
return `match operand "${operand}" contains a string longer than ${LIMITS.maxValueLength} chars`;
|
|
11351
|
+
}
|
|
11352
|
+
}
|
|
11353
|
+
}
|
|
11354
|
+
const parameterless = PARAMETERLESS_MATCH_TYPES.includes(match.type);
|
|
11355
|
+
if (!parameterless && (parameter === void 0 || parameter === null)) {
|
|
11356
|
+
return `match type "${match.type}" needs a parameter`;
|
|
11357
|
+
}
|
|
11358
|
+
if (parameterless && parameter !== void 0 && parameter !== null) {
|
|
11359
|
+
return `match type "${match.type}" reads the whole request and takes no parameter`;
|
|
11360
|
+
}
|
|
11361
|
+
const operands = MATCH_OPERANDS[match.type];
|
|
11362
|
+
if (operands) {
|
|
11363
|
+
for (const required of operands.required) {
|
|
11364
|
+
if (match[required] === void 0 || match[required] === null) {
|
|
11365
|
+
return `match type "${match.type}" needs "${required}"`;
|
|
11366
|
+
}
|
|
11367
|
+
}
|
|
11368
|
+
for (const [operand, shape] of Object.entries(operands.shapes ?? {})) {
|
|
11369
|
+
if (match[operand] === void 0) continue;
|
|
11370
|
+
const problem = operandShapeProblem(shape, match[operand]);
|
|
11371
|
+
if (problem) return `match type "${match.type}" operand "${operand}" ${problem}`;
|
|
11372
|
+
}
|
|
11373
|
+
} else if (!OPERANDLESS_MATCH_TYPES.includes(match.type) && match.value === void 0) {
|
|
11374
|
+
return `match type "${match.type}" needs a value`;
|
|
11375
|
+
}
|
|
11376
|
+
if (match.type === "array_key_value") {
|
|
11377
|
+
const paths2 = Array.isArray(match.key) ? match.key : [match.key];
|
|
11378
|
+
if (paths2.some((path17) => typeof path17 === "string" && path17.split(".").length > LIMITS.maxPathSegments)) {
|
|
11379
|
+
return `match type "array_key_value" key has more than ${LIMITS.maxPathSegments} path segments`;
|
|
11380
|
+
}
|
|
11381
|
+
}
|
|
11382
|
+
return null;
|
|
11383
|
+
}
|
|
11384
|
+
function mutationsProblem(mutations) {
|
|
11385
|
+
if (mutations === void 0 || mutations === null) return null;
|
|
11386
|
+
if (!Array.isArray(mutations)) return "mutations must be an array";
|
|
11387
|
+
if (mutations.length > LIMITS.maxOperandItems) {
|
|
11388
|
+
return `mutations has more than ${LIMITS.maxOperandItems} entries`;
|
|
11389
|
+
}
|
|
11390
|
+
for (const mutation of mutations) {
|
|
11391
|
+
if (typeof mutation !== "string" || !MUTATIONS.includes(mutation)) {
|
|
11392
|
+
return `unknown mutation "${String(mutation)}" \u2014 applied as a no-op, so the condition would test the untransformed value`;
|
|
11393
|
+
}
|
|
11394
|
+
}
|
|
11395
|
+
return null;
|
|
11396
|
+
}
|
|
11397
|
+
function whenProblem(when) {
|
|
11398
|
+
if (when === void 0 || when === null) return null;
|
|
11399
|
+
if (typeof when !== "object" || Array.isArray(when)) return "when must be an object";
|
|
11400
|
+
const keys = Object.keys(when);
|
|
11401
|
+
if (keys.length === 0) return "when is empty \u2014 remove it, or name a path or method";
|
|
11402
|
+
const unknown = keys.filter((key) => !WHEN_KEYS.includes(key));
|
|
11403
|
+
if (unknown.length > 0) {
|
|
11404
|
+
return `when names no supported key (${unknown.join(", ")}) \u2014 the engine understands ${WHEN_KEYS.join(" and ")}, and an unrecognised scope is ignored, so the rule would apply to every request`;
|
|
11405
|
+
}
|
|
11406
|
+
if ("path" in when && !isNonEmptyString(when.path)) {
|
|
11407
|
+
return "when.path must be a non-empty string";
|
|
11408
|
+
}
|
|
11409
|
+
if (typeof when.path === "string" && when.path.length > LIMITS.maxValueLength) {
|
|
11410
|
+
return `when.path is longer than ${LIMITS.maxValueLength} chars`;
|
|
11411
|
+
}
|
|
11412
|
+
if ("method" in when) {
|
|
11413
|
+
const methods = Array.isArray(when.method) ? when.method : [when.method];
|
|
11414
|
+
if (methods.length === 0) return "when.method is an empty list, so the rule would match no method";
|
|
11415
|
+
if (methods.length > LIMITS.maxParameterItems) {
|
|
11416
|
+
return `when.method has more than ${LIMITS.maxParameterItems} entries`;
|
|
11417
|
+
}
|
|
11418
|
+
if (!methods.every(isNonEmptyString)) return "when.method must be a non-empty string, or a list of them";
|
|
11419
|
+
if (methods.some((method) => method.length > LIMITS.maxParameterLength)) {
|
|
11420
|
+
return `when.method entries must not be longer than ${LIMITS.maxParameterLength} chars`;
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
return null;
|
|
11424
|
+
}
|
|
11425
|
+
function isNonEmptyString(value) {
|
|
11426
|
+
return typeof value === "string" && value.trim() !== "";
|
|
11427
|
+
}
|
|
11428
|
+
function nullPropertyProblem(rule) {
|
|
11429
|
+
if (!rule || typeof rule !== "object") return null;
|
|
11430
|
+
for (const property of RULE_PROPERTIES) {
|
|
11431
|
+
if (NULL_EXEMPT_PROPERTIES.includes(property)) continue;
|
|
11432
|
+
if (property in rule && rule[property] === null) {
|
|
11433
|
+
return `"${property}" is present but null; omit it to mean the default, since a null is a value that was meant to be something and is not`;
|
|
11434
|
+
}
|
|
11435
|
+
}
|
|
11436
|
+
return null;
|
|
11437
|
+
}
|
|
11438
|
+
function rulePropertyProblem(rule) {
|
|
11439
|
+
if (!rule || typeof rule !== "object") return null;
|
|
11440
|
+
for (const [property, shape] of Object.entries(RULE_PROPERTY_SHAPES)) {
|
|
11441
|
+
if (rule[property] === void 0 || rule[property] === null) continue;
|
|
11442
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
11443
|
+
if (problem) return `"${property}" ${problem}`;
|
|
11444
|
+
}
|
|
11445
|
+
for (const property of ["id", "rule_id", "title", "category", "message", "source_revision"]) {
|
|
11446
|
+
if (typeof rule[property] === "string" && rule[property].length > LIMITS.maxValueLength) {
|
|
11447
|
+
return `"${property}" is longer than ${LIMITS.maxValueLength} chars`;
|
|
11448
|
+
}
|
|
11449
|
+
}
|
|
11450
|
+
return null;
|
|
11451
|
+
}
|
|
11452
|
+
function actionProblem(action, rule) {
|
|
11453
|
+
if (action === void 0 || action === null) return null;
|
|
11454
|
+
if (!ACTIONS.includes(action)) return `unknown action "${action}"`;
|
|
11455
|
+
const spec = ACTION_PROPERTIES[action];
|
|
11456
|
+
for (const required of spec?.required ?? []) {
|
|
11457
|
+
if (rule?.[required] === void 0 || rule?.[required] === null) {
|
|
11458
|
+
return `action "${action}" needs "${required}"`;
|
|
11459
|
+
}
|
|
11460
|
+
}
|
|
11461
|
+
for (const [property, shape] of Object.entries(spec?.shapes ?? {})) {
|
|
11462
|
+
if (rule?.[property] === void 0 || rule?.[property] === null) continue;
|
|
11463
|
+
const problem = operandShapeProblem(shape, rule[property]);
|
|
11464
|
+
if (problem) return `action "${action}" property "${property}" ${problem}`;
|
|
11465
|
+
}
|
|
11466
|
+
const phase = rule?.phase ?? "request";
|
|
11467
|
+
if (spec !== void 0 && !spec.phases.includes(phase)) {
|
|
11468
|
+
return `action "${action}" is carried out on the ${spec.phases.join("/")} phase; on "${phase}" the rule blocks instead`;
|
|
11469
|
+
}
|
|
11470
|
+
return null;
|
|
11471
|
+
}
|
|
11472
|
+
|
|
11473
|
+
// src/protect/engine/response-json.js
|
|
11474
|
+
var MAX_RULE_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
11475
|
+
async function readBoundedJson2(response, maxBytes = MAX_RULE_RESPONSE_BYTES) {
|
|
11476
|
+
const stated2 = Number(response.headers?.get?.("content-length"));
|
|
11477
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) {
|
|
11478
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
11479
|
+
}
|
|
11480
|
+
const stream = response.body;
|
|
11481
|
+
if (!stream || typeof stream.getReader !== "function") {
|
|
11482
|
+
if (typeof response.text === "function") {
|
|
11483
|
+
const text = await response.text();
|
|
11484
|
+
if (new TextEncoder().encode(text).byteLength > maxBytes) {
|
|
11485
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
11486
|
+
}
|
|
11487
|
+
return JSON.parse(text);
|
|
11488
|
+
}
|
|
11489
|
+
throw new Error("rule response body cannot be read with a size bound");
|
|
11490
|
+
}
|
|
11491
|
+
const reader = stream.getReader();
|
|
11492
|
+
const chunks = [];
|
|
11493
|
+
let total = 0;
|
|
11494
|
+
try {
|
|
11495
|
+
for (; ; ) {
|
|
11496
|
+
const { done, value } = await reader.read();
|
|
11497
|
+
if (done) break;
|
|
11498
|
+
total += value.byteLength;
|
|
11499
|
+
if (total > maxBytes) {
|
|
11500
|
+
void reader.cancel().catch(() => {
|
|
11501
|
+
});
|
|
11502
|
+
throw new Error(`rule response exceeds ${maxBytes} bytes`);
|
|
11503
|
+
}
|
|
11504
|
+
chunks.push(value);
|
|
11505
|
+
}
|
|
11506
|
+
} finally {
|
|
11507
|
+
reader.releaseLock();
|
|
11508
|
+
}
|
|
11509
|
+
const bytes = new Uint8Array(total);
|
|
11510
|
+
let offset = 0;
|
|
11511
|
+
for (const chunk of chunks) {
|
|
11512
|
+
bytes.set(chunk, offset);
|
|
11513
|
+
offset += chunk.byteLength;
|
|
11514
|
+
}
|
|
11515
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
11516
|
+
}
|
|
11517
|
+
|
|
11518
|
+
// src/protect/engine/client.js
|
|
11519
|
+
var DEFAULT_BASE_URL = "https://api.patchstack.com";
|
|
11520
|
+
var DEFAULT_CACHE_TTL = 3e5;
|
|
11521
|
+
var JITTER_FRACTION = 0.1;
|
|
11522
|
+
var PatchstackRuleClient = class {
|
|
11523
|
+
#token;
|
|
11524
|
+
#baseUrl;
|
|
11525
|
+
#cacheTtl;
|
|
11526
|
+
#cache = null;
|
|
11527
|
+
#cacheTime = null;
|
|
11528
|
+
#ttlEffective = 0;
|
|
11529
|
+
#etag;
|
|
11530
|
+
#timeoutMs;
|
|
11531
|
+
constructor({ token, baseUrl, cacheTtl, etag, timeoutMs } = {}) {
|
|
11532
|
+
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
11533
|
+
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
|
|
11534
|
+
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_WAF_API_URL, DEFAULT_BASE_URL, "rule endpoint");
|
|
11535
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL;
|
|
11536
|
+
this.#etag = etag ?? null;
|
|
11537
|
+
if (!this.#token) {
|
|
11538
|
+
throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
|
|
11539
|
+
}
|
|
11540
|
+
}
|
|
11541
|
+
async getRules() {
|
|
11542
|
+
const now = Date.now();
|
|
11543
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
11544
|
+
return this.#cache;
|
|
11545
|
+
}
|
|
11546
|
+
const url = `${this.#baseUrl}/api/get-rules/3`;
|
|
11547
|
+
try {
|
|
11548
|
+
const headers = {
|
|
11549
|
+
"Content-Type": "application/json",
|
|
11550
|
+
"Authorization": `Bearer ${this.#token}`,
|
|
11551
|
+
"LicenseID": "1"
|
|
11552
|
+
// Hard-coded, is never actually used but needed by the API
|
|
11553
|
+
};
|
|
11554
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
11555
|
+
const response = await fetch(url, {
|
|
11556
|
+
method: "POST",
|
|
11557
|
+
headers,
|
|
11558
|
+
body: JSON.stringify({}),
|
|
11559
|
+
signal: AbortSignal.timeout(this.#timeoutMs)
|
|
11560
|
+
});
|
|
11561
|
+
if (response.status === 304) {
|
|
11562
|
+
this.#touch(now);
|
|
11563
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
11564
|
+
}
|
|
11565
|
+
if (!response.ok) {
|
|
11566
|
+
return {
|
|
11567
|
+
success: false,
|
|
11568
|
+
error: `API returned ${response.status}: ${response.statusText}`,
|
|
11569
|
+
firewall: [],
|
|
11570
|
+
whitelists: [],
|
|
11571
|
+
whitelist_keys: {}
|
|
11572
|
+
};
|
|
11573
|
+
}
|
|
11574
|
+
const data = await readBoundedJson2(response);
|
|
11575
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
11576
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
11577
|
+
}
|
|
11578
|
+
const result = {
|
|
11579
|
+
success: true,
|
|
11580
|
+
notModified: false,
|
|
11581
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
11582
|
+
firewall: data.firewall,
|
|
11583
|
+
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
11584
|
+
whitelist_keys: data.whitelist_keys ?? {}
|
|
11585
|
+
};
|
|
11586
|
+
this.#cache = result;
|
|
11587
|
+
this.#etag = result.etag;
|
|
11588
|
+
this.#touch(now);
|
|
11589
|
+
return result;
|
|
11590
|
+
} catch (err) {
|
|
11591
|
+
return {
|
|
11592
|
+
success: false,
|
|
11593
|
+
error: err.name === "TimeoutError" ? "Request timed out" : err.message,
|
|
11594
|
+
firewall: [],
|
|
11595
|
+
whitelists: [],
|
|
11596
|
+
whitelist_keys: {}
|
|
11597
|
+
};
|
|
11598
|
+
}
|
|
11599
|
+
}
|
|
11600
|
+
#touch(now) {
|
|
11601
|
+
this.#cacheTime = now;
|
|
11602
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
|
|
11603
|
+
}
|
|
11604
|
+
clearCache() {
|
|
11605
|
+
this.#cache = null;
|
|
11606
|
+
this.#cacheTime = null;
|
|
11607
|
+
this.#etag = null;
|
|
11608
|
+
}
|
|
11609
|
+
};
|
|
11610
|
+
|
|
11611
|
+
// src/protect/engine/normalizer.js
|
|
11612
|
+
var utf8 = new TextDecoder();
|
|
11613
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
11614
|
+
|
|
11615
|
+
// src/protect/notify.js
|
|
11616
|
+
var reported = /* @__PURE__ */ new WeakMap();
|
|
11617
|
+
var warnings = 0;
|
|
11618
|
+
var MAX_WARNINGS = 20;
|
|
11619
|
+
function notify(fn, arg, label) {
|
|
11620
|
+
if (typeof fn !== "function") return false;
|
|
11621
|
+
try {
|
|
11622
|
+
const result = fn(arg);
|
|
11623
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
11624
|
+
try {
|
|
11625
|
+
result.then(void 0, (err) => warnOnce2(fn, label, err));
|
|
11626
|
+
} catch {
|
|
11627
|
+
}
|
|
11628
|
+
}
|
|
11629
|
+
return true;
|
|
11630
|
+
} catch (err) {
|
|
11631
|
+
warnOnce2(fn, label, err);
|
|
11632
|
+
return false;
|
|
11633
|
+
}
|
|
11634
|
+
}
|
|
11635
|
+
function warnOnce2(fn, label, err) {
|
|
11636
|
+
let labels = reported.get(fn);
|
|
11637
|
+
if (labels?.has(label)) return;
|
|
11638
|
+
if (!labels) {
|
|
11639
|
+
labels = /* @__PURE__ */ new Set();
|
|
11640
|
+
reported.set(fn, labels);
|
|
11641
|
+
}
|
|
11642
|
+
labels.add(label);
|
|
11643
|
+
if (warnings > MAX_WARNINGS) return;
|
|
11644
|
+
warnings++;
|
|
11645
|
+
try {
|
|
11646
|
+
if (warnings > MAX_WARNINGS) {
|
|
11647
|
+
console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
|
|
11648
|
+
return;
|
|
11649
|
+
}
|
|
11650
|
+
console.warn(
|
|
11651
|
+
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this callback's failures are reported once. Cause: ${err && err.message ? err.message : String(err)}`
|
|
11652
|
+
);
|
|
11653
|
+
} catch {
|
|
11654
|
+
}
|
|
11655
|
+
}
|
|
11656
|
+
|
|
11657
|
+
// src/protect/engine/engine.js
|
|
11658
|
+
var GRP = "(?:[^()]|\\([^()]*\\))*";
|
|
11659
|
+
var REDOS_PATTERNS = [
|
|
11660
|
+
/\([^)]*[+*}][^)]*\)\s*[+*]/,
|
|
11661
|
+
/\([^)]*\|[^)]*\)\s*[+*]/,
|
|
11662
|
+
new RegExp("\\(" + GRP + "[+*}]" + GRP + "\\)\\s*[+*]"),
|
|
11663
|
+
// nested quantified subgroup
|
|
11664
|
+
new RegExp("\\(" + GRP + "\\|" + GRP + "\\)\\s*[+*]")
|
|
11665
|
+
// nested alternation under a quantifier
|
|
11666
|
+
];
|
|
11667
|
+
function hasAdjacentUnbounded(body, flags) {
|
|
11668
|
+
let previousUnboundedAtom = null;
|
|
11669
|
+
for (let i = 0; i < body.length; ) {
|
|
11670
|
+
const start = i;
|
|
11671
|
+
const ch = body[i];
|
|
11672
|
+
let assertion = false;
|
|
11673
|
+
let literal = null;
|
|
11674
|
+
let atom = ch;
|
|
11675
|
+
if (ch === "\\") {
|
|
11676
|
+
const escaped = body[i + 1] ?? "";
|
|
11677
|
+
assertion = /[bBAZz]/.test(escaped);
|
|
11678
|
+
if (!/[bBAZzdswDSWnrtvf0-9pPkKxuc]/.test(escaped)) literal = escaped;
|
|
11679
|
+
i += Math.min(2, body.length - i);
|
|
11680
|
+
atom = body.slice(start, i);
|
|
11681
|
+
} else if (ch === "[") {
|
|
11682
|
+
i++;
|
|
11683
|
+
while (i < body.length) {
|
|
11684
|
+
if (body[i] === "\\") i += 2;
|
|
11685
|
+
else if (body[i++] === "]") break;
|
|
11686
|
+
}
|
|
11687
|
+
atom = body.slice(start, i);
|
|
11688
|
+
} else if (ch === ")") {
|
|
11689
|
+
i++;
|
|
11690
|
+
atom = ".";
|
|
11691
|
+
} else if (ch === "(") {
|
|
11692
|
+
const lookaround = body.startsWith("(?=", i) || body.startsWith("(?!", i) || body.startsWith("(?<=", i) || body.startsWith("(?<!", i);
|
|
11693
|
+
if (lookaround) {
|
|
11694
|
+
i = groupEnd(body, i);
|
|
11695
|
+
continue;
|
|
11696
|
+
}
|
|
11697
|
+
i++;
|
|
11698
|
+
if (body.startsWith("?:", i)) i += 2;
|
|
11699
|
+
else if (body.startsWith("?<", i)) {
|
|
11700
|
+
const nameEnd = body.indexOf(">", i + 2);
|
|
11701
|
+
if (nameEnd !== -1) i = nameEnd + 1;
|
|
11702
|
+
}
|
|
11703
|
+
continue;
|
|
11704
|
+
} else if (ch === "|") {
|
|
11705
|
+
previousUnboundedAtom = null;
|
|
11706
|
+
i++;
|
|
11707
|
+
continue;
|
|
11708
|
+
} else if ("^$".includes(ch)) {
|
|
11709
|
+
i++;
|
|
11710
|
+
continue;
|
|
11711
|
+
} else if ("?*+{}".includes(ch)) {
|
|
11712
|
+
previousUnboundedAtom = null;
|
|
11713
|
+
i++;
|
|
11714
|
+
continue;
|
|
11715
|
+
} else {
|
|
11716
|
+
if (ch !== ".") literal = ch;
|
|
11717
|
+
i++;
|
|
11718
|
+
}
|
|
11719
|
+
if (assertion) continue;
|
|
11720
|
+
let unbounded = false;
|
|
11721
|
+
let mayBeEmpty = false;
|
|
11722
|
+
if (body[i] === "*" || body[i] === "+") {
|
|
11723
|
+
unbounded = true;
|
|
11724
|
+
mayBeEmpty = body[i] === "*";
|
|
11725
|
+
i++;
|
|
11726
|
+
} else if (body[i] === "{") {
|
|
11727
|
+
const quantifier = /^\{(\d+)(?:,(\d*))?\}/.exec(body.slice(i));
|
|
11728
|
+
if (quantifier) {
|
|
11729
|
+
unbounded = quantifier[2] === "";
|
|
11730
|
+
mayBeEmpty = Number(quantifier[1]) === 0;
|
|
11731
|
+
i += quantifier[0].length;
|
|
11732
|
+
}
|
|
11733
|
+
} else if (body[i] === "?") {
|
|
11734
|
+
mayBeEmpty = true;
|
|
11735
|
+
i++;
|
|
11736
|
+
}
|
|
11737
|
+
if (unbounded && (body[i] === "?" || body[i] === "+")) i++;
|
|
11738
|
+
if (unbounded && previousUnboundedAtom !== null) return true;
|
|
11739
|
+
if (unbounded) {
|
|
11740
|
+
previousUnboundedAtom = atom;
|
|
11741
|
+
} else if (previousUnboundedAtom !== null && !mayBeEmpty) {
|
|
11742
|
+
try {
|
|
11743
|
+
const atomFlags = flags.replace(/[^iu]/g, "");
|
|
11744
|
+
const complementaryClass = previousUnboundedAtom === "\\s" && atom === "\\S" || previousUnboundedAtom === "\\S" && atom === "\\s" || previousUnboundedAtom === "\\d" && atom === "\\D" || previousUnboundedAtom === "\\D" && atom === "\\d" || previousUnboundedAtom === "\\w" && atom === "\\W" || previousUnboundedAtom === "\\W" && atom === "\\w";
|
|
11745
|
+
const excludedLiteral = literal !== null && !new RegExp(`^(?:${previousUnboundedAtom})$`, atomFlags).test(literal);
|
|
11746
|
+
if (complementaryClass || excludedLiteral) {
|
|
11747
|
+
previousUnboundedAtom = null;
|
|
11748
|
+
}
|
|
11749
|
+
} catch {
|
|
11750
|
+
}
|
|
11751
|
+
}
|
|
11752
|
+
if (i === start) i++;
|
|
11753
|
+
}
|
|
11754
|
+
return false;
|
|
11755
|
+
}
|
|
11756
|
+
function parseRepetition(body) {
|
|
11757
|
+
let i = 0;
|
|
11758
|
+
const parseAlternation = () => {
|
|
11759
|
+
const branches = [parseSequence()];
|
|
11760
|
+
while (body[i] === "|") {
|
|
11761
|
+
i++;
|
|
11762
|
+
branches.push(parseSequence());
|
|
11763
|
+
}
|
|
11764
|
+
return branches.length === 1 ? branches[0] : { type: "alt", branches };
|
|
11765
|
+
};
|
|
11766
|
+
const parseSequence = () => {
|
|
11767
|
+
const items = [];
|
|
11768
|
+
while (i < body.length && body[i] !== "|" && body[i] !== ")") {
|
|
11769
|
+
const atom = parseAtom();
|
|
11770
|
+
if (atom === null) return null;
|
|
11771
|
+
const quantified = parseQuantifier(atom);
|
|
11772
|
+
if (quantified === null) return null;
|
|
11773
|
+
items.push(quantified);
|
|
11774
|
+
}
|
|
11775
|
+
return { type: "seq", items };
|
|
11776
|
+
};
|
|
11777
|
+
const parseAtom = () => {
|
|
11778
|
+
const ch = body[i];
|
|
11779
|
+
if (ch === "(") {
|
|
11780
|
+
const start2 = i;
|
|
11781
|
+
i++;
|
|
11782
|
+
let kind = "capture";
|
|
11783
|
+
if (body.startsWith("?:", i)) {
|
|
11784
|
+
kind = "group";
|
|
11785
|
+
i += 2;
|
|
11786
|
+
} else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
|
|
11787
|
+
kind = "look";
|
|
11788
|
+
i += 2;
|
|
11789
|
+
} else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
|
|
11790
|
+
kind = "look";
|
|
11791
|
+
i += 3;
|
|
11792
|
+
} else if (body.startsWith("?<", i)) {
|
|
11793
|
+
const close = body.indexOf(">", i + 2);
|
|
11794
|
+
if (close === -1) return null;
|
|
11795
|
+
i = close + 1;
|
|
11796
|
+
} else if (body[i] === "?") {
|
|
11797
|
+
return null;
|
|
11798
|
+
}
|
|
11799
|
+
const inner = parseAlternation();
|
|
11800
|
+
if (inner === null || body[i] !== ")") return null;
|
|
11801
|
+
i++;
|
|
11802
|
+
return { type: "group", kind, body: inner, source: body.slice(start2, i) };
|
|
11803
|
+
}
|
|
11804
|
+
const start = i;
|
|
11805
|
+
if (ch === "[") {
|
|
11806
|
+
i++;
|
|
11807
|
+
if (body[i] === "^") i++;
|
|
11808
|
+
if (body[i] === "]") i++;
|
|
11809
|
+
while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
|
|
11810
|
+
if (body[i] !== "]") return null;
|
|
11811
|
+
i++;
|
|
11812
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
11813
|
+
}
|
|
11814
|
+
if (ch === "\\") {
|
|
11815
|
+
const next = body[i + 1];
|
|
11816
|
+
if (next === void 0) return null;
|
|
11817
|
+
if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
|
|
11818
|
+
const close = body.indexOf("}", i + 3);
|
|
11819
|
+
if (close === -1) return null;
|
|
11820
|
+
i = close + 1;
|
|
11821
|
+
} else if (next === "k" && body[i + 2] === "<") {
|
|
11822
|
+
const close = body.indexOf(">", i + 3);
|
|
11823
|
+
if (close === -1) return null;
|
|
11824
|
+
i = close + 1;
|
|
11825
|
+
} else {
|
|
11826
|
+
i += 2;
|
|
11827
|
+
}
|
|
11828
|
+
return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
|
|
11829
|
+
}
|
|
11830
|
+
if (ch === "^" || ch === "$") {
|
|
11831
|
+
i++;
|
|
11832
|
+
return { type: "assert" };
|
|
11833
|
+
}
|
|
11834
|
+
if (ch === "*" || ch === "+" || ch === "?") return null;
|
|
11835
|
+
i++;
|
|
11836
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
11837
|
+
};
|
|
11838
|
+
const parseQuantifier = (atom) => {
|
|
11839
|
+
let min;
|
|
11840
|
+
let max;
|
|
11841
|
+
const ch = body[i];
|
|
11842
|
+
if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
|
|
11843
|
+
else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
|
|
11844
|
+
else if (ch === "?") [min, max, i] = [0, 1, i + 1];
|
|
11845
|
+
else if (ch === "{") {
|
|
11846
|
+
const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
|
|
11847
|
+
if (!bounds) return atom;
|
|
11848
|
+
min = Number(bounds[1]);
|
|
11849
|
+
max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
|
|
11850
|
+
i += bounds[0].length;
|
|
11851
|
+
} else {
|
|
11852
|
+
return atom;
|
|
11853
|
+
}
|
|
11854
|
+
if (body[i] === "?") i++;
|
|
11855
|
+
if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
|
|
11856
|
+
return { type: "quant", min, max, body: atom };
|
|
11857
|
+
};
|
|
11858
|
+
const root = parseAlternation();
|
|
11859
|
+
return root !== null && i === body.length ? root : null;
|
|
11860
|
+
}
|
|
11861
|
+
function* nodesOf(node) {
|
|
11862
|
+
if (!node) return;
|
|
11863
|
+
yield node;
|
|
11864
|
+
if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
|
|
11865
|
+
else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
|
|
11866
|
+
else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
|
|
11867
|
+
}
|
|
11868
|
+
var BOUNDED_NESTING_LIMIT = 100;
|
|
11869
|
+
var BOUNDED_ALTERNATION_LIMIT = 16;
|
|
11870
|
+
var OPTIONAL_REPEAT_LIMIT = 8;
|
|
11871
|
+
function hasFencedRepetitions(quant, flags) {
|
|
11872
|
+
let body = quant.body;
|
|
11873
|
+
if (body.type === "group" && body.kind !== "look") body = body.body;
|
|
11874
|
+
if (body.type !== "seq" || body.items.length < 2) return false;
|
|
11875
|
+
const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
|
|
11876
|
+
if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
|
|
11877
|
+
const atomFlags = flags.replace(/[^i]/g, "");
|
|
11878
|
+
const unicode = /[uv]/.test(flags);
|
|
11879
|
+
let insides;
|
|
11880
|
+
try {
|
|
11881
|
+
insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
|
|
11882
|
+
} catch {
|
|
11883
|
+
return false;
|
|
11884
|
+
}
|
|
11885
|
+
const literalOf = (source) => {
|
|
11886
|
+
if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
|
|
11887
|
+
if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
|
|
11888
|
+
return null;
|
|
11889
|
+
};
|
|
11890
|
+
const disjoint = (edge) => {
|
|
11891
|
+
if (edge?.type !== "atom" || edge.source === void 0) return false;
|
|
11892
|
+
const literal = literalOf(edge.source);
|
|
11893
|
+
if (literal !== null) return insides.every((re) => !re.test(literal));
|
|
11894
|
+
if (unicode) return false;
|
|
11895
|
+
let fence;
|
|
11896
|
+
try {
|
|
11897
|
+
fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
|
|
11898
|
+
} catch {
|
|
11899
|
+
return false;
|
|
11900
|
+
}
|
|
11901
|
+
for (let unit = 0; unit <= 65535; unit++) {
|
|
11902
|
+
const ch = String.fromCharCode(unit);
|
|
11903
|
+
if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
|
|
11904
|
+
}
|
|
11905
|
+
return true;
|
|
11906
|
+
};
|
|
11907
|
+
return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
|
|
11908
|
+
}
|
|
11909
|
+
function hasCatastrophicShape(body, flags) {
|
|
11910
|
+
const root = parseRepetition(body);
|
|
11911
|
+
if (root === null) return null;
|
|
11912
|
+
for (const node of nodesOf(root)) {
|
|
11913
|
+
if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
|
|
11914
|
+
if (node.type !== "quant" || node.max <= 1) continue;
|
|
11915
|
+
const fenced = hasFencedRepetitions(node, flags);
|
|
11916
|
+
for (const inner of nodesOf(node.body)) {
|
|
11917
|
+
if (inner === node.body && inner.type === "quant") continue;
|
|
11918
|
+
if (inner.type === "quant" && inner.max > 1 && !fenced) {
|
|
11919
|
+
if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
|
|
11920
|
+
}
|
|
11921
|
+
if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
|
|
11922
|
+
return true;
|
|
11923
|
+
}
|
|
11924
|
+
if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
|
|
11925
|
+
return true;
|
|
11926
|
+
}
|
|
11927
|
+
}
|
|
11928
|
+
}
|
|
11929
|
+
return false;
|
|
11930
|
+
}
|
|
11931
|
+
function lookaroundBody(source) {
|
|
11932
|
+
return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
|
|
11933
|
+
}
|
|
11934
|
+
function groupEnd(body, start) {
|
|
11935
|
+
let depth = 0;
|
|
11936
|
+
let inClass = false;
|
|
11937
|
+
for (let i = start; i < body.length; i++) {
|
|
11938
|
+
const ch = body[i];
|
|
11939
|
+
if (ch === "\\") {
|
|
11940
|
+
i++;
|
|
11941
|
+
continue;
|
|
11942
|
+
}
|
|
11943
|
+
if (ch === "[") inClass = true;
|
|
11944
|
+
else if (ch === "]") inClass = false;
|
|
11945
|
+
else if (!inClass && ch === "(") depth++;
|
|
11946
|
+
else if (!inClass && ch === ")" && --depth === 0) return i + 1;
|
|
11947
|
+
}
|
|
11948
|
+
return body.length;
|
|
11949
|
+
}
|
|
11950
|
+
var MAX_PATTERN_LENGTH = 1e3;
|
|
11951
|
+
var regexVerdicts = /* @__PURE__ */ new Map();
|
|
11952
|
+
var REGEX_VERDICT_LIMIT = 2048;
|
|
11953
|
+
function safeRegExp(pattern) {
|
|
11954
|
+
if (!pattern) {
|
|
11955
|
+
return null;
|
|
11956
|
+
}
|
|
11957
|
+
if (typeof pattern !== "string" || pattern.length > MAX_PATTERN_LENGTH) {
|
|
11958
|
+
return null;
|
|
11959
|
+
}
|
|
11960
|
+
const match = pattern.match(/^\/(.+?)\/([gimsuy]*)$/s);
|
|
11961
|
+
if (!match) {
|
|
11962
|
+
return null;
|
|
11963
|
+
}
|
|
11964
|
+
let safe = regexVerdicts.get(pattern);
|
|
11965
|
+
if (safe === void 0) {
|
|
11966
|
+
safe = isSafeBody(match[1], match[2]);
|
|
11967
|
+
if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
|
|
11968
|
+
regexVerdicts.set(pattern, safe);
|
|
11969
|
+
}
|
|
11970
|
+
if (!safe) return null;
|
|
11971
|
+
try {
|
|
11972
|
+
return new RegExp(match[1], match[2]);
|
|
11973
|
+
} catch {
|
|
11974
|
+
return null;
|
|
11975
|
+
}
|
|
11976
|
+
}
|
|
11977
|
+
function isSafeBody(body, flags) {
|
|
11978
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
11979
|
+
if (dangerous.test(body)) return false;
|
|
11980
|
+
}
|
|
11981
|
+
if (hasAdjacentUnbounded(body, flags)) return false;
|
|
11982
|
+
if (hasCatastrophicShape(body, flags) === true) return false;
|
|
11983
|
+
try {
|
|
11984
|
+
new RegExp(body, flags);
|
|
11985
|
+
} catch {
|
|
11986
|
+
return false;
|
|
11987
|
+
}
|
|
11988
|
+
return true;
|
|
11989
|
+
}
|
|
11990
|
+
|
|
11991
|
+
// src/protect/engine/pulse-client.js
|
|
11992
|
+
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
11993
|
+
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
11994
|
+
var BUILD_VERDICT_HEADER = "X-Patchstack-Build-Match";
|
|
11995
|
+
var BUILD_IDENTITY_HEADER = "X-Patchstack-Build-ID";
|
|
11996
|
+
var PER_RULE_DRY_RUN_HEADER = "X-Patchstack-Per-Rule-Dry-Run";
|
|
11997
|
+
var JITTER_FRACTION2 = 0.1;
|
|
11998
|
+
var PulseRuleClient = class {
|
|
11999
|
+
#siteUuid;
|
|
12000
|
+
#timeoutMs;
|
|
12001
|
+
#baseUrl;
|
|
12002
|
+
#cacheTtl;
|
|
12003
|
+
#cache = null;
|
|
12004
|
+
#cacheTime = null;
|
|
12005
|
+
#ttlEffective = 0;
|
|
12006
|
+
#etag;
|
|
12007
|
+
#pulseAuth;
|
|
12008
|
+
#detectionState;
|
|
12009
|
+
#buildId;
|
|
12010
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag, timeoutMs, pulseAuth, detectionState, buildId } = {}) {
|
|
12011
|
+
this.#timeoutMs = Number(timeoutMs) > 0 ? Number(timeoutMs) : 3e4;
|
|
12012
|
+
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
12013
|
+
this.#baseUrl = safeBaseUrl(baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL, DEFAULT_BASE_URL2, "rule endpoint");
|
|
12014
|
+
this.#cacheTtl = Number.isFinite(cacheTtl) && cacheTtl > 0 ? cacheTtl : DEFAULT_CACHE_TTL2;
|
|
12015
|
+
this.#etag = etag ?? null;
|
|
12016
|
+
this.#pulseAuth = pulseAuth ?? null;
|
|
12017
|
+
this.#detectionState = typeof detectionState === "string" ? detectionState : null;
|
|
12018
|
+
this.#buildId = canonicalBuildId(buildId);
|
|
12019
|
+
if (!this.#siteUuid) {
|
|
12020
|
+
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
12021
|
+
}
|
|
12022
|
+
}
|
|
12023
|
+
async getRules() {
|
|
12024
|
+
const now = Date.now();
|
|
12025
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
12026
|
+
return this.#cache;
|
|
12027
|
+
}
|
|
12028
|
+
const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
|
|
12029
|
+
try {
|
|
12030
|
+
const auth = await pulseAuthHeader(
|
|
12031
|
+
{ pulseAuth: this.#pulseAuth, endpoint: this.#baseUrl, timeoutMs: this.#timeoutMs },
|
|
12032
|
+
fetch
|
|
12033
|
+
);
|
|
12034
|
+
const headers = { Accept: "application/json", ...auth };
|
|
12035
|
+
if (this.#detectionState !== null && typeof auth.Authorization === "string") {
|
|
12036
|
+
headers["X-Patchstack-Detections"] = this.#detectionState;
|
|
12037
|
+
}
|
|
12038
|
+
if (this.#buildId !== null && typeof auth.Authorization === "string") {
|
|
12039
|
+
headers["X-Patchstack-Build"] = this.#buildId;
|
|
12040
|
+
}
|
|
12041
|
+
if (typeof auth.Authorization === "string") {
|
|
12042
|
+
headers[PER_RULE_DRY_RUN_HEADER] = "1";
|
|
12043
|
+
}
|
|
12044
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
12045
|
+
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(this.#timeoutMs) });
|
|
12046
|
+
if (response.status === 304) {
|
|
12047
|
+
this.#touch(now);
|
|
12048
|
+
const result2 = {
|
|
12049
|
+
...this.#cache ?? { firewall: [], whitelists: [], whitelist_keys: {} },
|
|
12050
|
+
success: true,
|
|
12051
|
+
notModified: true,
|
|
12052
|
+
etag: response.headers?.get?.("etag") ?? this.#etag,
|
|
12053
|
+
build: buildVerdictOf(response.headers)
|
|
12054
|
+
};
|
|
12055
|
+
this.#cache = result2;
|
|
12056
|
+
this.#etag = result2.etag;
|
|
12057
|
+
return result2;
|
|
12058
|
+
}
|
|
12059
|
+
if (!response.ok) {
|
|
12060
|
+
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
12061
|
+
}
|
|
12062
|
+
const data = await readBoundedJson2(response);
|
|
12063
|
+
if (!data || !Array.isArray(data.firewall)) {
|
|
12064
|
+
return { success: false, error: "unexpected response shape (no firewall array)", firewall: [], whitelists: [], whitelist_keys: {} };
|
|
12065
|
+
}
|
|
12066
|
+
const result = {
|
|
12067
|
+
success: true,
|
|
12068
|
+
notModified: false,
|
|
12069
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
12070
|
+
firewall: data.firewall,
|
|
12071
|
+
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
12072
|
+
whitelist_keys: data.whitelist_keys ?? {},
|
|
12073
|
+
...enforcementField(data),
|
|
12074
|
+
// Carried through so the caller can decide what a build-scoped rule may do. Response metadata,
|
|
12075
|
+
// rather than bundle content, because it varies with the build asking and is also present on 304.
|
|
12076
|
+
build: buildVerdictOf(response.headers)
|
|
12077
|
+
};
|
|
12078
|
+
this.#cache = result;
|
|
12079
|
+
this.#etag = result.etag;
|
|
12080
|
+
this.#touch(now);
|
|
12081
|
+
return result;
|
|
12082
|
+
} catch (err) {
|
|
12083
|
+
return {
|
|
12084
|
+
success: false,
|
|
12085
|
+
error: err.name === "TimeoutError" ? "Request timed out" : err.message,
|
|
12086
|
+
firewall: [],
|
|
12087
|
+
whitelists: [],
|
|
12088
|
+
whitelist_keys: {}
|
|
12089
|
+
};
|
|
12090
|
+
}
|
|
12091
|
+
}
|
|
12092
|
+
#touch(now) {
|
|
12093
|
+
this.#cacheTime = now;
|
|
12094
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
|
|
12095
|
+
}
|
|
12096
|
+
clearCache() {
|
|
12097
|
+
this.#cache = null;
|
|
12098
|
+
this.#cacheTime = null;
|
|
12099
|
+
this.#etag = null;
|
|
12100
|
+
}
|
|
12101
|
+
};
|
|
12102
|
+
function buildVerdictOf(headers) {
|
|
12103
|
+
const get = headers !== null && typeof headers === "object" && typeof headers.get === "function" ? (name) => headers.get(name) : () => null;
|
|
12104
|
+
const stated2 = get(BUILD_VERDICT_HEADER);
|
|
12105
|
+
const verdict = stated2 === "match" || stated2 === "missing" || stated2 === "mismatch" ? stated2 : "unknown";
|
|
12106
|
+
const matched = get(BUILD_IDENTITY_HEADER);
|
|
12107
|
+
return { verdict, matchedBuildId: typeof matched === "string" ? matched : null };
|
|
12108
|
+
}
|
|
12109
|
+
function enforcementField(data) {
|
|
12110
|
+
if (!data || typeof data !== "object") return {};
|
|
12111
|
+
const v = data.enforcement ?? data.mode;
|
|
12112
|
+
return v === "block" || v === "dry-run" ? { enforcement: v } : {};
|
|
12113
|
+
}
|
|
12114
|
+
|
|
12115
|
+
// src/protect/rules/validate.js
|
|
12116
|
+
var LIMITS2 = LIMITS;
|
|
12117
|
+
var PHASES2 = new Set(PHASES);
|
|
12118
|
+
var ACTIONS2 = new Set(ACTIONS);
|
|
12119
|
+
function validateBundle(bundle, opts = {}) {
|
|
12120
|
+
const rejected = [];
|
|
12121
|
+
let omittedRejections = 0;
|
|
12122
|
+
const reject = (entry) => {
|
|
12123
|
+
if (rejected.length < 100) rejected.push(entry);
|
|
12124
|
+
else omittedRejections++;
|
|
12125
|
+
};
|
|
12126
|
+
const inFirewall = Array.isArray(bundle?.firewall) ? bundle.firewall : [];
|
|
12127
|
+
const inWhitelists = Array.isArray(bundle?.whitelists) ? bundle.whitelists : [];
|
|
12128
|
+
const bundleBudget = { count: 0 };
|
|
12129
|
+
const firewall = [];
|
|
12130
|
+
for (const rule of inFirewall.slice(0, LIMITS2.maxRules)) {
|
|
12131
|
+
const reason = enforceableRuleProblem(rule, bundleBudget);
|
|
12132
|
+
if (reason) reject({ id: idOf(rule), reason });
|
|
12133
|
+
else firewall.push(rule);
|
|
12134
|
+
}
|
|
12135
|
+
if (inFirewall.length > LIMITS2.maxRules) {
|
|
12136
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxRules (${LIMITS2.maxRules})` });
|
|
12137
|
+
}
|
|
12138
|
+
const whitelists = [];
|
|
12139
|
+
for (const wl of inWhitelists.slice(0, LIMITS2.maxWhitelists)) {
|
|
12140
|
+
if (!opts.allowGlobalWhitelists && wl && Array.isArray(wl.rule_v2) && !wl.rule_id) {
|
|
12141
|
+
reject({ id: idOf(wl), reason: "whitelist has no rule_id (would suppress every rule); set allowGlobalWhitelists to permit" });
|
|
12142
|
+
continue;
|
|
12143
|
+
}
|
|
12144
|
+
if (wl && typeof wl === "object" && BUILD_SCOPE_PROPERTY in wl) {
|
|
12145
|
+
reject({ id: idOf(wl), reason: `whitelist may not carry ${BUILD_SCOPE_PROPERTY}` });
|
|
12146
|
+
continue;
|
|
12147
|
+
}
|
|
12148
|
+
const reason = conditionsProblem(wl?.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
12149
|
+
if (reason) reject({ id: idOf(wl), reason: `whitelist: ${reason}` });
|
|
12150
|
+
else whitelists.push(wl);
|
|
12151
|
+
}
|
|
12152
|
+
if (inWhitelists.length > LIMITS2.maxWhitelists) {
|
|
12153
|
+
reject({ id: "(bundle)", reason: `bundle exceeds maxWhitelists (${LIMITS2.maxWhitelists})` });
|
|
12154
|
+
}
|
|
12155
|
+
let whitelistKeys = {};
|
|
12156
|
+
if (bundle?.whitelist_keys !== void 0) {
|
|
12157
|
+
if (!bundle.whitelist_keys || typeof bundle.whitelist_keys !== "object" || Array.isArray(bundle.whitelist_keys)) {
|
|
12158
|
+
reject({ id: "(bundle)", reason: "whitelist_keys must be an object" });
|
|
12159
|
+
} else {
|
|
12160
|
+
const entries = Object.entries(bundle.whitelist_keys);
|
|
12161
|
+
if (entries.length > LIMITS2.maxMapEntries) {
|
|
12162
|
+
reject({ id: "(bundle)", reason: `whitelist_keys has more than ${LIMITS2.maxMapEntries} entries` });
|
|
12163
|
+
} else {
|
|
12164
|
+
const invalid = entries.find(([key, value]) => key.length > LIMITS2.maxParameterLength || !Array.isArray(value) || value.length > LIMITS2.maxOperandItems || value.some((entry) => typeof entry !== "string" || entry.length > LIMITS2.maxValueLength));
|
|
12165
|
+
if (invalid) {
|
|
12166
|
+
reject({ id: "(bundle)", reason: `whitelist_keys entry "${invalid[0].slice(0, 64)}" has an unsupported shape` });
|
|
12167
|
+
} else {
|
|
12168
|
+
whitelistKeys = bundle.whitelist_keys;
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
}
|
|
12172
|
+
}
|
|
12173
|
+
if (omittedRejections > 0) {
|
|
12174
|
+
rejected.push({ id: "(bundle)", reason: `${omittedRejections} additional rejected entries omitted` });
|
|
12175
|
+
}
|
|
12176
|
+
return {
|
|
12177
|
+
bundle: { ...bundle, firewall, whitelists, whitelist_keys: whitelistKeys },
|
|
12178
|
+
rejected
|
|
12179
|
+
};
|
|
12180
|
+
}
|
|
12181
|
+
function idOf(rule) {
|
|
12182
|
+
const id = rule?.id ?? rule?.rule_id;
|
|
12183
|
+
if (id === void 0 || id === null) return "(unidentified)";
|
|
12184
|
+
try {
|
|
12185
|
+
return String(id).slice(0, 128);
|
|
12186
|
+
} catch {
|
|
12187
|
+
return "(unidentified)";
|
|
12188
|
+
}
|
|
12189
|
+
}
|
|
12190
|
+
function enforceableRuleProblem(rule, bundleBudget = null) {
|
|
12191
|
+
if (!rule || typeof rule !== "object") return "not an object";
|
|
12192
|
+
const nullReason = nullPropertyProblem(rule);
|
|
12193
|
+
if (nullReason) return nullReason;
|
|
12194
|
+
if (rule.phase !== void 0 && !PHASES2.has(rule.phase)) return `unknown phase "${rule.phase}"`;
|
|
12195
|
+
const actionReason = actionProblem(rule.action, rule);
|
|
12196
|
+
if (actionReason) return actionReason;
|
|
12197
|
+
const scopeReason = whenProblem(rule.when);
|
|
12198
|
+
if (scopeReason) return scopeReason;
|
|
12199
|
+
const propertyReason = rulePropertyProblem(rule);
|
|
12200
|
+
if (propertyReason) return propertyReason;
|
|
12201
|
+
return conditionsProblem(rule.rule_v2, 0, { count: 0 }, bundleBudget);
|
|
12202
|
+
}
|
|
12203
|
+
function conditionsProblem(conditions, depth = 0, ruleBudget = { count: 0 }, bundleBudget = null) {
|
|
12204
|
+
if (!Array.isArray(conditions)) return "rule_v2 must be an array of conditions";
|
|
12205
|
+
if (conditions.length === 0) return "rule_v2 is empty (would never match)";
|
|
12206
|
+
if (depth > LIMITS2.maxNestingDepth) return `nesting deeper than ${LIMITS2.maxNestingDepth}`;
|
|
12207
|
+
if (conditions.length > LIMITS2.maxConditionsPerRule) {
|
|
12208
|
+
return `more than ${LIMITS2.maxConditionsPerRule} conditions`;
|
|
12209
|
+
}
|
|
12210
|
+
for (const c of conditions) {
|
|
12211
|
+
ruleBudget.count++;
|
|
12212
|
+
if (ruleBudget.count > LIMITS2.maxConditionNodesPerRule) {
|
|
12213
|
+
return `more than ${LIMITS2.maxConditionNodesPerRule} total condition nodes`;
|
|
12214
|
+
}
|
|
12215
|
+
if (bundleBudget) {
|
|
12216
|
+
bundleBudget.count++;
|
|
12217
|
+
if (bundleBudget.count > LIMITS2.maxConditionNodesPerBundle) {
|
|
12218
|
+
return `bundle exceeds ${LIMITS2.maxConditionNodesPerBundle} total condition nodes`;
|
|
12219
|
+
}
|
|
12220
|
+
}
|
|
12221
|
+
const shapeReason = conditionShapeProblem(c);
|
|
12222
|
+
if (shapeReason) return shapeReason;
|
|
12223
|
+
if (isGroup(c)) {
|
|
12224
|
+
const nested = conditionsProblem(c.rules, depth + 1, ruleBudget, bundleBudget);
|
|
12225
|
+
if (nested) return nested;
|
|
12226
|
+
continue;
|
|
12227
|
+
}
|
|
12228
|
+
const parameterReason = parameterProblem(c.parameter);
|
|
12229
|
+
if (parameterReason) return parameterReason;
|
|
12230
|
+
const mutationReason = mutationsProblem(c.mutations);
|
|
12231
|
+
if (mutationReason) return mutationReason;
|
|
12232
|
+
const matchReason = matchProblem(c.match, c.parameter);
|
|
12233
|
+
if (matchReason) return matchReason;
|
|
12234
|
+
const m = c.match;
|
|
12235
|
+
if (m.type === "regex") {
|
|
12236
|
+
if (typeof m.value !== "string") return "regex match.value must be a string";
|
|
12237
|
+
if (m.value.length > LIMITS2.maxRegexLength) return `regex longer than ${LIMITS2.maxRegexLength} chars`;
|
|
12238
|
+
if (safeRegExp(m.value) === null) return "regex is invalid or has an unsafe repetition shape";
|
|
12239
|
+
} else if (typeof m.value === "string" && m.value.length > LIMITS2.maxValueLength) {
|
|
12240
|
+
return `match.value longer than ${LIMITS2.maxValueLength} chars`;
|
|
12241
|
+
}
|
|
12242
|
+
if (m.match) {
|
|
12243
|
+
const nested = conditionsProblem(
|
|
12244
|
+
[{ parameter: c.parameter, match: m.match }],
|
|
12245
|
+
depth + 1,
|
|
12246
|
+
ruleBudget,
|
|
12247
|
+
bundleBudget
|
|
12248
|
+
);
|
|
12249
|
+
if (nested) return nested;
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
12252
|
+
return null;
|
|
12253
|
+
}
|
|
12254
|
+
|
|
12255
|
+
// src/protect/rules/source.js
|
|
12256
|
+
function liveUpdateRejections(res, options) {
|
|
12257
|
+
if (options.acceptPartialBundle) return [];
|
|
12258
|
+
const { rejected } = validateBundle(
|
|
12259
|
+
{
|
|
12260
|
+
firewall: Array.isArray(res.firewall) ? res.firewall : [],
|
|
12261
|
+
whitelists: Array.isArray(res.whitelists) ? res.whitelists : [],
|
|
12262
|
+
whitelist_keys: res.whitelist_keys
|
|
12263
|
+
},
|
|
12264
|
+
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
12265
|
+
);
|
|
12266
|
+
return rejected;
|
|
12267
|
+
}
|
|
12268
|
+
function reportRejections(rejected, options, label) {
|
|
12269
|
+
const report = options.onRuleRejected;
|
|
12270
|
+
for (const r of rejected) {
|
|
12271
|
+
if (typeof report === "function") {
|
|
12272
|
+
notify(report, { ...r, accepted: false }, "onRuleRejected");
|
|
12273
|
+
}
|
|
12274
|
+
}
|
|
12275
|
+
const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
|
|
12276
|
+
notify(options.onError, new Error(
|
|
12277
|
+
`${label}: rejected the entire update because ${rejected.length} rule(s) failed validation \u2014 keeping the previous ruleset and NOT caching this response: ${sample}${rejected.length > 3 ? ", \u2026" : ""}`
|
|
12278
|
+
), "onError");
|
|
12279
|
+
}
|
|
12280
|
+
function fromSource(bundle, origin, reason) {
|
|
12281
|
+
return {
|
|
12282
|
+
...bundle,
|
|
12283
|
+
source: reason === void 0 ? { ok: true, origin } : { ok: false, origin, reason }
|
|
12284
|
+
};
|
|
12285
|
+
}
|
|
12286
|
+
function buildIdentity(options) {
|
|
12287
|
+
if (options.buildId !== void 0 && options.buildId !== null) {
|
|
12288
|
+
const explicit = canonicalBuildId(options.buildId);
|
|
12289
|
+
return explicit === null ? { id: null, reason: "the buildId option is not a complete 64-hex SHA-256 identity" } : { id: explicit };
|
|
12290
|
+
}
|
|
12291
|
+
const stamped = readBuildStamp(options.rules);
|
|
12292
|
+
if (stamped !== null) return { id: stamped };
|
|
12293
|
+
return {
|
|
12294
|
+
id: null,
|
|
12295
|
+
reason: options.rules ? "the guard rules file carries no build identity \u2014 the build did not stamp one" : "this guard was configured without bundled rules, so it carries no build identity"
|
|
12296
|
+
};
|
|
12297
|
+
}
|
|
12298
|
+
function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
12299
|
+
let held = 0;
|
|
12300
|
+
let unusable = 0;
|
|
12301
|
+
const firewall = bundle.firewall.map((rule) => {
|
|
12302
|
+
if (rule === null || typeof rule !== "object") return rule;
|
|
12303
|
+
if (!(BUILD_SCOPE_PROPERTY in rule)) return rule;
|
|
12304
|
+
const scope = rule[BUILD_SCOPE_PROPERTY];
|
|
12305
|
+
const target = canonicalBuildId(scope);
|
|
12306
|
+
if (target !== null && confirmed !== null && target === confirmed) return rule;
|
|
12307
|
+
if (rule.enforcement === "dry-run") return rule;
|
|
12308
|
+
held += 1;
|
|
12309
|
+
if (target === null) unusable += 1;
|
|
12310
|
+
return { ...rule, enforcement: "dry-run" };
|
|
12311
|
+
});
|
|
12312
|
+
if (held === 0) return bundle;
|
|
12313
|
+
const explanations = [
|
|
12314
|
+
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
12315
|
+
...held > unusable ? [reason] : []
|
|
12316
|
+
];
|
|
12317
|
+
const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
|
|
12318
|
+
if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
|
|
12319
|
+
else warnHeldOnce(message);
|
|
12320
|
+
return { ...bundle, firewall };
|
|
12321
|
+
}
|
|
12322
|
+
var warnedHeld = /* @__PURE__ */ new Set();
|
|
12323
|
+
function warnHeldOnce(message) {
|
|
12324
|
+
if (warnedHeld.has(message)) return;
|
|
12325
|
+
warnedHeld.add(message);
|
|
12326
|
+
console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
|
|
12327
|
+
}
|
|
12328
|
+
function confirmedBy(res, presented) {
|
|
12329
|
+
if (presented === null) return null;
|
|
12330
|
+
const verdict = res?.build;
|
|
12331
|
+
if (verdict?.verdict !== "match") return null;
|
|
12332
|
+
return canonicalBuildId(verdict.matchedBuildId) === presented ? presented : null;
|
|
12333
|
+
}
|
|
12334
|
+
async function resolveRules(options, store, ctx = {}) {
|
|
12335
|
+
const timeoutMs = ctx.timeoutMs;
|
|
12336
|
+
if (options.siteUuid) {
|
|
12337
|
+
const prior = await store.read();
|
|
12338
|
+
const identity2 = buildIdentity(options);
|
|
12339
|
+
const sameBuild = prior?.buildId === identity2.id;
|
|
12340
|
+
const cacheConfirmed = identity2.id !== null && sameBuild && prior?.matchedBuildId === identity2.id ? identity2.id : null;
|
|
12341
|
+
const unconfirmed = identity2.reason ?? "the platform did not confirm that these coordinates belong to this map";
|
|
12342
|
+
const served = (bundle, confirmed) => withScopedDryRun(bundle, confirmed, unconfirmed, options);
|
|
12343
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
|
|
12344
|
+
const client = new PulseRuleClient({
|
|
12345
|
+
siteUuid: options.siteUuid,
|
|
12346
|
+
baseUrl: options.pulseRulesUrl,
|
|
12347
|
+
etag: sameBuild ? prior?.etag : null,
|
|
12348
|
+
timeoutMs,
|
|
12349
|
+
pulseAuth: ctx.pulseAuth,
|
|
12350
|
+
detectionState: ctx.detectionState,
|
|
12351
|
+
buildId: identity2.id
|
|
12352
|
+
});
|
|
12353
|
+
const res = await client.getRules();
|
|
12354
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
12355
|
+
const confirmed = confirmedBy(res, identity2.id);
|
|
12356
|
+
await store.write({
|
|
12357
|
+
bundle: prior.bundle,
|
|
12358
|
+
etag: res.etag ?? prior.etag ?? null,
|
|
12359
|
+
buildId: identity2.id,
|
|
12360
|
+
matchedBuildId: confirmed
|
|
12361
|
+
});
|
|
12362
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), confirmed), "cache");
|
|
12363
|
+
}
|
|
12364
|
+
if (res.success && !res.notModified) {
|
|
12365
|
+
const confirmed = confirmedBy(res, identity2.id);
|
|
12366
|
+
const rejected = liveUpdateRejections(res, options);
|
|
12367
|
+
if (rejected.length > 0) {
|
|
12368
|
+
reportRejections(rejected, options, "rule update rejected");
|
|
12369
|
+
if (prior?.bundle) return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", "update rejected");
|
|
12370
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
12371
|
+
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
12372
|
+
}
|
|
12373
|
+
const bundle = normalizeBundle(res, options);
|
|
12374
|
+
await store.write({ bundle, etag: res.etag ?? null, buildId: identity2.id, matchedBuildId: confirmed });
|
|
12375
|
+
return fromSource(served(bundle, confirmed), "api");
|
|
12376
|
+
}
|
|
12377
|
+
if (prior?.bundle) {
|
|
12378
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
12379
|
+
return fromSource(served(normalizeBundle(prior.bundle, options), cacheConfirmed), "cache", res.error ?? "no usable response");
|
|
12380
|
+
}
|
|
12381
|
+
if (options.rules) {
|
|
12382
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`), "onError");
|
|
12383
|
+
return fromSource(local(normalizeBundle(options.rules, options)), "bundled", res.error ?? "no usable response");
|
|
12384
|
+
}
|
|
12385
|
+
notify(options.onError, new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
12386
|
+
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
12387
|
+
}
|
|
12388
|
+
if (options.token) {
|
|
12389
|
+
const identity2 = buildIdentity(options);
|
|
12390
|
+
const remote = (bundle) => withScopedDryRun(
|
|
12391
|
+
bundle,
|
|
12392
|
+
null,
|
|
12393
|
+
"the token-authenticated rules service did not corroborate this map",
|
|
12394
|
+
options
|
|
12395
|
+
);
|
|
12396
|
+
const local = (bundle) => options.trustLocalRuleScope === true ? bundle : withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options);
|
|
12397
|
+
const prior = await store.read();
|
|
12398
|
+
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag, timeoutMs });
|
|
12399
|
+
const res = await client.getRules();
|
|
12400
|
+
if (res.success && res.notModified && prior?.bundle) {
|
|
12401
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache");
|
|
12402
|
+
}
|
|
12403
|
+
if (res.success && !res.notModified) {
|
|
12404
|
+
const rejected = liveUpdateRejections(res, options);
|
|
12405
|
+
if (rejected.length > 0) {
|
|
12406
|
+
reportRejections(rejected, options, "rule update rejected");
|
|
12407
|
+
if (prior?.bundle) return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", "update rejected");
|
|
12408
|
+
if (options.rules) return fromSource(local(normalizeBundle(options.rules, options)), "bundled", "update rejected");
|
|
12409
|
+
return fromSource(emptyBundle(), "empty", "update rejected");
|
|
12410
|
+
}
|
|
12411
|
+
const bundle = normalizeBundle(res, options);
|
|
12412
|
+
await store.write({ bundle, etag: res.etag ?? null });
|
|
12413
|
+
return fromSource(remote(bundle), "api");
|
|
12414
|
+
}
|
|
12415
|
+
if (prior?.bundle) {
|
|
12416
|
+
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`), "onError");
|
|
12417
|
+
return fromSource(remote(normalizeBundle(prior.bundle, options)), "cache", res.error ?? "no usable response");
|
|
12418
|
+
}
|
|
12419
|
+
notify(options.onError, new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`), "onError");
|
|
12420
|
+
return fromSource(emptyBundle(), "empty", res.error ?? "no usable response");
|
|
12421
|
+
}
|
|
12422
|
+
if (options.rules) {
|
|
12423
|
+
const identity2 = buildIdentity(options);
|
|
12424
|
+
const bundle = normalizeBundle(options.rules, options);
|
|
12425
|
+
if (options.trustLocalRuleScope === true) return fromSource(bundle, "bundled");
|
|
12426
|
+
return fromSource(
|
|
12427
|
+
withScopedDryRun(bundle, identity2.id, identity2.reason ?? "these rules name a different map", options),
|
|
12428
|
+
"bundled"
|
|
12429
|
+
);
|
|
12430
|
+
}
|
|
12431
|
+
return fromSource(emptyBundle(), "empty");
|
|
12432
|
+
}
|
|
12433
|
+
function normalizeBundle(b, options = {}) {
|
|
12434
|
+
const enforcement = b?.enforcement ?? b?.mode;
|
|
12435
|
+
const { bundle: checked, rejected } = validateBundle(
|
|
12436
|
+
{
|
|
12437
|
+
firewall: Array.isArray(b.firewall) ? b.firewall : [],
|
|
12438
|
+
whitelists: Array.isArray(b.whitelists) ? b.whitelists : [],
|
|
12439
|
+
whitelist_keys: b.whitelist_keys
|
|
12440
|
+
},
|
|
12441
|
+
{ allowGlobalWhitelists: options.allowGlobalWhitelists }
|
|
12442
|
+
);
|
|
12443
|
+
if (rejected.length > 0) {
|
|
12444
|
+
const report = options.onRuleRejected;
|
|
12445
|
+
if (typeof report === "function") {
|
|
12446
|
+
for (const r of rejected) {
|
|
12447
|
+
notify(report, r, "onRuleRejected");
|
|
12448
|
+
}
|
|
12449
|
+
} else {
|
|
12450
|
+
const sample = rejected.slice(0, 3).map((r) => `${r.id} (${r.reason})`).join("; ");
|
|
12451
|
+
console.warn(
|
|
12452
|
+
`[patchstack] ${rejected.length} delivered rule(s) rejected as invalid/oversized and are NOT enforced: ${sample}` + (rejected.length > 3 ? ", \u2026" : "")
|
|
12453
|
+
);
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12456
|
+
return {
|
|
12457
|
+
firewall: checked.firewall,
|
|
12458
|
+
whitelists: checked.whitelists,
|
|
12459
|
+
whitelist_keys: checked.whitelist_keys,
|
|
12460
|
+
...enforcement === "block" || enforcement === "dry-run" ? { enforcement } : {}
|
|
12461
|
+
};
|
|
12462
|
+
}
|
|
12463
|
+
function emptyBundle() {
|
|
12464
|
+
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
12465
|
+
}
|
|
12466
|
+
|
|
12467
|
+
// src/protect/rules/store.js
|
|
12468
|
+
var fsMod;
|
|
12469
|
+
async function loadFs() {
|
|
12470
|
+
if (fsMod !== void 0) return fsMod;
|
|
12471
|
+
try {
|
|
12472
|
+
const [fs, path17] = await Promise.all([import("fs"), import("path")]);
|
|
12473
|
+
fsMod = {
|
|
12474
|
+
chmodSync: fs.chmodSync,
|
|
12475
|
+
closeSync: fs.closeSync,
|
|
12476
|
+
constants: fs.constants,
|
|
12477
|
+
fstatSync: fs.fstatSync,
|
|
12478
|
+
fsyncSync: fs.fsyncSync,
|
|
12479
|
+
lstatSync: fs.lstatSync,
|
|
12480
|
+
openSync: fs.openSync,
|
|
12481
|
+
readFileSync: fs.readFileSync,
|
|
12482
|
+
mkdirSync: fs.mkdirSync,
|
|
12483
|
+
renameSync: fs.renameSync,
|
|
12484
|
+
unlinkSync: fs.unlinkSync,
|
|
12485
|
+
writeFileSync: fs.writeFileSync,
|
|
12486
|
+
join: path17.join
|
|
12487
|
+
};
|
|
12488
|
+
} catch {
|
|
12489
|
+
fsMod = null;
|
|
12490
|
+
}
|
|
12491
|
+
return fsMod;
|
|
12492
|
+
}
|
|
12493
|
+
async function sourceIdentity(options = {}) {
|
|
12494
|
+
if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
|
|
12495
|
+
const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
|
|
12496
|
+
return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
|
|
12497
|
+
}
|
|
12498
|
+
if (typeof options.token === "string" && options.token !== "") {
|
|
12499
|
+
const digest = await sha256Hex(options.token);
|
|
12500
|
+
if (digest === null) return null;
|
|
12501
|
+
const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
|
|
12502
|
+
return `token:${digest}@${endpoint}`;
|
|
12503
|
+
}
|
|
12504
|
+
return null;
|
|
12505
|
+
}
|
|
12506
|
+
async function sha256Hex(text) {
|
|
12507
|
+
try {
|
|
12508
|
+
const subtle = globalThis.crypto?.subtle;
|
|
12509
|
+
if (!subtle || typeof TextEncoder === "undefined") return null;
|
|
12510
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
12511
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
12512
|
+
} catch {
|
|
12513
|
+
return null;
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
function makeStore(options = {}) {
|
|
12517
|
+
let mem = null;
|
|
12518
|
+
const durable = durableTier(options);
|
|
12519
|
+
let identity2;
|
|
12520
|
+
const identityOf = () => identity2 ??= sourceIdentity(options);
|
|
12521
|
+
return {
|
|
12522
|
+
async read() {
|
|
12523
|
+
if (mem) return mem;
|
|
12524
|
+
const source = await identityOf();
|
|
12525
|
+
if (source === null) return null;
|
|
12526
|
+
const env = await durable.read();
|
|
12527
|
+
if (!env || env.source !== source) return null;
|
|
12528
|
+
mem = env;
|
|
12529
|
+
return env;
|
|
12530
|
+
},
|
|
12531
|
+
async write(env) {
|
|
12532
|
+
if (!env) return;
|
|
12533
|
+
const stamped = { ...env, source: await identityOf() };
|
|
12534
|
+
mem = stamped;
|
|
12535
|
+
await durable.write(stamped);
|
|
12536
|
+
}
|
|
12537
|
+
};
|
|
12538
|
+
}
|
|
12539
|
+
function durableTier(options) {
|
|
12540
|
+
const adapter = options.ruleCache;
|
|
12541
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
12542
|
+
return {
|
|
12543
|
+
read: async () => {
|
|
12544
|
+
try {
|
|
12545
|
+
return toEnvelope(await adapter.read());
|
|
12546
|
+
} catch {
|
|
12547
|
+
return null;
|
|
12548
|
+
}
|
|
12549
|
+
},
|
|
12550
|
+
write: async (env) => {
|
|
12551
|
+
try {
|
|
12552
|
+
await adapter.write(env);
|
|
12553
|
+
} catch {
|
|
12554
|
+
}
|
|
12555
|
+
}
|
|
12556
|
+
};
|
|
12557
|
+
}
|
|
12558
|
+
const dir = options.cacheDir;
|
|
12559
|
+
return {
|
|
12560
|
+
read: async () => cacheRead(dir),
|
|
12561
|
+
write: async (env) => cacheWrite(dir, env)
|
|
12562
|
+
};
|
|
12563
|
+
}
|
|
12564
|
+
async function cacheWrite(dir, env) {
|
|
12565
|
+
if (!dir) return;
|
|
12566
|
+
const fs = await loadFs();
|
|
12567
|
+
if (!fs) return;
|
|
12568
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
12569
|
+
const temporary = fs.join(dir, `.patchstack-rules-${randomSuffix()}.tmp`);
|
|
12570
|
+
let descriptor = null;
|
|
12571
|
+
try {
|
|
12572
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
12573
|
+
const directory = fs.lstatSync(dir);
|
|
12574
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return;
|
|
12575
|
+
let mode;
|
|
12576
|
+
try {
|
|
12577
|
+
const current = fs.lstatSync(target);
|
|
12578
|
+
if (current.isSymbolicLink() || !current.isFile()) return;
|
|
12579
|
+
mode = current.mode & 4095;
|
|
12580
|
+
} catch (error) {
|
|
12581
|
+
if (error?.code !== "ENOENT") return;
|
|
12582
|
+
}
|
|
12583
|
+
descriptor = fs.openSync(temporary, "wx", mode ?? 384);
|
|
12584
|
+
fs.writeFileSync(descriptor, JSON.stringify(env), "utf8");
|
|
12585
|
+
fs.fsyncSync(descriptor);
|
|
12586
|
+
fs.closeSync(descriptor);
|
|
12587
|
+
descriptor = null;
|
|
12588
|
+
if (mode !== void 0) fs.chmodSync(temporary, mode);
|
|
12589
|
+
try {
|
|
12590
|
+
fs.renameSync(temporary, target);
|
|
12591
|
+
} catch (error) {
|
|
12592
|
+
if (typeof process === "undefined" || process.platform !== "win32" || error?.code !== "EEXIST" && error?.code !== "EPERM") throw error;
|
|
12593
|
+
try {
|
|
12594
|
+
fs.unlinkSync(target);
|
|
12595
|
+
} catch (unlinkError) {
|
|
12596
|
+
if (unlinkError?.code !== "ENOENT") throw unlinkError;
|
|
12597
|
+
}
|
|
12598
|
+
fs.renameSync(temporary, target);
|
|
12599
|
+
}
|
|
12600
|
+
} catch {
|
|
12601
|
+
} finally {
|
|
12602
|
+
if (descriptor !== null) {
|
|
12603
|
+
try {
|
|
12604
|
+
fs.closeSync(descriptor);
|
|
12605
|
+
} catch {
|
|
12606
|
+
}
|
|
12607
|
+
}
|
|
12608
|
+
try {
|
|
12609
|
+
fs.unlinkSync(temporary);
|
|
12610
|
+
} catch {
|
|
12611
|
+
}
|
|
12612
|
+
}
|
|
12613
|
+
}
|
|
12614
|
+
async function cacheRead(dir) {
|
|
12615
|
+
if (!dir) return null;
|
|
12616
|
+
const fs = await loadFs();
|
|
12617
|
+
if (!fs) return null;
|
|
12618
|
+
const target = fs.join(dir, "patchstack-rules.json");
|
|
12619
|
+
let descriptor = null;
|
|
12620
|
+
try {
|
|
12621
|
+
const entry = fs.lstatSync(target);
|
|
12622
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > 16 * 1024 * 1024) return null;
|
|
12623
|
+
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0);
|
|
12624
|
+
descriptor = fs.openSync(target, flags);
|
|
12625
|
+
const opened = fs.fstatSync(descriptor);
|
|
12626
|
+
if (!opened.isFile() || opened.size > 16 * 1024 * 1024) return null;
|
|
12627
|
+
return toEnvelope(JSON.parse(fs.readFileSync(descriptor, "utf8")));
|
|
12628
|
+
} catch {
|
|
12629
|
+
return null;
|
|
12630
|
+
} finally {
|
|
12631
|
+
if (descriptor !== null) {
|
|
12632
|
+
try {
|
|
12633
|
+
fs.closeSync(descriptor);
|
|
12634
|
+
} catch {
|
|
12635
|
+
}
|
|
12636
|
+
}
|
|
12637
|
+
}
|
|
12638
|
+
}
|
|
12639
|
+
function randomSuffix() {
|
|
12640
|
+
try {
|
|
12641
|
+
if (typeof globalThis.crypto?.randomUUID === "function") return globalThis.crypto.randomUUID();
|
|
12642
|
+
} catch {
|
|
12643
|
+
}
|
|
12644
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
12645
|
+
}
|
|
12646
|
+
function toEnvelope(value) {
|
|
12647
|
+
if (!value || typeof value !== "object") return null;
|
|
12648
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
12649
|
+
const buildId = canonicalBuildId(value.buildId);
|
|
12650
|
+
const matched = canonicalBuildId(value.matchedBuildId);
|
|
12651
|
+
return {
|
|
12652
|
+
bundle: value.bundle,
|
|
12653
|
+
etag: value.etag ?? null,
|
|
12654
|
+
buildId,
|
|
12655
|
+
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
12656
|
+
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
12657
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null,
|
|
12658
|
+
source: typeof value.source === "string" ? value.source : null
|
|
12659
|
+
};
|
|
12660
|
+
}
|
|
12661
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
12662
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
|
|
12663
|
+
}
|
|
12664
|
+
return null;
|
|
12665
|
+
}
|
|
12666
|
+
|
|
12667
|
+
// src/setup-sync.ts
|
|
12668
|
+
async function syncSetupProtection(cwd, config) {
|
|
12669
|
+
const notices = [];
|
|
12670
|
+
const warnings2 = [];
|
|
12671
|
+
let map;
|
|
12672
|
+
try {
|
|
12673
|
+
map = await runMapDetailed(/* @__PURE__ */ new Map([["dir", cwd], ["upload", true]]), {
|
|
12674
|
+
config,
|
|
12675
|
+
setup: true,
|
|
12676
|
+
log: (line) => notices.push(line)
|
|
12677
|
+
});
|
|
12678
|
+
} catch {
|
|
12679
|
+
map = { code: 1, error: "Could not analyse or bind the map; check the project files and permissions." };
|
|
12680
|
+
}
|
|
12681
|
+
const unavailable = (error) => ({
|
|
12682
|
+
map,
|
|
12683
|
+
rules: { ok: false, count: 0, origin: "empty", error },
|
|
12684
|
+
notices,
|
|
12685
|
+
warnings: warnings2
|
|
12686
|
+
});
|
|
12687
|
+
if (!isCanonicalUuid(config.siteUuid)) return unavailable("No site UUID is configured.");
|
|
12688
|
+
if (!config.pulseAuth) return unavailable("Set PATCHSTACK_API_KEY or run patchstack-connect login to fetch live rules.");
|
|
12689
|
+
try {
|
|
12690
|
+
const rulesUrl = buildRulesUrl(config.endpoint, config.siteUuid);
|
|
12691
|
+
assertConnectableEndpoint(config, rulesUrl);
|
|
12692
|
+
const base = rulesUrl.slice(0, rulesUrl.lastIndexOf("/rules/"));
|
|
12693
|
+
const defaultRules = buildRulesUrl(DEFAULT_ENDPOINT, config.siteUuid);
|
|
12694
|
+
if (rulesUrl !== defaultRules && process.env.PATCHSTACK_PULSE_RULES_URL !== base) {
|
|
12695
|
+
warnings2.push("A custom rules service was used for setup. Set PATCHSTACK_PULSE_RULES_URL to the same Pulse base in the server environment for runtime updates.");
|
|
12696
|
+
}
|
|
12697
|
+
const options = {
|
|
12698
|
+
siteUuid: config.siteUuid,
|
|
12699
|
+
// Match the ordinary guard's default cache identity; explicit endpoints remain isolated.
|
|
12700
|
+
pulseRulesUrl: base,
|
|
12701
|
+
cacheDir: join26(cwd, ".patchstack"),
|
|
12702
|
+
buildId: map.buildId ?? void 0,
|
|
12703
|
+
onError: () => notices.push("Some rules could not be refreshed or are detect-only; verify delivery after starting the app.")
|
|
12704
|
+
};
|
|
12705
|
+
const ignored = await ensureIgnored(cwd, ".patchstack/", "Patchstack local runtime cache");
|
|
12706
|
+
if (!ignored.ignored) warnings2.push("The local rule cache is not git-ignored. Keep .patchstack/ out of version control.");
|
|
12707
|
+
const store = makeStore({ ...options, pulseRulesUrl: rulesUrl === defaultRules ? void 0 : base });
|
|
12708
|
+
const bundle = await resolveRules(options, store, {
|
|
12709
|
+
pulseAuth: config.pulseAuth,
|
|
12710
|
+
timeoutMs: Math.min(config.timeoutMs, 1e4)
|
|
12711
|
+
});
|
|
12712
|
+
return {
|
|
12713
|
+
map,
|
|
12714
|
+
notices,
|
|
12715
|
+
warnings: warnings2,
|
|
12716
|
+
rules: {
|
|
12717
|
+
ok: bundle.source.ok,
|
|
12718
|
+
origin: bundle.source.origin,
|
|
12719
|
+
count: bundle.firewall.length,
|
|
12720
|
+
...bundle.source.ok ? {} : { error: bundle.source.reason ?? "Live rules could not be fetched." }
|
|
12721
|
+
}
|
|
12722
|
+
};
|
|
12723
|
+
} catch {
|
|
12724
|
+
return unavailable("Live rules could not be fetched. Check the endpoint, authentication and project permissions.");
|
|
12725
|
+
}
|
|
12726
|
+
}
|
|
12727
|
+
|
|
12728
|
+
// src/cli.ts
|
|
12729
|
+
var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
|
|
12730
|
+
|
|
12731
|
+
Usage:
|
|
12732
|
+
patchstack-connect scan [options] Scan lockfile and POST to Patchstack.
|
|
12733
|
+
If no UUID is configured, the server
|
|
12734
|
+
provisions one and we persist it. After a
|
|
12735
|
+
successful post it also adds/updates the
|
|
12736
|
+
Patchstack Connector <script> tag in the root
|
|
12737
|
+
HTML shell (index.html, public/index.html,
|
|
12738
|
+
or src/app.html) \u2014 opt out with
|
|
12739
|
+
"widget": false in .patchstackrc.json.
|
|
12740
|
+
Run as a postinstall/prebuild/build hook, a
|
|
12741
|
+
report Patchstack cannot accept is printed
|
|
12742
|
+
and exits 0 so the build goes on; run
|
|
12743
|
+
directly, the same failure exits 1
|
|
12744
|
+
patchstack-connect setup [options] Finish the bounded project setup: run scan,
|
|
12745
|
+
manage the widget, install + verify runtime
|
|
12746
|
+
protection, upload the attack-surface map, pull
|
|
12747
|
+
live rules, and wire dependency/build scans.
|
|
12748
|
+
Never runs the project build
|
|
12749
|
+
patchstack-connect map [--dir <p>] [--out <f>] [--upload]
|
|
12750
|
+
Map the app's attack surface: entry points, the
|
|
12751
|
+
inputs each reads, the sinks it can reach, and
|
|
12752
|
+
evidence-backed input\u2192sink flows (each labelled
|
|
12753
|
+
with how the link was established). Best-effort static
|
|
12754
|
+
analysis \u2014 reports the DETECTED surface, with
|
|
12755
|
+
coverage counters. Prints JSON (--out writes a
|
|
12756
|
+
file; a pre-bundle --upload also stamps the existing
|
|
12757
|
+
guard rules file; --follow-symlinks leaves the project dir).
|
|
12758
|
+
Uses the app's own TypeScript
|
|
12759
|
+
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
12760
|
+
with an existing site UUID
|
|
12761
|
+
patchstack-connect status [options] Show current configuration and whether the
|
|
12762
|
+
site still exists on Patchstack (active /
|
|
12763
|
+
removed)
|
|
12764
|
+
patchstack-connect uninstall [options] Signal Patchstack that this package is being
|
|
12765
|
+
removed from the project. An unclaimed site
|
|
12766
|
+
record is deleted; a claimed site is flagged
|
|
12767
|
+
for its owner to remove in the dashboard.
|
|
12768
|
+
Does NOT touch local files \u2014 see the
|
|
12769
|
+
"Uninstalling" steps in AGENT-INSTALL.md
|
|
12770
|
+
patchstack-connect mark-build [options] Stamp built HTML with a production flag +
|
|
12771
|
+
build fingerprint, and ensure the widget
|
|
12772
|
+
tag in built pages (run as a postbuild step).
|
|
12773
|
+
The flag marks a page as the LIVE site, so it
|
|
12774
|
+
is stamped only on a production build; a local
|
|
12775
|
+
or preview build gets the widget tag and no
|
|
12776
|
+
marker (and any stale marker is removed).
|
|
12777
|
+
Looks in the framework's own output dir, an
|
|
12778
|
+
--output the build script names, then dist/,
|
|
12779
|
+
build/, out/, .output/public/, _site/ \u2014 inside
|
|
12780
|
+
the project only. --dir <path> overrides.
|
|
12781
|
+
patchstack-connect protect [--demo|--check] Install always-on runtime protection (the
|
|
12782
|
+
guard). Auto-wires supported server stacks;
|
|
12783
|
+
for others it scaffolds a
|
|
12784
|
+
generic guard + prints a wiring plan.
|
|
12785
|
+
--demo seeds a broad sample rule set (for
|
|
12786
|
+
demonstrations, not production).
|
|
12787
|
+
--check verifies the guard is wired (exit 1
|
|
12788
|
+
if not) \u2014 for the wire-then-verify loop.
|
|
12789
|
+
It reads the app's source and never runs it.
|
|
12790
|
+
--check --runtime additionally STARTS THE APP
|
|
12791
|
+
on a loopback port and sends it one request,
|
|
12792
|
+
to establish that a request reaches the
|
|
12793
|
+
guard. Opt-in, and the only mode that runs
|
|
12794
|
+
the app. Exit 0 traversed, 1 a listener
|
|
12795
|
+
answered instead of the guard, 2 could not
|
|
12796
|
+
be established (e.g. a TypeScript or
|
|
12797
|
+
framework entry this must not start).
|
|
12798
|
+
patchstack-connect demo node-serialize [--url URL] Run the production-backed node-serialize
|
|
12799
|
+
walkthrough: verify the vulnerable package,
|
|
12800
|
+
scan it, wait for live rule 18843, install +
|
|
12801
|
+
verify the guard, and print test requests.
|
|
12802
|
+
Does not install vulnerable dependencies.
|
|
12803
|
+
patchstack-connect demo-guide node-serialize Show a read-only, state-aware walkthrough for
|
|
12804
|
+
preparing, running, proving, and cleaning up
|
|
12805
|
+
the production-backed local demo.
|
|
12806
|
+
patchstack-connect guide [--full] Show this project's setup status (what's done,
|
|
12807
|
+
what's missing, with tailored commands), then
|
|
12808
|
+
print the full setup guide. --full prints the
|
|
12809
|
+
guide even when setup is complete
|
|
12810
|
+
patchstack-connect claim [--wait] Attach this site to a Patchstack account from
|
|
12811
|
+
the terminal, so its reports become visible.
|
|
10485
12812
|
Prints a link for the user to open and sign in
|
|
10486
12813
|
(or sign up) \u2014 whoever approves becomes the
|
|
10487
12814
|
site's owner. In a terminal it then waits. When
|
|
@@ -10874,6 +13201,8 @@ async function runScan(args, options = {}) {
|
|
|
10874
13201
|
const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
|
|
10875
13202
|
say(`Environment: ${config.environment}${because}.`);
|
|
10876
13203
|
}
|
|
13204
|
+
const overruled = ignoredFileEnvironmentWarning(config);
|
|
13205
|
+
if (overruled !== null) console.warn(`patchstack: ${overruled}`);
|
|
10877
13206
|
if (config.endpoint !== DEFAULT_ENDPOINT) {
|
|
10878
13207
|
say(`Endpoint override: ${config.endpoint}`);
|
|
10879
13208
|
}
|
|
@@ -11069,6 +13398,12 @@ function reportSourceWidget(siteUuid, framework, report) {
|
|
|
11069
13398
|
detail(`Widget: skipped (${err.message}).`);
|
|
11070
13399
|
}
|
|
11071
13400
|
}
|
|
13401
|
+
function ignoredFileEnvironmentWarning(config) {
|
|
13402
|
+
const ignored = config.ignoredFileEnvironment;
|
|
13403
|
+
if (ignored == null) return null;
|
|
13404
|
+
const because = (config.environmentEvidence ?? []).length > 0 ? ` (${config.environmentEvidence.join("; ")})` : "";
|
|
13405
|
+
return `.patchstackrc.json sets "environment": "${ignored}", but this build is production${because}, so it is treated as production. Remove "environment" from .patchstackrc.json and set PATCHSTACK_ENVIRONMENT=${ignored} only in the process that needs it.`;
|
|
13406
|
+
}
|
|
11072
13407
|
function reportSourceMarker(framework, checksum, report) {
|
|
11073
13408
|
try {
|
|
11074
13409
|
const shell = resolveWidgetFileHint(process.cwd(), framework);
|
|
@@ -11251,7 +13586,7 @@ async function runGuide(args) {
|
|
|
11251
13586
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
11252
13587
|
console.log("");
|
|
11253
13588
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
11254
|
-
console.log(
|
|
13589
|
+
console.log(readFileSync20(guidePath, "utf8"));
|
|
11255
13590
|
return 0;
|
|
11256
13591
|
}
|
|
11257
13592
|
function reportProtection(protection, report) {
|
|
@@ -11306,11 +13641,11 @@ Run: ${installCommand(before.packageManager)}`
|
|
|
11306
13641
|
return 1;
|
|
11307
13642
|
}
|
|
11308
13643
|
const report = emptyReport();
|
|
11309
|
-
let
|
|
13644
|
+
let reported2 = {};
|
|
11310
13645
|
const scanCode = await runScan(args, {
|
|
11311
13646
|
report,
|
|
11312
13647
|
onReported: (outcome) => {
|
|
11313
|
-
|
|
13648
|
+
reported2 = outcome;
|
|
11314
13649
|
}
|
|
11315
13650
|
});
|
|
11316
13651
|
if (scanCode !== 0) {
|
|
@@ -11321,11 +13656,29 @@ Run: ${installCommand(before.packageManager)}`
|
|
|
11321
13656
|
report.done.push(buildStepsLine(wired));
|
|
11322
13657
|
detail(`Build hooks: ${wired.detail}`);
|
|
11323
13658
|
const config = await resolveCliConfig(args);
|
|
13659
|
+
const synced = await syncSetupProtection(process.cwd(), config);
|
|
13660
|
+
for (const line of synced.notices) detail(line);
|
|
13661
|
+
for (const [index, warning] of synced.warnings.entries()) {
|
|
13662
|
+
report.missing.push({ key: `setup-sync-${index}`, text: warning });
|
|
13663
|
+
}
|
|
13664
|
+
const uploaded = synced.map.upload;
|
|
13665
|
+
if (uploaded?.result === "stored" || uploaded?.result === "unchanged") {
|
|
13666
|
+
report.done.push(`Attack-surface map uploaded (revision ${uploaded.revision}; ${synced.map.endpoints ?? 0} detected entry points)`);
|
|
13667
|
+
if (!synced.map.buildId) report.missing.push({ key: "map-binding", text: "Map is not linked to runtime protection", hint: ["Finish the reported protection integration, then rerun setup. Rules for specific inputs can only detect, not block, until linked."] });
|
|
13668
|
+
} else {
|
|
13669
|
+
report.missing.push({ key: "map-upload", text: "Attack-surface map was not uploaded", hint: [synced.map.error ?? uploaded?.message ?? "Check connectivity and rerun setup."] });
|
|
13670
|
+
}
|
|
13671
|
+
if (synced.rules.ok) {
|
|
13672
|
+
report.done.push(`Live rule lookup succeeded (${synced.rules.count} delivered rules; ${synced.rules.count === 0 ? "no rules currently assigned" : "not proof of runtime enforcement"})`);
|
|
13673
|
+
} else {
|
|
13674
|
+
report.missing.push({ key: "rules-pull", text: "Live rule lookup is incomplete", hint: [synced.rules.error ?? "Check authentication and rerun setup."] });
|
|
13675
|
+
}
|
|
13676
|
+
report.missing.push({ key: "runtime-restart", text: "Load the new protection in your running app", hint: ["Restart the preview/server if it is already running. Deploy a new build to update the live app. Setup does not start or deploy it."] });
|
|
11324
13677
|
const after = await collectGuideState(process.cwd());
|
|
11325
|
-
const missing = mergeMissing(report.missing, guideMissing(after,
|
|
13678
|
+
const missing = mergeMissing(report.missing, guideMissing(after, reported2));
|
|
11326
13679
|
const context = scanNextStepContext(config, after, after.siteUuid);
|
|
11327
13680
|
if (verbose) console.log("");
|
|
11328
|
-
for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after,
|
|
13681
|
+
for (const line of renderStatus("Patchstack setup", { done: report.done, missing }, guideProgress(after, reported2), context, {
|
|
11329
13682
|
useColor: useColor(),
|
|
11330
13683
|
verbose
|
|
11331
13684
|
})) {
|
|
@@ -11399,7 +13752,7 @@ function describeStack(stack) {
|
|
|
11399
13752
|
}
|
|
11400
13753
|
function readBuildScript(cwd) {
|
|
11401
13754
|
try {
|
|
11402
|
-
const pkg = JSON.parse(
|
|
13755
|
+
const pkg = JSON.parse(readFileSync20(path16.join(cwd, "package.json"), "utf8"));
|
|
11403
13756
|
return pkg.scripts?.build;
|
|
11404
13757
|
} catch {
|
|
11405
13758
|
return void 0;
|
|
@@ -11456,6 +13809,8 @@ async function runMarkBuild(args) {
|
|
|
11456
13809
|
}
|
|
11457
13810
|
environment = config.environment;
|
|
11458
13811
|
environmentEvidence = config.environmentEvidence ?? [];
|
|
13812
|
+
const overruled = ignoredFileEnvironmentWarning(config);
|
|
13813
|
+
if (overruled !== null) console.warn(`mark-build: ${overruled}`);
|
|
11459
13814
|
} catch (err) {
|
|
11460
13815
|
console.warn(
|
|
11461
13816
|
`mark-build: could not resolve the site UUID (${err.message}). Skipping the widget pass.`
|
|
@@ -11463,7 +13818,7 @@ async function runMarkBuild(args) {
|
|
|
11463
13818
|
}
|
|
11464
13819
|
const forcedProduction = args.flags.get("production") === true;
|
|
11465
13820
|
const published = forcedProduction || marksPublishedBuild(environment);
|
|
11466
|
-
const
|
|
13821
|
+
const reported2 = config !== null && forcedProduction && config.environment !== "production" ? { ...config, environment: "production", environmentSource: "override" } : config;
|
|
11467
13822
|
const buildDirOptions = { framework: stack?.framework ?? null, buildScript: readBuildScript(cwd) };
|
|
11468
13823
|
const dir = resolveBuildDir(cwd, getStringFlag(args.flags, "dir"), buildDirOptions);
|
|
11469
13824
|
if (dir === null) {
|
|
@@ -11474,14 +13829,14 @@ async function runMarkBuild(args) {
|
|
|
11474
13829
|
console.warn(
|
|
11475
13830
|
'mark-build: if the build writes elsewhere, set it once in package.json \u2192 "postbuild": "patchstack-connect mark-build --dir <path>".'
|
|
11476
13831
|
);
|
|
11477
|
-
await reportBuildStamp(
|
|
13832
|
+
await reportBuildStamp(reported2, wirePayload, "no-output");
|
|
11478
13833
|
return 0;
|
|
11479
13834
|
}
|
|
11480
13835
|
const files = findHtmlFiles(dir);
|
|
11481
13836
|
if (files.length === 0) {
|
|
11482
13837
|
console.warn(`mark-build: found ${dir} but no HTML files in it.`);
|
|
11483
13838
|
console.warn("mark-build: this looks like a server-rendered app. Run `npx @patchstack/connect guide` to see what your main layout needs.");
|
|
11484
|
-
await reportBuildStamp(
|
|
13839
|
+
await reportBuildStamp(reported2, wirePayload, "no-pages");
|
|
11485
13840
|
return 0;
|
|
11486
13841
|
}
|
|
11487
13842
|
const staleBy = staleBuildAge(files, startedAt);
|
|
@@ -11490,12 +13845,18 @@ async function runMarkBuild(args) {
|
|
|
11490
13845
|
`mark-build: nothing in ${dir} changed in the last ${Math.round(staleBy / 6e4)} minute(s). Run it right after the build (as postbuild).`
|
|
11491
13846
|
);
|
|
11492
13847
|
}
|
|
11493
|
-
const snippet = published ? buildInjectionSnippet(checksum, stack) : "";
|
|
13848
|
+
const snippet = published ? buildInjectionSnippet(checksum, stack) : environment === "sandbox" ? buildSandboxSnippet() : "";
|
|
11494
13849
|
let marked = 0;
|
|
11495
13850
|
let widgetTouched = 0;
|
|
13851
|
+
let handRemoved = 0;
|
|
11496
13852
|
for (const file of files) {
|
|
11497
|
-
const before =
|
|
13853
|
+
const before = readFileSync20(file, "utf8");
|
|
11498
13854
|
let after = injectMarker(before, snippet);
|
|
13855
|
+
if (!published) {
|
|
13856
|
+
const cleared = removeHandMarkers(after);
|
|
13857
|
+
if (cleared !== after) handRemoved += 1;
|
|
13858
|
+
after = cleared;
|
|
13859
|
+
}
|
|
11499
13860
|
if (widgetUuid !== null) {
|
|
11500
13861
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
11501
13862
|
if (ensured.action === "added" || ensured.action === "updated") {
|
|
@@ -11514,7 +13875,7 @@ async function runMarkBuild(args) {
|
|
|
11514
13875
|
detail(
|
|
11515
13876
|
`mark-build: marked ${marked} HTML file(s) in ${dir}${checksum !== null ? ` (build ${checksum})` : ""}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
11516
13877
|
);
|
|
11517
|
-
await reportBuildStamp(
|
|
13878
|
+
await reportBuildStamp(reported2, wirePayload, "stamped");
|
|
11518
13879
|
return 0;
|
|
11519
13880
|
}
|
|
11520
13881
|
const because = environmentEvidence.length > 0 ? ` (${environmentEvidence.join("; ")})` : "";
|
|
@@ -11524,12 +13885,17 @@ async function runMarkBuild(args) {
|
|
|
11524
13885
|
detail(
|
|
11525
13886
|
`mark-build: ${environment} build${because}, so no production marker in ${files.length} HTML file(s) in ${dir}${widgetTouched > 0 ? `, widget tag ensured in ${widgetTouched}` : ""}${stackSummary !== null ? ` [${stackSummary}]` : ""}.`
|
|
11526
13887
|
);
|
|
11527
|
-
|
|
13888
|
+
if (handRemoved > 0) {
|
|
13889
|
+
console.warn(
|
|
13890
|
+
`mark-build: removed a hand-added __PATCHSTACK_PROD__ script from ${plural(handRemoved, "page", "pages")}. Production builds get the marker from mark-build, so it does not need to be in your source.`
|
|
13891
|
+
);
|
|
13892
|
+
}
|
|
13893
|
+
await reportBuildStamp(reported2, wirePayload, "withheld");
|
|
11528
13894
|
return 0;
|
|
11529
13895
|
}
|
|
11530
13896
|
function packageVersion() {
|
|
11531
13897
|
try {
|
|
11532
|
-
const require2 =
|
|
13898
|
+
const require2 = createRequire6(import.meta.url);
|
|
11533
13899
|
const manifest = require2("../package.json");
|
|
11534
13900
|
return typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : "unknown";
|
|
11535
13901
|
} catch {
|