border-customs 0.1.0 → 0.3.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.
@@ -0,0 +1,228 @@
1
+ // provenance: original clean-room implementation per .omo/plans/border-push-channels.md todo C3
2
+ //
3
+ // cargo package-once artifact pipeline (plan C3 Scope + CHANNEL-CONTRACTS.md §a/§c).
4
+ // ONE `cargo package --allow-dirty --no-verify` into <repo>/.border/dist/ is the
5
+ // single source of bytes for the whole gate chain: the recorded {file, sha256,
6
+ // bytes} is what the push core re-hashes before publish and todo 14 pins in the
7
+ // ledger — nothing downstream rebuilds. The package is byte-deterministic for a
8
+ // given HEAD (spike §a: fixed tar member mtimes, gzip MTIME 0, digest bound to
9
+ // the .cargo_vcs_info.json sha1) — which is what makes the pre-publish repack
10
+ // digest-assert and the freshness repack sound. The .crate is a raw gzip tar
11
+ // that gitleaks 8.30.1 provably does NOT scan natively (spike §c), so it is
12
+ // extracted to a throwaway sandbox under .border/tmp/ and the EXTRACTED TREE is
13
+ // scanned (gitleaks dir + secretlint paths); the sandbox is removed on success
14
+ // AND failure. Build output lands in CARGO_TARGET_DIR under .border/tmp/
15
+ // (plan) so no target/ dir can pollute the repo tree.
16
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync } from "node:fs";
17
+ import { createHash } from "node:crypto";
18
+ import { join, relative } from "node:path";
19
+ import { spawnSync } from "node:child_process";
20
+
21
+ import type { BorderConfig } from "../config.ts";
22
+ import type { Finding } from "../findings.ts";
23
+ import { BORDER_STATE_DIR, ensureStateDir } from "../check/lock.ts";
24
+ import { extractArchive, removeSandbox } from "./extract.ts";
25
+ import { residueFindings } from "./residue.ts";
26
+ import { residueCratesHits } from "./residueRust.ts";
27
+ import { scanTree } from "../engines/gitleaks.ts";
28
+ import { scanPaths, type SecretlintMode } from "../engines/secretlint.ts";
29
+ import { EngineMissingError, EngineRunError, type EngineOptions } from "../engines/support.ts";
30
+ import type { TextSanitizer } from "../redact.ts";
31
+
32
+ /** Native-finding target label shared across the artifact-stage scans. */
33
+ export const CRATES_TARGET_LABEL = "artifact";
34
+
35
+ export type CrateArtifactRecord = {
36
+ /** repo-relative path of the single packed crate (`.border/dist/<file>`). */
37
+ readonly file: string;
38
+ readonly sha256: string;
39
+ readonly bytes: number;
40
+ };
41
+
42
+ export type CrateStageOptions = {
43
+ readonly repoDir: string;
44
+ readonly cfg: BorderConfig;
45
+ readonly env?: EngineOptions["env"];
46
+ readonly sanitizer?: TextSanitizer;
47
+ readonly skipGitleaks?: boolean;
48
+ readonly skipSecretlint?: boolean;
49
+ readonly secretlintMode?: SecretlintMode;
50
+ /** Exact cargo binary seam (tests / pinned toolchains); default "cargo" on PATH. */
51
+ readonly cargoBinPath?: string;
52
+ /** shared engine/extract timeout. */
53
+ readonly timeoutMs?: number;
54
+ readonly cargoTimeoutMs?: number;
55
+ };
56
+
57
+ export type CrateStageResult = {
58
+ readonly findings: readonly Finding[];
59
+ readonly artifact: CrateArtifactRecord | null;
60
+ };
61
+
62
+ function scopeFindingPath(f: Finding, extractDir: string): Finding {
63
+ if (f.path === undefined) return f;
64
+ const bang = f.path.indexOf("!");
65
+ if (bang > 0) {
66
+ const rel = relative(extractDir, f.path.slice(0, bang));
67
+ if (!rel.startsWith("..")) return { ...f, path: rel === "" ? f.path.slice(bang + 1) : `${rel}${f.path.slice(bang)}` };
68
+ }
69
+ const rel = relative(extractDir, f.path);
70
+ return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
71
+ }
72
+
73
+ function listFiles(dir: string): string[] {
74
+ const out: string[] = [];
75
+ const stack = [dir];
76
+ while (stack.length > 0) {
77
+ const cur = stack.pop() as string;
78
+ for (const ent of readdirSync(cur, { withFileTypes: true })) {
79
+ const p = join(cur, ent.name);
80
+ if (ent.isDirectory()) stack.push(p);
81
+ else if (ent.isFile()) out.push(p);
82
+ }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ /** Read `name` + `version` from the WORKING-TREE Cargo.toml — cargo packages
88
+ * the working tree, not HEAD, so the crate identity is the tree's identity. */
89
+ function manifestIdentity(repoDir: string, cfg: BorderConfig): { name: string; version: string } {
90
+ const manifestPath = join(repoDir, "Cargo.toml");
91
+ if (!existsSync(manifestPath)) {
92
+ throw new EngineRunError("targets.crates is configured but the repo has no Cargo.toml to package", null);
93
+ }
94
+ const raw = readFileSync(manifestPath, "utf8");
95
+ const pkg = /^\[package\]/m.test(raw);
96
+ if (!pkg) throw new EngineRunError("Cargo.toml has no [package] section — nothing cargo package can publish", null);
97
+ const name = cfg.targets.crates?.name ?? /^name\s*=\s*"([^"]+)"/m.exec(raw)?.[1];
98
+ if (name === undefined) {
99
+ throw new EngineRunError("Cargo.toml has no literal package name in the [package] section (and no targets.crates.name override)", null);
100
+ }
101
+ const version = /^version\s*=\s*"([^"]+)"/m.exec(raw)?.[1];
102
+ if (version === undefined) {
103
+ throw new EngineRunError(
104
+ "Cargo.toml has no literal package version — workspace-inherited or computed versions (version.workspace = true) are not supported; publish by setting a literal version",
105
+ null,
106
+ );
107
+ }
108
+ return { name, version };
109
+ }
110
+
111
+ /**
112
+ * Run the crates artifact stage. Throws EngineMissingError/EngineRunError
113
+ * (exit 2 via translateError) for anything that must NOT read as "clean":
114
+ * cargo absent, package failure, missing .crate output.
115
+ */
116
+ export async function runCargoArtifactStage(o: CrateStageOptions): Promise<CrateStageResult> {
117
+ if (o.cfg.targets.crates === undefined) {
118
+ throw new EngineRunError("runCargoArtifactStage invoked without targets.crates configured — pipeline bug", null);
119
+ }
120
+ const { repoDir } = o;
121
+ const bin = o.cargoBinPath ?? "cargo";
122
+ const identity = manifestIdentity(repoDir, o.cfg);
123
+ const crateName = `${identity.name}-${identity.version}.crate`;
124
+
125
+ const versionCheck = spawnSync(bin, ["--version"], {
126
+ encoding: "utf8",
127
+ stdio: "ignore",
128
+ ...(o.env === undefined ? {} : { env: { ...o.env } }),
129
+ });
130
+ if (versionCheck.error !== undefined || versionCheck.status !== 0) {
131
+ throw new EngineMissingError(
132
+ `cargo is not available (\`${bin} --version\` failed) — install Rust via rustup (https://rustup.rs) or pass cargoBinPath before running border check (border exit 2)`,
133
+ );
134
+ }
135
+
136
+ ensureStateDir(repoDir);
137
+ const stateDir = join(repoDir, BORDER_STATE_DIR);
138
+ const distDir = join(stateDir, "dist");
139
+ const targetDir = join(stateDir, "tmp", "cargo-target");
140
+ mkdirSync(distDir, { recursive: true });
141
+ rmSync(targetDir, { recursive: true, force: true }); // stale build output from an interrupted run
142
+ mkdirSync(targetDir, { recursive: true });
143
+
144
+ try {
145
+ const result = spawnSync(bin, ["package", "--allow-dirty", "--no-verify"], {
146
+ cwd: repoDir,
147
+ encoding: "utf8",
148
+ timeout: o.cargoTimeoutMs ?? 120_000,
149
+ env: {
150
+ ...(o.env === undefined ? process.env : { ...o.env }),
151
+ CARGO_TARGET_DIR: targetDir,
152
+ },
153
+ });
154
+ if (result.error !== undefined || result.status !== 0) {
155
+ throw new EngineRunError(
156
+ `cargo package failed (exit ${String(result.status)}) — nothing to gate: ${(result.stderr ?? "").trim().slice(0, 400)}`,
157
+ result.status,
158
+ );
159
+ }
160
+
161
+ // cargo writes the crate to <target-dir>/package/<name>-<version>.crate
162
+ const packedDir = join(targetDir, "package");
163
+ const packed = join(packedDir, crateName);
164
+ if (!existsSync(packed)) {
165
+ throw new EngineRunError(`cargo package produced no ${crateName} in ${relative(repoDir, packedDir)} — failing closed`, null);
166
+ }
167
+ const dist = join(distDir, crateName);
168
+ renameSync(packed, dist);
169
+
170
+ const bytes = readFileSync(dist);
171
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
172
+
173
+ // raw .crate is a gitleaks native miss (contract §c) — scan the EXTRACTED tree
174
+ const findings: Finding[] = [];
175
+ const extractDir = join(stateDir, "tmp", sha256.slice(0, 8));
176
+ rmSync(extractDir, { recursive: true, force: true });
177
+ mkdirSync(extractDir, { recursive: true });
178
+ try {
179
+ extractArchive(dist, extractDir, {
180
+ ...(o.env !== undefined ? { env: o.env } : {}),
181
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
182
+ });
183
+ const relPaths = listFiles(extractDir).map((p) => relative(extractDir, p));
184
+ if (relPaths.length === 0) throw new EngineRunError("extracted artifact is empty — unusable cargo package output", null);
185
+
186
+ if (o.skipGitleaks !== true) {
187
+ const tree = scanTree({
188
+ dir: extractDir,
189
+ stateDir,
190
+ target: CRATES_TARGET_LABEL,
191
+ ...(o.env !== undefined ? { env: o.env } : {}),
192
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
193
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
194
+ });
195
+ for (const f of tree) findings.push(scopeFindingPath(f, extractDir));
196
+ }
197
+ if (o.skipSecretlint !== true) {
198
+ const linted = await scanPaths({
199
+ dir: extractDir,
200
+ files: relPaths,
201
+ target: CRATES_TARGET_LABEL,
202
+ rules: o.cfg.rules,
203
+ ...(o.secretlintMode !== undefined ? { mode: o.secretlintMode } : {}),
204
+ ...(o.env !== undefined ? { env: o.env } : {}),
205
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
206
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
207
+ });
208
+ findings.push(...linted);
209
+ }
210
+ // R3a residue leg (plan §57): build.rs OUT_DIR confinement + [build-dependencies] net scan
211
+ // + artifact-wide T4; inner rels keep the `<name>-<version>/` wrapper (crates convention).
212
+ findings.push(...residueFindings(residueCratesHits(extractDir, relPaths), {
213
+ root: "",
214
+ identity: crateName,
215
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
216
+ }));
217
+ } finally {
218
+ removeSandbox(extractDir);
219
+ }
220
+
221
+ return {
222
+ findings,
223
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256, bytes: bytes.length },
224
+ };
225
+ } finally {
226
+ rmSync(targetDir, { recursive: true, force: true });
227
+ }
228
+ }
@@ -0,0 +1,294 @@
1
+ // provenance: original clean-room implementation per .omo/plans/border-push-gate.md todo 11
2
+ //
3
+ // npm pack-once artifact pipeline (plan G38/G39/G33/G23). ONE `npm pack` into
4
+ // <repo>/.border/dist/ is the single source of bytes for the whole gate chain:
5
+ // the recorded {file, sha256, bytes} is what todo 17 re-hashes before publish
6
+ // and todo 14 pins in the ledger — nothing downstream rebuilds. The tarball is
7
+ // extracted to a throwaway sandbox under .border/tmp/, content-scanned (gitleaks
8
+ // dir + secretlint paths), manifest-diffed against the `files` whitelist, then
9
+ // the sandbox is removed on success AND failure. publint runs against the
10
+ // tarball itself, never the working tree.
11
+ //
12
+ // PATH CONTRACT (the false-green trap, round-3 M-A): packs live under
13
+ // `.border/`, which filterBorderStateFindings hard-excludes at the user-repo
14
+ // root — so EVERY finding path emitted here is scoped to the artifact root
15
+ // (`package/<rel>`), never containing a `.border` segment. test/artifacts.npm.test.ts
16
+ // pins this end-to-end with a secret planted inside a packed file.
17
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
18
+ import { join, relative } from "node:path";
19
+
20
+ import type { BorderConfig } from "../config.ts";
21
+ import type { Finding, Severity } from "../findings.ts";
22
+ import { redact, type TextSanitizer } from "../redact.ts";
23
+ import { BORDER_STATE_DIR, ensureStateDir } from "../check/lock.ts";
24
+ import { extractArchive, removeSandbox } from "./extract.ts";
25
+ import { scanTree } from "../engines/gitleaks.ts";
26
+ import { scanPaths, type SecretlintMode } from "../engines/secretlint.ts";
27
+ import { EngineRunError, type EngineOptions } from "../engines/support.ts";
28
+ import { LIFECYCLE_SCRIPT_KEYS, parseNpmManifest, unexpectedEntries } from "./manifestDiff.ts";
29
+ import { residueFindings, scanNpmLifecycleHook, sweepResidueNpmArtifact } from "./residue.ts";
30
+ import { RESIDUE_T1_RULE } from "../rules/residueMatchers.ts";
31
+ import {
32
+ NPM_LIFECYCLE_RULE,
33
+ NPM_PUBLINT_RULE,
34
+ NPM_PRIVATE_RULE,
35
+ NPM_TARGET_LABEL,
36
+ NPM_UNEXPECTED_RULE,
37
+ packOnce,
38
+ publintFinding,
39
+ sanitizeOut,
40
+ tail,
41
+ } from "./npmPack.ts";
42
+
43
+ export { NPM_LIFECYCLE_RULE, NPM_PUBLINT_RULE, NPM_PRIVATE_RULE, NPM_TARGET_LABEL, NPM_UNEXPECTED_RULE };
44
+
45
+ export type NpmArtifactRecord = {
46
+ /** repo-relative path of the single packed tarball (`.border/dist/<file>`). */
47
+ readonly file: string;
48
+ readonly sha256: string;
49
+ readonly bytes: number;
50
+ };
51
+
52
+ export type NpmStageOptions = {
53
+ readonly repoDir: string;
54
+ readonly cfg: BorderConfig;
55
+ readonly env?: EngineOptions["env"];
56
+ readonly sanitizer?: TextSanitizer;
57
+ readonly skipGitleaks?: boolean;
58
+ readonly skipSecretlint?: boolean;
59
+ readonly secretlintMode?: SecretlintMode;
60
+ readonly npmBinPath?: string;
61
+ readonly publintBinPath?: string;
62
+ /** shared engine/extract timeout; npm/publint have their own knobs. */
63
+ readonly timeoutMs?: number;
64
+ readonly npmTimeoutMs?: number;
65
+ readonly publintTimeoutMs?: number;
66
+ };
67
+
68
+ export type NpmStageResult = {
69
+ readonly findings: readonly Finding[];
70
+ readonly artifact: NpmArtifactRecord | null;
71
+ };
72
+
73
+ function nativeFinding(
74
+ rule: string,
75
+ severity: Severity,
76
+ message: string,
77
+ path: string,
78
+ identity: string,
79
+ sanitizer: TextSanitizer | undefined,
80
+ ): Finding {
81
+ const digest = redact(identity);
82
+ return {
83
+ rule,
84
+ severity,
85
+ target: NPM_TARGET_LABEL,
86
+ path,
87
+ engine: "native",
88
+ message: sanitizeOut(sanitizer, message),
89
+ valueDigest: digest.valueDigest,
90
+ snippet: digest.snippet,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * GAP B (todo 11 wired into `border check`): re-attribute stage findings from
96
+ * artifact-root-relative (`package/<rel>`, publint's `package.json`) to the
97
+ * exact packed bytes: `<tarball>!<rel>`. The tarball basename never contains a
98
+ * `!` and carries no `.border` segment, so it survives the repo-scoped filter.
99
+ */
100
+ export function attributeToTarball(findings: readonly Finding[], artifact: NpmArtifactRecord | null): readonly Finding[] {
101
+ if (artifact === null) return findings;
102
+ const name = artifact.file.slice(artifact.file.lastIndexOf("/") + 1);
103
+ return findings.map((f) => {
104
+ const p = f.path ?? "";
105
+ const rel = p === "package.json" ? p : p.startsWith("package/") ? p.slice("package/".length) : null;
106
+ return rel === null || rel === "" ? f : { ...f, path: `${name}!${rel}` };
107
+ });
108
+ }
109
+
110
+ /** Re-scope engine findings (absolute / `<abs>!<inner>` shapes) to artifact-root-relative. */
111
+ function scopeFindingPath(f: Finding, extractDir: string): Finding {
112
+ if (f.path === undefined) return f;
113
+ const bang = f.path.indexOf("!");
114
+ if (bang > 0) {
115
+ const rel = relative(extractDir, f.path.slice(0, bang));
116
+ if (!rel.startsWith("..")) return { ...f, path: rel === "" ? f.path.slice(bang + 1) : `${rel}${f.path.slice(bang)}` };
117
+ }
118
+ const rel = relative(extractDir, f.path);
119
+ return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
120
+ }
121
+
122
+ function listFiles(dir: string): string[] {
123
+ const out: string[] = [];
124
+ const stack = [dir];
125
+ while (stack.length > 0) {
126
+ const cur = stack.pop() as string;
127
+ for (const ent of readdirSync(cur, { withFileTypes: true })) {
128
+ const p = join(cur, ent.name);
129
+ if (ent.isDirectory()) stack.push(p);
130
+ else if (ent.isFile()) out.push(p);
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ /**
137
+ * Run the npm artifact stage. Throws EngineMissingError/EngineRunError (exit 2
138
+ * via translateError) for anything that must NOT read as "clean": npm absent,
139
+ * pack failure, unparseable manifests, digest mismatch, unusable publint.
140
+ */
141
+ export async function runNpmArtifactStage(o: NpmStageOptions): Promise<NpmStageResult> {
142
+ if (o.cfg.targets.npm === undefined) {
143
+ throw new EngineRunError("runNpmArtifactStage invoked without targets.npm configured — pipeline bug", null);
144
+ }
145
+ const { repoDir } = o;
146
+ const manifestPath = join(repoDir, "package.json");
147
+ if (!existsSync(manifestPath)) {
148
+ throw new EngineRunError("targets.npm is configured but the repo has no package.json to pack", null);
149
+ }
150
+ const manifest = parseNpmManifest(readFileSync(manifestPath, "utf8"));
151
+ const identity = `${manifest.name ?? "?"}@${manifest.version ?? "?"}`;
152
+
153
+ if (manifest.isPrivate) {
154
+ return {
155
+ findings: [nativeFinding(
156
+ NPM_PRIVATE_RULE,
157
+ "CRITICAL",
158
+ `package.json is private:true but border.yaml configures targets.npm — nothing can ever publish from this repo; drop the private flag or the npm target`,
159
+ "package.json",
160
+ `${identity}:private`,
161
+ o.sanitizer,
162
+ )],
163
+ artifact: null,
164
+ };
165
+ }
166
+
167
+ ensureStateDir(repoDir);
168
+ const stateDir = join(repoDir, BORDER_STATE_DIR);
169
+ const distDir = join(stateDir, "dist");
170
+ mkdirSync(distDir, { recursive: true });
171
+ const { filename, sha256, bytes, tarballAbs } = packOnce({
172
+ repoDir,
173
+ distDir,
174
+ ...(o.env !== undefined ? { env: o.env } : {}),
175
+ ...(o.npmBinPath !== undefined ? { npmBinPath: o.npmBinPath } : {}),
176
+ ...(o.npmTimeoutMs !== undefined ? { npmTimeoutMs: o.npmTimeoutMs } : {}),
177
+ });
178
+
179
+ const findings: Finding[] = [];
180
+ const extractDir = join(stateDir, "tmp", sha256.slice(0, 8));
181
+ rmSync(extractDir, { recursive: true, force: true }); // stale sandbox from an interrupted run
182
+ mkdirSync(extractDir, { recursive: true });
183
+ try {
184
+ extractArchive(tarballAbs, extractDir, {
185
+ ...(o.env !== undefined ? { env: o.env } : {}),
186
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
187
+ });
188
+ const relPaths = listFiles(extractDir).map((p) => relative(extractDir, p));
189
+ if (relPaths.length === 0) throw new EngineRunError("extracted artifact is empty — unusable npm pack output", null);
190
+ const root = relPaths[0]?.split("/")[0] ?? "package";
191
+ if (!relPaths.every((p) => p.startsWith(`${root}/`))) {
192
+ throw new EngineRunError("extracted artifact mixes multiple top-level roots — unexpected tar layout, failing closed", null);
193
+ }
194
+ const packedManifestRel = `${root}/package.json`;
195
+ if (!relPaths.includes(packedManifestRel)) throw new EngineRunError("extracted artifact has no package/package.json", null);
196
+ // lifecycle gate reads the PUBLISHED manifest (the bytes consumers install),
197
+ // not the working tree — npm may rewrite package.json inside the tarball.
198
+ const packedManifest = parseNpmManifest(readFileSync(join(extractDir, packedManifestRel), "utf8"));
199
+
200
+ // RESIDUE-CONTRACT §1.6/§3 (R2): a hook is downgraded to MEDIUM ONLY when the
201
+ // classifier matches closed signature #1 in full; every other outcome — unknown
202
+ // grammar, unreadable target, out-of-signature literal — keeps the 0.2.0
203
+ // lifecycle-script CRITICAL row byte-identically (gate-2 pins). Family rows
204
+ // (T2/T3/cross-manager) are emitted IN ADDITION; the gate only ever gets stricter.
205
+ const pkgDir = join(extractDir, root);
206
+ const residueCtx = { root, identity, ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}) };
207
+ for (const key of LIFECYCLE_SCRIPT_KEYS) {
208
+ const cmd = packedManifest.scripts[key];
209
+ if (cmd === undefined) continue;
210
+ const scan = scanNpmLifecycleHook({ pkgDir, hook: cmd });
211
+ if (scan.signatureId !== null) {
212
+ findings.push(nativeFinding(
213
+ RESIDUE_T1_RULE,
214
+ "MEDIUM",
215
+ `package.json script '${key}' matches closed safe signature '${scan.signatureId}' (T1: install-root writes only, frozen probe/fallback argv literals, zero network/out-of-tree/persistence/cross-manager hits) — allowed but audit-visible; any deviation keeps the blocking lifecycle-script CRITICAL`,
216
+ packedManifestRel,
217
+ `${identity}:residue:t1:${key}`,
218
+ o.sanitizer,
219
+ ));
220
+ } else {
221
+ findings.push(nativeFinding(
222
+ NPM_LIFECYCLE_RULE,
223
+ "CRITICAL",
224
+ `package.json script '${key}' is a lifecycle hook — npm executes it on every consumer install (G33): ${tail(sanitizeOut(o.sanitizer, cmd))}`,
225
+ packedManifestRel,
226
+ `${identity}:lifecycle:${key}`,
227
+ o.sanitizer,
228
+ ));
229
+ }
230
+ findings.push(...residueFindings(scan.hits, residueCtx));
231
+ }
232
+ const entries = relPaths.filter((p) => p !== packedManifestRel).map((p) => p.slice(root.length + 1));
233
+ for (const entry of unexpectedEntries(entries, manifest.files)) {
234
+ findings.push(nativeFinding(
235
+ NPM_UNEXPECTED_RULE,
236
+ "HIGH",
237
+ `packed entry '${entry}' matches no package.json files whitelist pattern and is not an always-packed name — force-packed via main/bin or default inclusion (G39); publish only intentional bytes`,
238
+ `${root}/${entry}`,
239
+ `${identity}:unexpected:${entry}`,
240
+ o.sanitizer,
241
+ ));
242
+ }
243
+
244
+ // T4 + B2 marker pair-detection are artifact-wide (contract §4/§5.3): a persistence string
245
+ // in ANY packed file blocks, even with no lifecycle hook present. NpmManifest does
246
+ // not parse `bin`, so the CLI-inverse surface is scanned over the packed bin/ tree.
247
+ const binRels = relPaths.filter((p) => p.startsWith(`${root}/bin/`));
248
+ findings.push(...residueFindings(sweepResidueNpmArtifact({ pkgDir, files: relPaths, binRels }), residueCtx));
249
+
250
+ if (o.skipGitleaks !== true) {
251
+ const tree = scanTree({
252
+ dir: extractDir,
253
+ stateDir,
254
+ target: NPM_TARGET_LABEL,
255
+ ...(o.env !== undefined ? { env: o.env } : {}),
256
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
257
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
258
+ });
259
+ for (const f of tree) findings.push(scopeFindingPath(f, extractDir));
260
+ }
261
+ if (o.skipSecretlint !== true) {
262
+ const linted = await scanPaths({
263
+ dir: extractDir,
264
+ files: relPaths,
265
+ target: NPM_TARGET_LABEL,
266
+ rules: o.cfg.rules,
267
+ ...(o.secretlintMode !== undefined ? { mode: o.secretlintMode } : {}),
268
+ ...(o.env !== undefined ? { env: o.env } : {}),
269
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
270
+ ...(o.timeoutMs !== undefined ? { timeoutMs: o.timeoutMs } : {}),
271
+ });
272
+ findings.push(...linted);
273
+ }
274
+ } finally {
275
+ removeSandbox(extractDir);
276
+ }
277
+
278
+ const publint = publintFinding(
279
+ {
280
+ ...(o.env !== undefined ? { env: o.env } : {}),
281
+ ...(o.publintBinPath !== undefined ? { publintBinPath: o.publintBinPath } : {}),
282
+ ...(o.publintTimeoutMs !== undefined ? { publintTimeoutMs: o.publintTimeoutMs } : {}),
283
+ ...(o.sanitizer !== undefined ? { sanitizer: o.sanitizer } : {}),
284
+ },
285
+ tarballAbs,
286
+ identity,
287
+ );
288
+ if (publint !== null) findings.push(publint);
289
+
290
+ return {
291
+ findings,
292
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256, bytes },
293
+ };
294
+ }