@bongos/core 1.19.634 → 1.19.636
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 +20 -15
- package/docs/module-api-changelog.md +4 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/fitness-checks-route-pins.js +76 -0
- package/scripts/gds/fitness.js +3 -13
- package/src/bongos/route-rank-check.js +90 -3
- package/src/module-api.js +1 -1
- package/tests/fitness.mjs +11 -0
- package/tests/route_rank_check.mjs +122 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.636",
|
|
6
|
+
"core_contract": "1.19.636",
|
|
7
|
+
"source_commit": "7e7d5caa1a0c5ff310bdab82a7679fd56e2f71d7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-09-
|
|
9
|
+
"built_at": "2026-09-10T03:45:41.270Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 468,
|
|
13
13
|
"agent_docs_stubbed": 24,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2100,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2592,
|
|
20
|
+
"tree_sha256": "abf5929f41b5c8b538294d87d01632069ad0371e2e2eed440b97a9727a33d426",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/backlog-review/SKILL.md",
|
|
@@ -2777,7 +2777,7 @@
|
|
|
2777
2777
|
{
|
|
2778
2778
|
"path": "docs/module-api-changelog.md",
|
|
2779
2779
|
"mode": "0000644",
|
|
2780
|
-
"sha256": "
|
|
2780
|
+
"sha256": "74c46d5efae11f3adc3f0cb79bec6757e0f7bb820e7f77c1f66cd6fc083a503c"
|
|
2781
2781
|
},
|
|
2782
2782
|
{
|
|
2783
2783
|
"path": "docs/modules-contract.md",
|
|
@@ -7707,12 +7707,12 @@
|
|
|
7707
7707
|
{
|
|
7708
7708
|
"path": "package-lock.json",
|
|
7709
7709
|
"mode": "0000644",
|
|
7710
|
-
"sha256": "
|
|
7710
|
+
"sha256": "7e8bd5c903c8f50fa7bcc80c7e317a4ea202bf079e0e63a8beafc988718953e1"
|
|
7711
7711
|
},
|
|
7712
7712
|
{
|
|
7713
7713
|
"path": "package.json",
|
|
7714
7714
|
"mode": "0000644",
|
|
7715
|
-
"sha256": "
|
|
7715
|
+
"sha256": "0e2c886ce78f829a2b694c3c49af7fafff6297695da0ddb346d7c0936fe95ff8"
|
|
7716
7716
|
},
|
|
7717
7717
|
{
|
|
7718
7718
|
"path": "public-docs/index.html",
|
|
@@ -8144,6 +8144,11 @@
|
|
|
8144
8144
|
"mode": "0000644",
|
|
8145
8145
|
"sha256": "a34da5bd47c0410db31344ae042bae318e6daf0e774672999f14213c3c1180c5"
|
|
8146
8146
|
},
|
|
8147
|
+
{
|
|
8148
|
+
"path": "scripts/gds/fitness-checks-route-pins.js",
|
|
8149
|
+
"mode": "0000644",
|
|
8150
|
+
"sha256": "4bd0d9c3b39a6f8af4561925835ebf9b349170be36aef926c6af2160af0d1ab5"
|
|
8151
|
+
},
|
|
8147
8152
|
{
|
|
8148
8153
|
"path": "scripts/gds/fitness-checks-vocabulary.js",
|
|
8149
8154
|
"mode": "0000644",
|
|
@@ -8167,7 +8172,7 @@
|
|
|
8167
8172
|
{
|
|
8168
8173
|
"path": "scripts/gds/fitness.js",
|
|
8169
8174
|
"mode": "0000644",
|
|
8170
|
-
"sha256": "
|
|
8175
|
+
"sha256": "610d76cee575fa1a87f2ff7bb84f5576f3c4a8cf2e727e6c372d76ebf9a0a340"
|
|
8171
8176
|
},
|
|
8172
8177
|
{
|
|
8173
8178
|
"path": "scripts/gds/gate-review.js",
|
|
@@ -9312,7 +9317,7 @@
|
|
|
9312
9317
|
{
|
|
9313
9318
|
"path": "src/bongos/route-rank-check.js",
|
|
9314
9319
|
"mode": "0000644",
|
|
9315
|
-
"sha256": "
|
|
9320
|
+
"sha256": "f57293c2d157949ccb5a74511f167902adf096037b4830b0bf667a38baf6ab58"
|
|
9316
9321
|
},
|
|
9317
9322
|
{
|
|
9318
9323
|
"path": "src/bongos/routes.js",
|
|
@@ -9452,7 +9457,7 @@
|
|
|
9452
9457
|
{
|
|
9453
9458
|
"path": "src/module-api.js",
|
|
9454
9459
|
"mode": "0000644",
|
|
9455
|
-
"sha256": "
|
|
9460
|
+
"sha256": "4c4fcf6a9833c72d6c371208d00afc60eba49cd07e5a0c9b4cd384133089340d"
|
|
9456
9461
|
},
|
|
9457
9462
|
{
|
|
9458
9463
|
"path": "src/module-loader/catalog.js",
|
|
@@ -10482,7 +10487,7 @@
|
|
|
10482
10487
|
{
|
|
10483
10488
|
"path": "tests/fitness.mjs",
|
|
10484
10489
|
"mode": "0000644",
|
|
10485
|
-
"sha256": "
|
|
10490
|
+
"sha256": "2208a1e4d3df3ce712c4e4dbc7908d20520833375a2c07c3da5d37252dd56b7a"
|
|
10486
10491
|
},
|
|
10487
10492
|
{
|
|
10488
10493
|
"path": "tests/fitness_gds_ratchet.mjs",
|
|
@@ -12202,7 +12207,7 @@
|
|
|
12202
12207
|
{
|
|
12203
12208
|
"path": "tests/route_rank_check.mjs",
|
|
12204
12209
|
"mode": "0000644",
|
|
12205
|
-
"sha256": "
|
|
12210
|
+
"sha256": "755f2c2c1cd929f0a4449ceb1c4bd0ac719aadf64c56484e03b4bed57e6daf7c"
|
|
12206
12211
|
},
|
|
12207
12212
|
{
|
|
12208
12213
|
"path": "tests/route_shadow_guard.mjs",
|
|
@@ -1717,5 +1717,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1717
1717
|
landed since 1.19.632 with no explicit bump. run 34417769106. (task 1002620)
|
|
1718
1718
|
1.19.634 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1719
1719
|
landed since 1.19.633 with no explicit bump. run 34429621784. (task 1002620)
|
|
1720
|
+
1.19.635 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1721
|
+
landed since 1.19.634 with no explicit bump. run 34429906252. (task 1002620)
|
|
1722
|
+
1.19.636 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1723
|
+
landed since 1.19.635 with no explicit bump. run 34434517065. (task 1002620)
|
|
1720
1724
|
---------------------------------------------------------------------------
|
|
1721
1725
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.636",
|
|
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.636",
|
|
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.636",
|
|
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",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// scripts/gds/fitness-checks-route-pins.js — Check 4b (task 1003192): every
|
|
2
|
+
// MODULE_EXPECTED_RANKS key names a real module directory.
|
|
3
|
+
//
|
|
4
|
+
// Carved out of scripts/gds/fitness.js on the task-1003206 precedent (the R03
|
|
5
|
+
// de-monolith plan): that file sits against the 1,500-line ratchet with two lines
|
|
6
|
+
// of headroom, so a new check lands here rather than tipping it over. fitness.js
|
|
7
|
+
// imports the name straight back, so the CHECKS registry and the module's public
|
|
8
|
+
// surface — which the tests read heavily — are unchanged.
|
|
9
|
+
//
|
|
10
|
+
// WHY THE CHECK EXISTS. src/bongos/route-rank-check.js pins the expected rank of
|
|
11
|
+
// each privileged module route, but pin SCOPE derives from successfully-classified
|
|
12
|
+
// routes: a MODULE_EXPECTED_RANKS key matching no discovered module is dropped and
|
|
13
|
+
// its pins pass VACUOUSLY, while the loader keeps mounting modules/<key>/routes/ in
|
|
14
|
+
// production by the same convention the scan discovers them by. Measured on the real
|
|
15
|
+
// classifier: with modules/security/ renamed, all six security pins went inert and a
|
|
16
|
+
// POST /security/reports downgraded to any-builder drew zero findings — a pinned
|
|
17
|
+
// privileged WRITE route silently unguarded.
|
|
18
|
+
//
|
|
19
|
+
// THIS IS THE SOURCE-REPO HALF of a two-layer guard. checkRouteRanks fires when a
|
|
20
|
+
// module directory is PRESENT but contributed no route (a rename/move under a live
|
|
21
|
+
// module); it deliberately stays silent on a wholly absent directory, because it runs
|
|
22
|
+
// on every ship and an instance may legitimately omit an optional module. Here every
|
|
23
|
+
// module is expected to exist, so a whole-directory rename or a typo'd key — neither
|
|
24
|
+
// of which the other layer can see — hard-fails. The governance→government rename had
|
|
25
|
+
// to hand-carry double-keyed pins precisely because nothing mechanical caught this.
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const path = require('node:path');
|
|
31
|
+
|
|
32
|
+
const { REPO_ROOT } = require('./fitness-lib.js');
|
|
33
|
+
const { checkRouteRanks, MODULE_EXPECTED_RANKS } = require('../../src/bongos/route-rank-check');
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Check 4 — no route bypasses server-side rank checks (ADR 0016) [reuses route-rank-check]
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Moved here with Check 4b (task 1003192): the two are the same subject — Check 4 asks
|
|
39
|
+
// whether each route carries a gate, 4b whether the pins that assert those gates are
|
|
40
|
+
// actually in scope — and keeping them together is what buys fitness.js headroom against
|
|
41
|
+
// the 1,500-line ratchet instead of leaving it sitting exactly on the cap.
|
|
42
|
+
function checkRankGuards() {
|
|
43
|
+
const r = checkRouteRanks();
|
|
44
|
+
// hardFail = an ungated WRITE route or a downgraded privileged route. Unclassified read routes
|
|
45
|
+
// are 'concerns' (the Hacker adjudicates intent) — surfaced as warnings, not a hard failure.
|
|
46
|
+
const violations = r.findings.filter((f) => f.severity === 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
47
|
+
const warnings = r.findings.filter((f) => f.severity !== 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
48
|
+
return { name: 'route rank guards (ADR 0016)', ok: !r.hardFail, hardFail: r.hardFail, violations, warnings, note: `${r.routeCount} routes classify to a known rank; no ungated writes, no privileged downgrades.` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Check 4b — module rank-pin keys resolve to a real modules/<key>/ dir
|
|
53
|
+
// ============================================================================
|
|
54
|
+
function checkModulePinKeysResolve() {
|
|
55
|
+
const violations = [];
|
|
56
|
+
for (const key of Object.keys(MODULE_EXPECTED_RANKS)) {
|
|
57
|
+
if (!fs.existsSync(path.join(REPO_ROOT, 'modules', key))) {
|
|
58
|
+
violations.push(`MODULE_EXPECTED_RANKS.${key} names no modules/${key}/ directory — its `
|
|
59
|
+
+ `${Object.keys(MODULE_EXPECTED_RANKS[key]).length} rank pin(s) are INERT and pass vacuously `
|
|
60
|
+
+ '(src/bongos/route-rank-check.js). Rename the key to the module\'s current directory, or drop the entry.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
name: 'module rank-pin keys resolve (task 1003192)',
|
|
65
|
+
ok: violations.length === 0,
|
|
66
|
+
hardFail: violations.length > 0,
|
|
67
|
+
violations,
|
|
68
|
+
warnings: [],
|
|
69
|
+
note: `${Object.keys(MODULE_EXPECTED_RANKS).length} module pin blocks all name a live modules/<key>/ directory.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
checkRankGuards,
|
|
75
|
+
checkModulePinKeysResolve,
|
|
76
|
+
};
|
package/scripts/gds/fitness.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
const fs = require('node:fs');
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
const { execFileSync } = require('node:child_process');
|
|
26
|
-
const {
|
|
26
|
+
const { checkRankGuards, checkModulePinKeysResolve } = require('./fitness-checks-route-pins.js'); // Checks 4 + 4b — carved out by task 1003192 (the 1,500-line ratchet)
|
|
27
27
|
const genApiDocs = require('./gen-api-docs'); // R12 (#1999): reuse the route model (readsBody/validated) that drives the OpenAPI spec
|
|
28
28
|
const { parseRoutineFrontmatter } = require('./run-routine'); // System 4 (ADR 0078): deterministic-routine boundary
|
|
29
29
|
const { filenameViolation, stemCollisions, CORE_NS, INSTANCE_NS } = require('./migration-namespace'); // ADR 0108 §5: cross-repo migration namespacing
|
|
@@ -151,18 +151,6 @@ function checkNestedDocsPresent() {
|
|
|
151
151
|
return { name: 'nested CLAUDE.md presence', ok: violations.length === 0, hardFail: violations.length > 0, violations, warnings: [], note: `present for: ${HIGH_TRAFFIC.join(', ')}.` };
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// ============================================================================
|
|
155
|
-
// Check 4 — no route bypasses server-side rank checks (ADR 0016) [reuses route-rank-check]
|
|
156
|
-
// ============================================================================
|
|
157
|
-
function checkRankGuards() {
|
|
158
|
-
const r = checkRouteRanks();
|
|
159
|
-
// hardFail = an ungated WRITE route or a downgraded privileged route. Unclassified read routes
|
|
160
|
-
// are 'concerns' (the Hacker adjudicates intent) — surfaced as warnings, not a hard failure.
|
|
161
|
-
const violations = r.findings.filter((f) => f.severity === 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
162
|
-
const warnings = r.findings.filter((f) => f.severity !== 'blocker').map((f) => `${f.route} (${f.file}) — ${f.summary}`);
|
|
163
|
-
return { name: 'route rank guards (ADR 0016)', ok: !r.hardFail, hardFail: r.hardFail, violations, warnings, note: `${r.routeCount} routes classify to a known rank; no ungated writes, no privileged downgrades.` };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
154
|
// ============================================================================
|
|
167
155
|
// Check 5 — generated artifacts are fresh (Path C enforces Path B; ADR 0063 + ADR 0062 §8)
|
|
168
156
|
// ============================================================================
|
|
@@ -1357,6 +1345,7 @@ const CHECKS = [
|
|
|
1357
1345
|
checkSkillFrontmatter,
|
|
1358
1346
|
checkNestedDocsPresent,
|
|
1359
1347
|
checkRankGuards,
|
|
1348
|
+
checkModulePinKeysResolve,
|
|
1360
1349
|
checkGeneratedArtifactsFresh,
|
|
1361
1350
|
checkDeterministicRoutines,
|
|
1362
1351
|
checkCoreNeverImportsModule,
|
|
@@ -1443,6 +1432,7 @@ module.exports = {
|
|
|
1443
1432
|
checkClaudeMdBudget,
|
|
1444
1433
|
checkNestedDocsPresent,
|
|
1445
1434
|
checkRankGuards,
|
|
1435
|
+
checkModulePinKeysResolve,
|
|
1446
1436
|
checkGeneratedArtifactsFresh,
|
|
1447
1437
|
checkDeterministicRoutines,
|
|
1448
1438
|
checkCoreNeverImportsModule,
|
|
@@ -517,6 +517,61 @@ function rankFromRankGate(tail) {
|
|
|
517
517
|
return null;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
// The source text of ONE route declaration — `router.X(` through its matching `)`.
|
|
521
|
+
//
|
|
522
|
+
// Why this exists (task 1003190, from the task-1002671 adversarial verification):
|
|
523
|
+
// a route's middleware list IS its parenthesised argument list, so the honest bound
|
|
524
|
+
// on "what gates this route" is the declaration's own paren balance. Every earlier
|
|
525
|
+
// bound was a heuristic tail that cut at the next thing RESEMBLING a sibling, which
|
|
526
|
+
// meant anything between the route and that marker still read as this route's gate.
|
|
527
|
+
// Three shapes got through, each certifying an UNGATED route as gated with real
|
|
528
|
+
// `via:'middleware'` provenance — passing the ungated-write blocker and satisfying
|
|
529
|
+
// the EXPECTED_RANKS pins (the task-1002553 fix only stops `via:'annotation'`):
|
|
530
|
+
// 1. a non-route gate below it — `const guard = auth.requirePermission('…')`
|
|
531
|
+
// 2. an inline classic handler — `function (req, res) {` never matches `=> {`
|
|
532
|
+
// 3. a middleware array below it — `const chain = [auth.requireRank('archon')]`
|
|
533
|
+
// Shape 1 is the general case: task 1003062's next-`router.` clamp closed only the
|
|
534
|
+
// sibling-ROUTE spelling of it, so the originally-filed repro no longer reproduces
|
|
535
|
+
// while the hole itself stayed open.
|
|
536
|
+
//
|
|
537
|
+
// Scans quote-, template- and comment-aware so a `)` inside a route path, a string
|
|
538
|
+
// default, or a comment cannot end the span early. Returns the whole text unchanged
|
|
539
|
+
// when the opening `(` is absent or never closes inside the window — an unbalanced
|
|
540
|
+
// declaration is not evidence of a narrower gate, so the caller keeps its prior
|
|
541
|
+
// (wider, fail-loud) view rather than silently trusting a truncation.
|
|
542
|
+
function declarationSpan(text) {
|
|
543
|
+
const open = text.search(/router\.(get|post|patch|put|delete|use)\s*\(/);
|
|
544
|
+
if (open === -1) return text;
|
|
545
|
+
const start = text.indexOf('(', open);
|
|
546
|
+
if (start === -1) return text;
|
|
547
|
+
|
|
548
|
+
let depth = 0;
|
|
549
|
+
let quote = null; // "'" | '"' | '`' when inside a string/template
|
|
550
|
+
let comment = null; // 'line' | 'block'
|
|
551
|
+
for (let k = start; k < text.length; k++) {
|
|
552
|
+
const c = text[k];
|
|
553
|
+
const next = text[k + 1];
|
|
554
|
+
|
|
555
|
+
if (comment === 'line') { if (c === '\n') comment = null; continue; }
|
|
556
|
+
if (comment === 'block') { if (c === '*' && next === '/') { comment = null; k++; } continue; }
|
|
557
|
+
if (quote) {
|
|
558
|
+
if (c === '\\') { k++; continue; }
|
|
559
|
+
if (c === quote) quote = null;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (c === '/' && next === '/') { comment = 'line'; k++; continue; }
|
|
563
|
+
if (c === '/' && next === '*') { comment = 'block'; k++; continue; }
|
|
564
|
+
if (c === "'" || c === '"' || c === '`') { quote = c; continue; }
|
|
565
|
+
|
|
566
|
+
if (c === '(') depth++;
|
|
567
|
+
else if (c === ')') {
|
|
568
|
+
depth--;
|
|
569
|
+
if (depth === 0) return text.slice(0, k + 1);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return text;
|
|
573
|
+
}
|
|
574
|
+
|
|
520
575
|
// Classify one route's gate from the source lines and the declaration index.
|
|
521
576
|
// Returns { rank, perms, via } — `perms` is the permission atoms the route names, or
|
|
522
577
|
// null when it carries no permission gate; `via` is the PROVENANCE of the label:
|
|
@@ -555,8 +610,14 @@ function classifyGate(lines, i, { floors } = {}) {
|
|
|
555
610
|
// common because the fitness oversize ratchet pushes handler bodies out of large
|
|
556
611
|
// route files, so this is not an exotic shape. A window must never cross into a
|
|
557
612
|
// sibling declaration.
|
|
558
|
-
|
|
559
|
-
|
|
613
|
+
// task 1003190: the two clamps above are both HEURISTIC TAILS — they cut at the
|
|
614
|
+
// next thing that looks like a sibling, so anything between the route and that
|
|
615
|
+
// sibling is still read as this route's middleware. `declarationSpan` replaces
|
|
616
|
+
// the guesswork with the real bound: the declaration's own parenthesis balance.
|
|
617
|
+
// A route's middleware list is, by definition, the arguments inside `router.X(
|
|
618
|
+
// … )`; nothing after the matching `)` can be a gate on THIS route. See that
|
|
619
|
+
// function's header for the three shapes the heuristics let through.
|
|
620
|
+
const own = declarationSpan(window);
|
|
560
621
|
const handlerAt = own.search(/=>\s*\{/);
|
|
561
622
|
const tail = handlerAt === -1 ? own : own.slice(0, handlerAt);
|
|
562
623
|
|
|
@@ -873,8 +934,34 @@ function checkRouteRanks({ routesDir = ROUTES_DIR, modulesDir } = {}) {
|
|
|
873
934
|
if (mm) scopedModules.add(mm[1]);
|
|
874
935
|
}
|
|
875
936
|
const effectivePins = { ...EXPECTED_RANKS };
|
|
937
|
+
// task 1003192: the skip above is SILENT — a key matching no discovered module is
|
|
938
|
+
// dropped with no else branch, so its pins pass vacuously while the routes they
|
|
939
|
+
// guard stay live (the loader mounts modules/<key>/routes/ by the same convention
|
|
940
|
+
// this scan discovers them by). Probed: with modules/security/ renamed, all six
|
|
941
|
+
// security pins go inert and a POST /security/reports deliberately downgraded to
|
|
942
|
+
// any-builder draws ZERO findings. The governance→government rename had to
|
|
943
|
+
// hand-carry double-keyed pins precisely because nothing mechanical catches this.
|
|
944
|
+
//
|
|
945
|
+
// Fire only when the module DIRECTORY IS PRESENT but contributed no route: that is
|
|
946
|
+
// unambiguously a rename/move UNDER a live module, never a legitimately-absent
|
|
947
|
+
// optional one. A wholly-missing directory stays silent here on purpose — an
|
|
948
|
+
// instance may ship without an optional module, and this runs on every ship — so
|
|
949
|
+
// that half is caught at the SOURCE repo by fitness.js Check 4b, where every
|
|
950
|
+
// module is expected to exist. The two layers are complementary, not redundant.
|
|
876
951
|
for (const [mod, pins] of Object.entries(MODULE_EXPECTED_RANKS)) {
|
|
877
|
-
if (scopedModules.has(mod)) Object.assign(effectivePins, pins);
|
|
952
|
+
if (scopedModules.has(mod)) { Object.assign(effectivePins, pins); continue; }
|
|
953
|
+
if (!fs.existsSync(path.join(modulesDir, mod))) continue; // optional module absent — legitimate
|
|
954
|
+
findings.push({
|
|
955
|
+
severity: 'major',
|
|
956
|
+
file: `modules/${mod}/`,
|
|
957
|
+
route: `(MODULE_EXPECTED_RANKS.${mod})`,
|
|
958
|
+
method: '(pins)',
|
|
959
|
+
rank: '(inert)',
|
|
960
|
+
summary: `modules/${mod}/ exists but contributed NO route to this scan, so its `
|
|
961
|
+
+ `${Object.keys(pins).length} rank pin(s) were skipped and passed vacuously. `
|
|
962
|
+
+ 'The routes may still be mounted in production while nothing verifies their rank. '
|
|
963
|
+
+ `Point the pins at the module's current route location, or drop the ${mod} entry if the module no longer serves routes.`,
|
|
964
|
+
});
|
|
878
965
|
}
|
|
879
966
|
for (const [key, expected] of Object.entries(effectivePins)) {
|
|
880
967
|
const r = byKey.get(key);
|
package/src/module-api.js
CHANGED
|
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
|
|
|
55
55
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
56
56
|
// the entry to that file. Look for a version's history there, not here.
|
|
57
57
|
// ---------------------------------------------------------------------------
|
|
58
|
-
const CORE_VERSION = '1.19.
|
|
58
|
+
const CORE_VERSION = '1.19.636'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
59
59
|
|
|
60
60
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
61
61
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/fitness.mjs
CHANGED
|
@@ -68,6 +68,17 @@ test('no route bypasses server-side rank checks (ADR 0016)', () => {
|
|
|
68
68
|
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// task 1003192 — the source-repo half of the inert-pin guard. checkRouteRanks catches a
|
|
72
|
+
// pinned module whose dir is present but yields no route; it stays silent on a wholly
|
|
73
|
+
// absent dir because it runs on every ship and an instance may omit an optional module.
|
|
74
|
+
// Here every module is expected to exist, so a whole-dir rename or a typo'd key — which
|
|
75
|
+
// would park a block of rank pins vacuously while the routes stay mounted — must red.
|
|
76
|
+
test('every MODULE_EXPECTED_RANKS key names a real module dir (task 1003192)', () => {
|
|
77
|
+
const r = fitness.checkModulePinKeysResolve();
|
|
78
|
+
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
79
|
+
assert.equal(r.ok, true);
|
|
80
|
+
});
|
|
81
|
+
|
|
71
82
|
test('generated repo map is fresh (Path C enforces Path B)', () => {
|
|
72
83
|
const r = fitness.checkGeneratedArtifactsFresh();
|
|
73
84
|
assert.equal(r.hardFail, false, r.violations.join('\n'));
|
|
@@ -80,6 +80,78 @@ test('a route declared with NO middleware does not borrow the NEXT route\'s rank
|
|
|
80
80
|
assert.equal(rows.find((x) => x.method === 'POST').rank, 'archon');
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
+
// ---------- task 1003190: the bound is the DECLARATION, not a heuristic tail ----------
|
|
84
|
+
//
|
|
85
|
+
// From the task-1002671 adversarial verification (finding 1). The bleed above is only
|
|
86
|
+
// the sibling-ROUTE spelling; task 1003062 closed that one by clamping at the next
|
|
87
|
+
// `router.` declaration. But the clamp is still a heuristic TAIL — it cuts at the next
|
|
88
|
+
// thing RESEMBLING a sibling, so any gate-shaped text BEFORE that marker was still read
|
|
89
|
+
// as this route's middleware. Each case below is an UNGATED write that classified as
|
|
90
|
+
// gated with real `via:'middleware'` provenance — which passes the ungated-write blocker
|
|
91
|
+
// AND satisfies the EXPECTED_RANKS pins (the task-1002553 fix only stops
|
|
92
|
+
// `via:'annotation'`). `declarationSpan` bounds the tail by paren balance instead, so
|
|
93
|
+
// nothing after the declaration's matching `)` can be mistaken for its gate.
|
|
94
|
+
//
|
|
95
|
+
// The assertion that matters in each is `via`: 'unknown'/null is the auditor admitting
|
|
96
|
+
// it cannot verify a gate (a blocker on a write route — loud, correct), whereas the bug
|
|
97
|
+
// produced a confident, wrong 'middleware'.
|
|
98
|
+
|
|
99
|
+
test('a named-function handler does not absorb a NON-ROUTE gate below it [task 1003190]', () => {
|
|
100
|
+
// The general case task 1003062's next-`router.` clamp does not reach: the gate-shaped
|
|
101
|
+
// text is a plain const, so there is no sibling declaration to clamp at.
|
|
102
|
+
const src = `router.post('/ungated-write', handleUngatedWrite);\n` +
|
|
103
|
+
`\n` +
|
|
104
|
+
`const guard = auth.requirePermission('government.manage');\n`;
|
|
105
|
+
const row = rrc.classifyText(src)[0];
|
|
106
|
+
assert.equal(row.rank, 'unknown');
|
|
107
|
+
assert.equal(row.via, null);
|
|
108
|
+
assert.equal(row.perms, null); // must NOT inherit government.manage
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('an inline CLASSIC function handler does not absorb a trailing gate [task 1003190]', () => {
|
|
112
|
+
// `function (req, res) {` never matches the `=> {` truncation, so before this fix the
|
|
113
|
+
// classifier fell through to the full 20-line window.
|
|
114
|
+
const src = `router.post('/ungated-write', function (req, res) {\n` +
|
|
115
|
+
` res.end();\n` +
|
|
116
|
+
`});\n` +
|
|
117
|
+
`const guard = auth.requireRank('archon');\n`;
|
|
118
|
+
const row = rrc.classifyText(src)[0];
|
|
119
|
+
assert.equal(row.rank, 'unknown');
|
|
120
|
+
assert.equal(row.via, null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('a named-function handler does not absorb a middleware ARRAY below it [task 1003190]', () => {
|
|
124
|
+
const src = `router.post('/ungated-write', handleUngatedWrite);\n` +
|
|
125
|
+
`const chain = [auth.requireRank('archon')];\n`;
|
|
126
|
+
const row = rrc.classifyText(src)[0];
|
|
127
|
+
assert.equal(row.rank, 'unknown');
|
|
128
|
+
assert.equal(row.via, null);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('a REAL multi-line middleware list is still read in full [task 1003190]', () => {
|
|
132
|
+
// The counter-test: bounding by paren balance must not truncate a legitimate gate that
|
|
133
|
+
// spans several lines. Without this, the fix above could "pass" by blinding the auditor.
|
|
134
|
+
const src = `router.post(\n` +
|
|
135
|
+
` '/gated',\n` +
|
|
136
|
+
` auth.requireBuilder,\n` +
|
|
137
|
+
` auth.requirePermission('government.manage'),\n` +
|
|
138
|
+
` async (req, res) => {\n` +
|
|
139
|
+
` res.end();\n },\n);`;
|
|
140
|
+
const row = rrc.classifyText(src)[0];
|
|
141
|
+
assert.equal(row.via, 'middleware');
|
|
142
|
+
assert.deepEqual(row.perms, ['government.manage']);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('a gate inside a route PATH or string cannot be ended early by its parens [task 1003190]', () => {
|
|
146
|
+
// declarationSpan is quote-aware: a `)` inside the path string must not close the span
|
|
147
|
+
// before the real middleware is seen.
|
|
148
|
+
const src = `router.get('/re/(x)/y', auth.requireRank('archon'), async (req, res) => {\n` +
|
|
149
|
+
` res.end();\n});`;
|
|
150
|
+
const row = rrc.classifyText(src)[0];
|
|
151
|
+
assert.equal(row.rank, 'archon');
|
|
152
|
+
assert.equal(row.via, 'middleware');
|
|
153
|
+
});
|
|
154
|
+
|
|
83
155
|
// ---------- BV1.R109b: multi-line route DECLARATIONS are visible at all ----------
|
|
84
156
|
//
|
|
85
157
|
// Distinct from the "multi-line middleware" test above: there the METHOD AND PATH were
|
|
@@ -415,6 +487,56 @@ test('EXPECTED_RANKS: a MISSING pinned route trips a blocker (a protected route
|
|
|
415
487
|
assert.equal(res.hardFail, true);
|
|
416
488
|
});
|
|
417
489
|
|
|
490
|
+
// ---------- task 1003192: a module pin block must never go SILENTLY inert ----------
|
|
491
|
+
//
|
|
492
|
+
// From the task-1002671 adversarial verification (finding 4). Pin scope derives from
|
|
493
|
+
// successfully-classified routes, and the skip had no else branch: a MODULE_EXPECTED_RANKS
|
|
494
|
+
// key matching no discovered module was dropped without a word, while the loader kept
|
|
495
|
+
// mounting modules/<key>/routes/ by the same convention this scan discovers them by.
|
|
496
|
+
// Measured before the fix, with the security module's directory renamed: all six pins went
|
|
497
|
+
// inert and a POST /security/reports deliberately downgraded to any-builder drew ZERO
|
|
498
|
+
// findings — a pinned privileged WRITE route silently unguarded.
|
|
499
|
+
//
|
|
500
|
+
// The guard is deliberately split. checkRouteRanks fires only when the directory IS
|
|
501
|
+
// PRESENT but contributed no route (a rename/move under a live module); a wholly-absent
|
|
502
|
+
// directory stays silent here because this runs on every ship and an instance may
|
|
503
|
+
// legitimately omit an optional module. fitness.js Check 4b owns that half, at the source
|
|
504
|
+
// repo where every module is expected to exist — see tests/fitness.mjs.
|
|
505
|
+
|
|
506
|
+
test('a module whose dir EXISTS but yields no route surfaces an inert-pins finding [task 1003192]', () => {
|
|
507
|
+
const os = require('node:os');
|
|
508
|
+
const fs = require('node:fs');
|
|
509
|
+
const path = require('node:path');
|
|
510
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-inert-'));
|
|
511
|
+
// modules/security/ is present, but its routes live under handlers/ — so the scan
|
|
512
|
+
// discovers nothing for the key and all six security pins would be skipped.
|
|
513
|
+
fs.mkdirSync(path.join(dir, 'security', 'handlers'), { recursive: true });
|
|
514
|
+
fs.writeFileSync(path.join(dir, 'security', 'handlers', 'reports.js'),
|
|
515
|
+
`router.post('/security/reports', auth.requireBuilder, h);\n`);
|
|
516
|
+
const res = rrc.checkRouteRanks({ modulesDir: dir });
|
|
517
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
518
|
+
|
|
519
|
+
const inert = res.findings.find((f) => f.rank === '(inert)');
|
|
520
|
+
assert.ok(inert, `a present-but-routeless pinned module must surface a finding; got: ${res.findings.map((f) => f.route).join(', ')}`);
|
|
521
|
+
assert.equal(inert.severity, 'major');
|
|
522
|
+
assert.match(inert.route, /security/);
|
|
523
|
+
assert.match(inert.summary, /passed vacuously/);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test('an ABSENT optional module stays silent — no false alarm for an instance without it [task 1003192]', () => {
|
|
527
|
+
// The counter-test, and the reason the guard is split across two layers: this runs on
|
|
528
|
+
// every ship, so a module an instance legitimately does not ship must not red the board.
|
|
529
|
+
const os = require('node:os');
|
|
530
|
+
const fs = require('node:fs');
|
|
531
|
+
const path = require('node:path');
|
|
532
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'rrc-absent-'));
|
|
533
|
+
// An empty modules dir: no pinned module directory exists at all.
|
|
534
|
+
const res = rrc.checkRouteRanks({ modulesDir: dir });
|
|
535
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
536
|
+
assert.equal(res.findings.filter((f) => f.rank === '(inert)').length, 0,
|
|
537
|
+
'a module directory that is simply absent must not raise an inert-pins finding');
|
|
538
|
+
});
|
|
539
|
+
|
|
418
540
|
// ---------- LIVE: every real route classifies to a known rank ----------
|
|
419
541
|
|
|
420
542
|
test('LIVE: checkRouteRanks() on src/bongos/routes/ has zero unknowns', () => {
|