@patchstack/connect 0.5.2 → 0.5.3
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 +51 -7
- package/README.md +6 -3
- package/dist/{chunk-3G2I6QL6.js → chunk-IOODP4ZB.js} +19 -2
- package/dist/chunk-IOODP4ZB.js.map +1 -0
- package/dist/cli.js +315 -59
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect.cjs +182 -21
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +26 -0
- package/dist/protect.d.ts +26 -0
- package/dist/protect.edge.js +172 -21
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +162 -24
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-2MMPQN2A.js → refresh-manifest-3QGLYXV5.js} +2 -2
- package/dist/{refresh-manifest-2MMPQN2A.js.map → refresh-manifest-3QGLYXV5.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3G2I6QL6.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync11 } from "fs";
|
|
5
5
|
import { createRequire as createRequire2 } from "module";
|
|
6
6
|
|
|
7
7
|
// src/parsers/index.ts
|
|
@@ -1288,6 +1288,27 @@ async function pulseFetch(config, url, init, fetchImpl = fetch) {
|
|
|
1288
1288
|
return first.response;
|
|
1289
1289
|
}
|
|
1290
1290
|
|
|
1291
|
+
// src/build-id.ts
|
|
1292
|
+
var BUILD_ID = /^[0-9a-f]{64}$/i;
|
|
1293
|
+
function canonicalBuildId(value) {
|
|
1294
|
+
if (typeof value !== "string") return null;
|
|
1295
|
+
const trimmed = value.trim();
|
|
1296
|
+
return BUILD_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
1297
|
+
}
|
|
1298
|
+
var BUILD_STAMP_KEY = "_patchstack";
|
|
1299
|
+
function readBuildStamp(bundle) {
|
|
1300
|
+
if (bundle === null || typeof bundle !== "object") return null;
|
|
1301
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
1302
|
+
if (namespace === null || typeof namespace !== "object") return null;
|
|
1303
|
+
return canonicalBuildId(namespace.build_id);
|
|
1304
|
+
}
|
|
1305
|
+
function hasRawBuildStamp(bundle) {
|
|
1306
|
+
if (bundle === null || typeof bundle !== "object") return false;
|
|
1307
|
+
const namespace = bundle[BUILD_STAMP_KEY];
|
|
1308
|
+
if (namespace === null || typeof namespace !== "object") return false;
|
|
1309
|
+
return "build_id" in namespace;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1291
1312
|
// src/client.ts
|
|
1292
1313
|
var DEFAULT_ENDPOINT = "https://api.patchstack.com/monitor/pulse/manifest";
|
|
1293
1314
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
@@ -1359,12 +1380,13 @@ function buildInputMapUrl(manifestEndpoint, siteUuid) {
|
|
|
1359
1380
|
url.hash = "";
|
|
1360
1381
|
return url.toString();
|
|
1361
1382
|
}
|
|
1362
|
-
async function postInputMap(config, map) {
|
|
1383
|
+
async function postInputMap(config, map, buildId) {
|
|
1363
1384
|
if (config.siteUuid === null) {
|
|
1364
1385
|
return { result: "skipped", message: "No site UUID configured \u2014 run `patchstack-connect scan` first." };
|
|
1365
1386
|
}
|
|
1366
1387
|
const url = buildInputMapUrl(config.endpoint, config.siteUuid);
|
|
1367
1388
|
try {
|
|
1389
|
+
const canonical = canonicalBuildId(buildId);
|
|
1368
1390
|
const response = await pulseFetch(config, url, {
|
|
1369
1391
|
method: "POST",
|
|
1370
1392
|
headers: {
|
|
@@ -1372,7 +1394,12 @@ async function postInputMap(config, map) {
|
|
|
1372
1394
|
"Content-Type": "application/json",
|
|
1373
1395
|
"User-Agent": "@patchstack/connect"
|
|
1374
1396
|
},
|
|
1375
|
-
|
|
1397
|
+
// `build_id` is additive to the v3 document: a reader that ignores it behaves exactly as before.
|
|
1398
|
+
// It is the digest of the policy map carrying these coordinates, so the server can serve a rule pinned
|
|
1399
|
+
// to one of them as enforceable only to a guard carrying that same map. Omitted entirely when this
|
|
1400
|
+
// run could not bind the map into a pre-bundle guard — an absent field is a question the server
|
|
1401
|
+
// answers by withholding enforceability, which is the safe direction.
|
|
1402
|
+
body: JSON.stringify(canonical === null ? map : { ...map, build_id: canonical }),
|
|
1376
1403
|
signal: AbortSignal.timeout(config.timeoutMs)
|
|
1377
1404
|
});
|
|
1378
1405
|
if (response.status === 404) {
|
|
@@ -4292,9 +4319,9 @@ async function collectGuideState(cwd) {
|
|
|
4292
4319
|
endpointOverride,
|
|
4293
4320
|
hasBuildScript: Boolean(pkg?.scripts?.build?.trim()),
|
|
4294
4321
|
installScanWired: (pkg?.scripts?.postinstall ?? "").includes("patchstack-connect scan"),
|
|
4295
|
-
//
|
|
4296
|
-
//
|
|
4297
|
-
prebuildWired: (pkg?.scripts?.prebuild ?? "")
|
|
4322
|
+
// The scan has to run first: a later prebuild command may upload and stamp the map that the bundle
|
|
4323
|
+
// must retain. Bun skips npm-style pre/post scripts, so its build chain follows the same order.
|
|
4324
|
+
prebuildWired: /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.prebuild ?? "") || /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(pkg?.scripts?.build ?? ""),
|
|
4298
4325
|
postbuildWired: (pkg?.scripts?.postbuild ?? "").includes("patchstack-connect mark-build") || (pkg?.scripts?.build ?? "").includes("patchstack-connect mark-build"),
|
|
4299
4326
|
widgetInstalled: widget.found,
|
|
4300
4327
|
widgetTokenMatches: widget.uuidMatches,
|
|
@@ -5196,7 +5223,7 @@ function formatRuntimeCheck(report) {
|
|
|
5196
5223
|
}
|
|
5197
5224
|
|
|
5198
5225
|
// src/map-command.ts
|
|
5199
|
-
import { writeFileSync as
|
|
5226
|
+
import { writeFileSync as writeFileSync9 } from "fs";
|
|
5200
5227
|
|
|
5201
5228
|
// src/map/ts-loader.ts
|
|
5202
5229
|
import { createRequire } from "module";
|
|
@@ -6068,14 +6095,14 @@ function directSinks(node, ts, bindings, ctx) {
|
|
|
6068
6095
|
const root = base ? rootIdentifier(base, ts) : void 0;
|
|
6069
6096
|
if (!root) return {};
|
|
6070
6097
|
const spec = bindings.resolve(root);
|
|
6071
|
-
const
|
|
6098
|
+
const relative7 = spec !== void 0 && (spec.startsWith(".") || spec.startsWith("/"));
|
|
6072
6099
|
const pkg = npmPackageOf(spec);
|
|
6073
6100
|
if (pkg) return { pkg, root, spec };
|
|
6074
|
-
if (
|
|
6101
|
+
if (relative7 && ctx && spec) {
|
|
6075
6102
|
const viaModule = ctx.graph.importedBinding(ctx.file, spec, bindings.exportNameOf(root) ?? root);
|
|
6076
6103
|
if (viaModule) return { pkg: viaModule.package, root, spec };
|
|
6077
6104
|
}
|
|
6078
|
-
return { local: bindings.locals.has(root), root, spec, relative:
|
|
6105
|
+
return { local: bindings.locals.has(root), root, spec, relative: relative7 };
|
|
6079
6106
|
};
|
|
6080
6107
|
const attributionOf = (b, pkg) => b.pkg ? "import" : pkg ? "inferred" : void 0;
|
|
6081
6108
|
const dbApi = (pkg, attribution) => {
|
|
@@ -7608,6 +7635,243 @@ function getStringFlag(flags, name) {
|
|
|
7608
7635
|
return typeof value === "string" ? value : void 0;
|
|
7609
7636
|
}
|
|
7610
7637
|
|
|
7638
|
+
// src/build-stamp.ts
|
|
7639
|
+
import { chmodSync, lstatSync as lstatSync3, readFileSync as readFileSync14, readdirSync as readdirSync6, realpathSync as realpathSync5, renameSync, unlinkSync, writeFileSync as writeFileSync8 } from "fs";
|
|
7640
|
+
import { join as join19, relative as relative6, sep as sep2 } from "path";
|
|
7641
|
+
var RULES_FILE_NAMES = /* @__PURE__ */ new Set(["rules.json", "patchstack.rules.json"]);
|
|
7642
|
+
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", "dist", "build", "out", "coverage", "tmp", "vendor"]);
|
|
7643
|
+
var MAX_DEPTH2 = 6;
|
|
7644
|
+
var GUARD_SOURCE = /\.(?:[cm]?[jt]sx?)$/;
|
|
7645
|
+
var MAX_GUARD_SOURCE_BYTES = 1024 * 1024;
|
|
7646
|
+
var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7647
|
+
function guardImportsRulesFile(dir, rulesName) {
|
|
7648
|
+
const quotedRules = `["']\\./${escapeRegExp(rulesName)}["']`;
|
|
7649
|
+
const importsRules = new RegExp(`(?:\\bfrom\\s*|\\brequire\\s*\\(\\s*|\\bnew\\s+URL\\s*\\(\\s*)${quotedRules}`);
|
|
7650
|
+
let siblings;
|
|
7651
|
+
try {
|
|
7652
|
+
siblings = readdirSync6(dir);
|
|
7653
|
+
} catch {
|
|
7654
|
+
return false;
|
|
7655
|
+
}
|
|
7656
|
+
for (const name of siblings) {
|
|
7657
|
+
if (!GUARD_SOURCE.test(name)) continue;
|
|
7658
|
+
const path14 = join19(dir, name);
|
|
7659
|
+
try {
|
|
7660
|
+
const entry = lstatSync3(path14);
|
|
7661
|
+
if (!entry.isFile() || entry.isSymbolicLink() || entry.size > MAX_GUARD_SOURCE_BYTES) continue;
|
|
7662
|
+
const source = stripComments(readFileSync14(path14, "utf8"));
|
|
7663
|
+
if (source.includes("@patchstack/connect/protect") && importsRules.test(source)) return true;
|
|
7664
|
+
} catch {
|
|
7665
|
+
}
|
|
7666
|
+
}
|
|
7667
|
+
return false;
|
|
7668
|
+
}
|
|
7669
|
+
function isRuleBundle(value) {
|
|
7670
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
7671
|
+
const record = value;
|
|
7672
|
+
return Array.isArray(record.firewall) && Array.isArray(record.whitelists) && record.whitelist_keys !== null && typeof record.whitelist_keys === "object";
|
|
7673
|
+
}
|
|
7674
|
+
function findRulesFile(cwd) {
|
|
7675
|
+
const found = [];
|
|
7676
|
+
const unreadable = [];
|
|
7677
|
+
let root;
|
|
7678
|
+
try {
|
|
7679
|
+
root = realpathSync5(cwd);
|
|
7680
|
+
} catch {
|
|
7681
|
+
return { kind: "none" };
|
|
7682
|
+
}
|
|
7683
|
+
const contained = (path14) => {
|
|
7684
|
+
try {
|
|
7685
|
+
const real = realpathSync5(path14);
|
|
7686
|
+
return real === root || real.startsWith(root + sep2);
|
|
7687
|
+
} catch {
|
|
7688
|
+
return false;
|
|
7689
|
+
}
|
|
7690
|
+
};
|
|
7691
|
+
const walk2 = (dir, depth) => {
|
|
7692
|
+
if (depth > MAX_DEPTH2 || found.length > 8) return;
|
|
7693
|
+
let entries;
|
|
7694
|
+
try {
|
|
7695
|
+
entries = readdirSync6(dir);
|
|
7696
|
+
} catch {
|
|
7697
|
+
return;
|
|
7698
|
+
}
|
|
7699
|
+
for (const name of entries) {
|
|
7700
|
+
if (name.startsWith(".")) continue;
|
|
7701
|
+
const path14 = join19(dir, name);
|
|
7702
|
+
let entry;
|
|
7703
|
+
try {
|
|
7704
|
+
entry = lstatSync3(path14);
|
|
7705
|
+
} catch {
|
|
7706
|
+
continue;
|
|
7707
|
+
}
|
|
7708
|
+
if (entry.isSymbolicLink()) continue;
|
|
7709
|
+
if (entry.isDirectory()) {
|
|
7710
|
+
if (!SKIP_DIRECTORIES.has(name)) walk2(path14, depth + 1);
|
|
7711
|
+
continue;
|
|
7712
|
+
}
|
|
7713
|
+
if (!entry.isFile() || !RULES_FILE_NAMES.has(name)) continue;
|
|
7714
|
+
if (!contained(path14)) continue;
|
|
7715
|
+
if (!guardImportsRulesFile(dir, name)) continue;
|
|
7716
|
+
try {
|
|
7717
|
+
if (isRuleBundle(JSON.parse(readFileSync14(path14, "utf8")))) found.push(path14);
|
|
7718
|
+
} catch {
|
|
7719
|
+
unreadable.push(path14);
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
};
|
|
7723
|
+
walk2(cwd, 0);
|
|
7724
|
+
if (found.length > 1) return { kind: "ambiguous", paths: found.map((p) => relative6(cwd, p)) };
|
|
7725
|
+
if (found.length === 1) return { kind: "one", path: found[0] };
|
|
7726
|
+
if (unreadable.length > 0) return { kind: "unreadable", paths: unreadable.map((p) => relative6(cwd, p)) };
|
|
7727
|
+
return { kind: "none" };
|
|
7728
|
+
}
|
|
7729
|
+
function detectIndent(text) {
|
|
7730
|
+
const match = /\n([ \t]+)"/.exec(text);
|
|
7731
|
+
if (match === null) return 0;
|
|
7732
|
+
const whitespace = match[1];
|
|
7733
|
+
return whitespace.includes(" ") ? " " : whitespace.length;
|
|
7734
|
+
}
|
|
7735
|
+
function applyBuildStamp(cwd, id) {
|
|
7736
|
+
const location = findRulesFile(cwd);
|
|
7737
|
+
if (location.kind === "none") return { kind: "skipped", reason: "no guard rules file in this project" };
|
|
7738
|
+
if (location.kind === "unreadable") {
|
|
7739
|
+
return { kind: "skipped", reason: `${location.paths.join(", ")} is not valid JSON \u2014 left untouched` };
|
|
7740
|
+
}
|
|
7741
|
+
if (location.kind === "ambiguous") {
|
|
7742
|
+
return {
|
|
7743
|
+
kind: "skipped",
|
|
7744
|
+
reason: `more than one candidate guard rules file (${location.paths.join(", ")}) \u2014 none was stamped`
|
|
7745
|
+
};
|
|
7746
|
+
}
|
|
7747
|
+
const file = relative6(cwd, location.path);
|
|
7748
|
+
let text;
|
|
7749
|
+
try {
|
|
7750
|
+
text = readFileSync14(location.path, "utf8");
|
|
7751
|
+
} catch (err) {
|
|
7752
|
+
return { kind: "skipped", reason: `${file} could not be read (${err.message})` };
|
|
7753
|
+
}
|
|
7754
|
+
let parsed;
|
|
7755
|
+
try {
|
|
7756
|
+
parsed = JSON.parse(text);
|
|
7757
|
+
} catch {
|
|
7758
|
+
return { kind: "skipped", reason: `${file} is not valid JSON \u2014 left untouched` };
|
|
7759
|
+
}
|
|
7760
|
+
if (!isRuleBundle(parsed)) return { kind: "skipped", reason: `${file} is not a guard rules bundle` };
|
|
7761
|
+
const bundle = parsed;
|
|
7762
|
+
const current = readBuildStamp(bundle);
|
|
7763
|
+
if (id === null ? !hasRawBuildStamp(bundle) : current === id) return { kind: "unchanged", file };
|
|
7764
|
+
const namespace = { ...bundle[BUILD_STAMP_KEY] ?? {} };
|
|
7765
|
+
if (id === null) delete namespace.build_id;
|
|
7766
|
+
else namespace.build_id = id;
|
|
7767
|
+
if (Object.keys(namespace).length === 0) delete bundle[BUILD_STAMP_KEY];
|
|
7768
|
+
else bundle[BUILD_STAMP_KEY] = namespace;
|
|
7769
|
+
const serialised = JSON.stringify(bundle, null, detectIndent(text)) + (text.endsWith("\n") ? "\n" : "");
|
|
7770
|
+
const temporary = `${location.path}.patchstack-${process.pid}.tmp`;
|
|
7771
|
+
try {
|
|
7772
|
+
const mode = lstatSync3(location.path).mode & 4095;
|
|
7773
|
+
writeFileSync8(temporary, serialised, { encoding: "utf8", mode });
|
|
7774
|
+
chmodSync(temporary, mode);
|
|
7775
|
+
renameSync(temporary, location.path);
|
|
7776
|
+
} catch (err) {
|
|
7777
|
+
try {
|
|
7778
|
+
unlinkSync(temporary);
|
|
7779
|
+
} catch {
|
|
7780
|
+
}
|
|
7781
|
+
return { kind: "skipped", reason: `${file} could not be written (${err.message})` };
|
|
7782
|
+
}
|
|
7783
|
+
return id === null ? { kind: "cleared", file } : { kind: "stamped", file, id };
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
// src/build-hook.ts
|
|
7787
|
+
var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
|
|
7788
|
+
"preinstall",
|
|
7789
|
+
"install",
|
|
7790
|
+
"postinstall",
|
|
7791
|
+
"prepare",
|
|
7792
|
+
"prebuild",
|
|
7793
|
+
"build",
|
|
7794
|
+
"postbuild"
|
|
7795
|
+
]);
|
|
7796
|
+
function isInstallOrBuildHook(env = process.env) {
|
|
7797
|
+
const event = env.npm_lifecycle_event;
|
|
7798
|
+
return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
|
|
7799
|
+
}
|
|
7800
|
+
function isPreBundleBuildHook(env = process.env) {
|
|
7801
|
+
const event = env.npm_lifecycle_event;
|
|
7802
|
+
if (event === "prebuild") return true;
|
|
7803
|
+
if (event !== "build") return false;
|
|
7804
|
+
return /^\s*patchstack-connect\s+scan(?:\s*(?:&&|;)|\s*$)/.test(env.npm_lifecycle_script ?? "");
|
|
7805
|
+
}
|
|
7806
|
+
function undeliveredReportLines(err, config, cwd) {
|
|
7807
|
+
const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
|
|
7808
|
+
if (err.code === "UNAUTHORIZED") {
|
|
7809
|
+
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
7810
|
+
if (hasCredential) {
|
|
7811
|
+
lines.push(
|
|
7812
|
+
"patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
|
|
7813
|
+
);
|
|
7814
|
+
} else {
|
|
7815
|
+
lines.push(
|
|
7816
|
+
`patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
|
|
7817
|
+
`patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
|
|
7818
|
+
);
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
lines.push(
|
|
7822
|
+
"patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
|
|
7823
|
+
);
|
|
7824
|
+
return lines;
|
|
7825
|
+
}
|
|
7826
|
+
|
|
7827
|
+
// src/input-map-id.ts
|
|
7828
|
+
import { createHash as createHash6 } from "crypto";
|
|
7829
|
+
var NonCanonicalInputMap = class extends Error {
|
|
7830
|
+
};
|
|
7831
|
+
function canonicalString(value) {
|
|
7832
|
+
let out = '"';
|
|
7833
|
+
for (const character of value) {
|
|
7834
|
+
const code = character.codePointAt(0);
|
|
7835
|
+
if (character === '"') out += '\\"';
|
|
7836
|
+
else if (character === "\\") out += "\\\\";
|
|
7837
|
+
else if (code < 32) out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
7838
|
+
else out += character;
|
|
7839
|
+
}
|
|
7840
|
+
return `${out}"`;
|
|
7841
|
+
}
|
|
7842
|
+
function canonicalText(value) {
|
|
7843
|
+
if (value === null) return "null";
|
|
7844
|
+
if (value === true) return "true";
|
|
7845
|
+
if (value === false) return "false";
|
|
7846
|
+
if (typeof value === "string") return canonicalString(value);
|
|
7847
|
+
if (typeof value === "number") {
|
|
7848
|
+
if (!Number.isSafeInteger(value)) {
|
|
7849
|
+
throw new NonCanonicalInputMap("the input map contains a number outside the safe-integer form");
|
|
7850
|
+
}
|
|
7851
|
+
return String(value === 0 ? 0 : value);
|
|
7852
|
+
}
|
|
7853
|
+
if (Array.isArray(value)) return `[${value.map(canonicalText).join(",")}]`;
|
|
7854
|
+
if (value !== null && typeof value === "object") {
|
|
7855
|
+
const record = value;
|
|
7856
|
+
const keys = Object.keys(record).sort();
|
|
7857
|
+
return `{${keys.map((key) => `${canonicalString(key)}:${canonicalText(record[key])}`).join(",")}}`;
|
|
7858
|
+
}
|
|
7859
|
+
throw new NonCanonicalInputMap(`the input map contains a value with no JSON form (${typeof value})`);
|
|
7860
|
+
}
|
|
7861
|
+
function inputMapBuildId(map) {
|
|
7862
|
+
let material = map;
|
|
7863
|
+
if (map !== null && typeof map === "object" && !Array.isArray(map)) {
|
|
7864
|
+
const record = map;
|
|
7865
|
+
const { build_id: _buildId, ...mapDocument } = record;
|
|
7866
|
+
const coverage = record.coverage;
|
|
7867
|
+
if (coverage !== null && typeof coverage === "object" && !Array.isArray(coverage)) {
|
|
7868
|
+
const { analysisMs: _analysisMs, rssBytes: _rssBytes, peakRssBytes: _peakRssBytes, ...stableCoverage } = coverage;
|
|
7869
|
+
material = { ...mapDocument, coverage: stableCoverage };
|
|
7870
|
+
} else material = mapDocument;
|
|
7871
|
+
}
|
|
7872
|
+
return createHash6("sha256").update("patchstack-input-map-v1\0", "utf8").update(canonicalText(material), "utf8").digest("hex");
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7611
7875
|
// src/map-command.ts
|
|
7612
7876
|
async function runMap(flags) {
|
|
7613
7877
|
const cwd = getStringFlag(flags, "dir") ?? process.cwd();
|
|
@@ -7652,7 +7916,7 @@ async function runMap(flags) {
|
|
|
7652
7916
|
const json = JSON.stringify(map, null, 2);
|
|
7653
7917
|
const out = getStringFlag(flags, "out");
|
|
7654
7918
|
if (out) {
|
|
7655
|
-
|
|
7919
|
+
writeFileSync9(out, json);
|
|
7656
7920
|
console.error(`patchstack: wrote ${out}`);
|
|
7657
7921
|
} else if (flags.get("upload") !== true) {
|
|
7658
7922
|
console.log(json);
|
|
@@ -7668,7 +7932,25 @@ async function runMap(flags) {
|
|
|
7668
7932
|
cliSiteUuid: getStringFlag(flags, "site-uuid"),
|
|
7669
7933
|
cliEndpoint: getStringFlag(flags, "endpoint")
|
|
7670
7934
|
});
|
|
7671
|
-
|
|
7935
|
+
let buildId = null;
|
|
7936
|
+
if (isPreBundleBuildHook()) {
|
|
7937
|
+
const candidate = inputMapBuildId(map);
|
|
7938
|
+
const stamp = applyBuildStamp(cwd, candidate);
|
|
7939
|
+
if (stamp.kind === "stamped" || stamp.kind === "unchanged") {
|
|
7940
|
+
buildId = candidate;
|
|
7941
|
+
console.error(`patchstack: bound this map to ${stamp.file} (${candidate.slice(0, 12)}).`);
|
|
7942
|
+
} else {
|
|
7943
|
+
const reason = stamp.kind === "skipped" ? stamp.reason : "the rules file did not retain the map identity";
|
|
7944
|
+
console.error(
|
|
7945
|
+
`patchstack: could not bind this map to the runtime guard \u2014 ${reason}. Rules generated from these coordinates will detect only, not block.`
|
|
7946
|
+
);
|
|
7947
|
+
}
|
|
7948
|
+
} else {
|
|
7949
|
+
console.error(
|
|
7950
|
+
"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."
|
|
7951
|
+
);
|
|
7952
|
+
}
|
|
7953
|
+
const outcome = await postInputMap(config, map, buildId);
|
|
7672
7954
|
if (outcome.result === "stored") {
|
|
7673
7955
|
console.error(`patchstack: uploaded the attack surface (revision ${outcome.revision}).`);
|
|
7674
7956
|
} else if (outcome.result === "unchanged") {
|
|
@@ -7683,7 +7965,7 @@ async function runMap(flags) {
|
|
|
7683
7965
|
}
|
|
7684
7966
|
|
|
7685
7967
|
// src/setup.ts
|
|
7686
|
-
import { readFileSync as
|
|
7968
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync10 } from "fs";
|
|
7687
7969
|
import path13 from "path";
|
|
7688
7970
|
var SCAN_COMMAND = "patchstack-connect scan";
|
|
7689
7971
|
var MARK_BUILD_COMMAND = "patchstack-connect mark-build";
|
|
@@ -7701,9 +7983,16 @@ function appendHook(existing, command) {
|
|
|
7701
7983
|
}
|
|
7702
7984
|
return `${existing} && ${command}`;
|
|
7703
7985
|
}
|
|
7986
|
+
function prependHook(existing, command) {
|
|
7987
|
+
if (existing === void 0 || existing.trim().length === 0) return command;
|
|
7988
|
+
const escaped = command.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
7989
|
+
if (new RegExp(`^\\s*${escaped}\\s*(?:;|\\|\\|)`).test(existing)) return existing;
|
|
7990
|
+
const remaining = existing.split(/\s*&&\s*/).filter((part) => part.trim() !== command && part.trim().length > 0);
|
|
7991
|
+
return [command, ...remaining].join(" && ");
|
|
7992
|
+
}
|
|
7704
7993
|
function wireBuildScripts(cwd, packageManager) {
|
|
7705
7994
|
const target = path13.join(cwd, "package.json");
|
|
7706
|
-
const raw =
|
|
7995
|
+
const raw = readFileSync15(target, "utf8");
|
|
7707
7996
|
const pkg = JSON.parse(raw);
|
|
7708
7997
|
const scripts = pkg.scripts ?? {};
|
|
7709
7998
|
const build = scripts.build;
|
|
@@ -7718,10 +8007,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7718
8007
|
}
|
|
7719
8008
|
scripts.postinstall = postinstall;
|
|
7720
8009
|
} else if (packageManager === "bun") {
|
|
7721
|
-
let nextBuild = build;
|
|
7722
|
-
if (!nextBuild.includes(SCAN_COMMAND)) {
|
|
7723
|
-
nextBuild = `${SCAN_COMMAND} && ${nextBuild}`;
|
|
7724
|
-
}
|
|
8010
|
+
let nextBuild = prependHook(build, SCAN_COMMAND);
|
|
7725
8011
|
if (!nextBuild.includes(MARK_BUILD_COMMAND)) {
|
|
7726
8012
|
nextBuild = `${nextBuild} && ${MARK_BUILD_COMMAND}`;
|
|
7727
8013
|
}
|
|
@@ -7735,7 +8021,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7735
8021
|
scripts.postinstall = postinstall;
|
|
7736
8022
|
scripts.build = nextBuild;
|
|
7737
8023
|
} else {
|
|
7738
|
-
const prebuild =
|
|
8024
|
+
const prebuild = prependHook(scripts.prebuild, SCAN_COMMAND);
|
|
7739
8025
|
const postbuild = appendHook(scripts.postbuild, MARK_BUILD_COMMAND);
|
|
7740
8026
|
if (prebuild === scripts.prebuild && postbuild === scripts.postbuild && postinstall === scripts.postinstall) {
|
|
7741
8027
|
return {
|
|
@@ -7752,7 +8038,7 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7752
8038
|
const indentMatch = raw.match(/^[\t ]+(?=")/m)?.[0];
|
|
7753
8039
|
const indent = indentMatch?.includes(" ") ? " " : indentMatch?.length ?? 2;
|
|
7754
8040
|
const trailingNewline = raw.endsWith("\n") ? "\n" : "";
|
|
7755
|
-
|
|
8041
|
+
writeFileSync10(target, `${JSON.stringify(pkg, null, indent)}${trailingNewline}`, "utf8");
|
|
7756
8042
|
if (build === void 0 || build.trim().length === 0) {
|
|
7757
8043
|
return {
|
|
7758
8044
|
changed: true,
|
|
@@ -7771,41 +8057,6 @@ function wireBuildScripts(cwd, packageManager) {
|
|
|
7771
8057
|
};
|
|
7772
8058
|
}
|
|
7773
8059
|
|
|
7774
|
-
// src/build-hook.ts
|
|
7775
|
-
var INSTALL_AND_BUILD_EVENTS = /* @__PURE__ */ new Set([
|
|
7776
|
-
"preinstall",
|
|
7777
|
-
"install",
|
|
7778
|
-
"postinstall",
|
|
7779
|
-
"prepare",
|
|
7780
|
-
"prebuild",
|
|
7781
|
-
"build",
|
|
7782
|
-
"postbuild"
|
|
7783
|
-
]);
|
|
7784
|
-
function isInstallOrBuildHook(env = process.env) {
|
|
7785
|
-
const event = env.npm_lifecycle_event;
|
|
7786
|
-
return typeof event === "string" && INSTALL_AND_BUILD_EVENTS.has(event);
|
|
7787
|
-
}
|
|
7788
|
-
function undeliveredReportLines(err, config, cwd) {
|
|
7789
|
-
const lines = [`patchstack: manifest not reported \u2014 ${err.message}`];
|
|
7790
|
-
if (err.code === "UNAUTHORIZED") {
|
|
7791
|
-
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
7792
|
-
if (hasCredential) {
|
|
7793
|
-
lines.push(
|
|
7794
|
-
"patchstack: the credential this environment holds was rejected. If `login` rotated it, set the new value as PATCHSTACK_API_KEY here."
|
|
7795
|
-
);
|
|
7796
|
-
} else {
|
|
7797
|
-
lines.push(
|
|
7798
|
-
`patchstack: PATCHSTACK_API_KEY is not set, and neither ${SECRET_CONFIG_FILENAME} nor .patchstackrc.json in ${cwd} holds a credential.`,
|
|
7799
|
-
`patchstack: ${SECRET_CONFIG_FILENAME} is git-ignored, so a clean checkout never has it. Set PATCHSTACK_API_KEY in the platform's environment so builds can report.`
|
|
7800
|
-
);
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
lines.push(
|
|
7804
|
-
"patchstack: continuing the build. Patchstack keeps the last manifest it accepted for this site until a scan that can report."
|
|
7805
|
-
);
|
|
7806
|
-
return lines;
|
|
7807
|
-
}
|
|
7808
|
-
|
|
7809
8060
|
// src/cli.ts
|
|
7810
8061
|
var HELP = `@patchstack/connect \u2014 scan your lockfile and report packages to Patchstack.
|
|
7811
8062
|
|
|
@@ -7833,7 +8084,8 @@ Usage:
|
|
|
7833
8084
|
with how the link was established). Best-effort static
|
|
7834
8085
|
analysis \u2014 reports the DETECTED surface, with
|
|
7835
8086
|
coverage counters. Prints JSON (--out writes a
|
|
7836
|
-
file;
|
|
8087
|
+
file; a pre-bundle --upload also stamps the existing
|
|
8088
|
+
guard rules file; --follow-symlinks leaves the project dir).
|
|
7837
8089
|
Uses the app's own TypeScript
|
|
7838
8090
|
patchstack-connect init <site-uuid> Optional: pre-seed .patchstackrc.json
|
|
7839
8091
|
with an existing site UUID
|
|
@@ -8255,6 +8507,10 @@ async function runScan(args, options = {}) {
|
|
|
8255
8507
|
}
|
|
8256
8508
|
return 0;
|
|
8257
8509
|
}
|
|
8510
|
+
if (isPreBundleBuildHook()) {
|
|
8511
|
+
const cleared = applyBuildStamp(process.cwd(), null);
|
|
8512
|
+
if (cleared.kind === "cleared") console.log(`Removed the previous map binding from ${cleared.file}.`);
|
|
8513
|
+
}
|
|
8258
8514
|
if (config.widget) {
|
|
8259
8515
|
reportSourceMarker(detectStack(payload.packages).framework);
|
|
8260
8516
|
}
|
|
@@ -8553,7 +8809,7 @@ async function runGuide(args) {
|
|
|
8553
8809
|
console.log("\u2014\u2014\u2014\u2014 Full reference guide (ships as AGENT-INSTALL.md) \u2014\u2014\u2014\u2014");
|
|
8554
8810
|
console.log("");
|
|
8555
8811
|
const guidePath = new URL("../AGENT-INSTALL.md", import.meta.url);
|
|
8556
|
-
console.log(
|
|
8812
|
+
console.log(readFileSync16(guidePath, "utf8"));
|
|
8557
8813
|
return 0;
|
|
8558
8814
|
}
|
|
8559
8815
|
async function runSetup(args) {
|
|
@@ -8747,7 +9003,7 @@ async function runMarkBuild(args) {
|
|
|
8747
9003
|
let marked = 0;
|
|
8748
9004
|
let widgetTouched = 0;
|
|
8749
9005
|
for (const file of files) {
|
|
8750
|
-
const before =
|
|
9006
|
+
const before = readFileSync16(file, "utf8");
|
|
8751
9007
|
let after = injectMarker(before, snippet);
|
|
8752
9008
|
if (widgetUuid !== null) {
|
|
8753
9009
|
const ensured = ensureWidgetInHtml(after, widgetUuid);
|
|
@@ -8757,7 +9013,7 @@ async function runMarkBuild(args) {
|
|
|
8757
9013
|
after = ensured.html;
|
|
8758
9014
|
}
|
|
8759
9015
|
if (after !== before) {
|
|
8760
|
-
|
|
9016
|
+
writeFileSync11(file, after);
|
|
8761
9017
|
marked += 1;
|
|
8762
9018
|
}
|
|
8763
9019
|
}
|