@bongos/core 1.19.533 → 1.19.535
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 +121 -56
- package/clients/bongos-client/index.d.ts +1 -1
- package/config/branding.neutral.json +3 -2
- package/docs/adr/0245-the-way-back-into-an-optional-step-is-that-step-s-own-answer-route.md +144 -0
- package/docs/adr/0246-catalog-membership-is-a-projection-not-a-federation-side-effect.md +96 -0
- package/docs/adr/0247-the-join-door-is-one-composed-answer-and-dark-matter-has-no-live-verdict-yet.md +202 -0
- package/docs/adr/README.md +4 -0
- package/docs/api/openapi.json +8 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +162 -157
- package/docs/copy-registry.json +252 -196
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +16 -0
- package/modules/hall-ui/public/hall-kit.js +93 -7
- package/modules/hall-ui/public/roadmap-timeline.js +105 -0
- package/modules/hall-ui/public/roadmap.html +7 -1
- package/modules/hall-ui/public/roadmap.js +16 -0
- package/modules/hall-ui/public/style.css +4 -2
- package/modules/hall-ui/public/watch.html +4 -0
- package/modules/hall-ui/public/watch.js +116 -0
- package/modules/onboarding/module.json +1 -1
- package/modules/onboarding/routes/access-requests.js +21 -6
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-catalog.js +202 -0
- package/modules/platform-identity/routes/my-projects.js +7 -2
- package/modules/platform-identity/routes/projects.js +6 -0
- package/modules/platform-identity/routes/sso.js +10 -0
- package/modules/provisioning/catalog-bridge.js +105 -0
- package/modules/provisioning/provisioning.js +28 -2
- package/modules/provisioning/routes/provisioning.js +45 -11
- package/modules/provisioning/tests/provisioning.mjs +11 -8
- package/modules/public-landing/public/assets/cosmos.css +15 -0
- package/modules/public-landing/public/projects.html +162 -3
- package/modules/ui-design/kit/serve.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/provision-net.js +7 -0
- package/scripts/hall-preview/refresh-fixtures.js +6 -0
- package/src/bongos/project-door.js +138 -0
- package/src/branding.js +8 -0
- package/src/module-api.js +12 -1
- package/tests/access_requests_invite.mjs +2 -2
- package/tests/application_lifecycle.mjs +1 -0
- package/tests/catalog_only_client.mjs +242 -0
- package/tests/currency_label.mjs +8 -5
- package/tests/hall_kit_charts.mjs +173 -0
- package/tests/hall_oversight_world.mjs +159 -0
- package/tests/hall_reading_world.mjs +153 -0
- package/tests/module_api.mjs +1 -0
- package/tests/project_catalog_bridge.mjs +355 -0
- package/tests/project_catalog_routes.mjs +213 -0
- package/tests/project_join_door.mjs +166 -0
- package/tests/project_join_door_route.mjs +206 -0
- package/tests/project_modules_ui.mjs +318 -0
- package/tests/projects_hub.mjs +6 -1
- package/tests/projects_hub_pre_uat.mjs +2 -2
- package/tests/provision_settings_apply.mjs +6 -5
- package/tests/provisioning_module_selection.mjs +164 -3
- package/tests/provisioning_settings.mjs +10 -7
- package/tests/provisioning_settings_apply.mjs +3 -3
- package/tests/provisioning_settings_env.mjs +7 -4
|
@@ -1368,6 +1368,21 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
1368
1368
|
#view-manage .visOpt:has(input:checked) b::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
1369
1369
|
#view-manage .visOpt:focus-within{outline:2px solid var(--accent);outline-offset:3px;}
|
|
1370
1370
|
#view-manage #mVisRuns b,#view-manage #mJoinRuns b{color:var(--ok);font-weight:500;}
|
|
1371
|
+
/* the project's extras (task 1002342): the creation picker's own band, asked
|
|
1372
|
+
again after the project exists. Stacked full width rather than side by side
|
|
1373
|
+
like the choice bands above, because each row carries a sentence saying what
|
|
1374
|
+
the part is for — and because these are independent facts, not one choice. */
|
|
1375
|
+
#view-manage .modBand{display:grid;border-top:1px solid var(--rule);border-bottom:1px solid var(--rule);}
|
|
1376
|
+
#view-manage .modOpt{display:grid;grid-template-columns:auto 1fr;gap:2px 10px;margin:0;padding:16px 0 18px;border:0;border-top:1px solid var(--rule-soft);border-radius:0;background:none;text-align:left;cursor:pointer;}
|
|
1377
|
+
#view-manage .modOpt:first-child{border-top:0;}
|
|
1378
|
+
#view-manage .modOpt::before{content:"";grid-column:1;grid-row:1;width:7px;height:7px;margin-top:6px;border-radius:50%;border:1px solid var(--rule);background:var(--bg);}
|
|
1379
|
+
#view-manage .modOpt>b,#view-manage .modOpt>small{grid-column:2;min-width:0;}
|
|
1380
|
+
#view-manage .modOpt[aria-pressed="true"]::before{background:var(--accent);border-color:var(--accent);outline:4px solid color-mix(in srgb,var(--accent) 22%,transparent);}
|
|
1381
|
+
#view-manage .modOpt b{font-family:var(--font-body);font-weight:500;font-size:15px;color:var(--ink);}
|
|
1382
|
+
#view-manage .modOpt small{font-family:var(--font-body);font-size:13px;font-weight:400;line-height:1.45;color:var(--ink-faint);max-width:62ch;}
|
|
1383
|
+
#view-manage .modOpt:hover::before{border-color:var(--ink-faint);}
|
|
1384
|
+
#view-manage .modOpt[disabled]{cursor:default;opacity:.55;}
|
|
1385
|
+
#view-manage .modOpt:focus-visible{outline:2px solid var(--accent);outline-offset:3px;}
|
|
1371
1386
|
/* ── its planet (task 1003347; direction record §16, v1 the bands): the preview
|
|
1372
1387
|
planet left; four bands right — shape · material · style as hairline cells of
|
|
1373
1388
|
one band, a word each, the chosen one carrying the dot; the colour as five
|
|
@@ -829,6 +829,26 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
829
829
|
</details>
|
|
830
830
|
</div>
|
|
831
831
|
|
|
832
|
+
<!-- live since task 1002342 (BV1.R23): the way back into the creation
|
|
833
|
+
wizard's module picker. Step 4 of the wizard is skippable, and an
|
|
834
|
+
optional step is a default rather than a dead end only if there is a
|
|
835
|
+
way back — the same argument the About step's PATCH …/detail was built
|
|
836
|
+
on, which is why this section saves through THAT route and not the
|
|
837
|
+
settings channel beside it: a module set is a declaration on the
|
|
838
|
+
platform row, not a policy a running instance obeys (ADR 0240 §3,
|
|
839
|
+
ADR 0245). So, unlike its three siblings above, nothing is pushed to
|
|
840
|
+
the project and nothing restarts — and there is no "what your project
|
|
841
|
+
runs" read-back line, because there is nothing to read back. The fold
|
|
842
|
+
is a SIBLING of the volatile slot so no re-render can drop it. -->
|
|
843
|
+
<div class="card mSec" id="mModules">
|
|
844
|
+
<h2>Its extras</h2>
|
|
845
|
+
<div id="mMods"><p class="finePrint">Reading the project…</p></div>
|
|
846
|
+
<details class="finePrint" id="mModsWhat">
|
|
847
|
+
<summary>What an extra is — and what is always on</summary>
|
|
848
|
+
<p>Extras are the optional parts a project can run — a shared dev box, a Discord mirror, and the rest. You picked a set when you created the project, or took the suggested set by skipping that step; this is where you change your mind. Switching one on or off saves straight away and changes nothing that is already running: nothing restarts, and no work is lost either way. The parts every project runs — the platform itself, the way it tracks work, the builders’ hall — were never part of the choice, so they are not listed here and cannot be switched off.</p>
|
|
849
|
+
</details>
|
|
850
|
+
</div>
|
|
851
|
+
|
|
832
852
|
<!-- the planet picker (task 1003347; direction record §16, v1 the bands): the
|
|
833
853
|
owner's own choice of how the platform draws this project — the sky,
|
|
834
854
|
the list, this page's head and the card read the same row. The pick is
|
|
@@ -1033,7 +1053,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1033
1053
|
new class and no icon. It filters the band below and NOTHING
|
|
1034
1054
|
else — what is switched on is unaffected by what is on
|
|
1035
1055
|
screen, and the line under it counts the ones it is hiding
|
|
1036
|
-
(ADR
|
|
1056
|
+
(ADR 0245). -->
|
|
1037
1057
|
<input type="text" id="modQ" placeholder="Search extras…" autocomplete="off" aria-label="Search the extras by name" aria-describedby="modFilterNote">
|
|
1038
1058
|
<div class="hint" id="modFilterNote" aria-live="polite"></div>
|
|
1039
1059
|
<div class="typeCards" id="modCards" role="group" aria-labelledby="modLbl"></div>
|
|
@@ -1185,6 +1205,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
1185
1205
|
<div id="liveProgress" role="status" aria-live="polite"></div>
|
|
1186
1206
|
<h2 class="stepsHead">Your steps — the parts only you can play</h2>
|
|
1187
1207
|
<ol class="humanSteps" id="humanSteps"></ol>
|
|
1208
|
+
<!-- the modules step was optional (task 1002342, BV1.R23) — one line
|
|
1209
|
+
saying where it lives afterwards, so a skip is never a dead end -->
|
|
1210
|
+
<p class="samePath" id="doneModules"></p>
|
|
1188
1211
|
<p class="samePath">These are the same steps however a project is created — nothing here is special to this page.</p>
|
|
1189
1212
|
</section>
|
|
1190
1213
|
</div>
|
|
@@ -3275,7 +3298,7 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
3275
3298
|
see. Search by MEANING ("somewhere to chat") is a different feature and is
|
|
3276
3299
|
deferred (idea 1000552), so nothing here pretends to understand a query.
|
|
3277
3300
|
|
|
3278
|
-
It is a VIEW over the rows and never an edit to the answer (ADR
|
|
3301
|
+
It is a VIEW over the rows and never an edit to the answer (ADR 0245):
|
|
3279
3302
|
filtering paints fewer rows, and state.modules does not move. That is what
|
|
3280
3303
|
lets a switched-on module be off screen without being switched off — the
|
|
3281
3304
|
count under the field says so rather than leaving it to be discovered. */
|
|
@@ -4071,6 +4094,10 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4071
4094
|
$('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
|
|
4072
4095
|
(adopt ? ' — adopting your existing repo' : '') +
|
|
4073
4096
|
'. The platform stands up the server, database, and address on its own — watch it right here, or any time on <a href="?view=mine">your projects page</a>.';
|
|
4097
|
+
/* the picker's later half (task 1002342, BV1.R23). The modules step is
|
|
4098
|
+
skippable, so this panel names where it lives afterwards rather than
|
|
4099
|
+
leaving an owner to go looking for it. */
|
|
4100
|
+
$('doneModules').innerHTML = 'Add or remove modules later in ' + projectPageLink('the project’s settings') + '.';
|
|
4074
4101
|
renderSteps();
|
|
4075
4102
|
}
|
|
4076
4103
|
|
|
@@ -4084,7 +4111,14 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
4084
4111
|
var a = ev.target.closest && ev.target.closest('a[href^="?view="]');
|
|
4085
4112
|
if (!a || !root.contains(a)) return;
|
|
4086
4113
|
ev.preventDefault();
|
|
4087
|
-
|
|
4114
|
+
var href = a.getAttribute('href');
|
|
4115
|
+
var to = (href.match(/view=(\w+)/) || [])[1];
|
|
4116
|
+
/* a manage link names its project in the query, and setView reads manageId
|
|
4117
|
+
rather than the href — so the id has to be handed over or every link on
|
|
4118
|
+
this panel lands on a manage page with no project (task 1002342) */
|
|
4119
|
+
var mid = (href.match(/[?&]id=([^&]*)/) || [])[1];
|
|
4120
|
+
if (to === 'manage' && mid) return goToManage(decodeURIComponent(mid));
|
|
4121
|
+
setView(to, true);
|
|
4088
4122
|
});
|
|
4089
4123
|
|
|
4090
4124
|
/* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
|
|
@@ -5499,6 +5533,9 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
5499
5533
|
guard because its card holds a textarea the in-flight tick must
|
|
5500
5534
|
never repaint out from under a typing owner. */
|
|
5501
5535
|
if (!keepRepo) renderPublish(inst);
|
|
5536
|
+
/* the extras band (task 1002342) rides the same guard: its own toggles
|
|
5537
|
+
are the only thing that should ever repaint it, never the tick */
|
|
5538
|
+
if (!keepRepo) renderMods(inst);
|
|
5502
5539
|
})
|
|
5503
5540
|
.catch(function (err) {
|
|
5504
5541
|
var m = String(err && err.message);
|
|
@@ -6279,6 +6316,128 @@ summary{min-height:24px;padding:3px 0;}
|
|
|
6279
6316
|
if (t && t.hasAttribute('data-join-save')) doJoinSave();
|
|
6280
6317
|
});
|
|
6281
6318
|
|
|
6319
|
+
/* ── the project's extras (task 1002342, BV1.R23) ──
|
|
6320
|
+
The way back into the creation wizard's module picker. Step 4 is skippable,
|
|
6321
|
+
and an optional step is a default rather than a dead end only if there is a
|
|
6322
|
+
way back — the argument PATCH …/detail was built on for the About step
|
|
6323
|
+
beside it, which is why this saves through THAT route rather than the
|
|
6324
|
+
settings channel its three neighbours use: a module set is a declaration on
|
|
6325
|
+
the platform row, not a policy a running instance obeys (ADR 0240 §3, ADR
|
|
6326
|
+
0245). Nothing is pushed and nothing restarts, so this section has no "what
|
|
6327
|
+
your project runs" line — there is nothing to read back.
|
|
6328
|
+
|
|
6329
|
+
Two reads, and only one is new. The CURRENT selection rides in on the
|
|
6330
|
+
instance itself (publicInstance resolves it, so a project that skipped the
|
|
6331
|
+
picker reads as its own starter bundle and this page never re-derives that
|
|
6332
|
+
rule); the CATALOG — what may be chosen, and the owner-facing line for each
|
|
6333
|
+
— is the same public GET /provisioning/starter-bundles the wizard reads.
|
|
6334
|
+
|
|
6335
|
+
A toggle IS the save. The band is a list of independent facts rather than
|
|
6336
|
+
one choice, so a Save button would only add a way to lose one. The write
|
|
6337
|
+
sends the WHOLE list because that is what the column stores, and the band
|
|
6338
|
+
repaints from the server's own answer — never from the list we sent — so it
|
|
6339
|
+
can never show a set the platform did not accept. ── */
|
|
6340
|
+
|
|
6341
|
+
var modsCatalog = null; /* { core, optional } — the public catalog, read once */
|
|
6342
|
+
var modsPromise = null;
|
|
6343
|
+
var modsFailed = false; /* asked and did not get it; distinct from not yet asked */
|
|
6344
|
+
var modsBusy = false; /* a write is in flight — the band is inert until it answers */
|
|
6345
|
+
|
|
6346
|
+
function modsSlot(html) { $('mMods').innerHTML = html; }
|
|
6347
|
+
|
|
6348
|
+
/* Lazy and memoised, the wizard picker's rule: /projects serves four views
|
|
6349
|
+
from one file and the map has no business paying for this read. */
|
|
6350
|
+
function ensureModsCatalog() {
|
|
6351
|
+
if (modsPromise) return modsPromise;
|
|
6352
|
+
modsPromise = fetch(API + '/provisioning/starter-bundles', { credentials: 'same-origin' })
|
|
6353
|
+
.then(function (res) { return res.ok ? res.json() : null; })
|
|
6354
|
+
.then(function (data) {
|
|
6355
|
+
if (data && data.optional) modsCatalog = data; else modsFailed = true;
|
|
6356
|
+
if (manageInst) renderMods(manageInst);
|
|
6357
|
+
})
|
|
6358
|
+
.catch(function () { modsFailed = true; if (manageInst) renderMods(manageInst); });
|
|
6359
|
+
return modsPromise;
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
/* What this project runs today, straight off the instance read — never
|
|
6363
|
+
re-derived here, or a skipped picker would read differently on two pages. */
|
|
6364
|
+
function modsSelected(inst) {
|
|
6365
|
+
return ((inst && inst.modules && inst.modules.selected) || []).slice();
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
function renderMods(inst) {
|
|
6369
|
+
ensureModsCatalog();
|
|
6370
|
+
if (!modsCatalog) {
|
|
6371
|
+
/* a read still in flight is a quiet slot; only one that ANSWERED badly
|
|
6372
|
+
gets the clause, so a slow network never reports a break that isn't */
|
|
6373
|
+
modsSlot(modsFailed
|
|
6374
|
+
? '<p class="finePrint">We couldn’t load the list of extras just now — refresh in a moment. Nothing about your project has changed.</p>'
|
|
6375
|
+
: '<p class="finePrint">Reading the project…</p>');
|
|
6376
|
+
return;
|
|
6377
|
+
}
|
|
6378
|
+
var on = modsSelected(inst);
|
|
6379
|
+
var core = (modsCatalog.core || []).length;
|
|
6380
|
+
modsSlot('<div class="modBand" id="mModBand" role="group" aria-label="Extras">' +
|
|
6381
|
+
(modsCatalog.optional || []).map(function (m) {
|
|
6382
|
+
return '<button type="button" class="modOpt" data-mod="' + esc(m.key) + '" aria-pressed="' +
|
|
6383
|
+
(on.indexOf(m.key) === -1 ? 'false' : 'true') + '"' + (modsBusy ? ' disabled' : '') +
|
|
6384
|
+
'><b>' + esc(m.label) + '</b><small>' + esc(m.summary) + '</small></button>';
|
|
6385
|
+
}).join('') + '</div>' +
|
|
6386
|
+
(core ? '<p class="finePrint">Every project also starts with the same ' + core +
|
|
6387
|
+
' core parts — the platform itself, the way it tracks work, and the builders’ hall. They are always on, so they are not part of this choice.</p>' : '') +
|
|
6388
|
+
'<div class="projMsg" id="mModMsg" role="status" aria-live="polite"></div>');
|
|
6389
|
+
}
|
|
6390
|
+
|
|
6391
|
+
/* One toggle, sent as the whole list: the column stores a list, so a delta
|
|
6392
|
+
would have to be re-assembled somewhere and the two copies would drift. */
|
|
6393
|
+
function doModToggle(key) {
|
|
6394
|
+
if (!manageInst || !modsCatalog || modsBusy) return;
|
|
6395
|
+
var on = modsSelected(manageInst);
|
|
6396
|
+
var want = on.indexOf(key) === -1 ? on.concat([key]) : on.filter(function (k) { return k !== key; });
|
|
6397
|
+
/* CATALOG order — the order the picker paints and the server stores, so a
|
|
6398
|
+
re-read of what we just wrote can never look like a change */
|
|
6399
|
+
var next = (modsCatalog.optional || []).map(function (m) { return m.key; })
|
|
6400
|
+
.filter(function (k) { return want.indexOf(k) !== -1; });
|
|
6401
|
+
modsBusy = true;
|
|
6402
|
+
renderMods(manageInst);
|
|
6403
|
+
var box = $('mModMsg');
|
|
6404
|
+
box.className = 'projMsg'; box.textContent = 'Saving…';
|
|
6405
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(manageId) + '/detail', {
|
|
6406
|
+
method: 'PATCH', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' },
|
|
6407
|
+
body: JSON.stringify({ modules: next })
|
|
6408
|
+
}).then(function (res) {
|
|
6409
|
+
return jsonOrEmpty(res).then(function (data) {
|
|
6410
|
+
modsBusy = false;
|
|
6411
|
+
if (res.ok) {
|
|
6412
|
+
/* the platform's own answer, never the list we sent */
|
|
6413
|
+
manageInst.modules = data.modules || manageInst.modules;
|
|
6414
|
+
renderMods(manageInst);
|
|
6415
|
+
var ok = $('mModMsg');
|
|
6416
|
+
ok.className = 'projMsg ok';
|
|
6417
|
+
ok.textContent = 'Saved. Nothing on the project itself changes — there is nothing to restart.';
|
|
6418
|
+
return;
|
|
6419
|
+
}
|
|
6420
|
+
renderMods(manageInst); /* the band goes back to what the row still says */
|
|
6421
|
+
var e = apiError(data, res.status);
|
|
6422
|
+
var m = $('mModMsg');
|
|
6423
|
+
if (res.status === 401) { m.className = 'projMsg err'; m.innerHTML = signInAgain(); return; }
|
|
6424
|
+
m.className = 'projMsg err';
|
|
6425
|
+
m.textContent = e.message || 'The change could not be saved — try again in a moment.';
|
|
6426
|
+
});
|
|
6427
|
+
}).catch(function () {
|
|
6428
|
+
modsBusy = false;
|
|
6429
|
+
renderMods(manageInst);
|
|
6430
|
+
var m = $('mModMsg');
|
|
6431
|
+
m.className = 'projMsg err';
|
|
6432
|
+
m.textContent = 'Couldn’t reach the platform — check your connection and try again.';
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6436
|
+
$('mMods').addEventListener('click', function (ev) {
|
|
6437
|
+
var t = ev.target.closest && ev.target.closest('.modOpt[data-mod]');
|
|
6438
|
+
if (t && !t.disabled) doModToggle(t.getAttribute('data-mod'));
|
|
6439
|
+
});
|
|
6440
|
+
|
|
6282
6441
|
/* ── on the map (task 1002324): the join door, ADR 0182 D1 — the third axis,
|
|
6283
6442
|
beside "who can see it" (the project's own sign-in door) and "how people
|
|
6284
6443
|
join" (its membership door). The two reads above, key for key: the CHOICE
|
|
@@ -187,6 +187,7 @@ const STUB_VOCAB = {
|
|
|
187
187
|
platform_visibility: ['public', 'gated'],
|
|
188
188
|
joinability: ['open', 'apply', 'invite_only'],
|
|
189
189
|
visibility: ['public', 'private', 'stealth'],
|
|
190
|
+
join_grant: ['view', 'apply', 'full'],
|
|
190
191
|
planet_template: null, // the template set is long; shape is checked, not membership
|
|
191
192
|
planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
|
|
192
193
|
card_backdrop: ['nebula', 'dust', 'deepfield', 'chrome'],
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.535",
|
|
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.535",
|
|
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.535",
|
|
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/scripts/gds/fitness.js
CHANGED
|
@@ -529,6 +529,15 @@ const KERNEL_FILES = [
|
|
|
529
529
|
// KERNEL (already in the module-scope-map kernel key) and imports only the
|
|
530
530
|
// kernel (instance-config) + node built-ins, so it joins the roster cleanly.
|
|
531
531
|
'src/bongos/repo-info.js',
|
|
532
|
+
// task 1002331 (ADR 0182 D6 / ADR 0247): the project's composed join/apply door —
|
|
533
|
+
// a deterministic classifier over three policy values, the same category as
|
|
534
|
+
// route-rank-check.js and goal-scope-check.js above (a trust decision the doorway
|
|
535
|
+
// hands to modules so no module re-derives it). The doorway re-exports it, so
|
|
536
|
+
// leaving it off this roster reads as a kernel->non-kernel leak; it imports only
|
|
537
|
+
// branding.js and auth-admission.js — both on this roster — so joining costs
|
|
538
|
+
// nothing and buys the import-direction guarantee on the file that decides who
|
|
539
|
+
// may knock.
|
|
540
|
+
'src/bongos/project-door.js',
|
|
532
541
|
];
|
|
533
542
|
|
|
534
543
|
// Kernel files exempt from the no-module-keys scan (Check 10) because naming a
|
|
@@ -22,6 +22,13 @@ function federationHubOrigin() {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
// Whether provisioning should federate this instance's sign-in through the hub.
|
|
25
|
+
//
|
|
26
|
+
// THIS GATE DECIDES A CREDENTIAL, NOT CATALOG MEMBERSHIP (task 1003576, ADR 0246). It used
|
|
27
|
+
// to decide both by accident — federateInstance was the only writer of the hub catalog — so
|
|
28
|
+
// a `standalone` project could never appear on the public map. That hop now runs at every
|
|
29
|
+
// provisioning write (modules/provisioning/catalog-bridge.js → the `project.catalog` port),
|
|
30
|
+
// for every hosting shape. What survives here is the narrow question this gate is actually
|
|
31
|
+
// about: whose web.env does the platform write, and therefore who can hold a client secret.
|
|
25
32
|
// Dormant-by-default: true ONLY when the control plane is hub-configured (the
|
|
26
33
|
// platform-identity module is present, a signing key is loadable, AND a hub origin
|
|
27
34
|
// resolves), the instance is a CO-TENANT (the mothership-hosted shape whose web.env is
|
|
@@ -63,6 +63,12 @@ const SNAPSHOTS = [
|
|
|
63
63
|
['public/progress', 'public__progress'],
|
|
64
64
|
['public/recent-shipped', 'public__recent-shipped'],
|
|
65
65
|
['public/repo-info', 'public__repo-info'],
|
|
66
|
+
// The roadmap's OTHER read (task 1003447). It was missing, so /roadmap has
|
|
67
|
+
// always 404'd here and fallen through its `.catch(() => [])` — which meant
|
|
68
|
+
// no track chip and no shipped date in preview, and (once the deck learned to
|
|
69
|
+
// draw them) no timeline. `versions` above is the AUTHENTICATED list; this is
|
|
70
|
+
// the public one the page actually calls, and the two are not interchangeable.
|
|
71
|
+
['public/versions', 'public__versions'],
|
|
66
72
|
];
|
|
67
73
|
|
|
68
74
|
(async () => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// src/bongos/project-door.js — THE project's join/apply door, composed from every
|
|
2
|
+
// knob that can close it (BV1.R12, task 1002331; ADR 0182 D6, ADR 0247).
|
|
3
|
+
//
|
|
4
|
+
// WHAT IT ANSWERS: "a stranger has arrived at this project's door — what happens?"
|
|
5
|
+
// One value, so no two surfaces can answer it differently:
|
|
6
|
+
//
|
|
7
|
+
// open no request needed; signing in is the way in
|
|
8
|
+
// apply an application is accepted and queued for the owner
|
|
9
|
+
// invite_only no application is taken; an invite is the only way in
|
|
10
|
+
// locked nothing — the project is not published, so there is no door
|
|
11
|
+
//
|
|
12
|
+
// WHY IT EXISTS. Three separate owner-set knobs each narrow the same door, and
|
|
13
|
+
// before this file only one of them (`joinability`) was read at the door — so an
|
|
14
|
+
// owner could set their project `private` or `stealth` on the platform's map and
|
|
15
|
+
// still have its public application endpoint behave exactly as before. ADR 0182 D6
|
|
16
|
+
// wrote the composition rule down ("visibility sets the ceiling; join policy may
|
|
17
|
+
// only narrow it; the stricter of the two always wins, and neither can widen the
|
|
18
|
+
// other") and left it unimplemented. This is that rule, as code, in one place.
|
|
19
|
+
//
|
|
20
|
+
// THE THREE KNOBS (all owner-set on the platform, pushed into this instance's
|
|
21
|
+
// branding pack by the provisioning runner, applied by a restart):
|
|
22
|
+
//
|
|
23
|
+
// visibility public | private | stealth how the project stands on the map
|
|
24
|
+
// joinGrant view | apply | full what a PUBLIC join grants (R12)
|
|
25
|
+
// joinability open | apply | invite_only how someone becomes a member (ADR 0194)
|
|
26
|
+
//
|
|
27
|
+
// plus `publishable`, which is DERIVED and never stored (ADR 0182 D1/D4: a project
|
|
28
|
+
// with no name or no description is "dark matter" — off the map and un-joinable).
|
|
29
|
+
//
|
|
30
|
+
// THE REFUSAL NEVER NAMES THE STATE. `stealth`, `public`+`view`, and an explicit
|
|
31
|
+
// `invite_only` all answer with the SAME door, so a refusal can never be read
|
|
32
|
+
// backwards to learn which state a project is in. That is not a nicety: ADR 0182
|
|
33
|
+
// D2a makes stealth's anonymity a privacy primitive, and a door that said "this
|
|
34
|
+
// project is stealth" would hand back exactly what the feed redaction protects.
|
|
35
|
+
// A refused application is also never STORED, so a closed project accumulates no
|
|
36
|
+
// queue to leak (D6).
|
|
37
|
+
//
|
|
38
|
+
// FAIL CLOSED. An unreadable value — a truncated env write, a fourth enum value a
|
|
39
|
+
// future release adds, a pack this core cannot parse — is never the wider door.
|
|
40
|
+
// Same rule ADR 0194 D2 applies to `joinability` alone, and the same direction R06
|
|
41
|
+
// took for the feed (an unrecognised visibility draws as a black hole, not a planet).
|
|
42
|
+
|
|
43
|
+
'use strict';
|
|
44
|
+
|
|
45
|
+
// The door vocabulary, ORDERED widest → strictest. The order IS the composition
|
|
46
|
+
// rule: two knobs never negotiate, the higher index simply wins.
|
|
47
|
+
const JOIN_DOORS = Object.freeze(['open', 'apply', 'invite_only', 'locked']);
|
|
48
|
+
const DOOR_RANK = Object.freeze(Object.fromEntries(JOIN_DOORS.map((d, i) => [d, i])));
|
|
49
|
+
|
|
50
|
+
// What a join of a PUBLIC project grants — the owner's own words from the project
|
|
51
|
+
// interview (BV1.R12). It is a CEILING on the visibility axis, not a second
|
|
52
|
+
// admission policy: `full` means "this axis does not narrow anything", which is
|
|
53
|
+
// why it is the default (DEFAULT_SETTINGS must be today's behaviour).
|
|
54
|
+
const PROJECT_JOIN_GRANTS = Object.freeze(['view', 'apply', 'full']);
|
|
55
|
+
const DOOR_BY_GRANT = Object.freeze({ full: 'open', apply: 'apply', view: 'invite_only' });
|
|
56
|
+
|
|
57
|
+
// The ceiling each stored visibility state sets (ADR 0182 D6's table). `public` is
|
|
58
|
+
// absent on purpose — it is the one state that defers to the grant.
|
|
59
|
+
const DOOR_BY_VISIBILITY = Object.freeze({ private: 'apply', stealth: 'invite_only' });
|
|
60
|
+
|
|
61
|
+
// The joinability policies this core can read (ADR 0194). Its own reader is already
|
|
62
|
+
// fail-closed, so this is the guard against a caller that bypassed it.
|
|
63
|
+
const JOINABILITY_DOOR = Object.freeze({ open: 'open', apply: 'apply', invite_only: 'invite_only' });
|
|
64
|
+
|
|
65
|
+
// The strictest of two doors. PURE. An unknown name is treated as the strictest
|
|
66
|
+
// door there is rather than ignored, so a caller can never widen this by passing
|
|
67
|
+
// a value the table does not carry.
|
|
68
|
+
function strictestDoor(a, b) {
|
|
69
|
+
const ra = DOOR_RANK[a] === undefined ? DOOR_RANK.locked : DOOR_RANK[a];
|
|
70
|
+
const rb = DOOR_RANK[b] === undefined ? DOOR_RANK.locked : DOOR_RANK[b];
|
|
71
|
+
return JOIN_DOORS[Math.max(ra, rb)];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The ceiling the project's place on the map sets, before `joinability` narrows it
|
|
75
|
+
// (ADR 0182 D6). PURE. `public` hands the answer to the grant; every other value —
|
|
76
|
+
// including one this core does not recognise — is at most `invite_only`.
|
|
77
|
+
function visibilityCeiling(visibility, joinGrant) {
|
|
78
|
+
if (visibility === 'public') {
|
|
79
|
+
return DOOR_BY_GRANT[joinGrant] || 'invite_only';
|
|
80
|
+
}
|
|
81
|
+
return DOOR_BY_VISIBILITY[visibility] || 'invite_only';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// projectJoinDoor — the whole decision, PURE, from the four inputs a door surface
|
|
85
|
+
// holds. This is the function every join/apply surface must call instead of reading
|
|
86
|
+
// any single knob, so the public application endpoint, the hall's landing form, the
|
|
87
|
+
// CLI and the hub's join relay cannot disagree about which door this project runs.
|
|
88
|
+
//
|
|
89
|
+
// `publishable` is a TRI-STATE and the third value matters: `false` locks the door
|
|
90
|
+
// (dark matter — ADR 0182 D6: "refused — there is no door to knock on"), `true`
|
|
91
|
+
// does not open anything on its own, and `null`/undefined means NO VERDICT — the
|
|
92
|
+
// caller does not hold a publish gate and must not invent one (the same word
|
|
93
|
+
// `provisioning.publishVerdict` uses when the `project.publishGate` port is absent).
|
|
94
|
+
// A caller with no verdict is answered on the three stored knobs alone.
|
|
95
|
+
function projectJoinDoor({ visibility, joinGrant, joinability, publishable = null } = {}) {
|
|
96
|
+
if (publishable === false) return 'locked';
|
|
97
|
+
const ceiling = visibilityCeiling(visibility, joinGrant);
|
|
98
|
+
// An unreadable `joinability` closes to `invite_only`, not to `locked`: its own
|
|
99
|
+
// reader already chose that value for this failure (ADR 0194 D2), and `locked`
|
|
100
|
+
// is a specific CLAIM — "this project is not published" — that an unknown policy
|
|
101
|
+
// is no evidence for. Refusing honestly matters more than refusing hardest.
|
|
102
|
+
return strictestDoor(ceiling, JOINABILITY_DOOR[joinability] || 'invite_only');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// effectiveJoinDoor — this project's LIVE door: projectJoinDoor() over the policy
|
|
106
|
+
// this instance actually resolved. The one call a door surface makes; published
|
|
107
|
+
// through the module doorway as `api.projectJoinDoor()` so a module never
|
|
108
|
+
// re-derives the composition (the drift `joinabilityMode` exists to prevent).
|
|
109
|
+
//
|
|
110
|
+
// The requires are deliberately lazy: this file's pure half must stay importable by
|
|
111
|
+
// a test with no branding pack, no env and no database behind it.
|
|
112
|
+
//
|
|
113
|
+
// A pack that will not load leaves BOTH visibility and the grant unknown, and
|
|
114
|
+
// `visibilityCeiling` answers that with `invite_only` — the same fail-closed door
|
|
115
|
+
// ADR 0194 D2 chose for the same failure, so this composition never reopens
|
|
116
|
+
// something that reader had already closed.
|
|
117
|
+
function effectiveJoinDoor(deps = {}) {
|
|
118
|
+
const readBranding = deps.branding || require('../branding').branding; // eslint-disable-line global-require
|
|
119
|
+
const readJoinability = deps.joinabilityMode || require('./auth-admission').joinabilityMode; // eslint-disable-line global-require
|
|
120
|
+
let project = {};
|
|
121
|
+
try { project = readBranding().project || {}; } catch { project = {}; }
|
|
122
|
+
return projectJoinDoor({
|
|
123
|
+
visibility: project.visibility,
|
|
124
|
+
joinGrant: project.joinGrant,
|
|
125
|
+
joinability: readJoinability(),
|
|
126
|
+
publishable: deps.publishable === undefined ? null : deps.publishable,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = {
|
|
131
|
+
JOIN_DOORS,
|
|
132
|
+
DOOR_RANK,
|
|
133
|
+
PROJECT_JOIN_GRANTS,
|
|
134
|
+
strictestDoor,
|
|
135
|
+
visibilityCeiling,
|
|
136
|
+
projectJoinDoor,
|
|
137
|
+
effectiveJoinDoor,
|
|
138
|
+
};
|
package/src/branding.js
CHANGED
|
@@ -65,6 +65,10 @@ const ENV_OVERRIDES = [
|
|
|
65
65
|
// this instance's own sign-in door; this one is how the project stands on the
|
|
66
66
|
// platform's map, and D6 makes it the ceiling JOINABILITY may only narrow.
|
|
67
67
|
['VISIBILITY', ['project', 'visibility']],
|
|
68
|
+
// What a join of this project grants while it is PUBLIC (BV1.R12, task 1002331) —
|
|
69
|
+
// view | apply | full. The ceiling on the visibility axis that JOINABILITY may
|
|
70
|
+
// only narrow; composed into one door by src/bongos/project-door.js.
|
|
71
|
+
['JOIN_GRANT', ['project', 'joinGrant']],
|
|
68
72
|
];
|
|
69
73
|
|
|
70
74
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -224,6 +228,10 @@ function clientBranding(b = branding()) {
|
|
|
224
228
|
// two neighbours: it is how the project asks to be seen on the map, and it is
|
|
225
229
|
// the READ-BACK a manage page needs to tell a saved choice from a live one.
|
|
226
230
|
visibility: b.project?.visibility || 'public',
|
|
231
|
+
// What a public join grants (task 1002331). Public for the same reason: a
|
|
232
|
+
// stranger's client should be able to say what THIS door does before knocking.
|
|
233
|
+
// `full` = this axis narrows nothing, so an un-pushed instance is unchanged.
|
|
234
|
+
joinGrant: b.project?.joinGrant || 'full',
|
|
227
235
|
},
|
|
228
236
|
};
|
|
229
237
|
}
|
package/src/module-api.js
CHANGED
|
@@ -15,6 +15,7 @@ const util = require('node:util');
|
|
|
15
15
|
const { logger: coreLogger } = require('./bongos/logger');
|
|
16
16
|
const { pool, instanceDbName } = require('./bongos/pool');
|
|
17
17
|
const branding = require('./branding');
|
|
18
|
+
const projectDoor = require('./bongos/project-door');
|
|
18
19
|
const instanceConfig = require('./instance-config');
|
|
19
20
|
const staleTimer = require('./stale-timer');
|
|
20
21
|
// Destructured, not namespaced, deliberately — the same form src/bongos/routes/
|
|
@@ -45,7 +46,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
45
46
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
46
47
|
// the entry to that file. Look for a version's history there, not here.
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
48
|
-
const CORE_VERSION = '1.19.
|
|
49
|
+
const CORE_VERSION = '1.19.535'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
49
50
|
|
|
50
51
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
51
52
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -242,6 +243,16 @@ module.exports = {
|
|
|
242
243
|
// reader, and the sign-in flows and the public access-request route
|
|
243
244
|
// must answer with the same door (SR-19).
|
|
244
245
|
joinabilityMode: auth.joinabilityMode,
|
|
246
|
+
// projectJoinDoor() — the project's COMPOSED join/apply door: 'open' | 'apply' |
|
|
247
|
+
// 'invite_only' | 'locked' (BV1.R12, task 1002331; ADR 0182 D6, ADR 0247).
|
|
248
|
+
// `joinabilityMode` above answers one knob; this answers the whole door,
|
|
249
|
+
// because the project's place on the platform's map (`visibility`) and what a
|
|
250
|
+
// public join grants (`joinGrant`) each narrow it too, and "the stricter of
|
|
251
|
+
// the two always wins" is only true if one function says so. Every join/apply
|
|
252
|
+
// surface calls THIS; a module that read a single knob would re-open a door
|
|
253
|
+
// the other two had closed. Optional `{ publishable }` for a caller that
|
|
254
|
+
// holds a publish verdict — see src/bongos/project-door.js.
|
|
255
|
+
projectJoinDoor: (opts) => projectDoor.effectiveJoinDoor(opts),
|
|
245
256
|
// userAgent — the instance's HTTP User-Agent string (branding-derived). The
|
|
246
257
|
// carved lifecycle module's github-push.js stamps every GitHub API request with
|
|
247
258
|
// it instead of reaching into src/branding.js directly (BV1.R86).
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// The doorway is stubbed in require.cache BEFORE the route loads (the route
|
|
10
10
|
// destructures pool/validators at require time — the web_admission_status
|
|
11
11
|
// pattern): the session gates admit a fixed Archon, the pool replays a script
|
|
12
|
-
// of results and records every query, and
|
|
12
|
+
// of results and records every query, and projectJoinDoor is a switch.
|
|
13
13
|
//
|
|
14
14
|
// Run: node --test tests/access_requests_invite.mjs
|
|
15
15
|
|
|
@@ -39,7 +39,7 @@ const pool = {
|
|
|
39
39
|
require.cache[apiPath].exports = {
|
|
40
40
|
...realApi,
|
|
41
41
|
pool,
|
|
42
|
-
|
|
42
|
+
projectJoinDoor: () => mode,
|
|
43
43
|
requireBuilder: (req, _res, next) => { req.builder = { id: 42, github_login: 'archon' }; next(); },
|
|
44
44
|
requirePermission: () => (_req, _res, next) => next(),
|
|
45
45
|
};
|
|
@@ -252,6 +252,7 @@ require.cache[apiPath].exports = {
|
|
|
252
252
|
...realApi,
|
|
253
253
|
pool,
|
|
254
254
|
joinabilityMode: () => 'apply',
|
|
255
|
+
projectJoinDoor: () => 'apply',
|
|
255
256
|
logger: () => ({ error() {}, warn() {}, info() {}, debug() {} }),
|
|
256
257
|
requireBuilder: (req, _res, next) => { req.builder = { id: 42, github_login: 'owner' }; next(); },
|
|
257
258
|
// requirePermission is NOT stubbed: realApi's own gate is used, so the denial
|