@bongos/core 1.19.826 → 1.19.827
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 +26 -16
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +17 -0
- package/docs/module-api-changelog.md +2 -0
- package/modules/platform-identity/routes/my-projects.js +27 -1
- package/modules/provisioning/module.json +1 -1
- package/modules/provisioning/routes/provisioning.js +2 -22
- package/modules/provisioning/seams.js +54 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/join_dark_matter.mjs +208 -0
- package/tests/projects_hub.mjs +2 -1
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.827",
|
|
6
|
+
"core_contract": "1.19.827",
|
|
7
|
+
"source_commit": "4fe673594c63823c15cf02108dde24ac25c40757",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-17T13:
|
|
9
|
+
"built_at": "2026-09-17T13:57:26.543Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 491,
|
|
13
13
|
"agent_docs_stubbed": 26,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2236,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2753,
|
|
20
|
+
"tree_sha256": "a39afbe8aa551c281c44ae315b31b984c38436ff1e4f632f3a70c67c30d6bb15",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -1722,7 +1722,7 @@
|
|
|
1722
1722
|
{
|
|
1723
1723
|
"path": "docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md",
|
|
1724
1724
|
"mode": "0000644",
|
|
1725
|
-
"sha256": "
|
|
1725
|
+
"sha256": "b7146a1b5f563657371935ad6e71013b9ce89a13f814300efc8ea1d2b815f0f3"
|
|
1726
1726
|
},
|
|
1727
1727
|
{
|
|
1728
1728
|
"path": "docs/adr/0248-a-hub-project-s-owner-is-the-provisioning-row-carried-on-a-port.md",
|
|
@@ -2552,7 +2552,7 @@
|
|
|
2552
2552
|
{
|
|
2553
2553
|
"path": "docs/module-api-changelog.md",
|
|
2554
2554
|
"mode": "0000644",
|
|
2555
|
-
"sha256": "
|
|
2555
|
+
"sha256": "dc603258b3556e3044710de8bf32a074d12865b74f0e25b93a9944720771962a"
|
|
2556
2556
|
},
|
|
2557
2557
|
{
|
|
2558
2558
|
"path": "docs/modules-contract.md",
|
|
@@ -6257,7 +6257,7 @@
|
|
|
6257
6257
|
{
|
|
6258
6258
|
"path": "modules/platform-identity/routes/my-projects.js",
|
|
6259
6259
|
"mode": "0000644",
|
|
6260
|
-
"sha256": "
|
|
6260
|
+
"sha256": "c920f13eea6bdfc2f361d7c23500d295bb87066205ee2f4792adc22fc5dfa715"
|
|
6261
6261
|
},
|
|
6262
6262
|
{
|
|
6263
6263
|
"path": "modules/platform-identity/routes/profile.js",
|
|
@@ -6442,7 +6442,7 @@
|
|
|
6442
6442
|
{
|
|
6443
6443
|
"path": "modules/provisioning/module.json",
|
|
6444
6444
|
"mode": "0000644",
|
|
6445
|
-
"sha256": "
|
|
6445
|
+
"sha256": "b3ec65266823f6f50cdfaf839f579411b68b91e2fe5652a6e7736d8a3024c5ca"
|
|
6446
6446
|
},
|
|
6447
6447
|
{
|
|
6448
6448
|
"path": "modules/provisioning/onboard-plan.js",
|
|
@@ -6502,7 +6502,12 @@
|
|
|
6502
6502
|
{
|
|
6503
6503
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
6504
6504
|
"mode": "0000644",
|
|
6505
|
-
"sha256": "
|
|
6505
|
+
"sha256": "e9eb66469fc14adc526f97815ef61f4145cf17bbf36bdb638a2ed4db897149b7"
|
|
6506
|
+
},
|
|
6507
|
+
{
|
|
6508
|
+
"path": "modules/provisioning/seams.js",
|
|
6509
|
+
"mode": "0000644",
|
|
6510
|
+
"sha256": "68ba324773bbd9a347ef682c5b1f105b447c6760f24595b8850590ed6d64d78e"
|
|
6506
6511
|
},
|
|
6507
6512
|
{
|
|
6508
6513
|
"path": "modules/provisioning/starter-bundles.js",
|
|
@@ -7817,12 +7822,12 @@
|
|
|
7817
7822
|
{
|
|
7818
7823
|
"path": "package-lock.json",
|
|
7819
7824
|
"mode": "0000644",
|
|
7820
|
-
"sha256": "
|
|
7825
|
+
"sha256": "15fdc8e49c1219b3d4f3ebca13c55e530d02b9ad469163a82749d7999d37314d"
|
|
7821
7826
|
},
|
|
7822
7827
|
{
|
|
7823
7828
|
"path": "package.json",
|
|
7824
7829
|
"mode": "0000644",
|
|
7825
|
-
"sha256": "
|
|
7830
|
+
"sha256": "c7e6e06cc67ddfd968ac745b125971e1097dfe56b4e7e428c6331ffdc39de325"
|
|
7826
7831
|
},
|
|
7827
7832
|
{
|
|
7828
7833
|
"path": "public-docs/index.html",
|
|
@@ -9742,7 +9747,7 @@
|
|
|
9742
9747
|
{
|
|
9743
9748
|
"path": "src/module-api.js",
|
|
9744
9749
|
"mode": "0000644",
|
|
9745
|
-
"sha256": "
|
|
9750
|
+
"sha256": "e674350e5eca098fd473e43be69fc725f8ec211cb13d5541bfd6c70c6d5133c3"
|
|
9746
9751
|
},
|
|
9747
9752
|
{
|
|
9748
9753
|
"path": "src/module-loader/catalog.js",
|
|
@@ -11829,6 +11834,11 @@
|
|
|
11829
11834
|
"mode": "0000644",
|
|
11830
11835
|
"sha256": "2cdb7e9ecd4652e2c09e4809e465943709aa9f7fbeda53fdff5cdbd0c50aa653"
|
|
11831
11836
|
},
|
|
11837
|
+
{
|
|
11838
|
+
"path": "tests/join_dark_matter.mjs",
|
|
11839
|
+
"mode": "0000644",
|
|
11840
|
+
"sha256": "5f736541695c8786b2579c5c08d037479b40dc7096718b1ab5f36499b65c5898"
|
|
11841
|
+
},
|
|
11832
11842
|
{
|
|
11833
11843
|
"path": "tests/join_relay_outcome.mjs",
|
|
11834
11844
|
"mode": "0000644",
|
|
@@ -12492,7 +12502,7 @@
|
|
|
12492
12502
|
{
|
|
12493
12503
|
"path": "tests/projects_hub.mjs",
|
|
12494
12504
|
"mode": "0000644",
|
|
12495
|
-
"sha256": "
|
|
12505
|
+
"sha256": "8898b1a4458b0d76c071c901f89eacc21fb64cbfed37614afad5bdea16bef4b3"
|
|
12496
12506
|
},
|
|
12497
12507
|
{
|
|
12498
12508
|
"path": "tests/projects_hub_module_picker.mjs",
|
|
@@ -200,3 +200,20 @@ now, with the code, rather than when the verdict is wired.
|
|
|
200
200
|
- **Making the composed door fail to `locked` when the pack is unreadable.** Rejected: it
|
|
201
201
|
contradicts ADR 0194 D2 for no safety gain, and it tells an applicant a specific untruth
|
|
202
202
|
about the project's publication state.
|
|
203
|
+
|
|
204
|
+
## Amendment — 2026-09-17 (task 1003578): the verdict is wired at the hub relay, from the provisioning row
|
|
205
|
+
|
|
206
|
+
Decision 4's filed follow-up, built the way it named. `modules/provisioning/seams.js` provides
|
|
207
|
+
`provisioning.<redacted>` (`{ slug, host }` → `{ name, description }` or `null`),
|
|
208
|
+
keyed on both halves of the ADR 0246 projection like `provisioning.projectOwnedBy`. The hub's
|
|
209
|
+
`POST /my-projects/join` asks it before relaying and judges the answer with its own
|
|
210
|
+
`projectPublishability`, so the mapping stays in provisioning and the rule stays in the hub.
|
|
211
|
+
A positive `publishable: false` answers `not_published` with no relay and no echo.
|
|
212
|
+
|
|
213
|
+
Everything else is still **no verdict**, per Decision 4's tri-state: no port, no provisioning
|
|
214
|
+
row (a self-hosted or hand-seeded project, whose hub copy remains a cache) or a failed read.
|
|
215
|
+
The request is relayed as before, because relaying grants nothing and a refusal on a guess
|
|
216
|
+
would be an untruth about someone else's project. The instance's own door is unchanged: it
|
|
217
|
+
still passes no verdict, so an applicant arriving at the project directly is answered on the
|
|
218
|
+
stored knobs alone. The rejected alternative above still stands. This reads the provisioning
|
|
219
|
+
row, not `platform_identity_projects`.
|
|
@@ -2126,5 +2126,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2126
2126
|
landed since 1.19.824 with no explicit bump. run 35227529838. (task 1002620)
|
|
2127
2127
|
1.19.826 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2128
2128
|
landed since 1.19.825 with no explicit bump. run 35228618989. (task 1002620)
|
|
2129
|
+
1.19.827 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2130
|
+
landed since 1.19.826 with no explicit bump. run 35230339528. (task 1002620)
|
|
2129
2131
|
---------------------------------------------------------------------------
|
|
2130
2132
|
```
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// asked to join and when (ADR 0201 D1.3)
|
|
7
7
|
// POST /my-projects/join — apply to a project: one click plus an optional
|
|
8
8
|
// note in the applicant's own words (task 1002330).
|
|
9
|
-
// Relayed; the project executes and decides
|
|
9
|
+
// Relayed; the project executes and decides —
|
|
10
|
+
// unless the platform's record says it is unpublished.
|
|
10
11
|
// POST /my-projects/:clientId/leave — self-service offboard (reflected to the hub)
|
|
11
12
|
//
|
|
12
13
|
// All gated on a live HUB session (requireBuilder). Authority stays LOCAL: JOIN only
|
|
@@ -24,9 +25,30 @@ const api = require('../../../src/module-api');
|
|
|
24
25
|
const pi = require('../platform-identity');
|
|
25
26
|
const projectStats = require('../project-stats');
|
|
26
27
|
const applicationEcho = require('../application-echo');
|
|
28
|
+
const { projectPublishability } = require('../publish-gate');
|
|
27
29
|
|
|
28
30
|
const { pool, requireBuilder, validateOrRespond, userAgent } = api;
|
|
29
31
|
|
|
32
|
+
// Dark matter, refused before the relay (task 1003578, ADR 0247 D4). The project's own
|
|
33
|
+
// door cannot lock itself — its instance never holds its description — so the hub asks the
|
|
34
|
+
// provisioning row, the one live copy, through a port resolved by name (ADR 0083; optional,
|
|
35
|
+
// the `provisioning.projectOwnedBy` precedent). Only a POSITIVE verdict refuses. No port, no
|
|
36
|
+
// row (a self-hosted or hand-seeded project) or a failed read is no verdict, and the request
|
|
37
|
+
// is relayed as before: relaying grants nothing, and refusing on a guess would tell an
|
|
38
|
+
// applicant something untrue about someone else's project.
|
|
39
|
+
const PUBLISH_IDENTITY_PORT = 'provisioning.projectPublishIdentity';
|
|
40
|
+
async function isDarkMatter(client) {
|
|
41
|
+
const identityOf = api.resolveOptional(PUBLISH_IDENTITY_PORT);
|
|
42
|
+
if (typeof identityOf !== 'function') return false;
|
|
43
|
+
try {
|
|
44
|
+
const identity = await identityOf({ slug: client.client_id, host: new URL(client.origin).hostname });
|
|
45
|
+
return !!identity && projectPublishability(identity).publishable === false;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
api.logger('platform-identity').warn(`publish verdict read failed, relaying (non-fatal): ${e && e.message}`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
30
52
|
// joinRelayOutcome — what the project's own admission surface answered, said in
|
|
31
53
|
// the HUB's words (task 1003044). The project decides; the hub relays and must
|
|
32
54
|
// never paint a refusal as "requested" — nor as "unreachable". A 2xx is a filed
|
|
@@ -172,6 +194,10 @@ module.exports = function myProjectsRoutes() {
|
|
|
172
194
|
if (!client || client.status !== 'active' || !client.origin) {
|
|
173
195
|
return res.fail('unknown_project', 404);
|
|
174
196
|
}
|
|
197
|
+
if (await isDarkMatter(client)) {
|
|
198
|
+
return res.json({ ok: true, status: 'not_published', project: { name: client.name, origin: client.origin },
|
|
199
|
+
message: RELAY_SENTENCE.not_published });
|
|
200
|
+
}
|
|
175
201
|
// Relay an access request to the project's OWN (public) admission endpoint —
|
|
176
202
|
// the project decides (open-join, or request/invite per ADR 0112). We post to
|
|
177
203
|
// the registry-trusted origin, never the caller's raw input.
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
"migrations": true,
|
|
15
15
|
"pollers": ["liveness-sweep", "catalog-backfill"]
|
|
16
16
|
},
|
|
17
|
-
"provides": ["provisioning.instanceState", "provisioning.projectPlanets", "provisioning.projectOwnedBy"],
|
|
17
|
+
"provides": ["provisioning.instanceState", "provisioning.projectPlanets", "provisioning.projectOwnedBy", "provisioning.projectPublishIdentity"],
|
|
18
18
|
"consumes": []
|
|
19
19
|
}
|
|
@@ -58,6 +58,8 @@ const capacity = require('../capacity'); // the box's slot ceiling (ADR 0285,
|
|
|
58
58
|
const { callbackPage } = require('./callback-page');
|
|
59
59
|
// the LOUD body validators (task 1003504) — lifted out when this file hit the size ratchet
|
|
60
60
|
const { badProjectDetail, badModuleSelection, badProjectType } = require('./body-validators');
|
|
61
|
+
// the kernel ports this module provides (moved out at the size ratchet, task 1003578)
|
|
62
|
+
const { registerProvisioningSeams } = require('../seams');
|
|
61
63
|
// task 1003208: structured logging (pino via the doorway) — was console.*.
|
|
62
64
|
const log = api.logger('provisioning');
|
|
63
65
|
|
|
@@ -110,28 +112,6 @@ function ownerFromTargetRef(ref) {
|
|
|
110
112
|
return m ? m[1] : null;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
// Register this module's kernel seam (ADR 0083 / the registerBoxSeams precedent).
|
|
114
|
-
// Idempotent — the loader calls this factory once at boot, but tests may
|
|
115
|
-
// instantiate the router repeatedly; gating on hasProvider keeps the port
|
|
116
|
-
// single-registered.
|
|
117
|
-
// • PROVIDES port `provisioning.instanceState` (slug) → { status, … } | null.
|
|
118
|
-
// Lets other code read an instance's provisioning status via
|
|
119
|
-
// resolveOptional without importing this module (box.hasEverConnected pattern).
|
|
120
|
-
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
121
|
-
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
122
|
-
// public featured feed can carry them without importing this module.
|
|
123
|
-
// • PROVIDES port `provisioning.projectOwnedBy` ({ slug, host, builderId }) → boolean —
|
|
124
|
-
// the OWNERSHIP fact behind a hub catalog row (task 1003580), so a hub surface can
|
|
125
|
-
// own-scope a project without importing this module and without treating the hub's
|
|
126
|
-
// `owner_github_id` columns as authority (ADR 0238). See projectOwnedBy for why the
|
|
127
|
-
// question names both halves of the ADR 0246 projection.
|
|
128
|
-
function registerProvisioningSeams() {
|
|
129
|
-
if (api.hasProvider('provisioning.instanceState')) return;
|
|
130
|
-
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
131
|
-
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
132
|
-
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
115
|
// Is this project on the public map, and if not, what is it still missing?
|
|
136
116
|
// (R16, task 1002335 — the surface R15 pointed at by name.)
|
|
137
117
|
//
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// modules/provisioning/seams.js — the kernel ports this module provides (ADR 0083 / the
|
|
2
|
+
// registerBoxSeams precedent). Moved out of routes/provisioning.js under task 1003578,
|
|
3
|
+
// because that file sits at the 1500-line fitness ratchet and a new port needed a line.
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const api = require('../../src/module-api');
|
|
7
|
+
const { pool } = api;
|
|
8
|
+
const provisioning = require('./provisioning');
|
|
9
|
+
|
|
10
|
+
// The `provisioning.projectPublishIdentity` port (task 1003578, ADR 0247 D4): the name and
|
|
11
|
+
// description the publish gate judges, for the control-plane project a hub catalog row was
|
|
12
|
+
// projected from. This row is the one LIVE copy of a control-plane project's description;
|
|
13
|
+
// the hub's catalog column is a cache (ADR 0182 D5), so the hub's join relay asks here.
|
|
14
|
+
//
|
|
15
|
+
// Keyed on BOTH halves of the ADR 0246 projection (`client_id = slug` at `https://<domain>`),
|
|
16
|
+
// the `projectOwnedBy` rule: a hand-seeded catalog row's name is squattable in this table,
|
|
17
|
+
// and a stranger's instance must not answer for it. The slug IS the name on this plane, so
|
|
18
|
+
// the mapping stays here and the rule stays in the hub. `null` = no row = NO verdict.
|
|
19
|
+
async function projectPublishIdentity(db, { slug, host } = {}) {
|
|
20
|
+
if (typeof slug !== 'string' || !slug) return null;
|
|
21
|
+
const { rows } = await db.query(
|
|
22
|
+
`SELECT slug, description FROM provisioning_instances
|
|
23
|
+
WHERE slug = $1 AND lower(domain) = $2
|
|
24
|
+
LIMIT 1`,
|
|
25
|
+
[slug, typeof host === 'string' ? host.toLowerCase() : ''],
|
|
26
|
+
);
|
|
27
|
+
return rows[0] ? { name: rows[0].slug, description: rows[0].description } : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Idempotent — the loader calls the routes factory once at boot, but tests may
|
|
31
|
+
// instantiate the router repeatedly; gating on hasProvider keeps each port
|
|
32
|
+
// single-registered.
|
|
33
|
+
// • PROVIDES port `provisioning.instanceState` (slug) → { status, … } | null.
|
|
34
|
+
// Lets other code read an instance's provisioning status via
|
|
35
|
+
// resolveOptional without importing this module (box.hasEverConnected pattern).
|
|
36
|
+
// • PROVIDES port `provisioning.projectPlanets` (hosts[]) → { host: { planet_template,
|
|
37
|
+
// planet_accent } } — the owners' planet choices (task 1003347), so the hub's
|
|
38
|
+
// public featured feed can carry them without importing this module.
|
|
39
|
+
// • PROVIDES port `provisioning.projectOwnedBy` ({ slug, host, builderId }) → boolean —
|
|
40
|
+
// the OWNERSHIP fact behind a hub catalog row (task 1003580), so a hub surface can
|
|
41
|
+
// own-scope a project without importing this module and without treating the hub's
|
|
42
|
+
// `owner_github_id` columns as authority (ADR 0238). See projectOwnedBy for why the
|
|
43
|
+
// question names both halves of the ADR 0246 projection.
|
|
44
|
+
// • PROVIDES port `provisioning.projectPublishIdentity` ({ slug, host }) → { name,
|
|
45
|
+
// description } | null — see projectPublishIdentity above.
|
|
46
|
+
function registerProvisioningSeams() {
|
|
47
|
+
if (api.hasProvider('provisioning.instanceState')) return;
|
|
48
|
+
api.registerProvider('provisioning.instanceState', (slug) => provisioning.instanceStateForSlug(pool, slug));
|
|
49
|
+
api.registerProvider('provisioning.projectPlanets', (hosts) => provisioning.planetChoicesForHosts(pool, hosts));
|
|
50
|
+
api.registerProvider('provisioning.projectOwnedBy', (target) => provisioning.projectOwnedBy(pool, target));
|
|
51
|
+
api.registerProvider('provisioning.projectPublishIdentity', (target) => projectPublishIdentity(pool, target));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { registerProvisioningSeams, projectPublishIdentity };
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.827",
|
|
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.827",
|
|
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.827",
|
|
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.
|
|
74
|
+
const CORE_VERSION = '1.19.827'; // 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,208 @@
|
|
|
1
|
+
// tests/join_dark_matter.mjs — the hub's join relay refuses a project the platform's own
|
|
2
|
+
// record says is unpublished, before it relays anything (task 1003578, ADR 0247 D4).
|
|
3
|
+
//
|
|
4
|
+
// TWO HALVES.
|
|
5
|
+
//
|
|
6
|
+
// Part A — the provider: `provisioning.projectPublishIdentity` reads the provisioning row
|
|
7
|
+
// (the one LIVE copy of a control-plane project's description) keyed on BOTH halves of the
|
|
8
|
+
// ADR 0246 projection, and maps slug → name so the hub's gate never learns this schema.
|
|
9
|
+
//
|
|
10
|
+
// Part B — the real POST /my-projects/join with the port scripted. Only a positive
|
|
11
|
+
// "unpublished" verdict refuses. No port, no row (a self-hosted or hand-seeded project),
|
|
12
|
+
// or a failed read is NO verdict, and the request is relayed exactly as before — the
|
|
13
|
+
// project's own door still decides, and the hub grants nothing by relaying.
|
|
14
|
+
//
|
|
15
|
+
// DB-free. Run: node --test tests/join_dark_matter.mjs
|
|
16
|
+
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test, before, after } from 'node:test';
|
|
19
|
+
import http from 'node:http';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import { readFileSync } from 'node:fs';
|
|
22
|
+
|
|
23
|
+
// ESM import so the knip ratchet credits the exports this file consumes
|
|
24
|
+
// (a createRequire() import does not credit a CJS export).
|
|
25
|
+
import { projectPublishIdentity, registerProvisioningSeams } from '../modules/provisioning/seams.js';
|
|
26
|
+
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const express = require('express');
|
|
29
|
+
|
|
30
|
+
const PORT_NAME = 'provisioning.projectPublishIdentity';
|
|
31
|
+
|
|
32
|
+
// ── the doorway, with the port scripted ──────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
let portMode = 'present'; // 'present' | 'absent' | 'throws'
|
|
35
|
+
let identity = null; // what the port answers
|
|
36
|
+
let portCalls = [];
|
|
37
|
+
let fetches = [];
|
|
38
|
+
let echoWrites = 0;
|
|
39
|
+
|
|
40
|
+
const pool = {
|
|
41
|
+
query: async (text, params = []) => {
|
|
42
|
+
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
43
|
+
if (/FROM platform_identity_sso_clients WHERE origin = \$1 AND status = 'active'/.test(sql)) {
|
|
44
|
+
return { rows: params[0] === 'https://Target.example'
|
|
45
|
+
? [{ client_id: 'proj-open', name: 'Target', origin: 'https://Target.example', status: 'active' }]
|
|
46
|
+
: [] };
|
|
47
|
+
}
|
|
48
|
+
if (/^INSERT INTO platform_identity_application_echoes/.test(sql)) { echoWrites++; return { rows: [] }; }
|
|
49
|
+
throw new Error(`fake pool: unsupported statement — ${sql}`);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const apiPath = require.resolve('../src/module-api.js');
|
|
54
|
+
const realApi = require(apiPath);
|
|
55
|
+
require.cache[apiPath].exports = {
|
|
56
|
+
...realApi,
|
|
57
|
+
pool,
|
|
58
|
+
logger: () => ({ error() {}, warn() {}, info() {}, debug() {} }),
|
|
59
|
+
requireBuilder: (req, _res, next) => {
|
|
60
|
+
req.builder = { id: 1, github_id: 4242, github_login: 'octo', display_name: 'Octo' };
|
|
61
|
+
next();
|
|
62
|
+
},
|
|
63
|
+
resolveOptional: (name, fallback = null) => {
|
|
64
|
+
if (name !== PORT_NAME) return realApi.resolveOptional(name, fallback);
|
|
65
|
+
if (portMode === 'absent') return fallback;
|
|
66
|
+
return async (target) => {
|
|
67
|
+
portCalls.push(target);
|
|
68
|
+
if (portMode === 'throws') throw new Error('provisioning is down');
|
|
69
|
+
return identity;
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const statsPath = require.resolve('../modules/platform-identity/project-stats.js');
|
|
75
|
+
require.cache[statsPath] = {
|
|
76
|
+
id: statsPath, filename: statsPath, loaded: true,
|
|
77
|
+
exports: { getProjectStats: async () => ({}) },
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const myProjectsRoutes = require('../modules/platform-identity/routes/my-projects.js');
|
|
81
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
82
|
+
|
|
83
|
+
const realFetch = globalThis.fetch;
|
|
84
|
+
globalThis.fetch = async (url) => {
|
|
85
|
+
fetches.push(String(url));
|
|
86
|
+
return { status: 201, json: async () => ({ ok: true }) };
|
|
87
|
+
};
|
|
88
|
+
after(() => { globalThis.fetch = realFetch; });
|
|
89
|
+
|
|
90
|
+
const app = express();
|
|
91
|
+
app.use(express.json());
|
|
92
|
+
app.use(attachFail);
|
|
93
|
+
app.use('/api/bongos', myProjectsRoutes());
|
|
94
|
+
|
|
95
|
+
let server, port;
|
|
96
|
+
before(async () => {
|
|
97
|
+
server = http.createServer(app);
|
|
98
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
99
|
+
port = server.address().port;
|
|
100
|
+
});
|
|
101
|
+
after(async () => { await new Promise((r) => server.close(r)); });
|
|
102
|
+
|
|
103
|
+
function join(origin = 'https://Target.example') {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const payload = Buffer.from(JSON.stringify({ origin }));
|
|
106
|
+
const req = http.request({ host: '127.0.0.1', port, method: 'POST', path: '/api/bongos/my-projects/join',
|
|
107
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': payload.length } }, (res) => {
|
|
108
|
+
let data = '';
|
|
109
|
+
res.on('data', (c) => { data += c; });
|
|
110
|
+
res.on('end', () => resolve({ status: res.statusCode, body: data ? JSON.parse(data) : null }));
|
|
111
|
+
});
|
|
112
|
+
req.on('error', reject);
|
|
113
|
+
req.end(payload);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reset(mode, answer) {
|
|
118
|
+
portMode = mode; identity = answer; portCalls = []; fetches = []; echoWrites = 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Part A — the provider ────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function recorder(rows = []) {
|
|
124
|
+
const calls = [];
|
|
125
|
+
return { calls, query: async (sql, params) => { calls.push({ sql: String(sql).replace(/\s+/g, ' ').trim(), params }); return { rows }; } };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test('projectPublishIdentity asks the provisioning row for BOTH halves of the projection', async () => {
|
|
129
|
+
const db = recorder([{ slug: 'spike', description: 'A harbour of builders' }]);
|
|
130
|
+
const got = await projectPublishIdentity(db, { slug: 'spike', host: 'Spike.Example.com' });
|
|
131
|
+
assert.deepEqual(got, { name: 'spike', description: 'A harbour of builders' },
|
|
132
|
+
'the slug IS the name on this plane — the mapping stays here, the rule stays in the hub');
|
|
133
|
+
const [{ sql, params }] = db.calls;
|
|
134
|
+
assert.match(sql, /FROM provisioning_instances WHERE slug = \$1 AND lower\(domain\) = \$2/);
|
|
135
|
+
assert.deepEqual(params, ['spike', 'spike.example.com']);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('projectPublishIdentity answers null — no verdict — when no row carries both halves', async () => {
|
|
139
|
+
assert.equal(await projectPublishIdentity(recorder([]), { slug: 'spike', host: 'h.test' }), null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('projectPublishIdentity never queries for a target that names no project', async () => {
|
|
143
|
+
for (const target of [{}, { slug: '', host: 'h.test' }, { slug: ['spike'], host: 'h.test' }, undefined]) {
|
|
144
|
+
const db = recorder([{ slug: 'spike', description: 'x' }]);
|
|
145
|
+
assert.equal(await projectPublishIdentity(db, target), null, JSON.stringify(target));
|
|
146
|
+
assert.equal(db.calls.length, 0);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('the port is registered once, declared by provisioning, and not required by the hub', () => {
|
|
151
|
+
registerProvisioningSeams();
|
|
152
|
+
registerProvisioningSeams();
|
|
153
|
+
assert.equal(typeof realApi.resolveOptional(PORT_NAME), 'function');
|
|
154
|
+
const read = (rel) => JSON.parse(readFileSync(new URL(rel, import.meta.url), 'utf8'));
|
|
155
|
+
assert.ok(read('../modules/provisioning/module.json').provides.includes(PORT_NAME), 'declared');
|
|
156
|
+
assert.ok(!(read('../modules/platform-identity/module.json').consumes || []).includes(PORT_NAME),
|
|
157
|
+
'optional — a hub with no provisioning module must still boot and relay');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// ── Part B — the relay ───────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
test('an unpublished project is refused at the hub: not_published, no relay, no echo', async () => {
|
|
163
|
+
reset('present', { name: 'proj-open', description: null });
|
|
164
|
+
const r = await join();
|
|
165
|
+
assert.equal(r.status, 200);
|
|
166
|
+
assert.equal(r.body.status, 'not_published');
|
|
167
|
+
assert.match(r.body.message, /isn’t published yet/);
|
|
168
|
+
assert.deepEqual(r.body.project, { name: 'Target', origin: 'https://Target.example' });
|
|
169
|
+
assert.deepEqual(fetches, [], 'the request never reaches a project with no door');
|
|
170
|
+
assert.equal(echoWrites, 0, 'a refusal is not an application');
|
|
171
|
+
assert.deepEqual(portCalls, [{ slug: 'proj-open', host: 'target.example' }],
|
|
172
|
+
'asked about the client row the relay would have used: client_id + its origin host');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('a blank description is dark matter too — the gate trims, so the relay does', async () => {
|
|
176
|
+
reset('present', { name: 'proj-open', description: ' ' });
|
|
177
|
+
const r = await join();
|
|
178
|
+
assert.equal(r.body.status, 'not_published');
|
|
179
|
+
assert.deepEqual(fetches, []);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('a published project is relayed exactly as before', async () => {
|
|
183
|
+
reset('present', { name: 'proj-open', description: 'A harbour of builders' });
|
|
184
|
+
const r = await join();
|
|
185
|
+
assert.equal(r.body.status, 'requested');
|
|
186
|
+
assert.equal(fetches.length, 1);
|
|
187
|
+
assert.equal(echoWrites, 1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
for (const [label, mode, answer] of [
|
|
191
|
+
['no provisioning module (no port)', 'absent', null],
|
|
192
|
+
['no provisioning row (self-hosted or hand-seeded)', 'present', null],
|
|
193
|
+
['a verdict read that fails', 'throws', null],
|
|
194
|
+
]) {
|
|
195
|
+
test(`${label} is NO verdict: the request is relayed and the project decides`, async () => {
|
|
196
|
+
reset(mode, answer);
|
|
197
|
+
const r = await join();
|
|
198
|
+
assert.equal(r.body.status, 'requested');
|
|
199
|
+
assert.equal(fetches.length, 1, 'never refused on a guess');
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
test('an unknown project is still a 404 and the port is never asked', async () => {
|
|
204
|
+
reset('present', { name: 'x', description: null });
|
|
205
|
+
const r = await join('https://nowhere.example');
|
|
206
|
+
assert.equal(r.status, 404);
|
|
207
|
+
assert.deepEqual(portCalls, []);
|
|
208
|
+
});
|
package/tests/projects_hub.mjs
CHANGED
|
@@ -1950,7 +1950,8 @@ test('the feed carries the owner\'s choice through a seam: platform-identity res
|
|
|
1950
1950
|
assert.ok(manifest.provides.includes('provisioning.projectPlanets'), 'the port is declared');
|
|
1951
1951
|
const pi = JSON.parse(fs.readFileSync(path.join(ROOT, 'modules', 'platform-identity', 'module.json'), 'utf8'));
|
|
1952
1952
|
assert.ok(!(pi.consumes || []).includes('provisioning.projectPlanets'), 'not a required capability — the hub must boot without provisioning');
|
|
1953
|
-
|
|
1953
|
+
// the registrations moved to seams.js at the routes file's size ratchet (task 1003578)
|
|
1954
|
+
const prov = fs.readFileSync(path.join(ROOT, 'modules', 'provisioning', 'seams.js'), 'utf8');
|
|
1954
1955
|
assert.match(prov, /api\.registerProvider\('provisioning\.projectPlanets', \(hosts\) => provisioning\.planetChoicesForHosts\(pool, hosts\)\);/, 'registered beside instanceState');
|
|
1955
1956
|
});
|
|
1956
1957
|
|