@bongos/core 1.19.67 → 1.19.69
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 +22 -17
- package/docs/canonical-permissions.md +3 -1
- package/modules/hall-ui/public/builders.js +11 -10
- package/modules/hall-ui/public/shell.js +15 -7
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +206 -38
- package/src/bongos/serve-internal.js +41 -24
- package/src/module-api.js +5 -1
- package/tests/hall_nav.mjs +3 -2
- package/tests/hall_page_gate_map.mjs +166 -0
- package/tests/onboard.mjs +21 -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.69",
|
|
6
|
+
"core_contract": "1.19.69",
|
|
7
|
+
"source_commit": "d1240d104fd475ebb67216412776c96dbffb1709",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-12T14:
|
|
9
|
+
"built_at": "2026-08-12T14:45:41.164Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 292,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1223,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1536,
|
|
20
|
+
"tree_sha256": "d983bcf2513056ef9cb9aca9fb6ecdb74aa1289b7a7601ba3ad00104a26b8992",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1337,7 +1337,7 @@
|
|
|
1337
1337
|
{
|
|
1338
1338
|
"path": "docs/canonical-permissions.md",
|
|
1339
1339
|
"mode": "0000644",
|
|
1340
|
-
"sha256": "
|
|
1340
|
+
"sha256": "96ae791fc131dc389158f326e5fc698283de3d67d3396003bae2632f24a148b0"
|
|
1341
1341
|
},
|
|
1342
1342
|
{
|
|
1343
1343
|
"path": "docs/design-contract.md",
|
|
@@ -2977,7 +2977,7 @@
|
|
|
2977
2977
|
{
|
|
2978
2978
|
"path": "modules/hall-ui/public/builders.js",
|
|
2979
2979
|
"mode": "0000644",
|
|
2980
|
-
"sha256": "
|
|
2980
|
+
"sha256": "076103658308541ef6b976bc333c67a2dc0069913fadbd5551c6241cc320bda4"
|
|
2981
2981
|
},
|
|
2982
2982
|
{
|
|
2983
2983
|
"path": "modules/hall-ui/public/chime.wav",
|
|
@@ -3282,7 +3282,7 @@
|
|
|
3282
3282
|
{
|
|
3283
3283
|
"path": "modules/hall-ui/public/shell.js",
|
|
3284
3284
|
"mode": "0000644",
|
|
3285
|
-
"sha256": "
|
|
3285
|
+
"sha256": "ac31de1ecb51a0899d043b7fa098693ae69cbc3ebe858c97e6c8df2c4cef8015"
|
|
3286
3286
|
},
|
|
3287
3287
|
{
|
|
3288
3288
|
"path": "modules/hall-ui/public/style.css",
|
|
@@ -4042,12 +4042,12 @@
|
|
|
4042
4042
|
{
|
|
4043
4043
|
"path": "package-lock.json",
|
|
4044
4044
|
"mode": "0000644",
|
|
4045
|
-
"sha256": "
|
|
4045
|
+
"sha256": "cfa1a680de61b09d8231908031aedaee2c923dc91ccdf933f1e62895f00f681d"
|
|
4046
4046
|
},
|
|
4047
4047
|
{
|
|
4048
4048
|
"path": "package.json",
|
|
4049
4049
|
"mode": "0000644",
|
|
4050
|
-
"sha256": "
|
|
4050
|
+
"sha256": "314dffe32265d0bade3708b1591061bbfef4cd8f3e8cfcf6e9eb2879f539ecb9"
|
|
4051
4051
|
},
|
|
4052
4052
|
{
|
|
4053
4053
|
"path": "public-docs/index.html",
|
|
@@ -4292,7 +4292,7 @@
|
|
|
4292
4292
|
{
|
|
4293
4293
|
"path": "public-landing/create.html",
|
|
4294
4294
|
"mode": "0000644",
|
|
4295
|
-
"sha256": "
|
|
4295
|
+
"sha256": "1874575c2241df1e868d158f3fc4c6c123b8c35531a5b1425de9974230da6510"
|
|
4296
4296
|
},
|
|
4297
4297
|
{
|
|
4298
4298
|
"path": "public-landing/gate/README.md",
|
|
@@ -5642,7 +5642,7 @@
|
|
|
5642
5642
|
{
|
|
5643
5643
|
"path": "src/bongos/serve-internal.js",
|
|
5644
5644
|
"mode": "0000644",
|
|
5645
|
-
"sha256": "
|
|
5645
|
+
"sha256": "006a51d3bd71cc496742e9399c53dc5b1ebd18c87a54e675aabda6ac312b4b96"
|
|
5646
5646
|
},
|
|
5647
5647
|
{
|
|
5648
5648
|
"path": "src/bongos/uptime-poller.js",
|
|
@@ -5672,7 +5672,7 @@
|
|
|
5672
5672
|
{
|
|
5673
5673
|
"path": "src/module-api.js",
|
|
5674
5674
|
"mode": "0000644",
|
|
5675
|
-
"sha256": "
|
|
5675
|
+
"sha256": "7e3fc1c20665a092ead5773ba12e2714ecce38641335f602abbb0597bb87dc2f"
|
|
5676
5676
|
},
|
|
5677
5677
|
{
|
|
5678
5678
|
"path": "src/module-loader/loader.js",
|
|
@@ -6697,7 +6697,12 @@
|
|
|
6697
6697
|
{
|
|
6698
6698
|
"path": "tests/hall_nav.mjs",
|
|
6699
6699
|
"mode": "0000644",
|
|
6700
|
-
"sha256": "
|
|
6700
|
+
"sha256": "b0da8fb0e46c57419821dbf8cb5ddeda3c98da76d1b2918c3ce77798ee2768fb"
|
|
6701
|
+
},
|
|
6702
|
+
{
|
|
6703
|
+
"path": "tests/hall_page_gate_map.mjs",
|
|
6704
|
+
"mode": "0000644",
|
|
6705
|
+
"sha256": "1e59d5608c205aa6f271c5efd0acb7899f4996dfbaecbf35c387737f19c735f5"
|
|
6701
6706
|
},
|
|
6702
6707
|
{
|
|
6703
6708
|
"path": "tests/hall_palette.mjs",
|
|
@@ -7037,7 +7042,7 @@
|
|
|
7037
7042
|
{
|
|
7038
7043
|
"path": "tests/onboard.mjs",
|
|
7039
7044
|
"mode": "0000644",
|
|
7040
|
-
"sha256": "
|
|
7045
|
+
"sha256": "6a66477263031b252d6a92d5036304a78e11cbbd9e7230d836c4ec96a9297fdd"
|
|
7041
7046
|
},
|
|
7042
7047
|
{
|
|
7043
7048
|
"path": "tests/onboarding_gate.mjs",
|
|
@@ -179,7 +179,9 @@ or missing ranks fail closed.
|
|
|
179
179
|
**[ADR 0157](adr/<redacted>.md) added the whole operational block here** —
|
|
180
180
|
20 permissions that had been Archon-gated only by ADR 0016's fail-closed default, never by a decision:
|
|
181
181
|
the governance/monitoring **pages** (`/watch`, `/harbor`, `/gate` — `page.view.governance`) and
|
|
182
|
-
`GET /api/gds/sessions/search
|
|
182
|
+
`GET /api/gds/sessions/search` (whose own `/sessions` page had no server gate at all until
|
|
183
|
+
task 1002710 put it behind `page.view.governance` alongside its three group-mates); the
|
|
184
|
+
**dev-box + provisioning fleets** (`GET /boxes[/cost-ledger]`,
|
|
183
185
|
`POST /boxes/:b/close`, `PATCH .../block`, `GET /provisioning/fleet[/cost-ledger]`,
|
|
184
186
|
`POST .../force-teardown`); **security adjudication** (`GET /security/reports`,
|
|
185
187
|
`POST /security/reports/:id/{confirm,dispute,fix,link-fix}`, `/security/docs` CRUD);
|
|
@@ -493,16 +493,17 @@
|
|
|
493
493
|
// Roll) stays hidden so a fresh arrival isn't drowned before they've
|
|
494
494
|
// shipped their first works. The Settings link is the exception (task
|
|
495
495
|
// 1291): a newcomer needs it for first-time setup — CLI re-auth (Settings
|
|
496
|
-
// → CLI Access) and choosing their craft both live there.
|
|
497
|
-
//
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
//
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
496
|
+
// → CLI Access) and choosing their craft both live there. The citizen's
|
|
497
|
+
// primer and the "how the system works" maps are ALSO visible to a Xenos as
|
|
498
|
+
// of task 1002710 — #766/#770 had hidden them on an "action-only path"
|
|
499
|
+
// reading, but they are the onboarding documents, so the reader they were
|
|
500
|
+
// written for was the one being turned away. #770: the Work Board link IS
|
|
501
|
+
// shown to a Xenos — a newcomer needs to find the work they can claim, so the
|
|
502
|
+
// path "find work → claim → ship three" stays one click away; the rank ladder
|
|
503
|
+
// also stays so they can see what they're climbing toward. This is cosmetic
|
|
504
|
+
// UX only — every data endpoint stays server-rank-gated regardless (the Work
|
|
505
|
+
// Board's claimable feed only returns work accessible to the caller's rank).
|
|
506
|
+
// Set early in loadAll so the async karma/acclaim feeds
|
|
506
507
|
// (which un-hide their own scroll on data) respect it; the section-hiding
|
|
507
508
|
// sweep then runs before renderNav so the chip strip matches.
|
|
508
509
|
const NEWCOMER_VISIBLE_SECTIONS = new Set([
|
|
@@ -136,14 +136,22 @@
|
|
|
136
136
|
{ group: 'Community', gate: 'member', items: [
|
|
137
137
|
{ id: 'leaderboard', label: 'Leaderboard', icon: 'leaderboard', href: '/#leaderboard', view: 'leaderboard', gate: 'member' },
|
|
138
138
|
// task 1002577: the cross-project builder directory (/scouting graduated
|
|
139
|
-
// into the hall).
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
139
|
+
// into the hall). Gated to MATCH its data — /api/gds/scouting checks
|
|
140
|
+
// `project.curate`, whose catalog floor is METIC. This read 'archon' until
|
|
141
|
+
// task 1002710: the nav was stricter than the wall, so a Metic could open
|
|
142
|
+
// /people by URL but never saw the link. Cosmetic either way —
|
|
143
|
+
// requireProjectCuratePage is the wall — but a nav that disagrees with the
|
|
144
|
+
// gate teaches the ladder wrong. The module gate hides the tab on
|
|
145
|
+
// instances without the platform-identity hub.
|
|
146
|
+
{ id: 'people', label: 'People', icon: 'people', href: '/people', gate: 'governance', module: 'platform-identity' },
|
|
144
147
|
] },
|
|
145
148
|
{ group: 'Docs', gate: 'signed', items: [
|
|
146
|
-
|
|
149
|
+
// task 1002710: primer + diagrams were gate:'member' (rank !== xenos) to
|
|
150
|
+
// mirror the old thetes+ page gate. That gate is gone — the reading rooms
|
|
151
|
+
// are for the first-session newcomer above all — so the links follow it
|
|
152
|
+
// down to 'signed'. Leaving them at 'member' would have ungated the pages
|
|
153
|
+
// while still hiding them from the only readers the change was for.
|
|
154
|
+
{ id: 'primer', label: 'Primer', icon: 'docs', href: '/primer', gate: 'signed' },
|
|
147
155
|
{ id: 'ranks', label: 'Ranks', icon: 'docs', href: '/ranks', gate: 'signed' },
|
|
148
156
|
// task 2175: /drachmae had no nav entry at all — reachable only from prose
|
|
149
157
|
// links since task 2174 moved the bounty rate card onto it. It is reference
|
|
@@ -153,7 +161,7 @@
|
|
|
153
161
|
// stays right whatever `branding.currency.label` is set to. Page is public
|
|
154
162
|
// like /ranks — 'signed' is the same cosmetic "show it once you're in".
|
|
155
163
|
{ id: 'economy', label: 'Economy', icon: 'docs', href: '/drachmae', gate: 'signed' },
|
|
156
|
-
{ id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: '
|
|
164
|
+
{ id: 'diagrams', label: 'Diagrams', icon: 'docs', href: '/diagrams', gate: 'signed' },
|
|
157
165
|
{ id: 'atlas', label: 'Repo Atlas', icon: 'docs', href: '/atlas', gate: 'member' },
|
|
158
166
|
{ id: 'modules', label: 'Modules', icon: 'docs', href: '/modules', gate: 'member' },
|
|
159
167
|
// The rendered OpenAPI reference lives on the APEX (/docs), not this builders
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.69",
|
|
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.69",
|
|
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.69",
|
|
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",
|
|
@@ -251,6 +251,14 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
251
251
|
}
|
|
252
252
|
.steps b{font-size:15px;}
|
|
253
253
|
.steps p{font-size:13.5px;font-weight:500;color:var(--ink-soft);line-height:1.55;margin-top:4px;}
|
|
254
|
+
/* live checklist states (task 1002694): a step the platform has verified done, and a
|
|
255
|
+
step that can't be performed yet (its prerequisite — usually the domain — is missing) */
|
|
256
|
+
.steps>li.sdone{border-color:#bbe7c8;background:linear-gradient(180deg,#f0fdf4 0%,#fcfffc 100%);}
|
|
257
|
+
.steps>li.sdone::before{content:"✓";background:var(--ok);color:#fff;}
|
|
258
|
+
.steps>li.slocked{opacity:.6;}
|
|
259
|
+
.steps details{margin-top:10px;}
|
|
260
|
+
.steps summary{cursor:pointer;font-weight:700;font-size:12.5px;color:var(--ink-faint);}
|
|
261
|
+
.steps details p{margin-top:8px;}
|
|
254
262
|
.kv{
|
|
255
263
|
display:flex;align-items:center;gap:8px;margin-top:9px;background:var(--bg);border:1.5px solid var(--line);
|
|
256
264
|
border-radius:11px;padding:8px 8px 8px 12px;font-size:12.5px;
|
|
@@ -727,7 +735,10 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
727
735
|
} else if (clash) {
|
|
728
736
|
hint.className = 'hint err'; hint.innerHTML = 'You already have a repo named <strong>' + esc(state.repo) + '</strong> — pick another name, or adopt it instead.';
|
|
729
737
|
} else {
|
|
730
|
-
|
|
738
|
+
/* Visibility stated up front (task 1002694 / idea 1000684): repos WE create are
|
|
739
|
+
public by design (github-repos.js — open-source, capture-resistant); the
|
|
740
|
+
private path is adopting a private repo the owner makes themselves. */
|
|
741
|
+
hint.className = 'hint'; hint.innerHTML = 'We’ll create an empty <strong>public</strong> repo at <code style="font-family:var(--font-mono)">github.com/' + esc(myLogin()) + '/' + esc(state.repo || 'name') + '</code> — projects here are open by design. Want private code? Create the repo yourself on GitHub and “adopt” it instead.';
|
|
731
742
|
}
|
|
732
743
|
}
|
|
733
744
|
$('ghNewName').addEventListener('input', function () { state.repo = this.value.trim(); updateFreshPreview(); saveDraft(); });
|
|
@@ -983,56 +994,203 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
983
994
|
});
|
|
984
995
|
});
|
|
985
996
|
|
|
986
|
-
/* ── done panel: the canonical
|
|
987
|
-
|
|
988
|
-
|
|
997
|
+
/* ── done panel: rendered FROM the canonical onboard plan (task 1002694, gap 2 /
|
|
998
|
+
idea 1000675). GET /provisioning/onboard-plan is the single source the
|
|
999
|
+
/new-project skill + `bongos onboard` read too, so the checklist can't
|
|
1000
|
+
drift. A per-key renderer adds this page's rich UI; an unknown human step
|
|
1001
|
+
falls back to the plan's own label, so a new canonical step surfaces here
|
|
1002
|
+
by default instead of silently vanishing. ── */
|
|
989
1003
|
function copyBtn(text) {
|
|
990
1004
|
return '<button type="button" class="copy" data-copy="' + esc(text) + '">Copy</button>';
|
|
991
1005
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
1006
|
+
var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
|
|
1007
|
+
|
|
1008
|
+
var instanceId = null; // set when a create lands / a resumed watch starts
|
|
1009
|
+
var live = { instStatus: null, manifest: null, manifestMsg: '' };
|
|
1010
|
+
var planPromises = {}; // mode|domain → Promise<steps[]>
|
|
1011
|
+
var lastStepsHtml = '';
|
|
1012
|
+
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'first-sign-in', 'enter-hall'];
|
|
1013
|
+
|
|
1014
|
+
function fetchPlan() {
|
|
1015
|
+
var domain = state.domain || '';
|
|
1016
|
+
var key = state.mode + '|' + domain;
|
|
1017
|
+
if (!planPromises[key]) {
|
|
1018
|
+
planPromises[key] = fetch(API + '/provisioning/onboard-plan?mode=' + encodeURIComponent(state.mode) + (domain ? '&domain=' + encodeURIComponent(domain) : ''))
|
|
1019
|
+
.then(function (res) { if (!res.ok) throw new Error('plan'); return res.json(); })
|
|
1020
|
+
.then(function (d) { return (d && d.steps) || []; })
|
|
1021
|
+
.catch(function () {
|
|
1022
|
+
delete planPromises[key]; // transient — retried on the next render
|
|
1023
|
+
return FALLBACK_HUMAN_KEYS.map(function (k) { return { key: k, kind: 'human', label: '' }; });
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
return planPromises[key];
|
|
1027
|
+
}
|
|
998
1028
|
|
|
999
|
-
|
|
1029
|
+
/* github-repo — adopt / auto-created / manual-create, each aware of whether the
|
|
1030
|
+
scaffold has landed (instance active ⇒ the build already pushed). The wizard
|
|
1031
|
+
files onboard_mode to the server, so for adopt the RUNNER clones + layers
|
|
1032
|
+
(task 1002562) — the owner no longer runs `bongos init --adopt` here. */
|
|
1033
|
+
function renderStepGithubRepo() {
|
|
1034
|
+
var landed = live.instStatus === 'active';
|
|
1000
1035
|
var repoLabel = state.owner && state.repo ? '<strong>' + esc(state.owner + '/' + state.repo) + '</strong>' : 'your new repository';
|
|
1036
|
+
if (state.mode === 'adopt') {
|
|
1037
|
+
return landed
|
|
1038
|
+
? '<li class="sdone" data-onboard-step="github-repo"><b>Your repo carries the platform</b><p>The build layered the platform onto ' + repoLabel + ' — existing files, history, and backlog kept. Pull to see it.</p></li>'
|
|
1039
|
+
: '<li data-onboard-step="github-repo"><b>Your existing repo</b><p>The build clones ' + repoLabel + ' and layers the platform onto its existing history — nothing is created or emptied, and your backlog is imported. Nothing for you to do here.</p></li>';
|
|
1040
|
+
}
|
|
1041
|
+
if (state.repoAutoCreated) {
|
|
1042
|
+
return landed
|
|
1043
|
+
? '<li class="sdone" data-onboard-step="github-repo"><b>Repo ready — scaffold landed</b><p>We created ' + repoLabel + ' (public, by design) and the build pushed your project’s starting files to it.</p></li>'
|
|
1044
|
+
: '<li data-onboard-step="github-repo"><b>Your empty repo is ready</b><p>We created ' + repoLabel + ' on GitHub for you — public (projects here are open by design), empty, and waiting. The scaffold lands there when the build runs; nothing for you to do here.</p></li>';
|
|
1045
|
+
}
|
|
1001
1046
|
var ghNewUrl = githubNewRepoUrl(state.owner, state.repo);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1047
|
+
return '<li' + (landed ? ' class="sdone"' : '') + ' data-onboard-step="github-repo"><b>Create the empty GitHub repo</b><p>Do this first — the build needs the repo to exist. Click below (GitHub opens with the name already filled in), press <strong>Create repository</strong>, and leave it completely empty. Seeing “repository not found” above? That just means the repo isn’t created yet — make it here.</p>' +
|
|
1048
|
+
'<a class="ghbtn" href="' + esc(ghNewUrl) + '" target="_blank" rel="noopener">' + GH_ICON + 'Create ' + repoLabel + ' on GitHub</a></li>';
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/* The manual OAuth-app path stays as a documented fallback (ADR 0133), but the
|
|
1052
|
+
control-plane placement is named as the operator's, never handed to the owner
|
|
1053
|
+
as a command to run (gap 4). */
|
|
1054
|
+
function manualOauthDetails() {
|
|
1055
|
+
var oa = oauthAppValues(state.domain);
|
|
1056
|
+
return '<details><summary>Prefer to create an OAuth app by hand?</summary>' +
|
|
1057
|
+
'<p>GitHub → <em>your account’s</em> Settings → Developer settings → OAuth Apps → New OAuth App. (Developer settings live under your account, not the repo.)</p>' +
|
|
1058
|
+
'<div class="kv"><span class="kk">Homepage URL</span><code>' + esc(oa.homepageUrl) + '</code>' + copyBtn(oa.homepageUrl) + '</div>' +
|
|
1059
|
+
'<div class="kv"><span class="kk">Callback URL</span><code>' + esc(oa.callbackUrl) + '</code>' + copyBtn(oa.callbackUrl) + '</div>' +
|
|
1060
|
+
'<p>The generated credentials are placed on the platform’s control plane by an operator — that part isn’t yours to run (the one-click path above does it all automatically). It’s done when <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> reports <code style="font-family:var(--font-mono)">auth_configured: true</code>.</p></details>';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/* oauth-app — the ADR 0133 one-click GitHub-App manifest flow is the primary path
|
|
1064
|
+
(gap 3 / idea 1000676); locked until a domain exists (the App needs the real
|
|
1065
|
+
callback host — the server refuses domainless with domain_required). */
|
|
1066
|
+
function renderStepOauthApp() {
|
|
1009
1067
|
if (!state.domain) {
|
|
1010
|
-
|
|
1068
|
+
return '<li class="slocked" data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Unlocks once your project has an address — sign-in needs a real domain. Attach one any time from <a href="/projects">your projects page</a>.</p></li>';
|
|
1069
|
+
}
|
|
1070
|
+
if (live.manifest === 'done') {
|
|
1071
|
+
return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(state.domain) + '</code> is live.</p></li>';
|
|
1072
|
+
}
|
|
1073
|
+
if (live.manifest === 'error' || live.manifest === 'expired') {
|
|
1074
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' +
|
|
1075
|
+
(live.manifest === 'expired' ? 'That setup link expired (they last 30 minutes) — mint a fresh one.' : 'GitHub setup hit a snag' + (live.manifestMsg ? ': ' + esc(live.manifestMsg) : '') + '. Try again.') + '</p>' +
|
|
1076
|
+
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
1077
|
+
}
|
|
1078
|
+
if (live.manifest) { // pending / code_received — GitHub or the runner is mid-flight
|
|
1079
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>Waiting on GitHub — approve the app in the tab that opened. Once approved, the platform places the credentials and verifies sign-in on its own; this step ticks itself.</p></li>';
|
|
1011
1080
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
steps.push('<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '</code> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>');
|
|
1020
|
-
steps.push('<li><b>Verify</b><p><code style="font-family:var(--font-mono)">' + esc(oa.homepageUrl) + '/healthz</code> should report <code style="font-family:var(--font-mono)">auth_configured: true</code>. Then the hall is yours.</p></li>');
|
|
1081
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in — one click</b><p>Approve a pre-filled GitHub App and the platform wires sign-in by itself: no keys to copy, and the secret never leaves the server side.</p>' +
|
|
1082
|
+
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function renderStepFirstSignIn() {
|
|
1086
|
+
if (!state.domain) {
|
|
1087
|
+
return '<li class="slocked" data-onboard-step="first-sign-in"><b>First sign-in</b><p>Unlocks with sign-in above — you’ll open your project’s address, sign in with GitHub once, and be seated as the owner (the founding Archon).</p></li>';
|
|
1021
1088
|
}
|
|
1022
|
-
|
|
1089
|
+
return '<li data-onboard-step="first-sign-in"><b>First sign-in</b><p>Open <a href="https://' + esc(state.domain) + '" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(state.domain) + '</code></a> and sign in with GitHub once. That seats you as the owner — the founding Archon.</p></li>';
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function renderStepEnterHall() {
|
|
1093
|
+
return state.domain
|
|
1023
1094
|
? '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>Your project’s home — open <a href="https://' + esc(state.domain) + '/builders" target="_blank" rel="noopener"><code style="font-family:var(--font-mono)">' + esc(state.domain) + '/builders</code></a> and start building.</p></li>'
|
|
1024
|
-
: '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>'
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1095
|
+
: '<li data-onboard-step="enter-hall"><b>Enter your builders hall</b><p>No address yet — run <code style="font-family:var(--font-mono)">bongos dev</code> in your repo checkout to start the local hall on your machine.</p></li>';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
var STEP_RENDERERS = {
|
|
1099
|
+
'github-repo': renderStepGithubRepo,
|
|
1100
|
+
'oauth-app': renderStepOauthApp,
|
|
1101
|
+
'first-sign-in': renderStepFirstSignIn,
|
|
1102
|
+
'enter-hall': renderStepEnterHall
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1105
|
+
function renderSteps() {
|
|
1106
|
+
fetchPlan().then(function (steps) {
|
|
1107
|
+
var html = steps.filter(function (s) { return s && s.kind === 'human'; }).map(function (s) {
|
|
1108
|
+
var r = STEP_RENDERERS[s.key];
|
|
1109
|
+
return r ? r(s) : '<li data-onboard-step="' + esc(s.key) + '"><b>' + esc(s.label || s.key) + '</b></li>';
|
|
1110
|
+
}).join('');
|
|
1111
|
+
if (html === lastStepsHtml) return; // change-only re-render — polls never clobber open UI
|
|
1112
|
+
lastStepsHtml = html;
|
|
1113
|
+
$('humanSteps').innerHTML = html;
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function renderDone() {
|
|
1118
|
+
var adopt = state.mode === 'adopt';
|
|
1119
|
+
$('doneLede').innerHTML = 'Provisioning is requested for <strong>' + esc(state.name || state.slug) + '</strong>' +
|
|
1120
|
+
(adopt ? ' — adopting your existing repo' : '') +
|
|
1121
|
+
'. The platform stands up the server, database, and address on its own — you can watch its live status right here.';
|
|
1122
|
+
renderSteps();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/* One delegated listener — renderSteps swaps innerHTML, so handlers live on the list. */
|
|
1126
|
+
$('humanSteps').addEventListener('click', function (ev) {
|
|
1127
|
+
var mBtn = ev.target.closest('[data-action="manifest-start"]');
|
|
1128
|
+
if (mBtn) { startManifest(mBtn); return; }
|
|
1129
|
+
var t = ev.target.closest('.copy');
|
|
1130
|
+
if (!t) return;
|
|
1131
|
+
navigator.clipboard.writeText(t.getAttribute('data-copy')).then(function () {
|
|
1132
|
+
t.classList.add('did'); t.textContent = 'Copied';
|
|
1133
|
+
setTimeout(function () { t.classList.remove('did'); t.textContent = 'Copy'; }, 1600);
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
/* ── GitHub-App manifest flow (ADR 0133): mint a one-time state for OUR instance,
|
|
1138
|
+
form-POST the pre-filled manifest to GitHub in a new tab (opened inside the
|
|
1139
|
+
click gesture so popup blockers allow it; blocked → same-tab, the callback
|
|
1140
|
+
page carries the rest), then poll the public status until the control-plane
|
|
1141
|
+
runner has placed + verified the credentials. ── */
|
|
1142
|
+
var manifestPollTimer = null;
|
|
1143
|
+
function stopManifestPoll() { if (manifestPollTimer) { clearTimeout(manifestPollTimer); manifestPollTimer = null; } }
|
|
1144
|
+
|
|
1145
|
+
function startManifest(btn) {
|
|
1146
|
+
if (!instanceId) return;
|
|
1147
|
+
btn.disabled = true; btn.textContent = 'Preparing GitHub…';
|
|
1148
|
+
var tab = window.open('about:blank', 'ghAppSetup');
|
|
1149
|
+
fetch(API + '/provisioning/instances/' + encodeURIComponent(instanceId) + '/github-app', {
|
|
1150
|
+
method: 'POST', credentials: 'same-origin',
|
|
1151
|
+
headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
1152
|
+
}).then(function (res) {
|
|
1153
|
+
if (res.status === 401) { window.location.assign(API + '/auth/web/start?return=/create'); throw new Error('signing in'); }
|
|
1154
|
+
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
1155
|
+
if (!res.ok) {
|
|
1156
|
+
var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
|
|
1157
|
+
throw e;
|
|
1158
|
+
}
|
|
1159
|
+
return d;
|
|
1032
1160
|
});
|
|
1161
|
+
}).then(function (d) {
|
|
1162
|
+
var form = document.createElement('form');
|
|
1163
|
+
form.method = 'POST'; form.action = d.post_url; form.target = tab ? 'ghAppSetup' : '_self';
|
|
1164
|
+
var inp = document.createElement('input');
|
|
1165
|
+
inp.type = 'hidden'; inp.name = 'manifest'; inp.value = JSON.stringify(d.manifest);
|
|
1166
|
+
form.appendChild(inp); document.body.appendChild(form);
|
|
1167
|
+
form.submit(); form.remove();
|
|
1168
|
+
live.manifest = 'pending'; live.manifestMsg = '';
|
|
1169
|
+
renderSteps();
|
|
1170
|
+
pollManifest(d.state);
|
|
1171
|
+
}).catch(function (err) {
|
|
1172
|
+
if (String(err && err.message) === 'signing in') return;
|
|
1173
|
+
if (tab) { try { tab.close(); } catch (e) {} }
|
|
1174
|
+
live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
1175
|
+
renderSteps();
|
|
1033
1176
|
});
|
|
1034
1177
|
}
|
|
1035
1178
|
|
|
1179
|
+
function pollManifest(stateToken) {
|
|
1180
|
+
stopManifestPoll();
|
|
1181
|
+
function tick() {
|
|
1182
|
+
fetch(API + '/provisioning/github-app/status?state=' + encodeURIComponent(stateToken))
|
|
1183
|
+
.then(function (res) { if (!res.ok) throw new Error('soft'); return res.json(); })
|
|
1184
|
+
.then(function (d) {
|
|
1185
|
+
var next = d.ready ? 'done' : (d.status || 'pending');
|
|
1186
|
+
if (next !== live.manifest) { live.manifest = next; renderSteps(); }
|
|
1187
|
+
if (!d.terminal) manifestPollTimer = setTimeout(tick, 4000);
|
|
1188
|
+
})
|
|
1189
|
+
.catch(function () { manifestPollTimer = setTimeout(tick, 8000); });
|
|
1190
|
+
}
|
|
1191
|
+
tick();
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1036
1194
|
/* ── live build progress (task 2083): poll the instance until it's up.
|
|
1037
1195
|
Read-only — GET /provisioning/instances/:id (own-scoped) every 5s. ── */
|
|
1038
1196
|
var pollTimer = null;
|
|
@@ -1076,6 +1234,7 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1076
1234
|
}
|
|
1077
1235
|
function startProgress(id) {
|
|
1078
1236
|
if (!id) return;
|
|
1237
|
+
instanceId = id; // the manifest flow mints against this instance
|
|
1079
1238
|
stopPolling();
|
|
1080
1239
|
var ticks = 0;
|
|
1081
1240
|
var el = $('liveProgress');
|
|
@@ -1100,7 +1259,12 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1100
1259
|
return res.json();
|
|
1101
1260
|
})
|
|
1102
1261
|
.then(function (data) {
|
|
1103
|
-
var
|
|
1262
|
+
var inst = data.instance;
|
|
1263
|
+
/* the checklist reflects live state (task 1002694): a status transition —
|
|
1264
|
+
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
1265
|
+
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
1266
|
+
if (inst && inst.status !== live.instStatus) { live.instStatus = inst.status; renderSteps(); }
|
|
1267
|
+
var terminal = renderProgress(inst);
|
|
1104
1268
|
if (!terminal) pollTimer = setTimeout(tick, 5000);
|
|
1105
1269
|
else stopPolling();
|
|
1106
1270
|
})
|
|
@@ -1124,6 +1288,10 @@ input[type=text].bad{border-color:var(--err);}
|
|
|
1124
1288
|
again.type = 'button'; again.className = 'again'; again.textContent = 'Start another project';
|
|
1125
1289
|
again.addEventListener('click', function () {
|
|
1126
1290
|
stopPolling();
|
|
1291
|
+
stopManifestPoll();
|
|
1292
|
+
instanceId = null;
|
|
1293
|
+
live = { instStatus: null, manifest: null, manifestMsg: '' };
|
|
1294
|
+
lastStepsHtml = '';
|
|
1127
1295
|
try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
1128
1296
|
state = { step: 1, name: '', mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '' };
|
|
1129
1297
|
$('f-name').value = ''; $('f-owner').value = ''; $('f-repo').value = ''; $('f-slug').value = ''; $('f-domain').value = '';
|
|
@@ -696,6 +696,13 @@ function mountInternalSurfaces(app) {
|
|
|
696
696
|
const WATCH_PAGE_RE = /^\/builders\/watch(?:\.html)?\/?$/;
|
|
697
697
|
const HARBOR_PAGE_RE = /^\/builders\/harbor(?:\.html)?\/?$/;
|
|
698
698
|
const GATE_PAGE_RE = /^\/builders\/gate(?:\.html)?\/?$/;
|
|
699
|
+
// /sessions (the session-log ledger) belongs to that same Governance group and
|
|
700
|
+
// its data route (`session.search`) carries the same metic floor since ADR 0157
|
|
701
|
+
// — but it had NO page gate at all: sessions.js only called showSealed() after
|
|
702
|
+
// /api/bongos/me, so the shell was served to anyone who asked for the URL. The
|
|
703
|
+
// data never leaked (the route gate held), but the page was the one item in the
|
|
704
|
+
// group without a wall. Gated here so the group is uniform (task 1002710).
|
|
705
|
+
const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
|
|
699
706
|
// The /governance roles-and-permissions tab (BV1.R113) sits one floor higher:
|
|
700
707
|
// the `governance.manage` atom itself (archon), the same key its data routes
|
|
701
708
|
// check — requireGovernanceManagePage, not the metic-floored gate above.
|
|
@@ -704,17 +711,25 @@ function mountInternalSurfaces(app) {
|
|
|
704
711
|
// exact permission its data route (GET /scouting) checks — same posture as the
|
|
705
712
|
// /governance tab above (the page can never drift wider than the API it fronts).
|
|
706
713
|
const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
|
|
714
|
+
// /scouting is the LEGACY twin of /people — the same /api/gds/scouting directory,
|
|
715
|
+
// left behind when task 1002577 graduated it into the hall. It has no nav entry
|
|
716
|
+
// and its own header claims "Archon-only", but it was never page-gated: the page
|
|
717
|
+
// rendered for anyone and leaned entirely on the data route's 403. Gated on the
|
|
718
|
+
// same atom as /people so the two can't drift apart (task 1002710). Deleting it
|
|
719
|
+
// in favour of /people is a separate call — it is walled, not removed, here.
|
|
720
|
+
const SCOUTING_PAGE_RE = /^\/builders\/scouting(?:\.html)?\/?$/;
|
|
707
721
|
app.use((req, res, next) => {
|
|
708
722
|
if (isModuleSurfaceHost(req)) return next();
|
|
709
723
|
if (!req.path.startsWith('/builders')) return next();
|
|
710
724
|
const norm = path.posix.normalize(req.path);
|
|
711
|
-
if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
|
|
725
|
+
if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
|
|
726
|
+
|| SESSIONS_PAGE_RE.test(norm)) {
|
|
712
727
|
return gdsAuth.requireGovernancePage(req, res, next);
|
|
713
728
|
}
|
|
714
729
|
if (GOVERNANCE_PAGE_RE.test(norm)) {
|
|
715
730
|
return gdsAuth.requireGovernanceManagePage(req, res, next);
|
|
716
731
|
}
|
|
717
|
-
if (PEOPLE_PAGE_RE.test(norm)) {
|
|
732
|
+
if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
|
|
718
733
|
return gdsAuth.requireProjectCuratePage(req, res, next);
|
|
719
734
|
}
|
|
720
735
|
next();
|
|
@@ -820,24 +835,28 @@ function mountInternalSurfaces(app) {
|
|
|
820
835
|
// (style.css, *.js, fonts, cursors), the not-ready notice, and the sign-in flow
|
|
821
836
|
// itself stay PUBLIC so the landing card + GitHub button render for a stranger.
|
|
822
837
|
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
838
|
+
// ONE tier of gate: ANY SIGNED-IN BUILDER — the Work Board, Roadmap, the Goals
|
|
839
|
+
// page, the task-detail page, the Repo Atlas, the Dev Box pair page, Settings
|
|
840
|
+
// (#748, #904, task 1293, task 1757, task 751) and, since task 1002710, the
|
|
841
|
+
// orientation reading rooms (primer, diagrams). A Xenos IS allowed; an
|
|
842
|
+
// unauthenticated visitor is bounced into sign-in rather than served the shell.
|
|
843
|
+
// Settings is in here (not gated higher) because a newcomer needs it for
|
|
844
|
+
// first-time setup — CLI re-auth and choosing their craft both live there
|
|
845
|
+
// (task 1291) — while still staying behind sign-in: it shows personal prefs
|
|
846
|
+
// and mints CLI tokens.
|
|
847
|
+
//
|
|
848
|
+
// The reading rooms USED to sit a tier above this, behind requireNonXenosPage
|
|
849
|
+
// (page.view.builder, thetes+), on #770's reasoning that a Xenos's path is
|
|
850
|
+
// action-only. That got it backwards: the citizen's primer and the onboarding
|
|
851
|
+
// diagrams are written FOR the first-session newcomer, and gating them at
|
|
852
|
+
// thetes redirected exactly that reader to /builders/not-ready. Owner's call
|
|
853
|
+
// (task 1002710) — the RANK floor is gone; sign-in is unchanged.
|
|
854
|
+
// The link-hiding in shell.js is only cosmetic; these are the real gates.
|
|
855
|
+
const SIGNEDIN_BUILDERS_PAGE_RE =
|
|
856
|
+
/^\/builders\/(?:work|pair|settings|atlas|modules|goals|roadmap|primer(?:\.md)?|diagrams)(?:\.html)?\/?$/;
|
|
857
|
+
// The diagram image assets under /builders/diagrams/ ride the same tier as the
|
|
858
|
+
// page that embeds them.
|
|
859
|
+
const SIGNEDIN_BUILDERS_PREFIX_RE = /^\/builders\/diagrams\//;
|
|
841
860
|
app.use((req, res, next) => {
|
|
842
861
|
if (isModuleSurfaceHost(req)) return next();
|
|
843
862
|
// Cheap early-out: only /builders/* paths can ever be gated, so skip the
|
|
@@ -850,10 +869,8 @@ function mountInternalSurfaces(app) {
|
|
|
850
869
|
// closes that bypass so the gate holds at the application layer, not just
|
|
851
870
|
// behind the proxy's path normalization.
|
|
852
871
|
const norm = path.posix.normalize(req.path);
|
|
853
|
-
if (
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || TASK_DETAIL_PAGE_RE.test(norm)) {
|
|
872
|
+
if (SIGNEDIN_BUILDERS_PAGE_RE.test(norm) || SIGNEDIN_BUILDERS_PREFIX_RE.test(norm)
|
|
873
|
+
|| TASK_DETAIL_PAGE_RE.test(norm)) {
|
|
857
874
|
return gdsAuth.requireBuilderPage(req, res, next);
|
|
858
875
|
}
|
|
859
876
|
next();
|
package/src/module-api.js
CHANGED
|
@@ -561,8 +561,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
561
561
|
// landed since 1.19.65 with no explicit bump. run 31599966075. (task 1002620)
|
|
562
562
|
// 1.19.67 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
563
563
|
// landed since 1.19.66 with no explicit bump. run 31605591253. (task 1002620)
|
|
564
|
+
// 1.19.68 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
565
|
+
// landed since 1.19.67 with no explicit bump. run 31608183268. (task 1002620)
|
|
566
|
+
// 1.19.69 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
567
|
+
// landed since 1.19.68 with no explicit bump. run 31608503813. (task 1002620)
|
|
564
568
|
// ---------------------------------------------------------------------------
|
|
565
|
-
const CORE_VERSION = '1.19.
|
|
569
|
+
const CORE_VERSION = '1.19.69'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
566
570
|
|
|
567
571
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
568
572
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -134,8 +134,9 @@ test('sidebar groups read by audience, in order', () => {
|
|
|
134
134
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
135
135
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
136
136
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
|
137
|
-
//
|
|
138
|
-
//
|
|
137
|
+
// Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
|
|
138
|
+
// so the nav entry reads 'governance' since task 1002710 (it said 'archon', which
|
|
139
|
+
// was stricter than the wall). requireProjectCuratePage is the wall server-side.
|
|
139
140
|
assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard', 'people'], 'Community = everyone else');
|
|
140
141
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
141
142
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// tests/hall_page_gate_map.mjs
|
|
2
|
+
//
|
|
3
|
+
// GUARD (task 1002710): pins WHICH page gate each hall URL routes to.
|
|
4
|
+
//
|
|
5
|
+
// tests/governance_page_gates.mjs proves each gate FUNCTION admits the right
|
|
6
|
+
// audience. Nothing proved the other half — that a given /builders/* URL is
|
|
7
|
+
// wired to the gate it is supposed to be wired to. That gap is not theoretical:
|
|
8
|
+
// /sessions and /scouting each shipped with NO page gate at all, relying on a
|
|
9
|
+
// client-side showSealed() and a data-route 403 respectively, and no test
|
|
10
|
+
// noticed. A page whose shell is served to everyone is not a data leak while
|
|
11
|
+
// its API holds, but it is a wall that exists only in the reviewer's head.
|
|
12
|
+
//
|
|
13
|
+
// It parses serve-internal.js STATICALLY (no import — that pulls the whole app),
|
|
14
|
+
// in the style of tests/served_paths_shipped.mjs: pull the `*_PAGE_RE` literals
|
|
15
|
+
// out of the source, pull the `if (…) return gdsAuth.<gate>(…)` dispatch out of
|
|
16
|
+
// the same source, and run the oracle's URLs through the reconstructed map.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/hall_page_gate_map.mjs
|
|
19
|
+
|
|
20
|
+
import { test } from 'node:test';
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { posix as posixPath } from 'node:path';
|
|
26
|
+
|
|
27
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'utf8');
|
|
29
|
+
|
|
30
|
+
// ═══ the ORACLE — hand-written, one row per gated hall URL ═══════════════════
|
|
31
|
+
// `null` means "no page gate": the shell is served to anyone who asks. That is
|
|
32
|
+
// correct for the genuinely public pages and a BUG anywhere else, so every such
|
|
33
|
+
// row is spelled out rather than left to a default.
|
|
34
|
+
//
|
|
35
|
+
// Audiences (the floors live in modules/governance/catalog.js; the gate
|
|
36
|
+
// functions in src/bongos/auth.js):
|
|
37
|
+
// requireBuilderPage page.view.public any signed-in builder (xenos+)
|
|
38
|
+
// requireGovernancePage page.view.governance metic+ (ADR 0157)
|
|
39
|
+
// requireProjectCuratePage project.curate metic+
|
|
40
|
+
// requireGovernanceManagePage governance.manage archon
|
|
41
|
+
const ORACLE = {
|
|
42
|
+
// Any signed-in builder — no rank floor.
|
|
43
|
+
'/builders/work': 'requireBuilderPage',
|
|
44
|
+
'/builders/pair': 'requireBuilderPage',
|
|
45
|
+
'/builders/settings': 'requireBuilderPage',
|
|
46
|
+
'/builders/atlas': 'requireBuilderPage',
|
|
47
|
+
'/builders/modules': 'requireBuilderPage',
|
|
48
|
+
'/builders/goals': 'requireBuilderPage',
|
|
49
|
+
'/builders/roadmap': 'requireBuilderPage',
|
|
50
|
+
// task 1002710: the orientation reading rooms moved DOWN to this tier from
|
|
51
|
+
// requireNonXenosPage (thetes+). They are the onboarding documents, so gating
|
|
52
|
+
// them above xenos turned away the exact reader they were written for. The
|
|
53
|
+
// sign-in requirement is unchanged — only the RANK floor is gone.
|
|
54
|
+
'/builders/primer': 'requireBuilderPage',
|
|
55
|
+
'/builders/primer.md': 'requireBuilderPage',
|
|
56
|
+
'/builders/diagrams': 'requireBuilderPage',
|
|
57
|
+
'/builders/diagrams/02-b.png': 'requireBuilderPage',
|
|
58
|
+
|
|
59
|
+
// Metic+ operator surfaces (the Governance nav group).
|
|
60
|
+
'/builders/watch': 'requireGovernancePage',
|
|
61
|
+
'/builders/harbor': 'requireGovernancePage',
|
|
62
|
+
'/builders/gate': 'requireGovernancePage',
|
|
63
|
+
'/builders/sessions': 'requireGovernancePage',
|
|
64
|
+
|
|
65
|
+
// Metic+ via the `project.curate` atom — the exact permission GET /scouting
|
|
66
|
+
// checks, so neither page can drift wider than the API it fronts.
|
|
67
|
+
'/builders/people': 'requireProjectCuratePage',
|
|
68
|
+
'/builders/scouting': 'requireProjectCuratePage',
|
|
69
|
+
|
|
70
|
+
// Archon.
|
|
71
|
+
'/builders/governance': 'requireGovernanceManagePage',
|
|
72
|
+
|
|
73
|
+
// Deliberately UNGATED — the landing card, the rank ladder, the reward
|
|
74
|
+
// schedule and a builder's public portfolio all render for a stranger.
|
|
75
|
+
'/builders/': null,
|
|
76
|
+
'/builders/ranks': null,
|
|
77
|
+
'/builders/drachmae': null,
|
|
78
|
+
'/builders/profile': null,
|
|
79
|
+
'/builders/not-ready': null,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// ─── reconstruct the URL→gate map from the source ───────────────────────────
|
|
83
|
+
// 1. every `const NAME_RE = /…/;` (the page-matching regexes)
|
|
84
|
+
function readRegexes(src) {
|
|
85
|
+
const out = new Map();
|
|
86
|
+
const re = /const\s+([A-Z0-9_]+_RE)\s*=\s*(\/(?:\\.|\[(?:\\.|[^\]])*\]|[^/\\])+\/[gimsuy]*)\s*;/g;
|
|
87
|
+
for (const m of src.matchAll(re)) out.set(m[1], eval(m[2])); // eslint-disable-line no-eval
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. every `if (<condition>) { return gdsAuth.<gate>(req, res, next); }`
|
|
92
|
+
function readDispatch(src) {
|
|
93
|
+
const out = [];
|
|
94
|
+
// `[^{};]` keeps the condition capture INSIDE one if-header. A permissive
|
|
95
|
+
// `[^]*?` looks lazy enough but will happily start at some earlier `if (` and
|
|
96
|
+
// swallow every intervening line, hoovering up unrelated *_RE names and
|
|
97
|
+
// mapping them all to the first gate it reaches.
|
|
98
|
+
const re = /if\s*\(([^{};]*?)\)\s*\{\s*return\s+gdsAuth\.(\w+)\(req,\s*res,\s*next\);/g;
|
|
99
|
+
for (const m of src.matchAll(re)) {
|
|
100
|
+
const names = [...m[1].matchAll(/([A-Z0-9_]+_RE)/g)].map((x) => x[1]);
|
|
101
|
+
if (names.length) out.push({ names, gate: m[2] });
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const REGEXES = readRegexes(SRC);
|
|
107
|
+
const DISPATCH = readDispatch(SRC);
|
|
108
|
+
|
|
109
|
+
// The gate a URL resolves to, mirroring the middleware: first matching branch wins.
|
|
110
|
+
function gateFor(urlPath) {
|
|
111
|
+
const norm = posixPath.normalize(urlPath);
|
|
112
|
+
for (const { names, gate } of DISPATCH) {
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const rx = REGEXES.get(name);
|
|
115
|
+
if (rx && rx.test(norm)) return gate;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
test('the static parse actually found the gate wiring', () => {
|
|
122
|
+
assert.ok(REGEXES.size >= 8, `expected the *_PAGE_RE literals, found ${REGEXES.size}`);
|
|
123
|
+
assert.ok(DISPATCH.length >= 3, `expected the gdsAuth dispatch branches, found ${DISPATCH.length}`);
|
|
124
|
+
// If this file is ever restructured past the parser, these two fail LOUDLY
|
|
125
|
+
// rather than letting every row below pass vacuously against an empty map.
|
|
126
|
+
assert.ok(DISPATCH.some((d) => d.gate === 'requireBuilderPage'));
|
|
127
|
+
assert.ok(DISPATCH.some((d) => d.gate === 'requireGovernancePage'));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('every hall URL routes to the page gate the oracle names', () => {
|
|
131
|
+
for (const [urlPath, expected] of Object.entries(ORACLE)) {
|
|
132
|
+
assert.equal(gateFor(urlPath), expected,
|
|
133
|
+
`${urlPath} → ${gateFor(urlPath) ?? 'NO GATE'} (expected ${expected ?? 'NO GATE'})`);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('the .html twin of a gated page is gated identically', () => {
|
|
138
|
+
// express.static resolves /builders/watch from watch.html via extensions:['html'],
|
|
139
|
+
// so the explicit .html form must not slip past the gate the bare form carries.
|
|
140
|
+
for (const [urlPath, expected] of Object.entries(ORACLE)) {
|
|
141
|
+
if (expected === null || urlPath.includes('.')) continue;
|
|
142
|
+
assert.equal(gateFor(`${urlPath}.html`), expected, `${urlPath}.html must match ${urlPath}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('path-normalization tricks do not slip past a gate', () => {
|
|
147
|
+
// The middleware normalizes before matching precisely so these forms — which
|
|
148
|
+
// express.static WILL resolve to the same file — cannot bypass an anchored RE.
|
|
149
|
+
for (const [urlPath, expected] of Object.entries(ORACLE)) {
|
|
150
|
+
if (expected === null) continue;
|
|
151
|
+
const tail = urlPath.replace(/^\/builders\//, '');
|
|
152
|
+
for (const sneaky of [`/builders//${tail}`, `/builders/./${tail}`]) {
|
|
153
|
+
assert.equal(gateFor(sneaky), expected, `${sneaky} must resolve to ${expected}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('no gated page is left matching only a REMOVED gate function', () => {
|
|
159
|
+
// requireNonXenosPage still EXISTS in auth.js (page.view.builder is a live
|
|
160
|
+
// catalog atom an Archon can attach to a custom role) but task 1002710 left it
|
|
161
|
+
// guarding nothing. If someone re-points a URL at it, that is a deliberate act
|
|
162
|
+
// that should update this oracle — not something to happen silently.
|
|
163
|
+
const wired = new Set(DISPATCH.map((d) => d.gate));
|
|
164
|
+
assert.ok(!wired.has('requireNonXenosPage'),
|
|
165
|
+
'requireNonXenosPage guards no hall URL since task 1002710 — update the ORACLE if that changes');
|
|
166
|
+
});
|
package/tests/onboard.mjs
CHANGED
|
@@ -93,6 +93,23 @@ test('every render surface carries the terminal hall step (task 1002703)', () =>
|
|
|
93
93
|
assert.match(enterHall.join(''), /bongos dev/, '/create offers the local hall when no domain');
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
+
test('the /create done panel is plan-driven, manifest-driven, and owner-honest (task 1002694)', () => {
|
|
97
|
+
// Gaps 2/3/4/14 of the project-creation audit (docs/audits/project-creation-gap-list.md).
|
|
98
|
+
const { readFileSync } = require('node:fs');
|
|
99
|
+
const path = require('node:path');
|
|
100
|
+
const src = readFileSync(path.join(import.meta.dirname, '..', 'public-landing/create.html'), 'utf8');
|
|
101
|
+
// gap 2 (idea 1000675): the checklist renders FROM the canonical plan endpoint
|
|
102
|
+
assert.match(src, /provisioning\/onboard-plan/, 'the done panel fetches the canonical plan');
|
|
103
|
+
// gap 3 (idea 1000676): the ADR 0133 one-click manifest flow has a UI caller again
|
|
104
|
+
assert.match(src, /\/github-app/, 'the done panel drives the GitHub-App manifest flow');
|
|
105
|
+
assert.match(src, /github-app\/status/, 'and polls its progress to reflect live state');
|
|
106
|
+
// gap 4: the operator-only control-plane command never reaches a new owner
|
|
107
|
+
assert.doesNotMatch(src, /oauth-secret place/, 'owners are never handed the control-plane placement command');
|
|
108
|
+
// gap 14 (idea 1000684): repo visibility is stated, with the private path named
|
|
109
|
+
assert.match(src, /public/, 'visibility is stated');
|
|
110
|
+
assert.match(src, /adopt/i, 'the private path (adopt your own private repo) is named');
|
|
111
|
+
});
|
|
112
|
+
|
|
96
113
|
test('oauthAppInstructions pins the apex callback + homepage and points at ACCOUNT Developer settings', () => {
|
|
97
114
|
const oa = ob.oauthAppInstructions('mercury.example.com');
|
|
98
115
|
assert.equal(oa.homepageUrl, 'https://mercury.example.com');
|
|
@@ -154,7 +171,10 @@ test('the web form (/create) declares EXACTLY the canonical human steps — drif
|
|
|
154
171
|
};
|
|
155
172
|
for (const [name, rel] of Object.entries(surfaces)) {
|
|
156
173
|
const src = readFileSync(path.join(import.meta.dirname, '..', rel), 'utf8');
|
|
157
|
-
|
|
174
|
+
// keys are kebab-case literals; the charset excludes the page's DYNAMIC fallback
|
|
175
|
+
// tag (data-onboard-step="' + esc(s.key) + '"), which renders unknown plan steps
|
|
176
|
+
// at runtime and has no literal key to pin here (task 1002694).
|
|
177
|
+
const found = [...src.matchAll(/data-onboard-step="([a-z0-9-]+)"/g)].map((m) => m[1]);
|
|
158
178
|
const uniq = [...new Set(found)].sort();
|
|
159
179
|
assert.deepEqual(uniq, humanKeys, `${name} (${rel}) must tag exactly the canonical human steps [${humanKeys}], found [${uniq}]`);
|
|
160
180
|
}
|