@bongos/core 1.19.507 → 1.19.508

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 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.507",
6
- "core_contract": "1.19.507",
7
- "source_commit": "f4008a4f0c0527a72d19cfc989889a3610a164d1",
5
+ "core_version": "1.19.508",
6
+ "core_contract": "1.19.508",
7
+ "source_commit": "f80c3ed5ba139cbecb704b5cd064990b3ff9aaaf",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-03T18:09:01.161Z",
9
+ "built_at": "2026-09-03T18:24:24.662Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 427,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 1946,
14
+ "functional_verbatim": 1947,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2397,
20
- "tree_sha256": "cf9525f9de033ff0382bbc7986cc416062ed1a629caabc30849cc4efa1065398",
19
+ "file_count": 2398,
20
+ "tree_sha256": "8482fdb735f30dbc4866dbf952a55196a50e643fd932b0ef0bb5b701ada05eab",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -2577,7 +2577,7 @@
2577
2577
  {
2578
2578
  "path": "docs/module-api-changelog.md",
2579
2579
  "mode": "0000644",
2580
- "sha256": "ca0326ffd2f4996d2c4f42940ba5c2a2b40b18f9a676023918fd2cde9cd85634"
2580
+ "sha256": "4af1ded195d83c7060e26bf72c6aa27144bfba92e2ce679e2e90d57b866a0c93"
2581
2581
  },
2582
2582
  {
2583
2583
  "path": "docs/modules-contract.md",
@@ -7227,12 +7227,12 @@
7227
7227
  {
7228
7228
  "path": "package-lock.json",
7229
7229
  "mode": "0000644",
7230
- "sha256": "e59012a1aaea2c2e25ac2a7f0c40f096bfe2cae832502cdabfe3519fdb37322d"
7230
+ "sha256": "e9270218a7b9b405011ac398777c9eb70fca7f0cdf3010649ea7450f25d399bb"
7231
7231
  },
7232
7232
  {
7233
7233
  "path": "package.json",
7234
7234
  "mode": "0000644",
7235
- "sha256": "aae944a6300fb887bd38a4692813e2f03965d7bb044c843fe7f23a26715233d8"
7235
+ "sha256": "6a1614277a1b71103a1882256a82e138cbdb4bfbc28be233e69cc751d7f24eed"
7236
7236
  },
7237
7237
  {
7238
7238
  "path": "public-docs/index.html",
@@ -8902,7 +8902,7 @@
8902
8902
  {
8903
8903
  "path": "src/module-api.js",
8904
8904
  "mode": "0000644",
8905
- "sha256": "c8e168b4ae8a7dc956802de03248b6509b871e98bf0315dbd94d8dc7cc16be50"
8905
+ "sha256": "eef34fbd7a43f793b876c07999581137f4e8e37cc0b3f03d7db41759b0271693"
8906
8906
  },
8907
8907
  {
8908
8908
  "path": "src/module-loader/catalog.js",
@@ -10317,7 +10317,7 @@
10317
10317
  {
10318
10318
  "path": "tests/hall_page_gate_map.mjs",
10319
10319
  "mode": "0000644",
10320
- "sha256": "d529685063de8c4aa6676abf86f1180bb9cd13d27a3c372d8a3d9cf078af1999"
10320
+ "sha256": "e65e22471469a9bf4551925f0549cfd6720aa7d5b7b7cf9df17984604e07c3cb"
10321
10321
  },
10322
10322
  {
10323
10323
  "path": "tests/hall_palette.mjs",
@@ -10879,6 +10879,11 @@
10879
10879
  "mode": "0000644",
10880
10880
  "sha256": "079d7eef5828912e306e9dbbe081fef6004526067bd15aa6433dc2ff1873a4be"
10881
10881
  },
10882
+ {
10883
+ "path": "tests/my_projects_surface.mjs",
10884
+ "mode": "0000644",
10885
+ "sha256": "03b61cb8de6713b03fe17b2b3c5d70af7c511b33b17a42295f49c36e27771470"
10886
+ },
10882
10887
  {
10883
10888
  "path": "tests/needs_rebase.mjs",
10884
10889
  "mode": "0000644",
@@ -1451,5 +1451,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1451
1451
  landed since 1.19.505 with no explicit bump. run 33787453336. (task 1002620)
1452
1452
  1.19.507 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1453
1453
  landed since 1.19.506 with no explicit bump. run 33788726591. (task 1002620)
1454
+ 1.19.508 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1455
+ landed since 1.19.507 with no explicit bump. run 33790206682. (task 1002620)
1454
1456
  ---------------------------------------------------------------------------
1455
1457
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.507",
3
+ "version": "1.19.508",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.507",
9
+ "version": "1.19.508",
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.507",
3
+ "version": "1.19.508",
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/src/module-api.js CHANGED
@@ -45,7 +45,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
45
45
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
46
46
  // the entry to that file. Look for a version's history there, not here.
47
47
  // ---------------------------------------------------------------------------
48
- const CORE_VERSION = '1.19.507'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
48
+ const CORE_VERSION = '1.19.508'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
49
49
 
50
50
  // A namespaced logger so a module's log lines are attributable + consistent.
51
51
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -47,6 +47,13 @@ const ORACLE = {
47
47
  '/builders/modules': 'requireBuilderPage',
48
48
  '/builders/goals': 'requireBuilderPage',
49
49
  '/builders/roadmap': 'requireBuilderPage',
50
+ // Gated at this tier all along, but with no row until task 1003448 walked the
51
+ // module's pages: copy-desk (task 1003113) and ideas (task 1002928) are open to
52
+ // every rank by design, and collab's reads (task 1003172) are scoped to the
53
+ // caller by the server — so for all three, sign-in IS the whole gate.
54
+ '/builders/copy-desk': 'requireBuilderPage',
55
+ '/builders/ideas': 'requireBuilderPage',
56
+ '/builders/collab': 'requireBuilderPage',
50
57
  // task 1002710: the orientation reading rooms moved DOWN to this tier from
51
58
  // requireNonXenosPage (thetes+). They are the onboarding documents, so gating
52
59
  // them above xenos turned away the exact reader they were written for. The
@@ -92,10 +99,33 @@ const ORACLE = {
92
99
  '/builders/drachmae': null,
93
100
  '/builders/profile': null,
94
101
  '/builders/not-ready': null,
102
+
103
+ // NOT deliberate — recorded because it is true, and filed as idea 1000907
104
+ // (task 1003448). The detail pages are addressed by a second segment, and
105
+ // /builders/task/:id and /builders/idea/:id are both gated at signed-in; the
106
+ // id-less slugs still resolve to the same shells through BUILDERS_PAGE_RE and
107
+ // match no gate. No data rides the shell and every read behind it is gated
108
+ // server-side, so this is an inconsistency rather than an exposure — but it is
109
+ // a row that should turn into a real gate (or a redirect) when that is decided.
110
+ '/builders/task': null,
111
+ '/builders/idea': null,
112
+
113
+ // Gone, not public — the hall ships no my-projects page at all since task
114
+ // 1003308 (owner's decision: "my projects" is a user-level page the hub owns,
115
+ // so the hall ships no page, no link and no redirect). Nothing is served here,
116
+ // so there is nothing to gate. Recorded rather than left out, because "absent
117
+ // from the map" is indistinguishable from the oversight this file exists to
118
+ // catch. GONE_PAGES below keeps the row honest if the file ever returns.
119
+ '/builders/my-projects': null,
95
120
  };
96
121
 
97
122
  // ─── reconstruct the URL→gate map from the source ───────────────────────────
98
123
  // 1. every `const NAME_RE = /…/;` (the page-matching regexes)
124
+ // The eval below turns a matched regex LITERAL back into a RegExp. Its only
125
+ // input is this repo's own serve-internal.js, read from a path fixed at the top
126
+ // of this file — never a caller's string, never anything fetched. Keep it that
127
+ // way: sourcing SRC from anywhere an outsider can influence makes this arbitrary
128
+ // code execution rather than a parser.
99
129
  function readRegexes(src) {
100
130
  const out = new Map();
101
131
  const re = /const\s+([A-Z0-9_]+_RE)\s*=\s*(\/(?:\\.|\[(?:\\.|[^\]])*\]|[^/\\])+\/[gimsuy]*)\s*;/g;
@@ -170,6 +200,47 @@ test('path-normalization tricks do not slip past a gate', () => {
170
200
  }
171
201
  });
172
202
 
203
+ // ═══ exhaustiveness — the reason a page can go missing from the oracle ═══════
204
+ // The ORACLE above is hand-written, so until now a page could ship with NO gate
205
+ // and NO row and nothing would notice — the same blind spot that let /sessions
206
+ // and /scouting ship ungated, and that left /my-projects out of the map entirely
207
+ // (task 1003448). Walking the module's own pages turns "someone remembered to
208
+ // add a row" into "the tree is walked", which is the half this test was missing.
209
+ const HALL_PAGES_DIR = path.join(ROOT, 'modules/hall-ui/public');
210
+
211
+ function hallPageUrls() {
212
+ return fs.readdirSync(HALL_PAGES_DIR)
213
+ .filter((f) => f.endsWith('.html'))
214
+ .map((f) => (f === 'index.html' ? '/builders/' : `/builders/${f.replace(/\.html$/, '')}`));
215
+ }
216
+
217
+ test('every hall page the module ships has a row in the oracle', () => {
218
+ const missing = hallPageUrls().filter((url) => !(url in ORACLE));
219
+ assert.deepEqual(missing, [],
220
+ `hall pages with no ORACLE row: ${missing.join(', ')} — give each a gate, or a deliberate null with the reason`);
221
+ });
222
+
223
+ // A page can also LEAVE the hall, and then `null` claims something weaker than
224
+ // "the shell is public": nothing is served at that URL at all. Spelling those out
225
+ // keeps the row honest — if the file ever comes back, the null stops describing
226
+ // reality, and this fails instead of quietly waving an ungated shell through.
227
+ const GONE_PAGES = {
228
+ // Removed by an owner decision in task 1003308 — "my projects" is a user-level
229
+ // page the hub already owns, so the hall ships no page, no link and no
230
+ // redirect. tests/hall_settings_world.mjs pins the deletion itself; this row
231
+ // pins what the deletion means for the gate map.
232
+ '/builders/my-projects': 'task 1003308 — the hub owns "my projects"',
233
+ };
234
+
235
+ test('a row that is null because the page left the hall serves no page', () => {
236
+ for (const [url, why] of Object.entries(GONE_PAGES)) {
237
+ assert.equal(ORACLE[url], null, `${url} is gone (${why}), so it can carry no gate`);
238
+ const file = path.join(HALL_PAGES_DIR, `${url.replace('/builders/', '')}.html`);
239
+ assert.ok(!fs.existsSync(file),
240
+ `${url} is back, so "${why}" no longer holds — give it a real gate and a real row`);
241
+ }
242
+ });
243
+
173
244
  test('no gated page is left matching only a REMOVED gate function', () => {
174
245
  // requireNonXenosPage still EXISTS in auth.js (page.view.builder is a live
175
246
  // catalog atom an Archon can attach to a custom role) but task 1002710 left it
@@ -0,0 +1,112 @@
1
+ // tests/my_projects_surface.mjs
2
+ //
3
+ // GUARD (task 1003448): pins the three answers the "/my-projects is unreachable"
4
+ // audit finding actually has, so that none of them is a claim in a session log
5
+ // that the next reader has to take on faith.
6
+ //
7
+ // The audit (2026-08-30) reported the hall's my-projects page as unreachable and
8
+ // ungated, and flagged the second half as "NOT CONFIRMED AS A LEAK — check the
9
+ // API gates first, then decide whether the fix is a gate, a nav entry, or both".
10
+ // By the time the task was worked, the answer to "a gate or a nav entry" had
11
+ // become "neither":
12
+ //
13
+ // 1. WHERE THE PAGE WENT. Task 1003308 deleted it on an owner decision — "my
14
+ // projects" is a user-level page the hub already owns, so the hall ships no
15
+ // page, no link and no redirect. A nav entry would reverse that decision and
16
+ // a page gate would guard a URL that resolves to no file. (The deletion
17
+ // itself is pinned by tests/hall_settings_world.mjs; the gate map's row for
18
+ // it by tests/hall_page_gate_map.mjs.)
19
+ // 2. WHETHER ANYTHING LEAKED. No: every /my-projects* route is sign-in-gated
20
+ // and reads the caller's OWN identity off the session. This is the half the
21
+ // audit could not confirm, so it is asserted here rather than described.
22
+ // 3. WHAT IS STILL MISSING. The invitations half has no caller anywhere — no
23
+ // surface fetches the invite list, so a builder holding an invitation still
24
+ // cannot act on it. That is tracked work (task 1002970, idea 1000903), and
25
+ // pinning it keeps a known gap from decaying back into an unknown one.
26
+ //
27
+ // Static reads only (no import of the app, no DB) → runs in the `unit` gate.
28
+ //
29
+ // Run: node --test --test-reporter=tap tests/my_projects_surface.mjs
30
+
31
+ import { test } from 'node:test';
32
+ import assert from 'node:assert/strict';
33
+ import fs from 'node:fs';
34
+ import path from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+
37
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
38
+ const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
39
+
40
+ const ROUTES = 'modules/platform-identity/routes/my-projects.js';
41
+
42
+ // ── 1. the page is gone from the hall, by decision ──────────────────────────
43
+ // Kept to the ONE fact this file's other assertions lean on: there is no hall
44
+ // page, so "unreachable from the hall" is a decision to honour and not a defect
45
+ // to fix. tests/hall_settings_world.mjs owns the full deletion pin.
46
+ test('the hall ships no my-projects page, so there is nothing to link or gate', () => {
47
+ const dir = path.join(ROOT, 'modules/hall-ui/public');
48
+ const strays = fs.readdirSync(dir).filter((f) => f.startsWith('my-projects'));
49
+ assert.deepEqual(strays, [],
50
+ 'task 1003308 removed the hall page (the hub owns "my projects") — if it is back, that decision changed and this file, the nav and the page gate all need revisiting');
51
+ });
52
+
53
+ // ── 2. the "not confirmed as a leak" half, confirmed ────────────────────────
54
+ // The audit's worry was a shell served to anyone who asked. The shell is gone,
55
+ // but the routes behind it outlived it, so the gate is what matters now.
56
+ const MY_PROJECTS_ROUTES = [
57
+ "router.get('/my-projects'",
58
+ "router.post('/my-projects/join'",
59
+ "router.get('/my-projects/invites'",
60
+ "router.post('/my-projects/invites/:clientId/accept'",
61
+ "router.post('/my-projects/invites/:clientId/decline'",
62
+ "router.post('/my-projects/:clientId/leave'",
63
+ ];
64
+
65
+ test('every /my-projects route is sign-in gated', () => {
66
+ const src = read(ROUTES);
67
+ for (const decl of MY_PROJECTS_ROUTES) {
68
+ const at = src.indexOf(decl);
69
+ assert.notEqual(at, -1, `${decl} — route missing; this list is stale`);
70
+ // The gate is the argument immediately after the path, so read the rest of
71
+ // that line rather than the whole file: a requireBuilder somewhere else in
72
+ // the module must not vouch for a route that carries no gate of its own.
73
+ const line = src.slice(at, src.indexOf('\n', at));
74
+ assert.match(line, /,\s*requireBuilder\s*,/,
75
+ `${decl} must carry requireBuilder — an ungated route here exposes cross-project membership`);
76
+ }
77
+ });
78
+
79
+ test('every /my-projects handler reads the caller identity from the session', () => {
80
+ // The gate only proves SOMEONE is signed in. What keeps one builder out of
81
+ // another's projects is that the github_id comes off req.builder and is never
82
+ // accepted from the caller — no :githubId param, no body-supplied account.
83
+ const src = read(ROUTES);
84
+ assert.ok(src.includes('req.builder.github_id'), 'identity must come off the session');
85
+ assert.doesNotMatch(src, /\/my-projects[^']*:githubId/,
86
+ 'no /my-projects route may take an account id as a path parameter');
87
+ assert.doesNotMatch(src, /githubId:\s*(?:req\.body|req\.query)/,
88
+ 'no /my-projects handler may take the account id from the request body or query');
89
+ });
90
+
91
+ // ── 3. the gap that is left, named and tracked ─────────────────────────────
92
+ test('the invitations API still has no caller — the tracked gap, pinned', () => {
93
+ // Deliberately asserts a MISSING feature. Task 1003308 removed the only
94
+ // surface that fetched invitations, so a builder who holds one has nowhere to
95
+ // accept or decline it. Task 1002970 (blocked on task 1002969) and idea
96
+ // 1000903 own the replacement; this pin exists so the gap stays visible until
97
+ // one of them lands, and so it is impossible to close this quietly.
98
+ const surfaces = [];
99
+ const modules = path.join(ROOT, 'modules');
100
+ for (const key of fs.readdirSync(modules)) {
101
+ const pub = path.join(modules, key, 'public');
102
+ if (!fs.existsSync(pub)) continue;
103
+ for (const f of fs.readdirSync(pub)) {
104
+ if (!/\.(js|html)$/.test(f)) continue;
105
+ if (fs.readFileSync(path.join(pub, f), 'utf8').includes('/my-projects/invites')) {
106
+ surfaces.push(`modules/${key}/public/${f}`);
107
+ }
108
+ }
109
+ }
110
+ assert.deepEqual(surfaces, [],
111
+ `an invitations surface exists now (${surfaces.join(', ')}) — good: delete this pin and close task 1002970 / idea 1000903`);
112
+ });