@bongos/core 1.21.52 → 1.21.54
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 +88 -73
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +7 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +135 -3
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +23 -11
- package/docs/copy-registry.json +142 -13
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +202 -202
- package/modules/hall-ui/public/atlas.html +3 -3
- package/modules/hall-ui/public/blockers.html +3 -3
- package/modules/hall-ui/public/board-room.html +3 -3
- package/modules/hall-ui/public/city.html +3 -3
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/commands.html +3 -3
- package/modules/hall-ui/public/copy-desk.html +3 -3
- package/modules/hall-ui/public/craft-mode.js +7 -1
- package/modules/hall-ui/public/deploy.html +3 -3
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/drachmae.html +3 -3
- package/modules/hall-ui/public/fleet.html +3 -3
- package/modules/hall-ui/public/gate.html +3 -3
- package/modules/hall-ui/public/goal-map.html +3 -3
- package/modules/hall-ui/public/goals.html +3 -3
- package/modules/hall-ui/public/government.html +3 -3
- package/modules/hall-ui/public/idea.html +3 -3
- package/modules/hall-ui/public/ideas.html +3 -3
- package/modules/hall-ui/public/index.html +3 -3
- package/modules/hall-ui/public/lightbulb.js +213 -0
- package/modules/hall-ui/public/modules.html +3 -3
- package/modules/hall-ui/public/primer.html +3 -3
- package/modules/hall-ui/public/profile.html +3 -3
- package/modules/hall-ui/public/project-settings.html +3 -3
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +3 -3
- package/modules/hall-ui/public/roster.html +3 -3
- package/modules/hall-ui/public/sessions.html +3 -3
- package/modules/hall-ui/public/settings.html +3 -3
- package/modules/hall-ui/public/street-card.css +18 -0
- package/modules/hall-ui/public/street-card.js +259 -4
- package/modules/hall-ui/public/street-desk.js +11 -7
- package/modules/hall-ui/public/street.html +7 -7
- package/modules/hall-ui/public/street.js +121 -14
- package/modules/hall-ui/public/studio.html +3 -3
- package/modules/hall-ui/public/style.css +1 -0
- package/modules/hall-ui/public/task.html +3 -3
- package/modules/hall-ui/public/thinking.html +3 -3
- package/modules/hall-ui/public/tweak-editor.html +3 -3
- package/modules/hall-ui/public/watch.html +3 -3
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/craft-mode.md +8 -0
- package/modules/hall-ui/records/governor-street.md +15 -0
- package/modules/page-notes/db.js +34 -0
- package/modules/page-notes/notes.js +54 -8
- package/modules/page-notes/routes/page-notes.js +49 -1
- package/modules/page-notes/split.js +39 -8
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/module-api.js +1 -1
- package/tests/hall_street_desk.mjs +370 -5
- package/tests/hall_tweak_editor.mjs +1 -1
- package/tests/lightbulb.mjs +385 -0
- package/tests/page_notes_panel.mjs +3 -1
- package/tests/paintbrush.mjs +2 -1
- package/tests/privacy_matrix.mjs +689 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
// tests/privacy_matrix.mjs — PROOF: the privacy matrix as a test (task 1002973,
|
|
2
|
+
// goal 1000110; privacy spec docs/specs/bongos-v1-privacy-and-invites.md D2/D3/D4,
|
|
3
|
+
// ADR 0171 D4, ADR 0215, ADR 0356).
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE IS. Every row of the spec's D2 table — states × who sees what —
|
|
6
|
+
// is already proved somewhere, one surface at a time: the profile 404 in
|
|
7
|
+
// tests/effective_visibility_predicate.mjs, the owner's side door in
|
|
8
|
+
// tests/profile_privacy_owner_view.mjs, the sliver in
|
|
9
|
+
// tests/recruiter_sliver_boundary.mjs, search and the leaderboard in their own
|
|
10
|
+
// route files, the invite window in tests/invite_expiry_db.mjs. What none of
|
|
11
|
+
// them is, is THE TABLE. This file is: one fixture account per D2 row, every
|
|
12
|
+
// viewer column, every cross-project surface that exists today, run through the
|
|
13
|
+
// real routers in one process, and asserted against a literal transcription of
|
|
14
|
+
// the spec's cells (MATRIX below). A cell that drifts — a surface added without
|
|
15
|
+
// the predicate, a predicate loosened for one reader — reds here as the cell it
|
|
16
|
+
// is, beside the six it should still agree with.
|
|
17
|
+
//
|
|
18
|
+
// The four claims the task names, each asserted across the whole table rather
|
|
19
|
+
// than on one route:
|
|
20
|
+
// 1. a private account's PUBLIC read 404s to a stranger AND to its own session,
|
|
21
|
+
// and the owner's own record comes from the own-scope door instead (both
|
|
22
|
+
// halves; the D2 "public responses stay viewer-independent" rule);
|
|
23
|
+
// 2. hidden stats never appear in ANY response to anyone but the account
|
|
24
|
+
// itself — planted sentinels, checked against every recorded body, with
|
|
25
|
+
// controls so absence cannot pass vacuously;
|
|
26
|
+
// 3. the recruiter sliver is exactly D3's shape (the module's own key lists);
|
|
27
|
+
// 4. an expired invite refuses sign-in, through the real admission read.
|
|
28
|
+
// The fifth clause the task was filed with — "a declined invite's door is
|
|
29
|
+
// closed instance-side" — is NOT asserted here: that relay does not exist yet.
|
|
30
|
+
// It moved to task 1004444, which builds it and adds the row to this file
|
|
31
|
+
// (owner decision, 2026-10-04).
|
|
32
|
+
//
|
|
33
|
+
// CELLS WITH NO SURFACE TODAY are named, not dropped: the /u/<handle> page
|
|
34
|
+
// (canonical-profile.js keeps it switched off — a tripwire test below reds when
|
|
35
|
+
// it is switched on, so its cells get added), the sitemap and an Explore people
|
|
36
|
+
// surface (not built), and the Ops column (internal only, never a response).
|
|
37
|
+
//
|
|
38
|
+
// MECHANISM (tests/recruiter_sliver_boundary.mjs's, generalised to every route):
|
|
39
|
+
// in-memory tables whose row filter is parsed FROM EACH STATEMENT'S OWN WHERE
|
|
40
|
+
// TEXT, consuming each recognised term and refusing whatever is left unless it
|
|
41
|
+
// is pure conjunction — so a predicate this interpreter cannot read, or one
|
|
42
|
+
// whose AND became an OR, is a loud error rather than a silently different
|
|
43
|
+
// filter. Above the pool everything is real: the routers, the module, HTTP. The
|
|
44
|
+
// interpreter is calibrated against the module's own JS twins before any cell
|
|
45
|
+
// is trusted.
|
|
46
|
+
//
|
|
47
|
+
// DB-free by construction; the last test proves it runs in the unit lane.
|
|
48
|
+
//
|
|
49
|
+
// Run: node tests/privacy_matrix.mjs
|
|
50
|
+
|
|
51
|
+
import { strict as assert } from 'node:assert';
|
|
52
|
+
import { test, after } from 'node:test';
|
|
53
|
+
import { createRequire } from 'node:module';
|
|
54
|
+
import http from 'node:http';
|
|
55
|
+
import fs from 'node:fs';
|
|
56
|
+
import path from 'node:path';
|
|
57
|
+
import { fileURLToPath } from 'node:url';
|
|
58
|
+
|
|
59
|
+
// The pure predicate module only — it has no doorway dependency, so importing it
|
|
60
|
+
// before the stubs below is safe. Everything that reaches the doorway is
|
|
61
|
+
// REQUIRED after them (an ESM import is hoisted and would load the real one).
|
|
62
|
+
import {
|
|
63
|
+
accountVisibleSql,
|
|
64
|
+
isAccountVisible,
|
|
65
|
+
rollupVisibleSql,
|
|
66
|
+
isRollupVisible,
|
|
67
|
+
recruiterSliverVisibleSql,
|
|
68
|
+
isRecruiterSliverVisible,
|
|
69
|
+
scoutingListableSql,
|
|
70
|
+
scoutingNameOnlySql,
|
|
71
|
+
} from '../modules/platform-identity/account-visibility.js';
|
|
72
|
+
|
|
73
|
+
const require = createRequire(import.meta.url);
|
|
74
|
+
const express = require('express');
|
|
75
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
76
|
+
|
|
77
|
+
// ===========================================================================
|
|
78
|
+
// The fixture — one account per D2 row
|
|
79
|
+
// ===========================================================================
|
|
80
|
+
//
|
|
81
|
+
// Every value is an improbable per-account literal, so "absent from the body"
|
|
82
|
+
// means absent. The provisional account carries the MOST permissive flags on
|
|
83
|
+
// purpose (public, shown, discoverable, terms accepted): D2 says provisional
|
|
84
|
+
// dominates every flag, so nothing else may let it through.
|
|
85
|
+
|
|
86
|
+
const KEYS = ['PROV', 'PUB', 'SHY', 'PRIV', 'PRIVD', 'PRIVDH', 'OPTOUT'];
|
|
87
|
+
const STATE = {
|
|
88
|
+
PROV: { label: 'provisional (any flags)', profile_state: 'provisional', account_visibility: 'public', hide_stats: false, recruiter_discoverable: true },
|
|
89
|
+
PUB: { label: 'active · public · stats shown', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: null },
|
|
90
|
+
SHY: { label: 'active · public · hide_stats', profile_state: 'active', account_visibility: 'public', hide_stats: true, recruiter_discoverable: null },
|
|
91
|
+
PRIV: { label: 'active · private (not discoverable)', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: null },
|
|
92
|
+
PRIVD: { label: 'active · private · discoverable', profile_state: 'active', account_visibility: 'private', hide_stats: false, recruiter_discoverable: true },
|
|
93
|
+
PRIVDH: { label: 'active · private · discoverable · hide_stats', profile_state: 'active', account_visibility: 'private', hide_stats: true, recruiter_discoverable: true },
|
|
94
|
+
OPTOUT: { label: 'active · public · opted out of recruiting', profile_state: 'active', account_visibility: 'public', hide_stats: false, recruiter_discoverable: false },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const ACCOUNTS = KEYS.map((k, i) => {
|
|
98
|
+
const id = String(3120001 + i);
|
|
99
|
+
const low = k.toLowerCase();
|
|
100
|
+
return {
|
|
101
|
+
key: k,
|
|
102
|
+
github_id: id,
|
|
103
|
+
handle: `${low}-mx`,
|
|
104
|
+
github_login: `${low}-login-${id}`,
|
|
105
|
+
display_name: `${k} Matrix ${id}`,
|
|
106
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${id}?v=4`,
|
|
107
|
+
previous_handle: null,
|
|
108
|
+
bio: `${low}-bio-${id}`,
|
|
109
|
+
links: null,
|
|
110
|
+
created_at: 'T0',
|
|
111
|
+
terms_accepted_at: 'T1',
|
|
112
|
+
...STATE[k],
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
const ACCT = Object.fromEntries(ACCOUNTS.map((a) => [a.key, a]));
|
|
116
|
+
|
|
117
|
+
// The cross-project record behind each account: every number a sentinel.
|
|
118
|
+
const ROLLUP = Object.fromEntries(ACCOUNTS.map((a, i) => {
|
|
119
|
+
const n = 960000 + i * 1000;
|
|
120
|
+
const low = a.key.toLowerCase();
|
|
121
|
+
return [a.github_id, {
|
|
122
|
+
credits: n + 1, works_shipped: n + 2, karma: n + 3, project_count: n + 4,
|
|
123
|
+
last_active: `${low}-lastactive-${n + 5}`,
|
|
124
|
+
disciplines: [`${low}-craft-${n + 8}`],
|
|
125
|
+
projects: [{ name: `${a.key}-Project-${n + 6}`, origin: `https://${low}.example`, rank_held: 'metic', recent_ships: [`${a.key}-Ship-${n + 7}`], is_public: true }],
|
|
126
|
+
}];
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
/** Every rollup-derived string an account's record could leak. */
|
|
130
|
+
function rollupSentinels(acct) {
|
|
131
|
+
const r = ROLLUP[acct.github_id];
|
|
132
|
+
return [
|
|
133
|
+
String(r.credits), String(r.works_shipped), String(r.karma), String(r.project_count),
|
|
134
|
+
r.last_active, ...r.disciplines,
|
|
135
|
+
...r.projects.flatMap((p) => [p.name, p.origin, ...p.recent_ships]),
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
/** Every string that names an account — what "absent" must mean for it. */
|
|
139
|
+
function identitySentinels(acct) {
|
|
140
|
+
return [acct.handle, acct.github_login, acct.display_name, acct.bio];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ===========================================================================
|
|
144
|
+
// THE MATRIX — spec D2, transcribed cell by cell for the surfaces that exist
|
|
145
|
+
// ===========================================================================
|
|
146
|
+
//
|
|
147
|
+
// profile GET /profiles/:handle Anon · Builder · Self's PUBLIC read (viewer-independent)
|
|
148
|
+
// 404 | 'full' (record shown) | 'identity' (renders, record null)
|
|
149
|
+
// search GET /community/search Anon · Builder — "findable by name/handle"
|
|
150
|
+
// 'listed' | 'name-only' (listed, disciplines null) | 'absent'
|
|
151
|
+
// board GET /community/leaderboard Anon · Builder — the cross-project leaderboard
|
|
152
|
+
// 'listed' | 'absent'
|
|
153
|
+
// directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
|
|
154
|
+
// sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
|
|
155
|
+
// self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
|
|
156
|
+
const MATRIX = {
|
|
157
|
+
PROV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'n/a' },
|
|
158
|
+
PUB: { profile: 'full', search: 'listed', board: 'listed', directory: 'full', sliver: 'full', self: 'own' },
|
|
159
|
+
SHY: { profile: 'identity', search: 'name-only', board: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own' },
|
|
160
|
+
PRIV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'own' },
|
|
161
|
+
PRIVD: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'full', self: 'own' },
|
|
162
|
+
PRIVDH: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'name-only', self: 'own' },
|
|
163
|
+
OPTOUT: { profile: 'full', search: 'listed', board: 'listed', directory: 'absent', sliver: 404, self: 'own' },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// ===========================================================================
|
|
167
|
+
// The in-memory tables + their statement interpreter
|
|
168
|
+
// ===========================================================================
|
|
169
|
+
|
|
170
|
+
const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'handle', 'previous_handle', 'recruiter_discoverable']);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* A row test built from a statement's own WHERE text (the
|
|
174
|
+
* tests/recruiter_sliver_boundary.mjs interpreter). Each recognised term is
|
|
175
|
+
* CONSUMED as it is matched; whatever survives must be whitespace, parentheses
|
|
176
|
+
* and AND, or this throws — an OR, a NOT it cannot read, an unread placeholder
|
|
177
|
+
* or an unknown literal comparison is never degraded to "no filter".
|
|
178
|
+
*/
|
|
179
|
+
function accountFilter(where, params) {
|
|
180
|
+
const tests = [];
|
|
181
|
+
const consumed = new Set();
|
|
182
|
+
let residue = where;
|
|
183
|
+
// an alias may carry a digit (the search's pa2), so A is [a-z_][a-z0-9_]*
|
|
184
|
+
const take = (re, make) => {
|
|
185
|
+
const m = re.exec(residue);
|
|
186
|
+
if (!m) return null;
|
|
187
|
+
residue = residue.slice(0, m.index) + residue.slice(m.index + m[0].length);
|
|
188
|
+
tests.push(make(m));
|
|
189
|
+
return m;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// reach FIRST: its COALESCE spells `account_visibility = 'public'` as its own
|
|
193
|
+
// fallback, which must not be scored as an existence check
|
|
194
|
+
take(
|
|
195
|
+
/\bCOALESCE\(\s*(?:[a-z_][a-z0-9_]*\.)?recruiter_discoverable\s*,\s*(?:[a-z_][a-z0-9_]*\.)?account_visibility = 'public'\s*\)/,
|
|
196
|
+
() => (r) => (typeof r.recruiter_discoverable === 'boolean' ? r.recruiter_discoverable : r.account_visibility === 'public'),
|
|
197
|
+
);
|
|
198
|
+
take(/\b(?:[a-z_][a-z0-9_]*\.)?profile_state = 'active'/, () => (r) => r.profile_state === 'active');
|
|
199
|
+
take(/\b(?:[a-z_][a-z0-9_]*\.)?terms_accepted_at IS NOT NULL/, () => (r) => r.terms_accepted_at != null);
|
|
200
|
+
take(/\b(?:[a-z_][a-z0-9_]*\.)?account_visibility = 'public'/, () => (r) => r.account_visibility === 'public');
|
|
201
|
+
take(/\bNOT (?:[a-z_][a-z0-9_]*\.)?hide_stats\b/, () => (r) => !r.hide_stats);
|
|
202
|
+
take(/\b(?:[a-z_][a-z0-9_]*\.)?hide_stats\b/, () => (r) => !!r.hide_stats);
|
|
203
|
+
take(/\b(?:[a-z_][a-z0-9_]*\.)?github_id = \$(\d+)/, (m) => {
|
|
204
|
+
consumed.add(m[1]);
|
|
205
|
+
const v = params[Number(m[1]) - 1];
|
|
206
|
+
return (r) => String(r.github_id) === String(v);
|
|
207
|
+
});
|
|
208
|
+
// resolveHandle's rename window is the one legitimate OR, consumed whole
|
|
209
|
+
const both = take(/lower\(handle\) = \$(\d+) OR lower\(previous_handle\) = \$(\d+)/, (m) => {
|
|
210
|
+
consumed.add(m[1]); consumed.add(m[2]);
|
|
211
|
+
const v = String(params[Number(m[1]) - 1]);
|
|
212
|
+
return (r) => String(r.handle || '').toLowerCase() === v || String(r.previous_handle || '').toLowerCase() === v;
|
|
213
|
+
});
|
|
214
|
+
if (!both) {
|
|
215
|
+
take(/lower\(handle\) = \$(\d+)/, (m) => {
|
|
216
|
+
consumed.add(m[1]);
|
|
217
|
+
const v = String(params[Number(m[1]) - 1]);
|
|
218
|
+
return (r) => String(r.handle || '').toLowerCase() === v;
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const m of where.matchAll(/\$(\d+)/g)) {
|
|
223
|
+
if (!consumed.has(m[1])) throw new Error(`fake pool: unread predicate on $${m[1]} in "${where}"`);
|
|
224
|
+
}
|
|
225
|
+
for (const m of where.matchAll(/\b([a-z_]+)\s*(?:=|<>|!=)\s*'/g)) {
|
|
226
|
+
if (!KNOWN_COLUMNS.has(m[1])) throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
|
|
227
|
+
}
|
|
228
|
+
if (!/^[\s()]*(?:AND[\s()]*)*$/.test(residue)) {
|
|
229
|
+
throw new Error(`fake pool: unmodelled boolean structure "${residue.trim()}" in "${where}"`);
|
|
230
|
+
}
|
|
231
|
+
return (r) => tests.every((fn) => fn(r));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Lift one predicate out of a statement by the text around it, or fail loudly. */
|
|
235
|
+
function lift(sql, re, what) {
|
|
236
|
+
const m = re.exec(sql);
|
|
237
|
+
if (!m) throw new Error(`fake pool: could not read the ${what} predicate — the statement moved; update this fake alongside it: ${sql.slice(0, 200)}`);
|
|
238
|
+
return m[1];
|
|
239
|
+
}
|
|
240
|
+
function activityScope(sql, params) {
|
|
241
|
+
const m = /\ba\.github_id = \$(\d+)/.exec(sql);
|
|
242
|
+
if (!m) throw new Error(`fake pool: an activity read with no github_id scope — ${sql.slice(0, 120)}`);
|
|
243
|
+
return String(params[Number(m[1]) - 1]);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const pool = {
|
|
247
|
+
query: async (text, params = []) => {
|
|
248
|
+
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
249
|
+
|
|
250
|
+
// THE LEADERBOARD's totals: the rollup floor sits on the act CTE's join.
|
|
251
|
+
if (/platform_identity_activity_snapshots/.test(sql)) {
|
|
252
|
+
const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'leaderboard totals');
|
|
253
|
+
const rows = ACCOUNTS.filter(accountFilter(where, [])).map((a) => {
|
|
254
|
+
const r = ROLLUP[a.github_id];
|
|
255
|
+
return { github_id: a.github_id, credits: String(r.credits), tasks_shipped: String(r.works_shipped), karma: String(r.karma), disciplines: r.disciplines };
|
|
256
|
+
});
|
|
257
|
+
return { rows, rowCount: rows.length };
|
|
258
|
+
}
|
|
259
|
+
// ... and its eligibility re-read, scoped to the candidate ids.
|
|
260
|
+
if (/= ANY\(\$1::bigint\[\]\)/.test(sql)) {
|
|
261
|
+
const where = lift(sql, /WHERE pa\.github_id = ANY\(\$1::bigint\[\]\) AND (.+)$/, 'leaderboard eligibility');
|
|
262
|
+
const ids = new Set((params[0] || []).map(String));
|
|
263
|
+
const rows = ACCOUNTS.filter((a) => ids.has(a.github_id)).filter(accountFilter(where, []))
|
|
264
|
+
.map((a) => ({ github_id: a.github_id, handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url }));
|
|
265
|
+
return { rows, rowCount: rows.length };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// HUB SEARCH: the name match floors on existence, the disciplines on the rollup.
|
|
269
|
+
if (/WITH matches AS/.test(sql)) {
|
|
270
|
+
const names = accountFilter(lift(sql, /FROM platform_identity_accounts pa WHERE (.+?) AND \( lower\(pa\.handle\) LIKE/, 'search name-match'), []);
|
|
271
|
+
const disc = accountFilter(lift(sql, /WHERE pa2\.github_id = a\.github_id AND (.+?)\)\) x WHERE/, 'search discipline'), []);
|
|
272
|
+
const [rawQ, likePattern, discipline, limit, offset] = params;
|
|
273
|
+
const q = String(rawQ).toLowerCase();
|
|
274
|
+
const prefix = String(likePattern).replace(/\\(.)/g, '$1').replace(/%$/, '').toLowerCase();
|
|
275
|
+
const rows = ACCOUNTS.filter(names)
|
|
276
|
+
.filter((a) => [a.handle, a.display_name].some((s) => s.toLowerCase().startsWith(prefix) || s.toLowerCase().includes(q)))
|
|
277
|
+
.map((a) => ({ handle: a.handle, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url, disciplines: disc(a) ? ROLLUP[a.github_id].disciplines : null }))
|
|
278
|
+
.filter((r) => !discipline || (r.disciplines || []).includes(discipline))
|
|
279
|
+
.slice(offset, offset + limit);
|
|
280
|
+
return { rows, rowCount: rows.length };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// THE RECRUITING DIRECTORY: consent lives in an EXISTS correlated to activity.
|
|
284
|
+
if (/WITH agg AS/.test(sql) && /platform_identity_accounts pa/.test(sql)) {
|
|
285
|
+
const where = lift(sql, /pa\.github_id = a\.github_id AND ([\s\S]*?)\)\s*GROUP BY/, 'directory');
|
|
286
|
+
const rows = ACCOUNTS.filter(accountFilter(where, [])).map((a) => {
|
|
287
|
+
const r = ROLLUP[a.github_id];
|
|
288
|
+
return { github_id: a.github_id, github_login: a.github_login, display_name: a.display_name, avatar_url: a.avatar_url, project_count: r.project_count, total_credits: r.credits, total_tasks_shipped: r.works_shipped, total_karma: r.karma, last_active: r.last_active, disciplines: r.disciplines };
|
|
289
|
+
});
|
|
290
|
+
return { rows, rowCount: rows.length };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// THE SLIVER's activity half — with columns the statement never selects, so
|
|
294
|
+
// a projection that spreads instead of whitelisting leaks a number here.
|
|
295
|
+
if (/WITH act AS/.test(sql)) {
|
|
296
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
297
|
+
return {
|
|
298
|
+
rows: [{
|
|
299
|
+
active_recently: r ? true : null, disciplines: r ? r.disciplines : null,
|
|
300
|
+
last_active: r ? r.last_active : null, credits: r ? r.credits : null, works_shipped: r ? r.works_shipped : null,
|
|
301
|
+
karma: r ? r.karma : null, project_count: r ? r.project_count : null, project_name: r ? r.projects[0].name : null,
|
|
302
|
+
}],
|
|
303
|
+
rowCount: 1,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// The directory's ownership door (scouting-authz): nobody here owns a project.
|
|
308
|
+
if (/platform_identity_project_memberships/.test(sql)) return { rows: [], rowCount: 0 };
|
|
309
|
+
|
|
310
|
+
// Account reads — the public profile, the sliver, the own-scope read, the
|
|
311
|
+
// directory's name-only tier. Rollup scalars ride the row so a spread leaks.
|
|
312
|
+
if (/FROM platform_identity_accounts/.test(sql)) {
|
|
313
|
+
const where = sql.includes(' WHERE ') ? sql.slice(sql.indexOf(' WHERE ') + 7).replace(/ ORDER BY .*| LIMIT .*/g, '') : '';
|
|
314
|
+
const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
|
|
315
|
+
let rows = ACCOUNTS.filter(accountFilter(where, params)).map((a) => ({ ...a, ...ROLLUP[a.github_id] }));
|
|
316
|
+
if (/is_current/.test(cols)) {
|
|
317
|
+
const asked = String(params[0]).toLowerCase();
|
|
318
|
+
rows = rows.map((r) => ({ ...r, is_current: r.handle.toLowerCase() === asked }))
|
|
319
|
+
.sort((a, b) => Number(b.is_current) - Number(a.is_current)).slice(0, 1);
|
|
320
|
+
}
|
|
321
|
+
return { rows, rowCount: rows.length };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// The per-builder rollup behind the profile's record and the own-scope door.
|
|
325
|
+
if (/WITH agg AS/.test(sql) && /platform_identity_builder_project_activity/.test(sql)) {
|
|
326
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
327
|
+
return { rows: [{ projects: r.project_count, credits: r.credits, works_shipped: r.works_shipped, karma: r.karma, last_active: r.last_active, disciplines: r.disciplines }], rowCount: 1 };
|
|
328
|
+
}
|
|
329
|
+
if (/platform_identity_builder_project_activity/.test(sql)) {
|
|
330
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
331
|
+
return { rows: r.projects.map((p) => ({ ...p })), rowCount: r.projects.length };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 140)}`);
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
// ===========================================================================
|
|
339
|
+
// The real routers over those tables
|
|
340
|
+
// ===========================================================================
|
|
341
|
+
|
|
342
|
+
// Who the stubbed session presents. `rank` is the hub rank (the sliver's floor),
|
|
343
|
+
// `curate` the directory's permission door.
|
|
344
|
+
let viewer = null;
|
|
345
|
+
const ANON = null;
|
|
346
|
+
const STRANGER = { name: 'Builder', github_id: '7100001', rank: 'xenos', curate: false };
|
|
347
|
+
const RECRUITER = { name: 'Recruiter', github_id: '7100002', rank: 'archon', curate: true };
|
|
348
|
+
const selfOf = (k) => ({ name: 'Self', github_id: ACCT[k].github_id, rank: 'xenos', curate: false });
|
|
349
|
+
|
|
350
|
+
const moduleApiPath = require.resolve('../src/module-api.js');
|
|
351
|
+
const helpers = require('../src/bongos/routes/_helpers.js');
|
|
352
|
+
require.cache[moduleApiPath] = {
|
|
353
|
+
id: moduleApiPath,
|
|
354
|
+
filename: moduleApiPath,
|
|
355
|
+
loaded: true,
|
|
356
|
+
exports: {
|
|
357
|
+
pool,
|
|
358
|
+
parsePagination: helpers.parsePagination,
|
|
359
|
+
validateOrRespond: helpers.validateOrRespond,
|
|
360
|
+
corsPublicGet: () => (_req, _res, next) => next(),
|
|
361
|
+
logger: () => ({ error: () => {}, info: () => {}, warn: () => {} }),
|
|
362
|
+
requireBuilder: (req, res, next) => {
|
|
363
|
+
if (!viewer) return res.fail('unauthenticated', 401);
|
|
364
|
+
req.builder = { github_id: viewer.github_id };
|
|
365
|
+
next();
|
|
366
|
+
},
|
|
367
|
+
requirePermission: () => (req, res, next) => (viewer && viewer.curate ? next() : res.status(403).json({ ok: false, error: 'forbidden' })),
|
|
368
|
+
requireRank: (...allowed) => (req, res, next) => (viewer && allowed.includes(viewer.rank) ? next() : res.fail('rank_forbidden', 403)),
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// db-kernel reaches Postgres through ./pool: the access_requests table behind the
|
|
373
|
+
// sign-in gate's invited-check (section 5). Stubbed before anything loads it.
|
|
374
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
375
|
+
const NOW = Date.now();
|
|
376
|
+
const ACCESS_REQUESTS = [
|
|
377
|
+
{ github_login: 'fresh-invitee', status: 'invited', kind: 'invite', created_at: NOW - 29 * DAY_MS },
|
|
378
|
+
{ github_login: 'stale-invitee', status: 'invited', kind: 'invite', created_at: NOW - 31 * DAY_MS },
|
|
379
|
+
{ github_login: 'approved-applicant', status: 'invited', kind: 'application', created_at: NOW - 400 * DAY_MS },
|
|
380
|
+
{ github_login: 'waiting-applicant', status: 'pending', kind: 'application', created_at: NOW - 1 * DAY_MS },
|
|
381
|
+
];
|
|
382
|
+
const admissionPool = {
|
|
383
|
+
query: async (text, params = []) => {
|
|
384
|
+
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
385
|
+
if (!/^SELECT 1 FROM access_requests WHERE (.+) LIMIT 1$/.test(sql)) throw new Error(`admission pool: unsupported statement — ${sql.slice(0, 140)}`);
|
|
386
|
+
let residue = sql.replace(/^SELECT 1 FROM access_requests WHERE /, '').replace(/ LIMIT 1$/, '');
|
|
387
|
+
const tests = [];
|
|
388
|
+
const take = (re, make) => {
|
|
389
|
+
const m = re.exec(residue);
|
|
390
|
+
if (!m) throw new Error(`admission pool: expected ${re} in "${residue}"`);
|
|
391
|
+
residue = residue.slice(0, m.index) + residue.slice(m.index + m[0].length);
|
|
392
|
+
tests.push(make(m));
|
|
393
|
+
};
|
|
394
|
+
take(/lower\(github_login\) = lower\(\$1\)/, () => (r) => r.github_login.toLowerCase() === String(params[0]).toLowerCase());
|
|
395
|
+
take(/status = 'invited'/, () => (r) => r.status === 'invited');
|
|
396
|
+
// the expiry unit, whole — the window is read from the statement, not assumed
|
|
397
|
+
take(/NOT \(kind = 'invite' AND created_at <= now\(\) - interval '(\d+) days'\)/, (m) => (r) => !(r.kind === 'invite' && r.created_at <= NOW - Number(m[1]) * DAY_MS));
|
|
398
|
+
if (!/^[\s()]*(?:AND[\s()]*)*$/.test(residue)) throw new Error(`admission pool: unmodelled structure "${residue.trim()}"`);
|
|
399
|
+
const rows = ACCESS_REQUESTS.filter((r) => tests.every((fn) => fn(r))).map(() => ({ '?column?': 1 }));
|
|
400
|
+
return { rows, rowCount: rows.length };
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
404
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: admissionPool } };
|
|
405
|
+
|
|
406
|
+
const { SLIVER_KEYS, SLIVER_KEYS_HIDDEN } = require('../modules/platform-identity/recruiter-sliver.js');
|
|
407
|
+
const { CANONICAL_PROFILE_PAGE_LIVE } = require('../modules/platform-identity/canonical-profile.js');
|
|
408
|
+
const { clearTotalsMemo } = require('../modules/platform-identity/community-leaderboard.js');
|
|
409
|
+
const { INVITE_EXPIRY_DAYS } = require('../src/bongos/invite-expiry.js');
|
|
410
|
+
const dbKernel = require('../src/bongos/db-kernel.js');
|
|
411
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
412
|
+
|
|
413
|
+
const app = express();
|
|
414
|
+
app.use(express.json());
|
|
415
|
+
app.use(attachFail);
|
|
416
|
+
for (const file of ['public-profile', 'profile', 'community-search', 'community-leaderboard', 'scouting']) {
|
|
417
|
+
app.use('/api/bongos', require(`../modules/platform-identity/routes/${file}.js`)());
|
|
418
|
+
}
|
|
419
|
+
const server = app.listen(0, '127.0.0.1');
|
|
420
|
+
await new Promise((r) => server.once('listening', r));
|
|
421
|
+
const port = server.address().port;
|
|
422
|
+
after(() => server.close());
|
|
423
|
+
|
|
424
|
+
// Every response the matrix walk sees, so the cross-cutting claims (section 3)
|
|
425
|
+
// can be checked against ALL of them, not only the ones a cell looked at.
|
|
426
|
+
const SEEN = [];
|
|
427
|
+
function get(who, path_) {
|
|
428
|
+
viewer = who;
|
|
429
|
+
// a signed-in viewer's browser sends its credentials on a public read too
|
|
430
|
+
const headers = who ? { Authorization: `Bearer viewer-${who.github_id}`, Cookie: `bongos_session=viewer-${who.github_id}` } : {};
|
|
431
|
+
return new Promise((resolve, reject) => {
|
|
432
|
+
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: path_, headers }, (res) => {
|
|
433
|
+
const chunks = [];
|
|
434
|
+
res.on('data', (c) => chunks.push(c));
|
|
435
|
+
res.on('end', () => {
|
|
436
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
437
|
+
let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
|
|
438
|
+
const out = { who: who ? who.name : 'Anon', whoId: who ? who.github_id : null, path: path_, status: res.statusCode, text, json };
|
|
439
|
+
SEEN.push(out);
|
|
440
|
+
resolve(out);
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
req.on('error', reject);
|
|
444
|
+
req.end();
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ===========================================================================
|
|
449
|
+
// 0. The instrument is calibrated before it is trusted
|
|
450
|
+
// ===========================================================================
|
|
451
|
+
|
|
452
|
+
test('the interpreter refuses what it cannot read, and reads each real predicate as the module’s own JS twin does', () => {
|
|
453
|
+
assert.throws(() => accountFilter('github_id = $1 AND tier = $2', ['1', 'x']), /unread predicate on \$2/);
|
|
454
|
+
assert.throws(() => accountFilter("github_id = $1 AND tier = 'gold'", ['1']), /unrecognised literal comparison on tier/);
|
|
455
|
+
assert.throws(() => accountFilter(accountVisibleSql('pa').replace(' AND pa.account_visibility', ' OR pa.account_visibility'), []), /unmodelled boolean structure/,
|
|
456
|
+
'an AND turned OR must be refused, never read as the conjunction it used to be');
|
|
457
|
+
for (const [sql, twin, name] of [
|
|
458
|
+
[accountVisibleSql('pa'), isAccountVisible, 'existence'],
|
|
459
|
+
[rollupVisibleSql('pa2'), isRollupVisible, 'rollup'],
|
|
460
|
+
[recruiterSliverVisibleSql(), isRecruiterSliverVisible, 'sliver'],
|
|
461
|
+
]) {
|
|
462
|
+
const f = accountFilter(sql, []);
|
|
463
|
+
for (const a of ACCOUNTS) assert.equal(f(a), twin(a), `${name} predicate, ${a.key}: the interpreter and the module disagree`);
|
|
464
|
+
}
|
|
465
|
+
// the two directory tiers have no twin; pin them to the D2 cells directly
|
|
466
|
+
assert.deepEqual(ACCOUNTS.filter(accountFilter(scoutingListableSql('pa'), [])).map((a) => a.key), ['PUB']);
|
|
467
|
+
assert.deepEqual(ACCOUNTS.filter(accountFilter(scoutingNameOnlySql('pa'), [])).map((a) => a.key), ['SHY']);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// ===========================================================================
|
|
471
|
+
// 1. The cells, row by row
|
|
472
|
+
// ===========================================================================
|
|
473
|
+
|
|
474
|
+
clearTotalsMemo();
|
|
475
|
+
const PUBLIC_VIEWERS = (k) => [ANON, STRANGER, RECRUITER, selfOf(k)];
|
|
476
|
+
const NOBODY = 'nobody-mx'; // a handle no account has ever held
|
|
477
|
+
|
|
478
|
+
for (const k of KEYS) {
|
|
479
|
+
const a = ACCT[k];
|
|
480
|
+
const cell = MATRIX[k];
|
|
481
|
+
|
|
482
|
+
test(`${STATE[k].label} — the public profile read: ${cell.profile}, the same bytes to Anon, Builder, Recruiter and Self`, async () => {
|
|
483
|
+
const seen = [];
|
|
484
|
+
for (const who of PUBLIC_VIEWERS(k)) seen.push(await get(who, `/api/bongos/profiles/${a.handle}`));
|
|
485
|
+
for (const r of seen.slice(1)) {
|
|
486
|
+
assert.equal(r.status, seen[0].status, `${r.who} got a different status — the public read must not vary by viewer (D2)`);
|
|
487
|
+
assert.equal(r.text, seen[0].text, `${r.who} got a different body — the public read must not vary by viewer (D2)`);
|
|
488
|
+
}
|
|
489
|
+
const [r] = seen;
|
|
490
|
+
if (cell.profile === 404) {
|
|
491
|
+
assert.equal(r.status, 404, 'a 404, never a 403: a refusal would confirm the account exists');
|
|
492
|
+
const none = await get(ANON, `/api/bongos/profiles/${NOBODY}`);
|
|
493
|
+
assert.equal(r.text.replaceAll(a.handle, 'H'), none.text.replaceAll(NOBODY, 'H'), 'byte-identical to a handle nobody holds: no existence oracle');
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
assert.equal(r.status, 200);
|
|
497
|
+
assert.equal(r.json.account.handle, a.handle);
|
|
498
|
+
if (cell.profile === 'full') {
|
|
499
|
+
assert.notEqual(r.json.cross_project, null);
|
|
500
|
+
assert.ok(r.text.includes(String(ROLLUP[a.github_id].credits)), 'the record renders');
|
|
501
|
+
} else {
|
|
502
|
+
assert.equal(r.json.cross_project, null, 'identity renders, the record does not');
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test(`${STATE[k].label} — Self, through the own-scope door: ${cell.self}`, async () => {
|
|
507
|
+
if (cell.self === 'n/a') {
|
|
508
|
+
// D2: a provisional account has never had a hub session, so there is no
|
|
509
|
+
// Self to ask. Nothing to call; the cell is stated rather than invented.
|
|
510
|
+
assert.equal(a.profile_state, 'provisional');
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const r = await get(selfOf(k), '/api/bongos/me/cross-project');
|
|
514
|
+
assert.equal(r.status, 200);
|
|
515
|
+
assert.equal(r.json.handle, a.handle);
|
|
516
|
+
assert.equal(r.json.account_visibility, a.account_visibility, 'the page gets what it needs to say "your page is private"');
|
|
517
|
+
assert.equal(r.json.hide_stats, a.hide_stats);
|
|
518
|
+
assert.notEqual(r.json.cross_project, null, 'the owner always sees their own record, whatever the flags (ADR 0171 D4 side door)');
|
|
519
|
+
assert.ok(r.text.includes(String(ROLLUP[a.github_id].credits)));
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test(`${STATE[k].label} — the recruiter sliver: ${cell.sliver}`, async () => {
|
|
523
|
+
const r = await get(RECRUITER, `/api/bongos/scouting/${a.handle}`);
|
|
524
|
+
if (cell.sliver === 404) {
|
|
525
|
+
assert.equal(r.status, 404);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
assert.equal(r.status, 200);
|
|
529
|
+
const want = cell.sliver === 'full' ? SLIVER_KEYS : SLIVER_KEYS_HIDDEN;
|
|
530
|
+
assert.deepEqual(Object.keys(r.json.sliver).sort(), [...want].sort(), 'exactly D3’s keys — the module’s own list, never a copy');
|
|
531
|
+
assert.equal(r.json.sliver.handle, a.handle);
|
|
532
|
+
for (const s of [a.github_login, a.bio, String(ROLLUP[a.github_id].credits), ROLLUP[a.github_id].last_active, ROLLUP[a.github_id].projects[0].name]) {
|
|
533
|
+
assert.ok(!r.text.includes(s), `the sliver carries "${s}" — D3 grants handle, name, avatar, disciplines and a boolean, nothing else`);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
test('the sliver’s key lists are D3’s, transcribed: handle, name, avatar, disciplines, a recent-activity boolean — or the first three', () => {
|
|
539
|
+
// The cells above compare a response to the MODULE's lists; this pins those
|
|
540
|
+
// lists to the spec, so a sixth key added to the module cannot pass with them.
|
|
541
|
+
assert.deepEqual([...SLIVER_KEYS], ['handle', 'display_name', 'avatar_url', 'disciplines', 'active_recently']);
|
|
542
|
+
assert.deepEqual([...SLIVER_KEYS_HIDDEN], ['handle', 'display_name', 'avatar_url'], 'hide_stats drops the two rollup-derived keys');
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('search — "findable by name": listed, name-only (no disciplines) or absent, the same for every viewer', async () => {
|
|
546
|
+
const seen = [];
|
|
547
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/search?q=-mx&limit=50'));
|
|
548
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got a different search body`);
|
|
549
|
+
const byHandle = new Map(seen[0].json.results.map((x) => [x.handle, x]));
|
|
550
|
+
for (const k of KEYS) {
|
|
551
|
+
const hit = byHandle.get(ACCT[k].handle);
|
|
552
|
+
if (MATRIX[k].search === 'absent') { assert.equal(hit, undefined, `${k} must not be found`); continue; }
|
|
553
|
+
assert.ok(hit, `${k} must be findable by name`);
|
|
554
|
+
assert.deepEqual(hit.disciplines, MATRIX[k].search === 'listed' ? ROLLUP[ACCT[k].github_id].disciplines : null);
|
|
555
|
+
}
|
|
556
|
+
// a stats-derived filter drops the hidden account; the control finds a shown one
|
|
557
|
+
const shy = await get(ANON, `/api/bongos/community/search?q=-mx&discipline=${ROLLUP[ACCT.SHY.github_id].disciplines[0]}`);
|
|
558
|
+
assert.deepEqual(shy.json.results, [], 'D2: a hidden account is absent from any stats-derived filter');
|
|
559
|
+
const pub = await get(ANON, `/api/bongos/community/search?q=-mx&discipline=${ROLLUP[ACCT.PUB.github_id].disciplines[0]}`);
|
|
560
|
+
assert.deepEqual(pub.json.results.map((x) => x.handle), [ACCT.PUB.handle]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('the cross-project leaderboard — only shown, public, active accounts, the same for every viewer', async () => {
|
|
564
|
+
const seen = [];
|
|
565
|
+
for (const who of [ANON, STRANGER, RECRUITER]) seen.push(await get(who, '/api/bongos/community/leaderboard?limit=50'));
|
|
566
|
+
for (const r of seen.slice(1)) assert.equal(r.text, seen[0].text, `${r.who} got a different leaderboard body`);
|
|
567
|
+
const listed = new Set(seen[0].json.results.map((x) => x.handle));
|
|
568
|
+
for (const k of KEYS) {
|
|
569
|
+
assert.equal(listed.has(ACCT[k].handle), MATRIX[k].board === 'listed', `${k}: expected ${MATRIX[k].board}`);
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
test('the recruiting directory — the recruiter sees the full and name-only tiers; Anon and Builder are refused', async () => {
|
|
574
|
+
const r = await get(RECRUITER, '/api/bongos/scouting');
|
|
575
|
+
assert.equal(r.status, 200);
|
|
576
|
+
const full = new Set(r.json.builders.map((b) => b.github_login));
|
|
577
|
+
const nameOnly = new Set(r.json.name_only.map((b) => b.github_login));
|
|
578
|
+
for (const k of KEYS) {
|
|
579
|
+
const login = ACCT[k].github_login;
|
|
580
|
+
const got = full.has(login) ? 'full' : nameOnly.has(login) ? 'name-only' : 'absent';
|
|
581
|
+
assert.equal(got, MATRIX[k].directory, `${k} in the directory`);
|
|
582
|
+
}
|
|
583
|
+
for (const b of r.json.name_only) {
|
|
584
|
+
assert.deepEqual(Object.keys(b).sort(), ['avatar_url', 'display_name', 'github_login'], 'the name-only tier carries a name and nothing else');
|
|
585
|
+
}
|
|
586
|
+
assert.equal((await get(ANON, '/api/bongos/scouting')).status, 401);
|
|
587
|
+
assert.equal((await get(STRANGER, '/api/bongos/scouting')).status, 403);
|
|
588
|
+
assert.equal((await get(ANON, `/api/bongos/scouting/${ACCT.PRIVD.handle}`)).status, 401);
|
|
589
|
+
assert.equal((await get(STRANGER, `/api/bongos/scouting/${ACCT.PRIVD.handle}`)).status, 403, 'the sliver is the recruiter’s surface, not a signed-in builder’s');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// ===========================================================================
|
|
593
|
+
// 3. The rules the matrix encodes, checked against EVERY response seen above
|
|
594
|
+
// ===========================================================================
|
|
595
|
+
|
|
596
|
+
const isOwnRead = (r, a) => r.whoId === a.github_id && r.path === '/api/bongos/me/cross-project';
|
|
597
|
+
// What a response DISCLOSES: its body minus the fields that only echo the
|
|
598
|
+
// caller's own request back (search's `query` and `discipline`). A discipline
|
|
599
|
+
// the caller typed into the URL is not the server revealing it; the results
|
|
600
|
+
// beside it still are, and stay in.
|
|
601
|
+
const ECHO = { '/api/bongos/community/search': ['query', 'discipline'] };
|
|
602
|
+
function disclosed(r) {
|
|
603
|
+
const echo = ECHO[r.path.split('?')[0]];
|
|
604
|
+
if (!echo || !r.json) return r.text;
|
|
605
|
+
const rest = { ...r.json };
|
|
606
|
+
for (const key of echo) delete rest[key];
|
|
607
|
+
return JSON.stringify(rest);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
test('the walk covered every surface for every row (so the claims below are about something)', () => {
|
|
611
|
+
for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/scouting']) {
|
|
612
|
+
assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
|
|
613
|
+
}
|
|
614
|
+
assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test('hide_stats beats everything: no hidden account’s numbers reach anyone but the account itself', () => {
|
|
618
|
+
for (const k of KEYS.filter((x) => ACCT[x].hide_stats)) {
|
|
619
|
+
const a = ACCT[k];
|
|
620
|
+
for (const r of SEEN.filter((x) => !isOwnRead(x, a))) {
|
|
621
|
+
for (const s of rollupSentinels(a)) {
|
|
622
|
+
assert.ok(!disclosed(r).includes(s), `${k}'s "${s}" reached ${r.who} through GET ${r.path}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// control: the numbers exist and DO come back through the side door
|
|
626
|
+
const own = SEEN.find((x) => isOwnRead(x, a));
|
|
627
|
+
assert.ok(own && own.text.includes(String(ROLLUP[a.github_id].credits)), `${k}'s own read must carry its record, or this sweep proves nothing`);
|
|
628
|
+
}
|
|
629
|
+
// control: a shown account's numbers are on the public surfaces
|
|
630
|
+
assert.ok(SEEN.some((r) => r.who === 'Anon' && r.text.includes(String(ROLLUP[ACCT.PUB.github_id].credits))), 'a shown record must reach Anon, or absence above is vacuous');
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('provisional and private (not discoverable) are absent from every cross-project surface, for every viewer but Self', () => {
|
|
634
|
+
for (const k of ['PROV', 'PRIV']) {
|
|
635
|
+
const a = ACCT[k];
|
|
636
|
+
for (const r of SEEN.filter((x) => !isOwnRead(x, a) && !x.path.endsWith(`/profiles/${a.handle}`) && !x.path.endsWith(`/scouting/${a.handle}`))) {
|
|
637
|
+
for (const s of identitySentinels(a)) assert.ok(!disclosed(r).includes(s), `${k}'s "${s}" reached ${r.who} through GET ${r.path}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('private · discoverable reaches the recruiter’s sliver and nowhere else', () => {
|
|
643
|
+
for (const k of ['PRIVD', 'PRIVDH']) {
|
|
644
|
+
const a = ACCT[k];
|
|
645
|
+
const leaks = SEEN.filter((r) => !isOwnRead(r, a) && identitySentinels(a).some((s) => disclosed(r).includes(s)));
|
|
646
|
+
assert.deepEqual(leaks.map((r) => `${r.who} ${r.path}`), [`Recruiter /api/bongos/scouting/${a.handle}`],
|
|
647
|
+
`${k} may be named only by the sliver, to the recruiter (D3)`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ===========================================================================
|
|
652
|
+
// 4. Cells with no surface today — named, with a tripwire
|
|
653
|
+
// ===========================================================================
|
|
654
|
+
|
|
655
|
+
test('the cells with no surface today are stated, and the /u/<handle> page cannot land unnoticed', () => {
|
|
656
|
+
// When this flips, the page exists: add its column to MATRIX above (404 for a
|
|
657
|
+
// private account to everyone, rendered to its owner from the own-scope door).
|
|
658
|
+
assert.equal(CANONICAL_PROFILE_PAGE_LIVE, false, 'the /u/<handle> page is live — give it its cells in this matrix');
|
|
659
|
+
for (const line of [
|
|
660
|
+
'N/A not-yet-built: the /u/<handle> page (canonical-profile.js keeps it switched off)',
|
|
661
|
+
'N/A not-yet-built: the sitemap, and an Explore people surface',
|
|
662
|
+
'N/A by design: the Ops column (internal only, never a response)',
|
|
663
|
+
'N/A moved: a declined invite closes the project’s door — task 1004444 adds that row with the relay',
|
|
664
|
+
]) console.log(` ${line}`);
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
// ===========================================================================
|
|
668
|
+
// 5. An expired invite refuses sign-in (D4)
|
|
669
|
+
// ===========================================================================
|
|
670
|
+
|
|
671
|
+
test('an expired invite refuses sign-in through the real admission read; a live one and an approved application do not', async () => {
|
|
672
|
+
assert.equal(INVITE_EXPIRY_DAYS, 30, 'the owner-signed window (spec D4)');
|
|
673
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('stale-invitee'), false, 'day 31: the door has closed');
|
|
674
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('STALE-INVITEE'), false, 'and no casing reopens it');
|
|
675
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('fresh-invitee'), true, 'control: day 29 still admits');
|
|
676
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('Fresh-Invitee'), true);
|
|
677
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('approved-applicant'), true, 'only an INVITE expires; an approved application never does');
|
|
678
|
+
assert.equal(await dbKernel.hasInvitedAccessRequest('waiting-applicant'), false, 'a pending request was never a door');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// ===========================================================================
|
|
682
|
+
// 6. This file runs in the lane that executes
|
|
683
|
+
// ===========================================================================
|
|
684
|
+
|
|
685
|
+
test('this proof runs in the DB-free unit lane', () => {
|
|
686
|
+
const runner = fs.readFileSync(path.join(ROOT, 'scripts/gds/run-unit-tests.js'), 'utf8');
|
|
687
|
+
assert.ok(!/privacy_matrix/.test(runner),
|
|
688
|
+
'named in run-unit-tests.js it would be parked into INTEGRATION or KNOWN_FAILING and gate nothing (learning 1000218)');
|
|
689
|
+
});
|