@bongos/core 1.19.430 → 1.19.432
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 +64 -44
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0215-the-recruiter-sliver-is-a-ceiling-not-a-step.md +116 -0
- package/docs/adr/0216-a-fake-pool-interpreter-must-fail-loudly-not-silently-ignore.md +102 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +64 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +48 -48
- package/docs/copy-registry.json +57 -57
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/settings.js +17 -8
- package/modules/platform-identity/account-visibility.js +47 -0
- package/modules/platform-identity/platform-identity.js +4 -4
- package/modules/platform-identity/recruiter-sliver.js +197 -0
- package/modules/platform-identity/routes/scouting.js +58 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/account_privacy_flags.mjs +115 -9
- package/tests/application_echo.mjs +43 -9
- package/tests/application_lifecycle.mjs +18 -7
- package/tests/connections_ownership.mjs +44 -4
- package/tests/effective_visibility_predicate.mjs +137 -50
- package/tests/government_require_permission.mjs +22 -0
- package/tests/handle_machinery.mjs +15 -1
- package/tests/hub_profile_leak.mjs +18 -4
- package/tests/own_session_revoke.mjs +77 -9
- package/tests/profile_connections_ui.mjs +49 -10
- package/tests/profile_privacy_owner_view.mjs +68 -37
- package/tests/profile_rollup_consent.mjs +46 -14
- package/tests/profile_route.mjs +6 -0
- package/tests/recruiter_sliver_boundary.mjs +841 -0
- package/tests/scouting_owner_access_and_reach.mjs +175 -39
- package/tests/scouting_rollup_fanout.mjs +15 -4
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// tests/recruiter_sliver_boundary.mjs — THE PRE-ACCEPT BOUNDARY PROOF
|
|
2
|
+
// (task 1002968, goal 1000045; privacy spec D3, ADR 0215, ADR 0171 D2/D4/D5,
|
|
3
|
+
// ADR 0016).
|
|
4
|
+
//
|
|
5
|
+
// The sliver is a CONSENT BOUNDARY, so the thing worth proving is not that it
|
|
6
|
+
// works — it is that it stops. Four claims, in the order that would hurt most:
|
|
7
|
+
//
|
|
8
|
+
// 1. IT CARRIES EXACTLY FIVE KEYS, AND NOTHING ELSE. Asserted as a WHITELIST:
|
|
9
|
+
// the response's own key set is compared to the module's exported
|
|
10
|
+
// SLIVER_KEYS, so a sixth key fails whether or not this file was told about
|
|
11
|
+
// it. A blacklist ("no credits key") passes forever against the field
|
|
12
|
+
// nobody thought to name.
|
|
13
|
+
//
|
|
14
|
+
// 2. NOTHING RIDES ALONG IN THE TEXT. Every value the sliver must not carry is
|
|
15
|
+
// an IMPROBABLE PLANTED SENTINEL, and the assertion is that none appears
|
|
16
|
+
// ANYWHERE in the raw response body — the task 1002806 technique. A regex
|
|
17
|
+
// like /"credits":null/ is satisfied by a stray sibling key and proves
|
|
18
|
+
// nothing; absence from the whole body is the claim.
|
|
19
|
+
//
|
|
20
|
+
// THE SENTINELS SIT ON THE ROWS THE SLIVER'S OWN QUERIES RETURN, and that
|
|
21
|
+
// is load-bearing rather than incidental. In the first draft the numbers
|
|
22
|
+
// lived on rows those queries never touched, so "credits never leak" — the
|
|
23
|
+
// first thing spec D3 forbids — could not have failed under any projection
|
|
24
|
+
// change. The fake now returns MORE columns than each statement selects,
|
|
25
|
+
// exactly as a real row would if a query grew, so a row spread anywhere in
|
|
26
|
+
// the projection leaks a number and reds this file.
|
|
27
|
+
//
|
|
28
|
+
// CONTROLS, so absence cannot pass vacuously: the same values DO reach a
|
|
29
|
+
// recruiter through the directory for a builder who consented to it, and
|
|
30
|
+
// the SAME hidden account, with hide_stats flipped, DOES get its
|
|
31
|
+
// disciplines from the real port. (The control that used to stand here ran
|
|
32
|
+
// SQL this file wrote against this file's own fake and compared a fixture
|
|
33
|
+
// to itself — it proved that an object equals itself.)
|
|
34
|
+
//
|
|
35
|
+
// ONE HONEST EXCEPTION, asserted rather than glossed: `github_id` is NOT
|
|
36
|
+
// absent from the body. The avatar spec D3 grants is GitHub's own URL and
|
|
37
|
+
// embeds the id verbatim, so the proof pins where it may appear — inside
|
|
38
|
+
// that URL and nowhere else — instead of claiming a property the code does
|
|
39
|
+
// not have.
|
|
40
|
+
//
|
|
41
|
+
// 3. THE BOUNDARY IS A CEILING, NOT A STEP. A recruiter who holds a private
|
|
42
|
+
// builder's sliver holds their HANDLE — and the handle does not open the
|
|
43
|
+
// profile page: GET /profiles/:handle joins accountVisibleSql and answers
|
|
44
|
+
// 404 for exactly that account, while answering 200 for a public one. That
|
|
45
|
+
// is what "pre-accept" means operationally, and it is the assertion that
|
|
46
|
+
// would catch someone "helpfully" relaxing resolveHandle later.
|
|
47
|
+
//
|
|
48
|
+
// 4. IT GRANTS NOTHING (ADR 0016). Structurally: the sliver module is required
|
|
49
|
+
// by exactly one first-party server file, the route that serializes it —
|
|
50
|
+
// nothing consults it to decide anything. And the response carries no rank,
|
|
51
|
+
// role, permission or membership under any name.
|
|
52
|
+
//
|
|
53
|
+
// PLUS the gate, because a bounded projection behind an open door is not
|
|
54
|
+
// bounded. The sliver is ARCHON-gated and the DIRECTORY's audience is not the
|
|
55
|
+
// sliver's (ADR 0215 D4): spec D3 pins this read to an archon-floor atom that has
|
|
56
|
+
// not been minted, and the directory's door — `project.curate` at metic OR any
|
|
57
|
+
// project owner — is wider on both halves. A metic operator and a project owner
|
|
58
|
+
// both read the directory and are both refused here, which is asserted in both
|
|
59
|
+
// directions so a later "tidy-up" onto one shared gate cannot pass quietly.
|
|
60
|
+
//
|
|
61
|
+
// MECHANISM (tests/profile_privacy_owner_view.mjs + tests/connections_ownership.mjs):
|
|
62
|
+
// in-memory tables whose row filter is built FROM EACH STATEMENT'S OWN WHERE
|
|
63
|
+
// CLAUSE, refusing any statement whose placeholders or literal comparisons it did
|
|
64
|
+
// not consume — so a predicate this interpreter cannot read is a loud error and
|
|
65
|
+
// never a silently absent filter. Above the pool everything is real: the real
|
|
66
|
+
// router, the real module, real HTTP.
|
|
67
|
+
//
|
|
68
|
+
// AND IT READS BOOLEAN STRUCTURE, NOT ONLY TERMS. The house version of this
|
|
69
|
+
// interpreter collects the terms it recognises and ANDs them, which re-imposes a
|
|
70
|
+
// conjunction the SQL may no longer contain: rewriting the sliver predicate's
|
|
71
|
+
// `AND` to `OR` would serve every active account on the platform, and the first
|
|
72
|
+
// draft of this file stayed green through exactly that. Each term is now CONSUMED
|
|
73
|
+
// from the WHERE text as it is matched and the residue must be pure conjunction.
|
|
74
|
+
// (The same hole is still open in five sibling proofs — idea 1000877.)
|
|
75
|
+
//
|
|
76
|
+
// DB-free by construction. The last test proves this file runs in the lane that
|
|
77
|
+
// actually executes (learning 1000218: three files declared INTEGRATION in their
|
|
78
|
+
// headers, sat in NEITHER lane, and asserted nothing for months).
|
|
79
|
+
//
|
|
80
|
+
// Run: node tests/recruiter_sliver_boundary.mjs
|
|
81
|
+
|
|
82
|
+
import { strict as assert } from 'node:assert';
|
|
83
|
+
import { test, after } from 'node:test';
|
|
84
|
+
import { createRequire } from 'node:module';
|
|
85
|
+
import http from 'node:http';
|
|
86
|
+
import fs from 'node:fs';
|
|
87
|
+
import path from 'node:path';
|
|
88
|
+
import { fileURLToPath } from 'node:url';
|
|
89
|
+
|
|
90
|
+
// ESM so knip credits the module's test-only exports (a createRequire import
|
|
91
|
+
// reads as unused — the CJS blind spot).
|
|
92
|
+
import {
|
|
93
|
+
recruiterSliverVisibleSql,
|
|
94
|
+
isRecruiterSliverVisible,
|
|
95
|
+
accountActiveSql,
|
|
96
|
+
recruiterReachSql,
|
|
97
|
+
accountVisibleSql,
|
|
98
|
+
} from '../modules/platform-identity/account-visibility.js';
|
|
99
|
+
|
|
100
|
+
// The sliver module's own exports, imported as ESM for the same reason — and
|
|
101
|
+
// SLIVER_KEYS is why: the whitelist assertions compare a response's key set to
|
|
102
|
+
// the module's list, never to a copy of it kept here. A copy is the failure mode
|
|
103
|
+
// a whitelist exists to remove (add a key to both and the proof still passes).
|
|
104
|
+
// Loading it here also loads the real doorway; that is harmless because every
|
|
105
|
+
// call below passes its pool explicitly, and the require.cache stub installed
|
|
106
|
+
// further down still reaches the ROUTE files, which are required after it.
|
|
107
|
+
import {
|
|
108
|
+
SLIVER_KEYS,
|
|
109
|
+
SLIVER_KEYS_HIDDEN,
|
|
110
|
+
ACTIVE_WINDOW_DAYS,
|
|
111
|
+
getRecruiterSliver,
|
|
112
|
+
getRecruiterSliverByHandle,
|
|
113
|
+
} from '../modules/platform-identity/recruiter-sliver.js';
|
|
114
|
+
|
|
115
|
+
const require = createRequire(import.meta.url);
|
|
116
|
+
const express = require('express');
|
|
117
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
118
|
+
|
|
119
|
+
// ===========================================================================
|
|
120
|
+
// The fixture — one account per row of the privacy-spec D2 matrix
|
|
121
|
+
// ===========================================================================
|
|
122
|
+
//
|
|
123
|
+
// Sentinels are per-account improbable literals, so "absent from the body" means
|
|
124
|
+
// absent rather than "did not collide with a 0". `github_login` is deliberately
|
|
125
|
+
// NOT the handle: D3 names the handle and not the login, so the login is itself
|
|
126
|
+
// a sentinel that must never cross.
|
|
127
|
+
|
|
128
|
+
const DIR = '3110001'; // active · public · shown — the CONTROL: the directory carries them
|
|
129
|
+
const PRIV = '3110002'; // active · PRIVATE · reach=true — THE D3 SUBJECT
|
|
130
|
+
const OPTOUT = '3110003'; // active · public · reach=FALSE — opted out of recruiting
|
|
131
|
+
const SHY = '3110004'; // active · public · HIDDEN — the name-only sliver
|
|
132
|
+
const PROV = '3110005'; // PROVISIONAL · reach=true — never recruiter-visible
|
|
133
|
+
const PRIVDEF = '3110006'; // active · PRIVATE · reach=null — private, never opted in
|
|
134
|
+
|
|
135
|
+
// Every account column the tables hold. The queries select a SUBSET; the extra
|
|
136
|
+
// columns are here on purpose, so a row spread leaks something loud.
|
|
137
|
+
const ACCOUNTS = [
|
|
138
|
+
{ github_id: DIR, handle: 'dir-handle', github_login: 'dir-login-990101', display_name: 'Dir Person',
|
|
139
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${DIR}?v=4`, previous_handle: null, bio: 'dir-bio-990103',
|
|
140
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
141
|
+
hide_stats: false, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
142
|
+
{ github_id: PRIV, handle: 'priv-handle', github_login: 'priv-login-990201', display_name: 'Priv Person',
|
|
143
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PRIV}?v=4`, previous_handle: 'priv-oldname-990204', bio: 'priv-bio-990203',
|
|
144
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'private',
|
|
145
|
+
hide_stats: false, recruiter_discoverable: true, terms_accepted_at: 'T1' },
|
|
146
|
+
{ github_id: OPTOUT, handle: 'optout-handle', github_login: 'optout-login-990301', display_name: 'Optout Person',
|
|
147
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${OPTOUT}?v=4`, previous_handle: null, bio: null,
|
|
148
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
149
|
+
hide_stats: false, recruiter_discoverable: false, terms_accepted_at: 'T1' },
|
|
150
|
+
{ github_id: SHY, handle: 'shy-handle', github_login: 'shy-login-990401', display_name: 'Shy Person',
|
|
151
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${SHY}?v=4`, previous_handle: null, bio: null,
|
|
152
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
153
|
+
hide_stats: true, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
154
|
+
{ github_id: PROV, handle: 'prov-handle', github_login: 'prov-login-990501', display_name: 'Prov Person',
|
|
155
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PROV}?v=4`, previous_handle: null, bio: null,
|
|
156
|
+
links: null, created_at: 'T0', profile_state: 'provisional', account_visibility: 'public',
|
|
157
|
+
hide_stats: false, recruiter_discoverable: true, terms_accepted_at: 'T1' },
|
|
158
|
+
{ github_id: PRIVDEF, handle: 'privdef-handle', github_login: 'privdef-login-990601', display_name: 'Privdef Person',
|
|
159
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PRIVDEF}?v=4`, previous_handle: null, bio: null,
|
|
160
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'private',
|
|
161
|
+
hide_stats: false, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// The cross-project record behind each account. `disciplines` is the ONE
|
|
165
|
+
// rollup-derived value the sliver may carry, so it is not a sentinel for a shown
|
|
166
|
+
// account — but it IS one for the hidden account, whose sliver must be name-only.
|
|
167
|
+
const ROLLUP = {
|
|
168
|
+
[DIR]: { credits: 880001, works_shipped: 880002, karma: 880003, project_count: 880004,
|
|
169
|
+
last_active: 'dir-lastactive-880005', disciplines: ['dir-craft'],
|
|
170
|
+
projects: [{ name: 'Dir-Project-880006', origin: 'https://dir.example', rank_held: 'metic', recent_ships: ['Dir-Ship-880007'], is_public: true }] },
|
|
171
|
+
[PRIV]: { credits: 881001, works_shipped: 881002, karma: 881003, project_count: 881004,
|
|
172
|
+
last_active: 'priv-lastactive-881005', disciplines: ['priv-craft'],
|
|
173
|
+
projects: [{ name: 'Priv-Project-881006', origin: 'https://priv.example', rank_held: 'archon', recent_ships: ['Priv-Ship-881007'], is_public: true }] },
|
|
174
|
+
[OPTOUT]: { credits: 882001, works_shipped: 882002, karma: 882003, project_count: 882004,
|
|
175
|
+
last_active: 'optout-lastactive-882005', disciplines: ['optout-craft'],
|
|
176
|
+
projects: [{ name: 'Optout-Project-882006', origin: 'https://optout.example', rank_held: 'metic', recent_ships: ['Optout-Ship-882007'], is_public: true }] },
|
|
177
|
+
[SHY]: { credits: 883001, works_shipped: 883002, karma: 883003, project_count: 883004,
|
|
178
|
+
last_active: 'shy-lastactive-883005', disciplines: ['shy-craft-883008'],
|
|
179
|
+
projects: [{ name: 'Shy-Project-883006', origin: 'https://shy.example', rank_held: 'metic', recent_ships: ['Shy-Ship-883007'], is_public: true }] },
|
|
180
|
+
[PROV]: { credits: 884001, works_shipped: 884002, karma: 884003, project_count: 884004,
|
|
181
|
+
last_active: 'prov-lastactive-884005', disciplines: ['prov-craft'],
|
|
182
|
+
projects: [] },
|
|
183
|
+
[PRIVDEF]: { credits: 885001, works_shipped: 885002, karma: 885003, project_count: 885004,
|
|
184
|
+
last_active: 'privdef-lastactive-885005', disciplines: ['privdef-craft'],
|
|
185
|
+
projects: [] },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Whether each fixture is "active in the last 30 days" — the coarse boolean the
|
|
189
|
+
// SQL derives. Modelled here because the comparison happens in the database.
|
|
190
|
+
const ACTIVE_RECENTLY = { [DIR]: true, [PRIV]: true, [OPTOUT]: true, [SHY]: true, [PROV]: false, [PRIVDEF]: false };
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Everything about one account that the SLIVER must never disclose, as strings.
|
|
194
|
+
* The account's own `disciplines` are excluded for a shown account (they are
|
|
195
|
+
* granted) and added back by the caller for the hidden one.
|
|
196
|
+
*/
|
|
197
|
+
function forbiddenOf(githubId) {
|
|
198
|
+
const a = ACCOUNTS.find((x) => x.github_id === githubId);
|
|
199
|
+
const r = ROLLUP[githubId];
|
|
200
|
+
return [
|
|
201
|
+
// NOT githubId: the avatar D3 grants is GitHub's own URL and embeds the
|
|
202
|
+
// account id verbatim, so the id is DISCLOSED by an authorized field. That is
|
|
203
|
+
// asserted precisely below instead of pretended away here (ADR 0215).
|
|
204
|
+
a.github_login, // D3 names the handle, not the login
|
|
205
|
+
a.bio, // on the account row the sliver reads, and never granted
|
|
206
|
+
String(r.credits), String(r.works_shipped), String(r.karma), String(r.project_count),
|
|
207
|
+
r.last_active, // a timestamp is a trace; only the boolean crosses
|
|
208
|
+
...(a.previous_handle ? [a.previous_handle] : []), // a rename is itself a disclosure
|
|
209
|
+
...r.projects.flatMap((p) => [p.name, p.origin, p.rank_held, ...p.recent_ships]),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ===========================================================================
|
|
214
|
+
// The in-memory tables + their statement interpreter
|
|
215
|
+
// ===========================================================================
|
|
216
|
+
|
|
217
|
+
const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'handle', 'previous_handle', 'recruiter_discoverable']);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build a row test from a statement's own WHERE text. Literal comparisons carry
|
|
221
|
+
* as much weight as placeholders: the visibility predicates are made ENTIRELY of
|
|
222
|
+
* literal comparisons, so a fake modelling only $N filters would score every
|
|
223
|
+
* privacy term as absent and pass however the code changed.
|
|
224
|
+
*
|
|
225
|
+
* IT READS STRUCTURE, NOT JUST TERMS — and that is the whole reason this
|
|
226
|
+
* function is written as a consuming parse rather than a list of `.test()`
|
|
227
|
+
* calls. An interpreter that merely COLLECTS the terms it recognises and ANDs
|
|
228
|
+
* them re-imposes an `AND` the SQL may no longer contain: rewrite
|
|
229
|
+
* recruiterSliverVisibleSql's `AND` to `OR` and Postgres would serve the sliver
|
|
230
|
+
* for every active account on the platform, while a term-collecting fake keeps
|
|
231
|
+
* answering exactly as before and every assertion in this file stays green. So
|
|
232
|
+
* each recognised term is REMOVED from the text as it is matched, and whatever
|
|
233
|
+
* survives must be nothing but whitespace, parentheses and `AND`. An `OR`, a
|
|
234
|
+
* `NOT`, a `CASE` — anything this interpreter cannot faithfully model — is a
|
|
235
|
+
* loud error, on the same principle as the unread-placeholder guard below.
|
|
236
|
+
* (Found by adversarial review; the mutation is pinned in the calibration
|
|
237
|
+
* tests, and again on the real predicate.)
|
|
238
|
+
*/
|
|
239
|
+
function accountFilter(where, params) {
|
|
240
|
+
const tests = [];
|
|
241
|
+
const consumed = new Set();
|
|
242
|
+
let residue = where;
|
|
243
|
+
|
|
244
|
+
// Match a term, model it, and CONSUME its text so the leftovers can be judged.
|
|
245
|
+
const take = (re, make) => {
|
|
246
|
+
const m = re.exec(residue);
|
|
247
|
+
if (!m) return null;
|
|
248
|
+
residue = residue.slice(0, m.index) + residue.slice(m.index + m[0].length);
|
|
249
|
+
tests.push(make(m));
|
|
250
|
+
return m;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// The reach axis FIRST, because the COALESCE spells `account_visibility =
|
|
254
|
+
// 'public'` inside itself as its own fallback — consuming it first is what
|
|
255
|
+
// stops that fallback being scored as an existence check, which would filter
|
|
256
|
+
// out the private account the sliver exists to serve. The tri-state (null
|
|
257
|
+
// follows visibility, an explicit boolean overrides) is modelled exactly as
|
|
258
|
+
// recruiterReachSql spells it.
|
|
259
|
+
take(
|
|
260
|
+
/\bCOALESCE\(\s*(?:[a-z_]+\.)?recruiter_discoverable\s*,\s*(?:[a-z_]+\.)?account_visibility = 'public'\s*\)/,
|
|
261
|
+
() => (r) => (typeof r.recruiter_discoverable === 'boolean'
|
|
262
|
+
? r.recruiter_discoverable
|
|
263
|
+
: r.account_visibility === 'public'),
|
|
264
|
+
);
|
|
265
|
+
take(/\b(?:[a-z_]+\.)?profile_state = 'active'/, () => (r) => r.profile_state === 'active');
|
|
266
|
+
// The T&C half of the CONSENT floor (task 1002913, ADR 0213). accountActiveSql
|
|
267
|
+
// grew it while this branch was in flight and the sliver predicate composes
|
|
268
|
+
// that helper, so the sliver inherited the tightening without a line changing
|
|
269
|
+
// here — and this interpreter refused the statement outright until it could
|
|
270
|
+
// model the new term, which is the structure guard doing its job.
|
|
271
|
+
take(/\b(?:[a-z_]+\.)?terms_accepted_at IS NOT NULL/, () => (r) => r.terms_accepted_at != null);
|
|
272
|
+
take(/\b(?:[a-z_]+\.)?account_visibility = 'public'/, () => (r) => r.account_visibility === 'public');
|
|
273
|
+
take(/\bNOT (?:[a-z_]+\.)?hide_stats\b/, () => (r) => !r.hide_stats);
|
|
274
|
+
|
|
275
|
+
take(/\b(?:[a-z_]+\.)?github_id = \$(\d+)/, (m) => {
|
|
276
|
+
consumed.add(m[1]);
|
|
277
|
+
const v = params[Number(m[1]) - 1];
|
|
278
|
+
return (r) => String(r.github_id) === String(v);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// The handle disjunction is the ONE legitimate OR in this repo's account
|
|
282
|
+
// reads (resolveHandle's rename window), so it is consumed as a single unit
|
|
283
|
+
// — leaving a bare `OR` behind would trip the structure check below.
|
|
284
|
+
const bothHandles = take(
|
|
285
|
+
/lower\(handle\) = \$(\d+) OR lower\(previous_handle\) = \$(\d+)/,
|
|
286
|
+
(m) => {
|
|
287
|
+
consumed.add(m[1]); consumed.add(m[2]);
|
|
288
|
+
const v = String(params[Number(m[1]) - 1]);
|
|
289
|
+
return (r) => String(r.handle || '').toLowerCase() === v
|
|
290
|
+
|| String(r.previous_handle || '').toLowerCase() === v;
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
if (!bothHandles) {
|
|
294
|
+
take(/lower\(handle\) = \$(\d+)/, (m) => {
|
|
295
|
+
consumed.add(m[1]);
|
|
296
|
+
const v = String(params[Number(m[1]) - 1]);
|
|
297
|
+
return (r) => String(r.handle || '').toLowerCase() === v;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// The two TERM guards run before the STRUCTURE guard, so an unreadable term
|
|
302
|
+
// is reported as the unreadable term it is rather than as leftover text.
|
|
303
|
+
for (const m of where.matchAll(/\$(\d+)/g)) {
|
|
304
|
+
if (!consumed.has(m[1])) {
|
|
305
|
+
throw new Error(`fake pool: unread predicate on $${m[1]} in "${where}" — a filter this interpreter cannot read must never degrade to no filter`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
for (const m of where.matchAll(/\b([a-z_]+)\s*(?:=|<>|!=)\s*'/g)) {
|
|
309
|
+
if (!KNOWN_COLUMNS.has(m[1])) {
|
|
310
|
+
throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// What is left must be a pure conjunction. Anything else and this interpreter
|
|
314
|
+
// would be inventing a predicate the database was never given.
|
|
315
|
+
if (!/^[\s()]*(?:AND[\s()]*)*$/.test(residue)) {
|
|
316
|
+
throw new Error(`fake pool: unmodelled boolean structure "${residue.trim()}" in "${where}" — every recognised term is ANDed here, so a predicate that is not a pure conjunction must be a loud error, never a silently different filter`);
|
|
317
|
+
}
|
|
318
|
+
return (r) => tests.every((fn) => fn(r));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The github_id an activity statement is scoped to, read from its own WHERE. */
|
|
322
|
+
function activityScope(sql, params) {
|
|
323
|
+
const m = /\ba\.github_id = \$(\d+)/.exec(sql);
|
|
324
|
+
if (!m) throw new Error(`fake pool: an activity read with no github_id scope — ${sql.slice(0, 120)}`);
|
|
325
|
+
return String(params[Number(m[1]) - 1]);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let calls = [];
|
|
329
|
+
const pool = {
|
|
330
|
+
query: async (text, params = []) => {
|
|
331
|
+
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
332
|
+
calls.push({ sql, params });
|
|
333
|
+
|
|
334
|
+
// The recruiting DIRECTORY: consent lives in an EXISTS subquery correlated to
|
|
335
|
+
// the activity table, so its predicate is lifted out and judged on its own.
|
|
336
|
+
if (/WITH agg AS/.test(sql) && /platform_identity_accounts pa/.test(sql)) {
|
|
337
|
+
const m = /pa\.github_id = a\.github_id AND ([\s\S]*?)\)\s*GROUP BY/.exec(sql);
|
|
338
|
+
if (!m) throw new Error(`fake pool: could not read the directory's consent predicate — ${sql.slice(0, 200)}`);
|
|
339
|
+
const rows = ACCOUNTS.filter(accountFilter(m[1], [])).map((a) => {
|
|
340
|
+
const r = ROLLUP[a.github_id];
|
|
341
|
+
return {
|
|
342
|
+
github_id: a.github_id, github_login: a.github_login, display_name: a.display_name,
|
|
343
|
+
avatar_url: a.avatar_url, project_count: r.project_count, total_credits: r.credits,
|
|
344
|
+
total_tasks_shipped: r.works_shipped, total_karma: r.karma, last_active: r.last_active,
|
|
345
|
+
disciplines: r.disciplines,
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
return { rows, rowCount: rows.length };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The SLIVER's activity half: a boolean and a disciplines union, never a date.
|
|
352
|
+
if (/WITH act AS/.test(sql)) {
|
|
353
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
354
|
+
return {
|
|
355
|
+
rows: [{
|
|
356
|
+
active_recently: r ? ACTIVE_RECENTLY[activityScope(sql, params)] : null,
|
|
357
|
+
disciplines: r ? r.disciplines : null,
|
|
358
|
+
// NOT selected by the statement. Present because a real row grows
|
|
359
|
+
// columns, and a projection that spreads rather than whitelists would
|
|
360
|
+
// carry these straight into the response. Without them the sentinels
|
|
361
|
+
// for credits/karma/ships — the very fields D3 forbids — sit on rows
|
|
362
|
+
// the sliver's queries never touch, and "absent from the body" is
|
|
363
|
+
// unfalsifiable for exactly the values that matter most.
|
|
364
|
+
last_active: r ? r.last_active : null,
|
|
365
|
+
credits: r ? r.credits : null,
|
|
366
|
+
works_shipped: r ? r.works_shipped : null,
|
|
367
|
+
karma: r ? r.karma : null,
|
|
368
|
+
project_count: r ? r.project_count : null,
|
|
369
|
+
project_name: r ? (r.projects[0] || {}).name ?? null : null,
|
|
370
|
+
}],
|
|
371
|
+
rowCount: 1,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// The ownership door (scouting-authz): the REAL query, answered from the
|
|
376
|
+
// viewer's cast. Doubling the exported function instead would prove nothing —
|
|
377
|
+
// requireScoutingReader calls its module-local binding, so the export could be
|
|
378
|
+
// replaced with anything and the gate would never notice.
|
|
379
|
+
if (/platform_identity_project_memberships/.test(sql)) {
|
|
380
|
+
const [askedFor, kind, clientStatus] = params;
|
|
381
|
+
assert.equal(kind, 'owner', 'the ownership door asks only about owner memberships');
|
|
382
|
+
assert.equal(clientStatus, 'active', 'and only about projects still live on the platform (ADR 0210 D6)');
|
|
383
|
+
const owns = !!(viewer && viewer.owns && String(viewer.github_id) === String(askedFor));
|
|
384
|
+
return { rows: owns ? [{ membership_kind: 'owner', signin_verified_at: 'T0' }] : [], rowCount: owns ? 1 : 0 };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (/FROM platform_identity_accounts/.test(sql)) {
|
|
388
|
+
const where = sql.includes(' WHERE ')
|
|
389
|
+
? sql.slice(sql.indexOf(' WHERE ') + 7).replace(/ ORDER BY .*| LIMIT .*/g, '')
|
|
390
|
+
: '';
|
|
391
|
+
const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
|
|
392
|
+
// The rollup scalars ride the account row as well — the hidden account
|
|
393
|
+
// never reaches the activity query, so this is the only branch whose
|
|
394
|
+
// widening could leak a number for a name-only sliver.
|
|
395
|
+
let rows = ACCOUNTS.filter(accountFilter(where, params)).map((a) => ({ ...a, ...ROLLUP[a.github_id] }));
|
|
396
|
+
if (/is_current/.test(cols)) {
|
|
397
|
+
const asked = String(params[0]).toLowerCase();
|
|
398
|
+
rows = rows
|
|
399
|
+
.map((r) => ({ ...r, is_current: String(r.handle || '').toLowerCase() === asked }))
|
|
400
|
+
.sort((a, b) => Number(b.is_current) - Number(a.is_current))
|
|
401
|
+
.slice(0, 1);
|
|
402
|
+
}
|
|
403
|
+
return { rows: rows.map((r) => ({ ...r })), rowCount: rows.length };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// The per-builder rollup aggregate behind GET /profiles/:handle.
|
|
407
|
+
if (/WITH agg AS/.test(sql) && /platform_identity_builder_project_activity/.test(sql)) {
|
|
408
|
+
const r = ROLLUP[activityScope(sql, params)] || { credits: 0, works_shipped: 0, karma: 0, project_count: 0, last_active: null, disciplines: null };
|
|
409
|
+
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 };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (/platform_identity_builder_project_activity/.test(sql)) {
|
|
413
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
414
|
+
const rows = r ? r.projects.map((p) => ({ ...p })) : [];
|
|
415
|
+
return { rows, rowCount: rows.length };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 140)}`);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// ===========================================================================
|
|
423
|
+
// The real router over those tables
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
|
|
426
|
+
// The viewer the stubbed session middleware presents, and what the two doors of
|
|
427
|
+
// the recruiting gate answer for them. `curate` is the permission atom; `owns` is
|
|
428
|
+
// the instance-asserted project ownership scouting-authz reads.
|
|
429
|
+
let viewer = null;
|
|
430
|
+
|
|
431
|
+
const moduleApiPath = require.resolve('../src/module-api.js');
|
|
432
|
+
const helpers = require('../src/bongos/routes/_helpers.js');
|
|
433
|
+
const DENIED_BODY = { ok: false, error: 'forbidden', required: ['project.curate'] };
|
|
434
|
+
|
|
435
|
+
require.cache[moduleApiPath] = {
|
|
436
|
+
id: moduleApiPath,
|
|
437
|
+
filename: moduleApiPath,
|
|
438
|
+
loaded: true,
|
|
439
|
+
exports: {
|
|
440
|
+
pool,
|
|
441
|
+
parsePagination: helpers.parsePagination,
|
|
442
|
+
corsPublicGet: () => (_req, _res, next) => next(),
|
|
443
|
+
logger: () => ({ error: () => {} }),
|
|
444
|
+
requireBuilder: (req, res, next) => {
|
|
445
|
+
if (!viewer) return res.fail('unauthenticated', 401);
|
|
446
|
+
req.builder = { github_id: viewer.github_id };
|
|
447
|
+
next();
|
|
448
|
+
},
|
|
449
|
+
// The REAL gates are core auth over a real session; standing them up here
|
|
450
|
+
// would drag the whole auth graph in. What must be proved is their OUTPUT —
|
|
451
|
+
// WHO each of the two recruiting reads admits — so each is doubled by a gate
|
|
452
|
+
// answering the shape core sends. The two are DIFFERENT on purpose: the
|
|
453
|
+
// directory's permission door, and the sliver's archon floor.
|
|
454
|
+
requirePermission: () => (req, res, next) => {
|
|
455
|
+
if (viewer && viewer.curate) return next();
|
|
456
|
+
return res.status(403).json(DENIED_BODY);
|
|
457
|
+
},
|
|
458
|
+
requireRank: (...allowed) => (req, res, next) => {
|
|
459
|
+
if (viewer && allowed.includes(viewer.rank)) return next();
|
|
460
|
+
return res.fail('rank_forbidden', 403, { required: [...allowed], actual: (viewer && viewer.rank) || null });
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const buildScoutingRoutes = require('../modules/platform-identity/routes/scouting.js');
|
|
466
|
+
const buildPublicProfileRoutes = require('../modules/platform-identity/routes/public-profile.js');
|
|
467
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
468
|
+
|
|
469
|
+
const app = express();
|
|
470
|
+
app.use(express.json());
|
|
471
|
+
app.use(attachFail);
|
|
472
|
+
app.use('/api/bongos', buildScoutingRoutes());
|
|
473
|
+
app.use('/api/bongos', buildPublicProfileRoutes());
|
|
474
|
+
const server = app.listen(0, '127.0.0.1');
|
|
475
|
+
await new Promise((r) => server.once('listening', r));
|
|
476
|
+
const port = server.address().port;
|
|
477
|
+
after(() => server.close());
|
|
478
|
+
|
|
479
|
+
function get(path_) {
|
|
480
|
+
calls = [];
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path: path_ }, (res) => {
|
|
483
|
+
const chunks = [];
|
|
484
|
+
res.on('data', (c) => chunks.push(c));
|
|
485
|
+
res.on('end', () => {
|
|
486
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
487
|
+
let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
|
|
488
|
+
resolve({ status: res.statusCode, text, json });
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
req.on('error', reject);
|
|
492
|
+
req.end();
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// `rank` is the caller's rank ON THE HUB; `curate`/`owns` are the directory's two
|
|
497
|
+
// doors. The split is the point: a project owner is typically a bare xenos here
|
|
498
|
+
// and a metic operator holds `project.curate` without being an archon — so
|
|
499
|
+
// neither reaches the sliver, which is the whole of ADR 0215 D4.
|
|
500
|
+
const ARCHON = { github_id: '7000004', rank: 'archon', curate: false, owns: false };
|
|
501
|
+
const RECRUITER = { github_id: '7000001', rank: 'metic', curate: true, owns: false };
|
|
502
|
+
const OWNER = { github_id: '7000002', rank: 'xenos', curate: false, owns: true };
|
|
503
|
+
const STRANGER = { github_id: '7000003', rank: 'xenos', curate: false, owns: false };
|
|
504
|
+
|
|
505
|
+
const sliverOf = (handle) => get(`/api/bongos/scouting/${handle}`);
|
|
506
|
+
const handleOf = (githubId) => ACCOUNTS.find((a) => a.github_id === githubId).handle;
|
|
507
|
+
|
|
508
|
+
// ===========================================================================
|
|
509
|
+
// 0. The instrument is calibrated before it is trusted
|
|
510
|
+
// ===========================================================================
|
|
511
|
+
|
|
512
|
+
test('the interpreter refuses a predicate it cannot read, rather than ignoring it', () => {
|
|
513
|
+
assert.throws(() => accountFilter('github_id = $1 AND some_new_flag = $2', [DIR, true]), /unread predicate on \$2/);
|
|
514
|
+
assert.throws(() => accountFilter("github_id = $1 AND tier = 'gold'", [DIR]), /unrecognised literal comparison on tier/);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test('the interpreter really does model the reach tri-state, in both spellings', () => {
|
|
518
|
+
const bare = accountFilter(recruiterReachSql(), []);
|
|
519
|
+
const aliased = accountFilter(recruiterReachSql('pa'), []);
|
|
520
|
+
for (const f of [bare, aliased]) {
|
|
521
|
+
assert.equal(f({ account_visibility: 'public', recruiter_discoverable: null }), true, 'null follows a public visibility');
|
|
522
|
+
assert.equal(f({ account_visibility: 'private', recruiter_discoverable: null }), false, 'and follows a private one');
|
|
523
|
+
assert.equal(f({ account_visibility: 'private', recruiter_discoverable: true }), true, 'an explicit true overrides');
|
|
524
|
+
assert.equal(f({ account_visibility: 'public', recruiter_discoverable: false }), false, 'an explicit false overrides');
|
|
525
|
+
}
|
|
526
|
+
// ...and the reach COALESCE must not be mistaken for the EXISTENCE axis, which
|
|
527
|
+
// is spelled with the same two words inside it.
|
|
528
|
+
const reachOnly = accountFilter(recruiterReachSql(), []);
|
|
529
|
+
assert.equal(reachOnly({ account_visibility: 'private', recruiter_discoverable: true }), true,
|
|
530
|
+
'a private, discoverable account passes reach — if the interpreter read the COALESCE as an existence check it would not');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// ===========================================================================
|
|
534
|
+
// 1. The predicate: consent AND reach, and deliberately NOT existence
|
|
535
|
+
// ===========================================================================
|
|
536
|
+
|
|
537
|
+
test('recruiterSliverVisibleSql is COMPOSED from the consent floor and the reach axis', () => {
|
|
538
|
+
assert.ok(recruiterSliverVisibleSql('pa').includes(accountActiveSql('pa')), 'the consent floor, literally contained');
|
|
539
|
+
assert.ok(recruiterSliverVisibleSql('pa').includes(recruiterReachSql('pa')), 'and the reach axis, literally contained');
|
|
540
|
+
assert.ok(!recruiterSliverVisibleSql('pa').includes(accountVisibleSql('pa')),
|
|
541
|
+
'and NOT the existence axis: the private, discoverable account is the row D3 exists to serve');
|
|
542
|
+
assert.match(recruiterSliverVisibleSql(), /profile_state = 'active'/, 'a provisional account is never recruiter-visible');
|
|
543
|
+
assert.doesNotMatch(recruiterSliverVisibleSql(), /hide_stats/, 'hide changes the SHAPE of the sliver, never who appears in it');
|
|
544
|
+
assert.throws(() => recruiterSliverVisibleSql("pa; DROP TABLE x --"), /bare SQL identifier/);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test('the JS twin answers the D2 matrix exactly as the SQL asks it', () => {
|
|
548
|
+
const rows = [
|
|
549
|
+
[{ terms_accepted_at: 'T1', profile_state: 'active', account_visibility: 'private', recruiter_discoverable: true }, true],
|
|
550
|
+
[{ terms_accepted_at: 'T1', profile_state: 'active', account_visibility: 'private', recruiter_discoverable: null }, false],
|
|
551
|
+
[{ terms_accepted_at: 'T1', profile_state: 'active', account_visibility: 'public', recruiter_discoverable: null }, true],
|
|
552
|
+
[{ terms_accepted_at: 'T1', profile_state: 'active', account_visibility: 'public', recruiter_discoverable: false }, false],
|
|
553
|
+
[{ terms_accepted_at: 'T1', profile_state: 'provisional', account_visibility: 'public', recruiter_discoverable: true }, false],
|
|
554
|
+
];
|
|
555
|
+
for (const [row, want] of rows) {
|
|
556
|
+
assert.equal(isRecruiterSliverVisible(row), want, JSON.stringify(row));
|
|
557
|
+
assert.equal(accountFilter(recruiterSliverVisibleSql(), [])(row), want, `the SQL fragment agrees for ${JSON.stringify(row)}`);
|
|
558
|
+
}
|
|
559
|
+
for (const bad of [null, undefined, {}, { profile_state: 'active' }]) {
|
|
560
|
+
assert.equal(isRecruiterSliverVisible(bad), false, 'fail-closed on an absent or unreadable account');
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ===========================================================================
|
|
565
|
+
// 2. The WHITELIST — exactly these keys, and nothing else
|
|
566
|
+
// ===========================================================================
|
|
567
|
+
|
|
568
|
+
test('a full sliver carries EXACTLY the five keys D3 names', async () => {
|
|
569
|
+
viewer = ARCHON;
|
|
570
|
+
const res = await sliverOf(handleOf(PRIV));
|
|
571
|
+
assert.equal(res.status, 200, 'a private, explicitly-discoverable account IS the sliver subject');
|
|
572
|
+
assert.deepEqual(Object.keys(res.json.sliver).sort(), [...SLIVER_KEYS].sort(),
|
|
573
|
+
'the response key set is compared to the module\'s own list — a sixth key fails whether or not this file was told about it');
|
|
574
|
+
assert.deepEqual(Object.keys(res.json).sort(), ['sliver'], 'and nothing rides beside it at the top level');
|
|
575
|
+
assert.equal(res.json.sliver.handle, 'priv-handle');
|
|
576
|
+
assert.equal(res.json.sliver.display_name, 'Priv Person');
|
|
577
|
+
assert.deepEqual(res.json.sliver.disciplines, ['priv-craft']);
|
|
578
|
+
assert.equal(res.json.sliver.active_recently, true, 'the activity signal is a BOOLEAN, never a date');
|
|
579
|
+
assert.equal(ACTIVE_WINDOW_DAYS, 30, 'the window is the privacy parameter D3 names, not a tuning knob');
|
|
580
|
+
const activity = calls.find((c) => /WITH act AS/.test(c.sql));
|
|
581
|
+
assert.ok(activity.params.includes(ACTIVE_WINDOW_DAYS),
|
|
582
|
+
'and it is BOUND to the statement, so the constant and the query cannot drift apart');
|
|
583
|
+
assert.match(activity.sql, /make_interval\(days => \$2::int\)/,
|
|
584
|
+
'the window comparison happens in SQL — the timestamp never enters this process');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('hide_stats shrinks the sliver to NAME-ONLY, and the activity read is never issued', async () => {
|
|
588
|
+
viewer = ARCHON;
|
|
589
|
+
const res = await sliverOf(handleOf(SHY));
|
|
590
|
+
assert.equal(res.status, 200, 'hiding the record does not remove the person from the recruiting surface');
|
|
591
|
+
assert.deepEqual(Object.keys(res.json.sliver).sort(), [...SLIVER_KEYS_HIDDEN].sort(),
|
|
592
|
+
'the two rollup-derived keys are ABSENT, not null — there is no empty box where a number used to be');
|
|
593
|
+
assert.ok(!calls.some((c) => /platform_identity_builder_project_activity/.test(c.sql)),
|
|
594
|
+
'and the numbers are never fetched at all: the cheapest way for a later edit to leak one is for it to already be in hand');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
test('the port answers the same shapes as the route, keyed by github_id', async () => {
|
|
598
|
+
assert.deepEqual(Object.keys(await getRecruiterSliver(PRIV, { pool })).sort(),
|
|
599
|
+
[...SLIVER_KEYS].sort());
|
|
600
|
+
assert.deepEqual(Object.keys(await getRecruiterSliver(SHY, { pool })).sort(),
|
|
601
|
+
[...SLIVER_KEYS_HIDDEN].sort());
|
|
602
|
+
assert.equal(await getRecruiterSliver('system:bfg', { pool }), null,
|
|
603
|
+
'a non-numeric principal answers null rather than throwing a bigint cast mid-request');
|
|
604
|
+
assert.equal(await getRecruiterSliverByHandle('NOT A HANDLE', { pool }), null,
|
|
605
|
+
'and an unusable handle answers null without a query');
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
// ===========================================================================
|
|
609
|
+
// 3. LEAK BY SUBTRACTION — planted sentinels, absent from the whole body
|
|
610
|
+
// ===========================================================================
|
|
611
|
+
|
|
612
|
+
test('no forbidden value appears ANYWHERE in a full sliver response', async () => {
|
|
613
|
+
viewer = ARCHON;
|
|
614
|
+
const res = await sliverOf(handleOf(PRIV));
|
|
615
|
+
for (const sentinel of forbiddenOf(PRIV)) {
|
|
616
|
+
assert.ok(!res.text.includes(sentinel),
|
|
617
|
+
`"${sentinel}" leaked into the sliver body — the whitelist must be built literally, never spread from a row`);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test('github_id is never a FIELD — and it reaches a recruiter ONLY inside the avatar D3 grants', async () => {
|
|
622
|
+
// The honest version of a claim this file used to overstate. `github_id` was
|
|
623
|
+
// a plain sentinel asserted absent from the body, and it passed only because
|
|
624
|
+
// the fixture avatars were invented URLs. A real avatar is GitHub's own —
|
|
625
|
+
// https://avatars.githubusercontent.com/u/<github_id>?v=4 — so granting the
|
|
626
|
+
// avatar (spec D3 does, explicitly) grants the id inside it, and that id
|
|
627
|
+
// resolves through api.github.com to a login. Pretending otherwise in a
|
|
628
|
+
// comment does not make it false; asserting exactly where it may appear does.
|
|
629
|
+
viewer = ARCHON;
|
|
630
|
+
const res = await sliverOf(handleOf(PRIV));
|
|
631
|
+
for (const key of ['github_id', 'github_login', 'id', 'login']) {
|
|
632
|
+
assert.ok(!(key in res.json.sliver), `${key} must never be a KEY of the sliver`);
|
|
633
|
+
}
|
|
634
|
+
assert.ok(res.text.includes(PRIV),
|
|
635
|
+
'CONTROL: the id really is in this body — otherwise the "only inside the avatar" claim below proves nothing');
|
|
636
|
+
assert.match(res.json.sliver.avatar_url, /^https:\/\/avatars\.githubusercontent\.com\/u\/\d+/,
|
|
637
|
+
'the fixture must carry the SHAPE a real row carries, or this whole test is theatre');
|
|
638
|
+
const withoutAvatar = res.text.split(res.json.sliver.avatar_url).join('');
|
|
639
|
+
assert.ok(!withoutAvatar.includes(PRIV),
|
|
640
|
+
'the id may ride the avatar and NOTHING else — a second appearance is a field that should not exist');
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test('a hidden builder leaks neither their numbers NOR their disciplines', async () => {
|
|
644
|
+
viewer = ARCHON;
|
|
645
|
+
const res = await sliverOf(handleOf(SHY));
|
|
646
|
+
for (const sentinel of [...forbiddenOf(SHY), ...ROLLUP[SHY].disciplines]) {
|
|
647
|
+
assert.ok(!res.text.includes(sentinel), `"${sentinel}" leaked into a name-only sliver`);
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('CONTROL: those same values DO reach a recruiter for a builder who consented', async () => {
|
|
652
|
+
// Without this, "absent" could mean the sentinels were never in play at all and
|
|
653
|
+
// every assertion above would pass vacuously.
|
|
654
|
+
viewer = RECRUITER;
|
|
655
|
+
const dir = await get('/api/bongos/scouting');
|
|
656
|
+
assert.equal(dir.status, 200);
|
|
657
|
+
for (const sentinel of [String(ROLLUP[DIR].credits), String(ROLLUP[DIR].karma), String(ROLLUP[DIR].works_shipped), ROLLUP[DIR].last_active]) {
|
|
658
|
+
assert.ok(dir.text.includes(sentinel), `the directory must carry ${sentinel} — otherwise the absence tests prove nothing`);
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test('CONTROL: the SAME builder, unhidden, DOES get those disciplines from the real port', async () => {
|
|
663
|
+
// The control this replaced ran a query THIS FILE wrote against its own fake
|
|
664
|
+
// and compared the fixture to itself — it never entered production code, so it
|
|
665
|
+
// proved only that an object equals itself. What has to be shown is that the
|
|
666
|
+
// name-only sliver is `hide_stats` doing its job and not an empty fixture, and
|
|
667
|
+
// the only honest way to show it is to flip that one flag on that one account
|
|
668
|
+
// and watch the REAL port hand the values over.
|
|
669
|
+
const shy = ACCOUNTS.find((a) => a.github_id === SHY);
|
|
670
|
+
shy.hide_stats = false;
|
|
671
|
+
try {
|
|
672
|
+
const full = await getRecruiterSliver(SHY, { pool });
|
|
673
|
+
assert.deepEqual(Object.keys(full).sort(), [...SLIVER_KEYS].sort(),
|
|
674
|
+
'unhidden, the same account yields the FULL five-key sliver');
|
|
675
|
+
assert.deepEqual(full.disciplines, ROLLUP[SHY].disciplines,
|
|
676
|
+
'and its disciplines really are reachable — so their absence above is the hide flag, not an empty fixture');
|
|
677
|
+
assert.equal(full.active_recently, true);
|
|
678
|
+
} finally {
|
|
679
|
+
shy.hide_stats = true;
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// ===========================================================================
|
|
684
|
+
// 4. WHO APPEARS — the opt-out empties the recruiting surface, both halves
|
|
685
|
+
// ===========================================================================
|
|
686
|
+
|
|
687
|
+
test('an opted-out builder is absent from the recruiting surface ENTIRELY', async () => {
|
|
688
|
+
viewer = ARCHON;
|
|
689
|
+
const one = await sliverOf(handleOf(OPTOUT));
|
|
690
|
+
assert.equal(one.status, 404, 'the sliver refuses them');
|
|
691
|
+
assert.equal(one.json.error.code, 'builder_not_found', '404, never 403 — a 403 would confirm the handle exists');
|
|
692
|
+
const dir = await get('/api/bongos/scouting');
|
|
693
|
+
assert.ok(!dir.text.includes(String(ROLLUP[OPTOUT].credits)), 'and so does the directory');
|
|
694
|
+
assert.ok(!dir.text.includes(ACCOUNTS.find((a) => a.github_id === OPTOUT).display_name));
|
|
695
|
+
assert.equal(await getRecruiterSliver(OPTOUT, { pool }), null, 'and the port itself, not merely the route');
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test('a PRIVATE account never appears unless its owner opted in by hand', async () => {
|
|
699
|
+
viewer = ARCHON;
|
|
700
|
+
// The default: private + no explicit flag = not reachable.
|
|
701
|
+
assert.equal((await sliverOf(handleOf(PRIVDEF))).status, 404);
|
|
702
|
+
assert.equal(await getRecruiterSliver(PRIVDEF, { pool }), null);
|
|
703
|
+
// And a private account is absent from the DIRECTORY even when it IS
|
|
704
|
+
// discoverable — the full rollup is never its disclosure, the sliver is.
|
|
705
|
+
const dir = await get('/api/bongos/scouting');
|
|
706
|
+
assert.ok(!dir.text.includes('Priv Person'), 'the private, discoverable account stays out of the directory');
|
|
707
|
+
assert.ok(!dir.text.includes(String(ROLLUP[PRIV].credits)), 'with none of its numbers');
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
test('a PROVISIONAL account is never recruiter-visible, however its flags read', async () => {
|
|
711
|
+
viewer = ARCHON;
|
|
712
|
+
assert.equal((await sliverOf(handleOf(PROV))).status, 404,
|
|
713
|
+
'recruiter_discoverable=true does not survive the consent floor (spec D3)');
|
|
714
|
+
assert.equal(await getRecruiterSliver(PROV, { pool }), null);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// ===========================================================================
|
|
718
|
+
// 5. THE BOUNDARY IS A CEILING — the handle opens no page
|
|
719
|
+
// ===========================================================================
|
|
720
|
+
|
|
721
|
+
test('the handle a recruiter is given does NOT open the profile page', async () => {
|
|
722
|
+
viewer = ARCHON;
|
|
723
|
+
const sliver = await sliverOf(handleOf(PRIV));
|
|
724
|
+
assert.equal(sliver.status, 200);
|
|
725
|
+
const page = await get(`/api/bongos/profiles/${sliver.json.sliver.handle}`);
|
|
726
|
+
assert.equal(page.status, 404,
|
|
727
|
+
'the pre-accept sliver is a CEILING, not a step: the public page joins the EXISTENCE axis and this account is private');
|
|
728
|
+
for (const sentinel of forbiddenOf(PRIV)) {
|
|
729
|
+
assert.ok(!page.text.includes(sentinel), `"${sentinel}" reached a recruiter through the profile page instead`);
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test('a PREVIOUS handle opens nothing — the sliver does not confirm a rename', async () => {
|
|
734
|
+
viewer = ARCHON;
|
|
735
|
+
const stale = await sliverOf('priv-oldname-990204');
|
|
736
|
+
assert.equal(stale.status, 404,
|
|
737
|
+
'unlike the public page, which answers 301 so a SHARED LINK survives a rename, a recruiting read has '
|
|
738
|
+
+ 'no shared links to keep working — and resolving one would confirm to a recruiter that this account '
|
|
739
|
+
+ 'renamed, a disclosure the sliver never grants');
|
|
740
|
+
assert.equal((await sliverOf(handleOf(PRIV))).status, 200,
|
|
741
|
+
'CONTROL: the CURRENT handle still resolves — otherwise the 404 above proves only that the route is broken');
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test('CONTROL: that same page DOES render for a builder who is publicly visible', async () => {
|
|
745
|
+
viewer = RECRUITER;
|
|
746
|
+
const page = await get(`/api/bongos/profiles/${handleOf(DIR)}`);
|
|
747
|
+
assert.equal(page.status, 200, 'otherwise the 404 above proves only that the route is broken');
|
|
748
|
+
assert.ok(page.text.includes(String(ROLLUP[DIR].credits)), 'and it carries the post-accept record the sliver withholds');
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// ===========================================================================
|
|
752
|
+
// 6. THE GATE — a bounded projection behind an open door is not bounded
|
|
753
|
+
// ===========================================================================
|
|
754
|
+
|
|
755
|
+
test('the sliver is ARCHON-gated, and the directory audience does not reach it', async () => {
|
|
756
|
+
// ADR 0215 D4. Privacy spec D3 pins this read to `project.recruit`, floor
|
|
757
|
+
// ARCHON. The directory's door — `project.curate` at metic, OR any project
|
|
758
|
+
// owner (ADR 0210 D2) — is WIDER on both halves, so reusing it would widen an
|
|
759
|
+
// owner-signed privacy decision by implementation convenience. This is the
|
|
760
|
+
// first surface that discloses a private account, so it holds the signed floor
|
|
761
|
+
// until the atom exists; a blocker asks the owner about the wider audience.
|
|
762
|
+
viewer = ARCHON;
|
|
763
|
+
assert.equal((await sliverOf(handleOf(PRIV))).status, 200, 'an archon reads the sliver');
|
|
764
|
+
for (const who of [RECRUITER, OWNER]) {
|
|
765
|
+
viewer = who;
|
|
766
|
+
assert.equal((await sliverOf(handleOf(PRIV))).status, 403,
|
|
767
|
+
`a ${who.rank} holding the directory's door must NOT reach the sliver`);
|
|
768
|
+
assert.equal((await get('/api/bongos/scouting')).status, 200,
|
|
769
|
+
'while still reading the directory — two audiences, not one narrowed');
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
test('the refusal names the RANK it wanted, and no session reaches either door', async () => {
|
|
774
|
+
viewer = STRANGER;
|
|
775
|
+
const one = await sliverOf(handleOf(PRIV));
|
|
776
|
+
assert.equal(one.status, 403);
|
|
777
|
+
assert.deepEqual(one.json.error.details, { required: ['archon'], actual: 'xenos' },
|
|
778
|
+
'a rank refusal says which floor it wanted, so the hall can render why — it must not degrade to a bare 403');
|
|
779
|
+
assert.equal((await get('/api/bongos/scouting')).status, 403, 'and the directory refuses them too');
|
|
780
|
+
viewer = null;
|
|
781
|
+
assert.equal((await sliverOf(handleOf(PRIV))).status, 401, 'no session reaches either door');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test('a refusal happens BEFORE any builder data is read', async () => {
|
|
785
|
+
viewer = STRANGER;
|
|
786
|
+
await sliverOf(handleOf(PRIV));
|
|
787
|
+
assert.ok(!calls.some((c) => /platform_identity_accounts/.test(c.sql)),
|
|
788
|
+
'the gate refuses without touching the subject account at all');
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
test('each route spells its OWN gate in its OWN middleware list', () => {
|
|
792
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/platform-identity/routes/scouting.js'), 'utf8');
|
|
793
|
+
assert.equal((src.match(/requirePermission\('project\.curate'\)/g) || []).length, 1,
|
|
794
|
+
'the directory names its atom where route-rank-check.js and gen-api-docs.js can see it — a gate hidden in a helper records a privileged route as any-builder');
|
|
795
|
+
assert.equal((src.match(/api\.requireRank\('archon'\)/g) || []).length, 1,
|
|
796
|
+
'and the sliver names its floor the same way, so the generated docs record archon and not the directory metic');
|
|
797
|
+
const sliverBlock = src.slice(src.indexOf("'/scouting/:handle'"));
|
|
798
|
+
assert.doesNotMatch(sliverBlock.slice(0, sliverBlock.indexOf('async (req, res)')), /requireScoutingReader/,
|
|
799
|
+
'and the sliver must never pick up the directory wider door by accident');
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// ===========================================================================
|
|
803
|
+
// 7. IT GRANTS NOTHING (ADR 0016)
|
|
804
|
+
// ===========================================================================
|
|
805
|
+
|
|
806
|
+
test('the sliver module is read by exactly one first-party server file — the one that serializes it', () => {
|
|
807
|
+
const files = [];
|
|
808
|
+
const walk = (dir) => {
|
|
809
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
810
|
+
const p = path.join(dir, e.name);
|
|
811
|
+
if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p); }
|
|
812
|
+
else if (e.name.endsWith('.js')) files.push(p);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
for (const d of ['src', 'modules', 'scripts']) walk(path.join(ROOT, d));
|
|
816
|
+
assert.ok(files.length > 300, `only ${files.length} files scanned — the sweep went vacuous`);
|
|
817
|
+
const readers = files
|
|
818
|
+
.filter((f) => /require\(\s*['"][^'"]*recruiter-sliver['"]\s*\)/.test(fs.readFileSync(f, 'utf8')))
|
|
819
|
+
.map((f) => path.relative(ROOT, f).split(path.sep).join('/'));
|
|
820
|
+
assert.deepEqual(readers, ['modules/platform-identity/routes/scouting.js'],
|
|
821
|
+
'nothing may consult the sliver to DECIDE anything: cross-project numbers are descriptive analytics, never authority (ADR 0016)');
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test('no rank, role, permission or membership travels with the sliver, under any name', async () => {
|
|
825
|
+
viewer = ARCHON;
|
|
826
|
+
const res = await sliverOf(handleOf(PRIV));
|
|
827
|
+
for (const word of ['rank', 'role', 'permission', 'membership', 'archon', 'metic', 'credits', 'karma', 'project']) {
|
|
828
|
+
assert.ok(!res.text.toLowerCase().includes(word),
|
|
829
|
+
`"${word}" appears in a sliver body — being visible to a recruiter must confer and reveal nothing`);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// ===========================================================================
|
|
834
|
+
// 8. This file runs in the lane that executes
|
|
835
|
+
// ===========================================================================
|
|
836
|
+
|
|
837
|
+
test('this proof runs in the DB-free unit lane', () => {
|
|
838
|
+
const runner = fs.readFileSync(path.join(ROOT, 'scripts/gds/run-unit-tests.js'), 'utf8');
|
|
839
|
+
assert.ok(!/recruiter_sliver_boundary/.test(runner),
|
|
840
|
+
'named in run-unit-tests.js it would be parked into INTEGRATION or KNOWN_FAILING and gate nothing (learning 1000218)');
|
|
841
|
+
});
|