@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
package/tests/package_core.mjs
CHANGED
|
@@ -16,6 +16,9 @@ import { strict as assert } from 'node:assert';
|
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import zlib from 'node:zlib';
|
|
18
18
|
import crypto from 'node:crypto';
|
|
19
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
20
|
+
import { tmpdir } from 'node:os';
|
|
21
|
+
import { join, dirname } from 'node:path';
|
|
19
22
|
|
|
20
23
|
const require = createRequire(import.meta.url);
|
|
21
24
|
const m = require('../scripts/gds/package-core.js');
|
|
@@ -25,14 +28,22 @@ const {
|
|
|
25
28
|
ARTIFACT_NAME, MANIFEST_SCHEMA, TAR_ROOT,
|
|
26
29
|
// task 1824 — the consumer-core file-classification layer
|
|
27
30
|
isDocPath, functionalSecretLeaks, processCoreFile,
|
|
31
|
+
// idea 1000543 — npm-never-installed files dropped from the pin
|
|
32
|
+
isNpmStripped,
|
|
28
33
|
// task 2121 — agent-doc (CLAUDE.md/AGENTS.md) stub
|
|
29
34
|
isAgentDoc, CORE_AGENT_DOC_STUB,
|
|
30
35
|
// task 1882 (ADR 0108 §2) — the real installable npm package
|
|
31
36
|
CORE_DEP_NAMES, buildPackageJson, resolveLockDep, lockfileClosure, prunePackageLock,
|
|
32
37
|
// task 2089 (ADR 0134) — private-by-default npm publish
|
|
33
38
|
publishArgs,
|
|
39
|
+
// task 1002218 (audit H3) — build-time self-verify pin reproduction
|
|
40
|
+
reproducePinFromTree,
|
|
41
|
+
// task 1002220 (audit H6) — post-build leak scan
|
|
42
|
+
scanArtifactForLeaks, PRIOR_INSTANCE_LITERALS,
|
|
43
|
+
// task 1002217 (audit H4) — the fixed neutral floor
|
|
44
|
+
PRIOR_INSTANCE_FLOOR,
|
|
34
45
|
} = m;
|
|
35
|
-
const { buildRedactionRules, collectSensitiveLiterals } = require('../scripts/gds/mirror-redact.js');
|
|
46
|
+
const { buildRedactionRules, collectSensitiveLiterals, redactText, findLeaks } = require('../scripts/gds/mirror-redact.js');
|
|
36
47
|
|
|
37
48
|
// A synthetic instance brand for the classification tests (never the real OTB literals).
|
|
38
49
|
const TEST_BRANDING = {
|
|
@@ -485,5 +496,239 @@ t('prunePackageLock: on the real repo lockfile, drops the vast majority of packa
|
|
|
485
496
|
assert.ok(keptCount > 0);
|
|
486
497
|
});
|
|
487
498
|
|
|
499
|
+
// ---- task 1002218 (audit H3): build-time self-verify pin reproduction ----
|
|
500
|
+
// Build a tiny unpacked tree + a matching sidecar manifest (same hash fns the packager uses),
|
|
501
|
+
// then prove reproducePinFromTree accepts a faithful reproduction and rejects each tamper.
|
|
502
|
+
function makeTreeAndSidecar() {
|
|
503
|
+
const root = mkdtempSync(join(tmpdir(), 'pc-selfverify-'));
|
|
504
|
+
const files = [
|
|
505
|
+
{ path: 'src/a.js', content: 'module.exports = 1\n', mode: '0000644' },
|
|
506
|
+
{ path: 'sub/dir/b.json', content: '{"k":2}\n', mode: '0000644' },
|
|
507
|
+
{ path: 'bin/run.sh', content: '#!/bin/sh\necho hi\n', mode: '0000755' },
|
|
508
|
+
];
|
|
509
|
+
for (const f of files) {
|
|
510
|
+
const abs = join(root, ...f.path.split('/'));
|
|
511
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
512
|
+
writeFileSync(abs, f.content);
|
|
513
|
+
}
|
|
514
|
+
const meta = files
|
|
515
|
+
.map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: sha256hex(Buffer.from(f.content)) }))
|
|
516
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
517
|
+
const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(meta), files: meta, tarball: { sha256: 'abc123', bytes: 0 } };
|
|
518
|
+
return { root, sidecar, files };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
t('reproducePinFromTree: a faithful tree reproduces the pin (ok, no problems)', () => {
|
|
522
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
523
|
+
try {
|
|
524
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar }); // no tgzPath → skip the tarball-bytes check
|
|
525
|
+
assert.equal(r.ok, true, r.problems.join('; '));
|
|
526
|
+
assert.equal(r.fileCount, 3);
|
|
527
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
t('reproducePinFromTree: a tampered file content → not ok (content mismatch)', () => {
|
|
531
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
532
|
+
try {
|
|
533
|
+
writeFileSync(join(root, 'src', 'a.js'), 'module.exports = 666 // tampered\n');
|
|
534
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
535
|
+
assert.equal(r.ok, false);
|
|
536
|
+
assert.ok(r.problems.some((p) => /content mismatch: src\/a\.js/.test(p)), r.problems.join('; '));
|
|
537
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
t('reproducePinFromTree: a missing file → not ok (missing in artifact)', () => {
|
|
541
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
542
|
+
try {
|
|
543
|
+
rmSync(join(root, 'sub', 'dir', 'b.json'));
|
|
544
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
545
|
+
assert.equal(r.ok, false);
|
|
546
|
+
assert.ok(r.problems.some((p) => /missing in artifact: sub\/dir\/b\.json/.test(p)), r.problems.join('; '));
|
|
547
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
t('reproducePinFromTree: a manifest whose tree_sha256 lies → not ok (tree hash mismatch)', () => {
|
|
551
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
552
|
+
try {
|
|
553
|
+
const lying = { ...sidecar, tree_sha256: 'f'.repeat(64) }; // every file matches, but the recorded hash is wrong
|
|
554
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar: lying });
|
|
555
|
+
assert.equal(r.ok, false);
|
|
556
|
+
assert.ok(r.problems.some((p) => /recomputed tree_sha256/.test(p)), r.problems.join('; '));
|
|
557
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
t('reproducePinFromTree: a .tgz whose bytes differ from manifest.tarball.sha256 → not ok', () => {
|
|
561
|
+
const { root, sidecar } = makeTreeAndSidecar();
|
|
562
|
+
const tgz = join(root, 'artifact.tgz');
|
|
563
|
+
try {
|
|
564
|
+
writeFileSync(tgz, Buffer.from('the actual tarball bytes'));
|
|
565
|
+
const withTb = { ...sidecar, tarball: { sha256: sha256hex(Buffer.from('DIFFERENT bytes')), bytes: 24 } };
|
|
566
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar: withTb, tgzPath: tgz });
|
|
567
|
+
assert.equal(r.ok, false);
|
|
568
|
+
assert.ok(r.problems.some((p) => /\.tgz sha256/.test(p)), r.problems.join('; '));
|
|
569
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ---- task 1002220 (audit H6): post-build leak scan ----
|
|
573
|
+
t('scanArtifactForLeaks: flags a prior-instance literal in any shipped file', () => {
|
|
574
|
+
const files = [
|
|
575
|
+
{ path: 'src/x.js', content: 'const base = "https://amazonprimea.com";\n' },
|
|
576
|
+
{ path: 'docs/readme.md', content: 'Owned by lars589.\n' },
|
|
577
|
+
{ path: 'src/clean.js', content: 'export const ok = true;\n' },
|
|
578
|
+
];
|
|
579
|
+
const r = scanArtifactForLeaks(files);
|
|
580
|
+
const hitPaths = r.literalHits.map((h) => h.path);
|
|
581
|
+
assert.ok(hitPaths.includes('src/x.js'), 'amazonprimea in a functional file must be flagged');
|
|
582
|
+
assert.ok(hitPaths.includes('docs/readme.md'), 'owner login in a doc must be flagged too (literal scan spans all files)');
|
|
583
|
+
assert.ok(!hitPaths.includes('src/clean.js'), 'a clean file must not be flagged');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
t('scanArtifactForLeaks: flags a public IP but not a private/localhost one', () => {
|
|
587
|
+
const r = scanArtifactForLeaks([
|
|
588
|
+
{ path: 'a.js', content: 'host = 104.236.254.243\n' }, // public → flagged
|
|
589
|
+
{ path: 'b.js', content: 'host = 127.0.0.1; lan = 10.0.0.5\n' }, // localhost + private → not
|
|
590
|
+
]);
|
|
591
|
+
const byPath = Object.fromEntries(r.literalHits.map((h) => [h.path, h.label]));
|
|
592
|
+
assert.ok(byPath['a.js'] === 'public-ip', `expected a public-ip hit on a.js, got ${JSON.stringify(r.literalHits)}`);
|
|
593
|
+
assert.ok(!('b.js' in byPath), 'localhost/private IPs must not be flagged');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
t('scanArtifactForLeaks: credential SHAPE warning fires on functional files, not docs', () => {
|
|
597
|
+
const key = `sk-ant-${'A1b2C3d4E5f6G7h8'}`; // a fake Anthropic-shaped token
|
|
598
|
+
const r = scanArtifactForLeaks([
|
|
599
|
+
{ path: 'src/leak.js', content: `const k = "${key}";\n` },
|
|
600
|
+
{ path: 'docs/guide.md', content: `example key: ${key}\n` }, // docs are redacted+gated elsewhere → not a shape warning here
|
|
601
|
+
]);
|
|
602
|
+
const warnPaths = r.secretWarnings.map((w) => w.path);
|
|
603
|
+
assert.ok(warnPaths.includes('src/leak.js'), 'a credential shape in a functional file must warn');
|
|
604
|
+
assert.ok(!warnPaths.includes('docs/guide.md'), 'docs are out of scope for the credential-shape warning');
|
|
605
|
+
const w = r.secretWarnings.find((x) => x.path === 'src/leak.js');
|
|
606
|
+
assert.ok(w.shapes.includes('anthropic-key'), `expected anthropic-key shape, got ${JSON.stringify(w.shapes)}`);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
t('scanArtifactForLeaks: ordinary long identifiers / base64 hashes do NOT warn (low false-positive)', () => {
|
|
610
|
+
const r = scanArtifactForLeaks([
|
|
611
|
+
{ path: 'src/normal.js', content: 'const integrityHash = "sha512-Abc123Def456Ghi789Jkl012Mno345Pqr678";\nfunction reallyLongDescriptiveFunctionName() {}\n' },
|
|
612
|
+
{ path: 'data.json', content: JSON.stringify({ sha256: 'a'.repeat(64), token: 'short' }) },
|
|
613
|
+
]);
|
|
614
|
+
assert.equal(r.secretWarnings.length, 0, `no credential shapes expected, got ${JSON.stringify(r.secretWarnings)}`);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
t('PRIOR_INSTANCE_LITERALS: carries the customer-zero tokens the neutral trunk must not ship', () => {
|
|
618
|
+
assert.ok(Array.isArray(PRIOR_INSTANCE_LITERALS) && PRIOR_INSTANCE_LITERALS.length >= 3);
|
|
619
|
+
// Case-insensitive: the floor tokens are lowercased for the redaction rules + findLeaks
|
|
620
|
+
// (both case-insensitive), so assert coverage, not exact case.
|
|
621
|
+
const lower = PRIOR_INSTANCE_LITERALS.map((s) => s.toLowerCase());
|
|
622
|
+
for (const lit of ['amazonprimea', 'off the boats', 'lars589']) {
|
|
623
|
+
assert.ok(lower.includes(lit), `floor list should cover ${lit}`);
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// ---- task 1002217 (audit H4): the neutral floor gate (fails closed with no branding.json) ----
|
|
628
|
+
t('neutral floor: with EMPTY branding + the floor as extra, docs de-brand and the gate covers the floor', () => {
|
|
629
|
+
// The F2 gap: with no branding.json, buildRedactionRules({})/collectSensitiveLiterals({}) are
|
|
630
|
+
// empty, so a doc keeps its prior-instance identity. Injecting PRIOR_INSTANCE_FLOOR as `extra`
|
|
631
|
+
// (exactly what package-core's main() now does) closes it.
|
|
632
|
+
const rules = buildRedactionRules({}, PRIOR_INSTANCE_FLOOR);
|
|
633
|
+
const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
|
|
634
|
+
const doc = 'See https://amazonprimea.com, owned by lars589 — the Off the Boats world.';
|
|
635
|
+
const red = redactText(doc, rules);
|
|
636
|
+
assert.ok(!/amazonprimea/i.test(red), `amazonprimea survived: ${red}`);
|
|
637
|
+
assert.ok(!/lars589/i.test(red), `owner login survived: ${red}`);
|
|
638
|
+
assert.ok(!/off the boats/i.test(red), `product name survived: ${red}`);
|
|
639
|
+
assert.deepEqual(findLeaks(red, lits), [], `gate still sees a floor leak after redaction: ${JSON.stringify(findLeaks(red, lits))}`);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
t('neutral floor: the fail-closed gate WOULD catch an un-redacted survivor (empty branding)', () => {
|
|
643
|
+
const lits = collectSensitiveLiterals({}, PRIOR_INSTANCE_FLOOR);
|
|
644
|
+
// If a doc reached the gate still carrying a floor literal, findLeaks (the fail-closed detector)
|
|
645
|
+
// flags it — which aborts the build in package-core (atomic, produces nothing).
|
|
646
|
+
assert.ok(findLeaks('leftover amazonprimea reference', lits).length > 0, 'floor token must be gated');
|
|
647
|
+
assert.ok(findLeaks('signed off by lars589', lits).length > 0, 'floor owner must be gated');
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
t('neutral floor: WITHOUT the floor, empty branding leaks (proves the floor is what closes F2)', () => {
|
|
651
|
+
// The counterfactual — the pre-H4 neutral-trunk behavior: empty branding + no floor = no rules,
|
|
652
|
+
// no gate literals, so the doc ships its prior-instance identity verbatim.
|
|
653
|
+
const rules = buildRedactionRules({});
|
|
654
|
+
const lits = collectSensitiveLiterals({});
|
|
655
|
+
const doc = 'amazonprimea + lars589';
|
|
656
|
+
assert.equal(redactText(doc, rules).includes('amazonprimea'), true, 'no-floor redaction is a no-op (the F2 gap)');
|
|
657
|
+
assert.equal(findLeaks(doc, lits).length, 0, 'no-floor gate sees nothing (the F2 gap)');
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// ---- idea 1000543: npm-never-installed files are dropped from the pin ----
|
|
661
|
+
// npm strips .gitignore/.npmignore/.npmrc from an installed package, so a pin that
|
|
662
|
+
// LISTS one fails when a consumer recomputes the tree hash over node_modules/@bongos/core
|
|
663
|
+
// (verifyInstalledPin, upgrade.js) -> the vendored `bongos upgrade` auto-rolls-back.
|
|
664
|
+
t('isNpmStripped: matches .gitignore/.npmignore/.npmrc at root and any depth', () => {
|
|
665
|
+
assert.equal(isNpmStripped('.gitignore'), true);
|
|
666
|
+
assert.equal(isNpmStripped('.npmignore'), true);
|
|
667
|
+
assert.equal(isNpmStripped('.npmrc'), true);
|
|
668
|
+
assert.equal(isNpmStripped('modules/economy/.gitignore'), true); // npm strips nested ones too
|
|
669
|
+
assert.equal(isNpmStripped('a/b/c/.npmrc'), true);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
t('isNpmStripped: does NOT match ordinary files or basename lookalikes', () => {
|
|
673
|
+
assert.equal(isNpmStripped('src/index.js'), false);
|
|
674
|
+
assert.equal(isNpmStripped('package.json'), false);
|
|
675
|
+
assert.equal(isNpmStripped('docs/.gitignore.md'), false); // .gitignore is not the basename here
|
|
676
|
+
assert.equal(isNpmStripped('gitignore'), false); // no leading dot
|
|
677
|
+
assert.equal(isNpmStripped('src/.gitignoreignore'), false);
|
|
678
|
+
assert.equal(isNpmStripped(''), false);
|
|
679
|
+
assert.equal(isNpmStripped(null), false);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
t('the packager selection drops the stripped files from the pinned set (models main())', () => {
|
|
683
|
+
const treePaths = ['.gitignore', 'src/a.js', 'migrations/001.sql', 'modules/x/.npmrc', 'README.md'];
|
|
684
|
+
const kept = treePaths.filter((p) => !isNpmStripped(p)); // exactly main()'s `publishable` filter
|
|
685
|
+
assert.deepEqual(kept, ['src/a.js', 'migrations/001.sql', 'README.md']);
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
// End-to-end proof against the consume-side recompute. reproducePinFromTree and upgrade.js's
|
|
689
|
+
// verifyInstalledPin both iterate manifest.files[] and rebuild canonicalTreeHash, so a manifest
|
|
690
|
+
// that LISTS .gitignore fails when npm has stripped it (the bug); a manifest built through the
|
|
691
|
+
// isNpmStripped filter does not list it and reproduces cleanly (the fix).
|
|
692
|
+
function buildManifestAndNpmInstalledTree({ includeGitignore }) {
|
|
693
|
+
const root = mkdtempSync(join(tmpdir(), 'pc-npmstrip-'));
|
|
694
|
+
const treeFiles = [ // the FULL git tree the packager enumerates, incl. a tracked .gitignore
|
|
695
|
+
{ path: '.gitignore', content: 'dist/\nnode_modules/\n' },
|
|
696
|
+
{ path: 'src/a.js', content: 'module.exports = 1\n' },
|
|
697
|
+
{ path: 'package.json', content: '{"name":"@bongos/core"}\n' },
|
|
698
|
+
];
|
|
699
|
+
// What the packager PINS: pre-fix keeps everything; post-fix drops the npm-stripped files.
|
|
700
|
+
const pinned = treeFiles.filter((f) => (includeGitignore ? true : !isNpmStripped(f.path)));
|
|
701
|
+
const files = pinned
|
|
702
|
+
.map((f) => ({ path: f.path, mode: normalizeMode('100644'), sha256: sha256hex(Buffer.from(f.content)) }))
|
|
703
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
704
|
+
const sidecar = { core_version: '9.9.9', tree_sha256: canonicalTreeHash(files), files };
|
|
705
|
+
// What `npm install` ACTUALLY materializes: everything EXCEPT the files npm strips.
|
|
706
|
+
for (const f of treeFiles) {
|
|
707
|
+
if (isNpmStripped(f.path)) continue; // npm never writes .gitignore into the installed package
|
|
708
|
+
const abs = join(root, ...f.path.split('/'));
|
|
709
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
710
|
+
writeFileSync(abs, f.content);
|
|
711
|
+
}
|
|
712
|
+
return { root, sidecar };
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
t('idea 1000543 (the bug): a manifest that PINS .gitignore FAILS against an npm-installed tree', () => {
|
|
716
|
+
const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: true });
|
|
717
|
+
try {
|
|
718
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
719
|
+
assert.equal(r.ok, false, 'pinning .gitignore must fail once npm has stripped it');
|
|
720
|
+
assert.ok(r.problems.some((p) => /\.gitignore/.test(p)), `expected a .gitignore problem, got: ${r.problems.join('; ')}`);
|
|
721
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
t('idea 1000543 (the fix): the isNpmStripped filter makes the pin reproduce against the npm-installed tree', () => {
|
|
725
|
+
const { root, sidecar } = buildManifestAndNpmInstalledTree({ includeGitignore: false });
|
|
726
|
+
try {
|
|
727
|
+
assert.ok(!sidecar.files.some((f) => f.path === '.gitignore'), 'the fixed manifest must not list .gitignore');
|
|
728
|
+
const r = reproducePinFromTree({ treeRoot: root, sidecar });
|
|
729
|
+
assert.equal(r.ok, true, `the fixed manifest must reproduce; problems: ${r.problems.join('; ')}`);
|
|
730
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
731
|
+
});
|
|
732
|
+
|
|
488
733
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
489
734
|
process.exit(failed ? 1 : 0);
|
package/tests/provision.mjs
CHANGED
|
@@ -100,6 +100,35 @@ t('standalone step generators: root under standaloneBase + pull / npm-ci / insta
|
|
|
100
100
|
assert.match(P.standaloneMigrateCmd(inst), /PGDATABASE=mercury MEDUSA_GDS_ONLY=1 bash node_modules\/@bongos\/core\/scripts\/migrate\.sh/);
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
+
// ── nightly DB backup units (task 2213 — productizes the manual 2210 fix) ──────
|
|
104
|
+
t('backup units: per-instance names + the DB set on the unit (fixes the task-2210 unset GDS_BACKUP_DB)', () => {
|
|
105
|
+
assert.equal(P.backupUnitName(coTenant), 'mercury-db-backup');
|
|
106
|
+
assert.equal(P.backupServicePath(coTenant), '/etc/systemd/system/mercury-db-backup.service');
|
|
107
|
+
assert.equal(P.backupTimerPath(coTenant), '/etc/systemd/system/mercury-db-backup.timer');
|
|
108
|
+
const svc = P.backupService(coTenant);
|
|
109
|
+
// the DB is pinned on the unit → no fallback to the nonexistent amazonprimea default
|
|
110
|
+
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
111
|
+
assert.match(svc, /Environment=GDS_BACKUP_DIR=\/var\/backups\/gds/);
|
|
112
|
+
assert.match(svc, new RegExp(`User=${reEsc('lars')}`), 'runs as the app user (mirrors serviceUnit)');
|
|
113
|
+
assert.match(svc, /ReadWritePaths=\/var\/backups\/gds/, 'the only writable path is the backup dir');
|
|
114
|
+
const timer = P.backupTimer(coTenant);
|
|
115
|
+
assert.match(timer, /OnCalendar=\*-\*-\* 04:00:00 UTC/);
|
|
116
|
+
assert.match(timer, /Unit=mercury-db-backup\.service/, 'the timer drives the per-instance service');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
t('backupScriptPath resolves the CONSUMER LAYOUT, never the old /home/lars/amazonprimea monorepo path', () => {
|
|
120
|
+
// standalone → the script inside the instance's OWN pinned @bongos/core
|
|
121
|
+
const standalone = { ...coTenant, hosting_shape: 'standalone' };
|
|
122
|
+
const sPath = P.backupScriptPath(standalone);
|
|
123
|
+
assert.ok(sPath.startsWith(P.standaloneRoot(standalone) + '/'), 'under the instance root');
|
|
124
|
+
assert.match(sPath, /node_modules\/@bongos\/core\/scripts\/gds\/db-backup-nightly\.sh$/);
|
|
125
|
+
// co-tenant → this control-plane checkout's scripts/gds (mirrors serviceUnit's REPO_ROOT split)
|
|
126
|
+
assert.match(P.backupScriptPath(coTenant), /\/scripts\/gds\/db-backup-nightly\.sh$/);
|
|
127
|
+
// the regression this whole task fixes: NEVER the hardcoded old monorepo path
|
|
128
|
+
assert.doesNotMatch(P.backupService(standalone), /\/home\/lars\/amazonprimea/);
|
|
129
|
+
assert.doesNotMatch(P.backupService(coTenant), /\/home\/lars\/amazonprimea/);
|
|
130
|
+
});
|
|
131
|
+
|
|
103
132
|
t('webEnvBody: OAuth creds use the LOADED pack\'s envPrefix (not the slug) + brand/port/db', () => {
|
|
104
133
|
const b = P.webEnvBody(coTenant); // coTenant.domain === null
|
|
105
134
|
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=`));
|
|
@@ -280,6 +309,27 @@ await ta('provisionInstance(co-tenant) composes db → migrate → service → h
|
|
|
280
309
|
assert.ok(writes.some((w) => /\/mercury\/web\.env$/.test(w)), 'wrote web.env');
|
|
281
310
|
assert.ok(writes.some((w) => /\/mercury\.service$/.test(w)), 'wrote systemd unit');
|
|
282
311
|
assert.ok(writes.some((w) => /\/mercury\.caddy$/.test(w)), 'wrote per-instance Caddy snippet');
|
|
312
|
+
// task 2213: EVERY instance is born with backups — dir + per-instance timer/service
|
|
313
|
+
assert.match(joined, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/, 'creates the backup dir');
|
|
314
|
+
assert.match(joined, /systemctl enable --now mercury-db-backup\.timer/, 'enables the per-instance backup timer');
|
|
315
|
+
assert.ok(writes.some((w) => /\/mercury-db-backup\.service$/.test(w)), 'wrote backup service unit');
|
|
316
|
+
assert.ok(writes.some((w) => /\/mercury-db-backup\.timer$/.test(w)), 'wrote backup timer unit');
|
|
317
|
+
// the backup timer is enabled AFTER the dir exists (else the first run exits 2)
|
|
318
|
+
assert.ok(idx(/install -d .*\/var\/backups\/gds/) < idx(/enable --now mercury-db-backup\.timer/), 'dir before timer enable');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await ta('provisionInstance(standalone): the backup unit ExecStart points at the instance\'s OWN pinned core (consumer layout)', async () => {
|
|
322
|
+
const { deps, writes } = recorder();
|
|
323
|
+
// capture the unit BODIES, not just paths, so we can assert the ExecStart path
|
|
324
|
+
const bodies = {};
|
|
325
|
+
deps.writeFile = (p, body) => { writes.push(p); bodies[p] = body; return { ok: true }; };
|
|
326
|
+
const inst = { ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' };
|
|
327
|
+
await P.provisionInstance(inst, deps);
|
|
328
|
+
const svc = bodies[P.backupServicePath(inst)];
|
|
329
|
+
assert.ok(svc, 'wrote the backup service unit');
|
|
330
|
+
const root = P.standaloneRoot(inst);
|
|
331
|
+
assert.match(svc, new RegExp('ExecStart=/bin/bash ' + reEsc(root) + '/node_modules/@bongos/core/scripts/gds/db-backup-nightly\\.sh'));
|
|
332
|
+
assert.match(svc, /Environment=GDS_BACKUP_DB=mercury/);
|
|
283
333
|
});
|
|
284
334
|
|
|
285
335
|
await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + npm ci) + migrates from the instance root', async () => {
|
|
@@ -427,6 +477,22 @@ await ta('provisionInstance(dedicated) does NOT run local createdb/migrate/seed
|
|
|
427
477
|
assert.ok(!cmds.some((c) => /INSERT INTO versions/.test(c)), 'dedicated does not run the version seed locally (it rides cloud-init)');
|
|
428
478
|
});
|
|
429
479
|
|
|
480
|
+
await ta('dedicatedUserData bakes the nightly backup: unit files (b64) + dir + timer enable (task 2213)', async () => {
|
|
481
|
+
const inst = { ...coTenant, hosting_shape: 'dedicated' };
|
|
482
|
+
const cloudInit = P.dedicatedUserData(inst);
|
|
483
|
+
// the two unit files land via write_files (base64, so no YAML block-scalar escaping)
|
|
484
|
+
assert.match(cloudInit, /write_files:/);
|
|
485
|
+
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupServicePath(inst))));
|
|
486
|
+
assert.match(cloudInit, new RegExp('path: ' + reEsc(P.backupTimerPath(inst))));
|
|
487
|
+
assert.match(cloudInit, /encoding: b64/);
|
|
488
|
+
// the base64 content round-trips to a real unit carrying the right DB
|
|
489
|
+
const b64 = cloudInit.match(/content: ([A-Za-z0-9+/=]+)/)[1];
|
|
490
|
+
assert.match(Buffer.from(b64, 'base64').toString('utf8'), /Environment=GDS_BACKUP_DB=mercury/);
|
|
491
|
+
// and the dir is created + the timer enabled on first boot
|
|
492
|
+
assert.match(cloudInit, /install -d -o lars -g lars -m 0750 \/var\/backups\/gds/);
|
|
493
|
+
assert.match(cloudInit, /enable --now mercury mercury-db-backup\.timer/);
|
|
494
|
+
});
|
|
495
|
+
|
|
430
496
|
console.log('\nDNS UPSERT composition (P4, CONFIG override):');
|
|
431
497
|
|
|
432
498
|
// A recorder that also captures each exec's env (so we can assert the DNS hook env).
|
|
@@ -739,6 +805,9 @@ await ta('teardownInstance(privileged): systemctl + rm-under-/etc are sudo-prefi
|
|
|
739
805
|
assert.match(joined, /sudo systemctl disable --now mercury/);
|
|
740
806
|
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury\.service/);
|
|
741
807
|
assert.match(joined, /sudo systemctl daemon-reload/);
|
|
808
|
+
// task 2213: the backup units come down too (the dumps + dir are left in place)
|
|
809
|
+
assert.match(joined, /sudo systemctl disable --now mercury-db-backup\.timer/);
|
|
810
|
+
assert.match(joined, /sudo rm -f \/etc\/systemd\/system\/mercury-db-backup\.service \/etc\/systemd\/system\/mercury-db-backup\.timer/);
|
|
742
811
|
});
|
|
743
812
|
|
|
744
813
|
await ta('teardownInstance(default): NO sudo (bare, unchanged)', async () => {
|
|
@@ -900,5 +969,162 @@ await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE
|
|
|
900
969
|
assert.equal(box.length, 0, 'nothing crosses to the co-hosting box for a standalone teardown');
|
|
901
970
|
});
|
|
902
971
|
|
|
972
|
+
console.log('\ncross-site identity federation (ADR 0141 §4, task #1002109):');
|
|
973
|
+
|
|
974
|
+
// Fakes for the hub-side module handles provisioning is injected. hubKeysOn = a
|
|
975
|
+
// hub-configured control plane; hubKeysOff = signing key unset. fakePI records every
|
|
976
|
+
// registerClient call so we can assert the (security-critical) origin binding.
|
|
977
|
+
const hubKeysOn = { hubSigningConfigured: () => true, hubPublicKeyB64: () => 'HUBPUBKEYB64==' };
|
|
978
|
+
const hubKeysOff = { hubSigningConfigured: () => false, hubPublicKeyB64: () => null };
|
|
979
|
+
function fakePI() { const calls = []; return { calls, registerClient: async (a) => { calls.push(a); return { client_id: a.clientId }; } }; }
|
|
980
|
+
function withEnv(vars, fn) {
|
|
981
|
+
const saved = {};
|
|
982
|
+
for (const k of Object.keys(vars)) { saved[k] = process.env[k]; if (vars[k] === undefined) delete process.env[k]; else process.env[k] = vars[k]; }
|
|
983
|
+
const restore = () => { for (const k of Object.keys(saved)) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } };
|
|
984
|
+
// Promise-aware: for an async fn, restore only AFTER it settles (a plain try/finally
|
|
985
|
+
// would restore synchronously, before the awaited body ever reads the env).
|
|
986
|
+
let result;
|
|
987
|
+
try { result = fn(); } catch (e) { restore(); throw e; }
|
|
988
|
+
if (result && typeof result.then === 'function') return result.then((v) => { restore(); return v; }, (e) => { restore(); throw e; });
|
|
989
|
+
restore();
|
|
990
|
+
return result;
|
|
991
|
+
}
|
|
992
|
+
const HUB_ENV = { PROVISION_HUB_ORIGIN: 'https://cloudbongos.com', [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined };
|
|
993
|
+
const coTenantWithDomain = { ...coTenant, domain: 'mercury.example.com' };
|
|
994
|
+
|
|
995
|
+
t('federationHubOrigin: explicit PROVISION_HUB_ORIGIN wins; falls back to <PREFIX>_PUBLIC_ORIGIN; strips trailing slash; null when neither', () => {
|
|
996
|
+
withEnv({ PROVISION_HUB_ORIGIN: 'https://hub.example/', [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: 'https://own.example' }, () => {
|
|
997
|
+
assert.equal(P.federationHubOrigin(), 'https://hub.example', 'explicit wins + trailing slash stripped');
|
|
998
|
+
});
|
|
999
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: 'https://own.example/' }, () => {
|
|
1000
|
+
assert.equal(P.federationHubOrigin(), 'https://own.example', 'falls back to the control plane\'s own public origin');
|
|
1001
|
+
});
|
|
1002
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined }, () => {
|
|
1003
|
+
assert.equal(P.federationHubOrigin(), null, 'null when nothing is configured (→ not hub-configured)');
|
|
1004
|
+
});
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
t('shouldFederate: dormant unless hub-configured AND co-tenant AND a domain is set', () => {
|
|
1008
|
+
withEnv(HUB_ENV, () => {
|
|
1009
|
+
// the happy path
|
|
1010
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, true, 'co-tenant + hub-configured + domain → federate');
|
|
1011
|
+
// each guard, in turn
|
|
1012
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: null, hubKeys: hubKeysOn }).federate, false, 'module absent (not the hub) → skip');
|
|
1013
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOff }).federate, false, 'no signing key → skip');
|
|
1014
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, domain: null }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'no domain → skip');
|
|
1015
|
+
// a standalone (owner-hosted) keeps its own GitHub app; a dedicated droplet has no local web.env bake yet
|
|
1016
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'standalone' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'standalone = self-hosted → skip');
|
|
1017
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'dedicated' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'dedicated → skip (no local web.env yet)');
|
|
1018
|
+
});
|
|
1019
|
+
// no hub origin resolvable (even with a signing key) → skip
|
|
1020
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined }, () => {
|
|
1021
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'no hub origin → skip');
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
t('webEnvBody(federation): flips <PREFIX>_AUTH_IDP + writes the HUB_* block; GitHub creds stay blank; NO block without federation', () => {
|
|
1026
|
+
const federation = { hubOrigin: 'https://cloudbongos.com', clientId: 'mercury', clientSecret: 'S'.repeat(43), hubPublicKey: 'PUB==' };
|
|
1027
|
+
const b = P.webEnvBody(coTenantWithDomain, { federation });
|
|
1028
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_AUTH_IDP=true`), 'flips the federation switch auth.js idpEnabled() reads');
|
|
1029
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_ORIGIN=https://cloudbongos\\.com`));
|
|
1030
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_ID=mercury`));
|
|
1031
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_SECRET=S{43}`));
|
|
1032
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_PUBLIC_KEY=PUB==`));
|
|
1033
|
+
// a federated instance needs no GitHub app of its own → those creds stay blank
|
|
1034
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=\\n`), 'GitHub id blank (federated)');
|
|
1035
|
+
// regression: a non-federated write carries NO hub block + NO AUTH_IDP flip
|
|
1036
|
+
const plain = P.webEnvBody(coTenantWithDomain);
|
|
1037
|
+
assert.doesNotMatch(plain, /_AUTH_IDP=/, 'default (unset) never flips federation on');
|
|
1038
|
+
assert.doesNotMatch(plain, /_HUB_ORIGIN=/, 'default write carries no hub block');
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
t('branding: <PREFIX>_AUTH_IDP env override flips auth.idp to a real boolean (idpEnabled reads === true)', () => {
|
|
1042
|
+
const requireB = createRequire(import.meta.url);
|
|
1043
|
+
const branding = requireB('../src/branding.js');
|
|
1044
|
+
const neutral = { envPrefix: NEUTRAL_PREFIX, llmProjectName: 'x', identity: { productName: 'x', worldName: 'w' }, domains: { publicOrigin: 'https://x', buildersOrigin: 'https://x', statusOrigin: 'https://x', oauthOrigin: 'https://x' }, auth: { idp: false } };
|
|
1045
|
+
const off = branding.resolveBranding({ neutral, instance: {}, env: {} });
|
|
1046
|
+
assert.equal(off.auth.idp, false, 'unset → stays false (dormant default)');
|
|
1047
|
+
const on = branding.resolveBranding({ neutral, instance: {}, env: { [`${NEUTRAL_PREFIX}_AUTH_IDP`]: 'true' } });
|
|
1048
|
+
assert.equal(on.auth.idp, true, 'the string "true" coerces to boolean true (not the string, which would fail === true)');
|
|
1049
|
+
assert.strictEqual(typeof on.auth.idp, 'boolean', 'coerced to a real boolean');
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
await ta('federateInstance(--apply): registers the client on the hub bound to the EXACT origin (the #1002108 takeover fix), returns the env block', async () => {
|
|
1053
|
+
await withEnv(HUB_ENV, async () => {
|
|
1054
|
+
const pi = fakePI();
|
|
1055
|
+
const deps = { apply: true, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [{ github_id: 'gh-owner-1' }] }) } };
|
|
1056
|
+
const block = await P.federateInstance(coTenantWithDomain, deps);
|
|
1057
|
+
assert.equal(pi.calls.length, 1, 'registered exactly once');
|
|
1058
|
+
const reg = pi.calls[0];
|
|
1059
|
+
assert.equal(reg.origin, 'https://mercury.example.com', 'origin is the instance EXACT public origin (hub binds the mint audience to it)');
|
|
1060
|
+
assert.equal(reg.clientId, 'mercury', 'client_id is the stable slug (re-provision rotates the same row)');
|
|
1061
|
+
assert.ok(reg.redirectUris.includes('https://mercury.example.com/api/gds/auth/web/callback'), 'registers the exact browser callback the instance sends');
|
|
1062
|
+
assert.equal(reg.ownerGithubId, 'gh-owner-1', 'threads the owner github_id for the catalog');
|
|
1063
|
+
assert.ok(typeof reg.secret === 'string' && reg.secret.length >= 32, 'a strong per-client secret was generated');
|
|
1064
|
+
// the block webEnvBody bakes in
|
|
1065
|
+
assert.equal(block.hubOrigin, 'https://cloudbongos.com');
|
|
1066
|
+
assert.equal(block.clientId, 'mercury');
|
|
1067
|
+
assert.equal(block.clientSecret, reg.secret, 'the secret written to the instance is the one registered on the hub');
|
|
1068
|
+
assert.equal(block.hubPublicKey, 'HUBPUBKEYB64==', 'the hub PUBLIC key (for offline assertion verify) rides along');
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
await ta('federateInstance(dry-run): registers NOTHING, returns a placeholder block (so webEnvBody can still compose)', async () => {
|
|
1073
|
+
await withEnv(HUB_ENV, async () => {
|
|
1074
|
+
const pi = fakePI();
|
|
1075
|
+
const block = await P.federateInstance(coTenantWithDomain, { apply: false, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [] }) } });
|
|
1076
|
+
assert.equal(pi.calls.length, 0, 'dry-run performs no hub write');
|
|
1077
|
+
assert.equal(block.clientId, 'mercury');
|
|
1078
|
+
assert.equal(block.hubPublicKey, 'HUBPUBKEYB64==');
|
|
1079
|
+
});
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
await ta('provisionInstance(co-tenant, hub-configured): bakes the HUB block into web.env, records a "federated" event, registers the client', async () => {
|
|
1083
|
+
await withEnv(HUB_ENV, async () => {
|
|
1084
|
+
const { deps } = recorder({ apply: true });
|
|
1085
|
+
const bodies = {}; const events = []; const pi = fakePI();
|
|
1086
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1087
|
+
deps.provisioning.recordEvent = async (_db, e) => { events.push(e.event); };
|
|
1088
|
+
deps.platformIdentity = pi; deps.hubKeys = hubKeysOn;
|
|
1089
|
+
await P.provisionInstance(coTenantWithDomain, deps);
|
|
1090
|
+
const env = bodies[P.webEnvPath(coTenantWithDomain)];
|
|
1091
|
+
assert.ok(env, 'wrote the per-instance web.env');
|
|
1092
|
+
assert.match(env, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_AUTH_IDP=true`), 'the co-tenant comes up already federated');
|
|
1093
|
+
assert.match(env, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_SECRET=`), 'hub creds baked into web.env');
|
|
1094
|
+
assert.ok(events.includes('federated'), 'records the federation event for the audit trail');
|
|
1095
|
+
assert.equal(pi.calls.length, 1, 'registered the client on the hub');
|
|
1096
|
+
assert.equal(pi.calls[0].origin, 'https://mercury.example.com');
|
|
1097
|
+
});
|
|
1098
|
+
});
|
|
1099
|
+
|
|
1100
|
+
await ta('provisionInstance: a NON-hub control plane (no module/key) does NOT federate — the instance owns its auth (unchanged)', async () => {
|
|
1101
|
+
await withEnv(HUB_ENV, async () => {
|
|
1102
|
+
const { deps } = recorder({ apply: true });
|
|
1103
|
+
const bodies = {};
|
|
1104
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1105
|
+
// default recorder has no platformIdentity/hubKeys → shouldFederate skips
|
|
1106
|
+
await P.provisionInstance(coTenantWithDomain, deps);
|
|
1107
|
+
const env = bodies[P.webEnvPath(coTenantWithDomain)];
|
|
1108
|
+
assert.ok(env, 'wrote web.env');
|
|
1109
|
+
assert.doesNotMatch(env, /_AUTH_IDP=/, 'no federation flip on a non-hub control plane');
|
|
1110
|
+
assert.doesNotMatch(env, /_HUB_ORIGIN=/, 'no hub block — the direct-GitHub path, byte-identical to today');
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
await ta('provisionInstance(standalone, hub-configured): still NOT federated — owner-hosted keeps its own GitHub app', async () => {
|
|
1115
|
+
await withEnv(HUB_ENV, async () => {
|
|
1116
|
+
const { deps } = recorder({ apply: true });
|
|
1117
|
+
const bodies = {}; const pi = fakePI();
|
|
1118
|
+
deps.controlWriteFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1119
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1120
|
+
deps.platformIdentity = pi; deps.hubKeys = hubKeysOn;
|
|
1121
|
+
await P.provisionInstance({ ...coTenantWithDomain, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
|
|
1122
|
+
const env = bodies[P.webEnvPath({ ...coTenantWithDomain, hosting_shape: 'standalone' })];
|
|
1123
|
+
assert.ok(env, 'wrote web.env');
|
|
1124
|
+
assert.doesNotMatch(env, /_AUTH_IDP=/, 'standalone is the self-hosted path — no federation');
|
|
1125
|
+
assert.equal(pi.calls.length, 0, 'no hub registration for a standalone');
|
|
1126
|
+
});
|
|
1127
|
+
});
|
|
1128
|
+
|
|
903
1129
|
console.log(`\nprovision: ${passed} passed, ${failed} failed`);
|
|
904
1130
|
process.exit(failed ? 1 : 0);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// tests/pruned_lockfile_integrity.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002221 / audit H2 (finding F5, the R88 failure class) — a STANDING guard on the exact
|
|
4
|
+
// thing that broke in R88: the original packager redacted the lockfile and BLANKED the npm
|
|
5
|
+
// integrity hashes (base64 → "looks like a secret" → replaced), so `npm ci` from the shipped
|
|
6
|
+
// pruned lockfile could no longer verify its packages. The fix (docs-redacted / functional-verbatim,
|
|
7
|
+
// task 1824) keeps lockfiles verbatim. This test proves it stays fixed WITHOUT a network or a DB:
|
|
8
|
+
// build the pruned lockfile package-core ships and assert every registry-resolved package keeps a
|
|
9
|
+
// well-formed Subresource-Integrity hash. A regression that blanks/garbles integrity RED-flags here,
|
|
10
|
+
// in the fast unit gate, on every PR — long before a consumer's `npm ci` fails in the field.
|
|
11
|
+
//
|
|
12
|
+
// Complements tests/consumer_layout_boot.mjs (the live install+boot proof, run via
|
|
13
|
+
// `run-unit-tests.js --integration`): this is the network-free half of "PROVE PUBLISH".
|
|
14
|
+
//
|
|
15
|
+
// Run: node tests/pruned_lockfile_integrity.mjs
|
|
16
|
+
|
|
17
|
+
import { strict as assert } from 'node:assert';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const { prunePackageLock, CORE_DEP_NAMES } = require('../scripts/gds/package-core.js');
|
|
22
|
+
const realLock = require('../package-lock.json');
|
|
23
|
+
|
|
24
|
+
let passed = 0, failed = 0;
|
|
25
|
+
function t(name, fn) {
|
|
26
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
27
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A well-formed Subresource Integrity string: <algo>-<base64>. This is exactly what R88 destroyed
|
|
31
|
+
// (the base64 payload got redacted to a placeholder), so an intact match here IS the regression guard.
|
|
32
|
+
const SRI_RE = /^sha(?:512|384|256|1)-[A-Za-z0-9+/]+={0,2}$/;
|
|
33
|
+
const isRegistryResolved = (v) => typeof v.resolved === 'string' && /^https?:\/\//.test(v.resolved);
|
|
34
|
+
|
|
35
|
+
const pruned = prunePackageLock(realLock, {
|
|
36
|
+
name: '@bongos/core', version: '0.0.0-test',
|
|
37
|
+
rootRanges: { express: '^4.21.2', pg: '^8.20.0' },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
t('pruned lockfile: non-trivial and closes over the core deps (express + pg present)', () => {
|
|
41
|
+
const keys = Object.keys(pruned.packages || {});
|
|
42
|
+
assert.ok(keys.length > 2, `pruned lockfile is suspiciously empty (${keys.length} entries)`);
|
|
43
|
+
assert.ok(pruned.packages['node_modules/express'], 'express missing from the pruned closure');
|
|
44
|
+
assert.ok(pruned.packages['node_modules/pg'], 'pg missing from the pruned closure');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
t('pruned lockfile: EVERY registry-resolved package keeps a well-formed integrity hash (R88 guard)', () => {
|
|
48
|
+
const bad = [];
|
|
49
|
+
let checked = 0;
|
|
50
|
+
for (const [name, v] of Object.entries(pruned.packages || {})) {
|
|
51
|
+
if (!v || typeof v !== 'object' || !isRegistryResolved(v)) continue; // skip root / link: / file: entries
|
|
52
|
+
checked++;
|
|
53
|
+
if (typeof v.integrity !== 'string' || !SRI_RE.test(v.integrity)) {
|
|
54
|
+
bad.push(`${name} → integrity=${JSON.stringify(v.integrity)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
assert.ok(checked > 0, 'no registry-resolved packages found to check — closure computed wrong?');
|
|
58
|
+
assert.equal(bad.length, 0,
|
|
59
|
+
`${bad.length}/${checked} registry package(s) have a blanked/garbled integrity (the R88 break):\n ${bad.slice(0, 10).join('\n ')}`);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
t('pruned lockfile: integrity hashes match the SOURCE lockfile verbatim (not rewritten)', () => {
|
|
63
|
+
// Stronger than "well-formed": each kept entry's integrity must be BYTE-IDENTICAL to the source
|
|
64
|
+
// lockfile's — proving the pruning copies verbatim and never transforms the hash.
|
|
65
|
+
const mismatched = [];
|
|
66
|
+
for (const [name, v] of Object.entries(pruned.packages || {})) {
|
|
67
|
+
if (!v || !isRegistryResolved(v)) continue;
|
|
68
|
+
const src = realLock.packages[name];
|
|
69
|
+
if (src && src.integrity && src.integrity !== v.integrity) mismatched.push(name);
|
|
70
|
+
}
|
|
71
|
+
assert.equal(mismatched.length, 0, `integrity rewritten for: ${mismatched.slice(0, 10).join(', ')}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
t('CORE_DEP_NAMES: the pin domain is exactly the runtime core deps (express + pg)', () => {
|
|
75
|
+
assert.ok(Array.isArray(CORE_DEP_NAMES) && CORE_DEP_NAMES.includes('express') && CORE_DEP_NAMES.includes('pg'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log(`\npruned_lockfile_integrity: ${passed} passed, ${failed} failed`);
|
|
79
|
+
process.exit(failed ? 1 : 0);
|