@bongos/core 1.17.3 → 1.18.3
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/.bongos-core.json +212 -332
- package/.devcontainer/healthcheck.sh +0 -0
- package/.devcontainer/init-firewall.sh +0 -0
- package/.devcontainer/post-create.sh +0 -0
- package/GOVERNANCE.md +1 -1
- package/README.md +26 -22
- package/bin/bongos.js +0 -0
- package/config/branding.neutral.json +5 -1
- package/docker-entrypoint.sh +0 -0
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +6 -0
- package/docs/api/openapi.json +1630 -632
- package/docs/api-reference.md +50 -15
- package/docs/file-map.md +0 -18
- package/docs/onboarding/diagrams/05-versioning.mmd +4 -6
- package/docs/onboarding/diagrams/assertions.json +5 -9
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/builders.js +37 -0
- package/modules/hall-ui/public/goals-page.js +98 -176
- package/modules/hall-ui/public/goals.css +183 -0
- package/modules/hall-ui/public/goals.html +35 -261
- package/modules/hall-ui/public/goals.js +7 -2
- package/modules/hall-ui/public/hall-widgets.js +12 -4
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/settings.css +355 -0
- package/modules/hall-ui/public/settings.html +86 -414
- package/modules/hall-ui/public/settings.js +96 -35
- package/modules/hall-ui/public/style.css +31 -0
- package/modules/hall-ui/public/watch.css +184 -0
- package/modules/hall-ui/public/watch.html +111 -285
- package/modules/hall-ui/public/watch.js +59 -21
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +292 -19
- package/public-landing/index.html +11 -76
- package/scripts/cron/style_guide_audit.sh +0 -0
- package/scripts/gds/api.js +0 -0
- package/scripts/gds/architect-audit.js +0 -0
- package/scripts/gds/audit-authorship.sh +0 -0
- package/scripts/gds/audit-replay.sh +0 -0
- package/scripts/gds/box.js +0 -0
- package/scripts/gds/capture.js +0 -0
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/conductor-dispatch.js +0 -0
- package/scripts/gds/coreB-carve-proof.sh +0 -0
- package/scripts/gds/db-backup-nightly.sh +0 -0
- package/scripts/gds/db-backup-offsite.sh +0 -0
- package/scripts/gds/doctor.js +0 -0
- package/scripts/gds/grade-smoke.js +0 -0
- package/scripts/gds/hooks/pre-push.js +0 -0
- package/scripts/gds/install-git-hooks.js +0 -0
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/main-audit.js +0 -0
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/oauth-secret.js +0 -0
- package/scripts/gds/onboarding-analyze.js +0 -0
- package/scripts/gds/onboarding-ship.js +0 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +0 -0
- package/scripts/gds/portability-smoke-art.sh +0 -0
- package/scripts/gds/portability-smoke-memory.sh +0 -0
- package/scripts/gds/provision.js +270 -11
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/push-main.js +0 -0
- package/scripts/gds/render-ideas.js +0 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/security-baseline-check.sh +0 -0
- package/scripts/gds/session.js +0 -0
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +126 -32
- package/scripts/gds/smoke-batch-claim.sh +0 -0
- package/scripts/gds/smoke-batch-release.sh +0 -0
- package/scripts/gds/smoke-box.sh +0 -0
- package/scripts/gds/smoke-claim-consistency.sh +0 -0
- package/scripts/gds/smoke-dependencies.sh +0 -0
- package/scripts/gds/smoke-write.sh +0 -0
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/hall-preview/server.js +18 -1
- package/scripts/migrate.sh +10 -0
- package/scripts/migrate_cost_ledger.sh +0 -0
- package/scripts/render-diagrams.sh +0 -0
- package/scripts/security/dep-audit.js +0 -0
- package/scripts/setup-builder.js +0 -0
- package/scripts/setup-builder.sh +0 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +0 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +48 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +226 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/scripts/hall-preview/fixtures/access-requests.json +0 -1
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -1
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -1
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -1
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/override-requests.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -1
- package/scripts/hall-preview/fixtures/security__reports.json +0 -1
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -1
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
|
@@ -52,8 +52,15 @@
|
|
|
52
52
|
//
|
|
53
53
|
// USAGE:
|
|
54
54
|
// node scripts/gds/package-core.js [--ref <git-ref>] [--out-dir dist]
|
|
55
|
-
// [--version <v>] [--droplet-ip <ip>] [--publish [--live]]
|
|
55
|
+
// [--version <v>] [--droplet-ip <ip>] [--self-verify [--self-verify-boot]] [--publish [--live]]
|
|
56
56
|
// Defaults: --ref HEAD, --out-dir dist, --version = platform CORE_VERSION.
|
|
57
|
+
// --self-verify after building, prove the artifact reproduces its pin (unpack the .tgz,
|
|
58
|
+
// recompute tree_sha256 == manifest + verify tarball.sha256). Fatal on mismatch.
|
|
59
|
+
// (task 1002218 / audit H3) — deterministic + network-free.
|
|
60
|
+
// --self-verify-boot ALSO `npm install <tgz>` into a throwaway consumer + boot /healthz
|
|
61
|
+
// (reuses tests/consumer_layout_boot.mjs; needs npm/registry access).
|
|
62
|
+
// --leak-scan-strict make the post-build leak scan FATAL on any prior-instance literal
|
|
63
|
+
// (default: the scan runs + WARNS; --no-leak-scan skips it). task 1002220 / H6.
|
|
57
64
|
// Outputs (nothing is committed — dist/ is gitignored):
|
|
58
65
|
// dist/bongos-core-<version>/ the redacted core tree (inspectable)
|
|
59
66
|
// dist/bongos-core-<version>.tgz the installable unit
|
|
@@ -64,6 +71,7 @@
|
|
|
64
71
|
// npm org on a paid plan + `npm login`; this script holds no token.
|
|
65
72
|
|
|
66
73
|
const fs = require('node:fs');
|
|
74
|
+
const os = require('node:os');
|
|
67
75
|
const path = require('node:path');
|
|
68
76
|
const zlib = require('node:zlib');
|
|
69
77
|
const crypto = require('node:crypto');
|
|
@@ -207,6 +215,33 @@ function processCoreFile({ pathRel, raw, rules, sensitiveLiterals, envSecrets })
|
|
|
207
215
|
};
|
|
208
216
|
}
|
|
209
217
|
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// npm-STRIPPED special files (idea 1000543) — the file-SELECTION gate that keeps
|
|
220
|
+
// the pin HONEST about what a consumer actually installs.
|
|
221
|
+
//
|
|
222
|
+
// `npm install <tgz>` never materializes certain special files into the installed
|
|
223
|
+
// package — most notably `.gitignore` (confirmed live: installing the 1.18.1 core
|
|
224
|
+
// .tgz drops exactly one file, ./.gitignore), plus `.npmignore` and `.npmrc`. But
|
|
225
|
+
// package-core enumerates the GIT tree, so isPublishable() happily selects a tracked
|
|
226
|
+
// `.gitignore`. If it lands in the manifest files[] + tree_sha256 pin, the tree a
|
|
227
|
+
// consumer recomputes over node_modules/@bongos/core is MISSING it → verifyInstalledPin
|
|
228
|
+
// (upgrade.js) reports `missing installed file: .gitignore` → hash mismatch → the bump
|
|
229
|
+
// auto-rolls-back. That breaks EVERY vendored `bongos upgrade` with pin-verify ON, and
|
|
230
|
+
// the unattended auto-upgrade subscription (task 2150 / ADR 0136) has no way to pass
|
|
231
|
+
// --skip-pin-verify — so it would roll back every patch (the fix reported in idea 1000543).
|
|
232
|
+
//
|
|
233
|
+
// The manifest must describe exactly what npm installs, so these files are dropped BEFORE
|
|
234
|
+
// they enter the tarball, filesMeta, or the pin (main(), below). Matched by BASENAME at any
|
|
235
|
+
// depth — npm strips nested ones too. The asymmetry is deliberately safe: excluding a file
|
|
236
|
+
// npm WOULD have kept only slightly weakens the pin (verifyInstalledPin iterates the manifest
|
|
237
|
+
// files[], so an un-pinned file is simply not checked); FAILING to exclude one npm strips is
|
|
238
|
+
// FATAL. And these files are useless inside a vendored dependency anyway.
|
|
239
|
+
const NPM_STRIPPED_BASENAMES = new Set(['.gitignore', '.npmignore', '.npmrc']);
|
|
240
|
+
function isNpmStripped(pathRel) {
|
|
241
|
+
const base = String(pathRel || '').split('/').pop();
|
|
242
|
+
return NPM_STRIPPED_BASENAMES.has(base);
|
|
243
|
+
}
|
|
244
|
+
|
|
210
245
|
// ---------------------------------------------------------------------------
|
|
211
246
|
// The CORE's own package.json + package-lock.json (task 1882, ADR 0108 §2).
|
|
212
247
|
//
|
|
@@ -545,6 +580,164 @@ function publishTarball({ tgzPath, live }) {
|
|
|
545
580
|
console.error(`[package-core] npm publish ${live ? 'succeeded — @bongos/core is live (private).' : 'dry-run OK — the tarball is publishable.'}`);
|
|
546
581
|
}
|
|
547
582
|
|
|
583
|
+
// The neutral FLOOR — customer-zero identity tokens a portable core must never carry (task 1002220
|
|
584
|
+
// / H6 + task 1002217 / H4, finding F2). When packaging from the NEUTRAL trunk there is no
|
|
585
|
+
// config/branding.json, so the branding-DERIVED redaction + gate see ZERO literals and report
|
|
586
|
+
// "clean" while stripping nothing. This fixed floor closes that gap two ways, both keyed off ONE
|
|
587
|
+
// definition:
|
|
588
|
+
// • DOC path (H4, fail-closed): injected as `extra` {tokens, owners} into buildRedactionRules +
|
|
589
|
+
// collectSensitiveLiterals, so the existing doc-redaction STRIPS these from published docs
|
|
590
|
+
// (tokens→NEUTRAL_TOKEN, owners→NEUTRAL_OWNER) and the fail-closed no-leak gate ABORTS the build
|
|
591
|
+
// if one SURVIVES — even with no branding.json. The floor lives HERE and is injected at call
|
|
592
|
+
// time, so mirror-redact.js stays literal-free (its self-leak invariant holds); functional files
|
|
593
|
+
// still ship VERBATIM (the R88 fix) and are covered by the H6 advisory below, not this gate.
|
|
594
|
+
// • SCAN path (H6, advisory): scanArtifactForLeaks measures the flat list across the WHOLE artifact.
|
|
595
|
+
// findLeaks' public-IP floor separately catches the old droplet IP.
|
|
596
|
+
const PRIOR_INSTANCE_FLOOR = { tokens: ['amazonprimea', 'off the boats'], owners: ['lars589'] };
|
|
597
|
+
const PRIOR_INSTANCE_LITERALS = [...PRIOR_INSTANCE_FLOOR.tokens, ...PRIOR_INSTANCE_FLOOR.owners];
|
|
598
|
+
|
|
599
|
+
// High-signal committed-credential shapes for the secret WARNING (task 1002220 / audit H6, finding
|
|
600
|
+
// F3). Deliberately NARROW — well-known provider token prefixes + PEM private-key headers + an
|
|
601
|
+
// explicit secret=<literal> assignment — rather than the generic entropy catch-all, which fires on
|
|
602
|
+
// ordinary long identifiers / base64 data and buries a real hit (an early cut warned on 339 files,
|
|
603
|
+
// ~all benign). A match is a WARNING to eyeball; the HARD gate against live env-secret VALUES is
|
|
604
|
+
// functionalSecretLeaks in the build loop. Scans functional files (docs are redacted + gated already).
|
|
605
|
+
const CREDENTIAL_SHAPE_PATTERNS = [
|
|
606
|
+
['anthropic-key', /\bsk-ant-[A-Za-z0-9_-]{16,}/],
|
|
607
|
+
['openai-key', /\bsk-[A-Za-z0-9]{24,}/],
|
|
608
|
+
['github-token', /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}/],
|
|
609
|
+
['aws-access-key-id', /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/],
|
|
610
|
+
['google-api-key', /\bAIza[A-Za-z0-9_-]{30,}/],
|
|
611
|
+
['slack-token', /\bxox[baprs]-[A-Za-z0-9-]{10,}/],
|
|
612
|
+
['private-key-pem', /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/],
|
|
613
|
+
['secret-assignment', /\b(?:api[_-]?key|secret|password|passwd|access[_-]?token|auth[_-]?token)\b\s*[:=]\s*['"][^'"\s]{16,}['"]/i],
|
|
614
|
+
];
|
|
615
|
+
function credentialShapeHits(text) {
|
|
616
|
+
const names = [];
|
|
617
|
+
for (const [name, re] of CREDENTIAL_SHAPE_PATTERNS) if (re.test(text)) names.push(name);
|
|
618
|
+
return names;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// classify a shipped path for the scan — mirrors processCoreFile's doc/stub/functional split.
|
|
622
|
+
function scanKindOf(pathRel) {
|
|
623
|
+
if (isAgentDoc(pathRel)) return 'stub';
|
|
624
|
+
if (isDocPath(pathRel)) return 'doc';
|
|
625
|
+
return 'functional';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Independent post-build leak scan over the built core file set (task 1002220 / audit H6). Two layers:
|
|
629
|
+
// • INSTANCE LITERALS — findLeaks() over PRIOR_INSTANCE_LITERALS (+ the public-IP floor) across
|
|
630
|
+
// EVERY shipped file: brand/owner/history that must not ship in a portable core. This is the
|
|
631
|
+
// "prove no leak" measurement the neutral trunk was missing (F2).
|
|
632
|
+
// • CREDENTIAL SHAPES — credentialShapeHits() over FUNCTIONAL files only (docs are redacted+gated
|
|
633
|
+
// already): a high-signal WARNING that a committed credential shape shipped.
|
|
634
|
+
// `files` = [{ path, content }]. Pure — returns findings; the caller decides warn vs fatal.
|
|
635
|
+
function scanArtifactForLeaks(files, { literals = PRIOR_INSTANCE_LITERALS } = {}) {
|
|
636
|
+
const literalHits = []; // { path, label, sample }
|
|
637
|
+
const secretWarnings = []; // { path, shapes }
|
|
638
|
+
for (const f of files || []) {
|
|
639
|
+
const text = Buffer.isBuffer(f.content) ? f.content.toString('utf8') : String(f.content == null ? '' : f.content);
|
|
640
|
+
for (const lk of findLeaks(text, literals)) literalHits.push({ path: f.path, label: lk.label, sample: lk.sample });
|
|
641
|
+
if (scanKindOf(f.path) === 'functional') {
|
|
642
|
+
const shapes = credentialShapeHits(text);
|
|
643
|
+
if (shapes.length) secretWarnings.push({ path: f.path, shapes });
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { literalHits, secretWarnings };
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Self-verify the built artifact (task 1002218 / audit H3, findings F1+F5). The packager
|
|
650
|
+
// claims a consumer "verifies an install by recomputing [the pin] from the unpacked tree" —
|
|
651
|
+
// this proves the packager's OWN output actually does, at build time, on the produce side:
|
|
652
|
+
// 1. REPRODUCE THE PIN (always; deterministic, network-free): unpack the built .tgz and
|
|
653
|
+
// recompute tree_sha256 over the unpacked tree, asserting it equals the manifest — plus
|
|
654
|
+
// the on-disk .tgz bytes vs manifest.tarball.sha256. Catches a tar-writer bug, a corrupt
|
|
655
|
+
// write, or pin drift BEFORE the artifact is trusted or published (the R88 failure class).
|
|
656
|
+
// 2. BARE-INSTALL BOOT (opt-in --self-verify-boot; needs npm/registry): `npm install <tgz>`
|
|
657
|
+
// into a throwaway consumer and boot /healthz, by reusing the proven consumer-layout
|
|
658
|
+
// harness (tests/consumer_layout_boot.mjs) with CORE_TGZ set — no duplication. The deeper
|
|
659
|
+
// standing CI gate is task 1002221; this is the packager's own quick smoke.
|
|
660
|
+
// Fatal (exit 1) on a reproduce mismatch or a boot failure — a self-verify that finds a
|
|
661
|
+
// problem must stop the build, never warn-and-ship.
|
|
662
|
+
// Pure recompute — read each pinned file from an unpacked tree, rebuild the canonical tree
|
|
663
|
+
// hash (using the manifest's already-canonical modes), and compare to the manifest, plus the
|
|
664
|
+
// on-disk .tgz bytes vs manifest.tarball.sha256. No process.exit / no spawn → unit-testable.
|
|
665
|
+
// Returns { ok, problems, fileCount }.
|
|
666
|
+
function reproducePinFromTree({ treeRoot, sidecar, tgzPath }, fsImpl = fs) {
|
|
667
|
+
const problems = [];
|
|
668
|
+
const recomputed = [];
|
|
669
|
+
for (const f of sidecar.files || []) {
|
|
670
|
+
const abs = path.join(treeRoot, ...String(f.path).split('/'));
|
|
671
|
+
let buf;
|
|
672
|
+
try { buf = fsImpl.readFileSync(abs); } catch { problems.push(`missing in artifact: ${f.path}`); continue; }
|
|
673
|
+
const sha = sha256hex(buf);
|
|
674
|
+
if (sha !== f.sha256) problems.push(`content mismatch: ${f.path}`);
|
|
675
|
+
recomputed.push({ path: f.path, mode: f.mode, sha256: sha });
|
|
676
|
+
}
|
|
677
|
+
const treeHash = canonicalTreeHash(recomputed);
|
|
678
|
+
if (problems.length === 0 && treeHash !== sidecar.tree_sha256) {
|
|
679
|
+
problems.push(`recomputed tree_sha256 ${treeHash.slice(0, 12)}… ≠ manifest ${String(sidecar.tree_sha256).slice(0, 12)}…`);
|
|
680
|
+
}
|
|
681
|
+
if (tgzPath && sidecar.tarball && sidecar.tarball.sha256) {
|
|
682
|
+
const tgzSha = sha256hex(fsImpl.readFileSync(tgzPath));
|
|
683
|
+
if (tgzSha !== sidecar.tarball.sha256) {
|
|
684
|
+
problems.push(`on-disk .tgz sha256 ${tgzSha.slice(0, 12)}… ≠ manifest.tarball.sha256 ${String(sidecar.tarball.sha256).slice(0, 12)}…`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return { ok: problems.length === 0, problems, fileCount: (sidecar.files || []).length };
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function selfVerify({ tgzPath, sidecar, outDir, boot }) {
|
|
691
|
+
console.error('');
|
|
692
|
+
console.error('[self-verify] verifying the built artifact reproduces its pin …');
|
|
693
|
+
|
|
694
|
+
// 1. reproduce the pin from the built .tgz.
|
|
695
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bongos-selfverify-'));
|
|
696
|
+
try {
|
|
697
|
+
let treeRoot;
|
|
698
|
+
const x = spawnSync('tar', ['-xzf', tgzPath, '-C', tmp], { encoding: 'utf8' });
|
|
699
|
+
if (!x.error && x.status === 0) {
|
|
700
|
+
treeRoot = path.join(tmp, TAR_ROOT.replace(/\/$/, '')); // the `package/` root inside the tarball
|
|
701
|
+
} else {
|
|
702
|
+
// tar unavailable — fall back to the on-disk stage dir (the SAME staged buffers written
|
|
703
|
+
// to the .tgz), so the pin is still recomputed from a materialized tree. Less faithful
|
|
704
|
+
// (won't catch a tar-writer bug), so it's a loud warning, not silent.
|
|
705
|
+
treeRoot = path.join(outDir, stageDirName(sidecar.core_version));
|
|
706
|
+
console.error(`[self-verify] ! tar unavailable (${x.error ? x.error.message : 'exit ' + x.status}); recomputing over the stage dir ${path.relative(REPO_ROOT, treeRoot)} instead of the unpacked .tgz.`);
|
|
707
|
+
if (!fs.existsSync(treeRoot)) { console.error('[self-verify] FAILED — neither `tar` nor the stage dir is available to recompute the pin.'); process.exit(1); }
|
|
708
|
+
}
|
|
709
|
+
const rep = reproducePinFromTree({ treeRoot, sidecar, tgzPath });
|
|
710
|
+
if (!rep.ok) {
|
|
711
|
+
console.error(`[self-verify] FAILED — the built artifact does NOT reproduce its pin (${rep.problems.length} problem(s)):`);
|
|
712
|
+
for (const p of rep.problems.slice(0, 20)) console.error(' ' + p);
|
|
713
|
+
if (rep.problems.length > 20) console.error(` … +${rep.problems.length - 20} more`);
|
|
714
|
+
process.exit(1);
|
|
715
|
+
}
|
|
716
|
+
console.error(`[self-verify] ✓ pin reproduced — tree_sha256 + tarball.sha256 match the manifest (${rep.fileCount} files).`);
|
|
717
|
+
} finally {
|
|
718
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// 2. bare-install boot (opt-in) — reuse the proven consumer-layout harness with CORE_TGZ.
|
|
722
|
+
if (boot) {
|
|
723
|
+
const harness = path.join(REPO_ROOT, 'tests', 'consumer_layout_boot.mjs');
|
|
724
|
+
if (!fs.existsSync(harness)) {
|
|
725
|
+
console.error('[self-verify] • bare-install boot skipped — tests/consumer_layout_boot.mjs not present (packaging from an installed core, where tests/ is not shipped).');
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
console.error('[self-verify] bare-install boot: `npm install <tgz>` + boot /healthz (via consumer_layout_boot, CORE_TGZ set) …');
|
|
729
|
+
const r = spawnSync(process.execPath, [harness], {
|
|
730
|
+
cwd: REPO_ROOT, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024,
|
|
731
|
+
env: { ...process.env, CORE_TGZ: tgzPath }, stdio: ['ignore', process.stderr, process.stderr],
|
|
732
|
+
});
|
|
733
|
+
if (r.error || r.status !== 0) {
|
|
734
|
+
console.error(`[self-verify] FAILED — the packaged core did not install + boot (${r.error ? r.error.message : 'exit ' + r.status}).`);
|
|
735
|
+
process.exit(1);
|
|
736
|
+
}
|
|
737
|
+
console.error('[self-verify] ✓ bare-install boot OK — the packaged core installs and serves /healthz.');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
548
741
|
function main() {
|
|
549
742
|
const ref = arg('--ref', 'HEAD');
|
|
550
743
|
const outDir = path.resolve(REPO_ROOT, arg('--out-dir', 'dist'));
|
|
@@ -564,8 +757,17 @@ function main() {
|
|
|
564
757
|
// dependency set, not the game's. Pulled out of the generic per-file loop below.
|
|
565
758
|
const SYNTHESIZED_PATHS = new Set(['package.json', 'package-lock.json']);
|
|
566
759
|
const entries = parseLsTree(git(['ls-tree', '-r', sha])).filter((e) => e.type === 'blob');
|
|
567
|
-
const
|
|
760
|
+
const selected = entries.filter((e) => isPublishable(e.path) && !SYNTHESIZED_PATHS.has(e.path));
|
|
761
|
+
// Drop npm-never-installed files (.gitignore, .npmignore, .npmrc) so the manifest +
|
|
762
|
+
// tree_sha256 pin describe exactly what `npm install` materializes — otherwise the
|
|
763
|
+
// consumer's verifyInstalledPin fails on the missing file and the vendored upgrade
|
|
764
|
+
// auto-rolls-back (idea 1000543). Done here, so they never reach the tarball or the pin.
|
|
765
|
+
const npmStripped = selected.filter((e) => isNpmStripped(e.path));
|
|
766
|
+
const publishable = selected.filter((e) => !isNpmStripped(e.path));
|
|
568
767
|
console.error(`[package-core] ${ref} = ${sha.slice(0, 12)} — ${entries.length} files, ${publishable.length} in the core (+2 synthesized), ${entries.length - publishable.length - 2} excluded.`);
|
|
768
|
+
if (npmStripped.length) {
|
|
769
|
+
console.error(`[package-core] dropped ${npmStripped.length} npm-never-installed file(s) from the pin (npm strips these on install; idea 1000543): ${npmStripped.map((e) => e.path).join(', ')}`);
|
|
770
|
+
}
|
|
569
771
|
if (publishable.length === 0) {
|
|
570
772
|
console.error('refusing: the core set is EMPTY — almost certainly a manifest/ref mismatch, not intentional. Aborting.');
|
|
571
773
|
process.exit(1);
|
|
@@ -595,11 +797,19 @@ function main() {
|
|
|
595
797
|
try { branding = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config/branding.json'), 'utf8')); }
|
|
596
798
|
catch { /* vanilla instance: derive from the public-IP floor only */ }
|
|
597
799
|
const ownerAliases = String(process.env.OWNER_ALIASES || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
598
|
-
|
|
800
|
+
// The NEUTRAL FLOOR (task 1002217 / audit H4): always inject the fixed prior-instance tokens so
|
|
801
|
+
// docs de-brand + the no-leak gate fires against them EVEN WITH NO branding.json (the F2 gap).
|
|
802
|
+
// Merged on top of this instance's own branding — for a branded instance the floor is harmless
|
|
803
|
+
// extra; for the neutral trunk it is the only thing standing between a doc and customer-zero's name.
|
|
804
|
+
const extra = {
|
|
805
|
+
owners: [...ownerAliases, ...PRIOR_INSTANCE_FLOOR.owners],
|
|
806
|
+
tokens: [...PRIOR_INSTANCE_FLOOR.tokens],
|
|
807
|
+
literals: dropletIp ? [dropletIp] : [],
|
|
808
|
+
};
|
|
599
809
|
const rules = buildRedactionRules(branding, extra);
|
|
600
810
|
const sensitiveLiterals = collectSensitiveLiterals(branding, extra);
|
|
601
811
|
const envSecrets = collectEnvSecrets();
|
|
602
|
-
console.error(`[package-core] ${rules.length} redaction rule(s), ${sensitiveLiterals.length} gate literal(s) + public-IP floor + ${envSecrets.length} env-secret literal(s).`);
|
|
812
|
+
console.error(`[package-core] ${rules.length} redaction rule(s), ${sensitiveLiterals.length} gate literal(s) + public-IP floor + ${envSecrets.length} env-secret literal(s) (incl. the neutral floor).`);
|
|
603
813
|
|
|
604
814
|
// 4. Process every file IN MEMORY (processCoreFile: docs redacted, functional verbatim)
|
|
605
815
|
// + the fail-closed no-leak gate. Nothing touches dist/ until the ENTIRE set is proven
|
|
@@ -710,6 +920,47 @@ function main() {
|
|
|
710
920
|
console.error(` manifest ${path.relative(REPO_ROOT, manifestPath)} ← the PIN a downstream repo records`);
|
|
711
921
|
console.error(` tree ${path.relative(REPO_ROOT, stageDir)}/`);
|
|
712
922
|
|
|
923
|
+
// Optional self-verify leg (task 1002218 / audit H3): prove the built artifact reproduces
|
|
924
|
+
// its pin (+ optionally installs & boots) BEFORE it can be trusted or published. Runs after
|
|
925
|
+
// the artifact is written; a failure exits non-zero so an unverified/broken artifact never
|
|
926
|
+
// reaches the publish leg below or a downstream consumer.
|
|
927
|
+
if (process.argv.includes('--self-verify')) {
|
|
928
|
+
selfVerify({ tgzPath, sidecar, outDir, boot: process.argv.includes('--self-verify-boot') });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Post-build leak scan (task 1002220 / audit H6): independent verification that the BUILT
|
|
932
|
+
// artifact carries no prior-instance identity, + a warn-level secret-shape scan over functional
|
|
933
|
+
// files. Runs on every build (skip with --no-leak-scan). Prior-instance literals are a WARNING
|
|
934
|
+
// by default and a hard FAILURE under --leak-scan-strict (the trunk still carries residual
|
|
935
|
+
// history until task 1002219 sweeps it + task 1002217 adds the fail-closed floor gate). A
|
|
936
|
+
// secret-shape hit is ALWAYS a non-fatal warning — the R88 functional-verbatim fix is preserved.
|
|
937
|
+
if (!process.argv.includes('--no-leak-scan')) {
|
|
938
|
+
const scanFiles = staged.map((s) => ({ path: s.name.startsWith(TAR_ROOT) ? s.name.slice(TAR_ROOT.length) : s.name, content: s.buf }));
|
|
939
|
+
const scan = scanArtifactForLeaks(scanFiles);
|
|
940
|
+
const strict = process.argv.includes('--leak-scan-strict');
|
|
941
|
+
if (scan.literalHits.length) {
|
|
942
|
+
const byLit = {};
|
|
943
|
+
for (const h of scan.literalHits) byLit[h.sample] = (byLit[h.sample] || 0) + 1;
|
|
944
|
+
const summary = Object.entries(byLit).sort((a, b) => b[1] - a[1]).map(([k, n]) => `${k}×${n}`).join(', ');
|
|
945
|
+
const files = [...new Set(scan.literalHits.map((h) => h.path))];
|
|
946
|
+
console.error('');
|
|
947
|
+
console.error(`[leak-scan] ${strict ? 'FAILED' : 'WARNING'} — ${scan.literalHits.length} prior-instance literal hit(s) across ${files.length} shipped file(s): ${summary}`);
|
|
948
|
+
for (const p of files.slice(0, 30)) console.error(` ${p}`);
|
|
949
|
+
if (files.length > 30) console.error(` … +${files.length - 30} more file(s)`);
|
|
950
|
+
if (strict) { console.error('[leak-scan] --leak-scan-strict: refusing to certify a leaky artifact.'); process.exit(1); }
|
|
951
|
+
console.error('[leak-scan] non-fatal for now — becomes a hard gate once the trunk sweep (task 1002219) + the fail-closed floor gate (task 1002217) land. Run --leak-scan-strict to enforce today.');
|
|
952
|
+
} else {
|
|
953
|
+
console.error('[leak-scan] ✓ no prior-instance literals in the built artifact.');
|
|
954
|
+
}
|
|
955
|
+
if (scan.secretWarnings.length) {
|
|
956
|
+
console.error(`[leak-scan] ! ${scan.secretWarnings.length} functional file(s) carry a credential SHAPE (WARNING, not fatal — confirm none is a real credential; a real one must be removed at SOURCE):`);
|
|
957
|
+
for (const w of scan.secretWarnings.slice(0, 20)) console.error(` ${w.path} [${w.shapes.join(', ')}]`);
|
|
958
|
+
if (scan.secretWarnings.length > 20) console.error(` … +${scan.secretWarnings.length - 20} more`);
|
|
959
|
+
} else {
|
|
960
|
+
console.error('[leak-scan] ✓ no committed-credential shapes in functional files.');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
713
964
|
// Optional publish leg (task 2089): --publish runs `npm publish` on the built
|
|
714
965
|
// tarball. DRY-RUN by default; the real, permanent publish needs --publish --live
|
|
715
966
|
// (+ npm auth). Kept AFTER the artifact is written so a publish failure never
|
|
@@ -730,12 +981,20 @@ module.exports = {
|
|
|
730
981
|
octal, tarChecksum, ustarHeader, splitUstarName, buildTar,
|
|
731
982
|
// task 1824 — the consumer-core file-classification layer (pure; unit-tested)
|
|
732
983
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
984
|
+
// idea 1000543 — npm-never-installed files dropped from the pin so vendored `bongos upgrade` pin-verify matches
|
|
985
|
+
isNpmStripped, NPM_STRIPPED_BASENAMES,
|
|
733
986
|
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub so a consumer's node_modules can't auto-load core memory
|
|
734
987
|
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
735
988
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
736
989
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
737
990
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
|
738
991
|
publishArgs,
|
|
992
|
+
// task 1002218 (audit H3) — build-time self-verify (reproduce pin + optional bare-install boot)
|
|
993
|
+
selfVerify, reproducePinFromTree,
|
|
994
|
+
// task 1002220 (audit H6) — post-build leak scan (prior-instance literals + functional secret shapes)
|
|
995
|
+
scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
|
|
996
|
+
// task 1002217 (audit H4) — the fixed neutral floor injected into the doc-redaction + fail-closed gate
|
|
997
|
+
PRIOR_INSTANCE_FLOOR,
|
|
739
998
|
};
|
|
740
999
|
|
|
741
1000
|
if (require.main === module) main();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|