border-customs 0.5.1 → 0.7.1

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/dist/index.js CHANGED
@@ -33124,8 +33124,8 @@ var createMaskValue = (value) => {
33124
33124
  };
33125
33125
  var maskWithValues = (str, maskedValues) => {
33126
33126
  let result = str;
33127
- for (const maskValue of maskedValues) {
33128
- result = result.replaceAll(maskValue, "*".repeat(maskValue.length));
33127
+ for (const maskValue2 of maskedValues) {
33128
+ result = result.replaceAll(maskValue2, "*".repeat(maskValue2.length));
33129
33129
  }
33130
33130
  return result;
33131
33131
  };
@@ -37387,6 +37387,158 @@ var creator4 = {
37387
37387
  }
37388
37388
  };
37389
37389
 
37390
+ // src/exfil/rulesPath.ts
37391
+ var HOME_POSIX_RE = /\/home\/([A-Za-z0-9._-]+)/g;
37392
+ var HOME_WINDOWS_RE = /([A-Za-z]:[\\/]+Users[\\/]+)([A-Za-z0-9._-]+)/gi;
37393
+ function matchHomePath(text, _o) {
37394
+ const hits = [];
37395
+ text.split("\n").forEach((body, idx) => {
37396
+ for (const m of body.matchAll(HOME_POSIX_RE)) {
37397
+ hits.push({ rule: "exfil-home-path", matched: m[0] ?? "", line: idx + 1, message: "Home-directory path exposing a local username (privacy leak \u2014 T5 calibration graded this rule MEDIUM per lead ruling)." });
37398
+ }
37399
+ for (const m of body.matchAll(HOME_WINDOWS_RE)) {
37400
+ hits.push({ rule: "exfil-home-path", matched: m[0] ?? "", line: idx + 1, message: "Windows user-profile path exposing a local username (privacy leak \u2014 T5 calibration graded this rule MEDIUM per lead ruling)." });
37401
+ }
37402
+ });
37403
+ return hits;
37404
+ }
37405
+ var PYPIRC_RE = /~\/\.pypirc/g;
37406
+ var SSHPASS_RE = /\bSSHPASS\b/g;
37407
+ var ENVFILE_RE = /[A-Za-z0-9_.~/-]*\.env(?![A-Za-z0-9_./-])/g;
37408
+ function envTokenIsLocation(tok) {
37409
+ const stem = tok.slice(0, -".env".length);
37410
+ if (stem === "") return true;
37411
+ if (/[\/~]/.test(stem)) return true;
37412
+ const parts = stem.split(".");
37413
+ const receiver = parts[parts.length - 1] ?? "";
37414
+ if (receiver === "process" || receiver === "os") return false;
37415
+ return receiver.length >= 3;
37416
+ }
37417
+ function matchCredLocation(text, _o) {
37418
+ const hits = [];
37419
+ const push = (matched, line) => {
37420
+ hits.push({ rule: "exfil-cred-location", matched, line, message: "Reference to a credential-bearing location (env file / pypirc / SSHPASS); the location itself, value not required to fire." });
37421
+ };
37422
+ text.split("\n").forEach((body, idx) => {
37423
+ for (const m of body.matchAll(PYPIRC_RE)) push(m[0] ?? "", idx + 1);
37424
+ for (const m of body.matchAll(SSHPASS_RE)) push(m[0] ?? "", idx + 1);
37425
+ for (const m of body.matchAll(ENVFILE_RE)) {
37426
+ const tok = m[0] ?? "";
37427
+ if ((body[(m.index ?? 0) - 1] ?? "") === "?") continue;
37428
+ if (envTokenIsLocation(tok)) push(tok, idx + 1);
37429
+ }
37430
+ });
37431
+ return hits;
37432
+ }
37433
+ var PROFILE_SIGNALS = [
37434
+ { name: "os+version", re: /\b(?:Ubuntu|Debian|Raspbian|CentOS|Rocky Linux|AlmaLinux|Amazon Linux|Red Hat Enterprise Linux|RHEL|Windows Server|SUSE Linux Enterprise|Fedora)\s+v?[0-9][0-9.]*/i },
37435
+ { name: "no-docker", re: /\bno docker\b|\bdocker\b[^\n]{0,24}?\bnot (?:installed|present|available|running)\b|\bwithout docker\b|\bno container (?:runtime|engine)\b/i },
37436
+ { name: "sudo-pattern", re: /\bpasswordless sudo\b|\bnopasswd\b|\bsudo\b[^\n]{0,24}?\b(?:without|needs?|requires?) (?:a )?password\b|\b(?:needs?|requires?) (?:a )?password\b[^\n]{0,12}?\bsudo\b/i }
37437
+ ];
37438
+ function matchHostProfile(text, _o) {
37439
+ const hits = [];
37440
+ let paraLines = [];
37441
+ let paraStart = 1;
37442
+ const flush = () => {
37443
+ if (paraLines.length === 0) return;
37444
+ const para = paraLines.join("\n");
37445
+ const found = PROFILE_SIGNALS.filter((s) => s.re.test(para)).map((s) => s.name);
37446
+ if (found.length >= 2) {
37447
+ hits.push({ rule: "exfil-host-profile", matched: found.join("+"), line: paraStart, message: `Host configuration fingerprint: ${String(found.length)} profile signals (os+version / no-docker / sudo-pattern) clustered in one paragraph.` });
37448
+ }
37449
+ paraLines = [];
37450
+ };
37451
+ text.split("\n").forEach((body, idx) => {
37452
+ if (body.trim() === "") {
37453
+ flush();
37454
+ return;
37455
+ }
37456
+ if (paraLines.length === 0) paraStart = idx + 1;
37457
+ paraLines.push(body);
37458
+ });
37459
+ flush();
37460
+ return hits;
37461
+ }
37462
+
37463
+ // src/exfil/rules.ts
37464
+ var OCTET = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])";
37465
+ var IPV4_SRC = `(?<![0-9A-Za-z.\\-])${OCTET}\\.${OCTET}\\.${OCTET}\\.${OCTET}(?![0-9A-Za-z.\\-])`;
37466
+ var DEFAULT_EXEMPT_BANDS = ["127", "192.0.2", "198.51.100", "203.0.113", "10.200"];
37467
+ function bandOctets(band) {
37468
+ return band.split(".").map(Number);
37469
+ }
37470
+ function inAnyBand(oct, bands) {
37471
+ return bands.some((b) => {
37472
+ const parts = bandOctets(b);
37473
+ return parts.every((v, i) => oct[i] === v);
37474
+ });
37475
+ }
37476
+ function inRfc1918(o) {
37477
+ const [a = 0, b = 0] = o;
37478
+ if (a === 10) return true;
37479
+ if (a === 172 && b >= 16 && b <= 31) return true;
37480
+ return a === 192 && b === 168;
37481
+ }
37482
+ function octetsOf(ip) {
37483
+ return ip.split(".").map(Number);
37484
+ }
37485
+ function isIpLiteral(host) {
37486
+ return /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(host);
37487
+ }
37488
+ var OCTET_GRP = `(?:${OCTET})`;
37489
+ var RFC1918_FULL = `10\\.${OCTET_GRP}\\.${OCTET_GRP}\\.${OCTET_GRP}|172\\.(?:1[6-9]|2[0-9]|3[01])\\.${OCTET_GRP}\\.${OCTET_GRP}|192\\.168\\.${OCTET_GRP}\\.${OCTET_GRP}`;
37490
+ var SUFFIX_HOST = `[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\\.(?:internal|lan|local|corp|intra)`;
37491
+ var BAND_NEGATIVE = `(?!127\\.|10\\.200\\.|192\\.0\\.2\\.|198\\.51\\.100\\.|203\\.0\\.113\\.)`;
37492
+ var EXFIL_TWIN_PATTERNS = [
37493
+ { name: "exfil-rfc1918", source: `(?<![0-9A-Za-z.\\-])${BAND_NEGATIVE}(?:${RFC1918_FULL})(?![0-9A-Za-z.\\-])` },
37494
+ { name: "exfil-ssh-target", source: `(?<![A-Za-z0-9._+%-])[A-Za-z0-9._+%-]+@${BAND_NEGATIVE}(?:${RFC1918_FULL}|${SUFFIX_HOST})(?![A-Za-z0-9._-])` }
37495
+ ];
37496
+ function matchRfc1918(text, o) {
37497
+ const re = new RegExp(IPV4_SRC, "g");
37498
+ const bands = o.exemptBands ?? DEFAULT_EXEMPT_BANDS;
37499
+ const hits = [];
37500
+ text.split("\n").forEach((body, idx) => {
37501
+ for (const m of body.matchAll(re)) {
37502
+ const ip = m[0] ?? "";
37503
+ const oct = octetsOf(ip);
37504
+ if (!inRfc1918(oct)) continue;
37505
+ if (o.facet !== "message" && inAnyBand(oct, bands)) continue;
37506
+ hits.push({ rule: "exfil-rfc1918", matched: ip, line: idx + 1, message: "RFC1918 private-network address literal on a surface about to become public." });
37507
+ }
37508
+ });
37509
+ return hits;
37510
+ }
37511
+ var PRIVATE_SUFFIXES = [".internal", ".lan", ".local", ".corp", ".intra"];
37512
+ var RFC2606_SUFFIXES = [".example", ".test", ".invalid", ".localhost"];
37513
+ var SSH_TOKEN_RE = /(?<![A-Za-z0-9._+%-])[A-Za-z0-9._+%-]+@([A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?)/g;
37514
+ function hasDotSuffix(host, suffixes) {
37515
+ return suffixes.some((s) => host.length > s.length && host.endsWith(s));
37516
+ }
37517
+ function matchSshTarget(text, o) {
37518
+ const hosts = (o.hosts ?? []).map((h) => h.trim().toLowerCase()).filter((h) => h !== "");
37519
+ const bands = o.exemptBands ?? DEFAULT_EXEMPT_BANDS;
37520
+ const hits = [];
37521
+ text.split("\n").forEach((body, idx) => {
37522
+ for (const m of body.matchAll(SSH_TOKEN_RE)) {
37523
+ const host = (m[1] ?? "").toLowerCase();
37524
+ if (host === "") continue;
37525
+ const isIp = isIpLiteral(host);
37526
+ let fired = false;
37527
+ if (isIp ? inRfc1918(octetsOf(host)) && !(o.facet !== "message" && inAnyBand(octetsOf(host), bands)) : false) {
37528
+ fired = true;
37529
+ } else if (!isIp && hosts.includes(host)) {
37530
+ fired = true;
37531
+ } else if (!isIp && !hasDotSuffix(host, RFC2606_SUFFIXES) && hasDotSuffix(host, PRIVATE_SUFFIXES)) {
37532
+ fired = true;
37533
+ }
37534
+ if (fired) {
37535
+ hits.push({ rule: "exfil-ssh-target", matched: `${m[0]}`, line: idx + 1, message: "SSH-style user@host naming an internal-only target (RFC1918 literal, configured host, or private suffix)." });
37536
+ }
37537
+ }
37538
+ });
37539
+ return hits;
37540
+ }
37541
+
37390
37542
  // src/findings.ts
37391
37543
  var SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"];
37392
37544
  function isBlocking(severity) {
@@ -37491,6 +37643,7 @@ function generatePatterns(rules2) {
37491
37643
  for (const ip of rules2?.ips ?? []) optionPatterns.push(makePattern(`internal-ip:${ip}`, ip, true, compiled));
37492
37644
  for (const pp of rules2?.pathPatterns ?? []) optionPatterns.push(makePattern(`path-pattern:${pp}`, pp, true, compiled));
37493
37645
  for (const dflt of DEFAULT_PATH_PATTERNS) optionPatterns.push(makePattern(`path-pattern:${dflt}`, dflt, false, compiled));
37646
+ for (const twin of EXFIL_TWIN_PATTERNS) optionPatterns.push(makePattern(twin.name, twin.source, false, compiled));
37494
37647
  return { optionPatterns, compiled };
37495
37648
  }
37496
37649
  var AWS_OVERRIDE = [{ id: AWS_RULE_ID, options: { enableIDScanRule: true } }];
@@ -38399,6 +38552,7 @@ var HEX64 = /^[0-9a-f]{64}$/;
38399
38552
  var HEX40 = /^[0-9a-f]{40}$/;
38400
38553
  var HEX8 = /^[0-9a-f]{8}$/;
38401
38554
  var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
38555
+ var LANDING_VERDICTS = ["clean", "blocked"];
38402
38556
  function ledgerPath(repoDir) {
38403
38557
  return join8(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
38404
38558
  }
@@ -38439,6 +38593,26 @@ function buildRoundtripRecord(i) {
38439
38593
  function bad(msg) {
38440
38594
  throw new Error(msg);
38441
38595
  }
38596
+ function buildLandingRecord(i) {
38597
+ if (!HEX64.test(i.key)) bad("key is not hex-shaped");
38598
+ if (!HEX64.test(i.rulesHash)) bad("rulesHash is not hex-shaped");
38599
+ if (!HEX40.test(i.remoteSha)) bad("remoteSha is not hex-shaped");
38600
+ if (i.target === "") bad("target must be non-empty");
38601
+ if (i.ref === "") bad("ref must be non-empty");
38602
+ if (!LANDING_VERDICTS.includes(i.verdict)) bad(`verdict must be one of ${LANDING_VERDICTS.join("|")}, got '${i.verdict}'`);
38603
+ if (!Number.isInteger(i.blocking) || i.blocking < 0) bad("blocking must be a non-negative integer");
38604
+ return {
38605
+ t: "landing",
38606
+ key: i.key,
38607
+ target: i.target,
38608
+ ref: i.ref,
38609
+ remoteSha: i.remoteSha,
38610
+ verdict: i.verdict,
38611
+ blocking: i.blocking,
38612
+ rulesHash: i.rulesHash,
38613
+ ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
38614
+ };
38615
+ }
38442
38616
  function needString(o, field) {
38443
38617
  const v = o[field];
38444
38618
  if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
@@ -38540,6 +38714,23 @@ function parseLedgerRecord(value) {
38540
38714
  ts: needString(o, "ts")
38541
38715
  };
38542
38716
  }
38717
+ if (t === "landing") {
38718
+ const verdict = needString(o, "verdict");
38719
+ if (!LANDING_VERDICTS.includes(verdict)) bad(`verdict must be one of ${LANDING_VERDICTS.join("|")}, got '${verdict}'`);
38720
+ const blocking = o.blocking;
38721
+ if (typeof blocking !== "number" || !Number.isInteger(blocking) || blocking < 0) bad("blocking must be a non-negative integer");
38722
+ return {
38723
+ t: "landing",
38724
+ key: needHex(o, "key", HEX64),
38725
+ target: needString(o, "target"),
38726
+ ref: needString(o, "ref"),
38727
+ remoteSha: needHex(o, "remoteSha", HEX40),
38728
+ verdict,
38729
+ blocking,
38730
+ rulesHash: needHex(o, "rulesHash", HEX64),
38731
+ ts: needString(o, "ts")
38732
+ };
38733
+ }
38543
38734
  return bad(`unknown record type ${JSON.stringify(t)}`);
38544
38735
  }
38545
38736
  function codeOf2(err) {
@@ -39412,6 +39603,23 @@ function resolveReleaseFingerprintFiles(env2 = process.env) {
39412
39603
  return resolveAsset(import.meta.url, ["release-src", base]);
39413
39604
  });
39414
39605
  }
39606
+ var EXFIL_FINGERPRINT_SOURCES = [
39607
+ { dir: "exfil", base: "severity.ts" },
39608
+ { dir: "exfil", base: "rules.ts" },
39609
+ { dir: "exfil", base: "rulesPath.ts" },
39610
+ { dir: "exfil", base: "scan.ts" }
39611
+ ];
39612
+ var EXFIL_FINGERPRINT_BASENAMES = EXFIL_FINGERPRINT_SOURCES.map((s) => s.base);
39613
+ function resolveExfilFingerprintFiles(env2 = process.env) {
39614
+ const here = dirname6(fileURLToPath4(import.meta.url));
39615
+ const seam = env2.BORDER_EXFIL_SRC_DIR;
39616
+ return EXFIL_FINGERPRINT_SOURCES.map(({ dir, base }) => {
39617
+ if (seam !== void 0) return join11(seam, base);
39618
+ const live = join11(here, "..", dir, base);
39619
+ if (existsSync7(live)) return live;
39620
+ return resolveAsset(import.meta.url, ["exfil-src", base]);
39621
+ });
39622
+ }
39415
39623
  function resolvePromptTemplatePath(env2 = process.env) {
39416
39624
  return env2.BORDER_PROMPT_TEMPLATE_PATH ?? resolveAsset(import.meta.url, ["prompts", "llm-review.md"]);
39417
39625
  }
@@ -39433,7 +39641,8 @@ async function computeCheckRulesHash(input2) {
39433
39641
  bundledRulePaths: [
39434
39642
  GITLEAKS_VENDORED_CONFIG,
39435
39643
  ...resolveResidueFingerprintFiles(input2.env ?? process.env),
39436
- ...resolveReleaseFingerprintFiles(input2.env ?? process.env)
39644
+ ...resolveReleaseFingerprintFiles(input2.env ?? process.env),
39645
+ ...resolveExfilFingerprintFiles(input2.env ?? process.env)
39437
39646
  ],
39438
39647
  configDigest: input2.configDigest,
39439
39648
  engineVersions: input2.engineVersions,
@@ -41389,7 +41598,7 @@ function npmNameClaimed(o) {
41389
41598
  return true;
41390
41599
  }
41391
41600
  function npmViewJson(args, registry2, timeoutMs, env2) {
41392
- return new Promise((resolve16, reject) => {
41601
+ return new Promise((resolve20, reject) => {
41393
41602
  const child = spawn2("npm", ["view", ...args, "--registry", registry2, "--fetch-retries=0", "--json"], {
41394
41603
  stdio: ["ignore", "pipe", "pipe"],
41395
41604
  ...env2 === void 0 ? {} : { env: env2 }
@@ -41408,7 +41617,7 @@ function npmViewJson(args, registry2, timeoutMs, env2) {
41408
41617
  });
41409
41618
  child.on("close", (status) => {
41410
41619
  clearTimeout(timer);
41411
- resolve16({ status: status ?? -1, stdout, stderr });
41620
+ resolve20({ status: status ?? -1, stdout, stderr });
41412
41621
  });
41413
41622
  });
41414
41623
  }
@@ -42546,6 +42755,22 @@ function headFile4(toplevel, rel) {
42546
42755
  }
42547
42756
  return r.stdout;
42548
42757
  }
42758
+ function remoteTrackingDrift(cwd) {
42759
+ const configured = parseRemoteV(runGit3(cwd, ["remote", "-v"]).stdout).map((r) => r.name).filter((n) => n !== void 0).sort();
42760
+ const refs = runGit3(cwd, ["for-each-ref", "--format=%(refname:lstrip=2)", "refs/remotes"]);
42761
+ const refNames = /* @__PURE__ */ new Set();
42762
+ if (refs.ok) {
42763
+ for (const line of refs.stdout.split("\n")) {
42764
+ const first = line.trim().split("/")[0];
42765
+ if (first !== void 0 && first !== "") {
42766
+ refNames.add(first);
42767
+ }
42768
+ }
42769
+ }
42770
+ const configuredSet = new Set(configured);
42771
+ const orphans = [...refNames].filter((n) => !configuredSet.has(n)).sort();
42772
+ return { configured, orphans };
42773
+ }
42549
42774
  function hasTargets(cfg) {
42550
42775
  return cfg.targets.git.remotes.length > 0 || publishChannels().some((c) => c.configured(cfg));
42551
42776
  }
@@ -42641,7 +42866,7 @@ function exposureSet(cfg, options = {}) {
42641
42866
  }
42642
42867
 
42643
42868
  // src/check.ts
42644
- import { join as join22 } from "node:path";
42869
+ import { join as join23 } from "node:path";
42645
42870
 
42646
42871
  // src/rules/aiArtifacts.ts
42647
42872
  import { spawnSync as spawnSync12 } from "node:child_process";
@@ -42884,6 +43109,301 @@ function applyAllowList(findings, allow, repoDir) {
42884
43109
  return { kept, allowHits: [...hits.values()] };
42885
43110
  }
42886
43111
 
43112
+ // src/check/exfilTreeScan.ts
43113
+ import { readFileSync as readFileSync14, statSync as statSync2 } from "node:fs";
43114
+ import { join as join21, resolve as resolve12 } from "node:path";
43115
+
43116
+ // src/exfil/scan.ts
43117
+ import { createHash as createHash13 } from "node:crypto";
43118
+
43119
+ // src/exfil/severity.ts
43120
+ var EXFIL_RULE_IDS = [
43121
+ "exfil-rfc1918",
43122
+ "exfil-ssh-target",
43123
+ "exfil-home-path",
43124
+ "exfil-cred-location",
43125
+ "exfil-host-profile"
43126
+ ];
43127
+ var EXFIL_MESSAGE_ID_SUFFIX = ":message";
43128
+ var EXFIL_TABLE_SEVERITY = {
43129
+ "exfil-rfc1918": "HIGH",
43130
+ "exfil-ssh-target": "HIGH",
43131
+ "exfil-home-path": "MEDIUM",
43132
+ "exfil-cred-location": "MEDIUM",
43133
+ "exfil-host-profile": "MEDIUM"
43134
+ };
43135
+ var EXFIL_HIGH_FAMILY = ["exfil-rfc1918", "exfil-ssh-target"];
43136
+ var EXFIL_SECRET_VALUE_RULES = EXFIL_HIGH_FAMILY;
43137
+ var TWIN_HIGH = {
43138
+ "exfil-rfc1918": "CRITICAL",
43139
+ "exfil-ssh-target": "CRITICAL",
43140
+ "exfil-home-path": null,
43141
+ "exfil-cred-location": null,
43142
+ "exfil-host-profile": null
43143
+ };
43144
+ var NULL_ROW = {
43145
+ "exfil-rfc1918": null,
43146
+ "exfil-ssh-target": null,
43147
+ "exfil-home-path": null,
43148
+ "exfil-cred-location": null,
43149
+ "exfil-host-profile": null
43150
+ };
43151
+ var EXFIL_MATRIX = {
43152
+ tree: {
43153
+ // native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
43154
+ native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
43155
+ // gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
43156
+ "gitleaks-twin": TWIN_HIGH,
43157
+ // secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
43158
+ "secretlint-twin": TWIN_HIGH
43159
+ },
43160
+ message: {
43161
+ // native-exclusive `:message` family at table severity.
43162
+ native: { ...EXFIL_TABLE_SEVERITY },
43163
+ "gitleaks-twin": NULL_ROW,
43164
+ "secretlint-twin": NULL_ROW
43165
+ },
43166
+ tagNote: {
43167
+ // engine既有腿 owns this facet as `tag-message-secret` (a non-exfil id);
43168
+ // NO exfil-* emission is legal on tag notes through ANY channel.
43169
+ native: NULL_ROW,
43170
+ "gitleaks-twin": NULL_ROW,
43171
+ "secretlint-twin": NULL_ROW
43172
+ }
43173
+ };
43174
+ function observedSeverity(rule, facet, channel) {
43175
+ return EXFIL_MATRIX[facet][channel][rule];
43176
+ }
43177
+ function emitRulesFor(facet, channel) {
43178
+ return EXFIL_RULE_IDS.filter((rule) => observedSeverity(rule, facet, channel) !== null);
43179
+ }
43180
+ function exfilFindingId(rule, facet) {
43181
+ switch (facet) {
43182
+ case "message":
43183
+ return `${rule}${EXFIL_MESSAGE_ID_SUFFIX}`;
43184
+ case "tree":
43185
+ case "tagNote":
43186
+ return rule;
43187
+ }
43188
+ }
43189
+
43190
+ // src/exfil/scan.ts
43191
+ var EXFIL_ENGINE = "border-exfil";
43192
+ var FULL_MASK2 = "\u25AE\u25AE\u25AE\u25AE";
43193
+ var ELLIPSIS2 = "\u2026";
43194
+ function maskValue(value) {
43195
+ const valueDigest = createHash13("sha256").update(value, "utf8").digest("hex");
43196
+ const points = [...value];
43197
+ const snippet = points.length <= 12 ? FULL_MASK2 : `${points.slice(0, 4).join("")}${ELLIPSIS2}${points.slice(-4).join("")}`;
43198
+ return { valueDigest, snippet };
43199
+ }
43200
+ var PREDICATES = {
43201
+ "exfil-rfc1918": matchRfc1918,
43202
+ "exfil-ssh-target": matchSshTarget,
43203
+ "exfil-home-path": matchHomePath,
43204
+ "exfil-cred-location": matchCredLocation,
43205
+ "exfil-host-profile": matchHostProfile
43206
+ };
43207
+ function scanFacet(input2, facet) {
43208
+ const channel = "native";
43209
+ const options = { facet, ...input2.hosts !== void 0 ? { hosts: input2.hosts } : {} };
43210
+ const findings = [];
43211
+ for (const rule of emitRulesFor(facet, channel)) {
43212
+ const severity = observedSeverity(rule, facet, channel);
43213
+ if (severity === null) continue;
43214
+ for (const hit of PREDICATES[rule](input2.text, options)) {
43215
+ const { valueDigest, snippet } = maskValue(hit.matched);
43216
+ if (EXFIL_SECRET_VALUE_RULES.includes(rule)) input2.onMatch?.(hit.matched, valueDigest);
43217
+ findings.push({
43218
+ id: exfilFindingId(rule, facet),
43219
+ rule,
43220
+ severity,
43221
+ facet,
43222
+ channel,
43223
+ engine: EXFIL_ENGINE,
43224
+ message: hit.message,
43225
+ source: input2.source,
43226
+ line: hit.line,
43227
+ valueDigest,
43228
+ snippet
43229
+ });
43230
+ }
43231
+ }
43232
+ return findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id) || a.valueDigest.localeCompare(b.valueDigest));
43233
+ }
43234
+ function scanTreeText(input2) {
43235
+ return scanFacet(input2, "tree");
43236
+ }
43237
+ function scanMessageText(input2) {
43238
+ return scanFacet(input2, "message");
43239
+ }
43240
+
43241
+ // src/check/exfilTreeScan.ts
43242
+ var BORDER_SEGMENT = ".border";
43243
+ var MAX_SCAN_BYTES = 2 * 1024 * 1024;
43244
+ function toFinding3(f, target) {
43245
+ return {
43246
+ rule: f.id,
43247
+ severity: f.severity,
43248
+ target,
43249
+ path: f.source,
43250
+ line: f.line,
43251
+ engine: f.engine,
43252
+ message: f.message,
43253
+ valueDigest: f.valueDigest,
43254
+ snippet: f.snippet
43255
+ };
43256
+ }
43257
+ function scanExfilTree(o) {
43258
+ const repoDir = resolve12(o.repoDir);
43259
+ const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
43260
+ const files = runGitChecked(repoDir, ["ls-files", "-z"], gitOpts).split("\0").filter((rel) => rel !== "" && !rel.split("/").includes(BORDER_SEGMENT));
43261
+ const findings = [];
43262
+ for (const rel of files) {
43263
+ const abs = join21(repoDir, rel);
43264
+ let size = 0;
43265
+ try {
43266
+ size = statSync2(abs).size;
43267
+ } catch {
43268
+ continue;
43269
+ }
43270
+ if (size > MAX_SCAN_BYTES) continue;
43271
+ let text = "";
43272
+ try {
43273
+ text = readFileSync14(abs, "utf8");
43274
+ } catch {
43275
+ continue;
43276
+ }
43277
+ if (text.includes("\0")) continue;
43278
+ for (const f of scanTreeText({
43279
+ text,
43280
+ source: rel,
43281
+ ...o.hosts !== void 0 ? { hosts: o.hosts } : {},
43282
+ ...o.sanitizer !== void 0 ? { onMatch: (raw, digest2) => o.sanitizer?.register(digest2, raw) } : {}
43283
+ })) {
43284
+ findings.push(toFinding3(f, o.target ?? "tree"));
43285
+ }
43286
+ }
43287
+ return findings;
43288
+ }
43289
+
43290
+ // src/check/messageScan.ts
43291
+ import { spawnSync as spawnSync13 } from "node:child_process";
43292
+ import { dirname as dirname10, resolve as resolve13 } from "node:path";
43293
+ var MESSAGE_REV_BATCH = 200;
43294
+ var MESSAGE_GIT_TIMEOUT_MS = 55e3;
43295
+ function defaultRunner(o) {
43296
+ const repoDir = resolve13(o.repoDir);
43297
+ const env2 = {};
43298
+ for (const [name, value] of Object.entries(o.env ?? process.env)) {
43299
+ if (!name.startsWith("GIT_")) env2[name] = value;
43300
+ }
43301
+ env2.GIT_CEILING_DIRECTORIES = dirname10(repoDir);
43302
+ env2.GIT_OPTIONAL_LOCKS = "0";
43303
+ return (args) => {
43304
+ const r = spawnSync13("git", ["-C", repoDir, ...args], {
43305
+ encoding: "utf8",
43306
+ env: env2,
43307
+ timeout: o.timeoutMs ?? MESSAGE_GIT_TIMEOUT_MS,
43308
+ maxBuffer: o.maxBufferBytes ?? 32 * 1024 * 1024
43309
+ });
43310
+ if (r.error !== void 0) {
43311
+ throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir}: ${r.error.message}`);
43312
+ }
43313
+ if (r.status === null) {
43314
+ throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir} killed by ${String(r.signal)} (timeout ${String(o.timeoutMs ?? MESSAGE_GIT_TIMEOUT_MS)}ms) \u2014 fail closed, never a silent clean`);
43315
+ }
43316
+ if (r.status !== 0) {
43317
+ throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir} exited ${String(r.status)}: ${(r.stderr ?? "").trim().slice(0, 200)}`);
43318
+ }
43319
+ return r.stdout ?? "";
43320
+ };
43321
+ }
43322
+ function revExists2(run2, rev) {
43323
+ try {
43324
+ run2(["rev-parse", "--verify", "--quiet", rev]);
43325
+ return true;
43326
+ } catch {
43327
+ return false;
43328
+ }
43329
+ }
43330
+ function trackingEndpoint2(run2, remoteName, ref) {
43331
+ const candidates = [`refs/remotes/${remoteName}/${ref}`];
43332
+ const short = ref.replace(/^refs\/(?:heads|tags)\//, "");
43333
+ if (short !== ref) candidates.push(`refs/remotes/${remoteName}/${short}`);
43334
+ return candidates.find((c) => revExists2(run2, c)) ?? null;
43335
+ }
43336
+ function willPublishShas(run2, refSet, remotes) {
43337
+ const out = [];
43338
+ const seen = /* @__PURE__ */ new Set();
43339
+ const collect = (revs) => {
43340
+ for (const sha of run2(["rev-list", ...revs]).split("\n")) {
43341
+ if (sha !== "" && !seen.has(sha)) {
43342
+ seen.add(sha);
43343
+ out.push(sha);
43344
+ }
43345
+ }
43346
+ };
43347
+ for (const ref of refSet) {
43348
+ if (!revExists2(run2, ref)) continue;
43349
+ const ranges = [];
43350
+ for (const remote of remotes) {
43351
+ const name = remote.name;
43352
+ if (name === void 0) continue;
43353
+ const endpoint = trackingEndpoint2(run2, name, ref);
43354
+ if (endpoint !== null) ranges.push(`${endpoint}..${ref}`);
43355
+ }
43356
+ if (ranges.length === 0) collect([ref]);
43357
+ else collect(ranges);
43358
+ }
43359
+ return out;
43360
+ }
43361
+ var MESSAGE_LOG_FORMAT = "--format=%x1e%H%x00%B";
43362
+ function scanOneRecord(record2, o, findings) {
43363
+ const trimmed = record2.replace(/^\n/, "");
43364
+ if (trimmed === "") return;
43365
+ const nul = trimmed.indexOf("\0");
43366
+ if (nul === -1) {
43367
+ throw new ConfigError("git-failed", "git log message record malformed (no NUL separator) \u2014 fail closed");
43368
+ }
43369
+ const sha = trimmed.slice(0, nul);
43370
+ const body = trimmed.slice(nul + 1).replace(/\n$/, "");
43371
+ for (const f of scanMessageText({
43372
+ text: body,
43373
+ source: sha,
43374
+ ...o.hosts !== void 0 ? { hosts: o.hosts } : {},
43375
+ ...o.sanitizer !== void 0 ? { onMatch: (raw, digest2) => o.sanitizer?.register(digest2, raw) } : {}
43376
+ })) {
43377
+ findings.push(toMessageFinding(f, sha));
43378
+ }
43379
+ }
43380
+ function toMessageFinding(f, sha) {
43381
+ return {
43382
+ rule: f.id,
43383
+ severity: f.severity,
43384
+ target: "git",
43385
+ path: f.source,
43386
+ // = sha: toRepoRelative passes it through unchanged (exclusions.ts:20-25) ⇒ per-commit allow rows
43387
+ commit: sha,
43388
+ line: f.line,
43389
+ engine: f.engine,
43390
+ message: f.message,
43391
+ valueDigest: f.valueDigest,
43392
+ snippet: f.snippet
43393
+ };
43394
+ }
43395
+ function scanCommitMessages(o) {
43396
+ const run2 = o.runGit ?? defaultRunner(o);
43397
+ const shas = willPublishShas(run2, o.refSet, o.remotes);
43398
+ const findings = [];
43399
+ for (let i = 0; i < shas.length; i += MESSAGE_REV_BATCH) {
43400
+ const batch = shas.slice(i, i + MESSAGE_REV_BATCH);
43401
+ const out = run2(["log", "--no-walk", MESSAGE_LOG_FORMAT, ...batch]);
43402
+ for (const record2 of out.split("")) scanOneRecord(record2, o, findings);
43403
+ }
43404
+ return findings;
43405
+ }
43406
+
42887
43407
  // src/check/proofValve.ts
42888
43408
  var ROUNDTRIP_PROOF_MISSING_RULE = "roundtrip-proof-missing";
42889
43409
  var ROUNDTRIP_PROOF_STALE_RULE = "roundtrip-proof-stale";
@@ -42923,10 +43443,10 @@ function proofFindings(o) {
42923
43443
  }
42924
43444
 
42925
43445
  // src/check/tagScan.ts
42926
- import { spawnSync as spawnSync13 } from "node:child_process";
43446
+ import { spawnSync as spawnSync14 } from "node:child_process";
42927
43447
  import { mkdtempSync as mkdtempSync3, rmSync as rmSync10, writeFileSync as writeFileSync4 } from "node:fs";
42928
43448
  import { tmpdir as tmpdir2 } from "node:os";
42929
- import { join as join21, resolve as resolve12 } from "node:path";
43449
+ import { join as join22, resolve as resolve14 } from "node:path";
42930
43450
  var TAG_MESSAGE_RULE = "tag-message-secret";
42931
43451
  function isEnoent2(error61) {
42932
43452
  return typeof error61 === "object" && error61 !== null && error61.code === "ENOENT";
@@ -42946,7 +43466,7 @@ function stdinFindings(text, ignorePath, o) {
42946
43466
  ];
42947
43467
  let lastError;
42948
43468
  for (const bin of binaryCandidates("gitleaks", o)) {
42949
- const r = spawnSync13(bin, args, {
43469
+ const r = spawnSync14(bin, args, {
42950
43470
  encoding: "utf8",
42951
43471
  input: text,
42952
43472
  timeout: o.timeoutMs ?? 3e5,
@@ -43013,12 +43533,12 @@ function dereferenceTagCommit(repoDir, ref, o) {
43013
43533
  }
43014
43534
  }
43015
43535
  function scanTagMessages(o) {
43016
- const repoDir = resolve12(o.repoDir);
43536
+ const repoDir = resolve14(o.repoDir);
43017
43537
  const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
43018
43538
  const refs = runGitChecked(repoDir, ["for-each-ref", "--format=%(refname)", "refs/tags"], gitOpts).split("\n").filter((ref) => ref !== "");
43019
43539
  if (refs.length === 0) return [];
43020
- const ignoreDir = mkdtempSync3(join21(tmpdir2(), "border-gitleaks-ignore-"));
43021
- const ignorePath = join21(ignoreDir, ".gitleaksignore");
43540
+ const ignoreDir = mkdtempSync3(join22(tmpdir2(), "border-gitleaks-ignore-"));
43541
+ const ignorePath = join22(ignoreDir, ".gitleaksignore");
43022
43542
  writeFileSync4(ignorePath, "");
43023
43543
  try {
43024
43544
  const findings = [];
@@ -43094,7 +43614,7 @@ async function runPipeline(o, ctx, lockWarning) {
43094
43614
  if (!broken.has("gitleaks")) {
43095
43615
  const eng = legOptions(o, sanitizer);
43096
43616
  findings.push(...filterBorderStateFindings(scanGitHistory({ repoDir, refRange: historyRefRange(ctx), target: "git", ...eng }), repoDir));
43097
- findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join22(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
43617
+ findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join23(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
43098
43618
  findings.push(...filterBorderStateFindings(scanTagMessages({ repoDir, target: "git", ...eng }), repoDir));
43099
43619
  }
43100
43620
  if (!broken.has("secretlint")) {
@@ -43105,6 +43625,17 @@ async function runPipeline(o, ctx, lockWarning) {
43105
43625
  }
43106
43626
  findings.push(...scanAiArtifacts({ repoDir, refSet: [...ctx.refSet], cfg: o.cfg.rules, ...envOpt }));
43107
43627
  findings.push(...scanIdentity({ repoDir, refSet: [...ctx.refSet], cfg: o.cfg, ...envOpt }));
43628
+ findings.push(...filterBorderStateFindings(scanExfilTree({ repoDir, hosts: o.cfg.rules.hosts, ...legOptions(o, sanitizer) }), repoDir));
43629
+ findings.push(...filterBorderStateFindings(
43630
+ scanCommitMessages({
43631
+ repoDir,
43632
+ refSet: ctx.refSet.length > 0 ? [...ctx.refSet] : [ctx.headSha],
43633
+ remotes: o.cfg.targets.git.remotes,
43634
+ hosts: o.cfg.rules.hosts,
43635
+ ...legOptions(o, sanitizer)
43636
+ }),
43637
+ repoDir
43638
+ ));
43108
43639
  const ledgerArtifacts = [];
43109
43640
  for (const channel of publishChannels()) {
43110
43641
  if (!o.effectiveTargets.includes(channel.id) || !channel.configured(o.cfg)) continue;
@@ -43279,21 +43810,271 @@ var NO_TARGETS_PROBE_CFG = {
43279
43810
  engines: { require: [], trufflehog: false }
43280
43811
  };
43281
43812
 
43813
+ // src/commands/exfil.ts
43814
+ import { createHash as createHash14 } from "node:crypto";
43815
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
43816
+ import { tmpdir as tmpdir3 } from "node:os";
43817
+ import { dirname as dirname12, join as join24 } from "node:path";
43818
+
43819
+ // src/commands/exfilGit.ts
43820
+ import { spawnSync as spawnSync15 } from "node:child_process";
43821
+ import { dirname as dirname11, resolve as resolve15 } from "node:path";
43822
+ var GIT_TIMEOUT_MS = 55e3;
43823
+ var MAX_BUFFER_BYTES = 32 * 1024 * 1024;
43824
+ var MAX_BLOB_BYTES = 2 * 1024 * 1024;
43825
+ var BATCH_SHAS = 500;
43826
+ function makeGit(repoDir, env2) {
43827
+ const clean = {};
43828
+ for (const [name, value] of Object.entries(env2)) {
43829
+ if (!name.startsWith("GIT_")) clean[name] = value;
43830
+ }
43831
+ clean.GIT_CEILING_DIRECTORIES = dirname11(resolve15(repoDir));
43832
+ clean.GIT_OPTIONAL_LOCKS = "0";
43833
+ clean.GIT_TERMINAL_PROMPT = "0";
43834
+ const spawnWith = (args, input2) => {
43835
+ const r = spawnSync15("git", ["-C", resolve15(repoDir), ...args], {
43836
+ timeout: GIT_TIMEOUT_MS,
43837
+ maxBuffer: MAX_BUFFER_BYTES,
43838
+ env: clean,
43839
+ ...input2 !== void 0 ? { input: input2 } : {}
43840
+ });
43841
+ if (r.error !== void 0) return { stdout: Buffer.alloc(0), fail: `git ${args.slice(0, 2).join(" ")}: ${r.error.message}` };
43842
+ if (r.status !== 0) {
43843
+ const why = r.status === null ? `killed by ${String(r.signal)} (timeout ${String(GIT_TIMEOUT_MS)}ms)` : `exited ${String(r.status)}`;
43844
+ return { stdout: Buffer.alloc(0), fail: `git ${args.slice(0, 3).join(" ")} ${why}: ${(r.stderr?.toString("utf8") ?? "").trim().slice(0, 200)}` };
43845
+ }
43846
+ return { stdout: r.stdout ?? Buffer.alloc(0), fail: null };
43847
+ };
43848
+ const runBuffer = (args, input2) => {
43849
+ const r = spawnWith(args, input2);
43850
+ if (r.fail !== null) throw new ConfigError("git-failed", r.fail);
43851
+ return r.stdout;
43852
+ };
43853
+ return { run: (args) => runBuffer(args).toString("utf8"), runBuffer };
43854
+ }
43855
+ function parseLsTree(line) {
43856
+ const m = /^\d{6} (\w+) ([0-9a-f]{40})\s+(\d+)\t(.+)$/.exec(line);
43857
+ if (m === null || m[1] !== "blob") return null;
43858
+ return { sha: m[2] ?? "", path: m[4] ?? "", size: Number(m[3]) };
43859
+ }
43860
+ function tipTree(git, rev) {
43861
+ return git.run(["-c", "core.quotePath=false", "ls-tree", "-r", "--long", rev]).split("\n").map(parseLsTree).filter((b) => b !== null);
43862
+ }
43863
+ function historyBlobs(git, rev) {
43864
+ const paths = /* @__PURE__ */ new Map();
43865
+ for (const line of git.run(["rev-list", "--objects", rev]).split("\n")) {
43866
+ if (line === "") continue;
43867
+ const sp = line.indexOf(" ");
43868
+ if (sp <= 0) continue;
43869
+ if (!paths.has(line.slice(0, sp))) paths.set(line.slice(0, sp), line.slice(sp + 1));
43870
+ }
43871
+ const shas = [...paths.keys()];
43872
+ const out = [];
43873
+ for (let i = 0; i < shas.length; i += BATCH_SHAS) {
43874
+ const checked = git.runBuffer(["cat-file", "--batch-check"], `${shas.slice(i, i + BATCH_SHAS).join("\n")}
43875
+ `).toString("utf8");
43876
+ for (const line of checked.split("\n")) {
43877
+ const m = /^([0-9a-f]{40}) (\w+) (\d+)$/.exec(line);
43878
+ if (m === null || m[2] !== "blob") continue;
43879
+ const path4 = paths.get(m[1] ?? "");
43880
+ if (path4 !== void 0) out.push({ sha: m[1] ?? "", path: path4, size: Number(m[3]) });
43881
+ }
43882
+ }
43883
+ return out;
43884
+ }
43885
+ function blobTexts(git, blobs) {
43886
+ const sizes = new Map(blobs.map((b) => [b.sha, b.size]));
43887
+ const wanted = [...new Set(blobs.map((b) => b.sha))].filter((s) => (sizes.get(s) ?? 0) <= MAX_BLOB_BYTES);
43888
+ const out = /* @__PURE__ */ new Map();
43889
+ for (let i = 0; i < wanted.length; i += BATCH_SHAS) {
43890
+ const buf = git.runBuffer(["cat-file", "--batch", "--buffer"], `${wanted.slice(i, i + BATCH_SHAS).join("\n")}
43891
+ `);
43892
+ let pos = 0;
43893
+ while (pos < buf.length) {
43894
+ const nl = buf.indexOf(10, pos);
43895
+ if (nl === -1) break;
43896
+ const parts = buf.subarray(pos, nl).toString("utf8").split(" ");
43897
+ pos = nl + 1;
43898
+ const sha = parts[0] ?? "";
43899
+ if (parts[1] === "missing" || parts.length < 3) continue;
43900
+ const size = Number(parts[2]);
43901
+ const body = buf.subarray(pos, pos + size);
43902
+ pos += size + 1;
43903
+ if (!body.includes(0)) out.set(sha, body.toString("utf8"));
43904
+ }
43905
+ }
43906
+ return out;
43907
+ }
43908
+
43909
+ // src/commands/exfil.ts
43910
+ var EXFIL_USAGE = "usage: border exfil <ref|url> [ref...] [--deep] [--json]";
43911
+ var URL_SHAPE_RE = /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+|\S+@\S+:\S+)$/;
43912
+ var isExfilId = (rule) => rule.startsWith("exfil-");
43913
+ function toTreeFinding(f) {
43914
+ return {
43915
+ rule: f.id,
43916
+ severity: f.severity,
43917
+ target: "tree",
43918
+ path: f.source,
43919
+ line: f.line,
43920
+ engine: f.engine,
43921
+ message: f.message,
43922
+ valueDigest: f.valueDigest,
43923
+ snippet: f.snippet
43924
+ };
43925
+ }
43926
+ function scanBlobsNative(git, blobs) {
43927
+ const texts = blobTexts(git, blobs);
43928
+ const findings = [];
43929
+ for (const b of blobs) {
43930
+ const text = texts.get(b.sha);
43931
+ if (text === void 0) continue;
43932
+ for (const f of scanTreeText({ text, source: b.path })) findings.push(toTreeFinding(f));
43933
+ }
43934
+ return findings;
43935
+ }
43936
+ async function scanTipTree(git, treeRoot, rev, env2) {
43937
+ const blobs = tipTree(git, rev);
43938
+ const texts = blobTexts(git, blobs);
43939
+ const native = [];
43940
+ const written = [];
43941
+ for (const b of blobs) {
43942
+ const text = texts.get(b.sha);
43943
+ if (text === void 0) continue;
43944
+ for (const f of scanTreeText({ text, source: b.path })) native.push(toTreeFinding(f));
43945
+ if (b.path.split("/").includes("..") || b.path === "") continue;
43946
+ const abs = join24(treeRoot, b.path);
43947
+ if (existsSync15(abs)) continue;
43948
+ mkdirSync10(dirname12(abs), { recursive: true });
43949
+ writeFileSync5(abs, text, "utf8");
43950
+ written.push(b.path);
43951
+ }
43952
+ const lint = (await scanPaths({ dir: treeRoot, files: written, target: "tree", env: env2 })).filter((f) => isExfilId(f.rule));
43953
+ const gl = scanTree({ dir: treeRoot, stateDir: join24(treeRoot, ".border-state"), target: "tree", env: env2 }).filter((f) => isExfilId(f.rule)).map((f) => f.path !== void 0 && f.path.startsWith(`${treeRoot}/`) ? { ...f, path: f.path.slice(treeRoot.length + 1) } : f);
43954
+ return [...native, ...lint, ...gl];
43955
+ }
43956
+ function exfilRulesDigest() {
43957
+ const h = createHash14("sha256");
43958
+ for (const p of [...resolveExfilFingerprintFiles({}), GITLEAKS_VENDORED_CONFIG]) {
43959
+ let bytes;
43960
+ try {
43961
+ bytes = readFileSync15(p);
43962
+ } catch {
43963
+ throw new ConfigError("unreadable", `border exfil: rule input unreadable (fail closed): ${p}`);
43964
+ }
43965
+ h.update(p).update("\0").update(bytes).update("\0");
43966
+ }
43967
+ return h.digest("hex");
43968
+ }
43969
+ function sha256Hex2(text) {
43970
+ return createHash14("sha256").update(text, "utf8").digest("hex");
43971
+ }
43972
+ async function runExfilCore(ctx) {
43973
+ const positionals = ctx.positionals;
43974
+ if (positionals.length === 0) throw new UnknownArgError(`border exfil expects <ref|url> [ref...] \u2014 ${EXFIL_USAGE}`);
43975
+ const deep = ctx.flags.deep === true;
43976
+ const first = positionals[0] ?? "";
43977
+ const sandbox = { dir: "", tree: "" };
43978
+ try {
43979
+ const isUrl = URL_SHAPE_RE.test(first);
43980
+ let repoDir;
43981
+ let units;
43982
+ let exposure = [];
43983
+ if (isUrl) {
43984
+ const url2 = first;
43985
+ exposure = [sanitizeUrl(url2)];
43986
+ sandbox.dir = mkdtempSync4(join24(tmpdir3(), "border-exfil-"));
43987
+ const git2 = makeGit(sandbox.dir, ctx.env);
43988
+ git2.run(["init", "-q"]);
43989
+ git2.run(["remote", "add", "origin", url2]);
43990
+ const specs = positionals.slice(1).length === 0 ? ["HEAD"] : positionals.slice(1);
43991
+ units = [];
43992
+ for (const spec of specs) {
43993
+ git2.run(["fetch", "-q", "--no-tags", "origin", spec]);
43994
+ units.push({ label: spec, rev: git2.run(["rev-parse", "FETCH_HEAD"]).trim() });
43995
+ }
43996
+ repoDir = sandbox.dir;
43997
+ } else {
43998
+ repoDir = resolveRepoDir(ctx.cwd, { env: ctx.env });
43999
+ const git2 = makeGit(repoDir, ctx.env);
44000
+ units = positionals.map((ref) => ({ label: ref, rev: git2.run(["rev-parse", "--verify", ref]).trim() }));
44001
+ }
44002
+ const git = makeGit(repoDir, ctx.env);
44003
+ sandbox.tree = mkdtempSync4(join24(tmpdir3(), "border-exfil-tree-"));
44004
+ const findings = [];
44005
+ const seenBlob = /* @__PURE__ */ new Set();
44006
+ for (const unit of units) {
44007
+ findings.push(...await scanTipTree(git, sandbox.tree, unit.rev, ctx.env));
44008
+ if (deep) {
44009
+ const fresh = historyBlobs(git, unit.rev).filter((b) => {
44010
+ const key = `${b.sha}\0${b.path}`;
44011
+ if (seenBlob.has(key)) return false;
44012
+ seenBlob.add(key);
44013
+ return true;
44014
+ });
44015
+ findings.push(...scanBlobsNative(git, fresh));
44016
+ }
44017
+ }
44018
+ if (isUrl) ctx.stderr("border exfil: URL mode scanned fetched tips only \u2014 tag notes are not fetched (remote-side leg, check/push own it)");
44019
+ else findings.push(...scanTagMessages({ repoDir, target: "git", env: ctx.env }));
44020
+ if (deep) {
44021
+ findings.push(...scanGitHistory({ repoDir, refRange: units.map((u) => u.rev).join(" "), target: "git", env: ctx.env }).filter((f) => isExfilId(f.rule)));
44022
+ findings.push(
44023
+ ...scanCommitMessages({
44024
+ repoDir,
44025
+ refSet: units.map((u) => u.rev),
44026
+ remotes: [],
44027
+ hosts: [],
44028
+ runGit: git.run,
44029
+ env: ctx.env
44030
+ })
44031
+ );
44032
+ }
44033
+ const verdict = computeVerdict(findings);
44034
+ const report = {
44035
+ schemaVersion: 1,
44036
+ key: sha256Hex2(`exfil:${isUrl ? "url" : "ref"}:${positionals.join(" ")}:${deep ? "deep" : "fast"}:${units.map((u) => u.rev).join(" ")}`),
44037
+ head: units[0]?.rev ?? "",
44038
+ dirty: false,
44039
+ exposureSet: exposure,
44040
+ refSet: positionals,
44041
+ rulesHash: exfilRulesDigest(),
44042
+ verdict,
44043
+ counts: countFindings(findings),
44044
+ findings,
44045
+ ts: (/* @__PURE__ */ new Date()).toISOString()
44046
+ };
44047
+ if (ctx.flags.json) ctx.stdout(renderReportJson(report));
44048
+ else {
44049
+ const scope = `${isUrl ? "URL" : "refs"} ${positionals.join(",")}${deep ? " (deep)" : ""}`;
44050
+ ctx.stdout(`border exfil ${scope} ${report.verdict}: ${String(report.counts.total)} finding(s), ${String(report.counts.blocking)} blocking`);
44051
+ for (const f of report.findings) {
44052
+ ctx.stdout(` ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.commit ?? f.target}${f.line !== void 0 ? `:${String(f.line)}` : ""} ${f.message}`);
44053
+ }
44054
+ }
44055
+ return exitCodeFromVerdict(verdict);
44056
+ } finally {
44057
+ if (sandbox.dir !== "") rmSync11(sandbox.dir, { recursive: true, force: true });
44058
+ if (sandbox.tree !== "") rmSync11(sandbox.tree, { recursive: true, force: true });
44059
+ }
44060
+ }
44061
+ var runExfil = (ctx) => runExfilCore(ctx);
44062
+
43282
44063
  // src/llm/bundle.ts
43283
- import { createHash as createHash13 } from "node:crypto";
43284
- import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
43285
- import { dirname as dirname11, join as join24 } from "node:path";
44064
+ import { createHash as createHash15 } from "node:crypto";
44065
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
44066
+ import { dirname as dirname14, join as join26 } from "node:path";
43286
44067
 
43287
44068
  // src/llm/diff.ts
43288
- import { spawnSync as spawnSync14 } from "node:child_process";
43289
- import { mkdtempSync as mkdtempSync4, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
43290
- import { dirname as dirname10, join as join23, resolve as resolve13 } from "node:path";
44069
+ import { spawnSync as spawnSync16 } from "node:child_process";
44070
+ import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync6 } from "node:fs";
44071
+ import { dirname as dirname13, join as join25, resolve as resolve16 } from "node:path";
43291
44072
  var GIT_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
43292
44073
  function gitOut(repoDir, args, o, allowFail = false) {
43293
- const abs = resolve13(repoDir);
43294
- const r = spawnSync14("git", ["-C", abs, ...args], {
44074
+ const abs = resolve16(repoDir);
44075
+ const r = spawnSync16("git", ["-C", abs, ...args], {
43295
44076
  encoding: "utf8",
43296
- env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname10(abs) },
44077
+ env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname13(abs) },
43297
44078
  timeout: 12e4,
43298
44079
  maxBuffer: GIT_DIFF_MAX_BUFFER
43299
44080
  });
@@ -43384,24 +44165,24 @@ function collectRawDeltas(repoDir, baseSha, headSha, o = {}) {
43384
44165
  var MASK_CHUNK_CHARS = 2e6;
43385
44166
  function maskViaEngineDetection(repoDir, texts, o = {}) {
43386
44167
  const sanitizer = new TextSanitizer();
43387
- const sandbox = mkdtempSync4(join23(repoDir, BORDER_STATE_DIR, "llm-mask-"));
44168
+ const sandbox = mkdtempSync5(join25(repoDir, BORDER_STATE_DIR, "llm-mask-"));
43388
44169
  try {
43389
44170
  let idx = 0;
43390
44171
  for (const text of texts) {
43391
44172
  if (text === "") continue;
43392
44173
  for (let off = 0; off < text.length; off += MASK_CHUNK_CHARS, idx += 1) {
43393
- writeFileSync5(join23(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
44174
+ writeFileSync6(join25(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
43394
44175
  }
43395
44176
  }
43396
44177
  scanTree({
43397
44178
  dir: sandbox,
43398
- stateDir: join23(repoDir, BORDER_STATE_DIR),
44179
+ stateDir: join25(repoDir, BORDER_STATE_DIR),
43399
44180
  target: "llm-mask",
43400
44181
  sanitizer,
43401
44182
  ...engineEnv(o)
43402
44183
  });
43403
44184
  } finally {
43404
- rmSync11(sandbox, { recursive: true, force: true });
44185
+ rmSync12(sandbox, { recursive: true, force: true });
43405
44186
  }
43406
44187
  return (text) => sanitizer.sanitize(text);
43407
44188
  }
@@ -43453,18 +44234,18 @@ async function runLlmRequestCore(ctx) {
43453
44234
  ctx.stderr("border: llm-request: no check record for the current state \u2014 run 'border check' first (the bundle derives from the recorded ctx)");
43454
44235
  return EXIT_ERROR;
43455
44236
  }
43456
- const absReport = join24(repoDir, record2.reportPath);
43457
- if (!existsSync15(absReport)) {
44237
+ const absReport = join26(repoDir, record2.reportPath);
44238
+ if (!existsSync16(absReport)) {
43458
44239
  ctx.stderr(`border: llm-request: report archive missing (${record2.reportPath} pruned?) \u2014 run 'border check' again`);
43459
44240
  return EXIT_ERROR;
43460
44241
  }
43461
- const report = JSON.parse(readFileSync14(absReport, "utf8"));
44242
+ const report = JSON.parse(readFileSync16(absReport, "utf8"));
43462
44243
  if (report.key !== record2.key) {
43463
44244
  ctx.stderr("border: llm-request: report.json does not match its ledger record \u2014 run 'border check' again");
43464
44245
  return EXIT_ERROR;
43465
44246
  }
43466
44247
  const templatePath = resolvePromptTemplatePath(ctx.env);
43467
- if (!existsSync15(templatePath)) {
44248
+ if (!existsSync16(templatePath)) {
43468
44249
  ctx.stderr(`border: llm-request: MISSING_TEMPLATE \u2014 prompt template not found at ${templatePath}; refusing to emit an unanswerable bundle`);
43469
44250
  return EXIT_ERROR;
43470
44251
  }
@@ -43510,11 +44291,11 @@ async function runLlmRequestCore(ctx) {
43510
44291
  fileDeltas,
43511
44292
  artifacts: record2.artifacts,
43512
44293
  deterministic: { verdict: report.verdict, counts: report.counts, findings: report.findings },
43513
- promptTemplate: { path: templatePath, sha256: createHash13("sha256").update(readFileSync14(templatePath)).digest("hex") }
44294
+ promptTemplate: { path: templatePath, sha256: createHash15("sha256").update(readFileSync16(templatePath)).digest("hex") }
43514
44295
  };
43515
- const outPath = join24(dirname11(absReport), "llm-request.json");
43516
- mkdirSync10(dirname11(outPath), { recursive: true });
43517
- writeFileSync6(outPath, `${JSON.stringify(bundle, null, 2)}
44296
+ const outPath = join26(dirname14(absReport), "llm-request.json");
44297
+ mkdirSync11(dirname14(outPath), { recursive: true });
44298
+ writeFileSync7(outPath, `${JSON.stringify(bundle, null, 2)}
43518
44299
  `, "utf8");
43519
44300
  const rel = outPath.slice(repoDir.length + 1);
43520
44301
  ctx.stdout(`border: llm-request: wrote ${rel} (base ${base.mode}, ${String(fileDeltas.length)} file deltas, ${String(report.findings.length)} deterministic findings)`);
@@ -43522,14 +44303,14 @@ async function runLlmRequestCore(ctx) {
43522
44303
  }
43523
44304
 
43524
44305
  // src/llm/ingest.ts
43525
- import { createHash as createHash14 } from "node:crypto";
43526
- import { existsSync as existsSync16, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "node:fs";
43527
- import { isAbsolute, join as join25, resolve as resolve14 } from "node:path";
44306
+ import { createHash as createHash16 } from "node:crypto";
44307
+ import { existsSync as existsSync17, readFileSync as readFileSync17, readdirSync as readdirSync9 } from "node:fs";
44308
+ import { isAbsolute, join as join27, resolve as resolve17 } from "node:path";
43528
44309
  var RULE_RE = /^[a-z0-9-]+$/;
43529
44310
  var MAX_TEXT = 4e3;
43530
44311
  var ALLOWED_KEYS = /* @__PURE__ */ new Set(["rule", "severity", "target", "path", "line", "commit", "message", "snippet", "engine", "valueDigest"]);
43531
44312
  function sha2562(text) {
43532
- return createHash14("sha256").update(text, "utf8").digest("hex");
44313
+ return createHash16("sha256").update(text, "utf8").digest("hex");
43533
44314
  }
43534
44315
  function usageError(ctx, why) {
43535
44316
  ctx.stderr(`border: llm-ingest: ${why}`);
@@ -43543,11 +44324,11 @@ function findLatestBundle(repoDir) {
43543
44324
  return "no llm-request bundle found \u2014 run 'border llm-request' first";
43544
44325
  }
43545
44326
  for (const d of dirs.sort().reverse()) {
43546
- const p = join25(runsDir(repoDir), d, "llm-request.json");
43547
- if (!existsSync16(p)) continue;
44327
+ const p = join27(runsDir(repoDir), d, "llm-request.json");
44328
+ if (!existsSync17(p)) continue;
43548
44329
  let parsed;
43549
44330
  try {
43550
- parsed = JSON.parse(readFileSync15(p, "utf8"));
44331
+ parsed = JSON.parse(readFileSync17(p, "utf8"));
43551
44332
  } catch (err) {
43552
44333
  return `bundle ${p} is not valid JSON (${err instanceof Error ? err.message : String(err)}) \u2014 re-run 'border llm-request'`;
43553
44334
  }
@@ -43556,7 +44337,7 @@ function findLatestBundle(repoDir) {
43556
44337
  if (b.schemaVersion !== 1 || typeof b.key !== "string" || !/^[0-9a-f]{64}$/.test(b.key) || typeof b.head !== "string" || !/^[0-9a-f]{40}$/.test(b.head) || !Array.isArray(b.fileDeltas) || b.deterministic === void 0) {
43557
44338
  return `bundle ${p} fails its own schema \u2014 re-run 'border llm-request'`;
43558
44339
  }
43559
- return { dir: join25(runsDir(repoDir), d), bundle: b };
44340
+ return { dir: join27(runsDir(repoDir), d), bundle: b };
43560
44341
  }
43561
44342
  return "no llm-request bundle found \u2014 run 'border llm-request' first";
43562
44343
  }
@@ -43607,10 +44388,10 @@ async function runLlmIngestCore(ctx) {
43607
44388
  const found = findLatestBundle(repoDir);
43608
44389
  if (typeof found === "string") return usageError(ctx, found);
43609
44390
  const { bundle } = found;
43610
- const findingsPath = isAbsolute(arg) ? arg : resolve14(ctx.cwd, arg);
44391
+ const findingsPath = isAbsolute(arg) ? arg : resolve17(ctx.cwd, arg);
43611
44392
  let text;
43612
44393
  try {
43613
- text = readFileSync15(findingsPath, "utf8");
44394
+ text = readFileSync17(findingsPath, "utf8");
43614
44395
  } catch {
43615
44396
  return usageError(ctx, `findings file not found: ${arg}`);
43616
44397
  }
@@ -43701,10 +44482,10 @@ async function runLlmRequest(ctx) {
43701
44482
  }
43702
44483
 
43703
44484
  // src/commands/opencode.ts
43704
- import { createHash as createHash15 } from "node:crypto";
43705
- import { existsSync as existsSync17, lstatSync, mkdirSync as mkdirSync11, readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
44485
+ import { createHash as createHash17 } from "node:crypto";
44486
+ import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
43706
44487
  import os3 from "node:os";
43707
- import { dirname as dirname12, join as join26 } from "node:path";
44488
+ import { dirname as dirname15, join as join28 } from "node:path";
43708
44489
  import { fileURLToPath as fileURLToPath5 } from "node:url";
43709
44490
  var PLUGIN_MARKER = "// border-opencode-plugin v";
43710
44491
  var COMMAND_MARKER = "<!-- border-opencode-command -->";
@@ -43712,27 +44493,27 @@ function opencodeConfigRoot(env2) {
43712
44493
  const xdg = env2["XDG_CONFIG_HOME"] ?? "";
43713
44494
  if (xdg.length > 0) return xdg;
43714
44495
  const home = env2["HOME"] ?? "";
43715
- return join26(home.length > 0 ? home : os3.homedir(), ".config");
44496
+ return join28(home.length > 0 ? home : os3.homedir(), ".config");
43716
44497
  }
43717
44498
  function resolvePluginAsset(moduleUrl, fileName) {
43718
- const here = dirname12(fileURLToPath5(moduleUrl));
44499
+ const here = dirname15(fileURLToPath5(moduleUrl));
43719
44500
  const candidates = [
43720
- join26(here, "..", "plugin", fileName),
44501
+ join28(here, "..", "plugin", fileName),
43721
44502
  // dist mode (bundled): <root>/plugin/<f>
43722
- join26(here, "..", "..", "plugin", fileName)
44503
+ join28(here, "..", "..", "plugin", fileName)
43723
44504
  // src mode: <root>/plugin/<f>
43724
44505
  ];
43725
- return candidates.find((p) => existsSync17(p)) ?? candidates[1];
44506
+ return candidates.find((p) => existsSync18(p)) ?? candidates[1];
43726
44507
  }
43727
44508
  function targetsFor(env2) {
43728
- const root = join26(opencodeConfigRoot(env2), "opencode");
44509
+ const root = join28(opencodeConfigRoot(env2), "opencode");
43729
44510
  return [
43730
- { fileName: "border.ts", destination: join26(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
43731
- { fileName: "border-command.md", destination: join26(root, "commands", "border.md"), marker: COMMAND_MARKER }
44511
+ { fileName: "border.ts", destination: join28(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
44512
+ { fileName: "border-command.md", destination: join28(root, "commands", "border.md"), marker: COMMAND_MARKER }
43732
44513
  ];
43733
44514
  }
43734
44515
  function sha2563(bytes) {
43735
- return createHash15("sha256").update(bytes).digest("hex");
44516
+ return createHash17("sha256").update(bytes).digest("hex");
43736
44517
  }
43737
44518
  function firstLine(bytes) {
43738
44519
  const text = bytes.toString("utf8");
@@ -43749,7 +44530,7 @@ function fail(ctx, message) {
43749
44530
  function readPackagedAsset(ctx, fileName) {
43750
44531
  const source = resolvePluginAsset(import.meta.url, fileName);
43751
44532
  try {
43752
- return readFileSync16(source);
44533
+ return readFileSync18(source);
43753
44534
  } catch {
43754
44535
  ctx.stderr(`border: opencode: packaged asset is missing (${source})`);
43755
44536
  ctx.stderr("border: opencode: the border install looks broken \u2014 `npm i -g border-customs` or a fresh checkout should carry plugin/.");
@@ -43758,7 +44539,7 @@ function readPackagedAsset(ctx, fileName) {
43758
44539
  }
43759
44540
  function readTarget(destination) {
43760
44541
  try {
43761
- return readFileSync16(destination);
44542
+ return readFileSync18(destination);
43762
44543
  } catch {
43763
44544
  return null;
43764
44545
  }
@@ -43825,8 +44606,8 @@ function opencodeInstall(ctx) {
43825
44606
  ctx.stdout(`${target.destination}: up to date`);
43826
44607
  continue;
43827
44608
  }
43828
- mkdirSync11(dirname12(target.destination), { recursive: true });
43829
- writeFileSync7(target.destination, packaged);
44609
+ mkdirSync12(dirname15(target.destination), { recursive: true });
44610
+ writeFileSync8(target.destination, packaged);
43830
44611
  ctx.stdout(`${target.destination}: ${state === "outdated" ? "updated" : "installed"}`);
43831
44612
  }
43832
44613
  ctx.stdout("note: restart opencode to load the plugin \u2014 tools and commands are scanned at startup.");
@@ -43860,7 +44641,7 @@ function opencodeUninstall(ctx) {
43860
44641
  `refusing to remove ${target.destination} \u2014 the file is not ours (its first line lacks the marker '${target.marker}'); nothing was removed anywhere`
43861
44642
  );
43862
44643
  }
43863
- rmSync12(target.destination);
44644
+ rmSync13(target.destination);
43864
44645
  ctx.stdout(`${target.destination}: removed`);
43865
44646
  }
43866
44647
  ctx.stdout("note: restart opencode for the tool and command to disappear.");
@@ -43888,7 +44669,7 @@ function runOpencode(ctx) {
43888
44669
  }
43889
44670
 
43890
44671
  // src/push/git.ts
43891
- import { spawnSync as spawnSync15 } from "node:child_process";
44672
+ import { spawnSync as spawnSync17 } from "node:child_process";
43892
44673
  var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
43893
44674
  function pushArgv(url2, branch, mode) {
43894
44675
  return mode === "dry-run" ? ["push", "--dry-run", url2, branch, "--follow-tags"] : ["push", url2, branch, "--follow-tags"];
@@ -43923,7 +44704,7 @@ function lsRemoteMap(repoDir, remote, env2) {
43923
44704
  return parseLsRemote(text, remote.target);
43924
44705
  }
43925
44706
  function isAncestor(repoDir, remoteSha, localSha, env2) {
43926
- const r = spawnSync15("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
44707
+ const r = spawnSync17("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
43927
44708
  encoding: "utf8",
43928
44709
  env: { ...env2, GIT_CEILING_DIRECTORIES: repoDir }
43929
44710
  });
@@ -43958,7 +44739,7 @@ function pushTimeoutMs(env2) {
43958
44739
  }
43959
44740
  function executePush(i) {
43960
44741
  const timeoutMs = pushTimeoutMs(i.env);
43961
- const r = spawnSync15("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
44742
+ const r = spawnSync17("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
43962
44743
  stdio: "inherit",
43963
44744
  env: i.env,
43964
44745
  timeout: timeoutMs
@@ -43983,6 +44764,101 @@ function confirmRemoteBranch(i) {
43983
44764
  return sha;
43984
44765
  }
43985
44766
 
44767
+ // src/push/landing.ts
44768
+ import { spawnSync as spawnSync18 } from "node:child_process";
44769
+ import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "node:fs";
44770
+ import { tmpdir as tmpdir4 } from "node:os";
44771
+ import { dirname as dirname16, join as join29, resolve as resolve18 } from "node:path";
44772
+ var LANDINGS_REF_PREFIX = "refs/border-landings/";
44773
+ async function runLandingVerification(o) {
44774
+ if (o.legs.length === 0) return null;
44775
+ const rulesHash = landingRulesHash(o);
44776
+ const observe = o.observeRemoteTip ?? ((remote, branch) => defaultObserveRemoteTip(o.repoDir, remote, branch, o.env));
44777
+ const scan = o.scanRemoteTip ?? ((remote, branch, sha) => defaultScanRemoteTip(o, remote, branch, sha));
44778
+ const sink = o.record ?? ((r) => appendRecord(o.repoDir, r));
44779
+ for (const leg of o.legs) {
44780
+ const sha = observe(leg.target, leg.branch);
44781
+ if (sha === null || sha === "") {
44782
+ o.err(`border: ${leg.target.target} landed, verification unavailable (remote tip unfetchable) \u2014 exit 2; PASS not endorsed for the public face`);
44783
+ return EXIT_ERROR;
44784
+ }
44785
+ let findings;
44786
+ try {
44787
+ findings = await scan(leg.target, leg.branch, sha);
44788
+ } catch (err) {
44789
+ o.err(
44790
+ `border: ${leg.target.target} landed, verification unavailable (remote tip fetch/scan failed: ${err instanceof Error ? err.message : String(err)}; first bytes: ${sha.slice(0, 8)}) \u2014 exit 2; PASS not endorsed for the public face`
44791
+ );
44792
+ return EXIT_ERROR;
44793
+ }
44794
+ const blocking = findings.filter((f) => isBlocking(f.severity)).length;
44795
+ const verdict = blocking > 0 ? "blocked" : "clean";
44796
+ sink(buildLandingRecord({ key: o.key, target: leg.target.target, ref: `refs/heads/${leg.branch}`, remoteSha: sha, verdict, blocking, rulesHash }));
44797
+ if (blocking > 0) {
44798
+ o.err(`border: ${leg.target.target} ALREADY PUBLIC; border detects, never erases \u2014 ${String(blocking)} blocking finding(s) on the landed tip ${sha.slice(0, 12)}`);
44799
+ for (const f of findings.filter((x) => isBlocking(x.severity))) {
44800
+ o.err(`border: ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.commit ?? f.target}${f.line !== void 0 ? `:${String(f.line)}` : ""}`);
44801
+ }
44802
+ return EXIT_BLOCKED;
44803
+ }
44804
+ o.out(`border: ${leg.target.target} landing verified \u2014 remote tip ${sha.slice(0, 12)} clean (t:${verdict} record appended)`);
44805
+ }
44806
+ return null;
44807
+ }
44808
+ function landingRulesHash(o) {
44809
+ const { records } = readLedger(o.repoDir);
44810
+ const pass = latestPassCoveringTargets(records, o.key, ["git"]);
44811
+ if (pass === null) {
44812
+ throw new Error(`landing: no PASS record under key ${o.key.slice(0, 8)} \u2014 refusing to record a landing fact against unknown rules`);
44813
+ }
44814
+ return pass.rulesHash;
44815
+ }
44816
+ function gitPlain(repoDir, env2, args) {
44817
+ const clean = {};
44818
+ for (const [name, value] of Object.entries(env2)) {
44819
+ if (!name.startsWith("GIT_")) clean[name] = value;
44820
+ }
44821
+ clean.GIT_CEILING_DIRECTORIES = dirname16(resolve18(repoDir));
44822
+ clean.GIT_OPTIONAL_LOCKS = "0";
44823
+ clean.GIT_TERMINAL_PROMPT = "0";
44824
+ const r = spawnSync18("git", ["-C", resolve18(repoDir), ...args], { encoding: "utf8", env: clean, timeout: 55e3, maxBuffer: 32 * 1024 * 1024 });
44825
+ return { status: r.status ?? 1, stdout: r.stdout ?? "" };
44826
+ }
44827
+ function defaultObserveRemoteTip(repoDir, remote, branch, env2) {
44828
+ const r = gitPlain(repoDir, env2, ["ls-remote", remote.url, `refs/heads/${branch}`]);
44829
+ if (r.status !== 0) return null;
44830
+ const sha = /^[0-9a-f]{40}/.exec(r.stdout)?.[0] ?? null;
44831
+ return sha;
44832
+ }
44833
+ async function defaultScanRemoteTip(o, remote, branch, remoteSha) {
44834
+ const git = makeGit(o.repoDir, o.env);
44835
+ let rev = remoteSha;
44836
+ let ownedTempRef = null;
44837
+ try {
44838
+ try {
44839
+ git.run(["cat-file", "-e", `${remoteSha}^{object}`]);
44840
+ } catch {
44841
+ const landingRef = `${LANDINGS_REF_PREFIX}${remote.target.replace(/[^A-Za-z0-9_.-]/g, "-")}`;
44842
+ git.run(["fetch", "-q", "--no-tags", remote.url, `refs/heads/${branch}:${landingRef}`]);
44843
+ ownedTempRef = landingRef;
44844
+ rev = git.run(["rev-parse", landingRef]).trim();
44845
+ }
44846
+ const treeRoot = mkdtempSync6(join29(tmpdir4(), "border-landing-tree-"));
44847
+ try {
44848
+ return await scanTipTree(git, treeRoot, rev, o.env);
44849
+ } finally {
44850
+ rmSync14(treeRoot, { recursive: true, force: true });
44851
+ }
44852
+ } finally {
44853
+ if (ownedTempRef !== null) {
44854
+ try {
44855
+ git.run(["update-ref", "-d", ownedTempRef]);
44856
+ } catch {
44857
+ }
44858
+ }
44859
+ }
44860
+ }
44861
+
43986
44862
  // src/commands/push.ts
43987
44863
  var DRY_RUN_PREFIX = "DRY-RUN";
43988
44864
  var GIT_PUSH_DRY_RUN = "git push --dry-run";
@@ -44001,6 +44877,18 @@ async function runPush(ctx) {
44001
44877
  });
44002
44878
  if (loaded.kind === "no-op") {
44003
44879
  for (const warning of loaded.warnings) ctx.stderr(`border: ${warning}`);
44880
+ let orphans = [];
44881
+ try {
44882
+ orphans = remoteTrackingDrift(resolveRepoDir(ctx.cwd, { env: ctx.env })).orphans;
44883
+ } catch {
44884
+ orphans = [];
44885
+ }
44886
+ if (orphans.length > 0) {
44887
+ ctx.stderr(
44888
+ `border: remote config drift \u2014 remote-tracking refs exist for UNCONFIGURED remotes: ${orphans.join(", ")}; their remote.* entries are gone (wiped or partial clone). Restore them or prune the refs; refusing to report this as a no-op.`
44889
+ );
44890
+ return EXIT_ERROR;
44891
+ }
44004
44892
  ctx.stdout("border: no targets discovered \u2014 nothing to push (NO-OP)");
44005
44893
  return EXIT_PASS;
44006
44894
  }
@@ -44134,6 +45022,15 @@ async function executeYesPush(ctx, loaded) {
44134
45022
  done.push(leg.target);
44135
45023
  ctx.stdout(`border: pushed ${leg.target} \u2014 ${shortSha(remoteSha)} confirmed via ls-remote`);
44136
45024
  }
45025
+ const landing = await runLandingVerification({
45026
+ repoDir,
45027
+ key: state.key,
45028
+ legs: legs.map((leg) => ({ target: leg, branch })),
45029
+ env: env2,
45030
+ out: ctx.stdout,
45031
+ err: ctx.stderr
45032
+ });
45033
+ if (landing !== null) return landing;
44137
45034
  }
44138
45035
  for (const channel of publishChannels()) {
44139
45036
  if (!pending.some((t) => t.kind === channel.id)) continue;
@@ -44152,17 +45049,17 @@ async function executeYesPush(ctx, loaded) {
44152
45049
  }
44153
45050
 
44154
45051
  // src/roundtrip/orchestrate.ts
44155
- import { mkdtempSync as mkdtempSync5, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
45052
+ import { mkdtempSync as mkdtempSync7, rmSync as rmSync15, writeFileSync as writeFileSync9 } from "node:fs";
44156
45053
  import { randomBytes } from "node:crypto";
44157
- import { tmpdir as tmpdir3 } from "node:os";
44158
- import { join as join27 } from "node:path";
45054
+ import { tmpdir as tmpdir5 } from "node:os";
45055
+ import { join as join30 } from "node:path";
44159
45056
 
44160
45057
  // src/scan/fetch.ts
44161
- import { createHash as createHash16 } from "node:crypto";
45058
+ import { createHash as createHash18 } from "node:crypto";
44162
45059
  import { basename as basename5 } from "node:path";
44163
45060
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
44164
45061
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
44165
- var SCAN_USER_AGENT = "border-customs/0.5.1 (+https://github.com/sumteclab/border; border scan)";
45062
+ var SCAN_USER_AGENT = "border-customs/0.7.1 (+https://github.com/sumteclab/border; border scan)";
44166
45063
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
44167
45064
  var defaultScanFetcher = async (url2, init) => {
44168
45065
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -44287,8 +45184,8 @@ async function fetchArtifact(spec, o) {
44287
45184
  }
44288
45185
  }
44289
45186
  }
44290
- function sha256Hex2(data) {
44291
- return createHash16("sha256").update(data).digest("hex");
45187
+ function sha256Hex3(data) {
45188
+ return createHash18("sha256").update(data).digest("hex");
44292
45189
  }
44293
45190
 
44294
45191
  // src/scan/spec.ts
@@ -44331,9 +45228,9 @@ function parseScanSpec(raw) {
44331
45228
  }
44332
45229
 
44333
45230
  // src/roundtrip/localArtifact.ts
44334
- import { createReadStream, existsSync as existsSync18, statSync as statSync2 } from "node:fs";
44335
- import { createHash as createHash17 } from "node:crypto";
44336
- import { basename as basename6, resolve as resolve15 } from "node:path";
45231
+ import { createReadStream, existsSync as existsSync19, statSync as statSync3 } from "node:fs";
45232
+ import { createHash as createHash19 } from "node:crypto";
45233
+ import { basename as basename6, resolve as resolve19 } from "node:path";
44337
45234
  var EXT_TABLE = /* @__PURE__ */ new Map([
44338
45235
  [".whl", "pypi"],
44339
45236
  [".tgz", "npm"],
@@ -44379,9 +45276,9 @@ var GRAMMAR_HINT = {
44379
45276
  rubygems: "gem package names, e.g. rt-gem-1.0.0.gem"
44380
45277
  };
44381
45278
  function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
44382
- const abs = resolve15(cwd, raw);
44383
- if (existsSync18(abs)) {
44384
- const st = statSync2(abs);
45279
+ const abs = resolve19(cwd, raw);
45280
+ if (existsSync19(abs)) {
45281
+ const st = statSync3(abs);
44385
45282
  if (st.isDirectory()) {
44386
45283
  bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
44387
45284
  }
@@ -44415,7 +45312,7 @@ function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
44415
45312
  return null;
44416
45313
  }
44417
45314
  async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
44418
- const hash2 = createHash17("sha256");
45315
+ const hash2 = createHash19("sha256");
44419
45316
  const parts = [];
44420
45317
  let total = 0;
44421
45318
  for await (const chunk of createReadStream(absPath)) {
@@ -44641,7 +45538,7 @@ var CLAIM_SCANNER_PY = [
44641
45538
  ].join("\n");
44642
45539
 
44643
45540
  // src/roundtrip/manifest.ts
44644
- import { createHash as createHash18 } from "node:crypto";
45541
+ import { createHash as createHash20 } from "node:crypto";
44645
45542
  var RT_ENGINE = "roundtrip";
44646
45543
  var RT_RESIDUE_RULE = "roundtrip-residue-file";
44647
45544
  var RT_MODIFY_RULE = "roundtrip-modified-file";
@@ -44692,7 +45589,7 @@ var PERSISTENCE_PATTERNS = [
44692
45589
  function isPersistenceSurface(path4) {
44693
45590
  return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
44694
45591
  }
44695
- var digestOf = (what) => createHash18("sha256").update(what).digest("hex");
45592
+ var digestOf = (what) => createHash20("sha256").update(what).digest("hex");
44696
45593
  function classifyResidue(label, diff, attribution = null) {
44697
45594
  const findings = [];
44698
45595
  for (const e of diff.added) {
@@ -44739,7 +45636,7 @@ function classifyResidue(label, diff, attribution = null) {
44739
45636
 
44740
45637
  // src/roundtrip/orchestrate.ts
44741
45638
  function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
44742
- return `rt-${sha256Hex2(label).slice(0, 8)}-${rand}`;
45639
+ return `rt-${sha256Hex3(label).slice(0, 8)}-${rand}`;
44743
45640
  }
44744
45641
  var shArgv = (name) => {
44745
45642
  if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
@@ -44860,7 +45757,7 @@ function runPipeline2(exec, profile, spec, label, hostPath, container, closure,
44860
45757
  return {
44861
45758
  findings: classifyResidue(label, diffManifests(m1, m3), attribution),
44862
45759
  installDelta,
44863
- baselineDigest: sha256Hex2([...m1.keys()].join("\n"))
45760
+ baselineDigest: sha256Hex3([...m1.keys()].join("\n"))
44864
45761
  };
44865
45762
  } finally {
44866
45763
  try {
@@ -44894,25 +45791,25 @@ async function runRoundtripCore(ctx, deps = {}) {
44894
45791
  ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
44895
45792
  return EXIT_ERROR;
44896
45793
  }
44897
- const baseDir = mkdtempSync5(join27(tmpdir3(), "border-roundtrip-"));
45794
+ const baseDir = mkdtempSync7(join30(tmpdir5(), "border-roundtrip-"));
44898
45795
  try {
44899
45796
  const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
44900
45797
  const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
44901
- const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex2(bytes);
44902
- const hostPath = join27(baseDir, "artifact");
44903
- writeFileSync8(hostPath, bytes);
45798
+ const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex3(bytes);
45799
+ const hostPath = join30(baseDir, "artifact");
45800
+ writeFileSync9(hostPath, bytes);
44904
45801
  const container = containerNameFor(label);
44905
45802
  const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
44906
45803
  const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
44907
45804
  const verdict = computeVerdict(findings);
44908
45805
  const report = {
44909
45806
  schemaVersion: 1,
44910
- key: sha256Hex2(`roundtrip:${label}:${baselineDigest}:${verdict}`),
45807
+ key: sha256Hex3(`roundtrip:${label}:${baselineDigest}:${verdict}`),
44911
45808
  head: baselineDigest.slice(0, 40),
44912
45809
  dirty: false,
44913
45810
  exposureSet: [label],
44914
45811
  refSet: [],
44915
- rulesHash: sha256Hex2(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
45812
+ rulesHash: sha256Hex3(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
44916
45813
  verdict,
44917
45814
  counts: countFindings(findings),
44918
45815
  findings,
@@ -44944,7 +45841,7 @@ async function runRoundtripCore(ctx, deps = {}) {
44944
45841
  }
44945
45842
  return exitCodeFromVerdict(verdict);
44946
45843
  } finally {
44947
- rmSync13(baseDir, { recursive: true, force: true });
45844
+ rmSync15(baseDir, { recursive: true, force: true });
44948
45845
  }
44949
45846
  }
44950
45847
  function parsesRegistrySpec(raw) {
@@ -45000,9 +45897,9 @@ function dockerAvailableGuard(exec) {
45000
45897
  var runRoundtrip = (ctx) => runRoundtripCore(ctx);
45001
45898
 
45002
45899
  // src/scan/scan.ts
45003
- import { mkdtempSync as mkdtempSync6 } from "node:fs";
45004
- import { tmpdir as tmpdir4 } from "node:os";
45005
- import { join as join29 } from "node:path";
45900
+ import { mkdtempSync as mkdtempSync8 } from "node:fs";
45901
+ import { tmpdir as tmpdir6 } from "node:os";
45902
+ import { join as join32 } from "node:path";
45006
45903
 
45007
45904
  // src/scan/config.ts
45008
45905
  function buildScanConfigYaml(eco) {
@@ -45029,8 +45926,8 @@ function synthesizeScanConfig(eco) {
45029
45926
  }
45030
45927
 
45031
45928
  // src/scan/materialize.ts
45032
- import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync17, rmSync as rmSync14, writeFileSync as writeFileSync9 } from "node:fs";
45033
- import { join as join28 } from "node:path";
45929
+ import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, readFileSync as readFileSync19, rmSync as rmSync16, writeFileSync as writeFileSync10 } from "node:fs";
45930
+ import { join as join31 } from "node:path";
45034
45931
  var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
45035
45932
  var fail3 = (why) => {
45036
45933
  throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
@@ -45047,12 +45944,12 @@ function unwrapSingleDir(dir) {
45047
45944
  const entries = readdirSync10(dir, { withFileTypes: true });
45048
45945
  const dirs = entries.filter((e) => e.isDirectory());
45049
45946
  if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
45050
- return join28(dir, dirs[0].name);
45947
+ return join31(dir, dirs[0].name);
45051
45948
  }
45052
45949
  return dir;
45053
45950
  }
45054
45951
  function requireManifest(repoDir, ecosystem, candidates) {
45055
- if (!candidates.some((rel) => existsSync19(join28(repoDir, rel)))) {
45952
+ if (!candidates.some((rel) => existsSync20(join31(repoDir, rel)))) {
45056
45953
  fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
45057
45954
  }
45058
45955
  }
@@ -45062,19 +45959,19 @@ function materializePackage(o) {
45062
45959
  `);
45063
45960
  });
45064
45961
  const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
45065
- const archive = join28(o.baseDir, safe);
45066
- writeFileSync9(archive, o.bytes);
45067
- const extractDir = join28(o.baseDir, "extract");
45068
- mkdirSync12(extractDir, { recursive: true });
45962
+ const archive = join31(o.baseDir, safe);
45963
+ writeFileSync10(archive, o.bytes);
45964
+ const extractDir = join31(o.baseDir, "extract");
45965
+ mkdirSync13(extractDir, { recursive: true });
45069
45966
  extractFlat(archive, extractDir);
45070
45967
  let repoDir = unwrapSingleDir(extractDir);
45071
45968
  if (o.ecosystem === "rubygems") {
45072
45969
  const outer = repoDir;
45073
45970
  const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
45074
- const inner = dataCandidates[0] ?? join28(outer, "data.tar.gz");
45075
- if (!existsSync19(inner)) fail3(".gem archive contains no data.tar.gz");
45076
- const gemTree = join28(o.baseDir, "gem-tree");
45077
- mkdirSync12(gemTree, { recursive: true });
45971
+ const inner = dataCandidates[0] ?? join31(outer, "data.tar.gz");
45972
+ if (!existsSync20(inner)) fail3(".gem archive contains no data.tar.gz");
45973
+ const gemTree = join31(o.baseDir, "gem-tree");
45974
+ mkdirSync13(gemTree, { recursive: true });
45078
45975
  extractFlat(inner, gemTree);
45079
45976
  repoDir = unwrapSingleDir(gemTree);
45080
45977
  } else if (o.ecosystem === "npm") {
@@ -45084,11 +45981,11 @@ function materializePackage(o) {
45084
45981
  } else {
45085
45982
  requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
45086
45983
  for (const file2 of CRATES_ENVELOPE_FILES) {
45087
- const path4 = join28(repoDir, file2);
45088
- if (!existsSync19(path4)) continue;
45089
- const original = readFileSync17(path4);
45090
- rmSync14(path4, { force: true });
45091
- note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex2(original)}`);
45984
+ const path4 = join31(repoDir, file2);
45985
+ if (!existsSync20(path4)) continue;
45986
+ const original = readFileSync19(path4);
45987
+ rmSync16(path4, { force: true });
45988
+ note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex3(original)}`);
45092
45989
  }
45093
45990
  }
45094
45991
  const env2 = o.env;
@@ -45114,7 +46011,7 @@ async function runScanCore(ctx, deps = {}) {
45114
46011
  }
45115
46012
  const cfg = synthesizeScanConfig(spec.ecosystem);
45116
46013
  const fetcher = deps.fetcher ?? defaultScanFetcher;
45117
- const baseDir = mkdtempSync6(join29(tmpdir4(), "border-scan-"));
46014
+ const baseDir = mkdtempSync8(join32(tmpdir6(), "border-scan-"));
45118
46015
  try {
45119
46016
  const { bytes, filename } = await fetchArtifact(spec, { fetcher });
45120
46017
  const env2 = { ...ctx.env };
@@ -45125,12 +46022,12 @@ async function runScanCore(ctx, deps = {}) {
45125
46022
  const verdict = computeVerdict(findings);
45126
46023
  const report = {
45127
46024
  schemaVersion: 1,
45128
- key: sha256Hex2(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
46025
+ key: sha256Hex3(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
45129
46026
  head,
45130
46027
  dirty: false,
45131
46028
  exposureSet: [`${spec.ecosystem}:${spec.name}@${spec.version}`],
45132
46029
  refSet: [],
45133
- rulesHash: sha256Hex2(buildScanConfigYaml(spec.ecosystem)),
46030
+ rulesHash: sha256Hex3(buildScanConfigYaml(spec.ecosystem)),
45134
46031
  verdict,
45135
46032
  counts: countFindings(findings),
45136
46033
  findings,
@@ -45193,6 +46090,14 @@ function runStatus(ctx) {
45193
46090
  return EXIT_ERROR;
45194
46091
  }
45195
46092
  for (const w of ledger.warnings) ctx.stderr(`border: WARNING ${w}`);
46093
+ try {
46094
+ const drift = remoteTrackingDrift(repoDir);
46095
+ const names = drift.configured.length > 0 ? drift.configured.join(", ") : "(none configured \u2014 nothing can ever be pushed)";
46096
+ const orphanNote = drift.orphans.length > 0 ? ` \u2014 DRIFT: tracking refs without config for: ${drift.orphans.join(", ")}` : "";
46097
+ ctx.stdout(`git remotes: ${names}${orphanNote}`);
46098
+ } catch {
46099
+ ctx.stdout("git remotes: (unknown \u2014 not a git repo?)");
46100
+ }
45196
46101
  let newest;
45197
46102
  for (const r of ledger.records) if (r.t === "check") newest = r;
45198
46103
  if (newest === void 0) {
@@ -45240,11 +46145,12 @@ var handlers = {
45240
46145
  "llm-ingest": runLlmIngest,
45241
46146
  scan: runScan2,
45242
46147
  roundtrip: runRoundtrip,
45243
- opencode: runOpencode
46148
+ opencode: runOpencode,
46149
+ exfil: runExfil
45244
46150
  };
45245
46151
 
45246
46152
  // src/cli/types.ts
45247
- var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode"];
46153
+ var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode", "exfil"];
45248
46154
 
45249
46155
  // src/cli.ts
45250
46156
  var VALID_TARGETS = targetIds();
@@ -45263,6 +46169,7 @@ function usage() {
45263
46169
  " scan inspect a third-party package ([ecosystem:]name@version) for residue",
45264
46170
  " roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
45265
46171
  " opencode install|status|uninstall the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode)",
46172
+ " exfil read-only exfil-lens scan of <ref|url> [ref...]: tip tree + tag notes (--deep adds reachable-history blobs + commit messages)",
45266
46173
  "",
45267
46174
  "flags:",
45268
46175
  " --config <path> config file (default: ./border.yaml, then git-remote fallback)",
@@ -45273,11 +46180,12 @@ function usage() {
45273
46180
  " --llm include the optional LLM review layer",
45274
46181
  " --json machine-readable report on stdout",
45275
46182
  " --record / --no-record `border roundtrip` records a ledger proof by default; --no-record opts out",
46183
+ " --deep `border exfil`: also scan reachable-history blobs and the `:message` commit-message facet",
45276
46184
  " --help, -h this table",
45277
46185
  "",
45278
46186
  "exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
45279
46187
  "",
45280
- "subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode"
46188
+ "subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode, exfil"
45281
46189
  ].join("\n");
45282
46190
  }
45283
46191
  function isSubcommand(value) {
@@ -45310,6 +46218,7 @@ function parseFlags(argv) {
45310
46218
  json: { type: "boolean" },
45311
46219
  record: { type: "boolean" },
45312
46220
  "no-record": { type: "boolean" },
46221
+ deep: { type: "boolean" },
45313
46222
  help: { type: "boolean" }
45314
46223
  }
45315
46224
  });
@@ -45326,7 +46235,8 @@ function parseFlags(argv) {
45326
46235
  ...requireEngine !== void 0 ? { requireEngine: splitList(requireEngine, { flag: "--require-engine", item: "engine" }) } : {},
45327
46236
  llm: llm === true,
45328
46237
  json: json2 === true,
45329
- ...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {}
46238
+ ...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {},
46239
+ ...values.deep === true ? { deep: true } : {}
45330
46240
  };
45331
46241
  return { flags, positionals, help: values.help === true };
45332
46242
  } catch (err) {