@bongos/core 1.19.411 → 1.19.412
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 +10 -10
- package/docs/module-api-changelog.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/profile_route.mjs +290 -10
package/.bongos-core.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
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.412",
|
|
6
|
+
"core_contract": "1.19.412",
|
|
7
|
+
"source_commit": "049b8f5c6bb893589c47e9e6e3855fc3da1bec06",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-29T15:07:18.728Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 354,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
19
|
"file_count": 2044,
|
|
20
|
-
"tree_sha256": "
|
|
20
|
+
"tree_sha256": "d71641215ed53df26d8573408c98ba6afad6b1a95232f3dc1b8055688a98eb08",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1867,7 +1867,7 @@
|
|
|
1867
1867
|
{
|
|
1868
1868
|
"path": "docs/module-api-changelog.md",
|
|
1869
1869
|
"mode": "0000644",
|
|
1870
|
-
"sha256": "
|
|
1870
|
+
"sha256": "251455d2e8adfcad824b3ce55ba08418109cfb6e386990f3d9acfb551a992884"
|
|
1871
1871
|
},
|
|
1872
1872
|
{
|
|
1873
1873
|
"path": "docs/modules-contract.md",
|
|
@@ -5367,12 +5367,12 @@
|
|
|
5367
5367
|
{
|
|
5368
5368
|
"path": "package-lock.json",
|
|
5369
5369
|
"mode": "0000644",
|
|
5370
|
-
"sha256": "
|
|
5370
|
+
"sha256": "07258342132575cf206c98c5c145423c12a5b16f96c3ecf9c8a5c4c4265bcafa"
|
|
5371
5371
|
},
|
|
5372
5372
|
{
|
|
5373
5373
|
"path": "package.json",
|
|
5374
5374
|
"mode": "0000644",
|
|
5375
|
-
"sha256": "
|
|
5375
|
+
"sha256": "79513e9d85183a56152ef6bb5a75f61548efc663860f89c1b55df41d3ced73b2"
|
|
5376
5376
|
},
|
|
5377
5377
|
{
|
|
5378
5378
|
"path": "public-docs/index.html",
|
|
@@ -7387,7 +7387,7 @@
|
|
|
7387
7387
|
{
|
|
7388
7388
|
"path": "src/module-api.js",
|
|
7389
7389
|
"mode": "0000644",
|
|
7390
|
-
"sha256": "
|
|
7390
|
+
"sha256": "b182b43409b6311ab968db425b6064378cc2842d9abc1f4628f18a3ea7cde9dc"
|
|
7391
7391
|
},
|
|
7392
7392
|
{
|
|
7393
7393
|
"path": "src/module-loader/catalog.js",
|
|
@@ -9382,7 +9382,7 @@
|
|
|
9382
9382
|
{
|
|
9383
9383
|
"path": "tests/profile_route.mjs",
|
|
9384
9384
|
"mode": "0000644",
|
|
9385
|
-
"sha256": "
|
|
9385
|
+
"sha256": "5af720ee7c18d4b47630fbbed7cb08901acfbf773960699e312496b8196c9fdc"
|
|
9386
9386
|
},
|
|
9387
9387
|
{
|
|
9388
9388
|
"path": "tests/profile_ui_cross_project.mjs",
|
|
@@ -1259,5 +1259,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1259
1259
|
landed since 1.19.409 with no explicit bump. run 33217599338. (task 1002620)
|
|
1260
1260
|
1.19.411 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1261
1261
|
landed since 1.19.410 with no explicit bump. run 33218290583. (task 1002620)
|
|
1262
|
+
1.19.412 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1263
|
+
landed since 1.19.411 with no explicit bump. run 33259357678. (task 1002620)
|
|
1262
1264
|
---------------------------------------------------------------------------
|
|
1263
1265
|
```
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.412",
|
|
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.412",
|
|
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.412",
|
|
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
|
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
33
33
|
// there. scripts/gds/bump-version.js still rewrites the literal below; it appends
|
|
34
34
|
// the entry to that file. Look for a version's history there, not here.
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
|
-
const CORE_VERSION = '1.19.
|
|
36
|
+
const CORE_VERSION = '1.19.412'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
37
37
|
|
|
38
38
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
39
39
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
package/tests/profile_route.mjs
CHANGED
|
@@ -11,14 +11,38 @@
|
|
|
11
11
|
// Stub auth + db + the lifecycle/reward ports (no network, no Postgres) exactly
|
|
12
12
|
// as tests/onboarding_route_signals.mjs does for the sibling public route.
|
|
13
13
|
//
|
|
14
|
+
// R06 (task 1002282) extends it with the consent PROOF — the five claims goal
|
|
15
|
+
// 1000045's C2 rests on, all DB-free so the `unit` gate actually runs them:
|
|
16
|
+
// 1. a two-project builder's COMBINED record is served to another account
|
|
17
|
+
// (the route is unauthenticated and viewer-independent, ADR 0171 D4);
|
|
18
|
+
// 2. flipping hide_stats removes it on the IMMEDIATE next load, and the
|
|
19
|
+
// response is `Cache-Control: no-store` — the old `public, max-age=60`
|
|
20
|
+
// would keep serving the pre-hide body for a minute (ADR 0171 D5);
|
|
21
|
+
// 3. `/scouting` (the endpoint the hall's /people page renders) drops the
|
|
22
|
+
// hidden builder too — enforcement is on every read path, not the profile;
|
|
23
|
+
// 4. the leak guard BITES: planting a private field turns the key-list and
|
|
24
|
+
// sentinel assertions red (a leak test that cannot fail is not a test);
|
|
25
|
+
// 5. no PARTIAL numbers survive a hide — a count beside a hidden total is a
|
|
26
|
+
// leak by subtraction.
|
|
27
|
+
//
|
|
28
|
+
// RESIDUAL, stated honestly (goal 1000045 C2): hide removes the hub's
|
|
29
|
+
// AGGREGATED view, not the per-instance public numbers it is derived from.
|
|
30
|
+
// Every instance's own public surfaces (its /public/leaderboard) stay public by
|
|
31
|
+
// design, so a determined reader can still reconstruct a sum the hard way.
|
|
32
|
+
//
|
|
14
33
|
// Run: node tests/profile_route.mjs
|
|
15
34
|
|
|
16
35
|
import { strict as assert } from 'node:assert';
|
|
17
36
|
import { createRequire } from 'node:module';
|
|
18
37
|
import http from 'node:http';
|
|
38
|
+
import { readFileSync } from 'node:fs';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
40
|
+
import { dirname, join } from 'node:path';
|
|
19
41
|
|
|
20
42
|
const require = createRequire(import.meta.url);
|
|
21
43
|
const express = require('express');
|
|
44
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
45
|
+
const sourceOf = (rel) => readFileSync(join(ROOT, rel), 'utf8').replace(/\r\n/g, '\n');
|
|
22
46
|
|
|
23
47
|
let passed = 0;
|
|
24
48
|
let failed = 0;
|
|
@@ -90,7 +114,7 @@ const CROSS_PROJECT_ROWS = [
|
|
|
90
114
|
// shape — named projects only where the catalog row is public; the rest counted.
|
|
91
115
|
function stubPublicProfileExtras() {
|
|
92
116
|
const named = CROSS_PROJECT_ROWS.filter((r) => r.is_public)
|
|
93
|
-
.map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held }));
|
|
117
|
+
.map((r) => ({ name: r.name, origin: r.origin, rank_held: r.rank_held, recent_ships: [] }));
|
|
94
118
|
return {
|
|
95
119
|
totals: { projects: 2, credits: 150, works_shipped: 7, karma: 4 },
|
|
96
120
|
disciplines: ['engineer', 'ui'],
|
|
@@ -101,7 +125,28 @@ function stubPublicProfileExtras() {
|
|
|
101
125
|
}
|
|
102
126
|
// The key contract for the cross_project object + its project rows.
|
|
103
127
|
const CP_PUBLIC_KEYS = ['totals', 'disciplines', 'last_active', 'projects', 'more_projects'];
|
|
104
|
-
|
|
128
|
+
// recent_ships rides the project row (task 1002803) and is ALWAYS present —
|
|
129
|
+
// getRollupForBuilder defaults it to [] — so it belongs in the exact key list.
|
|
130
|
+
const CP_PROJECT_ROW_KEYS = ['name', 'origin', 'rank_held', 'recent_ships'];
|
|
131
|
+
// Every module-internal value that must never reach a public body.
|
|
132
|
+
const CP_SENTINELS = [SECRET_CLIENT_ID, SECRET_PROJECT_NAME, SECRET_EMAIL, SECRET_USER, SECRET_REASON];
|
|
133
|
+
|
|
134
|
+
// The cross_project whitelist contract, in ONE place — the positive tests and
|
|
135
|
+
// the R06 negative control (task 1002282) run the SAME guard, which is what
|
|
136
|
+
// makes "planting a private field turns it red" a proof rather than a claim.
|
|
137
|
+
// Asserts the exact key list on the object and on every project row, and that
|
|
138
|
+
// no sentinel (nor the literal `client_id` key) survives in the raw body.
|
|
139
|
+
function assertCrossProjectContract(cp, text) {
|
|
140
|
+
assert.deepEqual(Object.keys(cp).sort(), [...CP_PUBLIC_KEYS].sort(), 'cross_project keys are exactly the contract');
|
|
141
|
+
assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
|
|
142
|
+
for (const row of cp.projects) {
|
|
143
|
+
assert.deepEqual(Object.keys(row).sort(), [...CP_PROJECT_ROW_KEYS].sort(), 'project rows carry only the whitelisted keys');
|
|
144
|
+
}
|
|
145
|
+
for (const secret of CP_SENTINELS) {
|
|
146
|
+
assert.ok(!text.includes(secret), `response body must not contain "${secret}"`);
|
|
147
|
+
}
|
|
148
|
+
assert.ok(!text.includes('client_id'), 'no client_id key may cross the port');
|
|
149
|
+
}
|
|
105
150
|
|
|
106
151
|
// ---- stubs ----------------------------------------------------------------
|
|
107
152
|
|
|
@@ -161,9 +206,9 @@ function listen(app) {
|
|
|
161
206
|
});
|
|
162
207
|
}
|
|
163
208
|
|
|
164
|
-
function get(port, path) {
|
|
209
|
+
function get(port, path, headers) {
|
|
165
210
|
return new Promise((resolve, reject) => {
|
|
166
|
-
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path }, (res) => {
|
|
211
|
+
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path, headers: headers || {} }, (res) => {
|
|
167
212
|
const chunks = [];
|
|
168
213
|
res.on('data', (c) => chunks.push(c));
|
|
169
214
|
res.on('end', () => {
|
|
@@ -235,8 +280,12 @@ await t('hub port ABSENT → no cross_project key + the cacheable posture stays'
|
|
|
235
280
|
});
|
|
236
281
|
|
|
237
282
|
let hubExtras = stubPublicProfileExtras();
|
|
283
|
+
// The port's public read sits behind a swappable reference: a port is
|
|
284
|
+
// single-provider by design (registerProvider throws on a duplicate), and the
|
|
285
|
+
// R06 proofs below repoint this at the REAL module read over a fake engine.
|
|
286
|
+
let hubPublicRead = async () => hubExtras;
|
|
238
287
|
seams.registerProvider('platform-identity.hub', {
|
|
239
|
-
getPublicProfileExtras: async () =>
|
|
288
|
+
getPublicProfileExtras: async (githubId) => hubPublicRead(githubId),
|
|
240
289
|
getOwnProfileExtras: async () => stubPublicProfileExtras(),
|
|
241
290
|
});
|
|
242
291
|
|
|
@@ -245,11 +294,7 @@ await t('hub port present → cross_project carries exactly the whitelist shape'
|
|
|
245
294
|
assert.equal(r.status, 200);
|
|
246
295
|
const cp = r.json.cross_project;
|
|
247
296
|
assert.ok(cp, 'cross_project attached when the port resolves');
|
|
248
|
-
|
|
249
|
-
assert.deepEqual(Object.keys(cp.totals).sort(), ['credits', 'karma', 'projects', 'works_shipped']);
|
|
250
|
-
for (const row of cp.projects) {
|
|
251
|
-
assert.deepEqual(Object.keys(row).sort(), [...CP_PROJECT_ROW_KEYS].sort(), 'project rows carry only name/origin/rank_held');
|
|
252
|
-
}
|
|
297
|
+
assertCrossProjectContract(cp, r.text);
|
|
253
298
|
assert.equal(cp.projects.length, 1, 'only the public catalog row is named');
|
|
254
299
|
assert.equal(cp.more_projects, 1, 'the non-public project is counted, never named');
|
|
255
300
|
});
|
|
@@ -277,6 +322,241 @@ await t('hub port returns null (hidden/provisional/none) → cross_project: null
|
|
|
277
322
|
assert.equal(r.headers['cache-control'], 'no-store');
|
|
278
323
|
});
|
|
279
324
|
|
|
325
|
+
// ---- R06 PROOF: hide is honored on every read path (task 1002282) ---------
|
|
326
|
+
//
|
|
327
|
+
// Deps R03 (task 1002279 — the hide_stats flag) and R04 (task 1002280 — the
|
|
328
|
+
// profile UI section) shipped; this pins their behaviour so it cannot rot. The
|
|
329
|
+
// proofs below run the REAL module reads (getPublicProfileExtras,
|
|
330
|
+
// setAccountHideStats, listScoutingRollup) and the REAL routes over a fake
|
|
331
|
+
// engine, so what is proven is the COMPOSITION — route → port → module read →
|
|
332
|
+
// query — not a stub of the answer.
|
|
333
|
+
|
|
334
|
+
// A deliberately literal-minded stand-in for Postgres. It holds the fixture
|
|
335
|
+
// rows and applies ONLY the consent predicates the query it is handed actually
|
|
336
|
+
// declares. That is what makes the scouting proof real rather than circular:
|
|
337
|
+
// delete `NOT pa.hide_stats` from listScoutingRollup and the hidden builder
|
|
338
|
+
// walks straight into the response, turning these tests red. The SQL semantics
|
|
339
|
+
// themselves (the un-fanned sums) are proven against a live Postgres by
|
|
340
|
+
// tests/profile_rollup_consent.mjs and tests/scouting_rollup_fanout.mjs.
|
|
341
|
+
const HUB_GITHUB_ID = FULL_ROW.github_id;
|
|
342
|
+
const HIDDEN_LOGIN = 'hidden-DO-NOT-LEAK-r06';
|
|
343
|
+
const PROV_LOGIN = 'provisional-DO-NOT-LEAK-r06';
|
|
344
|
+
|
|
345
|
+
// TWO projects — one publicly catalogued, one not. Distinctive digits so the
|
|
346
|
+
// "no partial numbers" scan below cannot pass by coincidence.
|
|
347
|
+
const PROOF_PROJECTS = [
|
|
348
|
+
{ name: 'R06 Public Proj', origin: 'https://r06-pub.example', rank_held: 'metic',
|
|
349
|
+
credits: 60271, tasks_shipped: 4103, karma: 9021, is_public: true },
|
|
350
|
+
{ name: SECRET_PROJECT_NAME, origin: 'https://r06-priv.example', rank_held: 'archon',
|
|
351
|
+
credits: 40129, tasks_shipped: 2011, karma: 3007, is_public: false },
|
|
352
|
+
];
|
|
353
|
+
const sumOf = (k) => PROOF_PROJECTS.reduce((n, p) => n + p[k], 0);
|
|
354
|
+
|
|
355
|
+
// The one account row the reads consult. setAccountHideStats mutates it.
|
|
356
|
+
const hubAccount = { profile_state: 'active', hide_stats: false, handle: 'r06-handle' };
|
|
357
|
+
|
|
358
|
+
// The scouting "table": one visible builder plus the two that consent excludes.
|
|
359
|
+
const SCOUTING_TABLE = [
|
|
360
|
+
{ profile_state: 'active', hide_stats: false,
|
|
361
|
+
row: { github_id: '11', github_login: 'r06-visible', display_name: 'Visible', avatar_url: null,
|
|
362
|
+
project_count: 2, total_credits: 5, total_tasks_shipped: 1, total_karma: 0,
|
|
363
|
+
last_active: '2026-08-01T00:00:00.000Z', disciplines: ['engineer'] } },
|
|
364
|
+
{ profile_state: 'active', hide_stats: true,
|
|
365
|
+
row: { github_id: '12', github_login: HIDDEN_LOGIN, display_name: 'Hidden', avatar_url: null,
|
|
366
|
+
project_count: 7, total_credits: 88817, total_tasks_shipped: 6613, total_karma: 5519,
|
|
367
|
+
last_active: '2026-08-02T00:00:00.000Z', disciplines: ['ui'] } },
|
|
368
|
+
{ profile_state: 'provisional', hide_stats: false,
|
|
369
|
+
row: { github_id: '13', github_login: PROV_LOGIN, display_name: 'Provisional', avatar_url: null,
|
|
370
|
+
project_count: 1, total_credits: 71713, total_tasks_shipped: 1, total_karma: 0,
|
|
371
|
+
last_active: '2026-08-03T00:00:00.000Z', disciplines: [] } },
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const engine = {
|
|
375
|
+
calls: [],
|
|
376
|
+
query: async (text, params = []) => {
|
|
377
|
+
const sql = String(text);
|
|
378
|
+
engine.calls.push({ sql, params });
|
|
379
|
+
|
|
380
|
+
// listScoutingRollup — the ONE read behind GET /scouting and the hall's
|
|
381
|
+
// /people page. Honors exactly the predicates the query declares.
|
|
382
|
+
if (/AS project_count/.test(sql)) {
|
|
383
|
+
let rows = SCOUTING_TABLE;
|
|
384
|
+
if (/pa\.profile_state = 'active'/.test(sql)) rows = rows.filter((r) => r.profile_state === 'active');
|
|
385
|
+
if (/NOT pa\.hide_stats/.test(sql)) rows = rows.filter((r) => !r.hide_stats);
|
|
386
|
+
return { rows: rows.map((r) => ({ ...r.row })) };
|
|
387
|
+
}
|
|
388
|
+
// setAccountHideStats — the own-scoped write behind PATCH /me/cross-project.
|
|
389
|
+
if (/INSERT INTO platform_identity_accounts/.test(sql)) {
|
|
390
|
+
hubAccount.hide_stats = !!params[4];
|
|
391
|
+
return { rows: [{ github_id: String(HUB_GITHUB_ID), github_login: 'jaxri', ...hubAccount }] };
|
|
392
|
+
}
|
|
393
|
+
// getPublicProfileExtras' consent read.
|
|
394
|
+
if (/SELECT profile_state, hide_stats/.test(sql)) {
|
|
395
|
+
return { rows: [{ profile_state: hubAccount.profile_state, hide_stats: hubAccount.hide_stats }] };
|
|
396
|
+
}
|
|
397
|
+
// getRollupForBuilder's per-project rows (the client_id → origin two-hop).
|
|
398
|
+
if (/platform_identity_sso_clients/.test(sql)) {
|
|
399
|
+
return { rows: PROOF_PROJECTS.map((p) => ({ name: p.name, origin: p.origin, rank_held: p.rank_held, is_public: p.is_public, recent_ships: [] })) };
|
|
400
|
+
}
|
|
401
|
+
// getRollupForBuilder's un-fanned aggregate — summed ACROSS both projects.
|
|
402
|
+
if (/AS works_shipped/.test(sql)) {
|
|
403
|
+
return { rows: [{ projects: PROOF_PROJECTS.length, credits: sumOf('credits'),
|
|
404
|
+
works_shipped: sumOf('tasks_shipped'), karma: sumOf('karma'),
|
|
405
|
+
last_active: '2026-08-02T00:00:00.000Z', disciplines: ['engineer', 'ui'] }] };
|
|
406
|
+
}
|
|
407
|
+
throw new Error(`fake engine: unexpected query ${sql.slice(0, 60)}`);
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Pin the module doorway (ADR 0083) BEFORE platform-identity loads, so the
|
|
412
|
+
// module's default pool — destructured at require time — is the fake engine.
|
|
413
|
+
const moduleApiPath = require.resolve('../src/module-api.js');
|
|
414
|
+
require.cache[moduleApiPath] = {
|
|
415
|
+
id: moduleApiPath, filename: moduleApiPath, loaded: true,
|
|
416
|
+
exports: {
|
|
417
|
+
pool: engine,
|
|
418
|
+
requireBuilder: (_req, _res, next) => next(),
|
|
419
|
+
requirePermission: () => (_req, _res, next) => next(),
|
|
420
|
+
parsePagination: () => ({ limit: 100, offset: 0 }),
|
|
421
|
+
corsPublicGet: () => (_req, _res, next) => next(),
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
425
|
+
const buildScoutingRoutes = require('../modules/platform-identity/routes/scouting.js');
|
|
426
|
+
|
|
427
|
+
// From here the port serves the REAL public read over the fake engine.
|
|
428
|
+
hubPublicRead = (githubId) => pi.getPublicProfileExtras(githubId, { pool: engine });
|
|
429
|
+
|
|
430
|
+
await t('R06.1 a two-project builder\'s COMBINED record shows to another account', async () => {
|
|
431
|
+
const r = await get(port, '/api/gds/builders/95/profile');
|
|
432
|
+
assert.equal(r.status, 200);
|
|
433
|
+
const cp = r.json.cross_project;
|
|
434
|
+
assert.ok(cp, 'the record is served');
|
|
435
|
+
assertCrossProjectContract(cp, r.text);
|
|
436
|
+
assert.equal(cp.totals.projects, 2, 'both projects count');
|
|
437
|
+
assert.equal(cp.totals.credits, sumOf('credits'), 'credits are the sum across BOTH projects');
|
|
438
|
+
assert.equal(cp.totals.works_shipped, sumOf('tasks_shipped'), 'ships are summed across both');
|
|
439
|
+
assert.equal(cp.totals.karma, sumOf('karma'), 'karma is summed across both');
|
|
440
|
+
assert.deepEqual(cp.disciplines, ['engineer', 'ui'], 'the disciplines union spans both projects');
|
|
441
|
+
assert.equal(cp.projects.length, 1, 'only the publicly-catalogued project is NAMED');
|
|
442
|
+
assert.equal(cp.projects[0].name, PROOF_PROJECTS[0].name);
|
|
443
|
+
assert.equal(cp.more_projects, 1, 'the other is counted, never named');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
await t('R06.1 the record is viewer-INDEPENDENT — another account sees the same body', async () => {
|
|
447
|
+
const anon = await get(port, '/api/gds/builders/95/profile');
|
|
448
|
+
const other = await get(port, '/api/gds/builders/95/profile', { Cookie: 'gds_session=some-other-builder' });
|
|
449
|
+
assert.equal(other.status, 200);
|
|
450
|
+
assert.equal(other.text, anon.text, 'one response shape for every reader (ADR 0171 D4)');
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await t('R06.1 the profile read takes NO auth middleware — the portfolio is shareable', () => {
|
|
454
|
+
// A future PR walling this behind requireBuilder would break "another account
|
|
455
|
+
// can see it" while every assertion above still passed, so pin the shape.
|
|
456
|
+
const line = sourceOf('src/bongos/routes/builders.js').split('\n')
|
|
457
|
+
.find((l) => l.includes("router.get('/builders/:id/profile'"));
|
|
458
|
+
assert.ok(line, 'the profile route registration is findable');
|
|
459
|
+
assert.match(line, /router\.get\('\/builders\/:id\/profile', async \(req, res\) => \{/,
|
|
460
|
+
'nothing may sit between the path and the handler — that is what makes the record public');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
await t('R06.4 NEGATIVE CONTROL: the leak guard goes red when a private field is planted', async () => {
|
|
464
|
+
const realRead = hubPublicRead;
|
|
465
|
+
const rollup = async () => pi.getPublicProfileExtras(HUB_GITHUB_ID, { pool: engine });
|
|
466
|
+
try {
|
|
467
|
+
// (a) a module-internal key on the object itself.
|
|
468
|
+
hubPublicRead = async () => ({ ...(await rollup()), client_id: SECRET_CLIENT_ID });
|
|
469
|
+
let r = await get(port, '/api/gds/builders/95/profile');
|
|
470
|
+
assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
|
|
471
|
+
/cross_project keys are exactly the contract/, 'an extra top-level key must turn the guard red');
|
|
472
|
+
|
|
473
|
+
// (b) a module-internal key on a project ROW (the is_public discriminator).
|
|
474
|
+
hubPublicRead = async () => {
|
|
475
|
+
const cp = await rollup();
|
|
476
|
+
return { ...cp, projects: cp.projects.map((p) => ({ ...p, is_public: true })) };
|
|
477
|
+
};
|
|
478
|
+
r = await get(port, '/api/gds/builders/95/profile');
|
|
479
|
+
assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
|
|
480
|
+
/project rows carry only the whitelisted keys/, 'an extra project-row key must turn the guard red');
|
|
481
|
+
|
|
482
|
+
// (c) a sentinel smuggled inside an ALLOWED field — key lists stay legal, so
|
|
483
|
+
// this isolates the raw-body scan. This is the leak the shape check cannot see.
|
|
484
|
+
hubPublicRead = async () => {
|
|
485
|
+
const cp = await rollup();
|
|
486
|
+
return { ...cp, projects: [{ ...cp.projects[0], name: SECRET_PROJECT_NAME }] };
|
|
487
|
+
};
|
|
488
|
+
r = await get(port, '/api/gds/builders/95/profile');
|
|
489
|
+
assert.deepEqual(Object.keys(r.json.cross_project).sort(), [...CP_PUBLIC_KEYS].sort(),
|
|
490
|
+
'the planted leak is shape-legal — only the sentinel scan can catch it');
|
|
491
|
+
assert.throws(() => assertCrossProjectContract(r.json.cross_project, r.text),
|
|
492
|
+
/must not contain/, 'a sentinel in an allowed field must turn the guard red');
|
|
493
|
+
} finally {
|
|
494
|
+
hubPublicRead = realRead;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
await t('R06.2 hide_stats removes the record on the IMMEDIATE next load', async () => {
|
|
499
|
+
const before = await get(port, '/api/gds/builders/95/profile');
|
|
500
|
+
assert.ok(before.json.cross_project, 'public before the flip');
|
|
501
|
+
assert.equal(before.headers['cache-control'], 'no-store');
|
|
502
|
+
|
|
503
|
+
// Flip it through the REAL write the own-scoped PATCH /me/cross-project uses.
|
|
504
|
+
const row = await pi.setAccountHideStats(
|
|
505
|
+
{ githubId: HUB_GITHUB_ID, githubLogin: 'jaxri', hide: true }, { pool: engine });
|
|
506
|
+
assert.equal(row.hide_stats, true, 'the write path set the flag');
|
|
507
|
+
|
|
508
|
+
// No sleep, no second server, no cache warm-up: the very next load.
|
|
509
|
+
const after = await get(port, '/api/gds/builders/95/profile');
|
|
510
|
+
assert.equal(after.status, 200);
|
|
511
|
+
assert.equal(after.json.cross_project, null, 'gone on the next load');
|
|
512
|
+
assert.equal(after.headers['cache-control'], 'no-store', 'hide must beat the cache (ADR 0171 D5)');
|
|
513
|
+
assert.ok(!/max-age/.test(after.headers['cache-control'] || ''),
|
|
514
|
+
'any max-age would let a cache keep serving the pre-hide body — the old public, max-age=60 failed exactly here');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
await t('R06.5 no PARTIAL numbers survive the hide (a count beside a hidden total leaks by subtraction)', async () => {
|
|
518
|
+
// Set the flag here too, so this proof stands on its own rather than on
|
|
519
|
+
// whatever R06.2 left behind.
|
|
520
|
+
await pi.setAccountHideStats({ githubId: HUB_GITHUB_ID, githubLogin: 'jaxri', hide: true }, { pool: engine });
|
|
521
|
+
const r = await get(port, '/api/gds/builders/95/profile');
|
|
522
|
+
assert.equal(r.json.cross_project, null);
|
|
523
|
+
for (const key of ['totals', 'works_shipped', 'more_projects', 'last_active', 'rank_held']) {
|
|
524
|
+
assert.ok(!r.text.includes(key), `nothing derived from the rollup may survive — found "${key}"`);
|
|
525
|
+
}
|
|
526
|
+
const digits = [sumOf('credits'), sumOf('tasks_shipped'), sumOf('karma'),
|
|
527
|
+
...PROOF_PROJECTS.map((p) => p.credits)].map(String);
|
|
528
|
+
for (const v of [...digits, ...PROOF_PROJECTS.map((p) => p.name), ...PROOF_PROJECTS.map((p) => p.origin)]) {
|
|
529
|
+
assert.ok(!r.text.includes(v), `no rollup value may survive a hide — found "${v}"`);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ---- R06.3: the OTHER read paths honor hide too ---------------------------
|
|
534
|
+
|
|
535
|
+
const scoutApp = express();
|
|
536
|
+
scoutApp.use(attachFail);
|
|
537
|
+
scoutApp.use('/api/gds', buildScoutingRoutes());
|
|
538
|
+
const scout = await listen(scoutApp);
|
|
539
|
+
|
|
540
|
+
await t('R06.3 GET /scouting drops the hidden AND the provisional builder', async () => {
|
|
541
|
+
engine.calls.length = 0;
|
|
542
|
+
const r = await get(scout.port, '/api/gds/scouting');
|
|
543
|
+
assert.equal(r.status, 200);
|
|
544
|
+
const logins = r.json.builders.map((b) => b.github_login);
|
|
545
|
+
assert.deepEqual(logins, ['r06-visible'], 'only the consented, non-hidden builder is listed');
|
|
546
|
+
assert.equal(engine.calls.length, 1, 'exactly ONE read — nothing can re-enter via a second, unfiltered query');
|
|
547
|
+
for (const s of [HIDDEN_LOGIN, PROV_LOGIN, '88817', '6613', '5519', '71713']) {
|
|
548
|
+
assert.ok(!r.text.includes(s), `the excluded builder leaks nothing — found "${s}"`);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
await t('R06.3 the /people page reads /scouting and nothing else', async () => {
|
|
553
|
+
// /people is the hall's render of the same endpoint, so whatever /scouting
|
|
554
|
+
// drops, /people drops. Pin that it has no second data source.
|
|
555
|
+
const endpoints = [...sourceOf('modules/hall-ui/public/people.js').matchAll(/fetch\(\s*'([^']+)'/g)].map((m) => m[1]);
|
|
556
|
+
assert.deepEqual(endpoints, ['/api/gds/scouting?limit=500'], '/people has exactly one data source');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
scout.server.close();
|
|
280
560
|
server.close();
|
|
281
561
|
|
|
282
562
|
console.log(`\n${passed} passed, ${failed} failed`);
|