border-customs 0.4.2 → 0.5.0
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 +51 -0
- package/dist/assets/package-lock.json +371 -3
- package/dist/index.js +260 -71
- package/package.json +9 -2
- package/plugin/border-command.md +17 -0
- package/plugin/border.ts +239 -0
package/dist/index.js
CHANGED
|
@@ -13029,7 +13029,7 @@ var require_micromatch = __commonJS({
|
|
|
13029
13029
|
|
|
13030
13030
|
// src/index.ts
|
|
13031
13031
|
import { realpathSync } from "node:fs";
|
|
13032
|
-
import { fileURLToPath as
|
|
13032
|
+
import { fileURLToPath as fileURLToPath6, pathToFileURL } from "node:url";
|
|
13033
13033
|
|
|
13034
13034
|
// src/cli.ts
|
|
13035
13035
|
import { parseArgs } from "node:util";
|
|
@@ -37940,12 +37940,12 @@ function packOnce(o) {
|
|
|
37940
37940
|
throw new EngineRunError(`npm pack reported '${report.filename}' but no tarball landed in ${o.distDir}`, null);
|
|
37941
37941
|
}
|
|
37942
37942
|
const data = readFileSync3(tarballAbs);
|
|
37943
|
-
const
|
|
37943
|
+
const sha2564 = createHash3("sha256").update(data).digest("hex");
|
|
37944
37944
|
const sha1 = createHash3("sha1").update(data).digest("hex");
|
|
37945
37945
|
if (sha1 !== report.shasum || data.byteLength !== report.size) {
|
|
37946
37946
|
throw new EngineRunError(`npm pack report disagrees with the tarball bytes for ${report.filename} \u2014 corrupted or swapped artifact, failing closed`, null);
|
|
37947
37947
|
}
|
|
37948
|
-
return { filename: report.filename, sha256:
|
|
37948
|
+
return { filename: report.filename, sha256: sha2564, bytes: data.byteLength, tarballAbs };
|
|
37949
37949
|
}
|
|
37950
37950
|
function publintFinding(o, tarballAbs, identity) {
|
|
37951
37951
|
const candidates = o.publintBinPath !== void 0 ? [o.publintBinPath] : [
|
|
@@ -38950,7 +38950,7 @@ async function runNpmArtifactStage(o) {
|
|
|
38950
38950
|
const stateDir = join10(repoDir, BORDER_STATE_DIR);
|
|
38951
38951
|
const distDir = join10(stateDir, "dist");
|
|
38952
38952
|
mkdirSync3(distDir, { recursive: true });
|
|
38953
|
-
const { filename, sha256:
|
|
38953
|
+
const { filename, sha256: sha2564, bytes, tarballAbs } = packOnce({
|
|
38954
38954
|
repoDir,
|
|
38955
38955
|
distDir,
|
|
38956
38956
|
...o.env !== void 0 ? { env: o.env } : {},
|
|
@@ -38958,7 +38958,7 @@ async function runNpmArtifactStage(o) {
|
|
|
38958
38958
|
...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
|
|
38959
38959
|
});
|
|
38960
38960
|
const findings = [];
|
|
38961
|
-
const extractDir = join10(stateDir, "tmp",
|
|
38961
|
+
const extractDir = join10(stateDir, "tmp", sha2564.slice(0, 8));
|
|
38962
38962
|
rmSync3(extractDir, { recursive: true, force: true });
|
|
38963
38963
|
mkdirSync3(extractDir, { recursive: true });
|
|
38964
38964
|
try {
|
|
@@ -39063,7 +39063,7 @@ async function runNpmArtifactStage(o) {
|
|
|
39063
39063
|
if (publint !== null) findings.push(publint);
|
|
39064
39064
|
return {
|
|
39065
39065
|
findings,
|
|
39066
|
-
artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256:
|
|
39066
|
+
artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256: sha2564, bytes }
|
|
39067
39067
|
};
|
|
39068
39068
|
}
|
|
39069
39069
|
|
|
@@ -40145,9 +40145,9 @@ async function runRubygemsArtifactStage(o) {
|
|
|
40145
40145
|
throw new EngineRunError(`gem build produced no ${gemName} in ${relative3(repoDir, distDir)} \u2014 failing closed (was the gemspec's literal name/version re-read?)`, null);
|
|
40146
40146
|
}
|
|
40147
40147
|
const bytes = readFileSync7(dist);
|
|
40148
|
-
const
|
|
40148
|
+
const sha2564 = createHash6("sha256").update(bytes).digest("hex");
|
|
40149
40149
|
const findings = [];
|
|
40150
|
-
const extractDir = join12(stateDir, "tmp",
|
|
40150
|
+
const extractDir = join12(stateDir, "tmp", sha2564.slice(0, 8));
|
|
40151
40151
|
rmSync4(extractDir, { recursive: true, force: true });
|
|
40152
40152
|
mkdirSync4(extractDir, { recursive: true });
|
|
40153
40153
|
const dataDir = `${extractDir}-data`;
|
|
@@ -40225,7 +40225,7 @@ async function runRubygemsArtifactStage(o) {
|
|
|
40225
40225
|
}
|
|
40226
40226
|
return {
|
|
40227
40227
|
findings,
|
|
40228
|
-
artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256:
|
|
40228
|
+
artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2564, bytes: bytes.length }
|
|
40229
40229
|
};
|
|
40230
40230
|
}
|
|
40231
40231
|
|
|
@@ -40891,17 +40891,17 @@ function requirePassedRecord(kind, i) {
|
|
|
40891
40891
|
function rehashRecordedArtifacts(kind, extensions, record2, repoDir) {
|
|
40892
40892
|
const conventions = extensions;
|
|
40893
40893
|
const entries = (record2.artifacts ?? []).filter((a) => conventions.some((ext) => a.file.endsWith(ext)));
|
|
40894
|
-
const
|
|
40894
|
+
const fail4 = (detail) => ({ ok: false, message: `artifact changed since check \u2014 ${detail}` });
|
|
40895
40895
|
if (entries.length === 0) {
|
|
40896
|
-
return
|
|
40896
|
+
return fail4(`the PASSED check recorded no ${kind} artifacts (${conventions.join("/")}) to re-verify`);
|
|
40897
40897
|
}
|
|
40898
40898
|
const files = [];
|
|
40899
40899
|
for (const a of entries) {
|
|
40900
40900
|
const rel = join17(DIST_DIR, basename4(a.file));
|
|
40901
40901
|
const abs = join17(repoDir, rel);
|
|
40902
|
-
if (!existsSync11(abs)) return
|
|
40902
|
+
if (!existsSync11(abs)) return fail4(`${rel} is missing from the working tree`);
|
|
40903
40903
|
const digest2 = createHash10("sha256").update(readFileSync10(abs)).digest("hex");
|
|
40904
|
-
if (digest2 !== a.sha256) return
|
|
40904
|
+
if (digest2 !== a.sha256) return fail4(`${rel} no longer hashes to the digest recorded by the check`);
|
|
40905
40905
|
files.push(rel);
|
|
40906
40906
|
}
|
|
40907
40907
|
return { ok: true, files: files.sort() };
|
|
@@ -41360,7 +41360,7 @@ function npmExposureCoords(cfg, read) {
|
|
|
41360
41360
|
return { name, version: version2 };
|
|
41361
41361
|
}
|
|
41362
41362
|
function classifyNpmVersionView(o) {
|
|
41363
|
-
const
|
|
41363
|
+
const fail4 = (why) => {
|
|
41364
41364
|
throw new EngineRunError(
|
|
41365
41365
|
`npm registry probe inconclusive (${why}); refusing to assume the version is absent \u2014 registry unreachable or returned unexpected output, push blocked until the probe succeeds`,
|
|
41366
41366
|
o.status
|
|
@@ -41368,19 +41368,19 @@ function classifyNpmVersionView(o) {
|
|
|
41368
41368
|
};
|
|
41369
41369
|
if (o.status === 0) {
|
|
41370
41370
|
const text = o.stdout.trim();
|
|
41371
|
-
if (text.length === 0) return
|
|
41371
|
+
if (text.length === 0) return fail4("exit 0 with empty stdout");
|
|
41372
41372
|
let body;
|
|
41373
41373
|
try {
|
|
41374
41374
|
body = JSON.parse(text);
|
|
41375
41375
|
} catch {
|
|
41376
|
-
return
|
|
41376
|
+
return fail4("exit 0 with malformed JSON body");
|
|
41377
41377
|
}
|
|
41378
|
-
if (typeof body !== "object" || body === null) return
|
|
41378
|
+
if (typeof body !== "object" || body === null) return fail4("exit 0 with non-object JSON body");
|
|
41379
41379
|
return "present";
|
|
41380
41380
|
}
|
|
41381
41381
|
if (/code E404\b/.test(o.stderr) || /\b404 Not Found\b/.test(o.stderr)) return "absent";
|
|
41382
41382
|
const m = /code (E[A-Z0-9_]+|ERR_[A-Z0-9_]+)/.exec(o.stderr);
|
|
41383
|
-
return
|
|
41383
|
+
return fail4(m === null ? `exit ${String(o.status)} without a 404 marker` : `npm error ${m[1]}`);
|
|
41384
41384
|
}
|
|
41385
41385
|
function npmNameClaimed(o) {
|
|
41386
41386
|
if (o.status === 0) return true;
|
|
@@ -41787,9 +41787,9 @@ async function runCargoArtifactStage(o) {
|
|
|
41787
41787
|
const dist = join18(distDir, crateName);
|
|
41788
41788
|
renameSync(packed, dist);
|
|
41789
41789
|
const bytes = readFileSync11(dist);
|
|
41790
|
-
const
|
|
41790
|
+
const sha2564 = createHash11("sha256").update(bytes).digest("hex");
|
|
41791
41791
|
const findings = [];
|
|
41792
|
-
const extractDir = join18(stateDir, "tmp",
|
|
41792
|
+
const extractDir = join18(stateDir, "tmp", sha2564.slice(0, 8));
|
|
41793
41793
|
rmSync8(extractDir, { recursive: true, force: true });
|
|
41794
41794
|
mkdirSync8(extractDir, { recursive: true });
|
|
41795
41795
|
try {
|
|
@@ -41842,7 +41842,7 @@ async function runCargoArtifactStage(o) {
|
|
|
41842
41842
|
}
|
|
41843
41843
|
return {
|
|
41844
41844
|
findings,
|
|
41845
|
-
artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256:
|
|
41845
|
+
artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256: sha2564, bytes: bytes.length }
|
|
41846
41846
|
};
|
|
41847
41847
|
} finally {
|
|
41848
41848
|
rmSync8(targetDir, { recursive: true, force: true });
|
|
@@ -43700,6 +43700,193 @@ async function runLlmRequest(ctx) {
|
|
|
43700
43700
|
return runLlmRequestCore(ctx);
|
|
43701
43701
|
}
|
|
43702
43702
|
|
|
43703
|
+
// 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";
|
|
43706
|
+
import os3 from "node:os";
|
|
43707
|
+
import { dirname as dirname12, join as join26 } from "node:path";
|
|
43708
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
43709
|
+
var PLUGIN_MARKER = "// border-opencode-plugin v";
|
|
43710
|
+
var COMMAND_MARKER = "<!-- border-opencode-command -->";
|
|
43711
|
+
function opencodeConfigRoot(env2) {
|
|
43712
|
+
const xdg = env2["XDG_CONFIG_HOME"] ?? "";
|
|
43713
|
+
if (xdg.length > 0) return xdg;
|
|
43714
|
+
const home = env2["HOME"] ?? "";
|
|
43715
|
+
return join26(home.length > 0 ? home : os3.homedir(), ".config");
|
|
43716
|
+
}
|
|
43717
|
+
function resolvePluginAsset(moduleUrl, fileName) {
|
|
43718
|
+
const here = dirname12(fileURLToPath5(moduleUrl));
|
|
43719
|
+
const candidates = [
|
|
43720
|
+
join26(here, "..", "plugin", fileName),
|
|
43721
|
+
// dist mode (bundled): <root>/plugin/<f>
|
|
43722
|
+
join26(here, "..", "..", "plugin", fileName)
|
|
43723
|
+
// src mode: <root>/plugin/<f>
|
|
43724
|
+
];
|
|
43725
|
+
return candidates.find((p) => existsSync17(p)) ?? candidates[1];
|
|
43726
|
+
}
|
|
43727
|
+
function targetsFor(env2) {
|
|
43728
|
+
const root = join26(opencodeConfigRoot(env2), "opencode");
|
|
43729
|
+
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 }
|
|
43732
|
+
];
|
|
43733
|
+
}
|
|
43734
|
+
function sha2563(bytes) {
|
|
43735
|
+
return createHash15("sha256").update(bytes).digest("hex");
|
|
43736
|
+
}
|
|
43737
|
+
function firstLine(bytes) {
|
|
43738
|
+
const text = bytes.toString("utf8");
|
|
43739
|
+
const newline = text.indexOf("\n");
|
|
43740
|
+
return newline < 0 ? text : text.slice(0, newline);
|
|
43741
|
+
}
|
|
43742
|
+
function carriesMarker(bytes, marker) {
|
|
43743
|
+
return firstLine(bytes).startsWith(marker);
|
|
43744
|
+
}
|
|
43745
|
+
function fail(ctx, message) {
|
|
43746
|
+
ctx.stderr(`border: opencode: ${message}`);
|
|
43747
|
+
return EXIT_ERROR;
|
|
43748
|
+
}
|
|
43749
|
+
function readPackagedAsset(ctx, fileName) {
|
|
43750
|
+
const source = resolvePluginAsset(import.meta.url, fileName);
|
|
43751
|
+
try {
|
|
43752
|
+
return readFileSync16(source);
|
|
43753
|
+
} catch {
|
|
43754
|
+
ctx.stderr(`border: opencode: packaged asset is missing (${source})`);
|
|
43755
|
+
ctx.stderr("border: opencode: the border install looks broken \u2014 `npm i -g border-customs` or a fresh checkout should carry plugin/.");
|
|
43756
|
+
return null;
|
|
43757
|
+
}
|
|
43758
|
+
}
|
|
43759
|
+
function readTarget(destination) {
|
|
43760
|
+
try {
|
|
43761
|
+
return readFileSync16(destination);
|
|
43762
|
+
} catch {
|
|
43763
|
+
return null;
|
|
43764
|
+
}
|
|
43765
|
+
}
|
|
43766
|
+
function requirePlainDestination(ctx, destination) {
|
|
43767
|
+
let shape;
|
|
43768
|
+
try {
|
|
43769
|
+
shape = lstatSync(destination);
|
|
43770
|
+
} catch {
|
|
43771
|
+
return null;
|
|
43772
|
+
}
|
|
43773
|
+
if (!shape.isFile() || shape.isSymbolicLink()) {
|
|
43774
|
+
return fail(
|
|
43775
|
+
ctx,
|
|
43776
|
+
`refusing to touch ${destination} \u2014 the path exists but is not a plain regular file (directories and symlinks are never followed, entered, or destroyed)`
|
|
43777
|
+
);
|
|
43778
|
+
}
|
|
43779
|
+
return null;
|
|
43780
|
+
}
|
|
43781
|
+
function classify(target, current, packaged) {
|
|
43782
|
+
if (current === null) return "absent";
|
|
43783
|
+
if (!carriesMarker(current, target.marker)) return "foreign";
|
|
43784
|
+
return current.equals(packaged) ? "up-to-date" : "outdated";
|
|
43785
|
+
}
|
|
43786
|
+
function requireNoForeign(ctx, targets) {
|
|
43787
|
+
for (const target of targets) {
|
|
43788
|
+
const shape = requirePlainDestination(ctx, target.destination);
|
|
43789
|
+
if (shape !== null) return shape;
|
|
43790
|
+
const current = readTarget(target.destination);
|
|
43791
|
+
if (current !== null && !carriesMarker(current, target.marker)) {
|
|
43792
|
+
return fail(
|
|
43793
|
+
ctx,
|
|
43794
|
+
`refusing to touch ${target.destination} \u2014 the file exists but is not ours (its first line lacks the marker '${target.marker}'); move it aside manually, nothing was written anywhere`
|
|
43795
|
+
);
|
|
43796
|
+
}
|
|
43797
|
+
}
|
|
43798
|
+
return null;
|
|
43799
|
+
}
|
|
43800
|
+
function prepareTargets(ctx, targets) {
|
|
43801
|
+
const prepared = [];
|
|
43802
|
+
for (const target of targets) {
|
|
43803
|
+
const packaged = readPackagedAsset(ctx, target.fileName);
|
|
43804
|
+
if (packaged === null) return null;
|
|
43805
|
+
prepared.push({ target, packaged });
|
|
43806
|
+
}
|
|
43807
|
+
return prepared;
|
|
43808
|
+
}
|
|
43809
|
+
function opencodeInstall(ctx) {
|
|
43810
|
+
const targets = targetsFor(ctx.env);
|
|
43811
|
+
const prepared = prepareTargets(ctx, targets);
|
|
43812
|
+
if (prepared === null) return EXIT_ERROR;
|
|
43813
|
+
const blocked = requireNoForeign(ctx, targets);
|
|
43814
|
+
if (blocked !== null) return blocked;
|
|
43815
|
+
for (const { target, packaged } of prepared) {
|
|
43816
|
+
const current = readTarget(target.destination);
|
|
43817
|
+
const state = classify(target, current, packaged);
|
|
43818
|
+
if (state === "foreign") {
|
|
43819
|
+
return fail(
|
|
43820
|
+
ctx,
|
|
43821
|
+
`refusing to touch ${target.destination} \u2014 the file exists but is not ours (its first line lacks the marker '${target.marker}'); move it aside manually, nothing was written anywhere`
|
|
43822
|
+
);
|
|
43823
|
+
}
|
|
43824
|
+
if (state === "up-to-date") {
|
|
43825
|
+
ctx.stdout(`${target.destination}: up to date`);
|
|
43826
|
+
continue;
|
|
43827
|
+
}
|
|
43828
|
+
mkdirSync11(dirname12(target.destination), { recursive: true });
|
|
43829
|
+
writeFileSync7(target.destination, packaged);
|
|
43830
|
+
ctx.stdout(`${target.destination}: ${state === "outdated" ? "updated" : "installed"}`);
|
|
43831
|
+
}
|
|
43832
|
+
ctx.stdout("note: restart opencode to load the plugin \u2014 tools and commands are scanned at startup.");
|
|
43833
|
+
return EXIT_PASS;
|
|
43834
|
+
}
|
|
43835
|
+
function opencodeStatus(ctx) {
|
|
43836
|
+
const targets = targetsFor(ctx.env);
|
|
43837
|
+
const prepared = prepareTargets(ctx, targets);
|
|
43838
|
+
if (prepared === null) return EXIT_ERROR;
|
|
43839
|
+
for (const { target, packaged } of prepared) {
|
|
43840
|
+
const current = readTarget(target.destination);
|
|
43841
|
+
const state = classify(target, current, packaged);
|
|
43842
|
+
const installedSha = current === null ? "-" : sha2563(current);
|
|
43843
|
+
ctx.stdout(`${target.destination}: ${state} installed=${installedSha} packaged=${sha2563(packaged)}`);
|
|
43844
|
+
}
|
|
43845
|
+
return EXIT_PASS;
|
|
43846
|
+
}
|
|
43847
|
+
function opencodeUninstall(ctx) {
|
|
43848
|
+
const targets = targetsFor(ctx.env);
|
|
43849
|
+
const blocked = requireNoForeign(ctx, targets);
|
|
43850
|
+
if (blocked !== null) return blocked;
|
|
43851
|
+
for (const target of targets) {
|
|
43852
|
+
const current = readTarget(target.destination);
|
|
43853
|
+
if (current === null) {
|
|
43854
|
+
ctx.stdout(`${target.destination}: absent (nothing to remove)`);
|
|
43855
|
+
continue;
|
|
43856
|
+
}
|
|
43857
|
+
if (!carriesMarker(current, target.marker)) {
|
|
43858
|
+
return fail(
|
|
43859
|
+
ctx,
|
|
43860
|
+
`refusing to remove ${target.destination} \u2014 the file is not ours (its first line lacks the marker '${target.marker}'); nothing was removed anywhere`
|
|
43861
|
+
);
|
|
43862
|
+
}
|
|
43863
|
+
rmSync12(target.destination);
|
|
43864
|
+
ctx.stdout(`${target.destination}: removed`);
|
|
43865
|
+
}
|
|
43866
|
+
ctx.stdout("note: restart opencode for the tool and command to disappear.");
|
|
43867
|
+
return EXIT_PASS;
|
|
43868
|
+
}
|
|
43869
|
+
function runOpencode(ctx) {
|
|
43870
|
+
const [sub, ...rest] = ctx.positionals;
|
|
43871
|
+
if (rest.length > 0) {
|
|
43872
|
+
throw new UnknownArgError(
|
|
43873
|
+
`opencode ${sub ?? "<none>"}: unexpected argument '${rest[0] ?? ""}' \u2014 usage: border opencode install | status | uninstall`
|
|
43874
|
+
);
|
|
43875
|
+
}
|
|
43876
|
+
switch (sub) {
|
|
43877
|
+
case "install":
|
|
43878
|
+
return opencodeInstall(ctx);
|
|
43879
|
+
case "status":
|
|
43880
|
+
return opencodeStatus(ctx);
|
|
43881
|
+
case "uninstall":
|
|
43882
|
+
return opencodeUninstall(ctx);
|
|
43883
|
+
default:
|
|
43884
|
+
throw new UnknownArgError(
|
|
43885
|
+
`opencode: unknown subcommand '${sub ?? "<none>"}' \u2014 usage: border opencode install | status | uninstall`
|
|
43886
|
+
);
|
|
43887
|
+
}
|
|
43888
|
+
}
|
|
43889
|
+
|
|
43703
43890
|
// src/push/git.ts
|
|
43704
43891
|
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
43705
43892
|
var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
|
|
@@ -43965,17 +44152,17 @@ async function executeYesPush(ctx, loaded) {
|
|
|
43965
44152
|
}
|
|
43966
44153
|
|
|
43967
44154
|
// src/roundtrip/orchestrate.ts
|
|
43968
|
-
import { mkdtempSync as mkdtempSync5, rmSync as
|
|
44155
|
+
import { mkdtempSync as mkdtempSync5, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
43969
44156
|
import { randomBytes } from "node:crypto";
|
|
43970
44157
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43971
|
-
import { join as
|
|
44158
|
+
import { join as join27 } from "node:path";
|
|
43972
44159
|
|
|
43973
44160
|
// src/scan/fetch.ts
|
|
43974
|
-
import { createHash as
|
|
44161
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
43975
44162
|
import { basename as basename5 } from "node:path";
|
|
43976
44163
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
43977
44164
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
43978
|
-
var SCAN_USER_AGENT = "border-customs/0.
|
|
44165
|
+
var SCAN_USER_AGENT = "border-customs/0.5.0 (+https://github.com/sumteclab/border; border scan)";
|
|
43979
44166
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
43980
44167
|
var defaultScanFetcher = async (url2, init) => {
|
|
43981
44168
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
|
@@ -44000,19 +44187,19 @@ var defaultScanFetcher = async (url2, init) => {
|
|
|
44000
44187
|
}
|
|
44001
44188
|
};
|
|
44002
44189
|
};
|
|
44003
|
-
var
|
|
44190
|
+
var fail2 = (why) => {
|
|
44004
44191
|
throw new EngineRunError(`border scan could not fetch the artifact (${why}) \u2014 refusing to scan a partial or absent download (fail-closed)`, null);
|
|
44005
44192
|
};
|
|
44006
44193
|
async function readCapped(res, url2, cap = SCAN_MAX_ARTIFACT_BYTES) {
|
|
44007
|
-
if (res.status !== 200)
|
|
44194
|
+
if (res.status !== 200) fail2(`HTTP ${String(res.status)} for ${url2}`);
|
|
44008
44195
|
if (res.contentLength !== null && res.contentLength > cap) {
|
|
44009
|
-
|
|
44196
|
+
fail2(`${url2} announces ${String(res.contentLength)} bytes, over the 200 MiB scan size cap`);
|
|
44010
44197
|
}
|
|
44011
44198
|
const parts = [];
|
|
44012
44199
|
let total = 0;
|
|
44013
44200
|
for await (const chunk of res.chunks()) {
|
|
44014
44201
|
total += chunk.byteLength;
|
|
44015
|
-
if (total > cap)
|
|
44202
|
+
if (total > cap) fail2(`${url2} streams past the 200 MiB scan size cap after ${String(total)} bytes`);
|
|
44016
44203
|
parts.push(Buffer.from(chunk));
|
|
44017
44204
|
}
|
|
44018
44205
|
return Buffer.concat(parts);
|
|
@@ -44026,7 +44213,7 @@ async function fetchJson(fetcher, url2, timeoutMs) {
|
|
|
44026
44213
|
try {
|
|
44027
44214
|
return JSON.parse(body.toString("utf8"));
|
|
44028
44215
|
} catch {
|
|
44029
|
-
|
|
44216
|
+
fail2(`metadata response from ${url2} is not valid JSON`);
|
|
44030
44217
|
}
|
|
44031
44218
|
}
|
|
44032
44219
|
var isRecord2 = (v) => typeof v === "object" && v !== null;
|
|
@@ -44062,11 +44249,11 @@ async function fetchArtifact(spec, o) {
|
|
|
44062
44249
|
const meta3 = await fetchJson(o.fetcher, npmMetadataUrl(spec.name, spec.version), timeoutMs);
|
|
44063
44250
|
const dist = isRecord2(meta3) && isRecord2(meta3["dist"]) ? meta3["dist"] : {};
|
|
44064
44251
|
const tarball = dist["tarball"];
|
|
44065
|
-
if (typeof tarball !== "string" || tarball.length === 0)
|
|
44066
|
-
if (!/^https?:\/\//.test(tarball))
|
|
44252
|
+
if (typeof tarball !== "string" || tarball.length === 0) fail2(`npm metadata for ${label} has no dist.tarball`);
|
|
44253
|
+
if (!/^https?:\/\//.test(tarball)) fail2(`npm dist.tarball for ${label} is not an http(s) URL`);
|
|
44067
44254
|
const bytes = await fetchBytes(o.fetcher, tarball, timeoutMs);
|
|
44068
44255
|
const filename = safeArtifactName(tarball, `${spec.name.replace("@", "").replace("/", "-")}-${spec.version}.tgz`);
|
|
44069
|
-
if (!filename.endsWith(".tgz") && !filename.endsWith(".tar.gz"))
|
|
44256
|
+
if (!filename.endsWith(".tgz") && !filename.endsWith(".tar.gz")) fail2(`npm tarball ${filename} is not a .tgz/.tar.gz`);
|
|
44070
44257
|
return { bytes, filename };
|
|
44071
44258
|
}
|
|
44072
44259
|
case "pypi": {
|
|
@@ -44076,11 +44263,11 @@ async function fetchArtifact(spec, o) {
|
|
|
44076
44263
|
const sdists = files.filter((f) => f["packagetype"] === "sdist");
|
|
44077
44264
|
const sdist = sdists.find((f) => typeof f["filename"] === "string" && f["filename"].endsWith(".tar.gz"));
|
|
44078
44265
|
if (sdist === void 0) {
|
|
44079
|
-
|
|
44266
|
+
fail2(`no .tar.gz sdist published for ${label} \u2014 wheels are pre-built artifacts and out of scan scope (setup code lives in the sdist)`);
|
|
44080
44267
|
}
|
|
44081
44268
|
const url2 = sdist["url"];
|
|
44082
44269
|
const name = sdist["filename"];
|
|
44083
|
-
if (typeof url2 !== "string" || typeof name !== "string")
|
|
44270
|
+
if (typeof url2 !== "string" || typeof name !== "string") fail2(`PyPI sdist entry for ${label} is malformed`);
|
|
44084
44271
|
const bytes = await fetchBytes(o.fetcher, url2, timeoutMs);
|
|
44085
44272
|
return { bytes, filename: safeArtifactName(url2, name) };
|
|
44086
44273
|
}
|
|
@@ -44101,7 +44288,7 @@ async function fetchArtifact(spec, o) {
|
|
|
44101
44288
|
}
|
|
44102
44289
|
}
|
|
44103
44290
|
function sha256Hex2(data) {
|
|
44104
|
-
return
|
|
44291
|
+
return createHash16("sha256").update(data).digest("hex");
|
|
44105
44292
|
}
|
|
44106
44293
|
|
|
44107
44294
|
// src/scan/spec.ts
|
|
@@ -44144,8 +44331,8 @@ function parseScanSpec(raw) {
|
|
|
44144
44331
|
}
|
|
44145
44332
|
|
|
44146
44333
|
// src/roundtrip/localArtifact.ts
|
|
44147
|
-
import { createReadStream, existsSync as
|
|
44148
|
-
import { createHash as
|
|
44334
|
+
import { createReadStream, existsSync as existsSync18, statSync as statSync2 } from "node:fs";
|
|
44335
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
44149
44336
|
import { basename as basename6, resolve as resolve15 } from "node:path";
|
|
44150
44337
|
var EXT_TABLE = /* @__PURE__ */ new Map([
|
|
44151
44338
|
[".whl", "pypi"],
|
|
@@ -44193,7 +44380,7 @@ var GRAMMAR_HINT = {
|
|
|
44193
44380
|
};
|
|
44194
44381
|
function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
|
|
44195
44382
|
const abs = resolve15(cwd, raw);
|
|
44196
|
-
if (
|
|
44383
|
+
if (existsSync18(abs)) {
|
|
44197
44384
|
const st = statSync2(abs);
|
|
44198
44385
|
if (st.isDirectory()) {
|
|
44199
44386
|
bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
|
|
@@ -44228,7 +44415,7 @@ function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
|
|
|
44228
44415
|
return null;
|
|
44229
44416
|
}
|
|
44230
44417
|
async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
|
|
44231
|
-
const hash2 =
|
|
44418
|
+
const hash2 = createHash17("sha256");
|
|
44232
44419
|
const parts = [];
|
|
44233
44420
|
let total = 0;
|
|
44234
44421
|
for await (const chunk of createReadStream(absPath)) {
|
|
@@ -44454,7 +44641,7 @@ var CLAIM_SCANNER_PY = [
|
|
|
44454
44641
|
].join("\n");
|
|
44455
44642
|
|
|
44456
44643
|
// src/roundtrip/manifest.ts
|
|
44457
|
-
import { createHash as
|
|
44644
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
44458
44645
|
var RT_ENGINE = "roundtrip";
|
|
44459
44646
|
var RT_RESIDUE_RULE = "roundtrip-residue-file";
|
|
44460
44647
|
var RT_MODIFY_RULE = "roundtrip-modified-file";
|
|
@@ -44505,7 +44692,7 @@ var PERSISTENCE_PATTERNS = [
|
|
|
44505
44692
|
function isPersistenceSurface(path4) {
|
|
44506
44693
|
return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
|
|
44507
44694
|
}
|
|
44508
|
-
var digestOf = (what) =>
|
|
44695
|
+
var digestOf = (what) => createHash18("sha256").update(what).digest("hex");
|
|
44509
44696
|
function classifyResidue(label, diff, attribution = null) {
|
|
44510
44697
|
const findings = [];
|
|
44511
44698
|
for (const e of diff.added) {
|
|
@@ -44707,13 +44894,13 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
44707
44894
|
ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
|
|
44708
44895
|
return EXIT_ERROR;
|
|
44709
44896
|
}
|
|
44710
|
-
const baseDir = mkdtempSync5(
|
|
44897
|
+
const baseDir = mkdtempSync5(join27(tmpdir3(), "border-roundtrip-"));
|
|
44711
44898
|
try {
|
|
44712
44899
|
const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
|
|
44713
44900
|
const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
|
|
44714
44901
|
const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex2(bytes);
|
|
44715
|
-
const hostPath =
|
|
44716
|
-
|
|
44902
|
+
const hostPath = join27(baseDir, "artifact");
|
|
44903
|
+
writeFileSync8(hostPath, bytes);
|
|
44717
44904
|
const container = containerNameFor(label);
|
|
44718
44905
|
const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
|
|
44719
44906
|
const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
|
|
@@ -44757,7 +44944,7 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
44757
44944
|
}
|
|
44758
44945
|
return exitCodeFromVerdict(verdict);
|
|
44759
44946
|
} finally {
|
|
44760
|
-
|
|
44947
|
+
rmSync13(baseDir, { recursive: true, force: true });
|
|
44761
44948
|
}
|
|
44762
44949
|
}
|
|
44763
44950
|
function parsesRegistrySpec(raw) {
|
|
@@ -44815,7 +45002,7 @@ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
|
|
|
44815
45002
|
// src/scan/scan.ts
|
|
44816
45003
|
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
44817
45004
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
44818
|
-
import { join as
|
|
45005
|
+
import { join as join29 } from "node:path";
|
|
44819
45006
|
|
|
44820
45007
|
// src/scan/config.ts
|
|
44821
45008
|
function buildScanConfigYaml(eco) {
|
|
@@ -44842,10 +45029,10 @@ function synthesizeScanConfig(eco) {
|
|
|
44842
45029
|
}
|
|
44843
45030
|
|
|
44844
45031
|
// src/scan/materialize.ts
|
|
44845
|
-
import { existsSync as
|
|
44846
|
-
import { join as
|
|
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";
|
|
44847
45034
|
var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
|
|
44848
|
-
var
|
|
45035
|
+
var fail3 = (why) => {
|
|
44849
45036
|
throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
|
|
44850
45037
|
};
|
|
44851
45038
|
var oneLine = (err) => (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ").trim();
|
|
@@ -44853,20 +45040,20 @@ function extractFlat(archive, destDir) {
|
|
|
44853
45040
|
try {
|
|
44854
45041
|
extractArchive(archive, destDir);
|
|
44855
45042
|
} catch (err) {
|
|
44856
|
-
|
|
45043
|
+
fail3(`extraction of ${archive} failed: ${oneLine(err)}`);
|
|
44857
45044
|
}
|
|
44858
45045
|
}
|
|
44859
45046
|
function unwrapSingleDir(dir) {
|
|
44860
45047
|
const entries = readdirSync10(dir, { withFileTypes: true });
|
|
44861
45048
|
const dirs = entries.filter((e) => e.isDirectory());
|
|
44862
45049
|
if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
|
|
44863
|
-
return
|
|
45050
|
+
return join28(dir, dirs[0].name);
|
|
44864
45051
|
}
|
|
44865
45052
|
return dir;
|
|
44866
45053
|
}
|
|
44867
45054
|
function requireManifest(repoDir, ecosystem, candidates) {
|
|
44868
|
-
if (!candidates.some((rel) =>
|
|
44869
|
-
|
|
45055
|
+
if (!candidates.some((rel) => existsSync19(join28(repoDir, rel)))) {
|
|
45056
|
+
fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
|
|
44870
45057
|
}
|
|
44871
45058
|
}
|
|
44872
45059
|
function materializePackage(o) {
|
|
@@ -44875,19 +45062,19 @@ function materializePackage(o) {
|
|
|
44875
45062
|
`);
|
|
44876
45063
|
});
|
|
44877
45064
|
const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
|
|
44878
|
-
const archive =
|
|
44879
|
-
|
|
44880
|
-
const extractDir =
|
|
44881
|
-
|
|
45065
|
+
const archive = join28(o.baseDir, safe);
|
|
45066
|
+
writeFileSync9(archive, o.bytes);
|
|
45067
|
+
const extractDir = join28(o.baseDir, "extract");
|
|
45068
|
+
mkdirSync12(extractDir, { recursive: true });
|
|
44882
45069
|
extractFlat(archive, extractDir);
|
|
44883
45070
|
let repoDir = unwrapSingleDir(extractDir);
|
|
44884
45071
|
if (o.ecosystem === "rubygems") {
|
|
44885
45072
|
const outer = repoDir;
|
|
44886
45073
|
const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
|
|
44887
|
-
const inner = dataCandidates[0] ??
|
|
44888
|
-
if (!
|
|
44889
|
-
const gemTree =
|
|
44890
|
-
|
|
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 });
|
|
44891
45078
|
extractFlat(inner, gemTree);
|
|
44892
45079
|
repoDir = unwrapSingleDir(gemTree);
|
|
44893
45080
|
} else if (o.ecosystem === "npm") {
|
|
@@ -44897,10 +45084,10 @@ function materializePackage(o) {
|
|
|
44897
45084
|
} else {
|
|
44898
45085
|
requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
|
|
44899
45086
|
for (const file2 of CRATES_ENVELOPE_FILES) {
|
|
44900
|
-
const path4 =
|
|
44901
|
-
if (!
|
|
44902
|
-
const original =
|
|
44903
|
-
|
|
45087
|
+
const path4 = join28(repoDir, file2);
|
|
45088
|
+
if (!existsSync19(path4)) continue;
|
|
45089
|
+
const original = readFileSync17(path4);
|
|
45090
|
+
rmSync14(path4, { force: true });
|
|
44904
45091
|
note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex2(original)}`);
|
|
44905
45092
|
}
|
|
44906
45093
|
}
|
|
@@ -44927,7 +45114,7 @@ async function runScanCore(ctx, deps = {}) {
|
|
|
44927
45114
|
}
|
|
44928
45115
|
const cfg = synthesizeScanConfig(spec.ecosystem);
|
|
44929
45116
|
const fetcher = deps.fetcher ?? defaultScanFetcher;
|
|
44930
|
-
const baseDir = mkdtempSync6(
|
|
45117
|
+
const baseDir = mkdtempSync6(join29(tmpdir4(), "border-scan-"));
|
|
44931
45118
|
try {
|
|
44932
45119
|
const { bytes, filename } = await fetchArtifact(spec, { fetcher });
|
|
44933
45120
|
const env2 = { ...ctx.env };
|
|
@@ -45052,11 +45239,12 @@ var handlers = {
|
|
|
45052
45239
|
"llm-request": runLlmRequest,
|
|
45053
45240
|
"llm-ingest": runLlmIngest,
|
|
45054
45241
|
scan: runScan2,
|
|
45055
|
-
roundtrip: runRoundtrip
|
|
45242
|
+
roundtrip: runRoundtrip,
|
|
45243
|
+
opencode: runOpencode
|
|
45056
45244
|
};
|
|
45057
45245
|
|
|
45058
45246
|
// src/cli/types.ts
|
|
45059
|
-
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip"];
|
|
45247
|
+
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode"];
|
|
45060
45248
|
|
|
45061
45249
|
// src/cli.ts
|
|
45062
45250
|
var VALID_TARGETS = targetIds();
|
|
@@ -45074,6 +45262,7 @@ function usage() {
|
|
|
45074
45262
|
" llm-ingest record provenance captured by the harness",
|
|
45075
45263
|
" scan inspect a third-party package ([ecosystem:]name@version) for residue",
|
|
45076
45264
|
" roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
|
|
45265
|
+
" opencode install|status|uninstall the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode)",
|
|
45077
45266
|
"",
|
|
45078
45267
|
"flags:",
|
|
45079
45268
|
" --config <path> config file (default: ./border.yaml, then git-remote fallback)",
|
|
@@ -45088,7 +45277,7 @@ function usage() {
|
|
|
45088
45277
|
"",
|
|
45089
45278
|
"exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
|
|
45090
45279
|
"",
|
|
45091
|
-
"subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip"
|
|
45280
|
+
"subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode"
|
|
45092
45281
|
].join("\n");
|
|
45093
45282
|
}
|
|
45094
45283
|
function isSubcommand(value) {
|
|
@@ -45212,7 +45401,7 @@ function isMainModule({ argv1, moduleUrl, realpath }) {
|
|
|
45212
45401
|
return p;
|
|
45213
45402
|
}
|
|
45214
45403
|
};
|
|
45215
|
-
return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(
|
|
45404
|
+
return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(fileURLToPath6(moduleUrl))).href;
|
|
45216
45405
|
}
|
|
45217
45406
|
if (isMainModule({ argv1: process.argv[1], moduleUrl: import.meta.url, realpath: realpathSync.native })) {
|
|
45218
45407
|
const outcome = run(process.argv.slice(2));
|
package/package.json
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "border-customs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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
8
|
"border": "dist/index.js"
|
|
9
9
|
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./server": "./plugin/border.ts",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
10
15
|
"files": [
|
|
11
|
-
"dist"
|
|
16
|
+
"dist",
|
|
17
|
+
"plugin"
|
|
12
18
|
],
|
|
13
19
|
"engines": {
|
|
14
20
|
"node": ">=22.0.0"
|
|
@@ -26,6 +32,7 @@
|
|
|
26
32
|
"test": "node --import ./tools/register-ts.mjs --test --test-reporter=tap test/*.test.ts"
|
|
27
33
|
},
|
|
28
34
|
"dependencies": {
|
|
35
|
+
"@opencode-ai/plugin": "^1.17.4",
|
|
29
36
|
"@secretlint/core": "13.0.5",
|
|
30
37
|
"@secretlint/secretlint-rule-no-dotenv": "13.0.5",
|
|
31
38
|
"@secretlint/secretlint-rule-no-homedir": "13.0.5",
|