border-customs 0.4.1 → 0.4.2

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.
Files changed (3) hide show
  1. package/README.md +14 -0
  2. package/dist/index.js +97 -15
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -675,6 +675,20 @@ same one that wrote the commit) how to run the five subcommands, how to produce
675
675
 
676
676
  ## Changelog
677
677
 
678
+ ### 0.4.2 (2026-09-12)
679
+ - Fix: `npm publish` no longer prints the misleading "bin[border] script name
680
+ dist/index.js was invalid and removed" warning — package.json now carries the
681
+ canonical bare-relative bin form npm normalizes to anyway (mechanism traced to
682
+ @npmcli/package-json publish-side; artifacts were never broken; an always-on
683
+ test locks the form).
684
+ - Change: RubyGems freshness (skip-ledger) parity compares a normalized CONTENT
685
+ digest instead of raw file bytes — `gem build` stamps the building toolchain
686
+ (RubyGems <3.6 embeds the build second in gzip headers), so raw-sha repack
687
+ parity could never be honored on those machines. The ledger digest and the
688
+ publish-time same-bytes re-hash stay RAW: what gets uploaded is still exactly
689
+ the certified bytes, and any unparseable artifact fails closed to a full
690
+ re-check.
691
+
678
692
  ### 0.4.1 (2026-09-11)
679
693
  - Add: release-coherence rule family — every version source inside a published artifact
680
694
  must agree (pyproject vs MODULE vs filename, gemspec vs gem metadata, Cargo.toml vs
package/dist/index.js CHANGED
@@ -39707,6 +39707,7 @@ import { spawnSync as spawnSync7 } from "node:child_process";
39707
39707
  import { createHash as createHash7 } from "node:crypto";
39708
39708
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
39709
39709
  import { join as join13 } from "node:path";
39710
+ import { gunzipSync as gunzipSync2 } from "node:zlib";
39710
39711
 
39711
39712
  // src/artifacts/rubygems.ts
39712
39713
  import { createHash as createHash6 } from "node:crypto";
@@ -40235,6 +40236,64 @@ function artifactTargets() {
40235
40236
  function sha256File(path4) {
40236
40237
  return createHash7("sha256").update(readFileSync8(path4)).digest("hex");
40237
40238
  }
40239
+ function octalField(block, start, end) {
40240
+ const text = block.subarray(start, end).toString("utf8").replace(/[\0 ].*$/, "");
40241
+ if (text === "") return 0;
40242
+ if (!/^[0-7]+$/.test(text)) return null;
40243
+ return Number.parseInt(text, 8);
40244
+ }
40245
+ function tarEntries(buf) {
40246
+ const magic = buf.subarray(257, 263).toString("latin1");
40247
+ if (!magic.startsWith("ustar")) return null;
40248
+ const out = [];
40249
+ let off = 0;
40250
+ while (off + 512 <= buf.length) {
40251
+ const header = buf.subarray(off, off + 512);
40252
+ if (header.every((b) => b === 0)) return out;
40253
+ const size = octalField(header, 124, 136);
40254
+ if (size === null) return null;
40255
+ const mode = octalField(header, 100, 108);
40256
+ if (mode === null) return null;
40257
+ const typeFlag = header[156] ?? 48;
40258
+ const body = off + 512;
40259
+ if (body + size > buf.length) return null;
40260
+ const name = header.subarray(0, 100).toString("utf8").replace(/\0.*$/, "");
40261
+ const linkName = header.subarray(157, 257).toString("utf8").replace(/\0.*$/, "");
40262
+ if (typeFlag !== 120 && typeFlag !== 103 && typeFlag !== 76 && typeFlag !== 75) {
40263
+ out.push({ name, member: { content: buf.subarray(body, body + size), typeFlag, mode, linkName } });
40264
+ }
40265
+ off = body + Math.ceil(size / 512) * 512;
40266
+ }
40267
+ return out;
40268
+ }
40269
+ var GEM_METADATA_STAMP_RE = /^(rubygems_version|gem_version|date):/;
40270
+ var GEM_LEGACY_EMPTY_RE = /^(autorequire|description|email|homepage|post_install_message|signing_key):[ ]*$/;
40271
+ function sha256Hex(buf) {
40272
+ return createHash7("sha256").update(buf).digest("hex");
40273
+ }
40274
+ function gemContentDigest(bytes) {
40275
+ const outer = tarEntries(bytes);
40276
+ if (outer === null) return null;
40277
+ const meta3 = outer.find((e) => e.name === "metadata.gz")?.member;
40278
+ const data = outer.find((e) => e.name === "data.tar.gz")?.member;
40279
+ if (meta3 === void 0 || data === void 0) return null;
40280
+ let metadata;
40281
+ let inner;
40282
+ try {
40283
+ metadata = gunzipSync2(meta3.content).toString("utf8");
40284
+ inner = gunzipSync2(data.content);
40285
+ } catch {
40286
+ return null;
40287
+ }
40288
+ const innerEntries = tarEntries(inner);
40289
+ if (innerEntries === null) return null;
40290
+ const chain = createHash7("sha256");
40291
+ for (const { name, member: m } of innerEntries) {
40292
+ chain.update(name).update("\0").update(String(m.typeFlag)).update("\0").update(String(m.mode & 73)).update("\0").update(m.linkName).update("\0").update(sha256Hex(m.content)).update("\0");
40293
+ }
40294
+ const metaStripped = metadata.split("\n").filter((line) => !GEM_METADATA_STAMP_RE.test(line) && !GEM_LEGACY_EMPTY_RE.test(line)).join("\n");
40295
+ return createHash7("sha256").update("border-gem-content-v1\0").update(chain.digest("hex")).update("\0").update(metaStripped).digest("hex");
40296
+ }
40238
40297
  var packSeq = 0;
40239
40298
  function packNpmArtifacts(repoDir, o = {}) {
40240
40299
  packSeq += 1;
@@ -40292,7 +40351,10 @@ function packRubygemsArtifacts(repoDir, o = {}) {
40292
40351
  if (r.error !== void 0 || r.status !== 0) return null;
40293
40352
  const files = readdirSync4(dest).filter((f) => f.endsWith(".gem")).sort();
40294
40353
  if (files.length === 0) return null;
40295
- return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
40354
+ return files.map((f) => {
40355
+ const raw = readFileSync8(join13(dest, f));
40356
+ return { file: f, sha256: sha256Hex(raw), contentDigest: gemContentDigest(raw) };
40357
+ });
40296
40358
  } finally {
40297
40359
  rmSync5(dest, { recursive: true, force: true });
40298
40360
  }
@@ -40305,11 +40367,30 @@ var REPACKERS = {
40305
40367
  function repackChannels() {
40306
40368
  return publishChannels().filter((c) => c.freshness === "repack").map((c) => c.id);
40307
40369
  }
40308
- function sameArtifacts(a, b, extensions) {
40309
- const digests = (xs) => xs.filter((x) => extensions.some((ext) => x.file.slice(x.file.lastIndexOf("/") + 1).endsWith(ext))).map((x) => `${x.file.slice(x.file.lastIndexOf("/") + 1)}\0${x.sha256}`).sort();
40370
+ function gemParity(entry, repoDir) {
40371
+ if (entry.contentDigest !== void 0) return entry.contentDigest;
40372
+ try {
40373
+ return gemContentDigest(readFileSync8(join13(repoDir, entry.file)));
40374
+ } catch {
40375
+ return null;
40376
+ }
40377
+ }
40378
+ function sameArtifacts(a, b, extensions, repoDir) {
40379
+ const digests = (xs) => {
40380
+ const out = [];
40381
+ for (const x of xs) {
40382
+ const base = x.file.slice(x.file.lastIndexOf("/") + 1);
40383
+ const ext = extensions.find((e) => base.endsWith(e));
40384
+ if (ext === void 0) continue;
40385
+ const parity = ext === ".gem" ? gemParity(x, repoDir) : x.sha256;
40386
+ if (parity === null) return null;
40387
+ out.push(`${base}\0${parity}`);
40388
+ }
40389
+ return out.sort();
40390
+ };
40310
40391
  const ka = digests(a);
40311
40392
  const kb = digests(b);
40312
- return ka.length === kb.length && ka.every((x, i) => x === kb[i]);
40393
+ return ka !== null && kb !== null && ka.length === kb.length && ka.every((x, i) => x === kb[i]);
40313
40394
  }
40314
40395
  function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
40315
40396
  if (!record2.effectiveTargets.some((t) => artifactTargets().includes(t))) return true;
@@ -40331,7 +40412,8 @@ function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
40331
40412
  needed.flatMap((id) => {
40332
40413
  const channel = publishChannels().find((c) => c.id === id);
40333
40414
  return channel?.artifactExtensions ?? [];
40334
- })
40415
+ }),
40416
+ repoDir
40335
40417
  );
40336
40418
  }
40337
40419
 
@@ -43893,7 +43975,7 @@ import { createHash as createHash15 } from "node:crypto";
43893
43975
  import { basename as basename5 } from "node:path";
43894
43976
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
43895
43977
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
43896
- var SCAN_USER_AGENT = "border-customs/0.4.1 (+https://github.com/sumteclab/border; border scan)";
43978
+ var SCAN_USER_AGENT = "border-customs/0.4.2 (+https://github.com/sumteclab/border; border scan)";
43897
43979
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
43898
43980
  var defaultScanFetcher = async (url2, init) => {
43899
43981
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -44018,7 +44100,7 @@ async function fetchArtifact(spec, o) {
44018
44100
  }
44019
44101
  }
44020
44102
  }
44021
- function sha256Hex(data) {
44103
+ function sha256Hex2(data) {
44022
44104
  return createHash15("sha256").update(data).digest("hex");
44023
44105
  }
44024
44106
 
@@ -44470,7 +44552,7 @@ function classifyResidue(label, diff, attribution = null) {
44470
44552
 
44471
44553
  // src/roundtrip/orchestrate.ts
44472
44554
  function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
44473
- return `rt-${sha256Hex(label).slice(0, 8)}-${rand}`;
44555
+ return `rt-${sha256Hex2(label).slice(0, 8)}-${rand}`;
44474
44556
  }
44475
44557
  var shArgv = (name) => {
44476
44558
  if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
@@ -44591,7 +44673,7 @@ function runPipeline2(exec, profile, spec, label, hostPath, container, closure,
44591
44673
  return {
44592
44674
  findings: classifyResidue(label, diffManifests(m1, m3), attribution),
44593
44675
  installDelta,
44594
- baselineDigest: sha256Hex([...m1.keys()].join("\n"))
44676
+ baselineDigest: sha256Hex2([...m1.keys()].join("\n"))
44595
44677
  };
44596
44678
  } finally {
44597
44679
  try {
@@ -44629,7 +44711,7 @@ async function runRoundtripCore(ctx, deps = {}) {
44629
44711
  try {
44630
44712
  const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
44631
44713
  const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
44632
- const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex(bytes);
44714
+ const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex2(bytes);
44633
44715
  const hostPath = join26(baseDir, "artifact");
44634
44716
  writeFileSync7(hostPath, bytes);
44635
44717
  const container = containerNameFor(label);
@@ -44638,12 +44720,12 @@ async function runRoundtripCore(ctx, deps = {}) {
44638
44720
  const verdict = computeVerdict(findings);
44639
44721
  const report = {
44640
44722
  schemaVersion: 1,
44641
- key: sha256Hex(`roundtrip:${label}:${baselineDigest}:${verdict}`),
44723
+ key: sha256Hex2(`roundtrip:${label}:${baselineDigest}:${verdict}`),
44642
44724
  head: baselineDigest.slice(0, 40),
44643
44725
  dirty: false,
44644
44726
  exposureSet: [label],
44645
44727
  refSet: [],
44646
- rulesHash: sha256Hex(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
44728
+ rulesHash: sha256Hex2(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
44647
44729
  verdict,
44648
44730
  counts: countFindings(findings),
44649
44731
  findings,
@@ -44819,7 +44901,7 @@ function materializePackage(o) {
44819
44901
  if (!existsSync18(path4)) continue;
44820
44902
  const original = readFileSync16(path4);
44821
44903
  rmSync13(path4, { force: true });
44822
- note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex(original)}`);
44904
+ note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex2(original)}`);
44823
44905
  }
44824
44906
  }
44825
44907
  const env2 = o.env;
@@ -44856,12 +44938,12 @@ async function runScanCore(ctx, deps = {}) {
44856
44938
  const verdict = computeVerdict(findings);
44857
44939
  const report = {
44858
44940
  schemaVersion: 1,
44859
- key: sha256Hex(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
44941
+ key: sha256Hex2(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
44860
44942
  head,
44861
44943
  dirty: false,
44862
44944
  exposureSet: [`${spec.ecosystem}:${spec.name}@${spec.version}`],
44863
44945
  refSet: [],
44864
- rulesHash: sha256Hex(buildScanConfigYaml(spec.ecosystem)),
44946
+ rulesHash: sha256Hex2(buildScanConfigYaml(spec.ecosystem)),
44865
44947
  verdict,
44866
44948
  counts: countFindings(findings),
44867
44949
  findings,
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "//": "publint is a runtime dep (exact pin): the npm-stage engine is invoked via bin resolution against the installed layout, so consumers get the publint-fail leg out of the box; the dev suite resolves the same node_modules/.bin/publint — no devDependencies entry is needed for it to stay green.",
5
5
  "description": "Fail-closed git push gate: secret scanning + supply-chain checks before any remote leaves the machine",
6
6
  "type": "module",
7
7
  "bin": {
8
- "border": "./dist/index.js"
8
+ "border": "dist/index.js"
9
9
  },
10
10
  "files": [
11
11
  "dist"