@bongos/core 1.19.744 → 1.19.745

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.744",
6
- "core_contract": "1.19.744",
7
- "source_commit": "a1c9e8b6b38e48c9f3a6040ff29b76ee3457b808",
5
+ "core_version": "1.19.745",
6
+ "core_contract": "1.19.745",
7
+ "source_commit": "35769e9ee6c0bd162dcd30f6360cf22a69adc5e6",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-15T15:04:25.874Z",
9
+ "built_at": "2026-09-15T15:42:00.191Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 479,
13
13
  "agent_docs_stubbed": 25,
14
- "functional_verbatim": 2107,
14
+ "functional_verbatim": 2109,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2611,
20
- "tree_sha256": "4cfb37d1d47bb997d911c6ae24958d363737b154c725bfb76fb65f3e71c92449",
19
+ "file_count": 2613,
20
+ "tree_sha256": "8ee268f754cacf633cc6358ef765b30b214928f4d38914cbbc0788b580835e83",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2492,7 +2492,7 @@
2492
2492
  {
2493
2493
  "path": "docs/module-api-changelog.md",
2494
2494
  "mode": "0000644",
2495
- "sha256": "8f98164df753a9a4a8c77ade44d450eeb65eee2987dcc5b46d0d251f2a62d399"
2495
+ "sha256": "68cb61e401d3e601932f1db71ef88346adad1f2474c862143a224ed354ba0615"
2496
2496
  },
2497
2497
  {
2498
2498
  "path": "docs/modules-contract.md",
@@ -6229,6 +6229,11 @@
6229
6229
  "mode": "0000644",
6230
6230
  "sha256": "f4562bd90fdae26c5f7c51851bd2ad7752535bd0dcfb62dd567c11e76330c42b"
6231
6231
  },
6232
+ {
6233
+ "path": "modules/provisioning/migrations/provisioning_015_domain_unique.sql",
6234
+ "mode": "0000644",
6235
+ "sha256": "136b0b7e0485c1602a403767863137da0536e058b8913228d3a3e5a6ad56f512"
6236
+ },
6232
6237
  {
6233
6238
  "path": "modules/provisioning/module.json",
6234
6239
  "mode": "0000644",
@@ -6257,7 +6262,7 @@
6257
6262
  {
6258
6263
  "path": "modules/provisioning/provisioning.js",
6259
6264
  "mode": "0000644",
6260
- "sha256": "9e6d1356fa046f97f3141c353302637dad2deed4c2460d124c8d7636b0ff0065"
6265
+ "sha256": "b3a1261dbdefd526a93d75c8f46a7b896f2331292b41a919a73dde1ffba45eed"
6261
6266
  },
6262
6267
  {
6263
6268
  "path": "modules/provisioning/rate-limit.js",
@@ -6282,7 +6287,7 @@
6282
6287
  {
6283
6288
  "path": "modules/provisioning/routes/provisioning.js",
6284
6289
  "mode": "0000644",
6285
- "sha256": "eac6fb471e91a08d2c1003f4245140b2a995ae892140d625c116143a4a6610e1"
6290
+ "sha256": "a3812af0cc12dfafff47c1b9b03edea6ef61f1f4728acead1628d0b1a1669432"
6286
6291
  },
6287
6292
  {
6288
6293
  "path": "modules/provisioning/starter-bundles.js",
@@ -7542,12 +7547,12 @@
7542
7547
  {
7543
7548
  "path": "package-lock.json",
7544
7549
  "mode": "0000644",
7545
- "sha256": "7d86631d2cad28001db3920822fd96bb638cede683d627fe0101445320c12d7e"
7550
+ "sha256": "c5eabd4e0de92c43d27602e996ffeaeef177349bb4ed045a208578785b8af8be"
7546
7551
  },
7547
7552
  {
7548
7553
  "path": "package.json",
7549
7554
  "mode": "0000644",
7550
- "sha256": "d4c3e36f1af2829fc4bc7d9b6dc29be667c75e6d5fd870855398bc5e13380ddc"
7555
+ "sha256": "d9ddaa7b2fa408cceb8cdc0540ad523f0d64da3ec91a9420f73058088585dee9"
7551
7556
  },
7552
7557
  {
7553
7558
  "path": "public-docs/index.html",
@@ -9347,7 +9352,7 @@
9347
9352
  {
9348
9353
  "path": "src/module-api.js",
9349
9354
  "mode": "0000644",
9350
- "sha256": "6cec169dc7c62404bb81e3b7fc00506de07048b3a0ce2ef59f892d467b9e0c3b"
9355
+ "sha256": "96b3c84f00546eba7a57b31eba4a96acf0d6285c3715a1370d3527242e4d161b"
9351
9356
  },
9352
9357
  {
9353
9358
  "path": "src/module-loader/catalog.js",
@@ -12009,6 +12014,11 @@
12009
12014
  "mode": "0000644",
12010
12015
  "sha256": "62ea8ffc2c21c351289dc7f96cab38fcd5824afba1fed0df7a545cd5df9e396c"
12011
12016
  },
12017
+ {
12018
+ "path": "tests/provisioning_domain_unique.mjs",
12019
+ "mode": "0000644",
12020
+ "sha256": "d1c9456ad442caf2485c5c38e5bad98ef6c834589e405eade59cbeffec534ea0"
12021
+ },
12012
12022
  {
12013
12023
  "path": "tests/provisioning_github_app_routes.mjs",
12014
12024
  "mode": "0000644",
@@ -1962,5 +1962,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1962
1962
  landed since 1.19.742 with no explicit bump. run 34984876076. (task 1002620)
1963
1963
  1.19.744 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1964
1964
  landed since 1.19.743 with no explicit bump. run 34985887902. (task 1002620)
1965
+ 1.19.745 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1966
+ landed since 1.19.744 with no explicit bump. run 34990123950. (task 1002620)
1965
1967
  ---------------------------------------------------------------------------
1966
1968
  ```
@@ -0,0 +1,46 @@
1
+ -- provisioning_015_domain_unique.sql — database uniqueness on an instance's
2
+ -- ADDRESS, closing the read-then-write race both write paths still carry
3
+ -- (task 1002901; source idea 1000722, the task 1002704 review).
4
+ --
5
+ -- THE GAP. `slug` has carried `UNIQUE` since provisioning_001_tables.sql:35;
6
+ -- `domain` was declared plainly at :49 and never constrained. Both address write
7
+ -- paths enforce uniqueness in APPLICATION code with an unguarded read-then-write:
8
+ -- the create route calls getInstanceByDomain and then INSERTs, and the
9
+ -- domain-attach route calls getInstanceByDomain and then updateInstanceDomain.
10
+ -- Two concurrent requests interleaving between the read and the write both pass
11
+ -- their check, and one address ends up held by two instances.
12
+ --
13
+ -- WHY IT MATTERS MORE NOW. Task 1002704 put the domain write on the HOT PATH:
14
+ -- auto-assign stamps defaultDomainForSlug(slug, apex) on EVERY creation, so the
15
+ -- race is no longer an occasional bring-your-own step — it is every project
16
+ -- anyone creates. Working area 1's done-when is that any new user can create a
17
+ -- project; two projects answering at one hostname is that promise breaking under
18
+ -- exactly the concurrency a growing platform produces.
19
+ --
20
+ -- THE APPLICATION CHECKS STAY. This index is a BACKSTOP, not a replacement: the
21
+ -- read-then-write checks produce the good error message ("That address is
22
+ -- already held by another instance"), and this makes the guarantee real when two
23
+ -- requests interleave. The routes map this index's 23505 onto that same
24
+ -- `domain_taken` 409, so a caller cannot tell which layer caught it.
25
+ --
26
+ -- WHY PARTIAL — `status <> 'torn_down'` IS LOAD-BEARING, NOT A TIDY-UP.
27
+ -- A torn-down instance KEEPS its domain by design (ADR 0181 Decision 2: the
28
+ -- revive path re-uses that exact hostname, and task 1002897 automates only the
29
+ -- DNS-park and Caddy-removal legs while confirming that retention). An
30
+ -- UNCONDITIONAL unique index would therefore permanently block re-creating a
31
+ -- project at an address that was once torn down — a worse bug than the race it
32
+ -- closes. The partial form mirrors the ux_provisioning_intents_open precedent at
33
+ -- provisioning_001_tables.sql:129.
34
+ --
35
+ -- WHY lower(domain). It matches getInstanceByDomain's existing lookup
36
+ -- (`WHERE lower(domain) = $1`), so the index and the application check agree on
37
+ -- what "the same address" means. Without it, 'Mercury.cloudbongos.com' and
38
+ -- 'mercury.cloudbongos.com' would be two rows to the index and one address to
39
+ -- every reader.
40
+ --
41
+ -- SAFE TO CREATE. Re-verified live against the fleet at claim time (2026-09-15):
42
+ -- 23 instances, 6 rows fall under this predicate, all distinct, no case
43
+ -- variants. `IF NOT EXISTS` keeps the migration idempotent across re-runs.
44
+ CREATE UNIQUE INDEX IF NOT EXISTS ux_provisioning_instances_domain
45
+ ON provisioning_instances (lower(domain))
46
+ WHERE domain IS NOT NULL AND status <> 'torn_down';
@@ -781,6 +781,23 @@ async function getInstanceBySlug(db, slug) {
781
781
  return rows[0] || null;
782
782
  }
783
783
 
784
+ // The partial unique index provisioning_015 puts on lower(domain) — the database
785
+ // backstop behind both address write paths' read-then-write check (task 1002901).
786
+ // Named once, here, so the migration that CREATES it and the predicate that
787
+ // RECOGNIZES it cannot drift: a rename in one and not the other would leave
788
+ // isDomainTaken() silently never matching. The test reads the .sql and pins both.
789
+ const DOMAIN_UNIQUE_INDEX = 'ux_provisioning_instances_domain';
790
+
791
+ // Did this error come from two requests racing for one address? The slug UNIQUE
792
+ // and the domain index share error code 23505, so the code alone cannot tell them
793
+ // apart — `constraint` is what names which one actually fired. Both address write
794
+ // paths check uniqueness in application code first and produce the good message;
795
+ // this predicate is how the clash that slips through that window still answers
796
+ // `domain_taken` instead of surfacing as a raw 500 (task 1002901).
797
+ function isDomainTaken(e) {
798
+ return !!e && e.code === '23505' && e.constraint === DOMAIN_UNIQUE_INDEX;
799
+ }
800
+
784
801
  // The instance currently holding `domain` (case-insensitive), if any — backs the
785
802
  // domain-attach route's uniqueness check so one owner's address can never silently
786
803
  // shadow another's (task 1002692). Caller normalizes; matching is defensive anyway.
@@ -1073,11 +1090,21 @@ async function createInstance(db, { ownerBuilderId, slug, targetRef, hostingShap
1073
1090
  );
1074
1091
  return { instance: rows[0], created: true };
1075
1092
  } catch (e) {
1076
- // 23505 = unique_violation on the slug UNIQUE constraint.
1093
+ // 23505 = unique_violation, and since provisioning_015 TWO indexes raise it here
1094
+ // — slug UNIQUE and ux_provisioning_instances_domain. Resolve by SLUG FIRST,
1095
+ // whichever one Postgres named. That order is load-bearing: an idempotent
1096
+ // re-request duplicates the slug AND the <slug>.<apex> address auto-assign
1097
+ // stamped on the first one (task 1002704), so it violates BOTH and Postgres
1098
+ // reports only the index it checked first. Branching on `e.constraint` would
1099
+ // turn that re-request into a 409 whenever the domain index won the race — a
1100
+ // coin-flip bug that appears only once an address is assigned.
1077
1101
  if (e && e.code === '23505') {
1078
1102
  const existing = await getInstanceBySlug(db, slug);
1079
1103
  if (existing) return { instance: existing, created: false };
1080
1104
  }
1105
+ // No row under this slug, so a 23505 can only be the domain index: a real race
1106
+ // for an address taken between the route's check and this INSERT. It propagates
1107
+ // as-is, and the route maps it via isDomainTaken() onto the same domain_taken 409.
1081
1108
  throw e;
1082
1109
  }
1083
1110
  }
@@ -1453,6 +1480,8 @@ module.exports = {
1453
1480
  normalizeDescription, normalizeProjectDetail, projectDetailPatch, projectDetail,
1454
1481
  // pure — GitHub App Manifest flow (task 2080)
1455
1482
  githubAppName, buildGithubAppManifest, manifestPostUrl, publicGithubApp,
1483
+ // pure — the address race's database backstop (task 1002901, provisioning_015)
1484
+ DOMAIN_UNIQUE_INDEX, isDomainTaken,
1456
1485
  // data access
1457
1486
  isProvisionedHostname,
1458
1487
  getInstanceById, getInstanceBySlug, getInstanceByDomain, listInstancesForOwner, listAllInstances,
@@ -416,9 +416,7 @@ module.exports = function provisioningRoutes() {
416
416
  // never silently shadow another's.
417
417
  if (domain) {
418
418
  const holder = await provisioning.getInstanceByDomain(pool, domain);
419
- if (holder && holder.slug !== slug) {
420
- return res.fail('domain_taken', { status: 409, message: 'That address is already held by another instance.' });
421
- }
419
+ if (holder && holder.slug !== slug) return res.fail('domain_taken', { status: 409, message: 'That address is already held by another instance.' });
422
420
  }
423
421
  }
424
422
  const { instance, created } = await provisioning.createInstance(pool, {
@@ -522,6 +520,8 @@ module.exports = function provisioningRoutes() {
522
520
  ...(modulesIgnored ? { modules_ignored: modulesIgnored } : {}),
523
521
  });
524
522
  } catch (err) {
523
+ // Another instance took the address between the check above and the INSERT (task 1002901).
524
+ if (provisioning.isDomainTaken(err)) return res.fail('domain_taken', { status: 409, message: 'That address is already held by another instance.' });
525
525
  log.error('[provisioning] POST /provisioning/instances', err);
526
526
  res.fail('request_failed', { status: 500, message: 'internal error' });
527
527
  }
@@ -1170,9 +1170,7 @@ module.exports = function provisioningRoutes() {
1170
1170
  const releasing = current && current !== assignedDefault
1171
1171
  && provisioning.DOMAIN_RELEASE_FROM.has(inst.status) ? current : null;
1172
1172
  const holder = await provisioning.getInstanceByDomain(pool, domain);
1173
- if (holder && String(holder.id) !== String(inst.id)) {
1174
- return res.fail('domain_taken', { status: 409, message: 'That address is already in use by another instance.' });
1175
- }
1173
+ if (holder && String(holder.id) !== String(inst.id)) return res.fail('domain_taken', { status: 409, message: 'That address is already in use by another instance.' });
1176
1174
  if (!provisioning.DOMAIN_ATTACH_FROM.has(inst.status)) {
1177
1175
  return res.fail('domain_not_attachable', { status: 409,
1178
1176
  message: `Instance is '${inst.status}' — an address can be attached once it settles ('active') or before standup ('requested').`,
@@ -1229,6 +1227,8 @@ module.exports = function provisioningRoutes() {
1229
1227
  res.json({ ok: true, changed: true, queued, domain, replaced: releasing,
1230
1228
  message: MESSAGES[`${queued ? 'queued' : 'recorded'}:${releasing ? 'repoint' : 'first'}`] });
1231
1229
  } catch (err) {
1230
+ // Same race on the attach side: the index refuses the UPDATE, so nothing is recorded and nothing is enqueued.
1231
+ if (provisioning.isDomainTaken(err)) return res.fail('domain_taken', { status: 409, message: 'That address is already in use by another instance.' });
1232
1232
  log.error('[provisioning] POST /provisioning/instances/:id/domain', err);
1233
1233
  res.fail('domain_attach_failed', { status: 500, message: 'internal error' });
1234
1234
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.744",
3
+ "version": "1.19.745",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.744",
9
+ "version": "1.19.745",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.744",
3
+ "version": "1.19.745",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
package/src/module-api.js CHANGED
@@ -71,7 +71,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
71
71
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
72
72
  // the entry to that file. Look for a version's history there, not here.
73
73
  // ---------------------------------------------------------------------------
74
- const CORE_VERSION = '1.19.744'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
74
+ const CORE_VERSION = '1.19.745'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
75
75
 
76
76
  // A namespaced logger so a module's log lines are attributable + consistent.
77
77
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,274 @@
1
+ // tests/provisioning_domain_unique.mjs
2
+ //
3
+ // The database backstop behind an instance's ADDRESS (task 1002901; source idea
4
+ // 1000722, the task 1002704 review).
5
+ //
6
+ // Both address write paths enforced uniqueness in application code with an
7
+ // unguarded read-then-write — getInstanceByDomain, then INSERT (create) or
8
+ // updateInstanceDomain (attach). Two concurrent requests interleaving between the
9
+ // read and the write both passed, and one address ended up held by two instances.
10
+ // provisioning_015 adds the partial unique index that makes the guarantee real;
11
+ // these tests hold the four things that index changed.
12
+ //
13
+ // Covers:
14
+ // • the migration — partial on status <> 'torn_down' (ADR 0181 Decision 2 keeps
15
+ // a torn-down instance's address for the revive path, so an UNCONDITIONAL
16
+ // index would permanently block re-creating there), keyed on lower(domain) to
17
+ // match getInstanceByDomain, and idempotent
18
+ // • DOMAIN_UNIQUE_INDEX / isDomainTaken — the predicate that tells the domain
19
+ // index's 23505 apart from the slug UNIQUE's, which share the error code
20
+ // • createInstance's SLUG-FIRST resolution — the ordering that keeps an
21
+ // idempotent re-request idempotent when it violates BOTH indexes at once
22
+ // • both routes translating the index's refusal into the same domain_taken 409
23
+ // the read-then-write check produces
24
+ //
25
+ // DB-free: the service slice drives the REAL createInstance through a fake `db`
26
+ // that throws like Postgres; the routes run over the real mounted router with
27
+ // data-access fns stubbed (the provisioning_domain_attach.mjs pattern). No network.
28
+ //
29
+ // Run: node tests/provisioning_domain_unique.mjs
30
+
31
+ import { strict as assert } from 'node:assert';
32
+ import { createRequire } from 'node:module';
33
+ import fs from 'node:fs';
34
+ import http from 'node:http';
35
+ import path from 'node:path';
36
+
37
+ const require = createRequire(import.meta.url);
38
+ const provisioning = require('../modules/provisioning/provisioning.js');
39
+
40
+ let passed = 0, failed = 0;
41
+ function t(name, fn) {
42
+ try { fn(); passed++; console.log(` PASS ${name}`); }
43
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
44
+ }
45
+ async function ta(name, fn) {
46
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
47
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
48
+ }
49
+
50
+ // A unique_violation shaped like the one `pg` raises: the code alone cannot say
51
+ // WHICH index fired, which is the whole reason isDomainTaken reads `constraint`.
52
+ function uniqueViolation(constraint) {
53
+ return Object.assign(new Error(`duplicate key value violates unique constraint "${constraint}"`),
54
+ { code: '23505', constraint });
55
+ }
56
+ const DOMAIN_VIOLATION = () => uniqueViolation('ux_provisioning_instances_domain');
57
+ const SLUG_VIOLATION = () => uniqueViolation('provisioning_instances_slug_key');
58
+
59
+ console.log('\nthe migration — provisioning_015_domain_unique.sql:');
60
+
61
+ const MIGRATION = fs.readFileSync(
62
+ path.resolve('modules/provisioning/migrations/provisioning_015_domain_unique.sql'), 'utf8');
63
+
64
+ t('creates the index this module names in code — one spelling, not two', () => {
65
+ assert.match(MIGRATION, new RegExp(`CREATE UNIQUE INDEX IF NOT EXISTS ${provisioning.DOMAIN_UNIQUE_INDEX}\\b`),
66
+ 'the migration and DOMAIN_UNIQUE_INDEX must agree, or isDomainTaken silently never matches');
67
+ });
68
+
69
+ t('is PARTIAL on status <> torn_down — a torn-down address is kept for the revive path (ADR 0181 D2)', () => {
70
+ assert.match(MIGRATION, /WHERE\s+domain IS NOT NULL AND status <> 'torn_down'/,
71
+ 'an unconditional index would permanently block re-creating a project at a torn-down address');
72
+ });
73
+
74
+ t('is keyed on lower(domain) so the index and getInstanceByDomain mean the same address', () => {
75
+ assert.match(MIGRATION, /ON provisioning_instances \(lower\(domain\)\)/);
76
+ });
77
+
78
+ t('is idempotent — IF NOT EXISTS, so a re-run is a no-op', () => {
79
+ assert.match(MIGRATION, /IF NOT EXISTS/);
80
+ });
81
+
82
+ console.log('\nisDomainTaken — telling two 23505s apart:');
83
+
84
+ t('the domain index firing is a domain clash', () => {
85
+ assert.equal(provisioning.isDomainTaken(DOMAIN_VIOLATION()), true);
86
+ });
87
+
88
+ t('the SLUG unique firing is NOT — same error code, different index, different answer', () => {
89
+ assert.equal(provisioning.isDomainTaken(SLUG_VIOLATION()), false,
90
+ 'mapping a slug clash to domain_taken would tell an owner the wrong thing is taken');
91
+ });
92
+
93
+ t('a 23505 naming no constraint is not assumed to be ours', () => {
94
+ assert.equal(provisioning.isDomainTaken(Object.assign(new Error('x'), { code: '23505' })), false);
95
+ });
96
+
97
+ t('any other error code is not a clash — not even one naming our index', () => {
98
+ // A 23503 (foreign-key violation) can name this index in `constraint` too, so the
99
+ // predicate must read BOTH fields; matching on the name alone would map an
100
+ // unrelated database fault onto a 409 that tells the caller the wrong thing.
101
+ assert.equal(provisioning.isDomainTaken(
102
+ Object.assign(new Error('fk'), { code: '23503', constraint: 'ux_provisioning_instances_domain' })), false);
103
+ assert.equal(provisioning.isDomainTaken(new Error('boom')), false);
104
+ });
105
+
106
+ t('null / undefined never throw — the predicate runs inside a catch', () => {
107
+ assert.equal(provisioning.isDomainTaken(null), false);
108
+ assert.equal(provisioning.isDomainTaken(undefined), false);
109
+ });
110
+
111
+ console.log('\ncreateInstance — SLUG-FIRST resolution (the coin-flip bug this ordering prevents):');
112
+
113
+ // The real createInstance, driven through a fake db: the INSERT throws like
114
+ // Postgres, and the SELECT the catch falls back to answers for the slug.
115
+ function fakeDb({ insertThrows, slugRow }) {
116
+ const calls = [];
117
+ return {
118
+ calls,
119
+ query: async (sql, params) => {
120
+ calls.push(sql.trim().split('\n')[0]);
121
+ if (/^INSERT INTO provisioning_instances/.test(sql.trim())) throw insertThrows;
122
+ if (/SELECT \* FROM provisioning_instances WHERE slug = \$1/.test(sql)) {
123
+ return { rows: slugRow ? [{ ...slugRow, slug: params[0] }] : [] };
124
+ }
125
+ return { rows: [] };
126
+ },
127
+ };
128
+ }
129
+
130
+ await ta('a re-request violating BOTH indexes stays idempotent when Postgres names the DOMAIN one', async () => {
131
+ // Auto-assign stamps <slug>.<apex> on every creation (task 1002704), so an
132
+ // idempotent re-request duplicates the slug AND its address. Postgres reports
133
+ // only the index it checked first; branching on e.constraint would turn the
134
+ // re-request into a 409 whenever the domain index won that race.
135
+ const db = fakeDb({ insertThrows: DOMAIN_VIOLATION(), slugRow: { id: 18, owner_builder_id: 7 } });
136
+ const out = await provisioning.createInstance(db, { ownerBuilderId: 7, slug: 'mercury', domain: 'mercury.example.com' });
137
+ assert.equal(out.created, false, 'a re-request is not a new project');
138
+ assert.equal(out.instance.id, 18, 'and it returns the EXISTING row, not an error');
139
+ });
140
+
141
+ await ta('the same re-request is idempotent when Postgres names the SLUG one (unchanged behaviour)', async () => {
142
+ const db = fakeDb({ insertThrows: SLUG_VIOLATION(), slugRow: { id: 18, owner_builder_id: 7 } });
143
+ const out = await provisioning.createInstance(db, { ownerBuilderId: 7, slug: 'mercury', domain: 'mercury.example.com' });
144
+ assert.equal(out.created, false);
145
+ assert.equal(out.instance.id, 18);
146
+ });
147
+
148
+ await ta('a REAL address race — domain taken, slug free — propagates for the route to map', async () => {
149
+ const db = fakeDb({ insertThrows: DOMAIN_VIOLATION(), slugRow: null });
150
+ await assert.rejects(
151
+ () => provisioning.createInstance(db, { ownerBuilderId: 7, slug: 'venus', domain: 'mercury.example.com' }),
152
+ (e) => provisioning.isDomainTaken(e),
153
+ 'no row under this slug means the clash can only be the address');
154
+ });
155
+
156
+ await ta('an unrelated database error is never swallowed as a re-request', async () => {
157
+ const db = fakeDb({ insertThrows: Object.assign(new Error('connection terminated'), { code: '57P01' }), slugRow: null });
158
+ await assert.rejects(
159
+ () => provisioning.createInstance(db, { ownerBuilderId: 7, slug: 'venus' }),
160
+ /connection terminated/);
161
+ });
162
+
163
+ // ── route slice ────────────────────────────────────────────────────────────
164
+ let instance = null;
165
+ let createThrows = null;
166
+ let updateThrows = null;
167
+ let updateCalls = [];
168
+ let enqueueCalls = [];
169
+
170
+ provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
171
+ provisioning.getInstanceByDomain = async () => null; // the check PASSES — the race is after it
172
+ provisioning.getInstanceBySlug = async () => null;
173
+ provisioning.createInstance = async () => { if (createThrows) throw createThrows; return { instance: { id: 1 }, created: true }; };
174
+ provisioning.updateInstanceDomain = async (_db, id, domain, releasing = null) => {
175
+ updateCalls.push({ id, domain, releasing });
176
+ if (updateThrows) throw updateThrows;
177
+ return { ...instance, id, domain };
178
+ };
179
+ provisioning.enqueueIntent = async (_db, id, action) => { enqueueCalls.push({ id, action }); return { intent: { id: 9, action, state: 'pending' }, created: true }; };
180
+ provisioning.getOpenIntent = async () => null;
181
+ provisioning.recordEvent = async () => {};
182
+
183
+ const api = require('../src/module-api.js');
184
+ api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
185
+ api.requirePermission = () => (_req, _res, next) => next();
186
+ api.branding = () => ({
187
+ domains: { publicOrigin: 'https://cb.test', instanceBase: 'cb.test' },
188
+ identity: { productName: 'Cloud Bongos' },
189
+ });
190
+
191
+ // Both routes are rate-limited per builder and every test here posts as builder
192
+ // 7, so the real cap would throttle the suite rather than the behaviour under
193
+ // test (the provisioning_domain_attach.mjs precedent).
194
+ const rateLimit = require('../modules/provisioning/rate-limit.js');
195
+ const realMakeAskRateLimiter = rateLimit.makeAskRateLimiter;
196
+ rateLimit.makeAskRateLimiter = (opts = {}) => realMakeAskRateLimiter({ ...opts, max: 10_000 });
197
+
198
+ const express = require('express');
199
+ const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
200
+ const app = express();
201
+ app.use(express.json());
202
+ app.use((_req, res, next) => {
203
+ res.fail = (code, opts, details) => {
204
+ const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
205
+ res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
206
+ };
207
+ next();
208
+ });
209
+ app.use('/api/bongos', provisioningRoutes());
210
+ const server = app.listen(0);
211
+ await new Promise((r) => server.once('listening', r));
212
+ const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
213
+
214
+ function post(p, body = {}) {
215
+ return new Promise((resolve, reject) => {
216
+ const payload = JSON.stringify(body);
217
+ const req = http.request(`${base}${p}`, { method: 'POST',
218
+ headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } }, (res) => {
219
+ let d = ''; res.on('data', (c) => { d += c; });
220
+ res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
221
+ });
222
+ req.on('error', reject);
223
+ req.end(payload);
224
+ });
225
+ }
226
+
227
+ function reset() { instance = null; createThrows = null; updateThrows = null; updateCalls = []; enqueueCalls = []; }
228
+
229
+ console.log('\nPOST /provisioning/instances — the index refusing a raced creation:');
230
+
231
+ await ta('the index catching the race answers domain_taken 409, not a raw 500', async () => {
232
+ reset();
233
+ createThrows = DOMAIN_VIOLATION();
234
+ const res = await post('/provisioning/instances', { slug: 'venus', domain: 'mercury.example.com', no_address: false });
235
+ assert.equal(res.status, 409, res.raw);
236
+ assert.equal(res.body.error.code, 'domain_taken',
237
+ 'the caller must not be able to tell which layer caught it — only the timing differs');
238
+ });
239
+
240
+ await ta('any OTHER failure is still the honest 500 — the mapping is narrow', async () => {
241
+ reset();
242
+ createThrows = Object.assign(new Error('connection terminated'), { code: '57P01' });
243
+ const res = await post('/provisioning/instances', { slug: 'venus', domain: 'mercury.example.com' });
244
+ assert.equal(res.status, 500, res.raw);
245
+ assert.equal(res.body.error.code, 'request_failed');
246
+ });
247
+
248
+ console.log('\nPOST /provisioning/instances/:id/domain — the index refusing a raced attach:');
249
+
250
+ await ta('the index catching the race answers domain_taken 409, and nothing is queued', async () => {
251
+ reset();
252
+ instance = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'standalone', domain: null };
253
+ updateThrows = DOMAIN_VIOLATION();
254
+ const res = await post('/provisioning/instances/18/domain', { domain: 'mercury.example.com' });
255
+ assert.equal(res.status, 409, res.raw);
256
+ assert.equal(res.body.error.code, 'domain_taken');
257
+ assert.equal(enqueueCalls.length, 0,
258
+ 'the UPDATE was refused, so nothing was recorded — an attach intent would wire an address this instance does not hold');
259
+ });
260
+
261
+ await ta('any OTHER failure on attach is still the honest 500', async () => {
262
+ reset();
263
+ instance = { owner_builder_id: 7, slug: 'spike', status: 'active', hosting_shape: 'standalone', domain: null };
264
+ updateThrows = Object.assign(new Error('connection terminated'), { code: '57P01' });
265
+ const res = await post('/provisioning/instances/18/domain', { domain: 'mercury.example.com' });
266
+ assert.equal(res.status, 500, res.raw);
267
+ assert.equal(res.body.error.code, 'domain_attach_failed');
268
+ });
269
+
270
+ server.close();
271
+ rateLimit.makeAskRateLimiter = realMakeAskRateLimiter;
272
+
273
+ console.log(`\n${passed} passed, ${failed} failed`);
274
+ process.exit(failed ? 1 : 0);