@bongos/core 1.19.422 → 1.19.424

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.
@@ -0,0 +1,477 @@
1
+ // tests/profile_privacy_owner_view.mjs — P10 (task 1002806, goal 1000045):
2
+ // "a private account's profile 404s to a stranger and RENDERS TO ITS OWNER",
3
+ // and a stats-hidden account renders NO PARTIAL NUMBERS.
4
+ //
5
+ // WHAT THIS FILE IS, AND WHAT IT DELIBERATELY IS NOT. The 404 half of P10 was
6
+ // already landed by task 1002967 in tests/effective_visibility_predicate.mjs:
7
+ // private / provisional / private-discoverable / private-previous-handle all
8
+ // 404 on GET /profiles/:handle, never 403, byte-identically for an anonymous
9
+ // viewer, a signed-in stranger and the owner. Re-asserting that here would buy
10
+ // nothing. This file is the OTHER half — the two things that proof left open:
11
+ //
12
+ // 1. THE OWNER'S PAGE, OVER REAL HTTP. 1002967 proves the own-scope side door
13
+ // at the FUNCTION level (pi.getOwnProfileExtras). But the page shell reads
14
+ // GET /me/cross-project, and that route is what must hand it the banner
15
+ // input (account_visibility='private') alongside the owner's own numbers.
16
+ // A function that behaves is not a route that behaves.
17
+ //
18
+ // 2. THE BYPASS THAT WOULD NULLIFY EVERY 404. The public read is viewer-
19
+ // independent precisely because the owner gets their page from the
20
+ // own-scope door instead. That door is therefore load-bearing for the
21
+ // privacy model: if it ever answered a CALLER-NAMED github_id instead of
22
+ // the session's, every 404 in 1002967 becomes a speed bump — ask the side
23
+ // door for the private account instead. Nothing tested that. This does.
24
+ //
25
+ // 3. NO PARTIAL NUMBERS, PROVED BY SUBTRACTION. 1002967 asserts the hidden
26
+ // account's body matches /"cross_project":null/. That is necessary and not
27
+ // sufficient: a future response that added `projects_count: 7` beside a
28
+ // null cross_project still matches it. So here every rollup-derived value
29
+ // is a PLANTED SENTINEL and the assertion is that none of them appears
30
+ // ANYWHERE in the hidden account's response text — with two controls that
31
+ // stop it passing vacuously (a public account's body DOES carry its
32
+ // sentinels; the hidden account's own numbers DO exist behind the door).
33
+ //
34
+ // THE RULE THIS FOLLOWS IS THE CODE'S, NOT THE PROFILE-PAGE SPEC'S. The spec
35
+ // docs/specs/bongos-v1-builder-profile-page.md says 404 "to everyone but the
36
+ // account itself", which reads as a viewer-dependent public response. The
37
+ // AUTHORITATIVE model — the privacy spec (task 1002785) D2 — reconciles it the
38
+ // other way and the code implements the reconciliation: the PUBLIC read 404s to
39
+ // everyone INCLUDING the owner, and the owner's page is composed from the
40
+ // own-scope endpoint. Both halves are asserted below.
41
+ //
42
+ // MECHANISM (tests/connections_ownership.mjs + tests/own_session_revoke.mjs):
43
+ // an in-memory accounts + activity table whose row filter is built FROM EACH
44
+ // STATEMENT'S OWN WHERE CLAUSE, refusing any statement whose placeholders or
45
+ // literal comparisons it did not consume — so a predicate the fake cannot read
46
+ // is a loud error rather than a silently absent filter. Everything above the
47
+ // pool is real: the real routers, the real domain module, real HTTP.
48
+ //
49
+ // DB-free by construction. Runs in the lane that actually executes (the last
50
+ // test in this file proves that).
51
+ //
52
+ // Run: node tests/profile_privacy_owner_view.mjs
53
+
54
+ import { strict as assert } from 'node:assert';
55
+ import { test, after } from 'node:test';
56
+ import { createRequire } from 'node:module';
57
+ import http from 'node:http';
58
+ import fs from 'node:fs';
59
+ import { fileURLToPath } from 'node:url';
60
+ import { dirname, join } from 'node:path';
61
+
62
+ const require = createRequire(import.meta.url);
63
+ const express = require('express');
64
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
65
+
66
+ // ===========================================================================
67
+ // The fixture: four accounts, and a sentinel for every rollup-derived value
68
+ // ===========================================================================
69
+ //
70
+ // Sentinels are per-account so a cross-wired response is caught too, and they
71
+ // are improbable literals so "absent from the body" means absent, not merely
72
+ // "did not collide with a 0 or a 1".
73
+
74
+ const OCTO = '4242'; // active · public · shown — the CONTROL
75
+ const GHOST = '5150'; // active · PRIVATE — the owner-renders subject
76
+ const SHY = '6060'; // active · public · HIDDEN — the no-partial-numbers subject
77
+ const NOSY = '7777'; // active · public — the stranger at the side door
78
+
79
+ const ACCOUNTS = [
80
+ { id: 1, github_id: OCTO, github_login: 'octo', display_name: 'Octo', avatar_url: null,
81
+ handle: 'octo', previous_handle: null, handle_changed_at: null, bio: null, links: null,
82
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
83
+ profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
84
+ { id: 2, github_id: GHOST, github_login: 'ghost', display_name: 'Ghost', avatar_url: null,
85
+ handle: 'ghost', previous_handle: null, handle_changed_at: null, bio: 'ghost-bio-515007', links: null,
86
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
87
+ profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
88
+ { id: 3, github_id: SHY, github_login: 'shy', display_name: 'Shy', avatar_url: null,
89
+ handle: 'shy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
90
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
91
+ profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
92
+ { id: 4, github_id: NOSY, github_login: 'nosy', display_name: 'Nosy', avatar_url: null,
93
+ handle: 'nosy', previous_handle: null, handle_changed_at: null, bio: null, links: null,
94
+ display_name_source: 'github', created_at: 'T0', updated_at: 'T0',
95
+ profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
96
+ ];
97
+
98
+ // Every number and name the cross-project rollup can produce, per account.
99
+ const ROLLUP = {
100
+ [OCTO]: {
101
+ agg: { projects: 811008, credits: 811001, works_shipped: 811002, karma: 811003, last_active: 'octo-active-811007', disciplines: ['octo-craft-811004'] },
102
+ projects: [{ name: 'Octo-Project-811005', origin: 'https://octo.example', rank_held: 'metic', recent_ships: ['Octo-Ship-811006'], is_public: true }],
103
+ },
104
+ [GHOST]: {
105
+ agg: { projects: 515008, credits: 515001, works_shipped: 515002, karma: 515003, last_active: 'ghost-active-515007', disciplines: ['ghost-craft-515004'] },
106
+ projects: [{ name: 'Ghost-Project-515005', origin: 'https://ghost.example', rank_held: 'metic', recent_ships: ['Ghost-Ship-515006'], is_public: true }],
107
+ },
108
+ [SHY]: {
109
+ agg: { projects: 606008, credits: 606001, works_shipped: 606002, karma: 606003, last_active: 'shy-active-606007', disciplines: ['shy-craft-606004'] },
110
+ projects: [{ name: 'Shy-Project-606005', origin: 'https://shy.example', rank_held: 'metic', recent_ships: ['Shy-Ship-606006'], is_public: true }],
111
+ },
112
+ [NOSY]: {
113
+ agg: { projects: 777008, credits: 777001, works_shipped: 777002, karma: 777003, last_active: 'nosy-active-777007', disciplines: ['nosy-craft-777004'] },
114
+ projects: [{ name: 'Nosy-Project-777005', origin: 'https://nosy.example', rank_held: 'metic', recent_ships: ['Nosy-Ship-777006'], is_public: true }],
115
+ },
116
+ };
117
+
118
+ // Every sentinel belonging to one account, as strings — the "did any of this
119
+ // leak?" alphabet. Numbers are included as their serialized form.
120
+ function sentinelsOf(githubId) {
121
+ const r = ROLLUP[githubId];
122
+ return [
123
+ String(r.agg.projects),
124
+ String(r.agg.credits), String(r.agg.works_shipped), String(r.agg.karma),
125
+ r.agg.last_active, ...r.agg.disciplines,
126
+ ...r.projects.flatMap((p) => [p.name, p.origin, ...p.recent_ships]),
127
+ ];
128
+ }
129
+
130
+ // ===========================================================================
131
+ // The in-memory tables + their statement interpreter
132
+ // ===========================================================================
133
+
134
+ // The columns this interpreter can filter on. A literal comparison against
135
+ // anything else throws — a WHERE it cannot read must never degrade to no WHERE.
136
+ const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'handle', 'previous_handle']);
137
+
138
+ /**
139
+ * Build a row test from a statement's own WHERE text. Literal comparisons carry
140
+ * as much weight as placeholders here: the effective-visibility predicate is
141
+ * made ENTIRELY of literal comparisons, so a fake that modelled only $N filters
142
+ * would score the privacy terms as absent and pass however the code changed.
143
+ */
144
+ function accountFilter(where, params) {
145
+ const tests = [];
146
+ const consumed = new Set();
147
+
148
+ if (/\bprofile_state = 'active'/.test(where)) tests.push((r) => r.profile_state === 'active');
149
+ if (/\baccount_visibility = 'public'/.test(where)) tests.push((r) => r.account_visibility === 'public');
150
+ if (/\bNOT (?:[a-z_]+\.)?hide_stats\b/.test(where)) tests.push((r) => !r.hide_stats);
151
+
152
+ const byId = /\b(?:[a-z_]+\.)?github_id = \$(\d+)/.exec(where);
153
+ if (byId) {
154
+ consumed.add(byId[1]);
155
+ const v = params[Number(byId[1]) - 1];
156
+ tests.push((r) => String(r.github_id) === String(v));
157
+ }
158
+
159
+ const byHandle = /lower\(handle\) = \$(\d+) OR lower\(previous_handle\) = \$(\d+)/.exec(where);
160
+ if (byHandle) {
161
+ consumed.add(byHandle[1]); consumed.add(byHandle[2]);
162
+ const v = String(params[Number(byHandle[1]) - 1]);
163
+ tests.push((r) => String(r.handle || '').toLowerCase() === v || String(r.previous_handle || '').toLowerCase() === v);
164
+ }
165
+
166
+ for (const m of where.matchAll(/\$(\d+)/g)) {
167
+ if (!consumed.has(m[1])) {
168
+ throw new Error(`fake pool: unread predicate on $${m[1]} in "${where}" — a filter this interpreter cannot read must never degrade to no filter`);
169
+ }
170
+ }
171
+ for (const m of where.matchAll(/\b([a-z_]+)\s*(?:=|<>|!=)\s*'/g)) {
172
+ if (!KNOWN_COLUMNS.has(m[1])) {
173
+ throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
174
+ }
175
+ }
176
+ return (r) => tests.every((fn) => fn(r));
177
+ }
178
+
179
+ // The github_id an activity statement is scoped to, read from its own WHERE.
180
+ function activityScope(sql, params) {
181
+ const m = /\ba\.github_id = \$(\d+)/.exec(sql);
182
+ if (!m) throw new Error(`fake pool: an activity read with no github_id scope — ${sql.slice(0, 120)}`);
183
+ return String(params[Number(m[1]) - 1]);
184
+ }
185
+
186
+ let calls = [];
187
+ const pool = {
188
+ query: async (text, params = []) => {
189
+ const sql = String(text).replace(/\s+/g, ' ').trim();
190
+ calls.push({ sql, params });
191
+
192
+ if (/FROM platform_identity_accounts/.test(sql)) {
193
+ const where = sql.includes(' WHERE ')
194
+ ? sql.slice(sql.indexOf(' WHERE ') + 7).replace(/ ORDER BY .*| LIMIT .*/g, '')
195
+ : '';
196
+ const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
197
+ let rows = ACCOUNTS.filter(accountFilter(where, params));
198
+ if (/is_current/.test(cols)) {
199
+ // resolveHandle asks the row whether the handle it matched is the CURRENT
200
+ // one — the flag that forks 200 from 301. Derived, never assumed.
201
+ const asked = String(params[0]).toLowerCase();
202
+ rows = rows
203
+ .map((r) => ({ ...r, is_current: String(r.handle || '').toLowerCase() === asked }))
204
+ .sort((a, b) => Number(b.is_current) - Number(a.is_current))
205
+ .slice(0, /LIMIT 1/.test(sql) ? 1 : rows.length);
206
+ }
207
+ return { rows: rows.map((r) => ({ ...r })), rowCount: rows.length };
208
+ }
209
+
210
+ // The rollup aggregate — one row, always (an aggregate with no GROUP BY).
211
+ if (/WITH agg AS/.test(sql) && /platform_identity_builder_project_activity/.test(sql)) {
212
+ const who = ROLLUP[activityScope(sql, params)];
213
+ const agg = who ? who.agg : { projects: 0, credits: 0, works_shipped: 0, karma: 0, last_active: null, disciplines: null };
214
+ return { rows: [{ ...agg }], rowCount: 1 };
215
+ }
216
+
217
+ // The per-project rows the rollup names (or collapses into more_projects).
218
+ if (/platform_identity_builder_project_activity/.test(sql)) {
219
+ const who = ROLLUP[activityScope(sql, params)];
220
+ const rows = who ? who.projects.map((p) => ({ ...p })) : [];
221
+ return { rows, rowCount: rows.length };
222
+ }
223
+
224
+ throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 140)}`);
225
+ },
226
+ };
227
+
228
+ // ===========================================================================
229
+ // The real routers over those tables
230
+ // ===========================================================================
231
+
232
+ // The viewer the stubbed session middleware presents. Real requireBuilder reads
233
+ // it from the session; the route body under test is untouched, and the route
234
+ // body is the whole point — a route that reached for req.query.github_id would
235
+ // be just as wrong with real auth in front of it.
236
+ let viewer = null;
237
+
238
+ const moduleApiPath = require.resolve('../src/module-api.js');
239
+ const helpers = require('../src/bongos/routes/_helpers.js');
240
+ require.cache[moduleApiPath] = {
241
+ id: moduleApiPath, filename: moduleApiPath, loaded: true,
242
+ exports: {
243
+ pool,
244
+ validateOrRespond: helpers.validateOrRespond,
245
+ // The public route's CORS wrapper is not under test; a pass-through keeps
246
+ // the doorway honest without dragging core config in.
247
+ corsPublicGet: () => (_req, _res, next) => next(),
248
+ requireBuilder: (req, res, next) => {
249
+ if (!viewer) return res.fail('unauthenticated', 401);
250
+ req.builder = { ...viewer };
251
+ next();
252
+ },
253
+ },
254
+ };
255
+
256
+ const pi = require('../modules/platform-identity/platform-identity.js');
257
+ const buildProfileRoutes = require('../modules/platform-identity/routes/profile.js');
258
+ const buildPublicProfileRoutes = require('../modules/platform-identity/routes/public-profile.js');
259
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
260
+
261
+ const app = express();
262
+ app.use(express.json());
263
+ app.use(attachFail);
264
+ app.use('/api/bongos', buildProfileRoutes());
265
+ app.use('/api/bongos', buildPublicProfileRoutes());
266
+ const server = app.listen(0, '127.0.0.1');
267
+ await new Promise((r) => server.once('listening', r));
268
+ const port = server.address().port;
269
+ after(() => server.close());
270
+
271
+ function get(path_, headers = {}) {
272
+ calls = [];
273
+ return new Promise((resolve, reject) => {
274
+ const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: path_, headers }, (res) => {
275
+ const chunks = [];
276
+ res.on('data', (c) => chunks.push(c));
277
+ res.on('end', () => {
278
+ const text = Buffer.concat(chunks).toString('utf8');
279
+ let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
280
+ resolve({ status: res.statusCode, text, json });
281
+ });
282
+ });
283
+ req.on('error', reject);
284
+ req.end();
285
+ });
286
+ }
287
+
288
+ const asAccount = (githubId) => {
289
+ const a = ACCOUNTS.find((x) => x.github_id === githubId);
290
+ viewer = { github_id: a.github_id, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url };
291
+ };
292
+
293
+ // ===========================================================================
294
+ // 0. The instrument is calibrated before it is trusted
295
+ // ===========================================================================
296
+
297
+ test('the interpreter refuses a predicate it cannot read, rather than ignoring it', () => {
298
+ assert.throws(
299
+ () => accountFilter('github_id = $1 AND some_new_flag = $2', [OCTO, true]),
300
+ /unread predicate on \$2/,
301
+ 'a filter the fake cannot model must be a loud error — otherwise a rewritten WHERE silently widens',
302
+ );
303
+ assert.throws(
304
+ () => accountFilter("github_id = $1 AND shadow_banned = 'yes'", [OCTO]),
305
+ /unrecognised literal comparison on shadow_banned/,
306
+ 'the privacy predicate is made of LITERAL comparisons — an unmodelled one must not read as absent',
307
+ );
308
+ });
309
+
310
+ test('the interpreter really does narrow on the visibility literals', () => {
311
+ const gated = ACCOUNTS.filter(accountFilter("(profile_state = 'active' AND account_visibility = 'public') AND (lower(handle) = $1 OR lower(previous_handle) = $1)", ['ghost']));
312
+ const ungated = ACCOUNTS.filter(accountFilter('(lower(handle) = $1 OR lower(previous_handle) = $1)', ['ghost']));
313
+ assert.equal(gated.length, 0, 'with the predicate, the private account is not reachable by handle');
314
+ assert.equal(ungated.length, 1, 'without it the SAME shape finds them — so dropping it genuinely changes this fixture');
315
+ });
316
+
317
+ // ===========================================================================
318
+ // 1. No partial numbers: the hidden account leaks nothing by subtraction
319
+ // ===========================================================================
320
+
321
+ test('CONTROL: a public account really does serve every rollup sentinel', async () => {
322
+ // Without this, "no sentinels in the hidden body" could just mean the rollup
323
+ // never ran at all, and every assertion below would be vacuous.
324
+ viewer = null;
325
+ const res = await get('/api/bongos/profiles/octo');
326
+ assert.equal(res.status, 200);
327
+ for (const s of sentinelsOf(OCTO)) {
328
+ assert.ok(res.text.includes(s), `a public account's body must carry its rollup value ${s} — otherwise the leak test proves nothing`);
329
+ }
330
+ assert.ok(res.json.cross_project && res.json.cross_project.totals, 'and the payload really is the rollup shape');
331
+ });
332
+
333
+ test('CONTROL: the hidden account HAS numbers — they exist, they are withheld', async () => {
334
+ // The other vacuity guard: prove the sentinels are real data behind the door,
335
+ // not an empty fixture that trivially cannot appear in any response.
336
+ const own = await pi.getOwnProfileExtras(SHY, { pool });
337
+ assert.equal(own.totals.credits, 606001, 'the hidden account has real numbers');
338
+ assert.equal(own.totals.works_shipped, 606002);
339
+ assert.deepEqual(own.projects.map((p) => p.name), ['Shy-Project-606005']);
340
+ });
341
+
342
+ test('a stats-hidden account renders NO PARTIAL NUMBERS — not one sentinel survives', async () => {
343
+ viewer = null;
344
+ const res = await get('/api/bongos/profiles/shy');
345
+ assert.equal(res.status, 200, 'hide removes the ROLLUP, not the page (spec D2) — the identity band still resolves');
346
+
347
+ // The whole point of P10: a count beside a hidden total is a leak by
348
+ // subtraction, so the test is not "cross_project is null" (which a stray
349
+ // sibling key would satisfy) but "none of these values is anywhere".
350
+ for (const s of sentinelsOf(SHY)) {
351
+ assert.ok(!res.text.includes(s), `the hidden account's ${s} appears in the response — a count beside a hidden total is a leak by subtraction`);
352
+ }
353
+
354
+ // ...and the withholding is TOTAL, not a hollowed-out object. An empty
355
+ // {totals:{}} would tell a reader the rollup exists and is being suppressed.
356
+ assert.equal(res.json.cross_project, null, 'cross_project must be exactly null, never a partial or emptied object');
357
+ assert.ok(!('totals' in res.json), 'no rollup key may be hoisted to the top level to survive the null');
358
+ assert.deepEqual(Object.keys(res.json).sort(), ['account', 'cross_project'],
359
+ 'the hidden response must carry no extra key — a new sibling key is exactly how a partial number arrives');
360
+
361
+ // The identity band is the other place a stray number can dock. The route
362
+ // whitelists it, and this pins that whitelist: a field added here reaches the
363
+ // world, so it must be a deliberate act that fails a test first.
364
+ assert.deepEqual(Object.keys(res.json.account).sort(),
365
+ ['avatar_url', 'bio', 'display_name', 'github_login', 'handle', 'links', 'member_since'],
366
+ 'a new key on the identity band of a HIDDEN account is how a count arrives beside a hidden total');
367
+ });
368
+
369
+ test('the withholding is indistinguishable from having no numbers at all', async () => {
370
+ // A distinct "hidden" marker would be the same disclosure as a 403: it tells
371
+ // a stranger the account has a track record it is choosing not to show.
372
+ viewer = null;
373
+ const shy = await get('/api/bongos/profiles/shy');
374
+ assert.doesNotMatch(shy.text, /hidden|hide_stats|private|withheld|suppressed/i,
375
+ 'the response must not name WHY the numbers are absent — that is itself the disclosure');
376
+ });
377
+
378
+ // ===========================================================================
379
+ // 2. Renders to its owner: the own-scope door, over real HTTP
380
+ // ===========================================================================
381
+
382
+ test('the private account gets its OWN page from the own-scope door', async () => {
383
+ // The half the public 404 exists to make safe. GET /me/cross-project is what
384
+ // the page shell reads when the viewer is the account.
385
+ asAccount(GHOST);
386
+ const res = await get('/api/bongos/me/cross-project');
387
+ assert.equal(res.status, 200, 'a private account is still a full participant in its own scope');
388
+
389
+ // The banner input: the shell renders "private" from this field, and it must
390
+ // be the STORED value, not a default.
391
+ assert.equal(res.json.account_visibility, 'private',
392
+ 'without this the owner cannot be told their page is private — the banner has no input');
393
+ assert.equal(res.json.handle, 'ghost', 'and it is their own record, identified');
394
+ assert.equal(res.json.bio, 'ghost-bio-515007');
395
+
396
+ // Their numbers are theirs: the side door is deliberately flag-blind.
397
+ assert.ok(res.json.cross_project, 'a private account must be able to read its own track record');
398
+ assert.equal(res.json.cross_project.totals.credits, 515001);
399
+ assert.equal(res.json.cross_project.totals.works_shipped, 515002);
400
+ assert.deepEqual(res.json.cross_project.projects.map((p) => p.name), ['Ghost-Project-515005']);
401
+
402
+ // ...and the reason it is a SIDE door: no visibility predicate is applied.
403
+ assert.ok(!calls.some((c) => /account_visibility = '|profile_state = '/.test(c.sql)),
404
+ 'the self view must apply no visibility predicate — that is what makes it a side door, not a second public read');
405
+ });
406
+
407
+ test('BOTH HALVES AT ONCE: the same account, same instant — 404 public, 200 own', async () => {
408
+ // P10 stated as one assertion. The public response is viewer-independent, so
409
+ // the owner sees 404 there too (privacy spec D2, reconciling the profile-page
410
+ // spec's looser "404 to everyone but the account itself"); the page they
411
+ // actually see is composed from the own-scope door.
412
+ asAccount(GHOST);
413
+ const publicRead = await get('/api/bongos/profiles/ghost');
414
+ assert.equal(publicRead.status, 404, 'the PUBLIC read 404s even for the owner — it takes no viewer, so it cannot vary by one');
415
+ assert.notEqual(publicRead.status, 403, 'a 403 would confirm the handle exists, which is the disclosure the 404 rule prevents');
416
+ assert.ok(!publicRead.text.includes('Ghost'), 'and nothing about the account rides in the 404 body');
417
+ assert.ok(!publicRead.text.includes('ghost-bio-515007'));
418
+
419
+ const ownRead = await get('/api/bongos/me/cross-project');
420
+ assert.equal(ownRead.status, 200, 'and the very same account, in the very same state, renders to itself');
421
+ assert.equal(ownRead.json.handle, 'ghost');
422
+ });
423
+
424
+ test('a HIDDEN owner still sees their own numbers — hide is a public rule, not a self rule', async () => {
425
+ asAccount(SHY);
426
+ const res = await get('/api/bongos/me/cross-project');
427
+ assert.equal(res.status, 200);
428
+ assert.equal(res.json.hide_stats, true, 'the toggle reads back, so Settings can render its state');
429
+ assert.equal(res.json.cross_project.totals.credits, 606001,
430
+ 'ADR 0171 D2: a hidden builder inspects their numbers HERE, which is why the public route may return null');
431
+ });
432
+
433
+ // ===========================================================================
434
+ // 3. The bypass that would nullify every 404
435
+ // ===========================================================================
436
+
437
+ test('the own-scope door answers the SESSION, never a caller-named account', async () => {
438
+ // If this door ever honoured a caller-named id, every 404 the public read
439
+ // returns becomes a speed bump: ask the side door for the private account
440
+ // instead. It is the single most load-bearing assertion in this file.
441
+ asAccount(NOSY);
442
+ const attempts = [
443
+ ['/api/bongos/me/cross-project?github_id=' + GHOST, {}],
444
+ ['/api/bongos/me/cross-project?id=' + GHOST, {}],
445
+ ['/api/bongos/me/cross-project?handle=ghost', {}],
446
+ ['/api/bongos/me/cross-project?github_login=ghost', {}],
447
+ ['/api/bongos/me/cross-project', { 'x-github-id': GHOST }],
448
+ ['/api/bongos/me/cross-project', { 'x-builder-id': GHOST, 'x-github-login': 'ghost' }],
449
+ ];
450
+ for (const [path_, headers] of attempts) {
451
+ const res = await get(path_, headers);
452
+ assert.equal(res.status, 200, `${path_} must still serve the caller`);
453
+ assert.equal(res.json.handle, 'nosy', `${path_} returned someone else's account — the own-scope door is not own-scoped`);
454
+ assert.equal(res.json.account_visibility, 'public', `${path_} leaked another account's privacy state`);
455
+ for (const s of sentinelsOf(GHOST)) {
456
+ assert.ok(!res.text.includes(s), `${path_} leaked the private account's ${s}`);
457
+ }
458
+ }
459
+ });
460
+
461
+ test('the own-scope door is closed to an unauthenticated caller', async () => {
462
+ viewer = null;
463
+ const res = await get('/api/bongos/me/cross-project');
464
+ assert.equal(res.status, 401, 'no session, no self — otherwise "own scope" means "anyone\'s scope"');
465
+ });
466
+
467
+ // ===========================================================================
468
+ // 4. Lane proof — a test that never runs is not a gate (task 1002282)
469
+ // ===========================================================================
470
+
471
+ test('this proof runs in the DB-free lane that executes on every PR', () => {
472
+ const runner = fs.readFileSync(join(ROOT, 'scripts', 'gds', 'run-unit-tests.js'), 'utf8');
473
+ assert.ok(
474
+ !/profile_privacy_owner_view/.test(runner),
475
+ 'named in run-unit-tests.js means parked into INTEGRATION or KNOWN_FAILING — where CI has no Postgres, this file would self-skip forever and gate nothing',
476
+ );
477
+ });