@bongos/core 1.19.401 → 1.19.403

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 (35) hide show
  1. package/.bongos-core.json +50 -40
  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 +141 -138
  7. package/docs/copy-registry.json +205 -176
  8. package/docs/design/projects-hub-direction-v2.md +16 -0
  9. package/modules/platform-identity/project-stats.js +123 -38
  10. package/modules/platform-identity/routes/my-projects.js +13 -0
  11. package/modules/platform-identity/routes/projects.js +7 -4
  12. package/modules/provisioning/migrations/provisioning_012_domain_repoint.sql +35 -0
  13. package/modules/provisioning/provisioning.js +68 -8
  14. package/modules/provisioning/routes/provisioning.js +50 -23
  15. package/modules/ui-design/kit/fixtures/featured.json +1 -1
  16. package/modules/ui-design/kit/fixtures/my-projects.json +1 -1
  17. package/modules/ui-design/kit/serve.js +14 -3
  18. package/package-lock.json +2 -2
  19. package/package.json +1 -1
  20. package/public-landing/assets/cosmos.css +126 -25
  21. package/public-landing/projects.html +155 -27
  22. package/public-landing/projects.probes.json +28 -0
  23. package/public-landing/projects.states.json +6 -3
  24. package/scripts/gds/provision-net.js +33 -0
  25. package/scripts/gds/provision-units.js +18 -0
  26. package/scripts/gds/provision.js +97 -5
  27. package/src/module-api.js +5 -1
  28. package/tests/cosmos_token_contrast.mjs +5 -2
  29. package/tests/project_stats.mjs +128 -2
  30. package/tests/projects_hub.mjs +106 -5
  31. package/tests/projects_hub_pre_uat.mjs +28 -2
  32. package/tests/provision.mjs +185 -12
  33. package/tests/provisioning_default_domain.mjs +5 -3
  34. package/tests/provisioning_domain_attach.mjs +166 -17
  35. package/tests/public_landing_projects.mjs +5 -1
@@ -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();
@@ -41,7 +41,7 @@ async function ta(name, fn) {
41
41
 
42
42
  console.log('service slice — updateInstanceDomain / getInstanceByDomain:');
43
43
 
44
- await ta('updateInstanceDomain: parameterized UPDATE of only the domain (+updated_at), returns the row', async () => {
44
+ await ta('updateInstanceDomain: parameterized UPDATE of the domain + the release queue, returns the row', async () => {
45
45
  const calls = [];
46
46
  const db = { query: async (sql, params) => {
47
47
  calls.push({ sql, params });
@@ -50,8 +50,34 @@ await ta('updateInstanceDomain: parameterized UPDATE of only the domain (+update
50
50
  const row = await provisioning.updateInstanceDomain(db, 18, 'spike.cloudbongos.com');
51
51
  assert.equal(row.domain, 'spike.cloudbongos.com');
52
52
  assert.match(calls[0].sql, /UPDATE provisioning_instances/);
53
- assert.match(calls[0].sql, /SET domain = \$2, updated_at = now\(\)/, 'touches ONLY the domain column');
54
- assert.deepEqual(calls[0].params, [18, 'spike.cloudbongos.com']);
53
+ assert.match(calls[0].sql, /SET domain = \$2/);
54
+ assert.match(calls[0].sql, /released_domains/, 'the release queue rides the SAME statement (task 1003237)');
55
+ assert.match(calls[0].sql, /last_verified_up = CASE WHEN domain IS DISTINCT FROM \$2 THEN NULL/,
56
+ 'a CHANGED address has never been probed - the overlay must not describe the address it left (F16)');
57
+ assert.doesNotMatch(calls[0].sql, /status/, 'still never drifts the lifecycle');
58
+ assert.match(calls[0].sql, /array_length\(released_domains, 1\), 0\) >= \$4/,
59
+ 'the queue is CAPPED in SQL, not trusted from the caller - the runner drains it against a shared token');
60
+ assert.deepEqual(calls[0].params, [18, 'spike.cloudbongos.com', null, provisioning.MAX_RELEASED_DOMAINS],
61
+ 'no `releasing` means a null 3rd param, and the CASE leaves the queue exactly as it stands');
62
+ });
63
+
64
+ await ta('updateInstanceDomain: a re-point APPENDS the old address, lowercased, and never duplicates it', async () => {
65
+ const calls = [];
66
+ const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 18 }] }; } };
67
+ await provisioning.updateInstanceDomain(db, 18, 'right.example.com', 'TYPO.example.com');
68
+ assert.deepEqual(calls[0].params, [18, 'right.example.com', 'typo.example.com', provisioning.MAX_RELEASED_DOMAINS]);
69
+ assert.match(calls[0].sql, /array_append\(released_domains, \$3\)/, 'appends — a second correction must not drop the first');
70
+ assert.match(calls[0].sql, /\$3 = ANY\(released_domains\)/, 'idempotent: a retry never queues the same address twice');
71
+ });
72
+
73
+ await ta('clearReleasedDomains: empties the queue and touches nothing else', async () => {
74
+ const calls = [];
75
+ const db = { query: async (sql, params) => { calls.push({ sql, params }); return { rows: [{ id: 18 }] }; } };
76
+ await provisioning.clearReleasedDomains(db, 18);
77
+ assert.match(calls[0].sql, /SET released_domains = '\{\}'/);
78
+ assert.doesNotMatch(calls[0].sql, /updated_at|status|domain = /,
79
+ 'draining a queue is not an owner-visible change (the clearErrorNote rule)');
80
+ assert.deepEqual(calls[0].params, [18]);
55
81
  });
56
82
 
57
83
  await ta('getInstanceByDomain: case-insensitive exact match, parameterized', async () => {
@@ -80,8 +106,8 @@ let createResult = null; // what createInstance reports (re-request test
80
106
  let manifestRows = [];
81
107
  provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
82
108
  provisioning.getInstanceByDomain = async (_db, d) => domainHolder;
83
- provisioning.updateInstanceDomain = async (_db, id, domain) => {
84
- updateCalls.push({ id, domain });
109
+ provisioning.updateInstanceDomain = async (_db, id, domain, releasing = null) => {
110
+ updateCalls.push({ id, domain, releasing });
85
111
  instance = { ...instance, domain };
86
112
  return { ...instance, id };
87
113
  };
@@ -100,10 +126,24 @@ const api = require('../src/module-api.js');
100
126
  api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
101
127
  api.requirePermission = () => (_req, _res, next) => next();
102
128
  api.branding = () => ({
103
- domains: { publicOrigin: 'https://cb.test' },
129
+ // instanceBase is the platform APEX the route's reserved + in-apex-squat guards
130
+ // read (instanceApex()). Set here so the re-point suite below can exercise them
131
+ // against a real apex instead of the guard-free '' the stub used to imply.
132
+ domains: { publicOrigin: 'https://cb.test', instanceBase: 'cb.test' },
104
133
  identity: { productName: 'Cloud Bongos' },
105
134
  });
106
135
 
136
+ // The domain route is rate-limited PER BUILDER (task 1003237) and every test in
137
+ // this file posts as builder 7, so the real cap would throttle the suite rather
138
+ // than the behaviour each test is about. Raise it for the shared app below —
139
+ // `limiterMax = null` restores the route's own number, which the dedicated
140
+ // limiter test at the end of this file uses to exercise the real 429.
141
+ const rateLimit = require('../modules/provisioning/rate-limit.js');
142
+ const realMakeAskRateLimiter = rateLimit.makeAskRateLimiter;
143
+ let limiterMax = 10_000;
144
+ rateLimit.makeAskRateLimiter = (opts = {}) =>
145
+ realMakeAskRateLimiter({ ...opts, ...(limiterMax === null ? {} : { max: limiterMax }) });
146
+
107
147
  const express = require('express');
108
148
  const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
109
149
  const app = express();
@@ -121,10 +161,12 @@ const server = app.listen(0);
121
161
  await new Promise((r) => server.once('listening', r));
122
162
  const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
123
163
 
124
- function post(path, body = {}) {
164
+ function post(path, body = {}) { return postTo(base, path, body); }
165
+
166
+ function postTo(origin, path, body = {}) {
125
167
  return new Promise((resolve, reject) => {
126
168
  const payload = JSON.stringify(body);
127
- const req = http.request(`${base}${path}`, {
169
+ const req = http.request(`${origin}${path}`, {
128
170
  method: 'POST',
129
171
  headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
130
172
  }, (res) => {
@@ -137,9 +179,9 @@ function post(path, body = {}) {
137
179
  });
138
180
  }
139
181
 
140
- function reset({ inst } = {}) {
182
+ function reset({ inst, holder } = {}) {
141
183
  instance = inst || null;
142
- domainHolder = null;
184
+ domainHolder = holder || null;
143
185
  updateCalls = [];
144
186
  enqueueCalls = [];
145
187
  enqueueResult = null;
@@ -155,7 +197,7 @@ await ta('ACTIVE domainless instance: domain is validated, recorded, and a domai
155
197
  assert.equal(res.status, 200, res.raw);
156
198
  assert.equal(res.body.ok, true);
157
199
  assert.equal(res.body.domain, 'spike.cloudbongos.com', 'normalized before storing');
158
- assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cloudbongos.com' }]);
200
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cloudbongos.com', releasing: null }]);
159
201
  assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
160
202
  assert.equal(res.body.queued, true);
161
203
  });
@@ -229,21 +271,91 @@ await ta('re-kick honors enqueueIntent created:false — a racing enqueue is nev
229
271
  } finally { provisioning.enqueueIntent = savedEnqueue; }
230
272
  });
231
273
 
232
- await ta('a DIFFERENT domain already set is a truthful 409 domain_already_set (re-pointing is not this route)', async () => {
233
- reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'old.cloudbongos.com' } });
234
- const res = await post('/provisioning/instances/18/domain', { domain: 'new.cloudbongos.com' });
274
+ // -- the re-point (task 1003237, owner decision 2026-08-28 / ADR 0200) --------
275
+ // A mistyped bring-your-own address used to be a permanent 409 domain_already_set.
276
+ // It is now correctable, under exactly the guards the FIRST attach applied. The
277
+ // apex here is cb.test (the harness's branding publicOrigin), so "inside the
278
+ // apex" means *.cb.test.
279
+
280
+ await ta('a DIFFERENT bring-your-own domain RE-POINTS — the address changes and the old one is queued for release', async () => {
281
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
282
+ const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
283
+ assert.equal(res.status, 200, res.raw);
284
+ assert.equal(res.body.changed, true);
285
+ assert.equal(res.body.queued, true, 'an ACTIVE instance re-points via a domain-attach intent');
286
+ assert.equal(res.body.domain, 'right.example.com');
287
+ assert.equal(res.body.replaced, 'typo.example.com', 'the response NAMES what is being let go');
288
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: 'typo.example.com' }],
289
+ 'the new address and the release ride ONE write');
290
+ assert.match(res.body.message, /right\.example\.com/);
291
+ assert.match(res.body.message, /typo\.example\.com/, 'the sentence says the old address is going');
292
+ assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
293
+ });
294
+
295
+ await ta('a re-point onto a PLATFORM-RESERVED name is refused — the first attach guard, unweakened', async () => {
296
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
297
+ const res = await post('/provisioning/instances/18/domain', { domain: 'cb.test' });
298
+ assert.equal(res.status, 400, res.raw);
299
+ assert.equal(res.body.error.code, 'domain_reserved');
300
+ assert.equal(updateCalls.length, 0, 'nothing was written - the old address still stands');
301
+ });
302
+
303
+ await ta('a re-point INSIDE the platform apex is refused as a squat (task 1002704) — except this slug own default', async () => {
304
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
305
+ const squat = await post('/provisioning/instances/18/domain', { domain: 'someone-else.cb.test' });
306
+ assert.equal(squat.status, 400, squat.raw);
307
+ assert.equal(squat.body.error.code, 'domain_reserved');
308
+ assert.equal(updateCalls.length, 0);
309
+
310
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
311
+ const own = await post('/provisioning/instances/18/domain', { domain: 'spike.cb.test' });
312
+ assert.equal(own.status, 200, own.raw);
313
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'spike.cb.test', releasing: 'typo.example.com' }],
314
+ 'falling back to the assigned default still releases the BYO address it left');
315
+ });
316
+
317
+ await ta('a re-point onto an address ANOTHER project holds is a truthful 409 domain_taken', async () => {
318
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' },
319
+ holder: { id: 99, slug: 'other' } });
320
+ const res = await post('/provisioning/instances/18/domain', { domain: 'taken.example.com' });
235
321
  assert.equal(res.status, 409, res.raw);
236
- assert.equal(res.body.error.code, 'domain_already_set');
237
- assert.equal(res.body.error.details.current, 'old.cloudbongos.com', 'names the current address');
322
+ assert.equal(res.body.error.code, 'domain_taken');
238
323
  assert.equal(updateCalls.length, 0);
239
324
  });
240
325
 
326
+ await ta('moving OFF the platform-assigned <slug>.<apex> queues NO release — that name is the platform own (ADR 0181)', async () => {
327
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'spike.cb.test' } });
328
+ const res = await post('/provisioning/instances/18/domain', { domain: 'mine.example.com' });
329
+ assert.equal(res.status, 200, res.raw);
330
+ assert.equal(res.body.replaced, null, 'the assigned default is never released - the revive path re-uses it');
331
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'mine.example.com', releasing: null }]);
332
+ });
333
+
334
+ await ta('a re-point on a REQUESTED instance records without an intent, and releases NOTHING — it never stood up', async () => {
335
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'requested', domain: 'typo.example.com' } });
336
+ const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
337
+ assert.equal(res.status, 200, res.raw);
338
+ assert.equal(res.body.queued, false);
339
+ assert.equal(res.body.replaced, null,
340
+ 'nothing ever wrote an A record for the address it is leaving - parking that name would CREATE a junk record, not release one');
341
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: null }]);
342
+ assert.deepEqual(enqueueCalls, [], 'nothing to re-assert on an instance that has not stood up');
343
+ });
344
+
345
+ await ta("an ERROR instance DOES release — a failed standup is frequently a PARTIAL one that already wrote the record (ADR 0181)", async () => {
346
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'error', domain: 'typo.example.com' } });
347
+ const res = await post('/provisioning/instances/18/domain', { domain: 'right.example.com' });
348
+ assert.equal(res.status, 200, res.raw);
349
+ assert.equal(res.body.replaced, 'typo.example.com');
350
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'right.example.com', releasing: 'typo.example.com' }]);
351
+ });
352
+
241
353
  await ta('REQUESTED instance: domain records with NO intent — the provision run wires it', async () => {
242
354
  reset({ inst: { owner_builder_id: 7, slug: 'fresh', status: 'requested', domain: null } });
243
355
  const res = await post('/provisioning/instances/18/domain', { domain: 'fresh.cloudbongos.com' });
244
356
  assert.equal(res.status, 200, res.raw);
245
357
  assert.equal(res.body.queued, false);
246
- assert.deepEqual(updateCalls, [{ id: 18, domain: 'fresh.cloudbongos.com' }]);
358
+ assert.deepEqual(updateCalls, [{ id: 18, domain: 'fresh.cloudbongos.com', releasing: null }]);
247
359
  assert.equal(enqueueCalls.length, 0, 'provision will wire it — no separate intent');
248
360
  });
249
361
 
@@ -310,6 +422,43 @@ await ta('a fresh create with a domain is untouched (the domain is stored at cre
310
422
  assert.equal(res.body.domain_ignored, undefined);
311
423
  });
312
424
 
425
+ console.log('\nPOST /provisioning/instances/:id/domain — the write is throttled per builder:');
426
+
427
+ await ta('the re-point write is rate-limited on the BUILDER, so one owner cannot spend the platform-wide DNS budget', async () => {
428
+ // A re-point QUEUES an address the runner later releases with a call against the
429
+ // SHARED control-plane Cloudflare token, so an unthrottled owner spends a
430
+ // platform resource, not merely their own. Keyed on the builder, not the IP:
431
+ // per-IP starves co-located builders while an attacker rotates IPs.
432
+ limiterMax = null; // the route's own number, whatever it is
433
+ const limited = express();
434
+ limited.use(express.json());
435
+ limited.use((_req, res, next) => {
436
+ res.fail = (code, opts, details) => {
437
+ const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
438
+ res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
439
+ };
440
+ next();
441
+ });
442
+ limited.use('/api/bongos', provisioningRoutes());
443
+ const srv = limited.listen(0);
444
+ await new Promise((r) => srv.once('listening', r));
445
+ const at = `http://127.0.0.1:${srv.address().port}/api/bongos`;
446
+ try {
447
+ reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', domain: 'typo.example.com' } });
448
+ let sawLimit = false;
449
+ // well past any sane cap for a human correcting one typo
450
+ for (let i = 0; i < 60 && !sawLimit; i++) {
451
+ const res = await postTo(at, `/provisioning/instances/18/domain?i=${i}`, { domain: `h${i}.example.com` });
452
+ if (res.status === 429) {
453
+ sawLimit = true;
454
+ assert.equal(res.body.error.code || res.body.error, 'rate_limited');
455
+ assert.ok(i > 0, 'a first call is never throttled');
456
+ }
457
+ }
458
+ assert.ok(sawLimit, 'the route must shed a flood — an unthrottled owner can queue unbounded DNS work');
459
+ } finally { srv.close(); limiterMax = 10_000; }
460
+ });
461
+
313
462
  console.log('\nPOST /provisioning/instances/:id/github-app — accepts the newly-addressed instance:');
314
463
 
315
464
  await ta('domainless instance still 409s domain_required; once addressed, the manifest mints', async () => {
@@ -95,11 +95,15 @@ test('every truthful refusal the API can give is mapped to human copy, never swa
95
95
  'not_teardownable', // teardown refused for the real state
96
96
  'bad_domain', // malformed hostname
97
97
  'domain_taken', // another instance already answers there
98
- 'domain_already_set', // re-pointing is not this surface
98
+ 'domain_reserved', // the platform's own name, or a squat inside its apex
99
99
  'domain_not_attachable', // mid-flight/torn-down states refuse truthfully
100
100
  ]) {
101
101
  assert.match(body, new RegExp(code), `the page must handle '${code}' explicitly`);
102
102
  }
103
+ // retired with the re-point (task 1003237): the server no longer sends it, so a
104
+ // branch for it would be copy the founder can never reach
105
+ assert.doesNotMatch(body, /domain_already_set/,
106
+ 'the refusal the re-point removed must not linger as dead copy');
103
107
  });
104
108
 
105
109
  test('a domainless instance gets an honest no-address state', async () => {