@bongos/core 1.19.185 → 1.19.186
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 +33 -33
- package/clients/bongos-client/index.d.ts +2 -2
- package/docs/api/openapi.json +4 -4
- package/modules/government/catalog.js +4 -4
- package/modules/government/db.js +1 -1
- package/modules/government/routes/government.js +4 -4
- package/modules/government/routes/rank-sync.js +2 -2
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/{governance.css → government.css} +10 -10
- package/modules/hall-ui/public/{governance.html → government.html} +14 -14
- package/modules/hall-ui/public/{governance.js → government.js} +105 -105
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +19 -15
- package/modules/hall-ui/public/watch.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth.js +9 -9
- package/src/bongos/serve-internal.js +8 -8
- package/src/module-api.js +3 -1
- package/tests/auth_resolve_failure.mjs +4 -4
- package/tests/governance_abuse_matrix.mjs +3 -3
- package/tests/governance_page_gates.mjs +9 -9
- package/tests/governance_routes.mjs +4 -4
- package/tests/hall_nav.mjs +8 -7
- package/tests/hall_page_gate_map.mjs +8 -8
|
@@ -751,7 +751,7 @@ function mountInternalSurfaces(app) {
|
|
|
751
751
|
// The /watch monitoring page and the /harbor dev-box management page (idea 257)
|
|
752
752
|
// and the /gate page (task 1179) all sit behind `page.view.government` — Metic
|
|
753
753
|
// and up since ADR 0157. Same server-side page gate, same posture: a builder
|
|
754
|
-
// without the permission never receives the shell (
|
|
754
|
+
// without the permission never receives the shell (requireGovernmentPage — named
|
|
755
755
|
// requireArchonPage before ADR 0157 — redirects them to the hall, and an
|
|
756
756
|
// unauthenticated visitor into sign-in). The subdomain shim above has already
|
|
757
757
|
// rewritten the bare /watch + /harbor + /gate onto /builders/*. (Defense in
|
|
@@ -766,13 +766,13 @@ function mountInternalSurfaces(app) {
|
|
|
766
766
|
// data never leaked (the route gate held), but the page was the one item in the
|
|
767
767
|
// group without a wall. Gated here so the group is uniform (task 1002710).
|
|
768
768
|
const SESSIONS_PAGE_RE = /^\/builders\/sessions(?:\.html)?\/?$/;
|
|
769
|
-
// The /
|
|
769
|
+
// The /government ranks-and-permissions tab (BV1.R113) sits one floor higher:
|
|
770
770
|
// the `government.manage` atom itself (archon), the same key its data routes
|
|
771
|
-
// check —
|
|
772
|
-
const
|
|
771
|
+
// check — requireGovernmentManagePage, not the metic-floored gate above.
|
|
772
|
+
const GOVERNMENT_PAGE_RE = /^\/builders\/government(?:\.html)?\/?$/;
|
|
773
773
|
// The /people builder directory (task 1002577): gated on `project.curate`, the
|
|
774
774
|
// exact permission its data route (GET /scouting) checks — same posture as the
|
|
775
|
-
// /
|
|
775
|
+
// /government tab above (the page can never drift wider than the API it fronts).
|
|
776
776
|
const PEOPLE_PAGE_RE = /^\/builders\/people(?:\.html)?\/?$/;
|
|
777
777
|
// /scouting is the LEGACY twin of /people — the same /api/gds/scouting directory,
|
|
778
778
|
// left behind when task 1002577 graduated it into the hall. It has no nav entry
|
|
@@ -787,10 +787,10 @@ function mountInternalSurfaces(app) {
|
|
|
787
787
|
const norm = path.posix.normalize(req.path);
|
|
788
788
|
if (WATCH_PAGE_RE.test(norm) || HARBOR_PAGE_RE.test(norm) || GATE_PAGE_RE.test(norm)
|
|
789
789
|
|| SESSIONS_PAGE_RE.test(norm)) {
|
|
790
|
-
return gdsAuth.
|
|
790
|
+
return gdsAuth.requireGovernmentPage(req, res, next);
|
|
791
791
|
}
|
|
792
|
-
if (
|
|
793
|
-
return gdsAuth.
|
|
792
|
+
if (GOVERNMENT_PAGE_RE.test(norm)) {
|
|
793
|
+
return gdsAuth.requireGovernmentManagePage(req, res, next);
|
|
794
794
|
}
|
|
795
795
|
if (PEOPLE_PAGE_RE.test(norm) || SCOUTING_PAGE_RE.test(norm)) {
|
|
796
796
|
return gdsAuth.requireProjectCuratePage(req, res, next);
|
package/src/module-api.js
CHANGED
|
@@ -797,8 +797,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
797
797
|
// landed since 1.19.183 with no explicit bump. run 31974847472. (task 1002620)
|
|
798
798
|
// 1.19.185 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
799
799
|
// landed since 1.19.184 with no explicit bump. run 31976564067. (task 1002620)
|
|
800
|
+
// 1.19.186 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
801
|
+
// landed since 1.19.185 with no explicit bump. run 31977207459. (task 1002620)
|
|
800
802
|
// ---------------------------------------------------------------------------
|
|
801
|
-
const CORE_VERSION = '1.19.
|
|
803
|
+
const CORE_VERSION = '1.19.186'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
802
804
|
|
|
803
805
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
804
806
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -93,17 +93,17 @@ test('AUDIT: every async middleware mounted directly on a router owns its reject
|
|
|
93
93
|
// rot: a NEW bare `async function require*` added later fails this test.
|
|
94
94
|
// requireBuilder — the API gate. Guarded here (BV1.R109-follow-up).
|
|
95
95
|
// requireBuilderPage \
|
|
96
|
-
//
|
|
97
|
-
//
|
|
96
|
+
// requireGovernmentPage \
|
|
97
|
+
// requireGovernmentManagePage > all five delegate to runPageGate, guarded by BV1.R107.
|
|
98
98
|
// requireNonXenosPage / (the R113 roles-tab gate; the /people curate gate)
|
|
99
99
|
// requireProjectCuratePage /
|
|
100
100
|
// Anything else async and router-mounted must be added with its own try/catch.
|
|
101
101
|
const src = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'auth.js'), 'utf8');
|
|
102
102
|
const asyncMw = [...src.matchAll(/^async function (require\w+|allow\w+)\s*\(req, res, next\)/gm)].map((m) => m[1]);
|
|
103
|
-
assert.deepEqual(asyncMw.sort(), ['requireBuilder', 'requireBuilderPage', '
|
|
103
|
+
assert.deepEqual(asyncMw.sort(), ['requireBuilder', 'requireBuilderPage', 'requireGovernmentManagePage', 'requireGovernmentPage', 'requireNonXenosPage', 'requireProjectCuratePage'],
|
|
104
104
|
'a new async router middleware appeared — give it a try/catch and add it here');
|
|
105
105
|
// The page gates must actually delegate (that is WHY they are safe).
|
|
106
|
-
for (const name of ['requireBuilderPage', '
|
|
106
|
+
for (const name of ['requireBuilderPage', 'requireGovernmentPage', 'requireGovernmentManagePage', 'requireNonXenosPage', 'requireProjectCuratePage']) {
|
|
107
107
|
const fn = src.slice(src.indexOf(`async function ${name}`));
|
|
108
108
|
assert.match(sliceFn(fn), /runPageGate\(/, `${name} must delegate to the guarded runPageGate`);
|
|
109
109
|
}
|
|
@@ -264,9 +264,9 @@ test('V4: mass-assignment on role create is refused outright (strict schema)', a
|
|
|
264
264
|
test('V4: a clean role create is never system and carries no grants', async () => {
|
|
265
265
|
const r = await call('POST', '/government/ranks', { rank_key: 'sneaky-role', label: 'Sneaky' });
|
|
266
266
|
assert.equal(r.status, 201);
|
|
267
|
-
assert.equal(r.json.
|
|
268
|
-
assert.equal(r.json.
|
|
269
|
-
assert.deepEqual(r.json.
|
|
267
|
+
assert.equal(r.json.rank.is_system, false, 'a created role is never system');
|
|
268
|
+
assert.equal(r.json.rank.seeded_rank, null);
|
|
269
|
+
assert.deepEqual(r.json.rank.permissions, [], 'creation carries no grants');
|
|
270
270
|
const c = calls.find(([k]) => k === 'createRole');
|
|
271
271
|
assert.deepEqual(Object.keys(c[1]).sort(), ['createdBy', 'description', 'label', 'rankKey'],
|
|
272
272
|
'nothing beyond the four whitelisted fields reaches the db');
|
|
@@ -50,13 +50,13 @@ const resolver = require('../modules/government/resolver.js');
|
|
|
50
50
|
const wandering = require('../modules/builder-settings/wandering-prefs.js');
|
|
51
51
|
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
52
52
|
|
|
53
|
-
const GATES = ['requireBuilderPage', 'requireNonXenosPage', '
|
|
53
|
+
const GATES = ['requireBuilderPage', 'requireNonXenosPage', 'requireGovernmentPage'];
|
|
54
54
|
const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
55
55
|
|
|
56
56
|
// ═══ the ORACLE — hand-written, from the rank tests R107 deleted ═════════════
|
|
57
57
|
// requireBuilderPage admitted any session at all → every live rank
|
|
58
58
|
// requireNonXenosPage admitted rank !== 'xenos' → thetes and up
|
|
59
|
-
//
|
|
59
|
+
// requireGovernmentPage admitted rank === 'archon' → archon only …until ADR 0157
|
|
60
60
|
//
|
|
61
61
|
// ADR 0157 (owner decision, 2026-08-05) lowered `page.view.government` archon→metic, so
|
|
62
62
|
// the monitoring pages (/watch, /harbor, /gate) now open to the working rank. That gate
|
|
@@ -71,7 +71,7 @@ const LIVE_RANKS = ['xenos', 'thetes', 'metic', 'archon'];
|
|
|
71
71
|
const PRE_R107_AUDIENCE = {
|
|
72
72
|
requireBuilderPage: { xenos: true, thetes: true, metic: true, archon: true },
|
|
73
73
|
requireNonXenosPage: { xenos: false, thetes: true, metic: true, archon: true },
|
|
74
|
-
|
|
74
|
+
requireGovernmentPage: { xenos: false, thetes: false, metic: true, archon: true },
|
|
75
75
|
};
|
|
76
76
|
// Where a SIGNED-IN builder who fails the gate is sent. Never the sign-in flow —
|
|
77
77
|
// they already have a session, so that would loop. Since task 1002613 (map N-1)
|
|
@@ -81,12 +81,12 @@ const PRE_R107_AUDIENCE = {
|
|
|
81
81
|
const DENIED_TO = {
|
|
82
82
|
requireBuilderPage: '/builders',
|
|
83
83
|
requireNonXenosPage: '/builders/not-ready',
|
|
84
|
-
|
|
84
|
+
requireGovernmentPage: '/builders',
|
|
85
85
|
};
|
|
86
86
|
const GATE_PERMISSION = {
|
|
87
87
|
requireBuilderPage: 'page.view.public',
|
|
88
88
|
requireNonXenosPage: 'page.view.builder',
|
|
89
|
-
|
|
89
|
+
requireGovernmentPage: 'page.view.government',
|
|
90
90
|
};
|
|
91
91
|
const deniedLoc = (gate, { unavailable = false } = {}) =>
|
|
92
92
|
`${DENIED_TO[gate]}?denied=${GATE_PERMISSION[gate]}${unavailable ? '&reason=unavailable' : ''}`;
|
|
@@ -95,7 +95,7 @@ const GATE_FLOOR = {
|
|
|
95
95
|
requireBuilderPage: 'xenos',
|
|
96
96
|
requireNonXenosPage: 'thetes',
|
|
97
97
|
// ADR 0157: `page.view.government` floor archon→metic.
|
|
98
|
-
|
|
98
|
+
requireGovernmentPage: 'metic',
|
|
99
99
|
};
|
|
100
100
|
const SIGN_IN = '/api/gds/auth/web/start';
|
|
101
101
|
|
|
@@ -249,16 +249,16 @@ test('B4: the page gates resolve per request — no memoised authority (ADR 0016
|
|
|
249
249
|
});
|
|
250
250
|
const req = { headers: { cookie: 'gds_session=validtoken' } };
|
|
251
251
|
let admitted = false;
|
|
252
|
-
await auth.
|
|
252
|
+
await auth.requireGovernmentPage(req, fakeRes(), () => { admitted = true; });
|
|
253
253
|
assert.equal(admitted, true, 'an Archon should reach the page');
|
|
254
254
|
keys = []; // the role is revoked between requests
|
|
255
255
|
const second = fakeRes();
|
|
256
256
|
let admittedAgain = false;
|
|
257
|
-
await auth.
|
|
257
|
+
await auth.requireGovernmentPage(req, second, () => { admittedAgain = true; });
|
|
258
258
|
seams._reset();
|
|
259
259
|
assert.equal(calls, 2, 'the resolver must be consulted on every request');
|
|
260
260
|
assert.equal(admittedAgain, false, 'a revoked role must deny on the very next request');
|
|
261
|
-
assert.equal(second._redirect.loc, deniedLoc('
|
|
261
|
+
assert.equal(second._redirect.loc, deniedLoc('requireGovernmentPage'));
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
test('B5: every permission a page gate names is a real catalog key', async () => {
|
|
@@ -189,8 +189,8 @@ test('without government.manage EVERY route 403s (reads included)', async () =>
|
|
|
189
189
|
test('GET /government/ranks returns the role list with grants', async () => {
|
|
190
190
|
const r = await call('GET', '/government/ranks');
|
|
191
191
|
assert.equal(r.status, 200);
|
|
192
|
-
assert.equal(r.json.
|
|
193
|
-
assert.deepEqual(r.json.
|
|
192
|
+
assert.equal(r.json.ranks[0].rank_key, 'metic');
|
|
193
|
+
assert.deepEqual(r.json.ranks[0].permissions, ['task.create']);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
test('GET /government/permissions renders the catalog: grantable flags + principals, no DB call', async () => {
|
|
@@ -221,8 +221,8 @@ test('GET /government/assignments returns builder↔role rows', async () => {
|
|
|
221
221
|
test('POST /government/ranks creates a custom role, created_by = the session builder', async () => {
|
|
222
222
|
const r = await call('POST', '/government/ranks', { rank_key: 'triage-helper', label: 'Triage helper' });
|
|
223
223
|
assert.equal(r.status, 201);
|
|
224
|
-
assert.equal(r.json.
|
|
225
|
-
assert.equal(r.json.
|
|
224
|
+
assert.equal(r.json.rank.rank_key, 'triage-helper');
|
|
225
|
+
assert.equal(r.json.rank.is_system, false);
|
|
226
226
|
const c = calls.find(([k]) => k === 'createRole');
|
|
227
227
|
assert.equal(c[1].createdBy, 42, 'created_by must be the session builder, never input');
|
|
228
228
|
});
|
package/tests/hall_nav.mjs
CHANGED
|
@@ -127,21 +127,22 @@ function hrefResolves(item) {
|
|
|
127
127
|
|
|
128
128
|
test('sidebar groups read by audience, in order', () => {
|
|
129
129
|
const nav = parseNav(boot().sidebarHtml);
|
|
130
|
-
// 'Archon' → 'Governance' in ADR 0157
|
|
130
|
+
// 'Archon' → 'Governance' in ADR 0157 (the group's pages opened to Metic, so a header
|
|
131
|
+
// naming the top rank was wrong), then → 'Government' by ADR 0174: a header
|
|
131
132
|
// reading "Archon" over items a Metic can open would misdescribe the ladder.
|
|
132
|
-
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', '
|
|
133
|
+
assert.deepEqual(nav.map((g) => g.label), ['', 'You', 'Community', 'Docs', 'Government']);
|
|
133
134
|
assert.deepEqual(nav[0].items.map((i) => i.id), ['home', 'roadmap', 'goals', 'work'],
|
|
134
135
|
'the unlabeled group is the project itself (task 1002425 order)');
|
|
135
136
|
assert.deepEqual(nav[1].items.map((i) => i.id), ['standing', 'profile'], 'You = your own numbers');
|
|
136
137
|
// People (task 1002577): the /scouting directory graduated into the hall.
|
|
137
138
|
// Gated cosmetically to match its data endpoint — `project.curate`, floor METIC,
|
|
138
|
-
// so the nav entry reads '
|
|
139
|
+
// so the nav entry reads 'government' since ADR 0174 (it said 'archon', then
|
|
139
140
|
// was stricter than the wall). requireProjectCuratePage is the wall server-side.
|
|
140
141
|
assert.deepEqual(nav[2].items.map((i) => i.id), ['leaderboard', 'people'], 'Community = everyone else');
|
|
141
142
|
assert.deepEqual(nav[3].items.map((i) => i.id),
|
|
142
143
|
['primer', 'ranks', 'economy', 'diagrams', 'atlas', 'modules', 'apidocs'], 'Docs = reference material');
|
|
143
|
-
// '
|
|
144
|
-
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', '
|
|
144
|
+
// 'government' (the Permissions tab, BV1.R113) is the group's one archon-gated item.
|
|
145
|
+
assert.deepEqual(nav[4].items.map((i) => i.id), ['watch', 'harbor', 'gate', 'sessions', 'government']);
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test('Economy lives in Docs and points at the reward-schedule page', () => {
|
|
@@ -190,11 +191,11 @@ test('Achievements has no nav entry, but #achievements still routes to Standing'
|
|
|
190
191
|
|
|
191
192
|
test('collapsible groups carry a stable storage key (collapse persistence)', () => {
|
|
192
193
|
const html = boot().sidebarHtml;
|
|
193
|
-
// 'archon' → 'governance' follows
|
|
194
|
+
// 'archon' → 'governance' → 'government' follows ADR 0157 then ADR 0174. The slug is the
|
|
194
195
|
// hall-nav-collapsed storage key, so any builder who had collapsed the old "archon"
|
|
195
196
|
// group gets the new one expanded once — cosmetic, and preferable to keeping a stale
|
|
196
197
|
// slug that no longer matches the visible label.
|
|
197
|
-
for (const g of ['you', 'community', 'docs', '
|
|
198
|
+
for (const g of ['you', 'community', 'docs', 'government']) {
|
|
198
199
|
assert.match(html, new RegExp(`data-group="${g}"`), `${g} is collapsible (hall-nav-collapsed key)`);
|
|
199
200
|
}
|
|
200
201
|
assert.ok(!/nav-group--collapsible[^>]*>(?!<button)/.test(html.split('<div class="nav-group')[1] || ''),
|
|
@@ -35,9 +35,9 @@ const SRC = fs.readFileSync(path.join(ROOT, 'src/bongos/serve-internal.js'), 'ut
|
|
|
35
35
|
// Audiences (the floors live in modules/government/catalog.js; the gate
|
|
36
36
|
// functions in src/bongos/auth.js):
|
|
37
37
|
// requireBuilderPage page.view.public any signed-in builder (xenos+)
|
|
38
|
-
//
|
|
38
|
+
// requireGovernmentPage page.view.government metic+ (ADR 0157)
|
|
39
39
|
// requireProjectCuratePage project.curate metic+
|
|
40
|
-
//
|
|
40
|
+
// requireGovernmentManagePage government.manage archon
|
|
41
41
|
const ORACLE = {
|
|
42
42
|
// Any signed-in builder — no rank floor.
|
|
43
43
|
'/builders/work': 'requireBuilderPage',
|
|
@@ -57,10 +57,10 @@ const ORACLE = {
|
|
|
57
57
|
'/builders/diagrams/02-b.png': 'requireBuilderPage',
|
|
58
58
|
|
|
59
59
|
// Metic+ operator surfaces (the Governance nav group).
|
|
60
|
-
'/builders/watch': '
|
|
61
|
-
'/builders/harbor': '
|
|
62
|
-
'/builders/gate': '
|
|
63
|
-
'/builders/sessions': '
|
|
60
|
+
'/builders/watch': 'requireGovernmentPage',
|
|
61
|
+
'/builders/harbor': 'requireGovernmentPage',
|
|
62
|
+
'/builders/gate': 'requireGovernmentPage',
|
|
63
|
+
'/builders/sessions': 'requireGovernmentPage',
|
|
64
64
|
|
|
65
65
|
// Metic+ via the `project.curate` atom — the exact permission GET /scouting
|
|
66
66
|
// checks, so neither page can drift wider than the API it fronts.
|
|
@@ -68,7 +68,7 @@ const ORACLE = {
|
|
|
68
68
|
'/builders/scouting': 'requireProjectCuratePage',
|
|
69
69
|
|
|
70
70
|
// Archon.
|
|
71
|
-
'/builders/
|
|
71
|
+
'/builders/government': 'requireGovernmentManagePage',
|
|
72
72
|
|
|
73
73
|
// Deliberately UNGATED — the landing card, the rank ladder, the reward
|
|
74
74
|
// schedule and a builder's public portfolio all render for a stranger.
|
|
@@ -124,7 +124,7 @@ test('the static parse actually found the gate wiring', () => {
|
|
|
124
124
|
// If this file is ever restructured past the parser, these two fail LOUDLY
|
|
125
125
|
// rather than letting every row below pass vacuously against an empty map.
|
|
126
126
|
assert.ok(DISPATCH.some((d) => d.gate === 'requireBuilderPage'));
|
|
127
|
-
assert.ok(DISPATCH.some((d) => d.gate === '
|
|
127
|
+
assert.ok(DISPATCH.some((d) => d.gate === 'requireGovernmentPage'));
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
test('every hall URL routes to the page gate the oracle names', () => {
|