@bongos/core 1.19.185 → 1.19.187
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 +90 -85
- package/clients/bongos-client/index.d.ts +2 -2
- package/config/government.neutral.json +6 -6
- package/docs/adr/0172-editable-rank-roles-substrate-axis.md +1 -1
- package/docs/api/openapi.json +12 -12
- package/docs/api-reference.md +7 -7
- package/modules/builder-settings/wandering-prefs.js +13 -13
- package/modules/government/catalog.js +79 -79
- package/modules/government/config.js +43 -33
- package/modules/government/db.js +58 -58
- package/modules/government/protected-surfaces.js +7 -7
- package/modules/government/protected-surfaces.json +4 -4
- package/modules/government/ranks.js +17 -17
- package/modules/government/resolver.js +24 -24
- package/modules/government/routes/government.js +88 -88
- package/modules/government/routes/rank-sync.js +8 -8
- package/modules/government/session-scopes.js +7 -7
- package/modules/government/session-scopes.json +5 -5
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/{governance.css → government.css} +11 -11
- package/modules/hall-ui/public/{governance.html → government.html} +14 -14
- package/modules/hall-ui/public/{governance.js → government.js} +108 -108
- 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/modules/lifecycle/claim-eligibility.js +1 -1
- package/modules/lifecycle/db.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness.js +138 -0
- package/scripts/hall-preview/server.js +1 -1
- package/src/bongos/auth.js +11 -11
- package/src/bongos/serve-internal.js +8 -8
- package/src/module-api.js +5 -1
- package/tests/auth_page_gate.mjs +2 -2
- package/tests/auth_resolve_failure.mjs +4 -4
- package/tests/{governance_abuse_matrix.mjs → government_abuse_matrix.mjs} +39 -39
- package/tests/{governance_catalog.mjs → government_catalog.mjs} +11 -11
- package/tests/{governance_claim_eligibility.mjs → government_claim_eligibility.mjs} +2 -2
- package/tests/{governance_config.mjs → government_config.mjs} +54 -54
- package/tests/{governance_fold.mjs → government_fold.mjs} +6 -6
- package/tests/{governance_fold_bench.mjs → government_fold_bench.mjs} +3 -3
- package/tests/{governance_live_proof.mjs → government_live_proof.mjs} +5 -5
- package/tests/{governance_ownership_scope.mjs → government_ownership_scope.mjs} +16 -16
- package/tests/{governance_page_gates.mjs → government_page_gates.mjs} +14 -14
- package/tests/{governance_parity.mjs → government_parity.mjs} +3 -3
- package/tests/{governance_principals.mjs → government_principals.mjs} +14 -14
- package/tests/{governance_protected_surfaces.mjs → government_protected_surfaces.mjs} +1 -1
- package/tests/{governance_role_sync.mjs → government_rank_sync.mjs} +2 -2
- package/tests/{governance_require_permission.mjs → government_require_permission.mjs} +1 -1
- package/tests/{governance_resolver.mjs → government_resolver.mjs} +6 -6
- package/tests/{governance_routes.mjs → government_routes.mjs} +36 -36
- package/tests/{governance_seed.mjs → government_seed.mjs} +8 -8
- package/tests/{governance_session_scope.mjs → government_session_scope.mjs} +10 -10
- package/tests/{governance_vanilla_boot.mjs → government_vanilla_boot.mjs} +26 -26
- package/tests/government_vocabulary.mjs +94 -0
- package/tests/hall_nav.mjs +8 -7
- package/tests/hall_page_gate_map.mjs +9 -9
package/scripts/gds/fitness.js
CHANGED
|
@@ -1640,6 +1640,141 @@ function checkWriteRoutesValidated() {
|
|
|
1640
1640
|
|
|
1641
1641
|
// ---- runner ----------------------------------------------------------------
|
|
1642
1642
|
|
|
1643
|
+
|
|
1644
|
+
// ── Check 19 — the ADR 0174 vocabulary wall (goal 1000068 R07, task 1003008) ──
|
|
1645
|
+
//
|
|
1646
|
+
// "Role" means ONE thing in this codebase: a craft — Artist, Ideator, or Builder.
|
|
1647
|
+
// The permission system is the GOVERNMENT and the positions in it are RANKS.
|
|
1648
|
+
// Before ADR 0174 the same word named both, and goal 1000066 was about to claim
|
|
1649
|
+
// it for the crafts while modules/governance/ still used it for the ladder. This
|
|
1650
|
+
// check is what stops that collision reforming the first time someone copies an
|
|
1651
|
+
// old file.
|
|
1652
|
+
//
|
|
1653
|
+
// TWO RULES, both scoped to the GOVERNMENT SURFACE — not the whole repo. A repo-
|
|
1654
|
+
// wide ban would be wrong: `builders.system_role` is a core column for machine
|
|
1655
|
+
// identities, Discord's API owns its own "role" noun, and the crafts are supposed
|
|
1656
|
+
// to use the word.
|
|
1657
|
+
//
|
|
1658
|
+
// 1. no "role" under modules/government/ or on the Government hall page
|
|
1659
|
+
// 2. no "governance" there either — the old name of the same surface
|
|
1660
|
+
//
|
|
1661
|
+
// EXCLUDED BY CONSTRUCTION (not by allowlist):
|
|
1662
|
+
// • modules/government/migrations/governance_*.sql — the already-applied
|
|
1663
|
+
// migrations keep their filenames forever (schema_migrations is stem-keyed;
|
|
1664
|
+
// renaming one re-runs it, and governance_002 is a seed/backfill). Their
|
|
1665
|
+
// bodies necessarily name the pre-rename tables they created.
|
|
1666
|
+
// • docs/adr/, docs/session-logs/, docs/governance/ — the historical record.
|
|
1667
|
+
// An ADR describes a decision at a point in time; rewriting it to match
|
|
1668
|
+
// today's vocabulary would erase the evidence that the words changed.
|
|
1669
|
+
//
|
|
1670
|
+
// The ALLOWLIST below is deliberately tiny and each entry carries its reason. A
|
|
1671
|
+
// rule that passes because everything is suppressed reads as evidence while
|
|
1672
|
+
// proving nothing — if this list grows, that is the signal to finish the rename,
|
|
1673
|
+
// not to add a line.
|
|
1674
|
+
const VOCAB_ALLOW = [
|
|
1675
|
+
{
|
|
1676
|
+
file: 'modules/government/config.js',
|
|
1677
|
+
match: 'roles',
|
|
1678
|
+
why: 'the legacy INSTANCE-config key fallback. The core ships `ranks`, but an '
|
|
1679
|
+
+ 'external self-hosted instance may still have a config/government.json '
|
|
1680
|
+
+ 'authored as `roles`; reading it is what stops their templates vanishing '
|
|
1681
|
+
+ 'on upgrade. Removing this line would break them silently.',
|
|
1682
|
+
},
|
|
1683
|
+
{
|
|
1684
|
+
file: 'modules/government/config.js',
|
|
1685
|
+
match: 'governance_role_labels',
|
|
1686
|
+
why: 'the legacy ENV-VAR fallback. <PREFIX>_GOVERNMENT_RANK_LABELS is the current '
|
|
1687
|
+
+ 'name, but an operator sets this in their own shell or env file, outside this '
|
|
1688
|
+
+ 'repo, so the old spelling is still read. Dropping it would silently stop '
|
|
1689
|
+
+ 'honouring a label override someone already configured.',
|
|
1690
|
+
},
|
|
1691
|
+
{
|
|
1692
|
+
file: 'modules/government/routes/government.js',
|
|
1693
|
+
match: '/governance',
|
|
1694
|
+
why: 'the one-release 308 alias from the pre-rename route paths (ADR 0174). A '
|
|
1695
|
+
+ 'compatibility shim has to name the thing it is compatible with. Note the '
|
|
1696
|
+
+ 'match requires the leading SLASH — this permits citing the legacy PATH, not '
|
|
1697
|
+
+ 'the bare word. Delete with the alias block one release after the rename ships.',
|
|
1698
|
+
},
|
|
1699
|
+
{
|
|
1700
|
+
file: 'modules/government/module.json',
|
|
1701
|
+
match: 'role',
|
|
1702
|
+
why: 'the manifest states the rule itself — "The word `role` is deliberately '
|
|
1703
|
+
+ 'absent here". Meta-commentary about the ban has to be able to name it.',
|
|
1704
|
+
},
|
|
1705
|
+
];
|
|
1706
|
+
|
|
1707
|
+
// A line CITING a historical artifact by its real name: an ADR filename, the
|
|
1708
|
+
// docs/governance/ evidence folder, or one of the already-applied
|
|
1709
|
+
// governance_NNN migrations. These names are permanent (see the header), so a
|
|
1710
|
+
// reference to one is a citation, not drift.
|
|
1711
|
+
const VOCAB_HISTORICAL_REF = /docs\/adr\/|docs\/governance\/|docs\/session-logs\/|governance_0\d\d/;
|
|
1712
|
+
|
|
1713
|
+
const VOCAB_SURFACE = [
|
|
1714
|
+
'modules/government',
|
|
1715
|
+
'modules/hall-ui/public/government.js',
|
|
1716
|
+
'modules/hall-ui/public/government.html',
|
|
1717
|
+
'modules/hall-ui/public/government.css',
|
|
1718
|
+
];
|
|
1719
|
+
const VOCAB_EXTS = new Set(['.js', '.json', '.html', '.css']);
|
|
1720
|
+
const VOCAB_BANNED = [
|
|
1721
|
+
{ re: /\brole/i, label: 'role', hint: '"role" is reserved for the crafts (Artist / Ideator / Builder). On this surface the noun is RANK.' },
|
|
1722
|
+
{ re: /governance/i, label: 'governance', hint: 'the pre-ADR-0174 name of this surface. It is the GOVERNMENT now.' },
|
|
1723
|
+
];
|
|
1724
|
+
|
|
1725
|
+
function vocabFilesToScan() {
|
|
1726
|
+
const out = [];
|
|
1727
|
+
const walk = (abs, rel) => {
|
|
1728
|
+
let st;
|
|
1729
|
+
try { st = fs.statSync(abs); } catch { return; }
|
|
1730
|
+
if (st.isDirectory()) {
|
|
1731
|
+
// the historical migrations keep their names + bodies (see header)
|
|
1732
|
+
if (rel.endsWith('/migrations')) return;
|
|
1733
|
+
for (const e of fs.readdirSync(abs)) walk(path.join(abs, e), `${rel}/${e}`);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
if (VOCAB_EXTS.has(path.extname(abs))) out.push({ abs, rel });
|
|
1737
|
+
};
|
|
1738
|
+
for (const s of VOCAB_SURFACE) walk(path.join(CORE_ROOT, s), s);
|
|
1739
|
+
return out;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function checkVocabularyWall() {
|
|
1743
|
+
const violations = [];
|
|
1744
|
+
let scanned = 0;
|
|
1745
|
+
for (const { abs, rel } of vocabFilesToScan()) {
|
|
1746
|
+
scanned += 1;
|
|
1747
|
+
const lines = fs.readFileSync(abs, 'utf8').split('\n');
|
|
1748
|
+
lines.forEach((line, i) => {
|
|
1749
|
+
// `role="…"` is HTML's own ARIA attribute — a different word that happens to
|
|
1750
|
+
// be spelled the same. Strip attributes before matching rather than
|
|
1751
|
+
// allowlisting each occurrence: this is a property of HTML, not of any one file.
|
|
1752
|
+
const scanLine = line.replace(/\srole\s*=\s*("[^"]*"|'[^']*')/g, ' ');
|
|
1753
|
+
for (const b of VOCAB_BANNED) {
|
|
1754
|
+
if (!b.re.test(scanLine)) continue;
|
|
1755
|
+
// A REFERENCE to a historical artifact by its real name is not a
|
|
1756
|
+
// vocabulary violation — it is a citation. The artifacts themselves are
|
|
1757
|
+
// excluded by construction (see the header); naming one has to be legal
|
|
1758
|
+
// too, or the code could not point at the migration that created its own
|
|
1759
|
+
// tables. Same principle, applied to the line rather than the file.
|
|
1760
|
+
if (VOCAB_HISTORICAL_REF.test(scanLine)) continue;
|
|
1761
|
+
const excused = VOCAB_ALLOW.some((a) => a.file === rel && scanLine.toLowerCase().includes(a.match));
|
|
1762
|
+
if (excused) continue;
|
|
1763
|
+
violations.push(`${rel}:${i + 1}: "${b.label}" — ${b.hint} → ${line.trim().slice(0, 90)}`);
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
return {
|
|
1768
|
+
name: 'the word "role" means a craft, never a rank (ADR 0174)',
|
|
1769
|
+
ok: violations.length === 0,
|
|
1770
|
+
hardFail: violations.length > 0,
|
|
1771
|
+
violations,
|
|
1772
|
+
warnings: [],
|
|
1773
|
+
note: `${scanned} government-surface file(s) scanned for "role"/"governance"; `
|
|
1774
|
+
+ `${VOCAB_ALLOW.length} documented allowlist entr(y|ies). Historical migrations + docs/adr are excluded by construction, not suppressed.`,
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1643
1778
|
const CHECKS = [
|
|
1644
1779
|
checkCoreHostBoundary,
|
|
1645
1780
|
checkClaudeMdBudget,
|
|
@@ -1661,6 +1796,7 @@ const CHECKS = [
|
|
|
1661
1796
|
checkCoreRequiresExcludedConfig, // task 1824 — core→instance-config require guard
|
|
1662
1797
|
checkCoreRequiresExcludedModule, // Check 16b — task 2135: core→excluded-module require guard (R89 split)
|
|
1663
1798
|
checkErrorEnvelope, // Check 17 — BV1 R03 / task 1990: no route hand-rolls a flat error (ADR 0116)
|
|
1799
|
+
checkVocabularyWall, // Check 19 — ADR 0174 / goal 1000068 R07: "role" is a craft, never a rank
|
|
1664
1800
|
checkWriteRoutesValidated, // Check 18 — BV1 R12 / task 1999: every body-reading write route validates (ADR 0118)
|
|
1665
1801
|
];
|
|
1666
1802
|
|
|
@@ -1690,6 +1826,8 @@ function main() {
|
|
|
1690
1826
|
if (require.main === module) main();
|
|
1691
1827
|
|
|
1692
1828
|
module.exports = {
|
|
1829
|
+
checkVocabularyWall,
|
|
1830
|
+
VOCAB_ALLOW,
|
|
1693
1831
|
runAll,
|
|
1694
1832
|
resolvesToHostGame,
|
|
1695
1833
|
checkCoreHostBoundary,
|
|
@@ -44,7 +44,7 @@ const EMPTY = process.argv.includes('--empty');
|
|
|
44
44
|
// --rank: view the hall as a different rank — the /me fixture's builder.rank is
|
|
45
45
|
// overridden in the response, so the Archon-gated pages (watch, harbor, gate)
|
|
46
46
|
// render their real content instead of the sealed notice. Preview-only theatre:
|
|
47
|
-
// the real pages are server-gated (
|
|
47
|
+
// the real pages are server-gated (requireGovernmentPage / ADR 0016).
|
|
48
48
|
const RANK_OVERRIDE = arg('--rank');
|
|
49
49
|
const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
50
50
|
// --modules-on: force-report modules as enabled — a dev box's local config
|
package/src/bongos/auth.js
CHANGED
|
@@ -1191,7 +1191,7 @@ async function resolveSession(req) {
|
|
|
1191
1191
|
// inside the session's scope at all". That projection is deliberately the ROLE-FREE
|
|
1192
1192
|
// half — folding the builder's roles in here would change who can reach the
|
|
1193
1193
|
// bootstrap routes (a builder on a rank with no seeded governance role resolves to
|
|
1194
|
-
// zero permissions by design — see tests/
|
|
1194
|
+
// zero permissions by design — see tests/government_parity.mjs Part D), and R99 is
|
|
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
|
|
@@ -1437,12 +1437,12 @@ 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
|
-
//
|
|
1440
|
+
// requireGovernmentPage was rank === 'archon' floor archon → page.view.government
|
|
1441
1441
|
// That third gate was spelled `requireArchonPage` until ADR 0157 lowered
|
|
1442
1442
|
// page.view.government's floor archon→metic and the name stopped being true; the
|
|
1443
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
|
-
// (tests/
|
|
1445
|
+
// (tests/government_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
|
|
1447
1447
|
// admitted. That is a TIGHTENING, never a widening.
|
|
1448
1448
|
//
|
|
@@ -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
|
-
|
|
1460
|
-
|
|
1459
|
+
requireGovernmentPage: Object.freeze({ permission: 'page.view.government', deniedTo: '/builders' }),
|
|
1460
|
+
requireGovernmentManagePage: Object.freeze({ permission: 'government.manage', deniedTo: '/builders' }),
|
|
1461
1461
|
requireProjectCuratePage: Object.freeze({ permission: 'project.curate', deniedTo: '/builders' }),
|
|
1462
1462
|
});
|
|
1463
1463
|
|
|
@@ -1540,8 +1540,8 @@ async function requireBuilderPage(req, res, next) {
|
|
|
1540
1540
|
// every data endpoint these pages call is independently gated server-side, so no
|
|
1541
1541
|
// cross-builder data leaks even if this gate were bypassed; it keeps a builder
|
|
1542
1542
|
// without the permission from ever seeing the page shell.
|
|
1543
|
-
async function
|
|
1544
|
-
return runPageGate('
|
|
1543
|
+
async function requireGovernmentPage(req, res, next) {
|
|
1544
|
+
return runPageGate('requireGovernmentPage', req, res, next);
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
1547
|
// The /governance roles-and-permissions tab (BV1.R113) — gated on the
|
|
@@ -1549,8 +1549,8 @@ async function requireGovernancePage(req, res, next) {
|
|
|
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.
|
|
1552
|
-
async function
|
|
1553
|
-
return runPageGate('
|
|
1552
|
+
async function requireGovernmentManagePage(req, res, next) {
|
|
1553
|
+
return runPageGate('requireGovernmentManagePage', req, res, next);
|
|
1554
1554
|
}
|
|
1555
1555
|
|
|
1556
1556
|
// The /people builder-directory page (task 1002577) — gated on the
|
|
@@ -1827,8 +1827,8 @@ module.exports = {
|
|
|
1827
1827
|
SESSION_SCOPES,
|
|
1828
1828
|
requireBuilder,
|
|
1829
1829
|
requireBuilderPage,
|
|
1830
|
-
|
|
1831
|
-
|
|
1830
|
+
requireGovernmentPage,
|
|
1831
|
+
requireGovernmentManagePage,
|
|
1832
1832
|
requireNonXenosPage,
|
|
1833
1833
|
requireProjectCuratePage,
|
|
1834
1834
|
requireRank,
|
|
@@ -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,12 @@ 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)
|
|
802
|
+
// 1.19.187 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
803
|
+
// landed since 1.19.186 with no explicit bump. run 31978548058. (task 1002620)
|
|
800
804
|
// ---------------------------------------------------------------------------
|
|
801
|
-
const CORE_VERSION = '1.19.
|
|
805
|
+
const CORE_VERSION = '1.19.187'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
802
806
|
|
|
803
807
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
804
808
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/auth_page_gate.mjs
CHANGED
|
@@ -31,8 +31,8 @@ const auth = require('../src/bongos/auth.js');
|
|
|
31
31
|
|
|
32
32
|
// BV1.R107: the page gate now also resolves a permission (page.view.public) through
|
|
33
33
|
// the `governance` kernel port, so an authenticated request needs the port stubbed —
|
|
34
|
-
// exactly as tests/
|
|
35
|
-
// The audience is unchanged; tests/
|
|
34
|
+
// exactly as tests/government_require_permission.mjs stubs it for requirePermission.
|
|
35
|
+
// The audience is unchanged; tests/government_page_gates.mjs is the parity proof.
|
|
36
36
|
const seams = require('../src/module-seams.js');
|
|
37
37
|
const catalog = require('../modules/government/catalog.js');
|
|
38
38
|
seams._reset();
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_abuse_matrix.mjs — the abuse matrix (BV1.R112, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// Mechanically proves the safety guarantee the governance module asserts in
|
|
4
4
|
// prose: NO configuration surface can grant a `system: true` (trust-boundary)
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
// V12 seed integrity: no rank-role below archon holds a system key; no
|
|
33
33
|
// rank-role at all holds a principal key
|
|
34
34
|
//
|
|
35
|
-
// DB-free (the
|
|
35
|
+
// DB-free (the government_routes.mjs pattern): real router + real
|
|
36
36
|
// requirePermission over a stub `governance` port, db sibling monkeypatched.
|
|
37
37
|
//
|
|
38
|
-
// Run: node tests/
|
|
38
|
+
// Run: node tests/government_abuse_matrix.mjs
|
|
39
39
|
|
|
40
40
|
import { strict as assert } from 'node:assert';
|
|
41
41
|
import { test, after } from 'node:test';
|
|
@@ -108,16 +108,16 @@ test('every system permission floors at archon or is a principal (never seeded l
|
|
|
108
108
|
|
|
109
109
|
const calls = [];
|
|
110
110
|
const stub = {
|
|
111
|
-
|
|
111
|
+
getRankByKey: async (k) => (SEEDED_ROLES.includes(k)
|
|
112
112
|
? { rank_key: k, label: k, is_system: true, seeded_rank: k }
|
|
113
113
|
: { rank_key: k, label: k, is_system: false, seeded_rank: null }),
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
createRank: async (a) => ({ rank_key: a.rankKey, label: a.label, description: a.description ?? null, is_system: false, seeded_rank: null, created_by: a.createdBy, created_at: 't' }),
|
|
115
|
+
grantRankPermission: async () => true,
|
|
116
|
+
revokeRankPermission: async () => true,
|
|
117
|
+
resetRankGrants: async ({ permissionKeys }) => permissionKeys.length,
|
|
118
|
+
assignRank: async () => true,
|
|
119
|
+
unassignRank: async () => true,
|
|
120
|
+
listRanksWithGrants: async () => [],
|
|
121
121
|
listAssignments: async () => [],
|
|
122
122
|
};
|
|
123
123
|
const REAL = {};
|
|
@@ -138,11 +138,11 @@ function grant(keys) {
|
|
|
138
138
|
}
|
|
139
139
|
grant(['government.manage']);
|
|
140
140
|
|
|
141
|
-
const
|
|
141
|
+
const buildGovernmentRouter = require(path.join(ROOT, 'modules', 'government', 'routes', 'government.js'));
|
|
142
142
|
const app = express();
|
|
143
143
|
app.use(express.json());
|
|
144
144
|
app.use(attachFail);
|
|
145
|
-
app.use(
|
|
145
|
+
app.use(buildGovernmentRouter());
|
|
146
146
|
const server = http.createServer(app);
|
|
147
147
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
148
148
|
const BASE = `http://127.0.0.1:${server.address().port}`;
|
|
@@ -171,7 +171,7 @@ test('V1: EVERY system permission is refused onto a custom role, before the db',
|
|
|
171
171
|
const r = await call('PUT', `/government/ranks/custom-role/permissions/${k}`);
|
|
172
172
|
assert.equal(r.status, 400, `${k} must be refused`);
|
|
173
173
|
assert.equal(r.json.error.code, 'permission_not_grantable', k);
|
|
174
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
174
|
+
assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 0,
|
|
175
175
|
`${k}: the refusal must land BEFORE any db write`);
|
|
176
176
|
}
|
|
177
177
|
});
|
|
@@ -188,13 +188,13 @@ test('V2a: the ARCHON rank-role refuses EVERY write — grant, revoke and reset
|
|
|
188
188
|
const r = await call(method, `/government/ranks/archon/permissions/${k}`);
|
|
189
189
|
assert.equal(r.status, 403, `archon ← ${method} ${k} must be refused`);
|
|
190
190
|
assert.equal(r.json.error.code, 'archon_rank_immutable', `archon ← ${method} ${k}`);
|
|
191
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
191
|
+
assert.equal(calls.filter(([c]) => c === 'grantRankPermission' || c === 'revokeRankPermission').length, 0);
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
const r = await call('POST', '/government/ranks/archon/reset');
|
|
195
195
|
assert.equal(r.status, 403, 'reset must not be a back door into the archon role');
|
|
196
196
|
assert.equal(r.json.error.code, 'archon_rank_immutable');
|
|
197
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
197
|
+
assert.equal(calls.filter(([c]) => c === 'resetRankGrants').length, 0);
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL key', async () => {
|
|
@@ -204,7 +204,7 @@ test('V2b: EVERY editable rank-role still refuses every SUBSTRATE and PRINCIPAL
|
|
|
204
204
|
const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
|
|
205
205
|
assert.equal(r.status, 400, `${role} ← ${k} must be refused`);
|
|
206
206
|
assert.equal(r.json.error.code, 'permission_not_grantable', `${role} ← ${k}`);
|
|
207
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
207
|
+
assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 0,
|
|
208
208
|
`${role} ← ${k}: the refusal must land BEFORE any db write`);
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -215,7 +215,7 @@ test('V2c: an editable rank-role ACCEPTS the in-app owner powers (the ADR 0172 l
|
|
|
215
215
|
for (const k of [...OWNER_POWERS, 'idea.triage']) {
|
|
216
216
|
const r = await call('PUT', `/government/ranks/${role}/permissions/${k}`);
|
|
217
217
|
assert.equal(r.status, 200, `${role} ← ${k} must be ACCEPTED — this delegation is the owner's decision (ADR 0172)`);
|
|
218
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
218
|
+
assert.equal(calls.filter(([c]) => c === 'grantRankPermission').length, 1, `${role} ← ${k} must reach the db`);
|
|
219
219
|
}
|
|
220
220
|
}
|
|
221
221
|
});
|
|
@@ -224,7 +224,7 @@ test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
|
|
|
224
224
|
for (const role of EDITABLE_RANK_ROLES) {
|
|
225
225
|
const r = await call('POST', `/government/ranks/${role}/reset`);
|
|
226
226
|
assert.equal(r.status, 200, `${role} must be resettable`);
|
|
227
|
-
const [, args] = calls.find(([c]) => c === '
|
|
227
|
+
const [, args] = calls.find(([c]) => c === 'resetRankGrants');
|
|
228
228
|
assert.deepEqual([...args.permissionKeys].sort(), [...catalog.seedForRank(role)].sort(),
|
|
229
229
|
`${role} reset must write exactly the floor-derived seed`);
|
|
230
230
|
for (const k of args.permissionKeys) {
|
|
@@ -236,7 +236,7 @@ test('V2d: reset restores the CATALOG seed, never anything wider', async () => {
|
|
|
236
236
|
const r = await call('POST', '/government/ranks/custom-role/reset');
|
|
237
237
|
assert.equal(r.status, 400);
|
|
238
238
|
assert.equal(r.json.error.code, 'rank_has_no_default');
|
|
239
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
239
|
+
assert.equal(calls.filter(([c]) => c === 'resetRankGrants').length, 0);
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
test('V3: EVERY seeded rank-role refuses assignment (rank is the only writer — R95b)', async () => {
|
|
@@ -244,7 +244,7 @@ test('V3: EVERY seeded rank-role refuses assignment (rank is the only writer —
|
|
|
244
244
|
const r = await call('POST', '/government/assignments', { builder_id: 7, rank_key: role });
|
|
245
245
|
assert.equal(r.status, 403, `assigning seeded role ${role} must be refused`);
|
|
246
246
|
assert.equal(r.json.error.code, 'seeded_rank_assignment_forbidden', role);
|
|
247
|
-
assert.equal(calls.filter(([c]) => c === '
|
|
247
|
+
assert.equal(calls.filter(([c]) => c === 'assignRank').length, 0);
|
|
248
248
|
}
|
|
249
249
|
});
|
|
250
250
|
|
|
@@ -264,13 +264,13 @@ 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.
|
|
270
|
-
const c = calls.find(([k]) => k === '
|
|
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
|
+
const c = calls.find(([k]) => k === 'createRank');
|
|
271
271
|
assert.deepEqual(Object.keys(c[1]).sort(), ['createdBy', 'description', 'label', 'rankKey'],
|
|
272
272
|
'nothing beyond the four whitelisted fields reaches the db');
|
|
273
|
-
assert.equal(calls.filter(([k]) => k === '
|
|
273
|
+
assert.equal(calls.filter(([k]) => k === 'grantRankPermission').length, 0);
|
|
274
274
|
});
|
|
275
275
|
|
|
276
276
|
test('V5: a caller without government.manage cannot reach ANY write vector at all', async () => {
|
|
@@ -295,35 +295,35 @@ test('V5: a caller without government.manage cannot reach ANY write vector at al
|
|
|
295
295
|
// ===========================================================================
|
|
296
296
|
|
|
297
297
|
test('V6: a config template proposing EVERY system key resolves with all of them stripped', () => {
|
|
298
|
-
const resolved = govConfig.
|
|
299
|
-
instance: {
|
|
298
|
+
const resolved = govConfig.resolveGovernmentConfig({
|
|
299
|
+
instance: { ranks: { 'evil-template': { label: 'Evil', permissions: [...SYSTEM_KEYS, 'no.such.key', 'idea.triage'] } } },
|
|
300
300
|
env: {},
|
|
301
301
|
});
|
|
302
|
-
const evil = resolved.
|
|
302
|
+
const evil = resolved.ranks['evil-template'];
|
|
303
303
|
assert.deepEqual([...evil.permissions], ['idea.triage'],
|
|
304
304
|
'only the operational key survives; every system + unknown key is stripped');
|
|
305
305
|
});
|
|
306
306
|
|
|
307
307
|
test('V6: a seeded rank-role template cannot re-grant — its permissions drop wholesale', () => {
|
|
308
|
-
const resolved = govConfig.
|
|
309
|
-
instance: {
|
|
308
|
+
const resolved = govConfig.resolveGovernmentConfig({
|
|
309
|
+
instance: { ranks: { thetes: { label: 'Upgraded', permissions: ['idea.triage', ...SYSTEM_KEYS] } } },
|
|
310
310
|
env: {},
|
|
311
311
|
});
|
|
312
|
-
assert.deepEqual([...resolved.
|
|
312
|
+
assert.deepEqual([...resolved.ranks.thetes.permissions], [],
|
|
313
313
|
'a seeded role takes NO permissions from config, operational ones included');
|
|
314
314
|
});
|
|
315
315
|
|
|
316
316
|
test('V6: the env layer relabels only — it cannot add a permission or a role', () => {
|
|
317
317
|
// The role must exist in the merged config for the relabel to land (an env
|
|
318
318
|
// var can only RELABEL a declared role) — mirror the neutral starter's shape.
|
|
319
|
-
const resolved = govConfig.
|
|
320
|
-
neutral: {
|
|
319
|
+
const resolved = govConfig.resolveGovernmentConfig({
|
|
320
|
+
neutral: { ranks: { metic: { label: 'Metic' } } },
|
|
321
321
|
instance: {},
|
|
322
322
|
env: { OTB_GOVERNANCE_ROLE_LABELS: 'metic=Overlord,ghost-role=Ghost' },
|
|
323
323
|
});
|
|
324
|
-
assert.equal(resolved.
|
|
325
|
-
assert.equal(resolved.
|
|
326
|
-
assert.deepEqual([...resolved.
|
|
324
|
+
assert.equal(resolved.ranks.metic.label, 'Overlord', 'relabeling works');
|
|
325
|
+
assert.equal(resolved.ranks['ghost-role'], undefined, 'an env var cannot mint a role');
|
|
326
|
+
assert.deepEqual([...resolved.ranks.metic.permissions], [],
|
|
327
327
|
'the relabeled seeded role still takes no grants from config or env');
|
|
328
328
|
});
|
|
329
329
|
|
|
@@ -438,7 +438,7 @@ test('V11: the substrate checker is structurally grant-blind (node builtins only
|
|
|
438
438
|
|
|
439
439
|
test('V12: no rank-role below archon holds a system key; none holds a principal key', () => {
|
|
440
440
|
for (const rank of SEEDED_ROLES) {
|
|
441
|
-
const seed = catalog.
|
|
441
|
+
const seed = catalog.RANK_SEED[rank];
|
|
442
442
|
for (const k of seed) {
|
|
443
443
|
assert.equal(catalog.isPrincipalKey(k), false, `${rank} seeds principal key ${k}`);
|
|
444
444
|
if (rank !== 'archon') {
|
|
@@ -446,7 +446,7 @@ test('V12: no rank-role below archon holds a system key; none holds a principal
|
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
|
-
const archonSystem = catalog.
|
|
449
|
+
const archonSystem = catalog.RANK_SEED.archon.filter((k) => catalog.isSystem(k)).sort();
|
|
450
450
|
const expected = SYSTEM_KEYS.filter((k) => catalog.byKey(k).floor === 'archon').sort();
|
|
451
451
|
assert.deepEqual(archonSystem, expected,
|
|
452
452
|
'archon holds exactly the archon-floored system keys — principals stay out of every rank-role');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_catalog.mjs — the permission catalog + compat seed (BV1.R94, ADR 0151).
|
|
2
2
|
//
|
|
3
3
|
// Guards the "zero day-one behavior change" promise structurally: the seed is
|
|
4
4
|
// DERIVED from per-permission floors, so these invariants prove the derivation is
|
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
const require = createRequire(import.meta.url);
|
|
13
13
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
14
|
const cat = require(path.join(ROOT, 'modules', 'government', 'catalog.js'));
|
|
15
|
-
const { PERMISSIONS, RANK_ORDER,
|
|
15
|
+
const { PERMISSIONS, RANK_ORDER, RANK_SEED, permissionKeys, byKey, isSystem, isGrantable, systemKeys, grantableKeys, seedForRank } = cat;
|
|
16
16
|
|
|
17
17
|
test('every permission is well-formed (key, boolean system, valid floor)', () => {
|
|
18
18
|
assert.ok(PERMISSIONS.length >= 60, `expected a substantial catalog, saw ${PERMISSIONS.length}`);
|
|
@@ -98,17 +98,17 @@ test('isGrantableTo fails closed on every unrecognised input', () => {
|
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
test('the four rank-roles exist; seed keys all reference real permissions', () => {
|
|
101
|
-
assert.deepEqual(Object.keys(
|
|
101
|
+
assert.deepEqual(Object.keys(RANK_SEED).sort(), [...RANK_ORDER].sort());
|
|
102
102
|
const all = new Set(permissionKeys());
|
|
103
103
|
for (const rank of RANK_ORDER) {
|
|
104
|
-
for (const k of
|
|
104
|
+
for (const k of RANK_SEED[rank]) assert.ok(all.has(k), `seed[${rank}] references unknown ${k}`);
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test('seed is cumulative: archon ⊇ metic ⊇ thetes ⊇ xenos (nested supersets)', () => {
|
|
109
109
|
for (let i = 1; i < RANK_ORDER.length; i++) {
|
|
110
|
-
const lower = new Set(
|
|
111
|
-
const higher = new Set(
|
|
110
|
+
const lower = new Set(RANK_SEED[RANK_ORDER[i - 1]]);
|
|
111
|
+
const higher = new Set(RANK_SEED[RANK_ORDER[i]]);
|
|
112
112
|
for (const k of lower) assert.ok(higher.has(k), `${RANK_ORDER[i]} missing ${k} that ${RANK_ORDER[i - 1]} has`);
|
|
113
113
|
assert.ok(higher.size > lower.size, `${RANK_ORDER[i]} should strictly extend ${RANK_ORDER[i - 1]}`);
|
|
114
114
|
}
|
|
@@ -116,7 +116,7 @@ test('seed is cumulative: archon ⊇ metic ⊇ thetes ⊇ xenos (nested superset
|
|
|
116
116
|
|
|
117
117
|
test('a role holds a permission iff its floor is a real rank at-or-below (derivation is faithful)', () => {
|
|
118
118
|
for (const rank of RANK_ORDER) {
|
|
119
|
-
const held = new Set(
|
|
119
|
+
const held = new Set(RANK_SEED[rank]);
|
|
120
120
|
const tier = RANK_ORDER.indexOf(rank);
|
|
121
121
|
for (const p of PERMISSIONS) {
|
|
122
122
|
const shouldHold = p.floor !== null && RANK_ORDER.indexOf(p.floor) <= tier;
|
|
@@ -127,12 +127,12 @@ test('a role holds a permission iff its floor is a real rank at-or-below (deriva
|
|
|
127
127
|
|
|
128
128
|
test('system permissions are Archon-only in the seed (never in xenos/thetes/metic)', () => {
|
|
129
129
|
for (const rank of ['xenos', 'thetes', 'metic']) {
|
|
130
|
-
for (const k of
|
|
130
|
+
for (const k of RANK_SEED[rank]) {
|
|
131
131
|
assert.equal(isSystem(k), false, `${rank} seed must not hold system perm ${k}`);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
// and archon DOES hold the archon-floored system perms (that is how Archon gets its power)
|
|
135
|
-
const archon = new Set(
|
|
135
|
+
const archon = new Set(RANK_SEED.archon);
|
|
136
136
|
assert.ok(archon.has('builder.rank.set'));
|
|
137
137
|
assert.ok(archon.has('deploy.push_main'));
|
|
138
138
|
});
|
|
@@ -142,14 +142,14 @@ test('machine principals are held by NO rank-role and are system + floor null',
|
|
|
142
142
|
assert.equal(p.system, true, `${p.key} must be system`);
|
|
143
143
|
assert.equal(p.floor, null, `${p.key} must have floor null (held by no rank-role)`);
|
|
144
144
|
for (const rank of RANK_ORDER) {
|
|
145
|
-
assert.ok(!
|
|
145
|
+
assert.ok(!RANK_SEED[rank].includes(p.key), `${rank} must not hold principal ${p.key}`);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
assert.ok(PERMISSIONS.some((p) => p.key === 'principal.bfg'));
|
|
149
149
|
});
|
|
150
150
|
|
|
151
151
|
test('threshold spot-checks reproduce today\'s requireRank floors', () => {
|
|
152
|
-
const has = (rank, k) =>
|
|
152
|
+
const has = (rank, k) => RANK_SEED[rank].includes(k);
|
|
153
153
|
// idea.triage: metic+ only (requireRank('metic','archon'))
|
|
154
154
|
assert.equal(has('xenos', 'idea.triage'), false);
|
|
155
155
|
assert.equal(has('thetes', 'idea.triage'), false);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// tests/
|
|
1
|
+
// tests/government_claim_eligibility.mjs — claim eligibility as PERMISSIONS
|
|
2
2
|
// (BV1.R106, ADR 0151).
|
|
3
3
|
//
|
|
4
4
|
// DB-free. The whole point of R106 is that the queue half of claim-time authority
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
// GENERAL_QUEUE_RANKS and knows nothing about catalog floors.
|
|
24
24
|
// The new side reads catalog.seedForRank. A check whose two sides both read
|
|
25
25
|
// catalog.floor would pass while proving nothing (the lesson tests/
|
|
26
|
-
//
|
|
26
|
+
// government_parity.mjs writes down); neither oracle here does.
|
|
27
27
|
import assert from 'node:assert/strict';
|
|
28
28
|
import { test } from 'node:test';
|
|
29
29
|
import { createRequire } from 'node:module';
|