@bongos/core 1.18.0 → 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.
Files changed (113) hide show
  1. package/.bongos-core.json +132 -322
  2. package/README.md +1 -1
  3. package/config/branding.neutral.json +5 -1
  4. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  5. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  6. package/docs/adr/README.md +5 -2
  7. package/docs/api/openapi.json +1789 -450
  8. package/docs/api-reference.md +52 -5
  9. package/docs/file-map.md +7 -7
  10. package/docs/recipes/packaging-the-core.md +13 -0
  11. package/docs/recipes/upgrading-the-core.md +5 -2
  12. package/migrations/039_newcomer_friendly_column.sql +5 -0
  13. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  14. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  15. package/modules/hall-ui/public/my-projects.html +78 -0
  16. package/modules/hall-ui/public/my-projects.js +142 -0
  17. package/modules/hall-ui/public/scouting.html +45 -0
  18. package/modules/hall-ui/public/scouting.js +73 -0
  19. package/modules/status-ui/public/explore.html +54 -0
  20. package/modules/status-ui/public/explore.js +67 -0
  21. package/package-lock.json +2 -2
  22. package/package.json +1 -1
  23. package/public-landing/create.html +211 -14
  24. package/scripts/gds/cli-lib.js +11 -5
  25. package/scripts/gds/lint-migrations.js +102 -0
  26. package/scripts/gds/login.js +42 -10
  27. package/scripts/gds/mirror-redact.js +5 -0
  28. package/scripts/gds/package-core.js +263 -4
  29. package/scripts/gds/provision.js +145 -7
  30. package/scripts/gds/publish-manifest.js +15 -0
  31. package/scripts/gds/run-unit-tests.js +38 -14
  32. package/scripts/gds/setup.js +41 -0
  33. package/scripts/gds/ship.js +115 -31
  34. package/scripts/gds/upgrade.js +147 -5
  35. package/scripts/migrate.sh +10 -0
  36. package/src/bongos/auth.js +362 -5
  37. package/src/bongos/db.js +66 -0
  38. package/src/bongos/github-repos.js +93 -0
  39. package/src/bongos/routes/auth.js +93 -11
  40. package/src/bongos/routes/github.js +90 -0
  41. package/src/bongos/routes/instance.js +15 -3
  42. package/src/bongos/routes.js +2 -0
  43. package/src/branding.js +17 -2
  44. package/src/instance-config.js +16 -0
  45. package/src/module-api.js +29 -1
  46. package/tests/branding.mjs +29 -23
  47. package/tests/github_repos_helpers.mjs +91 -0
  48. package/tests/github_token_store.mjs +121 -0
  49. package/tests/idp_device_exchange.mjs +105 -0
  50. package/tests/idp_federation.mjs +100 -0
  51. package/tests/migrations_forward_safe.mjs +86 -0
  52. package/tests/module_api.mjs +1 -1
  53. package/tests/package_core.mjs +246 -1
  54. package/tests/provision.mjs +157 -0
  55. package/tests/pruned_lockfile_integrity.mjs +79 -0
  56. package/tests/ship_ci_deploy.mjs +60 -0
  57. package/tests/upgrade.mjs +254 -5
  58. package/.gitignore +0 -91
  59. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  60. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  61. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  62. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  63. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  64. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  65. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  66. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  67. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  68. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  69. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  70. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  71. package/scripts/hall-preview/fixtures/goals.json +0 -502
  72. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  73. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  74. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  75. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  76. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  77. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  78. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  79. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  80. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  81. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  82. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  83. package/scripts/hall-preview/fixtures/instance.json +0 -124
  84. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  85. package/scripts/hall-preview/fixtures/me.json +0 -476
  86. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  87. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  88. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  89. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  90. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  91. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  92. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  93. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  94. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  95. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  96. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  97. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  98. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  99. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  100. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  101. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  102. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  103. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  104. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  105. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  106. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  107. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  108. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  109. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  110. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  111. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  112. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  113. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -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);
@@ -969,5 +969,162 @@ await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE
969
969
  assert.equal(box.length, 0, 'nothing crosses to the co-hosting box for a standalone teardown');
970
970
  });
971
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
+
972
1129
  console.log(`\nprovision: ${passed} passed, ${failed} failed`);
973
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);
@@ -71,6 +71,66 @@ test('parseDeployMode: SAFE DEFAULT laptop on no input at all', () => {
71
71
  assert.equal(parseDeployMode('', ''), 'laptop');
72
72
  });
73
73
 
74
+ // ---- 1b. resolveDeployTarget precedence + safe "no target" default (task 1002202) ----
75
+ // Every case passes env explicitly so the machine's own OTB_/GDS_ env can't leak in.
76
+
77
+ const { resolveDeployTarget } = ship;
78
+
79
+ test('resolveDeployTarget: SAFE DEFAULT — no env, no config, no branding → no target', () => {
80
+ const r = resolveDeployTarget({ env: {}, configRaw: null, branding: null });
81
+ assert.equal(r.sshTarget, null);
82
+ assert.equal(r.healthzUrl, null);
83
+ });
84
+
85
+ test('resolveDeployTarget: config sshTarget + healthzUrl are honored', () => {
86
+ const r = resolveDeployTarget({
87
+ env: {},
88
+ configRaw: '{"sshTarget":"deployer@host.example","healthzUrl":"https://inst.example/healthz"}',
89
+ branding: null,
90
+ });
91
+ assert.equal(r.sshTarget, 'deployer@host.example');
92
+ assert.equal(r.healthzUrl, 'https://inst.example/healthz');
93
+ });
94
+
95
+ test('resolveDeployTarget: env wins over config for the ssh target', () => {
96
+ const r = resolveDeployTarget({
97
+ env: { GDS_DEPLOY_SSH_TARGET: 'env@host.example' },
98
+ configRaw: '{"sshTarget":"config@host.example"}',
99
+ branding: null,
100
+ });
101
+ assert.equal(r.sshTarget, 'env@host.example');
102
+ });
103
+
104
+ test('resolveDeployTarget: healthzUrl falls back to branding publicOrigin + /healthz', () => {
105
+ const r = resolveDeployTarget({
106
+ env: {},
107
+ configRaw: '{"sshTarget":"deployer@host.example"}',
108
+ branding: { domains: { publicOrigin: 'https://cloudbongos.com' } },
109
+ });
110
+ assert.equal(r.healthzUrl, 'https://cloudbongos.com/healthz');
111
+ });
112
+
113
+ test('resolveDeployTarget: env healthz URL wins over the branding fallback', () => {
114
+ const r = resolveDeployTarget({
115
+ env: { GDS_DEPLOY_HEALTHZ_URL: 'https://explicit.example/healthz' },
116
+ configRaw: null,
117
+ branding: { domains: { publicOrigin: 'https://cloudbongos.com' } },
118
+ });
119
+ assert.equal(r.healthzUrl, 'https://explicit.example/healthz');
120
+ });
121
+
122
+ test('resolveDeployTarget: malformed config JSON → safe no-target (never throws)', () => {
123
+ const r = resolveDeployTarget({ env: {}, configRaw: '{not json', branding: null });
124
+ assert.equal(r.sshTarget, null);
125
+ assert.equal(r.healthzUrl, null);
126
+ });
127
+
128
+ test('resolveDeployTarget: blank/whitespace config values are treated as unset', () => {
129
+ const r = resolveDeployTarget({ env: {}, configRaw: '{"sshTarget":" ","healthzUrl":""}', branding: null });
130
+ assert.equal(r.sshTarget, null);
131
+ assert.equal(r.healthzUrl, null);
132
+ });
133
+
74
134
  test('parseDeployMode: SAFE DEFAULT laptop on malformed config JSON', () => {
75
135
  assert.equal(parseDeployMode(undefined, '{not json'), 'laptop');
76
136
  });