@bongos/core 1.19.183 → 1.19.185
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 +102 -92
- package/clients/bongos-client/bongos-client.global.js +19 -19
- package/clients/bongos-client/index.cjs +19 -19
- package/clients/bongos-client/index.d.ts +29 -29
- package/clients/bongos-client/index.mjs +19 -19
- package/docs/adr/0157-archon-is-rank-and-identity-only.md +2 -2
- package/docs/api/openapi.json +117 -117
- package/docs/api-reference.md +10 -10
- package/docs/canonical-permissions.md +6 -6
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +19 -19
- package/modules/{governance → government}/catalog.js +8 -8
- package/modules/{governance → government}/config.js +6 -6
- package/modules/{governance → government}/db.js +44 -44
- package/modules/government/migrations/government_001_rename_from_governance.sql +213 -0
- package/modules/government/migrations/government_002_rename_permission_keys.sql +112 -0
- package/modules/government/module.json +22 -0
- package/modules/{governance → government}/protected-surfaces.json +0 -1
- package/modules/{governance/rank-roles.js → government/ranks.js} +18 -18
- package/modules/{governance/routes/governance.js → government/routes/government.js} +109 -84
- package/modules/{governance/routes/role-sync.js → government/routes/rank-sync.js} +7 -7
- package/modules/hall-ui/public/gate.js +1 -1
- package/modules/hall-ui/public/governance.js +38 -38
- package/modules/hall-ui/public/harbor.js +2 -2
- package/modules/hall-ui/public/shell.js +3 -3
- package/modules/hall-ui/public/watch.js +1 -1
- package/modules/lifecycle/db.js +6 -6
- package/modules/lifecycle/routes/gate-approvals.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +11 -1
- package/scripts/gds/migration-namespace.js +15 -3
- package/scripts/gds/publish-manifest.js +2 -3
- package/scripts/gds/seed-governance-tasks.js +3 -3
- package/src/bongos/auth.js +16 -16
- package/src/bongos/db.js +6 -6
- package/src/bongos/module-scope-map.js +7 -22
- package/src/bongos/permission-path-check.js +3 -3
- package/src/bongos/route-rank-check.js +15 -21
- package/src/bongos/serve-internal.js +2 -2
- package/src/module-api.js +7 -3
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/criterion_authoring_gate.mjs +1 -1
- package/tests/governance_abuse_matrix.mjs +38 -38
- package/tests/governance_catalog.mjs +4 -4
- package/tests/governance_claim_eligibility.mjs +1 -1
- package/tests/governance_config.mjs +7 -7
- package/tests/governance_fold.mjs +4 -4
- package/tests/governance_fold_bench.mjs +2 -2
- package/tests/governance_live_proof.mjs +6 -6
- package/tests/governance_ownership_scope.mjs +2 -2
- package/tests/governance_page_gates.mjs +10 -10
- package/tests/governance_parity.mjs +3 -3
- package/tests/governance_principals.mjs +3 -3
- package/tests/governance_protected_surfaces.mjs +44 -19
- package/tests/governance_require_permission.mjs +11 -11
- package/tests/governance_resolver.mjs +4 -4
- package/tests/governance_role_sync.mjs +35 -35
- package/tests/governance_routes.mjs +98 -98
- package/tests/governance_seed.mjs +27 -4
- package/tests/governance_session_scope.mjs +5 -5
- package/tests/governance_vanilla_boot.mjs +4 -4
- package/tests/hall_page_gate_map.mjs +3 -3
- package/tests/memory_isolation.mjs +2 -2
- package/tests/module_loader.mjs +1 -1
- package/tests/onboarding_observability.mjs +2 -2
- package/tests/rank_tier_single_source.mjs +2 -2
- package/tests/route_permission_check.mjs +2 -2
- package/modules/governance/module.json +0 -11
- /package/config/{governance.neutral.json → government.neutral.json} +0 -0
- /package/modules/{governance → government}/CLAUDE.md +0 -0
- /package/modules/{governance → government}/migrations/governance_001_tables.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_002_seed_and_backfill.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_003_task_abandon_metic.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_004_criterion_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_005_resync_builder_rank_roles.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_006_version_create.sql +0 -0
- /package/modules/{governance → government}/migrations/governance_007_delegate_archon_ops_to_metic.sql +0 -0
- /package/modules/{governance → government}/protected-surfaces.js +0 -0
- /package/modules/{governance → government}/resolver.js +0 -0
- /package/modules/{governance → government}/session-scopes.js +0 -0
- /package/modules/{governance → government}/session-scopes.json +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// • this one is resolved by exactly three writers (the two _applyRankChangeTx
|
|
12
12
|
// twins + the sign-in path) and is a TRANSACTION PARTICIPANT — it takes the
|
|
13
13
|
// caller's client so the role rotation commits or rolls back with the rank write.
|
|
14
|
-
// The port name is namespaced (`
|
|
14
|
+
// The port name is namespaced (`government.rankSync`) like the `ideas.capture` /
|
|
15
15
|
// `art.getBuilderGeminiKeyRow` / `grade.builderRollingAvgGrade` ports.
|
|
16
16
|
//
|
|
17
17
|
// Registered from a route factory for the same reason routes/governance.js is:
|
|
@@ -28,16 +28,16 @@ const express = require('express');
|
|
|
28
28
|
const api = require('../../../src/module-api');
|
|
29
29
|
const db = require('../db');
|
|
30
30
|
|
|
31
|
-
// DELEGATE, never spread — a test that stubs db.
|
|
31
|
+
// DELEGATE, never spread — a test that stubs db.syncRank must still bite at call
|
|
32
32
|
// time (the modules/onboarding/port.js precedent; a spread captures the original
|
|
33
33
|
// reference at module-load).
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const rankSyncPort = {
|
|
35
|
+
syncRank: (...a) => db.syncRank(...a),
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
function registerSeams() {
|
|
39
|
-
if (!api.hasProvider('
|
|
40
|
-
api.registerProvider('
|
|
39
|
+
if (!api.hasProvider('government.rankSync')) {
|
|
40
|
+
api.registerProvider('government.rankSync', rankSyncPort);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -46,4 +46,4 @@ module.exports = function buildGovernanceRoleSyncRouter() {
|
|
|
46
46
|
return express.Router();
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
-
module.exports.
|
|
49
|
+
module.exports.rankSyncPort = rankSyncPort;
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
me = null;
|
|
218
218
|
}
|
|
219
219
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
220
|
-
// ADR 0157: the Gate page sits at the `page.view.
|
|
220
|
+
// ADR 0157: the Gate page sits at the `page.view.government` floor (metic). The
|
|
221
221
|
// server's requireGovernancePage is the wall — this only decides whether to render the
|
|
222
222
|
// body or the sealed notice, so it must agree or a Metic sees "sealed" on a page
|
|
223
223
|
// they were just admitted to.
|
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
// owner's 2026-08-16 decision, and the section header says so out loud.
|
|
17
17
|
// • the ARCHON rank-role is read-only in full: it holds everything, and its
|
|
18
18
|
// immutability is what makes it impossible to click your way into locking the
|
|
19
|
-
// owner out of this tab. The API refuses it as `
|
|
20
|
-
// ASSIGNMENT of any rank-role is still refused (`
|
|
19
|
+
// owner out of this tab. The API refuses it as `archon_rank_immutable`.
|
|
20
|
+
// ASSIGNMENT of any rank-role is still refused (`seeded_rank_assignment_forbidden`)
|
|
21
21
|
// — you get a rank-role by holding the rank, so that dial is the rank itself.
|
|
22
22
|
//
|
|
23
23
|
// Like The Gate, this reads /api/bongos/me for a friendly Sealed notice —
|
|
24
|
-
// DEFENCE IN DEPTH only. Every /
|
|
25
|
-
// gated server-side on the `
|
|
24
|
+
// DEFENCE IN DEPTH only. Every /government/* endpoint (reads included) is ALSO
|
|
25
|
+
// gated server-side on the `government.manage` atom (archon floor). On 401 →
|
|
26
26
|
// web sign-in.
|
|
27
27
|
//
|
|
28
28
|
// DELIBERATELY NOT IN THE SIDEBAR YET: the nav entry + the server page gate are
|
|
@@ -64,27 +64,27 @@
|
|
|
64
64
|
// The R110 refusal codes, translated for the person clicking.
|
|
65
65
|
const ERROR_COPY = {
|
|
66
66
|
permission_not_grantable: 'That permission can’t be granted to this role. (The server’s reply says which of the three walls applied.)',
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
archon_rank_immutable: 'The Archon role is read-only — it holds every permission, which is what keeps you from locking yourself out of this page.',
|
|
68
|
+
rank_has_no_default: 'Only a rank role has a catalog default to reset to. Revoke a custom role’s permissions individually instead.',
|
|
69
|
+
seeded_rank_assignment_forbidden: 'Rank roles are assigned by rank itself — change the builder’s rank instead.',
|
|
70
|
+
rank_exists: 'A role with that key already exists.',
|
|
71
|
+
rank_not_found: 'That role no longer exists — refreshing.',
|
|
72
72
|
builder_not_found: 'No builder with that id.',
|
|
73
73
|
unknown_permission: 'The permission catalog does not define that key.',
|
|
74
|
-
|
|
74
|
+
bad_rank_key: 'Role keys are kebab-case: lowercase letters, digits, dashes.',
|
|
75
75
|
validation_failed: 'Missing or invalid fields.',
|
|
76
76
|
};
|
|
77
77
|
const friendlyError = (code) => ERROR_COPY[code] || null;
|
|
78
78
|
|
|
79
79
|
// ---- state ---------------------------------------------------------------
|
|
80
80
|
|
|
81
|
-
let roles = []; // [{
|
|
81
|
+
let roles = []; // [{ rank_key, label, description, is_system, seeded_rank, permissions: [] }]
|
|
82
82
|
let permissions = []; // catalog rows: [{ key, system, floor, guards, grantable }]
|
|
83
83
|
let principals = []; // [{ system_role, permission, guards }]
|
|
84
|
-
let assignments = []; // [{ builder_id, github_login, display_name, rank,
|
|
84
|
+
let assignments = []; // [{ builder_id, github_login, display_name, rank, rank_key, role_label, is_system }]
|
|
85
85
|
let selectedRoleKey = null;
|
|
86
86
|
|
|
87
|
-
const selectedRole = () => roles.find((r) => r.
|
|
87
|
+
const selectedRole = () => roles.find((r) => r.rank_key === selectedRoleKey) || null;
|
|
88
88
|
|
|
89
89
|
// ---- render: role list ---------------------------------------------------
|
|
90
90
|
|
|
@@ -102,11 +102,11 @@
|
|
|
102
102
|
const btn = document.createElement('button');
|
|
103
103
|
btn.type = 'button';
|
|
104
104
|
btn.className = 'gov-role';
|
|
105
|
-
btn.setAttribute('aria-pressed', role.
|
|
105
|
+
btn.setAttribute('aria-pressed', role.rank_key === selectedRoleKey ? 'true' : 'false');
|
|
106
106
|
btn.innerHTML =
|
|
107
|
-
`<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.
|
|
108
|
-
<span class="gov-role__meta"><code>${escapeHtml(role.
|
|
109
|
-
btn.addEventListener('click', () => { selectedRoleKey = role.
|
|
107
|
+
`<span class="gov-role__head"><span class="gov-role__label">${escapeHtml(role.label || role.rank_key)}</span>${pill}</span>
|
|
108
|
+
<span class="gov-role__meta"><code>${escapeHtml(role.rank_key)}</code> · ${(role.permissions || []).length} permission${(role.permissions || []).length === 1 ? '' : 's'}</span>`;
|
|
109
|
+
btn.addEventListener('click', () => { selectedRoleKey = role.rank_key; renderRoles(); renderGrid(); });
|
|
110
110
|
list.appendChild(btn);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
return 'The Archon role holds every permission and is read-only — that is what makes it impossible to lock yourself out of this page. To give a power away, hand it to one of the ranks below instead.';
|
|
177
177
|
}
|
|
178
178
|
if (isRankRole(role)) {
|
|
179
|
-
return `Everything a ${role.label || role.
|
|
179
|
+
return `Everything a ${role.label || role.rank_key} can do, and yours to change — every builder at this rank is affected immediately. “Reset to default” puts it back to the shipped ladder.`;
|
|
180
180
|
}
|
|
181
181
|
return 'Toggle the everyday permissions this role carries. Owner powers can only go to a rank role, not to a custom one.';
|
|
182
182
|
}
|
|
@@ -185,7 +185,7 @@
|
|
|
185
185
|
const grid = document.getElementById('gov-grid');
|
|
186
186
|
const role = selectedRole();
|
|
187
187
|
document.getElementById('gov-grid-title').textContent = role
|
|
188
|
-
? `Permissions — ${role.label || role.
|
|
188
|
+
? `Permissions — ${role.label || role.rank_key}` : 'Permissions';
|
|
189
189
|
document.getElementById('gov-grid-note').textContent = gridNote(role);
|
|
190
190
|
|
|
191
191
|
// The reset escape hatch — only a rank role has a catalog default, and the
|
|
@@ -223,11 +223,11 @@
|
|
|
223
223
|
// Restore a rank role to the shipped ladder. Confirmed because it is the one
|
|
224
224
|
// action here that discards work in bulk — a drifted role can be ~60 rows.
|
|
225
225
|
async function resetRole(role, btn) {
|
|
226
|
-
const label = role.label || role.
|
|
226
|
+
const label = role.label || role.rank_key;
|
|
227
227
|
if (!window.confirm(`Reset ${label} to its default permissions?\n\nEvery change you've made to this rank is discarded, and it takes effect for every builder at this rank on their next request.`)) return;
|
|
228
228
|
btn.disabled = true;
|
|
229
229
|
try {
|
|
230
|
-
const out = await sendJson('POST', `/
|
|
230
|
+
const out = await sendJson('POST', `/government/ranks/${encodeURIComponent(role.rank_key)}/reset`);
|
|
231
231
|
toast(`${label} reset to its default ${out.permission_count} permissions.`);
|
|
232
232
|
await reloadRoles();
|
|
233
233
|
} catch (err) {
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
async function toggleGrant(role, permissionKey, grant, btn) {
|
|
241
241
|
btn.disabled = true;
|
|
242
242
|
try {
|
|
243
|
-
await sendJson(grant ? 'PUT' : 'DELETE', `/
|
|
243
|
+
await sendJson(grant ? 'PUT' : 'DELETE', `/government/ranks/${encodeURIComponent(role.rank_key)}/permissions/${encodeURIComponent(permissionKey)}`);
|
|
244
244
|
toast(grant ? `Granted ${permissionKey}.` : `Revoked ${permissionKey}.`);
|
|
245
245
|
await reloadRoles();
|
|
246
246
|
} catch (err) {
|
|
@@ -266,8 +266,8 @@
|
|
|
266
266
|
row.className = 'gov-assign-row';
|
|
267
267
|
const who = `${escapeHtml(a.display_name || a.github_login || `builder ${a.builder_id}`)} <span class="muted">(#${escapeHtml(String(a.builder_id))} · ${escapeHtml(a.rank || '')})</span>`;
|
|
268
268
|
const rolePill = a.is_system
|
|
269
|
-
? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.
|
|
270
|
-
: `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.
|
|
269
|
+
? `<span class="gov-pill gov-pill--seeded">${escapeHtml(a.role_label || a.rank_key)} · via rank</span>`
|
|
270
|
+
: `<span class="gov-pill gov-pill--custom">${escapeHtml(a.role_label || a.rank_key)}</span>`;
|
|
271
271
|
const action = a.is_system ? '' : `<button type="button" class="gov-btn gov-btn--ghost gov-btn--small" data-act="unassign">Remove</button>`;
|
|
272
272
|
row.innerHTML = `<span class="gov-assign-row__who">${who}</span>${rolePill}${action}`;
|
|
273
273
|
const btn = row.querySelector('[data-act=unassign]');
|
|
@@ -281,15 +281,15 @@
|
|
|
281
281
|
const sel = document.getElementById('gov-assign-role');
|
|
282
282
|
const custom = roles.filter((r) => !r.is_system);
|
|
283
283
|
sel.innerHTML = custom.length
|
|
284
|
-
? custom.map((r) => `<option value="${escapeHtml(r.
|
|
284
|
+
? custom.map((r) => `<option value="${escapeHtml(r.rank_key)}">${escapeHtml(r.label || r.rank_key)}</option>`).join('')
|
|
285
285
|
: `<option value="" disabled selected>No custom roles yet</option>`;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
async function unassign(a, btn) {
|
|
289
289
|
btn.disabled = true;
|
|
290
290
|
try {
|
|
291
|
-
await sendJson('DELETE', `/
|
|
292
|
-
toast(`Removed ${a.role_label || a.
|
|
291
|
+
await sendJson('DELETE', `/government/assignments/${encodeURIComponent(a.builder_id)}/${encodeURIComponent(a.rank_key)}`);
|
|
292
|
+
toast(`Removed ${a.role_label || a.rank_key} from builder ${a.builder_id}.`);
|
|
293
293
|
await reloadAssignments();
|
|
294
294
|
} catch (err) {
|
|
295
295
|
if (err && err.kind === '401') return;
|
|
@@ -321,22 +321,22 @@
|
|
|
321
321
|
// ---- loads ---------------------------------------------------------------
|
|
322
322
|
|
|
323
323
|
async function reloadRoles() {
|
|
324
|
-
const data = await fetchJson('/
|
|
324
|
+
const data = await fetchJson('/government/ranks');
|
|
325
325
|
roles = data.roles || [];
|
|
326
|
-
if (!roles.some((r) => r.
|
|
326
|
+
if (!roles.some((r) => r.rank_key === selectedRoleKey)) selectedRoleKey = roles.length ? roles[0].rank_key : null;
|
|
327
327
|
renderRoles();
|
|
328
328
|
renderGrid();
|
|
329
329
|
renderAssignSelect();
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
async function reloadAssignments() {
|
|
333
|
-
const data = await fetchJson('/
|
|
333
|
+
const data = await fetchJson('/government/assignments');
|
|
334
334
|
assignments = data.assignments || [];
|
|
335
335
|
renderAssignments();
|
|
336
336
|
}
|
|
337
337
|
|
|
338
338
|
async function load() {
|
|
339
|
-
const perms = await fetchJson('/
|
|
339
|
+
const perms = await fetchJson('/government/permissions');
|
|
340
340
|
permissions = perms.permissions || [];
|
|
341
341
|
principals = perms.principals || [];
|
|
342
342
|
await reloadRoles();
|
|
@@ -352,16 +352,16 @@
|
|
|
352
352
|
const btn = e.target.querySelector('button');
|
|
353
353
|
btn.disabled = true;
|
|
354
354
|
const body = {
|
|
355
|
-
|
|
355
|
+
rank_key: document.getElementById('gov-create-key').value.trim(),
|
|
356
356
|
label: document.getElementById('gov-create-label').value.trim(),
|
|
357
357
|
};
|
|
358
358
|
const desc = document.getElementById('gov-create-desc').value.trim();
|
|
359
359
|
if (desc) body.description = desc;
|
|
360
360
|
try {
|
|
361
|
-
const r = await sendJson('POST', '/
|
|
362
|
-
toast(`Created role ${r.role ? r.role.
|
|
361
|
+
const r = await sendJson('POST', '/government/ranks', body);
|
|
362
|
+
toast(`Created role ${r.role ? r.role.rank_key : body.rank_key}.`);
|
|
363
363
|
e.target.reset();
|
|
364
|
-
selectedRoleKey = body.
|
|
364
|
+
selectedRoleKey = body.rank_key;
|
|
365
365
|
await reloadRoles();
|
|
366
366
|
} catch (err) {
|
|
367
367
|
if (err && err.kind === '401') return;
|
|
@@ -379,7 +379,7 @@
|
|
|
379
379
|
if (!roleKey) { toast('Create a custom role first.'); return; }
|
|
380
380
|
btn.disabled = true;
|
|
381
381
|
try {
|
|
382
|
-
await sendJson('POST', '/
|
|
382
|
+
await sendJson('POST', '/government/assignments', { builder_id: builderId, rank_key: roleKey });
|
|
383
383
|
toast(`Assigned ${roleKey} to builder ${builderId}.`);
|
|
384
384
|
e.target.reset();
|
|
385
385
|
await reloadAssignments();
|
|
@@ -402,8 +402,8 @@
|
|
|
402
402
|
|
|
403
403
|
async function init() {
|
|
404
404
|
// Confirm rank for a friendly sealed notice; the server gates the page and
|
|
405
|
-
// every /governance endpoint on `
|
|
406
|
-
// defence-in-depth).
|
|
405
|
+
// every /governance endpoint on `government.manage` regardless (cosmetic
|
|
406
|
+
// defence-in-depth). government.manage is system:true, so only the archon
|
|
407
407
|
// rank-role can hold it — rank is an accurate client-side proxy.
|
|
408
408
|
let me;
|
|
409
409
|
try {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// shows a restricted notice below the Metic floor — DEFENCE IN DEPTH only. The data +
|
|
10
10
|
// mutation endpoints (/boxes, /boxes/:id/close, /boxes/:id/block) are ALSO gated
|
|
11
11
|
// server-side on `box.fleet.manage` (ADR 0016), and the page itself is gated by
|
|
12
|
-
// serve-internal.js requireGovernancePage — whose own floor is `page.view.
|
|
12
|
+
// serve-internal.js requireGovernancePage — whose own floor is `page.view.government`,
|
|
13
13
|
// metic since ADR 0157. On a 401 it redirects into the web sign-in flow.
|
|
14
14
|
|
|
15
15
|
(() => {
|
|
@@ -326,7 +326,7 @@
|
|
|
326
326
|
me = null;
|
|
327
327
|
}
|
|
328
328
|
const rank = (me && (me.builder || me).rank || '').toLowerCase();
|
|
329
|
-
// ADR 0157: Harbor sits at the `page.view.
|
|
329
|
+
// ADR 0157: Harbor sits at the `page.view.government` floor (metic), and the fleet
|
|
330
330
|
// data it reads is `box.fleet.manage` (also metic now). Cosmetic —
|
|
331
331
|
// requireGovernancePage + the route gates are the wall; this just picks body vs
|
|
332
332
|
// sealed notice.
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
// current (mirrors the old #hall-nav-work rule, task #833)
|
|
99
99
|
// archon — rank === 'archon'
|
|
100
100
|
// governance — rank is metic or archon (ADR 0157): the monitoring/ops pages.
|
|
101
|
-
// Mirrors the server's `page.view.
|
|
101
|
+
// Mirrors the server's `page.view.government` floor, which that ADR
|
|
102
102
|
// lowered archon→metic. Cosmetic only — requireGovernancePage is the wall.
|
|
103
103
|
// `view` marks an index view (hash-routed); `module` gates on window.__MODULES__.
|
|
104
104
|
// task 2175: grouped by AUDIENCE of the data, so the sidebar reads in coherent
|
|
@@ -178,7 +178,7 @@
|
|
|
178
178
|
{ id: 'sessions', label: 'Sessions', icon: 'admin', href: '/sessions', gate: 'governance' },
|
|
179
179
|
// BV1.R113: the roles-and-permissions tab (ADR 0151). The one genuinely
|
|
180
180
|
// Archon-only item the 'governance' gate comment anticipated — cosmetic
|
|
181
|
-
// only; requireGovernanceManagePage (the
|
|
181
|
+
// only; requireGovernanceManagePage (the government.manage atom) is the wall.
|
|
182
182
|
{ id: 'governance', label: 'Roles', icon: 'admin', href: '/governance', gate: 'archon' },
|
|
183
183
|
] },
|
|
184
184
|
];
|
|
@@ -420,7 +420,7 @@
|
|
|
420
420
|
case 'member': return member;
|
|
421
421
|
case 'work': return authed && (member || access.showWork);
|
|
422
422
|
case 'archon': return authed && access.rank === 'archon';
|
|
423
|
-
// ADR 0157: the monitoring/ops pages sit at the `page.view.
|
|
423
|
+
// ADR 0157: the monitoring/ops pages sit at the `page.view.government` floor,
|
|
424
424
|
// which is metic. Kept as its own key rather than widening 'archon', so a
|
|
425
425
|
// genuinely Archon-only nav item can still be added later.
|
|
426
426
|
case 'governance': return authed && (access.rank === 'metic' || access.rank === 'archon');
|
|
@@ -885,7 +885,7 @@
|
|
|
885
885
|
return;
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
-
// ADR 0157: Watch sits at the `page.view.
|
|
888
|
+
// ADR 0157: Watch sits at the `page.view.government` floor (metic), and every panel
|
|
889
889
|
// it reads (roster, security reports, audit log, sessions, box costs) delegated to
|
|
890
890
|
// metic with it. Cosmetic — requireGovernancePage + the route gates are the wall.
|
|
891
891
|
if (!builder || (builder.rank !== 'metic' && builder.rank !== 'archon')) {
|
package/modules/lifecycle/db.js
CHANGED
|
@@ -220,7 +220,7 @@ function xenosClaimAllowed(rank, newcomerFriendly) {
|
|
|
220
220
|
// 0016 — resolved server-side, uncached), and taking a second pool connection while
|
|
221
221
|
// a tx holds FOR UPDATE locks is a needless deadlock/exhaustion hazard.
|
|
222
222
|
async function claimPermissionsFor(builderId) {
|
|
223
|
-
const governance = seams.resolveOptional('
|
|
223
|
+
const governance = seams.resolveOptional('government');
|
|
224
224
|
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
225
225
|
try {
|
|
226
226
|
const keys = await governance.resolveBuilderPermissions(builderId);
|
|
@@ -408,12 +408,12 @@ function disambiguateWorktreeName(desired, taken) {
|
|
|
408
408
|
//
|
|
409
409
|
// Returns the updated builder row, or throws { code:'BUILDER_NOT_FOUND' } /
|
|
410
410
|
// { code:'RANK_UNCHANGED' }.
|
|
411
|
-
// syncGovernanceRankRole — keep
|
|
411
|
+
// syncGovernanceRankRole — keep government_builder_ranks in step with builders.rank
|
|
412
412
|
// (BV1.R95b, ADR 0151). This module's rank writer backs the automatic xenos→thetes
|
|
413
413
|
// graduation, which had the same hole as the Archon-driven path: the rank moved and
|
|
414
414
|
// the role did not, so a freshly-graduated Thetes kept the xenos role.
|
|
415
415
|
//
|
|
416
|
-
// Resolved through the `
|
|
416
|
+
// Resolved through the `government.rankSync` kernel port (never a sibling require —
|
|
417
417
|
// ADR 0083) and run on the CALLER's transaction client, so the rotation commits
|
|
418
418
|
// atomically with the ship + graduation. A missing port (governance disabled) skips:
|
|
419
419
|
// no role ⇒ zero permissions, fail-closed.
|
|
@@ -421,9 +421,9 @@ function disambiguateWorktreeName(desired, taken) {
|
|
|
421
421
|
// TWINNED with src/bongos/db.js (the boundary-forced _applyRankChangeTx duplication,
|
|
422
422
|
// ADR 0091 §2) — keep the two in sync.
|
|
423
423
|
async function syncGovernanceRankRole(client, builderId, rank) {
|
|
424
|
-
const roleSync = seams.resolveOptional('
|
|
425
|
-
if (!roleSync || typeof roleSync.
|
|
426
|
-
return roleSync.
|
|
424
|
+
const roleSync = seams.resolveOptional('government.rankSync');
|
|
425
|
+
if (!roleSync || typeof roleSync.syncRank !== 'function') return null;
|
|
426
|
+
return roleSync.syncRank(client, { builderId, rank });
|
|
427
427
|
}
|
|
428
428
|
|
|
429
429
|
// _applyRankChangeTx — the in-transaction CORE of a rank change, shared by the
|
|
@@ -68,7 +68,7 @@ module.exports = function buildGateApprovalsRouter() {
|
|
|
68
68
|
// BV1.R105 left this on requireRank because `gate_approval.decide` was declared over
|
|
69
69
|
// the /approve WRITE only and no atom named the read. ADR 0157 extends the catalog
|
|
70
70
|
// with exactly that atom — `gate_approval.read`, floor metic — so the mapping is now
|
|
71
|
-
// one the catalog makes. The split is load-bearing, not cosmetic: `page.view.
|
|
71
|
+
// one the catalog makes. The split is load-bearing, not cosmetic: `page.view.government`
|
|
72
72
|
// opens /gate to Metic, and without a Metic-readable data endpoint that page would
|
|
73
73
|
// render nothing but a 403. Approving a held PR stays archon (`.decide`, system).
|
|
74
74
|
router.get('/gate-approvals', auth.requireBuilder, auth.requirePermission('gate_approval.read'), async (req, res) => {
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.185",
|
|
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.185",
|
|
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.185",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -732,6 +732,16 @@ const LEGACY_NAMESPACED = {
|
|
|
732
732
|
// game module: world_events + players were created before the module system (V1 schema). The
|
|
733
733
|
// schema_migrations table is a core tracking table that every migration may re-create idempotently.
|
|
734
734
|
game: new Set(['world_events', 'players', 'schema_migrations']),
|
|
735
|
+
// government module: the module KEY was renamed governance→government (ADR 0174, goal 1000068),
|
|
736
|
+
// so its own historical migrations now read as "outside the namespace" purely because the key
|
|
737
|
+
// moved underneath them. These three tables ARE this module's — under the name it used to have.
|
|
738
|
+
// They are not renamed in place because schema_migrations is keyed by filename STEM: renaming an
|
|
739
|
+
// already-applied migration file makes it look unapplied and re-runs it, and governance_002 is a
|
|
740
|
+
// seed/backfill. government_001_rename_from_governance.sql renames the live tables forward; these
|
|
741
|
+
// entries let the historical files that created them keep describing what actually happened.
|
|
742
|
+
government: new Set([
|
|
743
|
+
'governance_roles', 'governance_builder_roles', 'governance_role_permissions',
|
|
744
|
+
]),
|
|
735
745
|
};
|
|
736
746
|
const DESTRUCTIVE_DDL = [
|
|
737
747
|
{ re: /\bdrop\s+table\b/i, label: 'DROP TABLE' },
|
|
@@ -1141,7 +1151,7 @@ function checkKernelPromptsNoHardcodedIdentity() {
|
|
|
1141
1151
|
// this check nothing in CI could see it.
|
|
1142
1152
|
//
|
|
1143
1153
|
// Only the NEUTRAL packs are scanned, deliberately: an instance pack (config/
|
|
1144
|
-
// branding.json, config/
|
|
1154
|
+
// branding.json, config/government.json) is host content that legitimately names
|
|
1145
1155
|
// its host, and is boundary-excluded from the published core anyway.
|
|
1146
1156
|
function checkNeutralConfigsNoHostIdentity() {
|
|
1147
1157
|
const violations = [];
|
|
@@ -57,6 +57,15 @@ const LEGACY_BARE_CORE_MAX = 187;
|
|
|
57
57
|
// module migrations must be `<key>_`-prefixed; these stay bare (already applied on prod).
|
|
58
58
|
const LEGACY_BARE_MODULES = new Set(['game', 'demo']);
|
|
59
59
|
|
|
60
|
+
// RENAMED_MODULE_PREFIXES: a module whose KEY was renamed keeps its historical
|
|
61
|
+
// migration-filename prefix for files that were already applied. schema_migrations
|
|
62
|
+
// is keyed by filename STEM, so renaming an applied migration makes it look
|
|
63
|
+
// unapplied and RE-RUNS it — and a re-run seed/backfill is a data event, not a
|
|
64
|
+
// no-op. The already-applied files therefore keep the old prefix forever; only
|
|
65
|
+
// NEW migrations must carry the module's current namespace.
|
|
66
|
+
// government <- governance (ADR 0174, goal 1000068)
|
|
67
|
+
const RENAMED_MODULE_PREFIXES = { government: new Set(['governance']) };
|
|
68
|
+
|
|
60
69
|
// ---- parsing ---------------------------------------------------------------
|
|
61
70
|
|
|
62
71
|
const BARE_RE = /^(\d{3,})_(.+)\.sql$/;
|
|
@@ -116,9 +125,11 @@ function filenameViolation(source, basename, { moduleKey } = {}) {
|
|
|
116
125
|
? null // legacy bare module — grandfathered
|
|
117
126
|
: `module "${moduleKey}" migration "${basename}" is bare — a NEW module migration must be ${ns}_-prefixed (ADR 0083 §Decision #5 / ADR 0108 §5): use ${ns}_${pad3(p.number)}_${p.slug}.sql`;
|
|
118
127
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
if (p.prefix === ns) return null;
|
|
129
|
+
// A renamed module's already-applied files keep their historical prefix (see
|
|
130
|
+
// RENAMED_MODULE_PREFIXES) — the stem is the schema_migrations key, so it cannot move.
|
|
131
|
+
if ((RENAMED_MODULE_PREFIXES[moduleKey] || new Set()).has(p.prefix)) return null;
|
|
132
|
+
return `module "${moduleKey}" migration "${basename}" carries the "${p.prefix}_" prefix — it must be ${ns}_-prefixed to its own module (ADR 0083 §Decision #5).`;
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
// ---- cross-root stem-collision detector ------------------------------------
|
|
@@ -186,6 +197,7 @@ function lineageReport({ core = [], instance = [], modulesByKey = {} } = {}) {
|
|
|
186
197
|
|
|
187
198
|
module.exports = {
|
|
188
199
|
CORE_NS,
|
|
200
|
+
RENAMED_MODULE_PREFIXES,
|
|
189
201
|
INSTANCE_NS,
|
|
190
202
|
RESERVED_NS,
|
|
191
203
|
LEGACY_BARE_CORE_MAX,
|
|
@@ -61,7 +61,6 @@ const PUBLISH_ALLOWLIST = [
|
|
|
61
61
|
'modules/sessions/',
|
|
62
62
|
'modules/autonomy/',
|
|
63
63
|
'modules/security/',
|
|
64
|
-
'modules/governance/', // configurable roles & permissions (BV1 goal 1000030, ADR 0151) — the rank/permission machinery publishes AS the open-source methodology (ADR 0065 §4 / ADR 0098); it is in PROTECTED_GLOBS for write-protection, NOT publish-exclusion. default:true. Renamed → modules/government/ (ADR 0174); dropped with the folder in goal 1000068 R04.
|
|
65
64
|
'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
|
|
66
65
|
'modules/hall-ui/',
|
|
67
66
|
'modules/status-ui/',
|
|
@@ -75,7 +74,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
75
74
|
'config/design-tokens.neutral.json',
|
|
76
75
|
'config/design-tokens.schema.json', // the design-token contract (generic schema, no instance values)
|
|
77
76
|
'config/hierarchy.neutral.json',
|
|
78
|
-
'config/
|
|
77
|
+
'config/government.neutral.json', // the starting rank templates a fresh instance inherits (BV1.R96; renamed from governance.neutral.json by ADR 0174)
|
|
79
78
|
// --- the portable methodology core, governance, and sanitized docs ---
|
|
80
79
|
'CLAUDE.md',
|
|
81
80
|
'README.md',
|
|
@@ -212,7 +211,7 @@ const INSTANCE_EXCLUDE = [
|
|
|
212
211
|
'config/modules.json',
|
|
213
212
|
'config/devbox-app.json',
|
|
214
213
|
'config/hierarchy.json',
|
|
215
|
-
'config/
|
|
214
|
+
'config/government.json',
|
|
216
215
|
'CODEOWNERS', // names the owner (a protected surface, excluded as identity)
|
|
217
216
|
// instance SEED migrations — this host's scope-truth SQL (planning seeds, world data).
|
|
218
217
|
// ADR 0105: a consumer's core must not carry them; migrate.sh reads instance seeds from
|
|
@@ -195,10 +195,10 @@ const TASKS = [
|
|
|
195
195
|
{
|
|
196
196
|
ref: 'R96', kind: 'feature', priority: 4, est_minutes: 100, manual_degree: 2, credits_reward: 40,
|
|
197
197
|
criterion_ids: [C4], depends_on_refs: ['R93', 'R94'],
|
|
198
|
-
title: 'config/
|
|
198
|
+
title: 'config/government.json + neutral + resolve-over-neutral',
|
|
199
199
|
description: `**Why:** role templates/defaults are per-instance config (the branding pattern); live data is DB.
|
|
200
200
|
|
|
201
|
-
**Done when:** \`config/
|
|
201
|
+
**Done when:** \`config/government.json\` (instance) resolves over \`config/government.neutral.json\` (neutral) with env override on top, mirroring src/branding.js precedence; it seeds the starting role templates a fresh instance inherits; live assignments stay in the DB (R93). Depends on R93/R94.`,
|
|
202
202
|
},
|
|
203
203
|
{
|
|
204
204
|
ref: 'R102', kind: 'verify', priority: 3, est_minutes: 70, manual_degree: 2, credits_reward: 35,
|
|
@@ -216,7 +216,7 @@ const TASKS = [
|
|
|
216
216
|
title: 'Governance API routes (roles/perms/assignments CRUD)',
|
|
217
217
|
description: `**Why:** the tab's server surface.
|
|
218
218
|
|
|
219
|
-
**Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`
|
|
219
|
+
**Done when:** routes list roles/permissions/assignments, create a role, toggle an operational permission on a role, and assign a role to a builder — all gated \`government.manage\` (Archon), system permissions rejected as non-grantable server-side, and pinned in route-rank-check. Depends on R97 + R109.`,
|
|
220
220
|
},
|
|
221
221
|
{
|
|
222
222
|
ref: 'R111', kind: 'feature', priority: 3, est_minutes: 180, manual_degree: 2, credits_reward: 55,
|
package/src/bongos/auth.js
CHANGED
|
@@ -367,7 +367,7 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
367
367
|
// flips xenos→archon outside any transaction), and it quietly repairs a builder whose
|
|
368
368
|
// assignment drifted for any other reason.
|
|
369
369
|
//
|
|
370
|
-
// Resolved through the `
|
|
370
|
+
// Resolved through the `government.rankSync` kernel port — auth.js is a KERNEL_FILE
|
|
371
371
|
// and may not require a module (ADR 0083 / ADR 0091 §1). Runs on the pool: there is
|
|
372
372
|
// no surrounding transaction here, unlike the two rank-change paths, which pass their
|
|
373
373
|
// own client so the rotation is atomic with the rank write.
|
|
@@ -377,8 +377,8 @@ async function maybeSeatFirstAdmin(builder) {
|
|
|
377
377
|
async function safeSyncRankRole(builder) {
|
|
378
378
|
if (!builder || builder.id == null) return;
|
|
379
379
|
try {
|
|
380
|
-
await seams.resolveOptional('
|
|
381
|
-
?.
|
|
380
|
+
await seams.resolveOptional('government.rankSync')
|
|
381
|
+
?.syncRank(null, { builderId: builder.id, rank: builder.rank });
|
|
382
382
|
} catch (err) {
|
|
383
383
|
console.error('[auth] rank-role sync failed (non-fatal):', err.message);
|
|
384
384
|
}
|
|
@@ -1195,8 +1195,8 @@ async function resolveSession(req) {
|
|
|
1195
1195
|
// a behaviour-identical remodelling of the #919 gate, not a re-gating of it.
|
|
1196
1196
|
//
|
|
1197
1197
|
// GOVERNANCE OWNS THE POLICY, THE KERNEL STAYS THE ENFORCER. The declaration is
|
|
1198
|
-
// governance-owned data in modules/
|
|
1199
|
-
// against the permission catalog at load by modules/
|
|
1198
|
+
// governance-owned data in modules/government/session-scopes.json, validated
|
|
1199
|
+
// against the permission catalog at load by modules/government/session-scopes.js.
|
|
1200
1200
|
// Core reads it as DATA with fs — never `require` (ADR 0083's one-way rule: a
|
|
1201
1201
|
// module's code and enable/disable state must never be something the kernel
|
|
1202
1202
|
// depends on). The sibling trust primitive permission-path-check.js reads the
|
|
@@ -1211,7 +1211,7 @@ async function resolveSession(req) {
|
|
|
1211
1211
|
// registry does not declare is DENIED rather than defaulting to full authority. A
|
|
1212
1212
|
// null/absent source is a DECLARED row (rows minted before migration 090 added the
|
|
1213
1213
|
// column, which predate the 'box' source existing), not an implicit default.
|
|
1214
|
-
const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', '
|
|
1214
|
+
const SESSION_SCOPE_REGISTRY_PATH = path.join(__dirname, '..', '..', 'modules', 'government', 'session-scopes.json');
|
|
1215
1215
|
|
|
1216
1216
|
// Pure: validate + normalize the parsed registry. Split from the file read so the
|
|
1217
1217
|
// shipped assertions are what the tests exercise. `label` identifies the file in
|
|
@@ -1437,10 +1437,10 @@ function signInWithReturn(req) {
|
|
|
1437
1437
|
// ranks (catalog.RANK_ORDER):
|
|
1438
1438
|
// requireBuilderPage was "any session" floor xenos → page.view.public
|
|
1439
1439
|
// requireNonXenosPage was rank !== 'xenos' floor thetes → page.view.builder
|
|
1440
|
-
// requireGovernancePage was rank === 'archon' floor archon → page.view.
|
|
1440
|
+
// requireGovernancePage was rank === 'archon' floor archon → page.view.government
|
|
1441
1441
|
// That third gate was spelled `requireArchonPage` until ADR 0157 lowered
|
|
1442
|
-
// page.view.
|
|
1443
|
-
// floor now lives in modules/
|
|
1442
|
+
// page.view.government's floor archon→metic and the name stopped being true; the
|
|
1443
|
+
// floor now lives in modules/government/catalog.js, not in the function's name.
|
|
1444
1444
|
// The one accepted divergence is the one governance_002 already documents
|
|
1445
1445
|
// (tests/governance_parity.mjs Part D): a builder on a reserved/divine rank holds
|
|
1446
1446
|
// no seeded role, so they now fail a page gate a bare rank test would have
|
|
@@ -1456,8 +1456,8 @@ function signInWithReturn(req) {
|
|
|
1456
1456
|
const PAGE_GATES = Object.freeze({
|
|
1457
1457
|
requireBuilderPage: Object.freeze({ permission: 'page.view.public', deniedTo: '/builders' }),
|
|
1458
1458
|
requireNonXenosPage: Object.freeze({ permission: 'page.view.builder', deniedTo: '/builders/not-ready' }),
|
|
1459
|
-
requireGovernancePage: Object.freeze({ permission: 'page.view.
|
|
1460
|
-
requireGovernanceManagePage: Object.freeze({ permission: '
|
|
1459
|
+
requireGovernancePage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
|
|
1460
|
+
requireGovernanceManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
|
|
1461
1461
|
requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
|
|
1462
1462
|
});
|
|
1463
1463
|
|
|
@@ -1476,7 +1476,7 @@ const PAGE_GATES = Object.freeze({
|
|
|
1476
1476
|
// through to the DB resolve, so the fold can only ever be an optimization.
|
|
1477
1477
|
async function resolveHeldPermissions(builderId, rawKeys) {
|
|
1478
1478
|
if (builderId === null || builderId === undefined || builderId === '') return null;
|
|
1479
|
-
const governance = seams.resolveOptional('
|
|
1479
|
+
const governance = seams.resolveOptional('government');
|
|
1480
1480
|
if (!governance || typeof governance.resolveBuilderPermissions !== 'function') return null;
|
|
1481
1481
|
if (Array.isArray(rawKeys) && typeof governance.effectivePermissions === 'function') {
|
|
1482
1482
|
try {
|
|
@@ -1534,7 +1534,7 @@ async function requireBuilderPage(req, res, next) {
|
|
|
1534
1534
|
}
|
|
1535
1535
|
|
|
1536
1536
|
// The /watch monitoring surface, the /harbor dev-box page and the /gate page
|
|
1537
|
-
// (#726, idea 257, task 1179) — Metic and up, per `page.view.
|
|
1537
|
+
// (#726, idea 257, task 1179) — Metic and up, per `page.view.government`'s catalog
|
|
1538
1538
|
// floor. Named `requireArchonPage` until ADR 0157 lowered that floor archon→metic
|
|
1539
1539
|
// (older ADRs still use the old name; this line is the trail). Defense-in-depth:
|
|
1540
1540
|
// every data endpoint these pages call is independently gated server-side, so no
|
|
@@ -1545,7 +1545,7 @@ async function requireGovernancePage(req, res, next) {
|
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
1547
|
// The /governance roles-and-permissions tab (BV1.R113) — gated on the
|
|
1548
|
-
// `
|
|
1548
|
+
// `government.manage` ATOM itself (system, archon floor), the same key every
|
|
1549
1549
|
// /governance/* data route checks, so the page layer can never drift wider than
|
|
1550
1550
|
// the API it fronts. Defense-in-depth as above: the data endpoints are all
|
|
1551
1551
|
// independently gated; this keeps the tab shell from rendering for anyone else.
|
|
@@ -1716,14 +1716,14 @@ function requireRank(...allowed) {
|
|
|
1716
1716
|
// time — never hardcoded prose, which goes stale when a floor moves (the
|
|
1717
1717
|
// D-16/N-3 failure shape ADR 0157 left behind).
|
|
1718
1718
|
//
|
|
1719
|
-
// Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('
|
|
1719
|
+
// Usage: router.post('/roles', auth.requireBuilder, auth.requirePermission('government.manage'), handler)
|
|
1720
1720
|
function requirePermission(...perms) {
|
|
1721
1721
|
const required = Object.freeze([...perms]);
|
|
1722
1722
|
return async function permissionGate(req, res, next) {
|
|
1723
1723
|
const deny = (held) => {
|
|
1724
1724
|
const missing = held === null ? [...required] : required.filter((key) => !held.includes(key));
|
|
1725
1725
|
const floors = {};
|
|
1726
|
-
const governance = seams.resolveOptional('
|
|
1726
|
+
const governance = seams.resolveOptional('government');
|
|
1727
1727
|
if (governance && typeof governance.permissionFloor === 'function') {
|
|
1728
1728
|
for (const key of missing) {
|
|
1729
1729
|
const floor = governance.permissionFloor(key);
|