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.
- package/README.md +14 -0
- package/dist/index.js +97 -15
- 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) =>
|
|
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
|
|
40309
|
-
|
|
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.
|
|
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
|
|
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-${
|
|
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:
|
|
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 :
|
|
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:
|
|
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:
|
|
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=${
|
|
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:
|
|
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:
|
|
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.
|
|
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": "
|
|
8
|
+
"border": "dist/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist"
|