@bongos/core 1.19.579 → 1.19.581
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 +75 -40
- package/bin/bongos.js +10 -1
- 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 +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +146 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +73 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +21 -18
- package/docs/copy-registry.json +50 -23
- package/docs/file-map.md +2 -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/oversight.css +7 -0
- package/modules/hall-ui/public/watch.js +75 -0
- package/modules/onboarding/routes/access-requests.js +56 -1
- package/modules/platform-identity/applicant-profile.js +158 -0
- package/modules/platform-identity/application-echo.js +50 -0
- package/modules/platform-identity/recruiter-sliver.js +19 -0
- package/modules/platform-identity/routes/sso.js +126 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/build-cli-package.js +8 -2
- package/scripts/gds/cli-lib.js +11 -3
- package/scripts/gds/recall.js +1 -1
- package/scripts/gds/start.js +39 -26
- package/scripts/gds/surface.js +89 -0
- package/src/bongos/auth-admission.js +61 -0
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +11 -1
- package/tests/adr_renumber_integrity.mjs +84 -0
- package/tests/applicant_profile_boundary.mjs +641 -0
- package/tests/applicant_profile_read_bounds.mjs +95 -0
- package/tests/cli_surface.mjs +146 -0
- package/tests/module_api.mjs +1 -0
- package/tests/recruiter_sliver_boundary.mjs +35 -2
- package/tests/watch_applications_queue.mjs +94 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
// tests/applicant_profile_boundary.mjs — THE D7 BOUNDARY PROOF (task 1002972,
|
|
2
|
+
// goal 1000045; privacy spec D7 + C2/C4, ADR 0215, ADR 0205, ADR 0171 D4/D5,
|
|
3
|
+
// ADR 0016).
|
|
4
|
+
//
|
|
5
|
+
// D7's claim is "ONE RULE, NO NEW DISCLOSURE CLASS": a reviewer sees what the
|
|
6
|
+
// applicant's OWN profile rules would already show, and nothing that any
|
|
7
|
+
// existing surface would not have shown them. So the things worth proving are
|
|
8
|
+
// not that the port returns data — they are the four ways that claim could
|
|
9
|
+
// quietly stop being true:
|
|
10
|
+
//
|
|
11
|
+
// 1. THE TWO SHAPES ARE THE EXISTING TWO, NOT LOOKALIKES. The private branch's
|
|
12
|
+
// key set is compared to recruiter-sliver's OWN exported SLIVER_KEYS /
|
|
13
|
+
// SLIVER_KEYS_HIDDEN, and the public branch's to the key list the public
|
|
14
|
+
// page's payload carries. Both are WHITELISTS read from the modules, never
|
|
15
|
+
// copies kept here: a sixth key fails whether or not this file was told
|
|
16
|
+
// about it. If someone builds a third shape, that IS the new disclosure
|
|
17
|
+
// class D7 forbids, and it reds this file.
|
|
18
|
+
//
|
|
19
|
+
// 2. THE CONSENT SWAP IS DELIBERATE AND STILL BOUNDED — the one place D7
|
|
20
|
+
// differs from the recruiter sliver, and therefore the assertion most worth
|
|
21
|
+
// writing down. getRecruiterSliver floors on recruiting REACH; the
|
|
22
|
+
// applicant view floors on the APPLICATION ("applying is an explicit act",
|
|
23
|
+
// D7). So a PRIVATE account that never opted into recruiting DOES get an
|
|
24
|
+
// applicant view and DOES NOT get a recruiter sliver — asserted in BOTH
|
|
25
|
+
// directions on the SAME account, so a later "tidy-up" that routes this
|
|
26
|
+
// port through getRecruiterSliver cannot pass quietly. It would look like a
|
|
27
|
+
// simplification and would silently empty C4 for most private applicants.
|
|
28
|
+
//
|
|
29
|
+
// And the swap is bounded: what it does NOT relax is asserted beside it —
|
|
30
|
+
// a PROVISIONAL account and an account that never accepted the terms
|
|
31
|
+
// disclose NOTHING, applying or not.
|
|
32
|
+
//
|
|
33
|
+
// 3. hide_stats BEATS THE APPLICATION, in both branches. A public applicant
|
|
34
|
+
// with hidden stats has a null rollup; a private one is name-only. Applying
|
|
35
|
+
// is consent to be SEEN, never consent to be COUNTED — and C2 says a hide
|
|
36
|
+
// takes effect on the next render, which is only meaningful if the render
|
|
37
|
+
// reads it.
|
|
38
|
+
//
|
|
39
|
+
// 4. NOTHING RIDES ALONG. Every value the private branch must not carry is an
|
|
40
|
+
// IMPROBABLE PLANTED SENTINEL and the assertion is absence from the whole
|
|
41
|
+
// serialized body — the recruiter_sliver_boundary technique. The sentinels
|
|
42
|
+
// sit on the rows this port's OWN queries return (the fake returns MORE
|
|
43
|
+
// columns than each statement selects), so a projection that spreads a row
|
|
44
|
+
// instead of whitelisting leaks a number and reds this file.
|
|
45
|
+
//
|
|
46
|
+
// PLUS THE GATE, because a bounded projection behind an open door is not
|
|
47
|
+
// bounded — and here the door is the entire security story (ADR 0205, security
|
|
48
|
+
// report 1000027). POST /sso/applicant-profile is authenticated by client
|
|
49
|
+
// credentials, which prove WHICH PROJECT is calling and NOTHING about who the
|
|
50
|
+
// github_id in the body is. Without a per-subject witness it is a bulk
|
|
51
|
+
// disclosure oracle over every account on the platform, private ones included.
|
|
52
|
+
// So: a client with no echo for that account gets 404 AND NO ACCOUNT IS READ AT
|
|
53
|
+
// ALL (asserted on the statement log, not just the response — a gate that
|
|
54
|
+
// refuses after reading has already done the disclosure work), the echo is
|
|
55
|
+
// matched against the AUTHENTICATED client_id rather than the body's, and an
|
|
56
|
+
// echo older than the window stops working.
|
|
57
|
+
//
|
|
58
|
+
// NOT AUTHORITY (ADR 0016), asserted structurally: the response carries no rank,
|
|
59
|
+
// role, permission or membership under any name.
|
|
60
|
+
//
|
|
61
|
+
// MECHANISM (the tests/recruiter_sliver_boundary.mjs harness): in-memory tables
|
|
62
|
+
// whose row filter is built FROM EACH STATEMENT'S OWN WHERE CLAUSE, refusing any
|
|
63
|
+
// statement whose placeholders or literal comparisons it did not consume — so a
|
|
64
|
+
// predicate this interpreter cannot read is a loud error and never a silently
|
|
65
|
+
// absent filter. Each recognised term is CONSUMED and the residue must be pure
|
|
66
|
+
// conjunction, so rewriting an AND to an OR cannot stay green. Above the pool
|
|
67
|
+
// everything is real: the real module, the real router, real HTTP.
|
|
68
|
+
//
|
|
69
|
+
// DB-free by construction.
|
|
70
|
+
//
|
|
71
|
+
// Run: node tests/applicant_profile_boundary.mjs
|
|
72
|
+
|
|
73
|
+
import { strict as assert } from 'node:assert';
|
|
74
|
+
import { test, after } from 'node:test';
|
|
75
|
+
import { createRequire } from 'node:module';
|
|
76
|
+
import http from 'node:http';
|
|
77
|
+
|
|
78
|
+
// Imported as ESM so the whitelists come from the MODULES rather than from
|
|
79
|
+
// copies kept here — the whole point of asserting against them.
|
|
80
|
+
import {
|
|
81
|
+
SLIVER_KEYS,
|
|
82
|
+
SLIVER_KEYS_HIDDEN,
|
|
83
|
+
getRecruiterSliver,
|
|
84
|
+
} from '../modules/platform-identity/recruiter-sliver.js';
|
|
85
|
+
import {
|
|
86
|
+
APPLICANT_VIEWS,
|
|
87
|
+
VIEW_PUBLIC,
|
|
88
|
+
VIEW_SLIVER,
|
|
89
|
+
getApplicantProfileView,
|
|
90
|
+
} from '../modules/platform-identity/applicant-profile.js';
|
|
91
|
+
import { applicationBacksProfileRead } from '../modules/platform-identity/application-echo.js';
|
|
92
|
+
|
|
93
|
+
const require = createRequire(import.meta.url);
|
|
94
|
+
const express = require('express');
|
|
95
|
+
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
// The fixture — one account per row of the privacy-spec D2 matrix that can APPLY
|
|
98
|
+
// ===========================================================================
|
|
99
|
+
|
|
100
|
+
const PUB = '4110001'; // active · public · shown — the public branch
|
|
101
|
+
const PUBSHY = '4110002'; // active · public · HIDDEN — public, no numbers
|
|
102
|
+
const PRIVIN = '4110003'; // active · PRIVATE · reach=true — private, opted in
|
|
103
|
+
const PRIVOUT = '4110004'; // active · PRIVATE · reach=null — private, NEVER opted in: the swap's subject
|
|
104
|
+
const PRIVSHY = '4110005'; // active · PRIVATE · HIDDEN — name-only sliver
|
|
105
|
+
const PROV = '4110006'; // PROVISIONAL — discloses nothing
|
|
106
|
+
const NOTERMS = '4110007'; // active · public · terms NEVER accepted — discloses nothing
|
|
107
|
+
|
|
108
|
+
// Every account column the table holds. The queries select a SUBSET; the extras
|
|
109
|
+
// are here on purpose, so a row spread leaks something loud.
|
|
110
|
+
const ACCOUNTS = [
|
|
111
|
+
{ github_id: PUB, handle: 'pub-handle', github_login: 'pub-login-770101', display_name: 'Pub Person',
|
|
112
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PUB}?v=4`, previous_handle: null, bio: 'pub-bio-770103',
|
|
113
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
114
|
+
hide_stats: false, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
115
|
+
{ github_id: PUBSHY, handle: 'pubshy-handle', github_login: 'pubshy-login-770201', display_name: 'Pubshy Person',
|
|
116
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PUBSHY}?v=4`, previous_handle: null, bio: null,
|
|
117
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
118
|
+
hide_stats: true, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
119
|
+
{ github_id: PRIVIN, handle: 'privin-handle', github_login: 'privin-login-770301', display_name: 'Privin Person',
|
|
120
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PRIVIN}?v=4`, previous_handle: null, bio: 'privin-bio-770303',
|
|
121
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'private',
|
|
122
|
+
hide_stats: false, recruiter_discoverable: true, terms_accepted_at: 'T1' },
|
|
123
|
+
{ github_id: PRIVOUT, handle: 'privout-handle', github_login: 'privout-login-770401', display_name: 'Privout Person',
|
|
124
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PRIVOUT}?v=4`, previous_handle: 'privout-oldname-770404', bio: 'privout-bio-770403',
|
|
125
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'private',
|
|
126
|
+
hide_stats: false, recruiter_discoverable: null, terms_accepted_at: 'T1' },
|
|
127
|
+
{ github_id: PRIVSHY, handle: 'privshy-handle', github_login: 'privshy-login-770501', display_name: 'Privshy Person',
|
|
128
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PRIVSHY}?v=4`, previous_handle: null, bio: null,
|
|
129
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'private',
|
|
130
|
+
hide_stats: true, recruiter_discoverable: true, terms_accepted_at: 'T1' },
|
|
131
|
+
{ github_id: PROV, handle: 'prov-handle', github_login: 'prov-login-770601', display_name: 'Prov Person',
|
|
132
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${PROV}?v=4`, previous_handle: null, bio: null,
|
|
133
|
+
links: null, created_at: 'T0', profile_state: 'provisional', account_visibility: 'public',
|
|
134
|
+
hide_stats: false, recruiter_discoverable: true, terms_accepted_at: 'T1' },
|
|
135
|
+
{ github_id: NOTERMS, handle: 'noterms-handle', github_login: 'noterms-login-770701', display_name: 'Noterms Person',
|
|
136
|
+
avatar_url: `https://avatars.githubusercontent.com/u/${NOTERMS}?v=4`, previous_handle: null, bio: null,
|
|
137
|
+
links: null, created_at: 'T0', profile_state: 'active', account_visibility: 'public',
|
|
138
|
+
hide_stats: false, recruiter_discoverable: true, terms_accepted_at: null },
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const ROLLUP = {};
|
|
142
|
+
for (const [i, a] of ACCOUNTS.entries()) {
|
|
143
|
+
const b = 990000 + i * 100;
|
|
144
|
+
ROLLUP[a.github_id] = {
|
|
145
|
+
credits: b + 1, works_shipped: b + 2, karma: b + 3, project_count: b + 4,
|
|
146
|
+
last_active: `lastactive-${b + 5}`, disciplines: [`craft-${b + 8}`],
|
|
147
|
+
projects: [{ name: `Project-${b + 6}`, origin: `https://p${b}.example`, rank_held: 'metic', recent_ships: [`Ship-${b + 7}`], is_public: true }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const ACTIVE_RECENTLY = {
|
|
152
|
+
[PUB]: true, [PUBSHY]: true, [PRIVIN]: true, [PRIVOUT]: true, [PRIVSHY]: true, [PROV]: false, [NOTERMS]: false,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const account = (id) => ACCOUNTS.find((a) => a.github_id === id);
|
|
156
|
+
|
|
157
|
+
/** Everything about one account the SLIVER shape must never disclose. */
|
|
158
|
+
function forbiddenOf(id) {
|
|
159
|
+
const a = account(id);
|
|
160
|
+
const r = ROLLUP[id];
|
|
161
|
+
return [
|
|
162
|
+
// NOT the github_id: the avatar D3 grants is GitHub's own URL and embeds it
|
|
163
|
+
// verbatim, so it is disclosed by an authorized field (ADR 0215's honest
|
|
164
|
+
// limit). Pinned precisely below rather than pretended away here.
|
|
165
|
+
a.github_login,
|
|
166
|
+
a.bio,
|
|
167
|
+
String(r.credits), String(r.works_shipped), String(r.karma), String(r.project_count),
|
|
168
|
+
r.last_active,
|
|
169
|
+
...(a.previous_handle ? [a.previous_handle] : []),
|
|
170
|
+
...r.projects.flatMap((p) => [p.name, p.origin, p.rank_held, ...p.recent_ships]),
|
|
171
|
+
].filter(Boolean);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ===========================================================================
|
|
175
|
+
// The in-memory tables + their statement interpreter
|
|
176
|
+
// ===========================================================================
|
|
177
|
+
|
|
178
|
+
const KNOWN_COLUMNS = new Set(['profile_state', 'account_visibility', 'hide_stats', 'github_id', 'client_id', 'recruiter_discoverable', 'status']);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build a row test from a statement's own WHERE text, CONSUMING each term it
|
|
182
|
+
* models so the residue can be judged. Literal comparisons carry as much weight
|
|
183
|
+
* as placeholders: the visibility predicates are made ENTIRELY of literal
|
|
184
|
+
* comparisons, so a fake modelling only $N filters would score every privacy
|
|
185
|
+
* term as absent and pass however the code changed.
|
|
186
|
+
*
|
|
187
|
+
* The residue check is what makes this a STRUCTURE reader rather than a term
|
|
188
|
+
* collector: rewriting accountActiveSql's `AND` to `OR` would disclose every
|
|
189
|
+
* account the port can reach, and a fake that merely ANDs the terms it
|
|
190
|
+
* recognised keeps answering exactly as before. Anything left that is not
|
|
191
|
+
* whitespace, parentheses or `AND` is a loud error.
|
|
192
|
+
*/
|
|
193
|
+
function accountFilter(where, params) {
|
|
194
|
+
const tests = [];
|
|
195
|
+
const consumed = new Set();
|
|
196
|
+
let residue = where;
|
|
197
|
+
|
|
198
|
+
const take = (re, make) => {
|
|
199
|
+
const m = re.exec(residue);
|
|
200
|
+
if (!m) return null;
|
|
201
|
+
residue = residue.slice(0, m.index) + residue.slice(m.index + m[0].length);
|
|
202
|
+
tests.push(make(m));
|
|
203
|
+
return m;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Reach FIRST — the COALESCE spells `account_visibility = 'public'` inside
|
|
207
|
+
// itself as its own fallback, and consuming it first stops that fallback being
|
|
208
|
+
// scored as an existence check.
|
|
209
|
+
take(
|
|
210
|
+
/\bCOALESCE\(\s*(?:[a-z_]+\.)?recruiter_discoverable\s*,\s*(?:[a-z_]+\.)?account_visibility = 'public'\s*\)/,
|
|
211
|
+
() => (r) => (typeof r.recruiter_discoverable === 'boolean'
|
|
212
|
+
? r.recruiter_discoverable
|
|
213
|
+
: r.account_visibility === 'public'),
|
|
214
|
+
);
|
|
215
|
+
take(/\b(?:[a-z_]+\.)?profile_state = 'active'/, () => (r) => r.profile_state === 'active');
|
|
216
|
+
take(/\b(?:[a-z_]+\.)?terms_accepted_at IS NOT NULL/, () => (r) => r.terms_accepted_at != null);
|
|
217
|
+
take(/\b(?:[a-z_]+\.)?account_visibility = 'public'/, () => (r) => r.account_visibility === 'public');
|
|
218
|
+
take(/\bNOT (?:[a-z_]+\.)?hide_stats\b/, () => (r) => !r.hide_stats);
|
|
219
|
+
take(/\b(?:[a-z_]+\.)?github_id = \$(\d+)(?:::bigint)?/, (m) => {
|
|
220
|
+
consumed.add(m[1]);
|
|
221
|
+
const v = params[Number(m[1]) - 1];
|
|
222
|
+
return (r) => String(r.github_id) === String(v);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
for (const m of where.matchAll(/\$(\d+)/g)) {
|
|
226
|
+
if (!consumed.has(m[1])) {
|
|
227
|
+
throw new Error(`fake pool: unread predicate on $${m[1]} in "${where}" — a filter this interpreter cannot read must never degrade to no filter`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const m of where.matchAll(/\b([a-z_]+)\s*(?:=|<>|!=)\s*'/g)) {
|
|
231
|
+
if (!KNOWN_COLUMNS.has(m[1])) {
|
|
232
|
+
throw new Error(`fake pool: unrecognised literal comparison on ${m[1]} in "${where}"`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!/^[\s()]*(?:AND[\s()]*)*$/.test(residue)) {
|
|
236
|
+
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`);
|
|
237
|
+
}
|
|
238
|
+
return (r) => tests.every((fn) => fn(r));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function activityScope(sql, params) {
|
|
242
|
+
const m = /\ba\.github_id = \$(\d+)/.exec(sql);
|
|
243
|
+
if (!m) throw new Error(`fake pool: an activity read with no github_id scope — ${sql.slice(0, 120)}`);
|
|
244
|
+
return String(params[Number(m[1]) - 1]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// The SSO client registry + the echo table the gate reads.
|
|
248
|
+
const CLIENT = { client_id: 'client-alpha', secret_hash: null, name: 'Alpha', redirect_uris: [], origin: 'https://alpha.example', owner_github_id: '1', status: 'active' };
|
|
249
|
+
const OTHER_CLIENT = { client_id: 'client-beta', secret_hash: null, name: 'Beta', redirect_uris: [], origin: 'https://beta.example', owner_github_id: '2', status: 'active' };
|
|
250
|
+
// (github_id, client_id) pairs the HUB witnessed, and whether they are inside
|
|
251
|
+
// the read-time window. Only PUB and PRIVOUT applied to alpha.
|
|
252
|
+
let ECHOES = [];
|
|
253
|
+
|
|
254
|
+
let calls = [];
|
|
255
|
+
const pool = {
|
|
256
|
+
query: async (text, params = []) => {
|
|
257
|
+
const sql = String(text).replace(/\s+/g, ' ').trim();
|
|
258
|
+
calls.push({ sql, params });
|
|
259
|
+
|
|
260
|
+
if (/FROM platform_identity_sso_clients/.test(sql)) {
|
|
261
|
+
const hit = [CLIENT, OTHER_CLIENT].find((c) => c.client_id === params[0]);
|
|
262
|
+
return { rows: hit ? [{ ...hit }] : [], rowCount: hit ? 1 : 0 };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// The GATE's own read. The window term is modelled explicitly rather than
|
|
266
|
+
// waved through: an echo outside it must stop working, and a fake that
|
|
267
|
+
// ignored `applied_at` would make that untestable.
|
|
268
|
+
if (/FROM platform_identity_application_echoes/.test(sql)) {
|
|
269
|
+
assert.match(sql, /applied_at > now\(\) - make_interval\(days => \$3::int\)/,
|
|
270
|
+
'the gate must be window-scoped — an echo that never expires re-opens the read forever');
|
|
271
|
+
assert.equal(params[2], 30, 'the gate reuses the 30-day echo window, not a second number');
|
|
272
|
+
const rows = ECHOES.filter((e) => String(e.github_id) === String(params[0])
|
|
273
|
+
&& e.client_id === params[1] && e.in_window);
|
|
274
|
+
return { rows: rows.map(() => ({ '?column?': 1 })), rowCount: rows.length };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The SLIVER's activity half: a boolean and a disciplines union, never a date.
|
|
278
|
+
if (/WITH act AS/.test(sql)) {
|
|
279
|
+
const id = activityScope(sql, params);
|
|
280
|
+
const r = ROLLUP[id];
|
|
281
|
+
return {
|
|
282
|
+
rows: [{
|
|
283
|
+
active_recently: r ? ACTIVE_RECENTLY[id] : null,
|
|
284
|
+
disciplines: r ? r.disciplines : null,
|
|
285
|
+
// NOT selected by the statement. Present because a real row grows
|
|
286
|
+
// columns, so a projection that spreads rather than whitelists carries
|
|
287
|
+
// these straight into the response.
|
|
288
|
+
last_active: r ? r.last_active : null,
|
|
289
|
+
credits: r ? r.credits : null,
|
|
290
|
+
works_shipped: r ? r.works_shipped : null,
|
|
291
|
+
karma: r ? r.karma : null,
|
|
292
|
+
project_count: r ? r.project_count : null,
|
|
293
|
+
}],
|
|
294
|
+
rowCount: 1,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// The per-builder rollup aggregate behind the PUBLIC branch.
|
|
299
|
+
if (/WITH agg AS/.test(sql) && /platform_identity_builder_project_activity/.test(sql)) {
|
|
300
|
+
const r = ROLLUP[activityScope(sql, params)] || { credits: 0, works_shipped: 0, karma: 0, project_count: 0, last_active: null, disciplines: null };
|
|
301
|
+
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 };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (/platform_identity_builder_project_activity/.test(sql)) {
|
|
305
|
+
const r = ROLLUP[activityScope(sql, params)];
|
|
306
|
+
const rows = r ? r.projects.map((p) => ({ ...p })) : [];
|
|
307
|
+
return { rows, rowCount: rows.length };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (/FROM platform_identity_accounts/.test(sql)) {
|
|
311
|
+
const where = sql.includes(' WHERE ')
|
|
312
|
+
? sql.slice(sql.indexOf(' WHERE ') + 7).replace(/ ORDER BY .*| LIMIT .*/g, '')
|
|
313
|
+
: '';
|
|
314
|
+
// The rollup scalars ride the account row too, so a widened account
|
|
315
|
+
// projection leaks a number even for a name-only sliver.
|
|
316
|
+
const rows = ACCOUNTS.filter(accountFilter(where, params)).map((a) => ({ ...a, ...ROLLUP[a.github_id] }));
|
|
317
|
+
return { rows: rows.map((r) => ({ ...r })), rowCount: rows.length };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 140)}`);
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const view = (id) => getApplicantProfileView(id, { pool });
|
|
325
|
+
|
|
326
|
+
// ===========================================================================
|
|
327
|
+
// 1. The two shapes are the EXISTING two
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
|
|
330
|
+
test('the port answers only the two declared views (a third shape IS the new disclosure class)', async () => {
|
|
331
|
+
for (const id of [PUB, PUBSHY, PRIVIN, PRIVOUT, PRIVSHY]) {
|
|
332
|
+
const v = await view(id);
|
|
333
|
+
assert.ok(v, `${id} should be disclosable`);
|
|
334
|
+
assert.ok(APPLICANT_VIEWS.includes(v.view), `${id} answered an undeclared view "${v.view}"`);
|
|
335
|
+
}
|
|
336
|
+
assert.deepEqual(APPLICANT_VIEWS, [VIEW_PUBLIC, VIEW_SLIVER],
|
|
337
|
+
'the declared vocabulary grew — a new view is a new disclosure class and needs its own owner sign-off, not a passing test');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test('the PUBLIC branch carries exactly the public page payload, key for key', async () => {
|
|
341
|
+
const v = await view(PUB);
|
|
342
|
+
assert.equal(v.view, VIEW_PUBLIC);
|
|
343
|
+
assert.deepEqual(Object.keys(v).sort(), ['account', 'cross_project', 'view']);
|
|
344
|
+
// The identity band the canonical /u/<handle> payload carries — and NOT
|
|
345
|
+
// github_id or profile_state, which stay internal there and must stay
|
|
346
|
+
// internal here.
|
|
347
|
+
assert.deepEqual(
|
|
348
|
+
Object.keys(v.account).sort(),
|
|
349
|
+
['avatar_url', 'bio', 'display_name', 'github_login', 'handle', 'links', 'member_since'],
|
|
350
|
+
'the public applicant view grew or lost a key relative to the public profile payload — D7 says it IS that payload',
|
|
351
|
+
);
|
|
352
|
+
assert.ok(!('github_id' in v.account), 'the join key must stay internal, exactly as on the public page');
|
|
353
|
+
assert.ok(!('profile_state' in v.account), 'the answer itself is the addressability signal');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('the PRIVATE branch is the D3 sliver projection — asserted against the module’s own whitelist', async () => {
|
|
357
|
+
const v = await view(PRIVIN);
|
|
358
|
+
assert.equal(v.view, VIEW_SLIVER);
|
|
359
|
+
assert.deepEqual(Object.keys(v).sort(), ['sliver', 'view']);
|
|
360
|
+
assert.deepEqual(Object.keys(v.sliver).sort(), [...SLIVER_KEYS].sort(),
|
|
361
|
+
'the private applicant view is not the D3 sliver’s key set — a second pre-accept projection is exactly what D7 forbids');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ===========================================================================
|
|
365
|
+
// 2. The consent swap — deliberate, and bounded
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
|
|
368
|
+
test('THE SWAP: a private account that never opted into recruiting gets an applicant view and NO recruiter sliver', async () => {
|
|
369
|
+
const a = account(PRIVOUT);
|
|
370
|
+
assert.equal(a.account_visibility, 'private');
|
|
371
|
+
assert.equal(a.recruiter_discoverable, null, 'the fixture must be the never-opted-in row or this proves nothing');
|
|
372
|
+
|
|
373
|
+
// The recruiting surface refuses them — the reach opt-out, working.
|
|
374
|
+
assert.equal(await getRecruiterSliver(PRIVOUT, { pool }), null,
|
|
375
|
+
'a private account with no recruiting opt-in must stay invisible to the DIRECTORY');
|
|
376
|
+
|
|
377
|
+
// The application does not. "Applying is an explicit act" (D7).
|
|
378
|
+
const v = await view(PRIVOUT);
|
|
379
|
+
assert.ok(v, 'a private builder who deliberately applied must not show their reviewer nothing — that is C4 satisfied by an empty box');
|
|
380
|
+
assert.equal(v.view, VIEW_SLIVER);
|
|
381
|
+
assert.deepEqual(Object.keys(v.sliver).sort(), [...SLIVER_KEYS].sort());
|
|
382
|
+
assert.equal(v.sliver.handle, 'privout-handle');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('the swap relaxes REACH only — provisional and terms-never-accepted still disclose NOTHING', async () => {
|
|
386
|
+
assert.equal(await view(PROV), null,
|
|
387
|
+
'a provisional account has no witnessed human behind it; applying cannot manufacture one');
|
|
388
|
+
assert.equal(await view(NOTERMS), null,
|
|
389
|
+
'terms_accepted_at is half the consent floor (ADR 0213) — applying is not accepting the platform terms');
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('an unknown account and a non-numeric principal are the SAME null as a refused one (ADR 0171 D4)', async () => {
|
|
393
|
+
assert.equal(await view('4119999'), null);
|
|
394
|
+
assert.equal(await view('system:bfg'), null);
|
|
395
|
+
assert.equal(await view(null), null);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// ===========================================================================
|
|
399
|
+
// 3. hide_stats beats the application, in both branches
|
|
400
|
+
// ===========================================================================
|
|
401
|
+
|
|
402
|
+
test('hide_stats: the PUBLIC branch keeps the name and drops the record', async () => {
|
|
403
|
+
const v = await view(PUBSHY);
|
|
404
|
+
assert.equal(v.view, VIEW_PUBLIC);
|
|
405
|
+
assert.equal(v.cross_project, null,
|
|
406
|
+
'applying is consent to be seen, never consent to be counted — and C2 says the hide takes effect on the NEXT render');
|
|
407
|
+
assert.equal(v.account.handle, 'pubshy-handle', 'hiding removes the numbers, not the person');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('hide_stats: the PRIVATE branch collapses to the name-only sliver', async () => {
|
|
411
|
+
const v = await view(PRIVSHY);
|
|
412
|
+
assert.equal(v.view, VIEW_SLIVER);
|
|
413
|
+
assert.deepEqual(Object.keys(v.sliver).sort(), [...SLIVER_KEYS_HIDDEN].sort(),
|
|
414
|
+
'a hidden private applicant must be name-only — the two rollup-derived keys are ABSENT, not null');
|
|
415
|
+
// CONTROL, so the absence above cannot pass vacuously: the same shape with
|
|
416
|
+
// hide_stats off really does carry them.
|
|
417
|
+
const shown = await view(PRIVIN);
|
|
418
|
+
assert.ok('disciplines' in shown.sliver && 'active_recently' in shown.sliver);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ===========================================================================
|
|
422
|
+
// 4. Nothing rides along
|
|
423
|
+
// ===========================================================================
|
|
424
|
+
|
|
425
|
+
test('no forbidden value appears ANYWHERE in a private applicant view’s body', async () => {
|
|
426
|
+
for (const id of [PRIVIN, PRIVOUT, PRIVSHY]) {
|
|
427
|
+
const body = JSON.stringify(await view(id));
|
|
428
|
+
const leaked = forbiddenOf(id).filter((s) => body.includes(s));
|
|
429
|
+
// The hidden account's own disciplines are forbidden too — it is name-only.
|
|
430
|
+
if (id === PRIVSHY) {
|
|
431
|
+
for (const d of ROLLUP[id].disciplines) {
|
|
432
|
+
if (body.includes(d)) leaked.push(d);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
assert.deepEqual(leaked, [], `${id} leaked ${leaked.join(', ')} into the sliver body`);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test('the ONE honest exception: github_id may ride the avatar URL and nothing else', async () => {
|
|
440
|
+
const v = await view(PRIVIN);
|
|
441
|
+
const body = JSON.stringify(v);
|
|
442
|
+
assert.ok(body.includes(PRIVIN), 'the avatar embeds the id — if this stops being true, delete the exception rather than the assertion');
|
|
443
|
+
const withoutAvatar = JSON.stringify({ ...v, sliver: { ...v.sliver, avatar_url: null } });
|
|
444
|
+
assert.ok(!withoutAvatar.includes(PRIVIN),
|
|
445
|
+
'the account id reached the body somewhere OTHER than the avatar URL — that is a new disclosure, not the documented limit');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// The authority-shaped field names, as a closed list. `rank_held` is excluded
|
|
449
|
+
// and pinned separately below — see the public-branch test.
|
|
450
|
+
const AUTHORITY_WORDS = ['rank', 'role', 'permission', 'permissions', 'membership', 'membership_kind', 'invited', 'admitted', 'status'];
|
|
451
|
+
|
|
452
|
+
test('it grants nothing (ADR 0016): the PRIVATE branch carries no authority-shaped field at all', async () => {
|
|
453
|
+
for (const id of [PRIVIN, PRIVOUT, PRIVSHY]) {
|
|
454
|
+
const body = JSON.stringify(await view(id)).toLowerCase();
|
|
455
|
+
for (const word of AUTHORITY_WORDS) {
|
|
456
|
+
assert.ok(!body.includes(`"${word}"`),
|
|
457
|
+
`the sliver applicant view carries a "${word}" field — this surface is descriptive, never authority`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('it grants nothing (ADR 0016): the PUBLIC branch carries exactly one rank-shaped field, and it is the public page’s own', async () => {
|
|
463
|
+
const v = await view(PUB);
|
|
464
|
+
// THE HONEST FACT, asserted rather than glossed. `rank_held` DOES appear —
|
|
465
|
+
// inside cross_project.projects[], because the canonical /u/<handle> payload
|
|
466
|
+
// publishes it there and D7's rule is that this view IS that payload. It is
|
|
467
|
+
// descriptive ("they held metic on that project"), it is already world-
|
|
468
|
+
// readable for a public account, and no admission path reads it. Pinning WHERE
|
|
469
|
+
// it may appear is the assertion; claiming it is absent would be false.
|
|
470
|
+
assert.ok(v.cross_project.projects.every((p) => 'rank_held' in p),
|
|
471
|
+
'the public payload lost rank_held — if the public page stopped publishing it, this view must stop too');
|
|
472
|
+
const withoutProjects = JSON.stringify({ ...v, cross_project: { ...v.cross_project, projects: [] } }).toLowerCase();
|
|
473
|
+
for (const word of AUTHORITY_WORDS) {
|
|
474
|
+
assert.ok(!withoutProjects.includes(`"${word}"`),
|
|
475
|
+
`an authority-shaped "${word}" field reached the public applicant view OUTSIDE the public page's own per-project record — that is a new disclosure, not the inherited one`);
|
|
476
|
+
}
|
|
477
|
+
// And nothing at the top level of either half grants anything.
|
|
478
|
+
assert.deepEqual(Object.keys(v).sort(), ['account', 'cross_project', 'view']);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// ===========================================================================
|
|
482
|
+
// 5. The gate — the whole security story of the federated route
|
|
483
|
+
// ===========================================================================
|
|
484
|
+
|
|
485
|
+
const moduleApiPath = require.resolve('../src/module-api.js');
|
|
486
|
+
require.cache[moduleApiPath] = {
|
|
487
|
+
id: moduleApiPath,
|
|
488
|
+
filename: moduleApiPath,
|
|
489
|
+
loaded: true,
|
|
490
|
+
exports: {
|
|
491
|
+
pool,
|
|
492
|
+
validateOrRespond: require('../src/bongos/routes/_helpers.js').validateOrRespond,
|
|
493
|
+
resolveSession: async () => null,
|
|
494
|
+
logger: () => ({ error: () => {}, info: () => {} }),
|
|
495
|
+
requireBuilder: (_req, res) => res.fail('unauthenticated', 401),
|
|
496
|
+
requireRank: () => (_req, res) => res.fail('rank_forbidden', 403),
|
|
497
|
+
requirePermission: () => (_req, res) => res.status(403).json({ ok: false, error: 'forbidden' }),
|
|
498
|
+
// The route factory registers this module's seam providers on build.
|
|
499
|
+
// Answering "already registered" skips that wiring: none of it is on the
|
|
500
|
+
// path under test, and standing it up would drag the seam registry in.
|
|
501
|
+
hasProvider: () => true,
|
|
502
|
+
registerProvider: () => {},
|
|
503
|
+
on: () => {},
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const buildSsoRoutes = require('../modules/platform-identity/routes/sso.js');
|
|
508
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
509
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
510
|
+
|
|
511
|
+
// The real secret check, over a real hash produced by the MODULE'S OWN hasher —
|
|
512
|
+
// doubling clientSecretValid, or hashing with a copy of its algorithm kept here,
|
|
513
|
+
// would prove nothing about the route that calls it.
|
|
514
|
+
const SECRET = 'alpha-secret-770801';
|
|
515
|
+
CLIENT.secret_hash = pi.sha256hex(SECRET);
|
|
516
|
+
|
|
517
|
+
const app = express();
|
|
518
|
+
app.use(express.json());
|
|
519
|
+
app.use(attachFail);
|
|
520
|
+
app.use('/api/bongos', buildSsoRoutes());
|
|
521
|
+
const server = app.listen(0, '127.0.0.1');
|
|
522
|
+
await new Promise((r) => server.once('listening', r));
|
|
523
|
+
const port = server.address().port;
|
|
524
|
+
after(() => server.close());
|
|
525
|
+
|
|
526
|
+
function post(path_, body) {
|
|
527
|
+
calls = [];
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
529
|
+
const payload = JSON.stringify(body);
|
|
530
|
+
const req = http.request({
|
|
531
|
+
host: '127.0.0.1', port, method: 'POST', path: path_,
|
|
532
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
533
|
+
}, (res) => {
|
|
534
|
+
const chunks = [];
|
|
535
|
+
res.on('data', (c) => chunks.push(c));
|
|
536
|
+
res.on('end', () => {
|
|
537
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
538
|
+
let json = null; try { json = JSON.parse(text); } catch { /* not JSON */ }
|
|
539
|
+
resolve({ status: res.statusCode, text, json, headers: res.headers });
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
req.on('error', reject);
|
|
543
|
+
req.end(payload);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const creds = { client_id: CLIENT.client_id, client_secret: SECRET };
|
|
548
|
+
const readAccounts = () => calls.filter((c) => /FROM platform_identity_accounts/.test(c.sql));
|
|
549
|
+
|
|
550
|
+
test('the echo gate: a project with NO application for this account gets 404 and reads NO account row', async () => {
|
|
551
|
+
ECHOES = [{ github_id: PUB, client_id: OTHER_CLIENT.client_id, in_window: true }];
|
|
552
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
553
|
+
assert.equal(r.status, 404, 'a 403 would confirm the account exists — the disclosure the rule withholds');
|
|
554
|
+
assert.equal(r.json.error.code, 'no_application');
|
|
555
|
+
assert.deepEqual(readAccounts(), [],
|
|
556
|
+
'the route read an account row before refusing — a gate that refuses AFTER reading has already done the disclosure work, and the next refactor that logs or caches that row leaks it');
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test('the echo gate: the witness is matched against the AUTHENTICATED client, not the body', async () => {
|
|
560
|
+
ECHOES = [{ github_id: PUB, client_id: OTHER_CLIENT.client_id, in_window: true }];
|
|
561
|
+
// Alpha's credentials, beta's client_id in the body: if the route trusted the
|
|
562
|
+
// body, alpha would inherit beta's applicants.
|
|
563
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, client_id: CLIENT.client_id, github_id: PUB });
|
|
564
|
+
assert.equal(r.status, 404);
|
|
565
|
+
const echoReads = calls.filter((c) => /application_echoes/.test(c.sql));
|
|
566
|
+
assert.equal(echoReads.length, 1);
|
|
567
|
+
assert.equal(echoReads[0].params[1], CLIENT.client_id,
|
|
568
|
+
'the gate must ask about the client that proved its secret');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('the echo gate: an echo OUTSIDE the 30-day window stops opening the read', async () => {
|
|
572
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: false }];
|
|
573
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
574
|
+
assert.equal(r.status, 404,
|
|
575
|
+
'when the platform stops vouching for the link between the row and the account, the profile must stop rendering');
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('a real applicant resolves, uncacheable, through the gate', async () => {
|
|
579
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true }];
|
|
580
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
581
|
+
assert.equal(r.status, 200);
|
|
582
|
+
assert.equal(r.json.profile.view, VIEW_PUBLIC);
|
|
583
|
+
assert.equal(r.headers['cache-control'], 'no-store',
|
|
584
|
+
'a cached applicant view is a snapshot, which is the one thing "live at review time" forbids');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test('bad credentials are refused before anything is read', async () => {
|
|
588
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true }];
|
|
589
|
+
const r = await post('/api/bongos/sso/applicant-profile', { client_id: CLIENT.client_id, client_secret: 'wrong', github_id: PUB });
|
|
590
|
+
assert.equal(r.status, 401);
|
|
591
|
+
assert.deepEqual(readAccounts(), []);
|
|
592
|
+
const echoReads = calls.filter((c) => /application_echoes/.test(c.sql));
|
|
593
|
+
assert.deepEqual(echoReads, [], 'even the witness read must wait until the caller has authenticated');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test('the route is rate-limited, and a refused probe costs no database work', async () => {
|
|
597
|
+
// DEFENCE IN DEPTH behind the echo gate (grade advisory on this task). The echo
|
|
598
|
+
// already closes the enumeration oracle; the ceiling bounds the cost of GUESSING
|
|
599
|
+
// for a caller holding a stolen secret or simply looping. Asserted on the
|
|
600
|
+
// statement log as well as the status, because a limiter that refuses AFTER
|
|
601
|
+
// querying has not saved the database anything.
|
|
602
|
+
ECHOES = [];
|
|
603
|
+
let limited = null;
|
|
604
|
+
for (let i = 0; i < 340; i++) {
|
|
605
|
+
const r = await post('/api/bongos/sso/applicant-profile', { ...creds, github_id: PUB });
|
|
606
|
+
if (r.status === 429) { limited = r; break; }
|
|
607
|
+
}
|
|
608
|
+
assert.ok(limited, 'the applicant read has no ceiling — a scripted walk of the github_id space is unbounded');
|
|
609
|
+
assert.equal(limited.json.error.code, 'rate_limited');
|
|
610
|
+
assert.equal(limited.headers['x-ratelimit-scope'], 'applicant-profile-read',
|
|
611
|
+
'the 429 must name its OWN budget — sharing the account-existence scope would let a busy project close the stranger admission check');
|
|
612
|
+
assert.ok(limited.headers['retry-after'], 'a refused caller is told when to come back');
|
|
613
|
+
assert.deepEqual(calls, [],
|
|
614
|
+
'the limiter must run BEFORE the client lookup — a refused probe that still queries has not saved the database anything');
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test('applicationBacksProfileRead is fail-closed on a non-numeric principal and an empty client', async () => {
|
|
618
|
+
ECHOES = [{ github_id: PUB, client_id: CLIENT.client_id, in_window: true }];
|
|
619
|
+
assert.equal(await applicationBacksProfileRead({ githubId: 'system:bfg', clientId: CLIENT.client_id }, { pool }), false);
|
|
620
|
+
assert.equal(await applicationBacksProfileRead({ githubId: PUB, clientId: '' }, { pool }), false);
|
|
621
|
+
assert.equal(await applicationBacksProfileRead({ githubId: PUB, clientId: CLIENT.client_id }, { pool }), true);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ===========================================================================
|
|
625
|
+
// 6. The lane check (learning 1000218 — a file in neither lane asserts nothing)
|
|
626
|
+
// ===========================================================================
|
|
627
|
+
|
|
628
|
+
test('this proof runs in a lane that actually executes', () => {
|
|
629
|
+
const fs = require('node:fs');
|
|
630
|
+
const path = require('node:path');
|
|
631
|
+
const self = 'applicant_profile_boundary.mjs';
|
|
632
|
+
const runners = fs.readdirSync(path.join(process.cwd(), 'tests'))
|
|
633
|
+
.filter((f) => /^run-|^unit|lane/i.test(f));
|
|
634
|
+
const listed = runners.some((f) => {
|
|
635
|
+
try { return fs.readFileSync(path.join(process.cwd(), 'tests', f), 'utf8').includes(self); } catch { return false; }
|
|
636
|
+
});
|
|
637
|
+
const globbed = fs.existsSync(path.join(process.cwd(), 'package.json'))
|
|
638
|
+
&& /tests\/\*|tests\/\*\*|\*\.mjs/.test(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
639
|
+
assert.ok(listed || globbed,
|
|
640
|
+
`${self} is in no runner list and no glob — it would assert nothing forever (learning 1000218)`);
|
|
641
|
+
});
|