@bongos/core 1.19.400 → 1.19.402

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 (33) hide show
  1. package/.bongos-core.json +48 -38
  2. package/clients/bongos-client/index.d.ts +1 -1
  3. package/docs/adr/0200-re-point-a-bring-your-own-domain.md +105 -0
  4. package/docs/adr/README.md +1 -0
  5. package/docs/api/openapi.json +3 -2
  6. package/docs/copy-inventory.md +233 -232
  7. package/docs/copy-registry.json +293 -284
  8. package/docs/design/projects-hub-direction-v2.md +38 -0
  9. package/docs/recipes/ui-look-before-you-ship.md +1 -1
  10. package/modules/provisioning/migrations/provisioning_012_domain_repoint.sql +35 -0
  11. package/modules/provisioning/provisioning.js +68 -8
  12. package/modules/provisioning/routes/provisioning.js +50 -23
  13. package/modules/ui-design/kit/lib.js +6 -2
  14. package/package-lock.json +2 -2
  15. package/package.json +1 -1
  16. package/public-landing/assets/cosmos.css +90 -35
  17. package/public-landing/index.html +13 -15
  18. package/public-landing/index.probes.json +8 -5
  19. package/public-landing/projects.html +49 -20
  20. package/public-landing/projects.probes.json +17 -10
  21. package/public-landing/projects.states.json +1 -1
  22. package/scripts/gds/provision-net.js +33 -0
  23. package/scripts/gds/provision-units.js +18 -0
  24. package/scripts/gds/provision.js +97 -5
  25. package/src/module-api.js +5 -1
  26. package/tests/landing_page.mjs +8 -4
  27. package/tests/projects_hub.mjs +28 -1
  28. package/tests/projects_hub_pre_uat.mjs +28 -2
  29. package/tests/provision.mjs +185 -12
  30. package/tests/provisioning_default_domain.mjs +5 -3
  31. package/tests/provisioning_domain_attach.mjs +166 -17
  32. package/tests/public_landing_projects.mjs +5 -1
  33. package/tests/ui_design_kit.mjs +5 -4
@@ -475,14 +475,17 @@ test('the bar is markup: the mark, the Projects trigger, the mode glyph, the sig
475
475
  assert.match(PAGE_CSS, /\.topbar\{position:relative;z-index:50;height:var\(--top-h\);display:flex;align-items:center;gap:22px;\s*padding:0 26px;background:var\(--bg\);border-bottom:1px solid var\(--rule\)\}/, 'the page ground closed by one hairline, z-index unified with the hub');
476
476
  });
477
477
 
478
- test('the Projects band deep-links the hub\'s views; About and Docs sit under a hairline; no item is current here', () => {
478
+ test('the Projects band deep-links the hub\'s views; About and Docs are footer links, not band items; no item is current here', () => {
479
479
  const body = HTML.split('<body>')[1].split('<script>')[0];
480
480
  assert.match(body, /<div class="menuBand" id="menuProjects" data-menu="projects" hidden>/);
481
481
  assert.match(body, /<a class="bandT" href="\/projects\?view=map">Sky<\/a>/);
482
482
  assert.match(body, /<a class="bandT" href="\/projects\?view=map&amp;sub=table">Table<\/a>/, 'the Table item lands on the table (the hub reads sub=table on boot)');
483
483
  assert.match(body, /<li class="bandCell" id="mineCell" hidden><a class="bandT" href="\/projects\?view=mine">Your projects<\/a>/, 'member-gated: hidden until the session resolves');
484
484
  assert.match(body, /<a class="bandT" href="\/projects\?view=new">New project<\/a>/);
485
- assert.match(body, /<ul class="bandRow bandMore">\s*<li class="bandCell"><a class="bandT" href="#about">About<\/a><\/li>\s*<li class="bandCell"><a class="bandT" href="\/docs">Docs<\/a><\/li>/, 'About and Docs under the hairline (§5)');
485
+ // About and Docs are site pages, not project views (owner, 2026-08-28; task 1003332):
486
+ // they left the band for the footer row, beside Privacy / Terms / Contact
487
+ assert.doesNotMatch(body, /bandMore|<a class="bandT" href="#about">|<a class="bandT" href="\/docs">/, 'About and Docs are not in the Projects band');
488
+ assert.match(body, /<a href="#about">About<\/a>\s*<a href="\/docs">Docs<\/a>\s*<a href="\/privacy">Privacy<\/a>/, 'About and Docs open the footer row');
486
489
  assert.match(body, /<section class="band" id="about">/, 'About has a real anchor: the lifecycle band');
487
490
  assert.doesNotMatch(body.split('<main')[0], /aria-current/, 'the landing is none of those views');
488
491
  // the profile band: the person, My projects, the deferred slot, Sign out as the honest anchor
@@ -496,9 +499,10 @@ test('the Projects band deep-links the hub\'s views; About and Docs sit under a
496
499
  assert.ok(body.includes('>' + copy + '<'), `"${copy}" is only reachable with scripting on`);
497
500
  }
498
501
  // the menu container is .menuBand: .band is this page's own content sections
499
- assert.match(PAGE_CSS, /\.menuBand\{position:relative;z-index:49;background:var\(--bg-card\);border-bottom:1px solid var\(--rule\);/);
502
+ // the bands OVERLAY the page (task 1003332): absolute at the bar's height, so the hero never reflows
503
+ assert.match(PAGE_CSS, /\.menuBand\{position:absolute;left:0;right:0;top:var\(--top-h\);z-index:49;background:var\(--bg-card\);border-bottom:1px solid var\(--rule\);/);
500
504
  assert.doesNotMatch(PAGE_CSS, /\.band\{[^}]*animation:bandIn/, 'the menu rule never lands on the content bands');
501
- assert.match(PAGE_CSS, /\.bandMore \.bandT\{font-size:17px;font-weight:400;min-height:24px;display:inline-flex;align-items:center\}/, 'the About/Docs links clear the 24px floor');
505
+ assert.doesNotMatch(PAGE_CSS, /\.bandMore/, 'the About/Docs rung left the sheet with the markup');
502
506
  });
503
507
 
504
508
  test('the bands are one disclosure: Escape returns focus, arrows cycle, Tab out closes, one closes the other', () => {
@@ -49,7 +49,14 @@ test('the Projects band carries the views; Home is gone and the mark is the way
49
49
  assert.match(HUB, /href="\?view=map" data-nav="map" data-sub="sky" aria-current="page">Sky<\/a>/);
50
50
  assert.match(HUB, /href="\?view=map&amp;sub=table" data-nav="map" data-sub="table">Table<\/a>/);
51
51
  assert.match(SCRIPT, /get\('sub'\) === 'table'\) setSub\('table'\);/, 'the Table deep link lands on the table');
52
- assert.match(HUB, /<a class="bandT" href="\/#about">About<\/a>/, 'About sits under the hairline (§5)');
52
+ // setView rewrites the URL to ?view=<v>; the boot query must be captured BEFORE it
53
+ // runs or sub=table is gone by the time it is read (task 1003332)
54
+ assert.match(SCRIPT, /var bootQ = new URLSearchParams\(location\.search\);\s*setView\(viewFromUrl\(\)\);[\s\S]{0,400}?if \(bootQ\.get\('sub'\) === 'table'\) setSub\('table'\);/, 'the boot query is read once, before setView');
55
+ // About and Docs are site pages, not project views (owner, 2026-08-28; task 1003332):
56
+ // they left the band for the footer row, beside Privacy / Terms / Contact
57
+ assert.doesNotMatch(HUB, /bandMore|<a class="bandT" href="\/#about">|<a class="bandT" href="\/docs">/, 'About and Docs are not in the Projects band');
58
+ assert.match(HUB, /<nav class="footNav" aria-label="Site"><a href="\/#about">About<\/a><a href="\/docs">Docs<\/a><a href="\/privacy">Privacy<\/a>/, 'About and Docs open the footer row');
59
+ assert.doesNotMatch(COSMOS, /\.bandMore/, 'the About/Docs rung left cosmos with the markup');
53
60
  assert.doesNotMatch(HUB, /id="sub-sky"|id="sub-table"|class="toolbar"|class="seg"/, 'the toolbar and its seg are retired');
54
61
  assert.doesNotMatch(SCRIPT, /dataset\.nav === 'home'/, 'no dead Home branch in the router');
55
62
  assert.match(SCRIPT, /if \(a\.dataset\.sub\) setSub\(a\.dataset\.sub\);/, 'a band item may pick the map sub-view');
@@ -138,6 +145,10 @@ test('the bands: one disclosure, two instances — Escape returns focus, the car
138
145
  assert.match(HUB, /<div class="menuBand" id="menuProjects" data-menu="projects" hidden>/);
139
146
  assert.match(HUB, /<div class="menuBand" id="menuProfile" data-menu="profile" hidden>/);
140
147
  assert.doesNotMatch(HUB, /role="menu"|aria-haspopup/, 'plain links in a disclosure — announced as what ships');
148
+ // the bands OVERLAY the page (owner, 2026-08-28; task 1003332): absolute against the
149
+ // document at the bar's height, so opening one never reflows the sky, the table or the footer
150
+ assert.match(COSMOS, /\.menuBand\{position:absolute;left:0;right:0;top:var\(--top-h\);z-index:49;background:var\(--bg-card\);border-bottom:1px solid var\(--rule\);/, 'a band is a layer under the bar, never a push');
151
+ assert.doesNotMatch(COSMOS, /PUSHES the page/, 'the sheet no longer describes the push-down');
141
152
  assert.match(SCRIPT, /function closeMenus\(\) \{[\s\S]*?if \(inside && trig\) trig\.focus\(\);/, 'focus restoration is a property of the close, not of one caller');
142
153
  assert.match(SCRIPT, /if \(ev\.key === 'Escape' && !openBand\(\)\) closeCard\(\);/, 'the card yields Escape to an open band');
143
154
  assert.match(SCRIPT, /function setView\(v, focus\) \{[\s\S]*?\$\('qWrap'\)\.hidden = \$\('qToggle'\)\.hidden = \(v !== 'map'\);/, 'the search hides off the map in the router');
@@ -525,6 +536,22 @@ test('no sort control is offered for a column that cannot sort', () => {
525
536
  }
526
537
  });
527
538
 
539
+ test('the table is in the world: a masthead, then ledger rows — no panel, no shadow, the accent only on the row\'s door (task 1003332)', () => {
540
+ // the owner's pick from three rendered treatments: the view title in the display
541
+ // voice and one chrome object; the title is two words ("every project", the owner cut ", as rows")
542
+ assert.match(HUB, /<div class="tblMast">\s*<h2 class="tblTitle">every project<\/h2>\s*<span class="mastObj" aria-hidden="true"><\/span>\s*<\/div>\s*<table class="tbl">/, 'one masthead ahead of the table');
543
+ assert.doesNotMatch(HUB, /tblMast--t[123]|data-tbl|mastCallouts|mastField/, 'the render round\'s switch and the unpicked mastheads left with the pick');
544
+ assert.match(COSMOS, /\.tbl\{width:100%;border-collapse:collapse;background:none;\}/, 'the rows sit on the page ground — no card');
545
+ assert.doesNotMatch((COSMOS.match(/\.tbl\{[^}]*\}/) || [''])[0], /border-radius|box-shadow/, 'no radius, no shadow on the table');
546
+ assert.match(COSMOS, /\.tbl td\{padding:17px 14px;border-bottom:1px solid var\(--rule-soft\);/, 'ledger rows: hairlines');
547
+ assert.match(COSMOS, /\.tbl tbody tr:hover td\{border-bottom-color:var\(--ink-faint\);\}/, 'hover is the hairline taking ink, not an accent tint');
548
+ assert.match(COSMOS, /\.rowGo\{[^}]*color:var\(--accent-deep\);text-decoration:underline;/, 'the row\'s door is accent text');
549
+ assert.match(HUB, /<a class="rowGo" href="\/">Open →<\/a>/, 'the self row\'s door');
550
+ assert.match(SCRIPT, /'<a class="rowGo" href="' \+ esc\(safeHttpUrl\(p\.origin\)\)/, 'the generated rows\' door');
551
+ assert.match(COSMOS, /\.mastObj\{display:block;--dia:clamp\(96px,10vw,132px\);[^}]*background-image:var\(--plate-set-cut\);/, 'the object is the set\'s small bead by token, in both modes');
552
+ assert.doesNotMatch(COSMOS, /mix-blend-mode:var\(--mast/, 'the cutout needs no blend — the view wrap is a stacking context');
553
+ });
554
+
528
555
  test('every live column keeps its styling — .colType included since task 1002772', () => {
529
556
  for (const live of ['.tbl .num', '.tbl .tags', '.colTags', '.colType', '.typeTag']) {
530
557
  assert.ok(COSMOS.includes(live), `${live} styles a column that renders real data`);
@@ -335,11 +335,37 @@ test('the F19 re-kick door survives the fold: open, pre-filled, one button', ()
335
335
  const fold = fn('attachFold');
336
336
  assert.match(fold, /var rekick = inst\.status === 'active' && inst\.domain && inst\.error_note;/);
337
337
  assert.match(fold, /\(rekick \? ' open' : ''\)/, 'the fold opens on its own for a stalled address');
338
- assert.match(fold, /\(rekick \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address that stalled');
339
- assert.match(fold, /rekick \? 'Try the address again' : 'Use this domain'/, 'and the button says what it does');
338
+ assert.match(fold, /\(prefill \? ' value="' \+ esc\(inst\.domain\) \+ '"' : ''\)/, 'pre-filled with the address in play');
339
+ assert.match(fold, /var prefill = rekick \|\| repoint;/, 'a stalled address and a correctable one both arrive in the field');
340
+ assert.match(fold, /rekick \? 'Try the address again' : repoint \? 'Change the address' : 'Use this domain'/, 'and the button says what it does');
340
341
  assert.match(fn('canAttachDomain'), /inst\.status === 'active' && inst\.domain && inst\.error_note && !inst\.open_intent\) return true;/, 'the route-side re-kick gate is untouched');
341
342
  });
342
343
 
344
+ test('the correction door: a bring-your-own address can be changed, and the fold says what that costs (task 1003237)', () => {
345
+ const fold = fn('attachFold');
346
+ // the three doors are told apart by what the project is actually addressed as
347
+ assert.match(fold, /var byo = inst\.domain && inst\.domain !== assignedDefaultFor\(inst\);/,
348
+ 'the platform-assigned <slug>.<apex> is NOT a correction — moving off it is the upgrade the fold already offered');
349
+ assert.match(fold, /var repoint = !rekick && byo;/, 'a stalled address keeps the F19 door, which is louder');
350
+ assert.match(fold, /'Use a different domain'/, 'the summary names the correction');
351
+ assert.match(fold, /attachNote/, 'and the consequence is stated where the button is');
352
+ assert.match(fold, /won’t use that address any more/, 'letting the old address go is SAID, not discovered');
353
+ // the door itself: an addressed, live project is no longer excluded
354
+ const gate = fn('canAttachDomain');
355
+ assert.doesNotMatch(gate, /!inst\.domain \|\| inst\.domain === assignedDefaultFor\(inst\)/,
356
+ 'the "only a domainless or platform-addressed project may attach" clause is what the re-point removes');
357
+ assert.match(gate, /inst\.status === 'requested' \|\| inst\.status === 'error' \|\| inst\.status === 'active'/,
358
+ 'state is still the gate — a mid-flight or torn-down project has nothing to point a domain at');
359
+ assert.match(COSMOS, /\.attachNote\{/, 'the note is styled');
360
+ assert.doesNotMatch((COSMOS.match(/\.attachNote[^\n]*/g) || []).join('\n'), /#[0-9a-f]{3,8}\b/i,
361
+ 'and paints from tokens, so dark mode needs no twin');
362
+ });
363
+
364
+ test('the retired refusal is gone from the card: domain_already_set can no longer be returned', () => {
365
+ assert.doesNotMatch(HUB, /domain_already_set/,
366
+ 'the server stopped sending it (task 1003237) — a branch for it would be unreachable copy');
367
+ });
368
+
343
369
  test('a heartbeat re-render keeps an open fold open and a half-typed domain typed', () => {
344
370
  const card = fn('renderCard');
345
371
  assert.match(card, /var keepOpen = !!\(oldFold && oldFold\.open\);/);
@@ -965,18 +965,21 @@ console.log('\nDNS UPSERT composition (P4, CONFIG override):');
965
965
 
966
966
  // A recorder that also captures each exec's env (so we can assert the DNS hook env).
967
967
  function dnsRecorder({ apply = false } = {}) {
968
- const cmds = [], envs = [];
968
+ const cmds = [], envs = [], cleared = [];
969
969
  const deps = {
970
970
  apply, log: () => {},
971
971
  db: { query: async () => ({ rows: [] }) },
972
- provisioning: { setInstanceStatus: async () => {}, recordEvent: async () => {} },
972
+ provisioning: {
973
+ setInstanceStatus: async () => {}, recordEvent: async () => {},
974
+ clearReleasedDomains: async (_db, id) => { cleared.push(id); },
975
+ },
973
976
  exec: (cmd, opts) => { cmds.push(cmd); envs.push(opts && opts.env); return { ok: true, stdout: '' }; },
974
977
  writeFile: () => ({ ok: true }), doApi: null,
975
978
  // Preflight (task 1980) stubbed reachable so --apply DNS tests reach the step-5
976
979
  // behavior they assert (the preflight itself is covered by its own test below).
977
980
  checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
978
981
  };
979
- return { deps, cmds, envs };
982
+ return { deps, cmds, envs, cleared };
980
983
  }
981
984
 
982
985
  await ta('a co-tenant WITH a domain UPSERTs DNS: the hook is invoked BY PATH with the control-plane env (proxied=false)', async () => {
@@ -1008,6 +1011,40 @@ await ta('--apply THROWS if a domain has no target IP (task-1180: never report s
1008
1011
  } finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
1009
1012
  });
1010
1013
 
1014
+ await ta('the PROVISION run drains the release queue too — a re-point on a pre-active project never reaches the attach leg (task 1003237)', async () => {
1015
+ const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1016
+ P.CONFIG.publicIp = '203.0.113.5';
1017
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1018
+ try {
1019
+ // an `error` row is frequently a PARTIAL standup (ADR 0181 §1): it may already
1020
+ // have UPSERTed the address it is now leaving, and domainAttachInstance no-ops
1021
+ // for anything but 'active', so this run is the only one that can let it go
1022
+ const { deps, cmds, envs, cleared } = dnsRecorder({ apply: true });
1023
+ await P.provisionInstance(
1024
+ { ...coTenant, domain: 'right.example.com', released_domains: ['typo.example.com'] }, deps);
1025
+ const hooks = cmds.map((c, i) => i).filter((i) => cmds[i] === '/opt/infra/box-dns-cloudflare.sh');
1026
+ assert.equal(hooks.length, 2, 'one UPSERT for the new address, one RELEASE for the old');
1027
+ assert.equal(envs[hooks[0]].BOX_HOSTNAME, 'right.example.com');
1028
+ assert.equal(envs[hooks[0]].BOX_IP, '203.0.113.5');
1029
+ assert.equal(envs[hooks[1]].BOX_HOSTNAME, 'typo.example.com');
1030
+ assert.equal(envs[hooks[1]].BOX_IP, '192.0.2.1', 'parked at the blackhole, not left pointing here');
1031
+ assert.deepEqual(cleared, [coTenant.id], 'and the queue is drained');
1032
+ } finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
1033
+ });
1034
+
1035
+ await ta('a provision with an EMPTY release queue never invokes the hook twice', async () => {
1036
+ const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1037
+ P.CONFIG.publicIp = '203.0.113.5';
1038
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1039
+ try {
1040
+ const { deps, cmds, cleared } = dnsRecorder({ apply: true });
1041
+ await P.provisionInstance({ ...coTenant, domain: 'right.example.com' }, deps);
1042
+ assert.equal(cmds.filter((c) => c === '/opt/infra/box-dns-cloudflare.sh').length, 1,
1043
+ 'the ordinary standup is unchanged — no release, no extra call');
1044
+ assert.deepEqual(cleared, [], 'and an empty queue is never "drained"');
1045
+ } finally { P.CONFIG.publicIp = savedIp; P.CONFIG.dnsHook = savedHook; }
1046
+ });
1047
+
1011
1048
  await ta('no domain → DNS is skipped even with a hook + IP configured', async () => {
1012
1049
  const savedIp = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1013
1050
  P.CONFIG.publicIp = '203.0.113.5';
@@ -1698,21 +1735,45 @@ console.log('\ndomain-attach leg (task 1002692 — give an ACTIVE instance its l
1698
1735
  // Recorder for the attach leg: captures commands (with env), file writes (with
1699
1736
  // bodies), and recorded events. No status writes to capture — domain-attach must
1700
1737
  // never touch the instance lifecycle.
1701
- function attachRecorder({ apply = false } = {}) {
1702
- const cmds = [], envs = [], writes = [], events = [], dnsNotes = [];
1738
+ // `envBody` is what a `cat <web.env>` returns — the attach leg patches the origin
1739
+ // lines IN PLACE (task 1003237), so a test that cares about the write has to hand
1740
+ // it a file to patch. Default: the shape a manifest-onboarded instance really has
1741
+ // — creds and a federation block the ROW does not know, which a template re-write
1742
+ // would blank (ADR 0190 §2 / idea 1000842). Values are obvious fixtures.
1743
+ const WEB_ENV_ON_BOX = [
1744
+ 'PORT=3010',
1745
+ 'PGDATABASE=spike',
1746
+ `${NEUTRAL_PREFIX}_PUBLIC_ORIGIN=https://old.example.com`,
1747
+ `${NEUTRAL_PREFIX}_BUILDERS_ORIGIN=https://old.example.com`,
1748
+ `${NEUTRAL_PREFIX}_OAUTH_CLIENT_ID=Iv1.fixture-client-id`,
1749
+ `${NEUTRAL_PREFIX}_OAUTH_CLIENT_SECRET=fixture-secret-placed-by-the-manifest-flow`,
1750
+ 'GITHUB_APP_PRIVATE_KEY=fixture-pem-flattened-to-one-line',
1751
+ `${NEUTRAL_PREFIX}_HUB_CLIENT_SECRET=fixture-federation-secret`,
1752
+ '',
1753
+ ].join('\n');
1754
+
1755
+ function attachRecorder({ apply = false, envBody = WEB_ENV_ON_BOX } = {}) {
1756
+ const cmds = [], envs = [], writes = [], events = [], dnsNotes = [], cleared = [];
1703
1757
  const deps = {
1704
1758
  apply, log: () => {},
1705
1759
  db: { query: async () => ({ rows: [] }) },
1706
1760
  provisioning: {
1707
1761
  recordEvent: async (_db, e) => { events.push(e); },
1708
1762
  recordDnsNote: async (_db, id, note) => { dnsNotes.push({ id, note }); },
1763
+ clearReleasedDomains: async (_db, id) => { cleared.push(id); },
1764
+ },
1765
+ exec: (cmd, opts = {}) => {
1766
+ cmds.push(cmd); envs.push((opts && opts.env) || null);
1767
+ if (/\bcat\b.*web\.env/.test(cmd)) {
1768
+ return envBody === null ? { ok: false, softFailed: true, stdout: '' } : { ok: true, stdout: envBody };
1769
+ }
1770
+ return { ok: true, stdout: '' };
1709
1771
  },
1710
- exec: (cmd, opts = {}) => { cmds.push(cmd); envs.push((opts && opts.env) || null); return { ok: true, stdout: '' }; },
1711
1772
  writeFile: (p, body) => { writes.push({ path: p, body }); return { ok: true }; },
1712
1773
  doApi: null,
1713
1774
  checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
1714
1775
  };
1715
- return { deps, cmds, envs, writes, events, dnsNotes };
1776
+ return { deps, cmds, envs, writes, events, dnsNotes, cleared };
1716
1777
  }
1717
1778
 
1718
1779
  const activeStandalone = {
@@ -1720,17 +1781,19 @@ const activeStandalone = {
1720
1781
  port: 3010, db_name: 'spike', domain: 'spike.cloudbongos.com', target_ref: 'jaxri/spike-harbor',
1721
1782
  };
1722
1783
 
1723
- await ta('domainAttachInstance(active standalone): env re-write → restart → DNS upsert → caddy → healthz', async () => {
1784
+ await ta('domainAttachInstance(active standalone, --apply): env patch → restart → DNS upsert → caddy → healthz', async () => {
1724
1785
  const savedPub = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1725
1786
  P.CONFIG.publicIp = '203.0.113.5';
1726
1787
  P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1727
1788
  try {
1728
- const { deps, cmds, envs, writes } = attachRecorder();
1789
+ const { deps, cmds, envs, writes } = attachRecorder({ apply: true });
1729
1790
  const r = await P.domainAttachInstance(activeStandalone, deps);
1730
1791
  assert.equal(r.ok, true);
1731
1792
  const env = writes.find((w) => /\/spike\/web\.env$/.test(w.path));
1732
- assert.ok(env, 're-writes web.env');
1733
- assert.match(env.body, /https:\/\/spike\.cloudbongos\.com/, 'the new address reaches the origins');
1793
+ assert.ok(env, 'writes web.env');
1794
+ assert.match(env.body, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_PUBLIC_ORIGIN=https://spike\\.cloudbongos\\.com`),
1795
+ 'the new address reaches the origins');
1796
+ assert.doesNotMatch(env.body, /old\.example\.com/, 'the stale origins are replaced, not appended');
1734
1797
  const joined = cmds.join('\n');
1735
1798
  assert.match(joined, /systemctl restart spike/, 'RESTART (not reload) so the process re-reads its env');
1736
1799
  const di = cmds.indexOf('/opt/infra/box-dns-cloudflare.sh');
@@ -1745,6 +1808,113 @@ await ta('domainAttachInstance(active standalone): env re-write → restart →
1745
1808
  } finally { P.CONFIG.publicIp = savedPub; P.CONFIG.dnsHook = savedHook; }
1746
1809
  });
1747
1810
 
1811
+ // -- the web.env hazard this leg used to carry (task 1003237, idea 1000842) -----
1812
+ // domainAttachInstance re-wrote web.env from the TEMPLATE (webEnvBody), which
1813
+ // regenerates the file FROM THE ROW — and the row knows neither the OAuth creds
1814
+ // placeManifestCreds fetched, nor the App private key, nor the federation block.
1815
+ // A first attach was masked by wizard order (address first, GitHub App second).
1816
+ // A RE-POINT is not masked: it runs on a project the founder is already signed
1817
+ // in to, so the same move would break sign-in on the project they just corrected.
1818
+
1819
+ await ta('domainAttachInstance(--apply): patches ONLY the origin lines — creds, App key and federation survive', async () => {
1820
+ const savedHook = P.CONFIG.dnsHook; P.CONFIG.dnsHook = '';
1821
+ try {
1822
+ const { deps, writes } = attachRecorder({ apply: true });
1823
+ const r = await P.domainAttachInstance(activeStandalone, deps);
1824
+ assert.equal(r.ok, true);
1825
+ const env = writes.find((w) => /\/spike\/web\.env$/.test(w.path));
1826
+ const P_ = reEsc(NEUTRAL_PREFIX);
1827
+ assert.match(env.body, new RegExp(`${P_}_OAUTH_CLIENT_ID=Iv1\\.fixture-client-id`), 'the manifest-placed client id survives');
1828
+ assert.match(env.body, new RegExp(`${P_}_OAUTH_CLIENT_SECRET=fixture-secret-placed-by-the-manifest-flow`), 'the client secret survives');
1829
+ assert.match(env.body, /GITHUB_APP_PRIVATE_KEY=fixture-pem-flattened-to-one-line/, 'the App private key survives');
1830
+ assert.match(env.body, new RegExp(`${P_}_HUB_CLIENT_SECRET=fixture-federation-secret`), 'the federation block survives');
1831
+ assert.match(env.body, /^PORT=3010$/m, 'and every unrelated line is untouched');
1832
+ assert.match(env.body, new RegExp(`${P_}_STATUS_ORIGIN=https://spike\\.cloudbongos\\.com`), 'a missing origin line is APPENDED');
1833
+ } finally { P.CONFIG.dnsHook = savedHook; }
1834
+ });
1835
+
1836
+ await ta('domainAttachInstance(--apply): an unreadable web.env is REFUSED, never "patched" over', async () => {
1837
+ const { deps, writes } = attachRecorder({ apply: true, envBody: null });
1838
+ const r = await P.domainAttachInstance(activeStandalone, deps);
1839
+ assert.equal(r.ok, false);
1840
+ assert.match(r.error, /cannot read/i);
1841
+ assert.equal(writes.length, 0, 'four origins over a missing PORT/PGDATABASE would take the instance down');
1842
+ });
1843
+
1844
+ await ta('domainAttachInstance(dry-run): PLANS the patch — nothing is written', async () => {
1845
+ const { deps, writes } = attachRecorder();
1846
+ const r = await P.domainAttachInstance(activeStandalone, deps);
1847
+ assert.equal(r.ok, true);
1848
+ assert.ok(!writes.some((w) => /web\.env$/.test(w.path)), 'a dry run must not write a synthetic body');
1849
+ });
1850
+
1851
+ // -- releasing what a re-point moved off (task 1003237, ADR 0200) --------------
1852
+
1853
+ await ta('releasableDomains: the queue, minus the address the instance is CURRENTLY on', () => {
1854
+ assert.deepEqual(P.releasableDomains({ domain: 'right.example.com', released_domains: ['TYPO.example.com'] }),
1855
+ ['typo.example.com'], 'normalized, so a case difference can never dodge the release');
1856
+ assert.deepEqual(P.releasableDomains({ domain: 'right.example.com', released_domains: ['right.example.com'] }),
1857
+ [], 'a row that somehow queued its own domain can never blackhole a live project');
1858
+ assert.deepEqual(P.releasableDomains({ domain: 'x.example.com' }), [], 'no queue is not a crash');
1859
+ assert.deepEqual(P.releasableDomains({ domain: 'x.example.com', released_domains: ['a.example.com', '', 'b.example.com'] }),
1860
+ ['a.example.com', 'b.example.com'], 'every abandoned address is released, not just the last');
1861
+ });
1862
+
1863
+ await ta('domainAttachInstance(--apply): a re-point parks the old address at the blackhole, AFTER wiring the new one', async () => {
1864
+ const savedPub = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1865
+ P.CONFIG.publicIp = '203.0.113.5';
1866
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1867
+ try {
1868
+ const { deps, cmds, envs, cleared } = attachRecorder({ apply: true });
1869
+ const r = await P.domainAttachInstance({ ...activeStandalone, released_domains: ['typo.example.com'] }, deps);
1870
+ assert.equal(r.ok, true);
1871
+ const hooks = cmds.map((c, i) => i).filter((i) => cmds[i] === '/opt/infra/box-dns-cloudflare.sh');
1872
+ assert.equal(hooks.length, 2, 'one UPSERT for the new address, one RELEASE for the old');
1873
+ assert.equal(envs[hooks[0]].BOX_HOSTNAME, 'spike.cloudbongos.com');
1874
+ assert.equal(envs[hooks[0]].BOX_IP, '203.0.113.5', 'the new address is wired first');
1875
+ assert.equal(envs[hooks[1]].BOX_HOSTNAME, 'typo.example.com');
1876
+ assert.equal(envs[hooks[1]].BOX_IP, '192.0.2.1', 'RFC 5737 TEST-NET-1 — guaranteed never routable');
1877
+ assert.equal(envs[hooks[1]].BOX_PROXIED, '0', 'a parked record must never ride the CF edge');
1878
+ assert.equal(envs[hooks[1]].CLOUDFLARE_ZONE, 'example.com', 'the zone is derived from the OLD host, not the row');
1879
+ const caddy = cmds.findIndex((c) => /reload caddy/.test(c));
1880
+ assert.ok(caddy >= 0 && caddy < hooks[1], 'the old vhost is gone before its record is parked');
1881
+ assert.deepEqual(cleared, [18], 'the queue is drained only once every address on it was released');
1882
+ } finally { P.CONFIG.publicIp = savedPub; P.CONFIG.dnsHook = savedHook; }
1883
+ });
1884
+
1885
+ await ta('domainAttachInstance(--apply): a release the hook REFUSES keeps the queue — ok:true, the re-point still stands', async () => {
1886
+ const savedPub = P.CONFIG.publicIp, savedHook = P.CONFIG.dnsHook;
1887
+ P.CONFIG.publicIp = '203.0.113.5';
1888
+ P.CONFIG.dnsHook = '/opt/infra/box-dns-cloudflare.sh';
1889
+ try {
1890
+ const { deps, cleared } = attachRecorder({ apply: true });
1891
+ let seen = 0;
1892
+ const inner = deps.exec;
1893
+ deps.exec = (cmd, opts) => {
1894
+ if (cmd === '/opt/infra/box-dns-cloudflare.sh' && ++seen === 2) throw new Error('cloudflare said no');
1895
+ return inner(cmd, opts);
1896
+ };
1897
+ const r = await P.domainAttachInstance({ ...activeStandalone, released_domains: ['typo.example.com'] }, deps);
1898
+ assert.equal(r.ok, true, 'a failed release must never fail a re-point that already landed');
1899
+ assert.equal(r.releaseFailed, 'typo.example.com');
1900
+ assert.deepEqual(cleared, [], 'the queue survives, so the next attach retries the release');
1901
+ } finally { P.CONFIG.publicIp = savedPub; P.CONFIG.dnsHook = savedHook; }
1902
+ });
1903
+
1904
+ await ta('domainAttachInstance(--apply): no DNS hook → the release is named for the operator, and the queue is kept', async () => {
1905
+ const savedHook = P.CONFIG.dnsHook; P.CONFIG.dnsHook = '';
1906
+ try {
1907
+ const lines = [];
1908
+ const { deps, cleared } = attachRecorder({ apply: true });
1909
+ deps.log = (m) => lines.push(String(m));
1910
+ const r = await P.domainAttachInstance({ ...activeStandalone, released_domains: ['typo.example.com'] }, deps);
1911
+ assert.equal(r.ok, true);
1912
+ assert.ok(lines.some((l) => /typo\.example\.com/.test(l) && /BOX_DNS_HOOK unset/.test(l)),
1913
+ 'the outstanding manual step is SAID, not swallowed');
1914
+ assert.deepEqual(cleared, [], 'nothing was released, so nothing is marked released');
1915
+ } finally { P.CONFIG.dnsHook = savedHook; }
1916
+ });
1917
+
1748
1918
  await ta('domainAttachInstance: no domain recorded → ok:false, nothing runs', async () => {
1749
1919
  const { deps, cmds } = attachRecorder();
1750
1920
  const r = await P.domainAttachInstance({ ...activeStandalone, domain: null }, deps);
@@ -1797,7 +1967,10 @@ await ta('cmdRunIntents (--apply) routes a domain-attach intent to the attach le
1797
1967
  claimNextOAuthManifest: async () => null,
1798
1968
  resolveOAuthManifest: async () => {},
1799
1969
  },
1800
- exec: () => ({ ok: true }), writeFile: () => ({ ok: true }), doApi: null,
1970
+ // the attach leg patches web.env IN PLACE, so a `cat` must return a real file
1971
+ // — an empty one is a deliberate refusal (task 1003237)
1972
+ exec: (cmd) => ({ ok: true, stdout: /\bcat\b.*web\.env/.test(String(cmd)) ? 'PORT=3010\n' : '' }),
1973
+ writeFile: () => ({ ok: true }), doApi: null,
1801
1974
  checkDnsTokenScope: async () => ({ reachable: true, zone: 'cloudbongos.com' }),
1802
1975
  };
1803
1976
  const r = await P.cmdRunIntents(deps);
@@ -318,12 +318,14 @@ await ta('re-pointing AWAY from the platform-assigned address → recorded + dom
318
318
  assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
319
319
  });
320
320
 
321
- await ta('a genuine bring-your-own address still refuses to re-point (unchanged)', async () => {
321
+ await ta('a genuine bring-your-own address RE-POINTS, and the one it leaves is queued for release (task 1003237)', async () => {
322
322
  reset();
323
323
  provisioning.getInstanceById = async (_db, id) => freshRow({ id, domain: 'spike.example.com', status: 'active' });
324
324
  const res = await post('/provisioning/instances/41/domain', { domain: 'another.example.com' });
325
- assert.equal(res.status, 409, res.raw);
326
- assert.equal(res.body.error.code, 'domain_already_set');
325
+ assert.equal(res.status, 200, res.raw);
326
+ assert.equal(res.body.domain, 'another.example.com');
327
+ assert.equal(res.body.replaced, 'spike.example.com',
328
+ 'the owner decision of 2026-08-28: a mistyped BYO address is correctable, and the old one is let go');
327
329
  });
328
330
 
329
331
  server.close();