@bongos/core 1.25.17 → 1.25.19
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 +135 -65
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +17 -1
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0408-a-genesis-stage-is-three-doors-onto-one-record.md +2 -1
- package/docs/api/openapi.json +288 -6
- package/docs/api-reference.md +14 -4
- package/docs/copy-inventory.md +132 -121
- package/docs/copy-registry.json +347 -195
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +3 -0
- package/docs/page-readings.json +63 -63
- package/modules/app-host/CLAUDE.md +15 -0
- package/modules/app-host/module.json +20 -0
- package/modules/app-host/public/settings-widget.js +143 -0
- package/modules/app-host/routes/app-host.js +88 -0
- package/modules/government/charter-advice.js +103 -0
- package/modules/government/presets.js +42 -0
- package/modules/government/recommendations.js +102 -7
- package/modules/government/routes/government.js +1 -0
- package/modules/hall-ui/public/genesis-home.js +28 -3
- package/modules/lifecycle/first-read-catalog.js +62 -0
- package/modules/lifecycle/first-read-db.js +47 -9
- package/modules/lifecycle/first-read.js +88 -23
- package/modules/lifecycle/migrations/lifecycle_023_repo_map_dismissed.sql +19 -0
- package/modules/lifecycle/routes/first-read.js +27 -5
- package/modules/onboarding/genesis-home.js +68 -7
- package/modules/onboarding/genesis-work.js +169 -4
- package/modules/onboarding/routes/genesis-work.js +5 -2
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/sso-app-status.js +65 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/seams.js +32 -1
- package/modules/provisioning/starter-bundles.js +1 -0
- package/modules/public-landing/public/assets/guide-core.js +475 -0
- package/modules/public-landing/public/assets/guide-hall.css +121 -0
- package/modules/public-landing/public/assets/guide-hall.js +164 -0
- package/modules/public-landing/public/assets/guide-scene.css +9 -0
- package/modules/public-landing/public/assets/guide-scene.js +58 -381
- package/modules/public-landing/public/index.html +46 -1
- package/modules/public-landing/public/projects.html +11 -2
- package/modules/ui-design/kit/serve.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +24 -0
- package/scripts/bongos/first-read.js +45 -11
- package/scripts/bongos/genesis.js +29 -2
- package/scripts/bongos/publish-manifest.js +1 -0
- package/scripts/bongos/seam-wiring-guard.js +3 -2
- package/scripts/hall-preview/server.js +2 -0
- package/src/bongos/auth-admission.js +64 -0
- package/src/bongos/auth.js +2 -1
- package/src/bongos/module-scope-map.js +4 -0
- package/src/bongos/routes/core-update.js +3 -16
- package/src/bongos/routes/modules.js +11 -0
- package/src/bongos/serve-internal.js +20 -6
- package/src/module-api.js +6 -1
- package/src/module-loader/loader.js +2 -2
- package/tests/app_host.mjs +413 -0
- package/tests/first_read.mjs +184 -8
- package/tests/genesis_home.mjs +81 -1
- package/tests/genesis_work.mjs +103 -3
- package/tests/genesis_work_routes.mjs +9 -0
- package/tests/government_charter_advice.mjs +150 -0
- package/tests/landing_guide_hall.mjs +364 -0
- package/tests/landing_guide_scene.mjs +8 -5
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_where.mjs +13 -3
|
@@ -132,7 +132,7 @@ const UNAUTH = { error: { code: 'unauthenticated', message: 'unauthenticated' }
|
|
|
132
132
|
// only its length matters here; `optional` and `bundles` are byte-for-byte.
|
|
133
133
|
const STARTER_BUNDLES_FIXTURE = {
|
|
134
134
|
core: [
|
|
135
|
-
'autonomy', 'builder-settings', 'copy-desk', 'economy', 'government', 'grading',
|
|
135
|
+
'app-host', 'autonomy', 'builder-settings', 'copy-desk', 'economy', 'government', 'grading',
|
|
136
136
|
'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'page-notes', 'public-landing',
|
|
137
137
|
'security', 'sessions', 'specialities', 'status-ui', 'ui-design',
|
|
138
138
|
],
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.19",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.19",
|
|
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.25.
|
|
3
|
+
"version": "1.25.19",
|
|
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/release-notes.json
CHANGED
|
@@ -11834,5 +11834,29 @@
|
|
|
11834
11834
|
"id": "1005280",
|
|
11835
11835
|
"text": "Owners bringing in an existing project get a prompt for their own AI to read the code and map goals, tasks and what's broken; they then choose what Bongos files as drafts."
|
|
11836
11836
|
}
|
|
11837
|
+
],
|
|
11838
|
+
"1.25.18": [
|
|
11839
|
+
{
|
|
11840
|
+
"id": "1005281",
|
|
11841
|
+
"text": "When an owner's own AI reads their existing project, it can now suggest modules from the project's own list that would do a weak job better; the owner accepts each one as a to-do or dismisses it."
|
|
11842
|
+
},
|
|
11843
|
+
{
|
|
11844
|
+
"id": "1005276",
|
|
11845
|
+
"text": "People who start a project from the front page can now talk with the guide in a plain, simple version instead of the game, and switch between the two mid-chat."
|
|
11846
|
+
},
|
|
11847
|
+
{
|
|
11848
|
+
"id": "1005119",
|
|
11849
|
+
"text": "When founding a project, the charter step now asks how decisions should go and what the founder believes, then recommends a government, a level of strictness, and a first draft of the rules, each with the reason in the founder"
|
|
11850
|
+
},
|
|
11851
|
+
{
|
|
11852
|
+
"id": "1004571",
|
|
11853
|
+
"text": "Inside a project's hall, the options menu now shows where the project's app runs (no platform yet, or Render and its progress and address), and gives the owner a one-click way to connect Render."
|
|
11854
|
+
}
|
|
11855
|
+
],
|
|
11856
|
+
"1.25.19": [
|
|
11857
|
+
{
|
|
11858
|
+
"id": "1005115",
|
|
11859
|
+
"text": "A solo founder's Genesis now moves to the next stage by itself when a stage's work is finished, and every stage leaves a named document (the scope, the strategy...) they can come back and edit any time."
|
|
11860
|
+
}
|
|
11837
11861
|
]
|
|
11838
11862
|
}
|
|
@@ -8,12 +8,15 @@
|
|
|
8
8
|
// the map file the session wrote, which `check` validates locally first with the same
|
|
9
9
|
// rules the server applies (modules/lifecycle/first-read.js).
|
|
10
10
|
//
|
|
11
|
-
// bongos first-read prompt print the prompt
|
|
12
|
-
// bongos first-read
|
|
11
|
+
// bongos first-read prompt print the prompt, with this project's module list
|
|
12
|
+
// bongos first-read catalog the modules a map may recommend (task 1005281)
|
|
13
|
+
// bongos first-read check <map.json> validate (asks for the module list only when
|
|
14
|
+
// the map recommends modules)
|
|
13
15
|
// bongos first-read submit <map.json> send it for review — files nothing
|
|
14
16
|
// bongos first-read show [<map id>] the newest map (or one), with what is filed
|
|
15
17
|
// bongos first-read accept <map id> --all | --items <ref>,<ref> ONLY with the owner's yes
|
|
16
|
-
// bongos first-read
|
|
18
|
+
// bongos first-read dismiss <map id> --items <ref>,<ref> the owner's no to some items
|
|
19
|
+
// bongos first-read reject <map id> the owner's no to all of it — files nothing
|
|
17
20
|
|
|
18
21
|
'use strict';
|
|
19
22
|
|
|
@@ -34,9 +37,10 @@ function printErrors(errors) {
|
|
|
34
37
|
|
|
35
38
|
function printItems(items) {
|
|
36
39
|
for (const i of items) {
|
|
37
|
-
const mark = i.filed ? ' [already in Bongos]'
|
|
38
|
-
const indent = i.type === 'goal' ? '' : ' ';
|
|
39
|
-
console.log(`${indent}${i.type.padEnd(
|
|
40
|
+
const mark = !i.filed ? '' : i.filed.kind === 'dismissed' ? ' [dismissed]' : ' [already in Bongos]';
|
|
41
|
+
const indent = i.type === 'goal' || i.type === 'module' ? '' : ' ';
|
|
42
|
+
console.log(`${indent}${i.type.padEnd(6)} ${i.title}${mark}\n${indent} ref: ${i.ref}`);
|
|
43
|
+
if (i.type === 'module' && i.found) console.log(`${indent} found: ${i.found}`);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -57,16 +61,37 @@ async function call(method, path, body) {
|
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
const fail = () => require('./cli-lib').cliExit(1);
|
|
64
|
+
|
|
65
|
+
// The live module list, or null when it cannot be read (no session, or the server says so).
|
|
66
|
+
async function liveCatalog() {
|
|
67
|
+
const out = await call('GET', '/api/bongos/first-read/catalog');
|
|
68
|
+
return out && out.available ? out.modules : null;
|
|
69
|
+
}
|
|
60
70
|
const flag = (name) => { const i = process.argv.indexOf(name); return i === -1 ? null : (process.argv[i + 1] || ''); };
|
|
61
71
|
|
|
62
72
|
(async () => {
|
|
63
73
|
const [verb, arg] = process.argv.slice(2);
|
|
64
74
|
switch (verb) {
|
|
65
|
-
case 'prompt':
|
|
66
|
-
|
|
75
|
+
case 'prompt': {
|
|
76
|
+
// The server's prompt carries the live module list; offline, the fixed text says
|
|
77
|
+
// how to fetch it later.
|
|
78
|
+
const out = await call('GET', '/api/bongos/first-read/prompt');
|
|
79
|
+
console.log(out ? out.prompt : fr.firstReadPrompt());
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
case 'catalog': {
|
|
83
|
+
const out = await call('GET', '/api/bongos/first-read/catalog');
|
|
84
|
+
if (!out) return fail();
|
|
85
|
+
if (!out.available) { console.log('The module list cannot be read on this project. Leave "recommended_modules" out of the map.'); return; }
|
|
86
|
+
if (!out.modules.length) { console.log('This project has no modules left to switch on. Leave "recommended_modules" out of the map.'); return; }
|
|
87
|
+
console.log('Modules this project can switch on (recommend only these, by key):');
|
|
88
|
+
for (const m of out.modules) console.log(` ${m.key}: ${m.title}. ${m.summary}`.trimEnd());
|
|
67
89
|
return;
|
|
90
|
+
}
|
|
68
91
|
case 'check': {
|
|
69
|
-
const
|
|
92
|
+
const raw = readMap(arg);
|
|
93
|
+
const recommends = Array.isArray(raw.recommended_modules) && raw.recommended_modules.length > 0;
|
|
94
|
+
const v = fr.validateRepoMap(raw, recommends ? { catalog: await liveCatalog() } : {});
|
|
70
95
|
if (!v.ok) { printErrors(v.errors); process.exit(1); }
|
|
71
96
|
console.log(`The map is in the right shape: ${v.items.length} items.`);
|
|
72
97
|
printItems(v.items);
|
|
@@ -80,7 +105,7 @@ const flag = (name) => { const i = process.argv.indexOf(name); return i === -1 ?
|
|
|
80
105
|
if (!out) return fail();
|
|
81
106
|
console.log(`Sent as map ${out.map_id}. Nothing is filed yet.`);
|
|
82
107
|
printItems(out.items);
|
|
83
|
-
console.log(`\nWalk the owner through it. Only with their yes:\n bongos first-read accept ${out.map_id} --all\n bongos first-read accept ${out.map_id} --items <ref>,<ref>\nor, if they want none of it:\n bongos first-read reject ${out.map_id}`);
|
|
108
|
+
console.log(`\nWalk the owner through it. Only with their yes:\n bongos first-read accept ${out.map_id} --all\n bongos first-read accept ${out.map_id} --items <ref>,<ref>\nwhat they turn down:\n bongos first-read dismiss ${out.map_id} --items <ref>,<ref>\nor, if they want none of it:\n bongos first-read reject ${out.map_id}`);
|
|
84
109
|
return;
|
|
85
110
|
}
|
|
86
111
|
case 'show': {
|
|
@@ -103,6 +128,15 @@ const flag = (name) => { const i = process.argv.indexOf(name); return i === -1 ?
|
|
|
103
128
|
for (const s of out.skipped) console.log(` skipped ${s.ref} (${s.reason.replace('_', ' ')})`);
|
|
104
129
|
return;
|
|
105
130
|
}
|
|
131
|
+
case 'dismiss': {
|
|
132
|
+
const list = (flag('--items') || '').split(',').map((x) => x.trim()).filter(Boolean);
|
|
133
|
+
if (!arg || !list.length) { console.error('usage: bongos first-read dismiss <map id> --items <ref>,<ref>'); process.exit(2); }
|
|
134
|
+
const out = await call('POST', `/api/bongos/first-read/maps/${arg}/dismiss`, { items: list });
|
|
135
|
+
if (!out) return fail();
|
|
136
|
+
console.log(`Dismissed: ${out.dismissed.length ? out.dismissed.join(', ') : 'nothing'}. A later read-through will not raise them again.`);
|
|
137
|
+
for (const x of out.skipped) console.log(` skipped ${x.ref} (${x.reason.replace('_', ' ')})`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
106
140
|
case 'reject': {
|
|
107
141
|
if (!arg) { console.error('usage: bongos first-read reject <map id>'); process.exit(2); }
|
|
108
142
|
if (!(await call('POST', `/api/bongos/first-read/maps/${arg}/reject`, {}))) return fail();
|
|
@@ -110,7 +144,7 @@ const flag = (name) => { const i = process.argv.indexOf(name); return i === -1 ?
|
|
|
110
144
|
return;
|
|
111
145
|
}
|
|
112
146
|
default:
|
|
113
|
-
console.log('usage: bongos first-read prompt | check <file> | submit <file> | show [id] | accept <id> --all|--items a,b | reject <id>');
|
|
147
|
+
console.log('usage: bongos first-read prompt | catalog | check <file> | submit <file> | show [id] | accept <id> --all|--items a,b | dismiss <id> --items a,b | reject <id>');
|
|
114
148
|
process.exit(verb ? 2 : 0);
|
|
115
149
|
}
|
|
116
150
|
})().catch((e) => { console.error(e.message || e); process.exitCode = 1; });
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Usage:
|
|
13
13
|
// npx bongos genesis show [stage] every stage, or one, with its answers
|
|
14
|
+
// npx bongos genesis document <stage> the stage's document (task 1005115): what it left behind, editable any time
|
|
14
15
|
// npx bongos genesis context what the founder said, as markdown (for planning sessions)
|
|
15
16
|
// npx bongos genesis assist <stage> the brief for the founder's AI session (the assisted door)
|
|
16
17
|
// npx bongos genesis answer <stage> <question> "<words>" [--origin founder|assisted]
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
const fs = require('node:fs');
|
|
29
30
|
const { apiCall, requireSession, errCode, cliExit } = require('./cli-lib');
|
|
30
31
|
|
|
31
|
-
const USAGE = 'usage: npx bongos genesis show [stage] | context | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
32
|
+
const USAGE = 'usage: npx bongos genesis show [stage] | context | document <stage> | assist <stage> | answer <stage> <question> "<words>" [--origin founder|assisted] | answer <stage> <question> --not-sure|--skip ["why"] | clear <stage> <question> | history <stage> <question> | door <stage> guided|open|assisted | page <stage> "<words>"|--file <path>|--remove | link <stage> <url>|--remove';
|
|
32
33
|
|
|
33
34
|
const STATE_LABEL = { answered: '', not_sure: '[not sure yet] ', skipped: '[skipped] ' };
|
|
34
35
|
const ORIGIN_LABEL = { assisted: ' (with AI help)', carried_over: ' (carried over — confirm or edit)' };
|
|
@@ -41,16 +42,36 @@ function formatStage(s) {
|
|
|
41
42
|
for (const q of s.questions) {
|
|
42
43
|
if (q.group && q.group !== group) { group = q.group; out.push(` — ${group} —`); }
|
|
43
44
|
out.push(` ${q.id}: ${q.ask}`);
|
|
45
|
+
if (q.choices) out.push(` choose: ${q.choices.map((c) => `${c.key} (${c.label})`).join(' · ')}`);
|
|
44
46
|
if (!q.answer) { out.push(' (empty)'); continue; }
|
|
45
47
|
const a = q.answer;
|
|
46
|
-
const
|
|
48
|
+
const said = a.choice_label || a.text;
|
|
49
|
+
const words = said ? said.replace(/\n/g, '\n ') : '';
|
|
47
50
|
out.push(` ${STATE_LABEL[a.state] || ''}${words}${ORIGIN_LABEL[a.origin] || ''}`.trimEnd());
|
|
48
51
|
}
|
|
49
52
|
if (s.page) out.push(' Page:', ` ${s.page.replace(/\n/g, '\n ')}`);
|
|
50
53
|
if (s.link) out.push(` Link: ${s.link}`);
|
|
54
|
+
if (s.advice) out.push('', ...formatAdvice(s.advice));
|
|
51
55
|
return out.join('\n');
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
// Pure: the charter stage's recommendation from the founder's answers (task 1005119).
|
|
59
|
+
// A recommendation only: choosing is still the founder's.
|
|
60
|
+
function formatAdvice(a) {
|
|
61
|
+
const out = [` Recommended government: ${a.charter.name}`, ` Why: ${a.charter.why}`];
|
|
62
|
+
if (a.charter.council_rank) out.push(` It is governed by a council rank you create: ${a.charter.council_rank}.`);
|
|
63
|
+
out.push(' It is a recommendation. Any form, or your own settings, is still yours to choose.');
|
|
64
|
+
if (a.preset) {
|
|
65
|
+
out.push(` Recommended strictness: ${a.preset.name}. ${a.preset.line}`, ` Why: ${a.preset.why}`, ` ${a.preset.is}`);
|
|
66
|
+
for (const l of a.preset.laws) out.push(` - ${l.text}: ${l.effect_on_ai}`);
|
|
67
|
+
} else {
|
|
68
|
+
out.push(' Strictness: no preset recommended. Written principles with no laws is a fine way to start.');
|
|
69
|
+
}
|
|
70
|
+
out.push(' Draft bylaws and code of conduct, from your answers (edit it, then save it with',
|
|
71
|
+
' npx bongos genesis page charter --file <path>):', '', ...String(a.draft_md || '').split('\n').map((l) => ` ${l}`));
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
// Pure: parse `answer` arguments into the PUT body, or { error }.
|
|
55
76
|
function parseAnswer(rest) {
|
|
56
77
|
const args = [...rest];
|
|
@@ -110,6 +131,12 @@ async function main(argv) {
|
|
|
110
131
|
return 0;
|
|
111
132
|
}
|
|
112
133
|
if (!stage) { console.error(USAGE); return 2; }
|
|
134
|
+
if (cmd === 'document') {
|
|
135
|
+
const d = await call('GET', `/founding/work/${enc(stage)}/document`, null, 'read the document');
|
|
136
|
+
if (!d) return 1;
|
|
137
|
+
console.log(d.document.markdown || `${d.document.title}: nothing written yet.`);
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
113
140
|
if (cmd === 'assist') {
|
|
114
141
|
const d = await call('GET', `/founding/work/${enc(stage)}/assist`, null, 'build the brief');
|
|
115
142
|
if (!d) return 1;
|
|
@@ -78,6 +78,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
78
78
|
'modules/render-deploy/', // the deploy page of a project whose app deploys through Render (task 1004270). default:false. MUST be here for the npm-release reason: modules/hall-ui/ ships as one subtree, so a released core without it carries deploy.js's loader for #rd-panel with no routes behind it.
|
|
79
79
|
'modules/market-watch/', // the weekly AI market watch (task 1004935, goal 1000137). default:false and empty for now, but MUST be here: shipped is not enabled, and a released core without it answers "unknown module" to an instance that turns it on; tests/market_watch_module.mjs (publishable) also reads its manifest.
|
|
80
80
|
'modules/ai-studio/', // AI generation for artists (task 1004916, ADR 0392): keys, prepaid balance, generate. default:false.
|
|
81
|
+
'modules/app-host/', // where the app runs (task 1004571): the options menu's panel, read from the hub. default:true.
|
|
81
82
|
'modules/front-door-guide/', // the start-a-project guide (task 1005272): a capped AI chat for a signed-out visitor on the platform's key. default:false, hub first; shipped is not enabled.
|
|
82
83
|
'modules/raise/', // the /raise investor page (task 1004847, ADR 0384) — ships in core, enabled ONLY on the hub, like platform-identity. Owner decision 2026-10-25: the whole page, its copy and starting numbers included, lives in the repo so builders can contribute to it, accepting that it publishes with the core. Secrets never do: the investor password is RAISE_PASSWORD_HASH in the instance env. default:false.
|
|
83
84
|
'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.
|
|
@@ -97,11 +97,12 @@ const SEAM_CONSUMERS = {
|
|
|
97
97
|
uiSections: {
|
|
98
98
|
// Per-MODULE, not per-entry: hallWidgetScripts looks for one widget file per page
|
|
99
99
|
// however many sections the module names — hall-widget.js for the index, and since
|
|
100
|
-
// task 1004301 task-widget.js for a task's page
|
|
100
|
+
// task 1004301 task-widget.js for a task's page, and since task 1004571 settings-widget.js
|
|
101
|
+
// for the options menu. Any one wires the declaration.
|
|
101
102
|
consumer: 'src/module-loader/loader.js hallWidgetScripts',
|
|
102
103
|
kind: 'file',
|
|
103
104
|
artifact: (dir) => path.join(dir, 'public', 'hall-widget.js'),
|
|
104
|
-
alternates: (dir) => [path.join(dir, 'public', 'task-widget.js')],
|
|
105
|
+
alternates: (dir) => [path.join(dir, 'public', 'task-widget.js'), path.join(dir, 'public', 'settings-widget.js')],
|
|
105
106
|
},
|
|
106
107
|
skills: {
|
|
107
108
|
// moduleSkillSources resolves a declared name against the module's OWN tree or
|
|
@@ -457,6 +457,8 @@ for (const key of MODULES_ON) {
|
|
|
457
457
|
const isOn = (k) => !!(mods.enabled && mods.enabled[k]);
|
|
458
458
|
if (stamped['index.html']) stamped['index.html'] = injectModuleHallWidgets(stamped['index.html'], hallWidgetScripts({ isEnabled: isOn }));
|
|
459
459
|
if (stamped['task.html']) stamped['task.html'] = injectModuleHallWidgets(stamped['task.html'], hallWidgetScripts({ isEnabled: isOn, file: 'task-widget.js' }), 'task');
|
|
460
|
+
// the options menu's module panels (task 1004571)
|
|
461
|
+
if (stamped['settings.html']) stamped['settings.html'] = injectModuleHallWidgets(stamped['settings.html'], hallWidgetScripts({ isEnabled: isOn, file: 'settings-widget.js' }), 'settings');
|
|
460
462
|
// ...and their assets, for the same set. The real asset route (serve-internal.js) gates on
|
|
461
463
|
// the instance config's enabled set, which ignores --modules-on, so a widget injected for
|
|
462
464
|
// a module this checkout's config leaves off 404'd and the slot stayed empty (task 1004940).
|
|
@@ -626,8 +626,72 @@ async function notifyHubOfInvite(githubLogin, { invited = true } = {}) {
|
|
|
626
626
|
}
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
+
// WHERE THIS PROJECT'S OWNER TAKES IT OFFLINE OR DISCONNECTS IT (task 1004570). Both
|
|
630
|
+
// controls live on the hub's Projects page for this project — the typed-name confirm and
|
|
631
|
+
// the intent queue are there (ADR 0290) — so the hall only links to them and gains no
|
|
632
|
+
// second delete path. Moved here from routes/core-update.js (task 1004571) so the kernel's
|
|
633
|
+
// readAppStatusFromHub can name the same page; the route re-exports it. The hub origin and
|
|
634
|
+
// this project's client id, which is its slug; null on a self-hosted instance or the hub itself, so the
|
|
635
|
+
// hall shows no section rather than a dead link. The page resolves ?project=<slug>
|
|
636
|
+
// against the visitor's OWN projects, so the link opens nothing for anyone else.
|
|
637
|
+
function hubManageLinks(config) {
|
|
638
|
+
if (!config || typeof config.origin !== 'string' || !config.clientId) return null;
|
|
639
|
+
if (!/^https?:\/\/[^\s"'<>]+$/i.test(config.origin)) return null;
|
|
640
|
+
const origin = config.origin.replace(/\/+$/, '');
|
|
641
|
+
const page = `${origin}/projects?view=manage&project=${encodeURIComponent(String(config.clientId))}`;
|
|
642
|
+
let host = origin;
|
|
643
|
+
try { host = new URL(origin).host; } catch { /* keep the origin as written */ }
|
|
644
|
+
return { hub_host: host, manage_url: page, offline_url: `${page}#mDelete`, disconnect_url: `${page}#mDisconnect` };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// readAppStatusFromHub() — where THIS project's own app runs, asked of the hub (task
|
|
648
|
+
// 1004571). We host every project's hall; its app runs on the owner's Render account, and
|
|
649
|
+
// that setup's state lives in the hub's provisioning row, not here. So the hall asks, as
|
|
650
|
+
// the project itself (client_id + client_secret, POST /sso/app-status), and the hub answers
|
|
651
|
+
// with modules/provisioning/app-status.js's own shape for this project only.
|
|
652
|
+
//
|
|
653
|
+
// In the kernel beside the other hub calls for readApplicantProfileFromHub's reason: it
|
|
654
|
+
// holds the project's hub client secret, and a module reaches it only through this narrow
|
|
655
|
+
// port — no argument, one project, one direction in.
|
|
656
|
+
//
|
|
657
|
+
// Answers, never throws:
|
|
658
|
+
// { hub: false } — not federated (self-hosted, or the hub)
|
|
659
|
+
// { hub: true, reachable: false, manage } — the hub did not answer (down, older hub)
|
|
660
|
+
// { hub: true, reachable: true, known, app, sandbox, manage }
|
|
661
|
+
// `manage` is the hub's page for this project ({ manage_url, hub_host }, hubManageLinks
|
|
662
|
+
// above), where the owner connects Render; it names an address and grants nothing.
|
|
663
|
+
|
|
664
|
+
const HUB_APP_STATUS_TIMEOUT_MS = 5000;
|
|
665
|
+
|
|
666
|
+
async function readAppStatusFromHub() {
|
|
667
|
+
let idp = null;
|
|
668
|
+
try { idp = loadIdpConfig(); } catch { idp = null; }
|
|
669
|
+
if (!idp) return { hub: false };
|
|
670
|
+
const links = hubManageLinks(idp);
|
|
671
|
+
const manage = links ? { manage_url: links.manage_url, hub_host: links.hub_host } : null;
|
|
672
|
+
try {
|
|
673
|
+
const r = await fetch(`${idp.origin}/api/bongos/sso/app-status`, {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': userAgent() },
|
|
676
|
+
signal: AbortSignal.timeout(HUB_APP_STATUS_TIMEOUT_MS),
|
|
677
|
+
body: JSON.stringify({ client_id: idp.clientId, client_secret: idp.clientSecret }),
|
|
678
|
+
});
|
|
679
|
+
const body = r.ok ? await r.json().catch(() => null) : null;
|
|
680
|
+
if (!body || body.ok !== true) return { hub: true, reachable: false, manage };
|
|
681
|
+
const app = body.app && typeof body.app === 'object' ? body.app : null;
|
|
682
|
+
// The sandbox beside the app (task 1005289); an older hub sends none.
|
|
683
|
+
const sandbox = body.sandbox && typeof body.sandbox === 'object' ? body.sandbox : null;
|
|
684
|
+
return { hub: true, reachable: true, known: body.known === true, app, sandbox, manage };
|
|
685
|
+
} catch (err) {
|
|
686
|
+
log.info('[auth] hub app-status read failed (non-blocking):', err.message);
|
|
687
|
+
return { hub: true, reachable: false, manage };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
629
691
|
module.exports = {
|
|
630
692
|
firstAdminLogin,
|
|
693
|
+
readAppStatusFromHub,
|
|
694
|
+
hubManageLinks,
|
|
631
695
|
firstTimeAdmissionBlocked,
|
|
632
696
|
instanceNeedsFirstAdmin,
|
|
633
697
|
readApplicantProfileFromHub,
|
package/src/bongos/auth.js
CHANGED
|
@@ -57,7 +57,7 @@ const ic = require('../instance-config');
|
|
|
57
57
|
const { branding, userAgent } = require('../branding');
|
|
58
58
|
|
|
59
59
|
const { SCOPE, authConfigured, grantedScopeCoversRepos, idpConfigured, idpEnabled, loadIdpConfig, scopeForWebFlow } = require('./auth-config.js');
|
|
60
|
-
const { firstAdminLogin, firstTimeAdmissionBlocked, instanceNeedsFirstAdmin, joinabilityMode, maybeSeatFirstAdmin, membershipKind, notifyHubOfInvite, openEnrollmentEnabled, readApplicantProfileFromHub, readApplicantViewFromHub, webAdmissionStatus } = require('./auth-admission.js');
|
|
60
|
+
const { firstAdminLogin, firstTimeAdmissionBlocked, instanceNeedsFirstAdmin, joinabilityMode, maybeSeatFirstAdmin, membershipKind, notifyHubOfInvite, openEnrollmentEnabled, readAppStatusFromHub, readApplicantProfileFromHub, readApplicantViewFromHub, webAdmissionStatus } = require('./auth-admission.js');
|
|
61
61
|
const { buildAuthorizeUrl, completeWebFlow, pollDeviceFlow, startDeviceFlow } = require('./auth-github.js');
|
|
62
62
|
const { buildIdpAuthorizeUrl, completeDeviceExchange, completeIdpFlow, idpAuthorizeUrl, loadHubPublicKey, logoutTokenJtiSeenAndRecord, readApplicationVouch, verifyHubAssertion, verifyHubLogoutToken } = require('./auth-idp.js');
|
|
63
63
|
const { addDiscordGuildMember, buildDiscordAuthorizeUrl, discordAuthorizeUrl, discordBotConfigured, discordConfigured, exchangeDiscordCode, fetchDiscordUser, isValidDiscordUserId, loadDiscordBotToken, loadDiscordCreds, removeDiscordGuildMember } = require('./auth-discord.js');
|
|
@@ -843,6 +843,7 @@ module.exports = {
|
|
|
843
843
|
joinabilityMode,
|
|
844
844
|
readApplicantProfileFromHub,
|
|
845
845
|
readApplicantViewFromHub,
|
|
846
|
+
readAppStatusFromHub,
|
|
846
847
|
notifyHubOfInvite,
|
|
847
848
|
readApplicationVouch,
|
|
848
849
|
firstTimeAdmissionBlocked,
|
|
@@ -193,6 +193,10 @@ const MODULE_GLOBS = {
|
|
|
193
193
|
'tests/front_door_guide_plan.mjs', 'tests/front_door_guide_db.mjs',
|
|
194
194
|
'tests/front_door_guide_existing.mjs',
|
|
195
195
|
],
|
|
196
|
+
// Where the app runs (task 1004571): the options menu's panel on a project's hall.
|
|
197
|
+
'app-host': [
|
|
198
|
+
'modules/app-host/', 'tests/app_host.mjs',
|
|
199
|
+
],
|
|
196
200
|
// The /raise investor page (task 1004847, ADR 0384): hub-only.
|
|
197
201
|
raise: [
|
|
198
202
|
'modules/raise/',
|
|
@@ -53,22 +53,9 @@ function hubDeployUrl(config) {
|
|
|
53
53
|
return `${config.origin.replace(/\/+$/, '')}/deploy?project=${encodeURIComponent(String(config.clientId))}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// second delete path. Same two inputs as hubDeployUrl (the hub origin and this project's
|
|
60
|
-
// client id, which is its slug); null on a self-hosted instance or the hub itself, so the
|
|
61
|
-
// hall shows no section rather than a dead link. The page resolves ?project=<slug>
|
|
62
|
-
// against the visitor's OWN projects, so the link opens nothing for anyone else.
|
|
63
|
-
function hubManageLinks(config) {
|
|
64
|
-
if (!config || typeof config.origin !== 'string' || !config.clientId) return null;
|
|
65
|
-
if (!/^https?:\/\/[^\s"'<>]+$/i.test(config.origin)) return null;
|
|
66
|
-
const origin = config.origin.replace(/\/+$/, '');
|
|
67
|
-
const page = `${origin}/projects?view=manage&project=${encodeURIComponent(String(config.clientId))}`;
|
|
68
|
-
let host = origin;
|
|
69
|
-
try { host = new URL(origin).host; } catch { /* keep the origin as written */ }
|
|
70
|
-
return { hub_host: host, manage_url: page, offline_url: `${page}#mDelete`, disconnect_url: `${page}#mDisconnect` };
|
|
71
|
-
}
|
|
56
|
+
// hubManageLinks — the hub's page for THIS project (task 1004570). Lives in the kernel's
|
|
57
|
+
// auth-admission.js since task 1004571, which needs it too and may not import a route file.
|
|
58
|
+
const { hubManageLinks } = require('../auth-admission');
|
|
72
59
|
|
|
73
60
|
// WHERE THIS PROJECT'S UPDATE BUTTON GOES (task 1004296). The act itself never happens in
|
|
74
61
|
// Settings: it is the deploy door's two-step confirm and auto-rollback (ADR 0293), and that
|
|
@@ -232,9 +232,20 @@ function contributeModuleDesk() {
|
|
|
232
232
|
seams.contribute('governor.docket', { desk: 'rules.modules', gate: { permission: 'module.enable' }, read: async () => moduleDeskRows(listModules()) });
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
+
// The port `modules.catalog` (task 1005281): every module on disk with its words and
|
|
236
|
+
// whether it is on, so a module (the lifecycle's first read-through) can name only real
|
|
237
|
+
// modules without requiring core. Read-only, built per call from listModules().
|
|
238
|
+
function registerModuleCatalogPort() {
|
|
239
|
+
if (seams.hasProvider('modules.catalog')) return;
|
|
240
|
+
seams.registerProvider('modules.catalog', () => listModules().map((m) => ({
|
|
241
|
+
key: m.key, title: m.title || m.key, description: m.description || '', enabled: m.enabled === true,
|
|
242
|
+
})));
|
|
243
|
+
}
|
|
244
|
+
|
|
235
245
|
module.exports = function buildModulesRouter() {
|
|
236
246
|
contributeModuleRechecksToDocket();
|
|
237
247
|
contributeModuleDesk();
|
|
248
|
+
registerModuleCatalogPort();
|
|
238
249
|
// Task 1004399: carry tarballs written inside the installed core package (the old
|
|
239
250
|
// store path) into the instance store before the next upgrade erases them. Async and
|
|
240
251
|
// never fatal; a no-op in a single checkout, where the two paths are one directory.
|
|
@@ -418,15 +418,18 @@ function clientBundleScriptTag() {
|
|
|
418
418
|
// [{ key, src }]; default reads the live enabled set. Pure + exported for the test.
|
|
419
419
|
function injectModuleHallWidgets(html, scripts = hallWidgetScripts({ isEnabled: isModuleEnabled }), consumer = 'builders') {
|
|
420
420
|
if (!Array.isArray(scripts) || scripts.length === 0) return html;
|
|
421
|
-
const tags = scripts
|
|
422
|
-
.map((s) => `<script src="${s.src}" defer></script>`)
|
|
423
|
-
.join('');
|
|
424
421
|
// Anchor on the consuming page script's <script> (builders.js on the index, task.js on
|
|
425
|
-
// a task's page — task 1004301
|
|
426
|
-
//
|
|
427
|
-
//
|
|
422
|
+
// a task's page — task 1004301; settings.js on the options menu — task 1004571)
|
|
423
|
+
// regardless of its ?v= stamp, so the module widgets always land immediately before the
|
|
424
|
+
// consumer. A widget loads the way its consumer does: builders.js and task.js are
|
|
425
|
+
// `defer`, so their widgets are too and run in document order; settings.js is a plain
|
|
426
|
+
// script, so its widgets are plain too — a deferred one would run after it had mounted.
|
|
428
427
|
const name = /^[a-z-]+$/.test(consumer) ? consumer : 'builders';
|
|
429
428
|
const anchor = html.match(new RegExp(`<script src="\\/builders\\/${name}\\.js[^"]*"[^>]*><\\/script>`));
|
|
429
|
+
const defer = !anchor || /\sdefer[\s>=]/.test(anchor[0]);
|
|
430
|
+
const tags = scripts
|
|
431
|
+
.map((s) => `<script src="${s.src}"${defer ? ' defer' : ''}></script>`)
|
|
432
|
+
.join('');
|
|
430
433
|
if (anchor) return html.replace(anchor[0], tags + anchor[0]);
|
|
431
434
|
return html.includes('</body>') ? html.replace('</body>', `${tags}</body>`) : html + tags;
|
|
432
435
|
}
|
|
@@ -1171,6 +1174,17 @@ function mountInternalSurfaces(app) {
|
|
|
1171
1174
|
);
|
|
1172
1175
|
}
|
|
1173
1176
|
|
|
1177
|
+
// task 1004571: the options menu. A module that ships public/settings-widget.js
|
|
1178
|
+
// registers a panel through window.OTB.contributeSettingsPanel; it is loaded just before
|
|
1179
|
+
// settings.js, which mounts every registered panel. With none enabled, nothing changes.
|
|
1180
|
+
if (buildersHtml['settings.html']) {
|
|
1181
|
+
buildersHtml['settings.html'] = injectModuleHallWidgets(
|
|
1182
|
+
buildersHtml['settings.html'],
|
|
1183
|
+
hallWidgetScripts({ isEnabled: isModuleEnabled, file: 'settings-widget.js' }),
|
|
1184
|
+
'settings',
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1174
1188
|
// Serve enabled modules' browser assets at /modules/<key>/public/* — the URL the
|
|
1175
1189
|
// loader's hallWidgetScripts projection points at. Mounted BEFORE the /builders
|
|
1176
1190
|
// static handler. Gated per-request on the live enabled set so a disabled module's
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.19'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -409,6 +409,11 @@ module.exports = {
|
|
|
409
409
|
// 1002304). The guild rides only beside a profile, so a null profile is a
|
|
410
410
|
// null view; an older hub that sends no guild answers guild: null.
|
|
411
411
|
get readApplicantViewFromHub() { return require('./bongos/auth').readApplicantViewFromHub; },
|
|
412
|
+
// readAppStatusFromHub() — where THIS project's app runs, read from the hub with the
|
|
413
|
+
// project's own client credentials (task 1004571). The same narrow-port posture: no
|
|
414
|
+
// argument, so a module can neither widen nor re-address it, and the secret stays off
|
|
415
|
+
// the doorway. Never throws: { hub: false } | { hub: true, reachable, known?, app?, manage }.
|
|
416
|
+
get readAppStatusFromHub() { return require('./bongos/auth').readAppStatusFromHub; },
|
|
412
417
|
// notifyHubOfInvite(githubLogin, { invited }) — tell the hub about one of THIS
|
|
413
418
|
// project's invites (true) or its rescind (false), so the invitee sees it in
|
|
414
419
|
// My Projects (task 1002818, ADR 0353). The same narrow-port reason as the
|
|
@@ -221,12 +221,12 @@ function mountModuleRoutes(router, { isEnabled } = {}, opts) {
|
|
|
221
221
|
//
|
|
222
222
|
// ONE SEAM, ONE FILE PER PAGE (task 1004301). `file` names which page's widget is being
|
|
223
223
|
// asked for: hall-widget.js for the hall index (the default, and the only caller until
|
|
224
|
-
// then), task-widget.js for a task's own page. Same declaration (uiSections), same
|
|
224
|
+
// then), task-widget.js for a task's own page, settings-widget.js for the options menu (1004571). Same declaration (uiSections), same
|
|
225
225
|
// enabled-only asset mount, same "a disabled module simply is not there" — so a module
|
|
226
226
|
// adds a line to the task page without the page's own code ever naming it. The name is
|
|
227
227
|
// matched against PAGE_WIDGET_FILES rather than trusted, so no caller can turn this into
|
|
228
228
|
// a way to serve an arbitrary file.
|
|
229
|
-
const PAGE_WIDGET_FILES = Object.freeze(['hall-widget.js', 'task-widget.js']);
|
|
229
|
+
const PAGE_WIDGET_FILES = Object.freeze(['hall-widget.js', 'task-widget.js', 'settings-widget.js']);
|
|
230
230
|
function hallWidgetScripts({ isEnabled, file = 'hall-widget.js' } = {}, opts) {
|
|
231
231
|
if (!PAGE_WIDGET_FILES.includes(file)) return [];
|
|
232
232
|
const out = [];
|