@bongos/core 1.19.121 → 1.19.122
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 +28 -23
- package/clients/bongos-client/index.d.ts +1 -1
- package/config/branding.neutral.json +1 -0
- package/docs/api/openapi.json +3 -0
- package/docs/api-reference.md +1 -1
- package/docs/branding-contract.md +1 -0
- package/modules/provisioning/provisioning.js +37 -0
- package/modules/provisioning/routes/provisioning.js +93 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/projects.html +38 -2
- package/scripts/gds/init.js +8 -2
- package/scripts/gds/onboard.js +1 -1
- package/src/bongos/serve-internal.js +11 -2
- package/src/module-api.js +3 -1
- package/tests/provisioning_default_domain.mjs +331 -0
- package/tests/provisioning_domain_attach.mjs +1 -0
- package/tests/provisioning_teardown_intent.mjs +1 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.122",
|
|
6
|
+
"core_contract": "1.19.122",
|
|
7
|
+
"source_commit": "b8e5d6f6d62653ae415e42551ccb3c3c4e6c6f8a",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-14T21:05:40.247Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 303,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1244,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1568,
|
|
20
|
+
"tree_sha256": "03f0574ec4219110a53d6b8faaa4b93bcc96f0371d85d2a2e412d06d21364892",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "eeb9b7bdf6054c92e3c605201c846d67ea20dd927355d2dc72479477c073fc7c"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
@@ -382,7 +382,7 @@
|
|
|
382
382
|
{
|
|
383
383
|
"path": "config/branding.neutral.json",
|
|
384
384
|
"mode": "0000644",
|
|
385
|
-
"sha256": "
|
|
385
|
+
"sha256": "dc69c5c9e9041da9cd3c297c3e7c81c8311f42952524297d9f228cc1751af3ea"
|
|
386
386
|
},
|
|
387
387
|
{
|
|
388
388
|
"path": "config/design-tokens.neutral.json",
|
|
@@ -1337,12 +1337,12 @@
|
|
|
1337
1337
|
{
|
|
1338
1338
|
"path": "docs/api-reference.md",
|
|
1339
1339
|
"mode": "0000644",
|
|
1340
|
-
"sha256": "
|
|
1340
|
+
"sha256": "b9af2341b7017c6e21b53a611a824b8d3a59653f967e6f83218c9f1d25d0ff58"
|
|
1341
1341
|
},
|
|
1342
1342
|
{
|
|
1343
1343
|
"path": "docs/api/openapi.json",
|
|
1344
1344
|
"mode": "0000644",
|
|
1345
|
-
"sha256": "
|
|
1345
|
+
"sha256": "35762381d39e1943558131e97d3541e28d47161a56ec0a3d664942c90fb9fa9b"
|
|
1346
1346
|
},
|
|
1347
1347
|
{
|
|
1348
1348
|
"path": "docs/architecture.md",
|
|
@@ -1352,7 +1352,7 @@
|
|
|
1352
1352
|
{
|
|
1353
1353
|
"path": "docs/branding-contract.md",
|
|
1354
1354
|
"mode": "0000644",
|
|
1355
|
-
"sha256": "
|
|
1355
|
+
"sha256": "f5918fa955f90ca30412ebce209bf2d51b67fa318f7f04d667262cd42c134745"
|
|
1356
1356
|
},
|
|
1357
1357
|
{
|
|
1358
1358
|
"path": "docs/branding-fill-prompt.md",
|
|
@@ -3862,7 +3862,7 @@
|
|
|
3862
3862
|
{
|
|
3863
3863
|
"path": "modules/provisioning/provisioning.js",
|
|
3864
3864
|
"mode": "0000644",
|
|
3865
|
-
"sha256": "
|
|
3865
|
+
"sha256": "59fd212df421b2bdcdfaef4871aa1927c371d3f7d6affafe44e810135acd12b6"
|
|
3866
3866
|
},
|
|
3867
3867
|
{
|
|
3868
3868
|
"path": "modules/provisioning/rate-limit.js",
|
|
@@ -3872,7 +3872,7 @@
|
|
|
3872
3872
|
{
|
|
3873
3873
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
3874
3874
|
"mode": "0000644",
|
|
3875
|
-
"sha256": "
|
|
3875
|
+
"sha256": "321db8769eda7c5f6f385e90edaeec70fc071f0bc2e53e6147705153b0d32d3a"
|
|
3876
3876
|
},
|
|
3877
3877
|
{
|
|
3878
3878
|
"path": "modules/provisioning/tests/liveness.mjs",
|
|
@@ -4167,12 +4167,12 @@
|
|
|
4167
4167
|
{
|
|
4168
4168
|
"path": "package-lock.json",
|
|
4169
4169
|
"mode": "0000644",
|
|
4170
|
-
"sha256": "
|
|
4170
|
+
"sha256": "969a257b3ae8148774d7d64cef01f68be0eec703043a129ae34984f3399208d9"
|
|
4171
4171
|
},
|
|
4172
4172
|
{
|
|
4173
4173
|
"path": "package.json",
|
|
4174
4174
|
"mode": "0000644",
|
|
4175
|
-
"sha256": "
|
|
4175
|
+
"sha256": "904c4fce25eeb089d80da1a5a0cfced1a96b99aea4f2bc28e5ce07d66bd8a880"
|
|
4176
4176
|
},
|
|
4177
4177
|
{
|
|
4178
4178
|
"path": "public-docs/index.html",
|
|
@@ -4427,7 +4427,7 @@
|
|
|
4427
4427
|
{
|
|
4428
4428
|
"path": "public-landing/projects.html",
|
|
4429
4429
|
"mode": "0000644",
|
|
4430
|
-
"sha256": "
|
|
4430
|
+
"sha256": "925e7964f66d59f2df260a611e602bdeafe3f8f3b73d3d16db7bdfa073b4163c"
|
|
4431
4431
|
},
|
|
4432
4432
|
{
|
|
4433
4433
|
"path": "public-landing/world/assets/clouds/cloud-island-large.png",
|
|
@@ -4892,7 +4892,7 @@
|
|
|
4892
4892
|
{
|
|
4893
4893
|
"path": "scripts/gds/init.js",
|
|
4894
4894
|
"mode": "0000644",
|
|
4895
|
-
"sha256": "
|
|
4895
|
+
"sha256": "bde86f3f65cdd4a1ad0e143c58f6517328dd7cdee7c1194c0fa614aaf9df56f6"
|
|
4896
4896
|
},
|
|
4897
4897
|
{
|
|
4898
4898
|
"path": "scripts/gds/install-git-hooks.js",
|
|
@@ -5037,7 +5037,7 @@
|
|
|
5037
5037
|
{
|
|
5038
5038
|
"path": "scripts/gds/onboard.js",
|
|
5039
5039
|
"mode": "0000644",
|
|
5040
|
-
"sha256": "
|
|
5040
|
+
"sha256": "c6fb2c531c6d84dcb16daea50984e1b24021b6379cf992a01482951b119bf9e4"
|
|
5041
5041
|
},
|
|
5042
5042
|
{
|
|
5043
5043
|
"path": "scripts/gds/onboarding-analyze.js",
|
|
@@ -5717,7 +5717,7 @@
|
|
|
5717
5717
|
{
|
|
5718
5718
|
"path": "src/bongos/serve-internal.js",
|
|
5719
5719
|
"mode": "0000644",
|
|
5720
|
-
"sha256": "
|
|
5720
|
+
"sha256": "465211ef9e62fd74828d326ab427b0b21b7b73f6124e842b55f8b23d51660ebe"
|
|
5721
5721
|
},
|
|
5722
5722
|
{
|
|
5723
5723
|
"path": "src/bongos/stealth-gate.js",
|
|
@@ -5752,7 +5752,7 @@
|
|
|
5752
5752
|
{
|
|
5753
5753
|
"path": "src/module-api.js",
|
|
5754
5754
|
"mode": "0000644",
|
|
5755
|
-
"sha256": "
|
|
5755
|
+
"sha256": "79041166a496039eff11ecc00334671609fadace4a54645cf2b287a5b5ff1a53"
|
|
5756
5756
|
},
|
|
5757
5757
|
{
|
|
5758
5758
|
"path": "src/module-loader/catalog.js",
|
|
@@ -7294,10 +7294,15 @@
|
|
|
7294
7294
|
"mode": "0000644",
|
|
7295
7295
|
"sha256": "edb6c5da9ec32536d50f7cb648b81b026b7926f73f891feb0906fc7343b5b11d"
|
|
7296
7296
|
},
|
|
7297
|
+
{
|
|
7298
|
+
"path": "tests/provisioning_default_domain.mjs",
|
|
7299
|
+
"mode": "0000644",
|
|
7300
|
+
"sha256": "1403cc764270e796b985cc0dc38cbf498001e4726b2bfe53020cc55d12d1ca66"
|
|
7301
|
+
},
|
|
7297
7302
|
{
|
|
7298
7303
|
"path": "tests/provisioning_domain_attach.mjs",
|
|
7299
7304
|
"mode": "0000644",
|
|
7300
|
-
"sha256": "
|
|
7305
|
+
"sha256": "fbbd7397fd03181e9c2ea58ed8accb1a9d9d66d4c904a09265161e1034b02f9f"
|
|
7301
7306
|
},
|
|
7302
7307
|
{
|
|
7303
7308
|
"path": "tests/provisioning_github_app_routes.mjs",
|
|
@@ -7312,7 +7317,7 @@
|
|
|
7312
7317
|
{
|
|
7313
7318
|
"path": "tests/provisioning_teardown_intent.mjs",
|
|
7314
7319
|
"mode": "0000644",
|
|
7315
|
-
"sha256": "
|
|
7320
|
+
"sha256": "68d328b69cd8b981e8454d679c00a757abeeafb4e083249489eec744cdd4898d"
|
|
7316
7321
|
},
|
|
7317
7322
|
{
|
|
7318
7323
|
"path": "tests/pruned_lockfile_integrity.mjs",
|
|
@@ -252,7 +252,7 @@ export interface PostProvisioningInstancesIdForceTeardownResponse { ok: boolean;
|
|
|
252
252
|
export interface PostProvisioningInstancesIdGithubAppRequest { account?: string }
|
|
253
253
|
export interface PostProvisioningInstancesIdGithubAppResponse { state: unknown; account: unknown; post_url: unknown; personal_url: unknown; org_url: unknown; org_login: unknown; manifest: unknown; redirect_url: unknown; expires_at: unknown }
|
|
254
254
|
export interface PostProvisioningInstancesIdTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
|
|
255
|
-
export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; tier?: string; onboard_mode?: string; type?: string }
|
|
255
|
+
export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; no_address?: boolean; tier?: string; onboard_mode?: string; type?: string }
|
|
256
256
|
export interface PostSearchMarkRequest { chunk_id?: StringifiableId; useful?: unknown }
|
|
257
257
|
export interface PostSearchRequest { expand?: unknown; q?: string; source_kind?: string; limit?: unknown }
|
|
258
258
|
export interface PostSearchResponse { expand?: unknown; chunk?: unknown; query?: unknown; count?: unknown; total_full_tokens?: unknown; results?: unknown }
|
package/docs/api/openapi.json
CHANGED
|
@@ -15826,6 +15826,9 @@
|
|
|
15826
15826
|
"type": "string",
|
|
15827
15827
|
"maxLength": 253
|
|
15828
15828
|
},
|
|
15829
|
+
"no_address": {
|
|
15830
|
+
"type": "boolean"
|
|
15831
|
+
},
|
|
15829
15832
|
"tier": {
|
|
15830
15833
|
"type": "string",
|
|
15831
15834
|
"maxLength": 40
|
package/docs/api-reference.md
CHANGED
|
@@ -368,7 +368,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
368
368
|
| GET | `/api/bongos/provisioning/github-app/status` | `public` | — | GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding callback page polls (task 2096) to know when setup… |
|
|
369
369
|
| GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
|
|
370
370
|
| GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
|
|
371
|
-
| POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
|
|
371
|
+
| POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
|
|
372
372
|
| GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one of the caller's own instances. |
|
|
373
373
|
| POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
|
|
374
374
|
| POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
|
|
@@ -31,6 +31,7 @@ env override → instance config (config/branding.json) → neutral star
|
|
|
31
31
|
| `domains.provisioningOrigin` | **Optional.** Control-plane front-door origin the [provisioning module](../../modules/provisioning/CLAUDE.md) uses to build a new customer's GitHub-App `redirect_url`. Set it when the control plane co-hosts a game whose `publicOrigin` is the game's own domain (OTB = customer 0) so the customer's App carries the PLATFORM brand (e.g. `https://get.cloudbongos.com`), not the game's. **Falls back to `publicOrigin` when unset** — an instance whose `publicOrigin` is already the platform needs no override. Env-overridable: `<PREFIX>_PROVISIONING_ORIGIN`. ([#2101](https://example.com/builders#/task/2101)) |
|
|
32
32
|
| `domains.devBoxBase` | Apex zone the **proxied** browser-terminal + sandbox hosts ride (`term-<login>.<devBoxBase>`, `sandbox-<login>.<devBoxBase>`). Must be a one-label-under-apex zone so free edge TLS (`*.<apex>`) covers it — a two-label host fails edge TLS (the [#771](https://example.com/builders#/task/771) lesson). Empty ⇒ no managed boxes. |
|
|
33
33
|
| `domains.sshBoxBase` | Zone for the **legacy direct-IP SSH** box host (`box-<login>.<sshBoxBase>`, unproxied). Deliberately distinct from `devBoxBase` (one proxied/edge-TLS, one direct-IP). |
|
|
34
|
+
| `domains.instanceBase` | Apex zone new projects get their **auto-assigned** address under (`<slug>.<instanceBase>`, task 1002704 — every project's hall reachable day one; `no_address: true` on the create call opts out). Same one-label-under-apex/edge-TLS constraint as `devBoxBase`. Deliberately NOT derived from `publicOrigin` (on a co-hosting control plane that is the game's domain). Empty ⇒ no auto-assign — creates are domainless unless the caller brings a domain. |
|
|
34
35
|
| `domains.sandboxPrefix` / `termPrefix` | Subdomain prefixes for the sandbox-preview + browser-terminal hosts. |
|
|
35
36
|
| `repo` | `{ owner, name }` GitHub binding. |
|
|
36
37
|
| `db.database` | DB name the GDS pool connects to when neither `DATABASE_URL` nor `PGHOST`/`PGDATABASE` is set (`src/bongos/pool.js`, ADR 0062 §5). `null` in the neutral starter = **fail loud** rather than silently target a host DB. |
|
|
@@ -111,6 +111,42 @@ function normalizeHostname(h) {
|
|
|
111
111
|
return s;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// The auto-assigned address for a new project: <slug>.<apex> (task 1002704 — the
|
|
115
|
+
// owner's auto-assign/opt-out decision; every project's hall reachable day one).
|
|
116
|
+
// The apex is branding domains.instanceBase; blank or unparseable → null, so a
|
|
117
|
+
// self-host or unconfigured instance keeps creating domainless instead of
|
|
118
|
+
// guessing (the client's location.hostname guess stays client-side only). PURE.
|
|
119
|
+
function defaultDomainForSlug(slug, apex) {
|
|
120
|
+
if (!isValidSlug(slug)) return null;
|
|
121
|
+
const a = String(apex == null ? '' : apex).trim();
|
|
122
|
+
if (!a) return null;
|
|
123
|
+
return normalizeHostname(`${slug}.${a}`) || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Labels the platform itself occupies (or has planned) directly under the instance
|
|
127
|
+
// apex, plus the dev-box hostname prefixes. The runner's DNS write is an UPSERT,
|
|
128
|
+
// so a project claiming one of these would overwrite platform records — refuse at
|
|
129
|
+
// the API layer instead (task 1002704; the guard applies to auto-assigned AND
|
|
130
|
+
// bring-your-own hostnames, on create and on attach). Anything deeper than one
|
|
131
|
+
// label under the apex is also refused: it breaks the wildcard-cert rule (one
|
|
132
|
+
// label under the apex — the #771 lesson) and nothing legitimate creates it.
|
|
133
|
+
const RESERVED_APEX_LABELS = new Set([
|
|
134
|
+
'www', 'api', 'get', 'status', 'builders', 'docs', 'mail', 'admin', 'app',
|
|
135
|
+
'cdn', 'ns1', 'ns2', 'demo', 'gate', 'downloads',
|
|
136
|
+
]);
|
|
137
|
+
const RESERVED_APEX_PREFIXES = ['term-', 'sandbox-'];
|
|
138
|
+
function isReservedHostname(domain, apex) {
|
|
139
|
+
const host = normalizeHostname(domain);
|
|
140
|
+
const zone = normalizeHostname(apex);
|
|
141
|
+
if (!host || !zone) return false;
|
|
142
|
+
if (host === zone) return true;
|
|
143
|
+
if (!host.endsWith(`.${zone}`)) return false;
|
|
144
|
+
const label = host.slice(0, -(zone.length + 1));
|
|
145
|
+
if (label.includes('.')) return true;
|
|
146
|
+
if (RESERVED_APEX_LABELS.has(label)) return true;
|
|
147
|
+
return RESERVED_APEX_PREFIXES.some((p) => label.startsWith(p));
|
|
148
|
+
}
|
|
149
|
+
|
|
114
150
|
// The instance lifecycle states for which an on-demand TLS cert may be minted: a
|
|
115
151
|
// host mid-standup ('provisioning' — Caddy's first handshake can land here, while
|
|
116
152
|
// the snippet is written + reloaded) or live ('active'). NOT 'requested' (nothing
|
|
@@ -658,6 +694,7 @@ module.exports = {
|
|
|
658
694
|
PROVISION_ENQUEUE_FROM, DOMAIN_ATTACH_FROM, TIER_MONTHLY_USD, CERT_ELIGIBLE_STATES,
|
|
659
695
|
// pure
|
|
660
696
|
isValidSlug, costEstimateUsd, publicInstance, normalizeHostname,
|
|
697
|
+
defaultDomainForSlug, isReservedHostname,
|
|
661
698
|
// pure — GitHub App Manifest flow (task 2080)
|
|
662
699
|
githubAppName, buildGithubAppManifest, manifestPostUrl,
|
|
663
700
|
// data access
|
|
@@ -55,6 +55,16 @@ function mothershipOrigin() {
|
|
|
55
55
|
catch { return ''; }
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// The apex under which new projects get their auto-assigned <slug>.<apex> address
|
|
59
|
+
// (task 1002704). A dedicated branding key — NOT publicOrigin, which on a control
|
|
60
|
+
// plane that co-hosts a game is the GAME's domain (see mothershipOrigin above), so
|
|
61
|
+
// deriving from it would assign customers under the wrong zone. Blank/absent →
|
|
62
|
+
// no auto-assign: self-host and unconfigured instances keep creating domainless.
|
|
63
|
+
function instanceApex() {
|
|
64
|
+
try { return (api.branding().domains || {}).instanceBase || ''; }
|
|
65
|
+
catch { return ''; }
|
|
66
|
+
}
|
|
67
|
+
|
|
58
68
|
// The provisioning mothership's product brand (e.g. "Cloud Bongos" on the control plane),
|
|
59
69
|
// used to brand-qualify the auto GitHub App name so a common project slug ("demo") does
|
|
60
70
|
// not collide with an existing GitHub App (task 2093). Portable — reads THIS instance's
|
|
@@ -311,6 +321,7 @@ module.exports = function provisioningRoutes() {
|
|
|
311
321
|
target_ref: { type: 'string', maxLength: 200 },
|
|
312
322
|
hosting_shape: { type: 'string', maxLength: 20 },
|
|
313
323
|
domain: { type: 'string', maxLength: 253 },
|
|
324
|
+
no_address: { type: 'boolean' },
|
|
314
325
|
tier: { type: 'string', maxLength: 40 },
|
|
315
326
|
onboard_mode: { type: 'string', maxLength: 20 },
|
|
316
327
|
// the declared project Type (task 1002772) — optional, closed vocabulary
|
|
@@ -337,10 +348,57 @@ module.exports = function provisioningRoutes() {
|
|
|
337
348
|
});
|
|
338
349
|
}
|
|
339
350
|
try {
|
|
351
|
+
// A re-request must not re-litigate the address: createInstance never updates
|
|
352
|
+
// an existing row's domain, so for one the guards below could only misfire —
|
|
353
|
+
// a "Try again" on a pre-existing reserved-label row, or domain_taken fired
|
|
354
|
+
// against the row's own address. Birth-time rules run at birth only.
|
|
355
|
+
const existing = await provisioning.getInstanceBySlug(pool, slug);
|
|
356
|
+
// Resolve the address (task 1002704 — auto-assign, opt-out). An explicit
|
|
357
|
+
// domain (bring-your-own) wins; `no_address: true` is the deliberate opt-out;
|
|
358
|
+
// otherwise default to <slug>.<instanceBase> so an API-first caller inherits
|
|
359
|
+
// the same day-one address the wizard promises. An omitted domain is NOT an
|
|
360
|
+
// opt-out. Explicit domains are normalized + refused when malformed — the
|
|
361
|
+
// same bar the attach route has always applied (create inserted them raw).
|
|
362
|
+
let domain;
|
|
363
|
+
const assignedDefault = provisioning.defaultDomainForSlug(slug, instanceApex());
|
|
364
|
+
if (!existing) {
|
|
365
|
+
if (String(body.domain || '').trim()) {
|
|
366
|
+
domain = provisioning.normalizeHostname(body.domain);
|
|
367
|
+
if (!domain) {
|
|
368
|
+
return res.fail('bad_domain', 400, { reason: 'not a plausible hostname (letters/digits/dashes, at least two labels)' });
|
|
369
|
+
}
|
|
370
|
+
} else if (!body.no_address) {
|
|
371
|
+
domain = assignedDefault || undefined;
|
|
372
|
+
}
|
|
373
|
+
if (domain && provisioning.isReservedHostname(domain, instanceApex())) {
|
|
374
|
+
return res.fail('domain_reserved', {
|
|
375
|
+
status: 400,
|
|
376
|
+
message: `${domain} is a platform-reserved address — pick a different handle or bring your own domain.`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Inside the platform apex, the only address a project can hold is its own
|
|
380
|
+
// <slug>.<apex> — any other label under the apex is a squat on another
|
|
381
|
+
// handle's address space. Domains outside the apex are the real BYO path.
|
|
382
|
+
const apexZone = provisioning.normalizeHostname(instanceApex());
|
|
383
|
+
if (domain && apexZone && domain.endsWith(`.${apexZone}`) && domain !== assignedDefault) {
|
|
384
|
+
return res.fail('domain_reserved', {
|
|
385
|
+
status: 400,
|
|
386
|
+
message: `Inside ${apexZone}, a project's address is its own <handle>.${apexZone} — bring a domain you own instead.`,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
// The same uniqueness bar as the attach route: one owner's address can
|
|
390
|
+
// never silently shadow another's.
|
|
391
|
+
if (domain) {
|
|
392
|
+
const holder = await provisioning.getInstanceByDomain(pool, domain);
|
|
393
|
+
if (holder && holder.slug !== slug) {
|
|
394
|
+
return res.fail('domain_taken', { status: 409, message: 'That address is already held by another instance.' });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
340
398
|
const { instance, created } = await provisioning.createInstance(pool, {
|
|
341
399
|
ownerBuilderId: req.builder.id, slug,
|
|
342
400
|
targetRef: body.target_ref, hostingShape: shape,
|
|
343
|
-
domain
|
|
401
|
+
domain, tier: body.tier, onboardMode,
|
|
344
402
|
type: body.type,
|
|
345
403
|
});
|
|
346
404
|
// Slug already taken by ANOTHER builder — refuse (never reassign ownership).
|
|
@@ -360,7 +418,7 @@ module.exports = function provisioningRoutes() {
|
|
|
360
418
|
if (created) {
|
|
361
419
|
await provisioning.recordEvent(pool, {
|
|
362
420
|
instanceId: instance.id, ownerBuilderId: req.builder.id, event: 'request',
|
|
363
|
-
detail: `provision requested (shape=${shape}, mode=${onboardMode}${
|
|
421
|
+
detail: `provision requested (shape=${shape}, mode=${onboardMode}${domain ? `, domain=${domain} (${domain === assignedDefault ? 'assigned' : 'byo'})` : ', no address'})`,
|
|
364
422
|
actor: 'api:self',
|
|
365
423
|
}).catch(() => {});
|
|
366
424
|
}
|
|
@@ -376,6 +434,15 @@ module.exports = function provisioningRoutes() {
|
|
|
376
434
|
};
|
|
377
435
|
}
|
|
378
436
|
}
|
|
437
|
+
// …and a re-request cannot REMOVE one either (task 1002704's opt-out only
|
|
438
|
+
// applies at birth) — same honesty, opposite direction.
|
|
439
|
+
let noAddressIgnored;
|
|
440
|
+
if (!created && body.no_address && instance.domain) {
|
|
441
|
+
noAddressIgnored = {
|
|
442
|
+
current: instance.domain,
|
|
443
|
+
hint: "a re-request never removes an existing instance's address",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
379
446
|
// Same honesty for a changed Type (task 1002772): a re-request keeps the
|
|
380
447
|
// original declaration, and unlike domain there is no mutation route yet —
|
|
381
448
|
// say so rather than silently dropping the new answer.
|
|
@@ -392,6 +459,7 @@ module.exports = function provisioningRoutes() {
|
|
|
392
459
|
queued: intentCreated,
|
|
393
460
|
created,
|
|
394
461
|
...(domainIgnored ? { domain_ignored: domainIgnored } : {}),
|
|
462
|
+
...(noAddressIgnored ? { no_address_ignored: noAddressIgnored } : {}),
|
|
395
463
|
...(typeIgnored ? { type_ignored: typeIgnored } : {}),
|
|
396
464
|
});
|
|
397
465
|
} catch (err) {
|
|
@@ -497,16 +565,38 @@ module.exports = function provisioningRoutes() {
|
|
|
497
565
|
if (!domain) {
|
|
498
566
|
return res.fail('bad_domain', 400, { reason: 'a real multi-label hostname (e.g. myproject.example.com) — no scheme, path, or bare TLD' });
|
|
499
567
|
}
|
|
568
|
+
// The runner's DNS write is an UPSERT, so an attach may not claim a
|
|
569
|
+
// platform-reserved name any more than a create may (task 1002704).
|
|
570
|
+
if (provisioning.isReservedHostname(domain, instanceApex())) {
|
|
571
|
+
return res.fail('domain_reserved', {
|
|
572
|
+
status: 400,
|
|
573
|
+
message: `${domain} is a platform-reserved address — bring a domain of your own instead.`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
500
576
|
const inst = await provisioning.getInstanceById(pool, id);
|
|
501
577
|
if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) {
|
|
502
578
|
return res.fail('instance_not_found', 404);
|
|
503
579
|
}
|
|
580
|
+
// Same rule as create: inside the platform apex the only address an instance
|
|
581
|
+
// can hold is its own <slug>.<apex> — anything else is a squat (task 1002704).
|
|
582
|
+
const apexZone = provisioning.normalizeHostname(instanceApex());
|
|
583
|
+
const assignedDefault = provisioning.defaultDomainForSlug(inst.slug, instanceApex());
|
|
584
|
+
if (apexZone && domain.endsWith(`.${apexZone}`) && domain !== assignedDefault) {
|
|
585
|
+
return res.fail('domain_reserved', {
|
|
586
|
+
status: 400,
|
|
587
|
+
message: `Inside ${apexZone}, an instance's address is its own <handle>.${apexZone} — bring a domain you own instead.`,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
504
590
|
const current = provisioning.normalizeHostname(inst.domain);
|
|
505
591
|
if (current && current === domain) {
|
|
506
592
|
return res.json({ ok: true, changed: false, queued: false, domain: inst.domain,
|
|
507
593
|
message: 'This instance already has that address.' });
|
|
508
594
|
}
|
|
509
|
-
|
|
595
|
+
// A platform-ASSIGNED address may be upgraded away to a bring-your-own
|
|
596
|
+
// domain — that upgrade path is half the task-1002704 decision (the old
|
|
597
|
+
// record lingers in DNS, the idea-1000710 class). A genuine BYO address
|
|
598
|
+
// stays un-re-pointable, as before.
|
|
599
|
+
if (current && current !== assignedDefault) {
|
|
510
600
|
return res.fail('domain_already_set', { status: 409,
|
|
511
601
|
message: `This instance is already addressed as '${inst.domain}' — re-pointing an existing address is not supported here.`,
|
|
512
602
|
details: { current: inst.domain } });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.122",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.122",
|
|
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.
|
|
3
|
+
"version": "1.19.122",
|
|
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",
|
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>{{productName}} — projects</title>
|
|
7
7
|
<meta name="description" content="{{tagline}} Browse the map of what is being built, manage your own projects, and start a new one.">
|
|
8
|
+
<!-- the apex new projects are addressed under (server-stamped, task 1002704) —
|
|
9
|
+
the wizard's address preview reads this so it matches what the server will
|
|
10
|
+
actually assign; blank/unstamped → fall back to guessing from the host -->
|
|
11
|
+
<meta name="instance-base" content="{{instanceBase}}">
|
|
8
12
|
<!-- Fonts: the production instance also injects these from the branding pack
|
|
9
13
|
(config/branding.json theme.ui.fonts.links); kept here so the page reads
|
|
10
14
|
correctly when viewed standalone. -->
|
|
@@ -2054,6 +2058,12 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2054
2058
|
dotless/local host (localhost, an IP) there is nothing sane to assign, so
|
|
2055
2059
|
the step degrades to the old optional-domain form. ── */
|
|
2056
2060
|
function apexBase() {
|
|
2061
|
+
/* the server-stamped apex wins — it is what POST /provisioning/instances
|
|
2062
|
+
will actually assign; the host guess below can disagree on a subdomain
|
|
2063
|
+
hub (e.g. get.<apex>) and would then preview an address that 400s */
|
|
2064
|
+
var meta = document.querySelector('meta[name="instance-base"]');
|
|
2065
|
+
var stamped = meta ? String(meta.content || '').toLowerCase() : '';
|
|
2066
|
+
if (stamped && stamped.indexOf('{{') < 0 && stamped.indexOf('.') > 0) return stamped;
|
|
2057
2067
|
var h = String(location.hostname || '').toLowerCase().replace(/^www\./, '');
|
|
2058
2068
|
if (!h || h.indexOf('.') < 0) return null;
|
|
2059
2069
|
if (/^(localhost|127\.|\d+\.\d+\.\d+\.\d+$|\[)/.test(h)) return null;
|
|
@@ -2479,6 +2489,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2479
2489
|
var code = err && err.data && err.data.error && (err.data.error.code || '');
|
|
2480
2490
|
if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
|
|
2481
2491
|
if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
|
|
2492
|
+
if (code === 'domain_reserved') return 'That address is reserved for the platform itself. Pick a different handle, or bring a domain you own.';
|
|
2493
|
+
if (code === 'domain_taken') return 'That address is already held by another project. Pick a different handle or domain.';
|
|
2494
|
+
if (code === 'bad_domain') return 'The domain didn’t parse as a real hostname — use something like myproject.example.com.';
|
|
2482
2495
|
return 'Couldn’t file the request: ' + (err && err.message ? err.message : 'something went sideways. Try again.');
|
|
2483
2496
|
}
|
|
2484
2497
|
|
|
@@ -2532,6 +2545,10 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2532
2545
|
type: state.projType || undefined,
|
|
2533
2546
|
hosting_shape: 'standalone',
|
|
2534
2547
|
domain: resolvedDomain() || undefined,
|
|
2548
|
+
/* The server now defaults an omitted domain to the assigned address
|
|
2549
|
+
(task 1002704), so the opt-out must travel explicitly — an absent
|
|
2550
|
+
domain no longer means "none". */
|
|
2551
|
+
no_address: state.noAddress ? true : undefined,
|
|
2535
2552
|
target_ref: state.owner + '/' + state.repo,
|
|
2536
2553
|
/* LOAD-BEARING for the control-plane runner, not just for the checklist we
|
|
2537
2554
|
render (task 1002562): 'adopt' makes it clone + layer onto the repo's
|
|
@@ -2554,6 +2571,7 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2554
2571
|
try {
|
|
2555
2572
|
sessionStorage.setItem(CREATED_KEY, JSON.stringify({
|
|
2556
2573
|
id: data.instance && data.instance.id, slug: state.slug, domain: resolvedDomain(),
|
|
2574
|
+
noaddr: !!state.noAddress,
|
|
2557
2575
|
mode: state.mode, name: state.name, at: Date.now()
|
|
2558
2576
|
}));
|
|
2559
2577
|
sessionStorage.removeItem(DRAFT_KEY);
|
|
@@ -2973,6 +2991,9 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
2973
2991
|
blank wizard */
|
|
2974
2992
|
state.name = created.name || created.slug; state.slug = created.slug || '';
|
|
2975
2993
|
state.domain = created.domain || ''; state.mode = created.mode || 'greenfield';
|
|
2994
|
+
/* restore the opt-out too — without it resolvedDomain() re-derives the
|
|
2995
|
+
assigned address and the done panel promises one that doesn't exist */
|
|
2996
|
+
state.noAddress = !!created.noaddr;
|
|
2976
2997
|
renderDone();
|
|
2977
2998
|
show(6, !!focus);
|
|
2978
2999
|
startProgress(created.id);
|
|
@@ -3155,7 +3176,21 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3155
3176
|
return inst.status === 'provisioning' || inst.status === 'active';
|
|
3156
3177
|
}
|
|
3157
3178
|
/* client mirror of provisioning.DOMAIN_ATTACH_FROM */
|
|
3158
|
-
|
|
3179
|
+
/* the platform-assigned <slug>.<apex> for this instance, from the server-stamped
|
|
3180
|
+
apex only — no host guessing here: the upgrade door below must never show for
|
|
3181
|
+
an address the server would refuse to re-point (task 1002704) */
|
|
3182
|
+
function assignedDefaultFor(inst) {
|
|
3183
|
+
var meta = document.querySelector('meta[name="instance-base"]');
|
|
3184
|
+
var base = meta ? String(meta.content || '').toLowerCase() : '';
|
|
3185
|
+
if (!base || base.indexOf('{{') >= 0 || base.indexOf('.') <= 0) return null;
|
|
3186
|
+
return inst.slug + '.' + base;
|
|
3187
|
+
}
|
|
3188
|
+
/* domainless instances get their first address; an assigned default may be
|
|
3189
|
+
upgraded to a bring-your-own domain (the server allows exactly that re-point) */
|
|
3190
|
+
function canAttachDomain(inst) {
|
|
3191
|
+
return (!inst.domain || inst.domain === assignedDefaultFor(inst)) &&
|
|
3192
|
+
(inst.status === 'requested' || inst.status === 'error' || inst.status === 'active');
|
|
3193
|
+
}
|
|
3159
3194
|
/* re-enqueue doors: Try again on snagged, Bring it back on torn down —
|
|
3160
3195
|
both re-POST the slug (the server's PROVISION_ENQUEUE_FROM gate) */
|
|
3161
3196
|
function canRetry(inst) { return inst.status === 'error' && !inst.open_intent; }
|
|
@@ -3292,7 +3327,8 @@ h1[tabindex="-1"]:focus{outline:none;}
|
|
|
3292
3327
|
if (canAttachDomain(inst)) {
|
|
3293
3328
|
body += '<div class="attach">' +
|
|
3294
3329
|
'<input type="text" id="dom-' + iid + '" placeholder="myproject.example.com" autocomplete="off" spellcheck="false" maxlength="253">' +
|
|
3295
|
-
'<button type="button" class="btn small" data-attach="' + iid + '">
|
|
3330
|
+
'<button type="button" class="btn small" data-attach="' + iid + '">' +
|
|
3331
|
+
(inst.domain ? 'Use your own domain' : 'Give it an address') + '</button></div>';
|
|
3296
3332
|
}
|
|
3297
3333
|
if (inst.status === 'error' && !inst.open_intent) {
|
|
3298
3334
|
/* once a retry is queued the strip tells the story — the stale error
|
package/scripts/gds/init.js
CHANGED
|
@@ -521,7 +521,7 @@ async function interview({ adopt = false } = {}) {
|
|
|
521
521
|
const wantProv = (await ask(rl, ' Provision production hosting now? (y/N)', 'N')).toLowerCase();
|
|
522
522
|
if (wantProv === 'y' || wantProv === 'yes') {
|
|
523
523
|
const shape = (await ask(rl, ' Hosting shape? (co-tenant = share an existing box, $0 / dedicated = its own droplet / standalone = its own repo + pinned @bongos/core, ADR 0108)', 'co-tenant')).toLowerCase();
|
|
524
|
-
const domain = await ask(rl, ' Domain you own — bring-your-own (blank =
|
|
524
|
+
const domain = await ask(rl, ' Domain you own — bring-your-own (blank = the platform assigns <slug>.<its apex> when configured)?');
|
|
525
525
|
const targetRef = await ask(rl, ' Target credential ref? (which control-plane creds the runner should use; optional)');
|
|
526
526
|
let tier = '';
|
|
527
527
|
if (shape === 'dedicated') tier = await ask(rl, ' Droplet tier? (e.g. s-1vcpu-2gb; optional)');
|
|
@@ -780,7 +780,13 @@ async function seedViaApi(spec, { dryRun, log = console.log, api = apiCall } = {
|
|
|
780
780
|
provisioned = true;
|
|
781
781
|
const inst = (pres.data && pres.data.instance) || {};
|
|
782
782
|
const cost = Number(inst.cost_estimate_usd || 0);
|
|
783
|
-
|
|
783
|
+
// Report the ROW's domain, not the request's: the server may have
|
|
784
|
+
// auto-assigned <slug>.<instanceBase> to a domainless request (task
|
|
785
|
+
// 1002704) — the operator must hear the address they actually got.
|
|
786
|
+
const addr = inst.domain
|
|
787
|
+
? `, ${inst.domain}${pr.domain ? '' : ' (auto-assigned — attach your own domain any time to replace it)'}`
|
|
788
|
+
: ', no address yet';
|
|
789
|
+
log(` ✓ requested provisioning of instance "${pr.slug}" (${pr.hosting_shape}${addr})` +
|
|
784
790
|
`${cost ? ` — est. $${cost}/mo` : ''} — a provision intent is queued for the control-plane runner`);
|
|
785
791
|
} else {
|
|
786
792
|
const why = pres.status === 404
|
package/scripts/gds/onboard.js
CHANGED
|
@@ -88,7 +88,7 @@ async function run() {
|
|
|
88
88
|
a.repoName = await ask(rl, 'GitHub repo name?');
|
|
89
89
|
a.firstAdmin = await ask(rl, 'First admin (GitHub login — the founding Archon)?', a.repoOwner);
|
|
90
90
|
a.slug = await ask(rl, 'Instance slug? (DNS/db-safe, e.g. mercury)');
|
|
91
|
-
a.domain = await ask(rl, 'Domain you own (bring-your-own; blank =
|
|
91
|
+
a.domain = await ask(rl, 'Domain you own (bring-your-own; blank = the platform assigns <slug>.<its apex> when configured)?');
|
|
92
92
|
|
|
93
93
|
// Step 2 — human: the GitHub repo (create empty for greenfield; use existing for adopt)
|
|
94
94
|
if (adopt) {
|
|
@@ -162,7 +162,7 @@ function injectBrandingGlobal(html, brand = clientBranding(), mods = clientModul
|
|
|
162
162
|
// fill cannot reach them, and a baked one would make every instance advertise
|
|
163
163
|
// whichever instance happened to author the file (task 1002748, direction C5/C6).
|
|
164
164
|
// Pure + exported for the test; both origins are injectable so it drives with zero I/O.
|
|
165
|
-
function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl()) {
|
|
165
|
+
function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl(), instanceBase = safeInstanceBase()) {
|
|
166
166
|
const world = (brand.identity && brand.identity.worldName) || '';
|
|
167
167
|
const product = (brand.identity && brand.identity.productName) || '';
|
|
168
168
|
const tagline = (brand.copy && brand.copy.tagline) || '';
|
|
@@ -178,7 +178,8 @@ function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOri
|
|
|
178
178
|
// a pack that names no hall of its own resolves to its OWN apex — never to
|
|
179
179
|
// whichever instance authored the file
|
|
180
180
|
.split('{{buildersOrigin}}').join(esc(hallOrigin || origin || ''))
|
|
181
|
-
.split('{{sourceRepoUrl}}').join(esc(sourceRepo || ''))
|
|
181
|
+
.split('{{sourceRepoUrl}}').join(esc(sourceRepo || ''))
|
|
182
|
+
.split('{{instanceBase}}').join(esc(instanceBase || ''));
|
|
182
183
|
}
|
|
183
184
|
// Every token lands in HTML — as element text, or inside a double-quoted
|
|
184
185
|
// attribute (href/content). Escaping is defence in depth, not a live fix: the
|
|
@@ -206,6 +207,14 @@ function safePublicOrigin() {
|
|
|
206
207
|
function safeBuildersOrigin() {
|
|
207
208
|
try { return branding().domains.buildersOrigin || ''; } catch { return ''; }
|
|
208
209
|
}
|
|
210
|
+
// The apex new projects get their auto-assigned <slug>.<apex> address under
|
|
211
|
+
// (task 1002704) — same server-only `domains` source. Stamped into markup (a
|
|
212
|
+
// <meta> content attribute, never a <script>) so the create wizard's address
|
|
213
|
+
// preview matches what the server will actually assign instead of guessing
|
|
214
|
+
// from location.hostname. Empty = no auto-assign; the wizard falls back.
|
|
215
|
+
function safeInstanceBase() {
|
|
216
|
+
try { return branding().domains.instanceBase || ''; } catch { return ''; }
|
|
217
|
+
}
|
|
209
218
|
// The instance's own source repo, from the pack's `repo: {owner, name}` binding —
|
|
210
219
|
// another server-only field (the branding contract keeps repo off clientBranding
|
|
211
220
|
// alongside domains). A page that wants to link "read the source" must take it
|
package/src/module-api.js
CHANGED
|
@@ -669,8 +669,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
669
669
|
// landed since 1.19.119 with no explicit bump. run 31836959112. (task 1002620)
|
|
670
670
|
// 1.19.121 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
671
671
|
// landed since 1.19.120 with no explicit bump. run 31838955115. (task 1002620)
|
|
672
|
+
// 1.19.122 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
673
|
+
// landed since 1.19.121 with no explicit bump. run 31840810826. (task 1002620)
|
|
672
674
|
// ---------------------------------------------------------------------------
|
|
673
|
-
const CORE_VERSION = '1.19.
|
|
675
|
+
const CORE_VERSION = '1.19.122'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
674
676
|
|
|
675
677
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
676
678
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// tests/provisioning_default_domain.mjs
|
|
2
|
+
//
|
|
3
|
+
// Auto-assign at creation (task 1002704, the owner's 2026-08-12 decision): every
|
|
4
|
+
// new project gets <slug>.<instanceBase> at creation so its hall is reachable day
|
|
5
|
+
// one; `no_address: true` is the explicit opt-out; an explicit body.domain
|
|
6
|
+
// (bring-your-own) wins. Also closes the create path's validation gap — the
|
|
7
|
+
// attach route normalized + uniqueness-checked its domain (task 1002692) while
|
|
8
|
+
// create inserted body.domain raw — and adds the reserved-name guard that keeps
|
|
9
|
+
// a project slug ('www', 'get', …) from UPSERTing over the platform's own DNS
|
|
10
|
+
// once every create carries an address.
|
|
11
|
+
//
|
|
12
|
+
// Covers:
|
|
13
|
+
// • defaultDomainForSlug / isReservedHostname — the pure helpers
|
|
14
|
+
// • POST /provisioning/instances — assign-by-default, opt-out, BYO-wins,
|
|
15
|
+
// bad_domain, domain_taken, domain_reserved, blank-apex degradation,
|
|
16
|
+
// re-request honesty (no phantom domain_ignored from the derived default;
|
|
17
|
+
// no_address_ignored when the opt-out can't apply)
|
|
18
|
+
// • POST /provisioning/instances/:id/domain — the same reserved-name refusal
|
|
19
|
+
//
|
|
20
|
+
// DB-free: data-access fns are stubbed on the cached module object (the
|
|
21
|
+
// provisioning_domain_attach.mjs harness). No network.
|
|
22
|
+
//
|
|
23
|
+
// Run: node tests/provisioning_default_domain.mjs
|
|
24
|
+
|
|
25
|
+
import { strict as assert } from 'node:assert';
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
import http from 'node:http';
|
|
28
|
+
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
31
|
+
|
|
32
|
+
let passed = 0, failed = 0;
|
|
33
|
+
function t(name, fn) {
|
|
34
|
+
try { fn(); passed++; console.log(` PASS ${name}`); }
|
|
35
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
36
|
+
}
|
|
37
|
+
async function ta(name, fn) {
|
|
38
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
39
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log('pure helpers — defaultDomainForSlug / isReservedHostname:');
|
|
43
|
+
|
|
44
|
+
t('defaultDomainForSlug: <slug>.<apex>, normalized', () => {
|
|
45
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', 'cloudbongos.com'), 'spike.cloudbongos.com');
|
|
46
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', 'CloudBongos.com.'), 'spike.cloudbongos.com');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
t('defaultDomainForSlug: blank/absent apex → null (self-host degrades to domainless)', () => {
|
|
50
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', ''), null);
|
|
51
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', null), null);
|
|
52
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', undefined), null);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
t('defaultDomainForSlug: invalid slug or unparseable apex → null', () => {
|
|
56
|
+
assert.equal(provisioning.defaultDomainForSlug('', 'cloudbongos.com'), null);
|
|
57
|
+
assert.equal(provisioning.defaultDomainForSlug('-bad-', 'cloudbongos.com'), null);
|
|
58
|
+
assert.equal(provisioning.defaultDomainForSlug('spike', '???'), null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
t('isReservedHostname: platform labels + box prefixes under the apex are reserved', () => {
|
|
62
|
+
for (const h of ['www.cloudbongos.com', 'get.cloudbongos.com', 'status.cloudbongos.com',
|
|
63
|
+
'term-alice.cloudbongos.com', 'sandbox-bob.cloudbongos.com', 'cloudbongos.com']) {
|
|
64
|
+
assert.equal(provisioning.isReservedHostname(h, 'cloudbongos.com'), true, `${h} should be reserved`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
t('isReservedHostname: ordinary one-label projects and foreign zones are not reserved', () => {
|
|
69
|
+
assert.equal(provisioning.isReservedHostname('spike.cloudbongos.com', 'cloudbongos.com'), false);
|
|
70
|
+
assert.equal(provisioning.isReservedHostname('www.example.com', 'cloudbongos.com'), false, 'BYO on a foreign zone is fine');
|
|
71
|
+
assert.equal(provisioning.isReservedHostname('spike.cloudbongos.com', ''), false, 'no apex configured → nothing to protect');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
t('isReservedHostname: deeper than one label under the apex is reserved (breaks the wildcard cert; nothing legitimate creates it)', () => {
|
|
75
|
+
assert.equal(provisioning.isReservedHostname('a.b.cloudbongos.com', 'cloudbongos.com'), true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ── the create route, over a real mounted router (stubbed data access) ───────────
|
|
79
|
+
let domainHolder = null; // what getInstanceByDomain returns
|
|
80
|
+
let existingRow = null; // what getInstanceBySlug returns (re-request tests)
|
|
81
|
+
let createCalls = []; // what createInstance was asked to insert
|
|
82
|
+
let createResult = null; // what createInstance reports
|
|
83
|
+
let enqueueCalls = [];
|
|
84
|
+
provisioning.getInstanceByDomain = async (_db, _d) => domainHolder;
|
|
85
|
+
provisioning.getInstanceBySlug = async (_db, _s) => existingRow;
|
|
86
|
+
provisioning.getInstanceById = async () => null;
|
|
87
|
+
provisioning.updateInstanceDomain = async (_db, id, domain) => ({ id, domain });
|
|
88
|
+
provisioning.createInstance = async (_db, args) => { createCalls.push(args); return createResult; };
|
|
89
|
+
provisioning.enqueueIntent = async (_db, id, action, requestedBy) => {
|
|
90
|
+
enqueueCalls.push({ id, action, requestedBy });
|
|
91
|
+
return { intent: { id: 9, action, state: 'pending' }, created: true };
|
|
92
|
+
};
|
|
93
|
+
provisioning.getOpenIntent = async () => null;
|
|
94
|
+
provisioning.recordEvent = async () => {};
|
|
95
|
+
|
|
96
|
+
const api = require('../src/module-api.js');
|
|
97
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 7, github_login: 'owner' }; next(); };
|
|
98
|
+
api.requirePermission = () => (_req, _res, next) => next();
|
|
99
|
+
let brandingDomains = { publicOrigin: 'https://cb.test', instanceBase: 'cloudbongos.com' };
|
|
100
|
+
api.branding = () => ({ domains: brandingDomains, identity: { productName: 'Cloud Bongos' } });
|
|
101
|
+
|
|
102
|
+
const express = require('express');
|
|
103
|
+
const provisioningRoutes = require('../modules/provisioning/routes/provisioning.js');
|
|
104
|
+
const app = express();
|
|
105
|
+
app.use(express.json());
|
|
106
|
+
app.use((_req, res, next) => {
|
|
107
|
+
res.fail = (code, opts, details) => {
|
|
108
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
109
|
+
res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}), ...(details || {}) } });
|
|
110
|
+
};
|
|
111
|
+
next();
|
|
112
|
+
});
|
|
113
|
+
app.use('/api/bongos', provisioningRoutes());
|
|
114
|
+
|
|
115
|
+
const server = app.listen(0);
|
|
116
|
+
await new Promise((r) => server.once('listening', r));
|
|
117
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
118
|
+
|
|
119
|
+
function post(path, body = {}) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const payload = JSON.stringify(body);
|
|
122
|
+
const req = http.request(`${base}${path}`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
125
|
+
}, (res) => {
|
|
126
|
+
let d = '';
|
|
127
|
+
res.on('data', (c) => { d += c; });
|
|
128
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
129
|
+
});
|
|
130
|
+
req.on('error', reject);
|
|
131
|
+
req.end(payload);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const freshRow = (over = {}) => ({
|
|
136
|
+
id: 41, owner_builder_id: 7, slug: 'spike', status: 'requested', hosting_shape: 'standalone',
|
|
137
|
+
domain: 'spike.cloudbongos.com', onboard_mode: 'greenfield', type: null, created_at: '', updated_at: '', ...over,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
function reset() {
|
|
141
|
+
domainHolder = null;
|
|
142
|
+
existingRow = null;
|
|
143
|
+
createCalls = [];
|
|
144
|
+
enqueueCalls = [];
|
|
145
|
+
createResult = { instance: freshRow(), created: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('\nPOST /provisioning/instances — auto-assign, opt-out, BYO wins:');
|
|
149
|
+
|
|
150
|
+
await ta('no domain in the body → the assigned <slug>.<instanceBase> is created (the API-first gap, N2)', async () => {
|
|
151
|
+
reset();
|
|
152
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
153
|
+
assert.equal(res.status, 201, res.raw);
|
|
154
|
+
assert.equal(createCalls[0].domain, 'spike.cloudbongos.com');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await ta('no_address: true → deliberately domainless (the opt-out survives the server default)', async () => {
|
|
158
|
+
reset();
|
|
159
|
+
createResult = { instance: freshRow({ domain: null }), created: true };
|
|
160
|
+
const res = await post('/provisioning/instances', { slug: 'spike', no_address: true });
|
|
161
|
+
assert.equal(res.status, 201, res.raw);
|
|
162
|
+
assert.equal(createCalls[0].domain == null, true, 'no domain may be assigned');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await ta('an explicit domain (bring-your-own) wins over the default, normalized', async () => {
|
|
166
|
+
reset();
|
|
167
|
+
createResult = { instance: freshRow({ domain: 'spike.example.com' }), created: true };
|
|
168
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'Spike.Example.com.' });
|
|
169
|
+
assert.equal(res.status, 201, res.raw);
|
|
170
|
+
assert.equal(createCalls[0].domain, 'spike.example.com');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await ta('a malformed explicit domain → 400 bad_domain, nothing created (create now matches attach)', async () => {
|
|
174
|
+
reset();
|
|
175
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'not a hostname' });
|
|
176
|
+
assert.equal(res.status, 400, res.raw);
|
|
177
|
+
assert.equal(res.body.error.code, 'bad_domain');
|
|
178
|
+
assert.equal(createCalls.length, 0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await ta("an address held by ANOTHER instance → 409 domain_taken (create had no uniqueness check)", async () => {
|
|
182
|
+
reset();
|
|
183
|
+
domainHolder = freshRow({ id: 99, slug: 'other' });
|
|
184
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
185
|
+
assert.equal(res.status, 409, res.raw);
|
|
186
|
+
assert.equal(res.body.error.code, 'domain_taken');
|
|
187
|
+
assert.equal(createCalls.length, 0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await ta('the holder being this slug’s own row (a re-request) is not a collision', async () => {
|
|
191
|
+
reset();
|
|
192
|
+
domainHolder = freshRow();
|
|
193
|
+
createResult = { instance: freshRow(), created: false };
|
|
194
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
195
|
+
assert.equal(res.status, 200, res.raw);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await ta('a BYO domain under the apex that is not the slug’s own address → 400 (squat on another handle)', async () => {
|
|
199
|
+
reset();
|
|
200
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'victim.cloudbongos.com' });
|
|
201
|
+
assert.equal(res.status, 400, res.raw);
|
|
202
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
203
|
+
assert.equal(createCalls.length, 0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await ta('an explicit domain equal to the slug’s own assigned address is fine (the wizard sends exactly this)', async () => {
|
|
207
|
+
reset();
|
|
208
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'spike.cloudbongos.com' });
|
|
209
|
+
assert.equal(res.status, 201, res.raw);
|
|
210
|
+
assert.equal(createCalls[0].domain, 'spike.cloudbongos.com');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await ta('a reserved label under the apex → 400 domain_reserved (auto-assign path)', async () => {
|
|
214
|
+
reset();
|
|
215
|
+
const res = await post('/provisioning/instances', { slug: 'www' });
|
|
216
|
+
assert.equal(res.status, 400, res.raw);
|
|
217
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
218
|
+
assert.equal(createCalls.length, 0);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await ta('a reserved hostname as BYO → 400 domain_reserved (same guard, explicit path)', async () => {
|
|
222
|
+
reset();
|
|
223
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'GET.cloudbongos.com' });
|
|
224
|
+
assert.equal(res.status, 400, res.raw);
|
|
225
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await ta('a reserved slug WITH no_address is fine — no hostname is being claimed', async () => {
|
|
229
|
+
reset();
|
|
230
|
+
createResult = { instance: freshRow({ slug: 'www', domain: null }), created: true };
|
|
231
|
+
const res = await post('/provisioning/instances', { slug: 'www', no_address: true });
|
|
232
|
+
assert.equal(res.status, 201, res.raw);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
await ta('blank instanceBase (self-host / unconfigured) → domainless create, no guessing', async () => {
|
|
236
|
+
reset();
|
|
237
|
+
brandingDomains = { publicOrigin: 'http://localhost:3000' };
|
|
238
|
+
createResult = { instance: freshRow({ domain: null }), created: true };
|
|
239
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
240
|
+
assert.equal(res.status, 201, res.raw);
|
|
241
|
+
assert.equal(createCalls[0].domain == null, true, 'no apex → nothing to assign');
|
|
242
|
+
brandingDomains = { publicOrigin: 'https://cb.test', instanceBase: 'cloudbongos.com' };
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
console.log('\nre-requests skip the birth-time guards (Try again / Bring it back must keep working):');
|
|
246
|
+
|
|
247
|
+
await ta('a re-request of an EXISTING reserved-label row → 200 with its provision intent (not domain_reserved)', async () => {
|
|
248
|
+
reset();
|
|
249
|
+
existingRow = freshRow({ slug: 'demo', domain: 'demo.cloudbongos.com', status: 'error' });
|
|
250
|
+
createResult = { instance: existingRow, created: false };
|
|
251
|
+
const res = await post('/provisioning/instances', { slug: 'demo', domain: 'demo.cloudbongos.com' });
|
|
252
|
+
assert.equal(res.status, 200, res.raw);
|
|
253
|
+
assert.deepEqual(enqueueCalls.map((c) => c.action), ['provision'], "the 'error' row re-enqueues its provision intent");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
await ta('a re-request is never 409ed against its own (or anyone’s) address — createInstance won’t write one', async () => {
|
|
257
|
+
reset();
|
|
258
|
+
existingRow = freshRow();
|
|
259
|
+
domainHolder = freshRow({ id: 99, slug: 'other' });
|
|
260
|
+
createResult = { instance: freshRow(), created: false };
|
|
261
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
262
|
+
assert.equal(res.status, 200, res.raw);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
console.log('\nre-request honesty:');
|
|
266
|
+
|
|
267
|
+
await ta('a re-request with NO explicit domain never reports domain_ignored (the derived default is not an ask)', async () => {
|
|
268
|
+
reset();
|
|
269
|
+
createResult = { instance: freshRow({ domain: 'custom.example.com' }), created: false };
|
|
270
|
+
const res = await post('/provisioning/instances', { slug: 'spike' });
|
|
271
|
+
assert.equal(res.status, 200, res.raw);
|
|
272
|
+
assert.equal(res.body.domain_ignored, undefined);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
await ta('a re-request with an explicit different domain still reports domain_ignored', async () => {
|
|
276
|
+
reset();
|
|
277
|
+
createResult = { instance: freshRow({ domain: 'custom.example.com' }), created: false };
|
|
278
|
+
const res = await post('/provisioning/instances', { slug: 'spike', domain: 'spike.other.org' });
|
|
279
|
+
assert.equal(res.status, 200, res.raw);
|
|
280
|
+
assert.equal(res.body.domain_ignored.requested, 'spike.other.org');
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
await ta("a re-request with no_address on an addressed instance says so (no_address_ignored)", async () => {
|
|
284
|
+
reset();
|
|
285
|
+
domainHolder = freshRow();
|
|
286
|
+
createResult = { instance: freshRow(), created: false };
|
|
287
|
+
const res = await post('/provisioning/instances', { slug: 'spike', no_address: true });
|
|
288
|
+
assert.equal(res.status, 200, res.raw);
|
|
289
|
+
assert.equal(res.body.no_address_ignored.current, 'spike.cloudbongos.com');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
console.log('\nPOST /provisioning/instances/:id/domain — the reserved guard applies to attach too:');
|
|
293
|
+
|
|
294
|
+
await ta('attaching a reserved hostname under the apex → 400 domain_reserved', async () => {
|
|
295
|
+
reset();
|
|
296
|
+
provisioning.getInstanceById = async (_db, id) => freshRow({ id, domain: null, status: 'active' });
|
|
297
|
+
const res = await post('/provisioning/instances/41/domain', { domain: 'www.cloudbongos.com' });
|
|
298
|
+
assert.equal(res.status, 400, res.raw);
|
|
299
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await ta('attaching another handle’s address under the apex → 400 (squat, same rule as create)', async () => {
|
|
303
|
+
reset();
|
|
304
|
+
provisioning.getInstanceById = async (_db, id) => freshRow({ id, domain: null, status: 'active' });
|
|
305
|
+
const res = await post('/provisioning/instances/41/domain', { domain: 'victim.cloudbongos.com' });
|
|
306
|
+
assert.equal(res.status, 400, res.raw);
|
|
307
|
+
assert.equal(res.body.error.code, 'domain_reserved');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
console.log('\nthe assigned default may be upgraded to a bring-your-own domain (the decision’s upgrade path):');
|
|
311
|
+
|
|
312
|
+
await ta('re-pointing AWAY from the platform-assigned address → recorded + domain-attach intent', async () => {
|
|
313
|
+
reset();
|
|
314
|
+
provisioning.getInstanceById = async (_db, id) => freshRow({ id, domain: 'spike.cloudbongos.com', status: 'active' });
|
|
315
|
+
const res = await post('/provisioning/instances/41/domain', { domain: 'spike.example.com' });
|
|
316
|
+
assert.equal(res.status, 200, res.raw);
|
|
317
|
+
assert.equal(res.body.domain, 'spike.example.com');
|
|
318
|
+
assert.deepEqual(enqueueCalls.map((c) => c.action), ['domain-attach']);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await ta('a genuine bring-your-own address still refuses to re-point (unchanged)', async () => {
|
|
322
|
+
reset();
|
|
323
|
+
provisioning.getInstanceById = async (_db, id) => freshRow({ id, domain: 'spike.example.com', status: 'active' });
|
|
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');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
server.close();
|
|
330
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
331
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -91,6 +91,7 @@ provisioning.enqueueIntent = async (_db, id, action, requestedBy) => {
|
|
|
91
91
|
};
|
|
92
92
|
provisioning.getOpenIntent = async () => null;
|
|
93
93
|
provisioning.createInstance = async () => createResult;
|
|
94
|
+
provisioning.getInstanceBySlug = async () => null; // the task-1002704 birth-time pre-check
|
|
94
95
|
provisioning.recordEvent = async () => {};
|
|
95
96
|
provisioning.createOAuthManifest = async (_db, row) => { manifestRows.push(row); return { id: 1, ...row }; };
|
|
96
97
|
provisioning.getLatestOAuthManifestForInstance = async () => null;
|
|
@@ -148,6 +148,7 @@ provisioning.enqueueIntent = async (_db, id, action, requestedBy) => {
|
|
|
148
148
|
};
|
|
149
149
|
provisioning.getOpenIntent = async () => null;
|
|
150
150
|
provisioning.createInstance = async () => createResult;
|
|
151
|
+
provisioning.getInstanceBySlug = async () => null; // the task-1002704 birth-time pre-check
|
|
151
152
|
provisioning.recordEvent = async () => {};
|
|
152
153
|
|
|
153
154
|
const api = require('../src/module-api.js');
|