@bongos/core 1.21.56 → 1.21.58

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.
Files changed (51) hide show
  1. package/.bongos-core.json +106 -46
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +6 -0
  4. package/clients/bongos-client/index.cjs +6 -0
  5. package/clients/bongos-client/index.d.ts +13 -2
  6. package/clients/bongos-client/index.mjs +6 -0
  7. package/docs/adr/0336-a-guilds-record-is-the-sum-of-what-its-members-already-show.md +2 -0
  8. package/docs/adr/0365-a-guild-engages-a-project-through-each-members-own-door.md +58 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +233 -6
  11. package/docs/api-reference.md +8 -5
  12. package/docs/copy-inventory.md +361 -315
  13. package/docs/copy-registry.json +868 -419
  14. package/docs/file-map.md +4 -0
  15. package/docs/module-api-changelog.md +4 -0
  16. package/docs/page-inventory.json +38 -4
  17. package/docs/page-readings.json +856 -771
  18. package/modules/platform-identity/application-echo.js +11 -5
  19. package/modules/platform-identity/guild-engagements.js +255 -0
  20. package/modules/platform-identity/guilds.js +3 -1
  21. package/modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql +64 -0
  22. package/modules/platform-identity/routes/guilds.js +35 -1
  23. package/modules/platform-identity/routes/my-projects.js +27 -3
  24. package/modules/platform-identity/routes/projects.js +44 -0
  25. package/modules/platform-identity/routes/sso.js +21 -3
  26. package/modules/public-landing/public/assets/cosmos.css +7 -0
  27. package/modules/public-landing/public/guild.html +633 -0
  28. package/modules/public-landing/public/guild.probes.json +63 -0
  29. package/modules/public-landing/public/guild.states.json +43 -0
  30. package/modules/public-landing/public/projects.html +252 -16
  31. package/modules/public-landing/public/projects.probes.json +26 -1
  32. package/modules/public-landing/public/projects.states.json +4 -0
  33. package/modules/ui-design/kit/fixtures/guilds.json +73 -0
  34. package/modules/ui-design/kit/fixtures/me__guild-engagements.json +7 -0
  35. package/modules/ui-design/kit/serve.js +99 -5
  36. package/package-lock.json +2 -2
  37. package/package.json +1 -1
  38. package/release-notes.json +12 -0
  39. package/scripts/gds/run-unit-tests.js +5 -0
  40. package/src/module-api.js +1 -1
  41. package/src/platform-server.js +13 -0
  42. package/tests/applicant_profile_boundary.mjs +104 -1
  43. package/tests/application_echo.mjs +91 -5
  44. package/tests/guild_engagement.mjs +881 -0
  45. package/tests/guild_engagement_db.mjs +240 -0
  46. package/tests/hub_engagement_cards.mjs +307 -0
  47. package/tests/hub_guild_page.mjs +624 -0
  48. package/tests/privacy_matrix.mjs +216 -14
  49. package/tests/project_invite_ui.mjs +187 -3
  50. package/tests/render_check.mjs +2 -2
  51. package/tests/ui_design_kit.mjs +3 -3
@@ -0,0 +1,881 @@
1
+ // tests/guild_engagement.mjs — guild engagements, DB-free (task 1002303, R27;
2
+ // ADR 0336 D7 as amended by ADR 0365).
3
+ //
4
+ // WHAT THIS PINS, each a rule rather than a behaviour:
5
+ //
6
+ // 1. THE STORED SHAPE — the engagement table has no filer and no status, the
7
+ // key is (guild, project, kind), the guild FK cascades, the echo's guild_id
8
+ // is SET NULL — and every statement the module issues agrees with it.
9
+ // 2. OWNER-ONLY FILING — a member is told plainly (403), a stranger gets the
10
+ // unlisted-safe 404, and the owner test lives INSIDE the INSERT: called
11
+ // straight at the domain, a non-owner files nothing.
12
+ // 3. NO ORACLE ON A PROJECT — unknown, inactive, private, stealth and dark-
13
+ // matter projects answer one byte-identical 404.
14
+ // 4. THE INBOX — only my guilds, inside 30 days, naming only projects on the
15
+ // map AND Public, an `invited` row only if I was a member when it was
16
+ // sent, and never a project I build on, hold a live invite to, or applied
17
+ // to inside the window. client_id never crosses.
18
+ // 5. A PROJECT INVITES A GUILD — the inviter gate is POST /projects/invite's,
19
+ // refusal and all; an unlisted guild is the unknown-guild 404; the links go
20
+ // to the PUBLIC roster only (shown_publicly AND accountVisibleSql); a
21
+ // private project's invite leaves no notice behind.
22
+ // 6. THE JOIN — a `guild` that no longer resolves is a 409 that relays
23
+ // NOTHING; one that does reaches the echo, and a later solo application
24
+ // clears it.
25
+ //
26
+ // MECHANISM (the tests/recruiter_sliver_boundary.mjs harness, generalised): an
27
+ // in-memory world whose row filters are built FROM EACH STATEMENT'S OWN WHERE
28
+ // TEXT. Each recognised term is CONSUMED as it is matched; whatever survives
29
+ // must be whitespace, parentheses and AND, and every placeholder must have been
30
+ // read — so a predicate this interpreter cannot read, or an AND turned OR, is a
31
+ // loud error rather than a silently different filter. Delete a term from the
32
+ // real statement and the matching rows genuinely come back. Above the pool
33
+ // everything is real: the routers, the domain, HTTP.
34
+ //
35
+ // Real-SQL semantics (the cascade, SET NULL, the upsert restarting the window)
36
+ // are proved against Postgres by tests/guild_engagement_db.mjs.
37
+ //
38
+ // Run: node tests/guild_engagement.mjs
39
+
40
+ import { strict as assert } from 'node:assert';
41
+ import { test, after, beforeEach } from 'node:test';
42
+ import { createRequire } from 'node:module';
43
+ import http from 'node:http';
44
+ import { readFileSync } from 'node:fs';
45
+ import { fileURLToPath } from 'node:url';
46
+ import { dirname, join } from 'node:path';
47
+
48
+ const require = createRequire(import.meta.url);
49
+ const express = require('express');
50
+
51
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
52
+ const read = (p) => readFileSync(join(ROOT, p), 'utf8').replace(/\r\n/g, '\n');
53
+ const MIGRATION = read('modules/platform-identity/migrations/platform_identity_030_guild_engagements.sql');
54
+ const ECHO_MIGRATION = read('modules/platform-identity/migrations/platform_identity_016_application_echo.sql');
55
+ const DOMAIN_SRC = read('modules/platform-identity/guild-engagements.js');
56
+ const ECHO_SRC = read('modules/platform-identity/application-echo.js');
57
+
58
+ const DAY = 24 * 60 * 60 * 1000;
59
+ const NOW = Date.now();
60
+ const ago = (days) => NOW - days * DAY;
61
+
62
+ // ===========================================================================
63
+ // The world
64
+ // ===========================================================================
65
+
66
+ const OWNER = '5001'; // owns guild-a and quiet-guild
67
+ const MEMBER = '5002'; // in guild-a and quiet-guild, from long ago
68
+ const LATE = '5003'; // joined guild-a AFTER proj-pub invited it
69
+ const PRIV = '5004'; // in guild-a, shown, but a PRIVATE account
70
+ const SHY = '5005'; // in guild-a, public account, NOT shown on the page
71
+ const PROV = '5006'; // in guild-a, provisional
72
+ const STRANGER = '5007'; // owns other-guild, in nothing else
73
+
74
+ const ACCOUNTS = [
75
+ [OWNER, 'gowner', 'Owner'], [MEMBER, 'gmember', 'Member'], [LATE, 'glate', 'Late'],
76
+ [PRIV, 'gpriv', 'Priv'], [SHY, 'gshy', 'Shy'], [PROV, 'gprov', 'Prov'], [STRANGER, 'gstranger', 'Stranger'],
77
+ ].map(([id, handle, name]) => ({
78
+ github_id: id, handle, github_login: `${handle}-login`, display_name: `${name} Display`,
79
+ profile_state: id === PROV ? 'provisional' : 'active',
80
+ terms_accepted_at: id === PROV ? null : 'T1',
81
+ account_visibility: id === PRIV ? 'private' : 'public',
82
+ hide_stats: false,
83
+ }));
84
+
85
+ const GUILDS = [
86
+ { id: '71', slug: 'guild-a', name: 'Guild A', visibility: 'public' },
87
+ { id: '72', slug: 'quiet-guild', name: 'Quiet Guild', visibility: 'unlisted' },
88
+ { id: '73', slug: 'other-guild', name: 'Other Guild', visibility: 'public' },
89
+ ];
90
+ const MEMBERS = [
91
+ { guild_id: '71', github_id: OWNER, membership_kind: 'owner', shown_publicly: true, joined_at: ago(200) },
92
+ { guild_id: '71', github_id: MEMBER, membership_kind: 'member', shown_publicly: true, joined_at: ago(100) },
93
+ { guild_id: '71', github_id: LATE, membership_kind: 'member', shown_publicly: true, joined_at: ago(2.5) },
94
+ { guild_id: '71', github_id: PRIV, membership_kind: 'member', shown_publicly: true, joined_at: ago(90) },
95
+ { guild_id: '71', github_id: SHY, membership_kind: 'member', shown_publicly: false, joined_at: ago(90) },
96
+ { guild_id: '71', github_id: PROV, membership_kind: 'member', shown_publicly: true, joined_at: ago(90) },
97
+ { guild_id: '72', github_id: OWNER, membership_kind: 'owner', shown_publicly: false, joined_at: ago(60) },
98
+ { guild_id: '72', github_id: MEMBER, membership_kind: 'member', shown_publicly: false, joined_at: ago(50) },
99
+ { guild_id: '73', github_id: STRANGER, membership_kind: 'owner', shown_publicly: true, joined_at: ago(30) },
100
+ ];
101
+
102
+ // One registered client per project; the client_id is a sentinel that must never
103
+ // reach a response body.
104
+ const onMap = { status: 'active', description: 'a real description', visibility_source: 'control-plane', visibility_reported_at: null };
105
+ const PROJECTS = [
106
+ ['pub', { visibility: 'public' }],
107
+ ['priv', { visibility: 'private' }],
108
+ ['stealth', { visibility: 'stealth' }],
109
+ ['dark', { visibility: 'public', description: null }],
110
+ ['unreported', { visibility: 'public', visibility_source: null }],
111
+ ['dead', { visibility: 'public' }, 'inactive'],
112
+ ['member', { visibility: 'public' }],
113
+ ['pending', { visibility: 'public' }],
114
+ ['lapsed', { visibility: 'public' }],
115
+ ['echoed', { visibility: 'public' }],
116
+ ['stale-echo', { visibility: 'public' }],
117
+ ['old', { visibility: 'public' }],
118
+ ];
119
+ const CLIENTS = PROJECTS.map(([k, , clientStatus], i) => ({
120
+ id: i + 1, client_id: `cid-${k}-SENTINEL`, name: `Client ${k}`, secret_hash: '', redirect_uris: [],
121
+ origin: `https://${k}.example`, owner_github_id: null, status: clientStatus || 'active',
122
+ }));
123
+ const PROJECT_ROWS = PROJECTS.map(([k, over]) => ({ origin: `https://${k}.example`, name: `Project ${k}`, ...onMap, ...over }));
124
+ const cid = (k) => `cid-${k}-SENTINEL`;
125
+
126
+ let ENGAGEMENTS = [];
127
+ let PROJECT_MEMBERSHIPS = [];
128
+ let ECHOES = [];
129
+ function seed() {
130
+ ENGAGEMENTS = [
131
+ { guild_id: '72', client_id: cid('pub'), kind: 'apply', created_at: ago(1) },
132
+ { guild_id: '71', client_id: cid('pub'), kind: 'apply', created_at: ago(2) },
133
+ { guild_id: '71', client_id: cid('pub'), kind: 'invited', created_at: ago(3) },
134
+ ...['priv', 'stealth', 'dark', 'unreported', 'dead', 'member', 'pending', 'echoed']
135
+ .map((k, i) => ({ guild_id: '71', client_id: cid(k), kind: 'apply', created_at: ago(4 + i * 0.1) })),
136
+ { guild_id: '71', client_id: cid('priv'), kind: 'invited', created_at: ago(4.5) },
137
+ { guild_id: '71', client_id: cid('lapsed'), kind: 'apply', created_at: ago(5) },
138
+ { guild_id: '71', client_id: cid('stale-echo'), kind: 'apply', created_at: ago(6) },
139
+ { guild_id: '71', client_id: cid('old'), kind: 'apply', created_at: ago(31) },
140
+ { guild_id: '73', client_id: cid('pub'), kind: 'apply', created_at: ago(1.5) },
141
+ ];
142
+ PROJECT_MEMBERSHIPS = [
143
+ { client_id: cid('member'), github_id: MEMBER, membership_kind: 'member', joined_at: ago(10) },
144
+ { client_id: cid('pending'), github_id: MEMBER, membership_kind: 'pending', joined_at: ago(5) },
145
+ { client_id: cid('lapsed'), github_id: MEMBER, membership_kind: 'pending', joined_at: ago(40) },
146
+ ];
147
+ ECHOES = [
148
+ { github_id: MEMBER, client_id: cid('echoed'), applied_at: ago(2), guild_id: null },
149
+ { github_id: MEMBER, client_id: cid('stale-echo'), applied_at: ago(40), guild_id: null },
150
+ ];
151
+ }
152
+
153
+ // ===========================================================================
154
+ // The interpreter
155
+ // ===========================================================================
156
+
157
+ let calls = [];
158
+ let PARAMS = [];
159
+ const P = (n) => PARAMS[Number(n) - 1];
160
+ const norm = (s) => String(s).replace(/\s+/g, ' ').replace(/\( /g, '(').replace(/ \)/g, ')').trim();
161
+
162
+ /**
163
+ * A row test built from a WHERE text: each [regex, make] term is consumed when
164
+ * it matches, then the residue must be pure conjunction and every placeholder
165
+ * in the text must have been inside a consumed term.
166
+ */
167
+ function interpret(where, terms, label) {
168
+ let residue = where;
169
+ const consumed = new Set();
170
+ const tests = [];
171
+ for (const [re, make] of terms) {
172
+ const m = re.exec(residue);
173
+ if (!m) continue;
174
+ residue = residue.slice(0, m.index) + residue.slice(m.index + m[0].length);
175
+ for (const ph of m[0].matchAll(/\$(\d+)/g)) consumed.add(ph[1]);
176
+ tests.push(make(m));
177
+ }
178
+ for (const ph of where.matchAll(/\$(\d+)/g)) {
179
+ if (!consumed.has(ph[1])) throw new Error(`fake pool (${label}): unread predicate on $${ph[1]} in "${where}"`);
180
+ }
181
+ if (!/^[\s()]*(?:AND[\s()]*)*$/.test(residue)) {
182
+ throw new Error(`fake pool (${label}): unmodelled boolean structure "${residue.trim()}" in "${where}"`);
183
+ }
184
+ return (r) => tests.every((fn) => fn(r));
185
+ }
186
+
187
+ // The naming rule's terms, on whatever the row calls its project.
188
+ const naming = (get) => [
189
+ [/\bp\.status = 'active'/, () => (r) => get(r).status === 'active'],
190
+ [/\bbtrim\(coalesce\(p\.name, ''\)\) <> ''/, () => (r) => String(get(r).name || '').trim() !== ''],
191
+ [/\bbtrim\(coalesce\(p\.description, ''\)\) <> ''/, () => (r) => String(get(r).description || '').trim() !== ''],
192
+ [/\(p\.visibility_source = 'control-plane' OR p\.visibility_reported_at IS NOT NULL\)/,
193
+ () => (r) => get(r).visibility_source === 'control-plane' || get(r).visibility_reported_at != null],
194
+ [/\bp\.visibility = 'public'/, () => (r) => get(r).visibility === 'public'],
195
+ ];
196
+
197
+ const ENG_FROM = 'platform_identity_guild_members m JOIN platform_identity_guilds g ON g.id = m.guild_id '
198
+ + 'JOIN platform_identity_guild_engagements e ON e.guild_id = m.guild_id '
199
+ + 'JOIN platform_identity_sso_clients c ON c.client_id = e.client_id '
200
+ + 'JOIN platform_identity_projects p ON p.origin = c.origin';
201
+ const ENG_TERMS = [
202
+ [/NOT EXISTS \(SELECT 1 FROM platform_identity_project_memberships pm WHERE pm\.client_id = e\.client_id AND pm\.github_id = \$(\d+)::bigint AND \(pm\.membership_kind <> 'pending' OR pm\.joined_at > now\(\) - make_interval\(days => \$(\d+)::int\)\)\)/,
203
+ (m) => {
204
+ const who = String(P(m[1])); const days = Number(P(m[2]));
205
+ return (r) => !PROJECT_MEMBERSHIPS.some((pm) => pm.client_id === r.e.client_id && pm.github_id === who
206
+ && (pm.membership_kind !== 'pending' || pm.joined_at > Date.now() - days * DAY));
207
+ }],
208
+ [/NOT EXISTS \(SELECT 1 FROM platform_identity_application_echoes ae WHERE ae\.github_id = \$(\d+)::bigint AND ae\.client_id = e\.client_id AND ae\.applied_at > now\(\) - make_interval\(days => \$(\d+)::int\)\)/,
209
+ (m) => {
210
+ const who = String(P(m[1])); const days = Number(P(m[2]));
211
+ return (r) => !ECHOES.some((ae) => ae.github_id === who && ae.client_id === r.e.client_id && ae.applied_at > Date.now() - days * DAY);
212
+ }],
213
+ [/\bm\.github_id = \$(\d+)::bigint/, (m) => { const v = String(P(m[1])); return (r) => r.m.github_id === v; }],
214
+ [/\be\.created_at > now\(\) - make_interval\(days => \$(\d+)::int\)/, (m) => { const d = Number(P(m[1])); return (r) => r.e.created_at > Date.now() - d * DAY; }],
215
+ [/\bc\.status = 'active'/, () => (r) => r.c.status === 'active'],
216
+ ...naming((r) => r.p),
217
+ [/\(e\.kind = 'apply' OR m\.joined_at <= e\.created_at\)/, () => (r) => r.e.kind === 'apply' || r.m.joined_at <= r.e.created_at],
218
+ [/\blower\(g\.slug\) = \$(\d+)/, (m) => { const v = String(P(m[1])); return (r) => r.g.slug.toLowerCase() === v; }],
219
+ [/\be\.client_id = \$(\d+)/, (m) => { const v = String(P(m[1])); return (r) => r.e.client_id === v; }],
220
+ ];
221
+ const COLUMN = {
222
+ 'g.slug': (r) => r.g.slug, 'g.name': (r) => r.g.name, 'g.id': (r) => r.g.id,
223
+ 'e.kind': (r) => r.e.kind, 'e.created_at': (r) => new Date(r.e.created_at).toISOString(),
224
+ 'p.name': (r) => r.p.name, 'p.origin': (r) => r.p.origin,
225
+ };
226
+ function project(selectList, r) {
227
+ // Every row also carries columns the statement never selected, so a response
228
+ // that spreads a row instead of whitelisting it leaks a sentinel.
229
+ const out = { client_id: r.e.client_id, guild_id: r.g.id, visibility: r.g.visibility };
230
+ for (const part of selectList.split(', ')) {
231
+ const [expr, alias] = part.split(' AS ');
232
+ if (!COLUMN[expr]) throw new Error(`fake pool: unknown column ${expr}`);
233
+ out[alias || expr.split('.')[1]] = COLUMN[expr](r);
234
+ }
235
+ return out;
236
+ }
237
+
238
+ const ACCT_TERMS = [
239
+ [/\bpa\.profile_state = 'active'/, () => (r) => r.a.profile_state === 'active'],
240
+ [/\bpa\.terms_accepted_at IS NOT NULL/, () => (r) => r.a.terms_accepted_at != null],
241
+ [/\bpa\.account_visibility = 'public'/, () => (r) => r.a.account_visibility === 'public'],
242
+ ];
243
+
244
+ function upsertEngagement(guildId, clientId, kind) {
245
+ const hit = ENGAGEMENTS.find((e) => e.guild_id === String(guildId) && e.client_id === clientId && e.kind === kind);
246
+ if (hit) hit.created_at = Date.now();
247
+ else ENGAGEMENTS.push({ guild_id: String(guildId), client_id: clientId, kind, created_at: Date.now() });
248
+ return ENGAGEMENTS.find((e) => e.guild_id === String(guildId) && e.client_id === clientId && e.kind === kind);
249
+ }
250
+ const projectOf = (c) => PROJECT_ROWS.find((p) => p.origin === c.origin);
251
+
252
+ let failNext = null;
253
+ const pool = {
254
+ query: async (text, params = []) => {
255
+ const sql = norm(text);
256
+ calls.push({ sql, params });
257
+ PARAMS = params;
258
+ if (failNext && failNext.test(sql)) { failNext = null; throw new Error('the table is on fire'); }
259
+ let m;
260
+
261
+ // getAccount — the acting account.
262
+ if (/^SELECT .* FROM platform_identity_accounts WHERE github_id = \$1$/.test(sql)) {
263
+ return { rows: ACCOUNTS.filter((a) => a.github_id === String(params[0])) };
264
+ }
265
+ // getMembership — the caller's own row in the guild named by slug.
266
+ if ((m = /^SELECT g\.id, g\.slug, .* FROM platform_identity_guilds g JOIN platform_identity_guild_members m ON m\.guild_id = g\.id AND m\.github_id = \$2::bigint WHERE lower\(g\.slug\) = \$1$/.exec(sql))) {
267
+ const g = GUILDS.find((x) => x.slug === String(params[0]));
268
+ const mm = g && MEMBERS.find((x) => x.guild_id === g.id && x.github_id === String(params[1]));
269
+ return { rows: mm ? [{ ...g, ...mm, description: null, counted_in_totals: true }] : [] };
270
+ }
271
+ // getClient / getClientByOrigin.
272
+ if (/FROM platform_identity_sso_clients WHERE client_id = \$1$/.test(sql)) {
273
+ return { rows: CLIENTS.filter((c) => c.client_id === params[0]) };
274
+ }
275
+ if (/FROM platform_identity_sso_clients WHERE origin = \$1 AND status = 'active' ORDER BY id ASC LIMIT 1$/.test(sql)) {
276
+ return { rows: CLIENTS.filter((c) => c.origin === params[0] && c.status === 'active').slice(0, 1) };
277
+ }
278
+
279
+ // THE MEMBER-FACING READS (the inbox and the join's check): one shared shape.
280
+ if ((m = /^SELECT (.+?) FROM (platform_identity_guild_members m JOIN .+?) WHERE (.+?)(?: ORDER BY (.+?))? LIMIT (\$\d+|\d+)$/.exec(sql))
281
+ && /platform_identity_guild_engagements/.test(sql)) {
282
+ if (m[2] !== ENG_FROM) throw new Error(`fake pool: the engagement join moved — ${m[2]}`);
283
+ const keep = interpret(m[3], ENG_TERMS, 'engagements');
284
+ const joined = [];
285
+ for (const mm of MEMBERS) {
286
+ const g = GUILDS.find((x) => x.id === mm.guild_id);
287
+ for (const e of ENGAGEMENTS.filter((x) => x.guild_id === mm.guild_id)) {
288
+ const c = CLIENTS.find((x) => x.client_id === e.client_id);
289
+ const p = c && projectOf(c);
290
+ if (g && c && p) joined.push({ m: mm, g, e, c, p });
291
+ }
292
+ }
293
+ let rows = joined.filter(keep);
294
+ if (m[4]) {
295
+ if (m[4] !== 'e.created_at DESC, g.slug, e.kind') throw new Error(`fake pool: the inbox order moved — ${m[4]}`);
296
+ rows.sort((a, b) => (b.e.created_at - a.e.created_at) || a.g.slug.localeCompare(b.g.slug) || a.e.kind.localeCompare(b.e.kind));
297
+ }
298
+ const limit = /^\$/.test(m[5]) ? Number(P(m[5].slice(1))) : Number(m[5]);
299
+ rows = rows.slice(0, limit);
300
+ return { rows: rows.map((r) => project(m[1], r)) };
301
+ }
302
+
303
+ // fileEngagement — target CTE + the owner-gated upsert, ONE statement.
304
+ if ((m = /^WITH target AS \(SELECT c\.client_id, p\.name, p\.origin FROM platform_identity_sso_clients c JOIN platform_identity_projects p ON p\.origin = c\.origin WHERE (.+?)\), filed AS \(INSERT INTO platform_identity_guild_engagements AS e \(guild_id, client_id, kind\) SELECT g\.id, target\.client_id, 'apply' FROM platform_identity_guilds g CROSS JOIN target WHERE (.+?) ON CONFLICT \(guild_id, client_id, kind\) DO UPDATE SET created_at = now\(\) RETURNING e\.guild_id, e\.kind, e\.created_at\) SELECT g\.slug, g\.name AS guild_name, filed\.kind, filed\.created_at, target\.name AS project_name, target\.origin AS project_origin FROM filed JOIN platform_identity_guilds g ON g\.id = filed\.guild_id CROSS JOIN target$/.exec(sql))) {
305
+ const target = interpret(m[1], [
306
+ [/c\.client_id = \(SELECT c1\.client_id FROM platform_identity_sso_clients c1 WHERE (.+?) ORDER BY c1\.id ASC LIMIT 1\)/, (mm) => {
307
+ const inner = interpret(mm[1], [
308
+ [/\bc1\.origin = \$(\d+)/, (x) => { const v = P(x[1]); return (c) => c.origin === v; }],
309
+ [/\bc1\.status = 'active'/, () => (c) => c.status === 'active'],
310
+ ], 'target client');
311
+ const chosen = CLIENTS.filter(inner).sort((a, b) => a.id - b.id)[0];
312
+ return (r) => !!chosen && r.c.client_id === chosen.client_id;
313
+ }],
314
+ ...naming((r) => r.p),
315
+ ], 'filing target');
316
+ const gate = interpret(m[2], [
317
+ [/EXISTS \(SELECT 1 FROM platform_identity_guild_members o WHERE o\.guild_id = g\.id AND o\.github_id = \$(\d+)::bigint AND o\.membership_kind = 'owner'\)/, (x) => {
318
+ const who = String(P(x[1]));
319
+ return (r) => MEMBERS.some((o) => o.guild_id === r.g.id && o.github_id === who && o.membership_kind === 'owner');
320
+ }],
321
+ [/\bg\.id = \$(\d+)/, (x) => { const v = String(P(x[1])); return (r) => r.g.id === v; }],
322
+ ], 'filing gate');
323
+ const targets = CLIENTS.map((c) => ({ c, p: projectOf(c) })).filter((r) => r.p && target(r));
324
+ const rows = [];
325
+ for (const g of GUILDS) {
326
+ for (const t of targets) {
327
+ if (!gate({ g, t })) continue;
328
+ const e = upsertEngagement(g.id, t.c.client_id, 'apply');
329
+ rows.push({ slug: g.slug, guild_name: g.name, kind: e.kind, created_at: new Date(e.created_at).toISOString(),
330
+ project_name: t.p.name, project_origin: t.p.origin, client_id: t.c.client_id });
331
+ }
332
+ }
333
+ return { rows };
334
+ }
335
+
336
+ // inviteGuild — the guild, by slug, PUBLIC only.
337
+ if ((m = /^SELECT id, slug, name FROM platform_identity_guilds WHERE (.+)$/.exec(sql))) {
338
+ const keep = interpret(m[1], [
339
+ [/\blower\(slug\) = \$(\d+)/, (x) => { const v = String(P(x[1])); return (g) => g.slug === v; }],
340
+ [/\bvisibility = 'public'/, () => (g) => g.visibility === 'public'],
341
+ ], 'invite guild');
342
+ return { rows: GUILDS.filter(keep).map(({ visibility, ...g }) => g) };
343
+ }
344
+ // ... the `invited` write, naming-gated.
345
+ if ((m = /^INSERT INTO platform_identity_guild_engagements AS e \(guild_id, client_id, kind\) SELECT \$1::bigint, c\.client_id, 'invited' FROM platform_identity_sso_clients c JOIN platform_identity_projects p ON p\.origin = c\.origin WHERE (.+?) ON CONFLICT \(guild_id, client_id, kind\) DO UPDATE SET created_at = now\(\)$/.exec(sql))) {
346
+ const keep = interpret(m[1], [
347
+ [/EXISTS \(SELECT 1 FROM platform_identity_guilds pg WHERE pg\.id = \$(\d+) AND pg\.visibility = 'public'\)/, (x) => {
348
+ const v = String(P(x[1])); return () => GUILDS.some((g) => g.id === v && g.visibility === 'public');
349
+ }],
350
+ [/\bc\.client_id = \$(\d+)/, (x) => { const v = P(x[1]); return (r) => r.c.client_id === v; }],
351
+ [/\bc\.status = 'active'/, () => (r) => r.c.status === 'active'],
352
+ ...naming((r) => r.p),
353
+ ], 'invited write');
354
+ const hits = CLIENTS.map((c) => ({ c, p: projectOf(c) })).filter((r) => r.p && keep(r));
355
+ for (const r of hits) upsertEngagement(params[0], r.c.client_id, 'invited');
356
+ return { rows: [], rowCount: hits.length };
357
+ }
358
+ // ... the public roster the links go to.
359
+ if ((m = /^SELECT pa\.handle, pa\.display_name, pa\.github_login FROM platform_identity_guild_members m JOIN platform_identity_accounts pa ON pa\.github_id = m\.github_id WHERE (.+?) ORDER BY \(m\.membership_kind = 'owner'\) DESC, lower\(pa\.handle\) LIMIT \$(\d+)$/.exec(sql))) {
360
+ const keep = interpret(m[1], [
361
+ [/\bm\.guild_id = \$(\d+)/, (x) => { const v = String(P(x[1])); return (r) => r.m.guild_id === v; }],
362
+ [/\bm\.shown_publicly\b/, () => (r) => r.m.shown_publicly === true],
363
+ ...ACCT_TERMS,
364
+ ], 'invite roster');
365
+ const rows = MEMBERS.map((mm) => ({ m: mm, a: ACCOUNTS.find((a) => a.github_id === mm.github_id) })).filter(keep)
366
+ .sort((x, y) => (Number(y.m.membership_kind === 'owner') - Number(x.m.membership_kind === 'owner')) || x.a.handle.localeCompare(y.a.handle))
367
+ .slice(0, Number(P(m[2])))
368
+ .map((r) => ({ handle: r.a.handle, display_name: r.a.display_name, github_login: r.a.github_login, github_id: r.a.github_id }));
369
+ return { rows };
370
+ }
371
+
372
+ // recordApplication — the echo upsert; the SET clause is READ, not assumed.
373
+ if ((m = /^INSERT INTO platform_identity_application_echoes \(github_id, client_id, applied_at, guild_id\) VALUES \(\$1::bigint, \$2, now\(\), \$3::bigint\) ON CONFLICT \(github_id, client_id\) DO UPDATE SET (.+)$/.exec(sql))) {
374
+ const sets = m[1].split(', ');
375
+ for (const s of sets) {
376
+ if (!['applied_at = now()', 'guild_id = EXCLUDED.guild_id'].includes(s)) throw new Error(`fake pool: unmodelled echo SET ${s}`);
377
+ }
378
+ const [githubId, clientId, guildId] = params;
379
+ const hit = ECHOES.find((e) => e.github_id === String(githubId) && e.client_id === clientId);
380
+ if (!hit) ECHOES.push({ github_id: String(githubId), client_id: clientId, applied_at: Date.now(), guild_id: guildId });
381
+ else {
382
+ if (sets.includes('applied_at = now()')) hit.applied_at = Date.now();
383
+ if (sets.includes('guild_id = EXCLUDED.guild_id')) hit.guild_id = guildId;
384
+ }
385
+ return { rows: [] };
386
+ }
387
+
388
+ // echoGuildLabel — the membership requirement is a JOIN, so the join is read too.
389
+ if ((m = /^SELECT g\.slug, g\.name, g\.visibility FROM platform_identity_application_echoes ae (.+?) WHERE (.+?) LIMIT 1$/.exec(sql))) {
390
+ const joins = m[1];
391
+ if (!/^JOIN platform_identity_guilds g ON g\.id = ae\.guild_id\b/.test(joins)) throw new Error(`fake pool: the label join moved — ${joins}`);
392
+ const rest = joins.replace(/^JOIN platform_identity_guilds g ON g\.id = ae\.guild_id/, '').trim();
393
+ const mustBeMember = rest === 'JOIN platform_identity_guild_members m ON m.guild_id = ae.guild_id AND m.github_id = ae.github_id';
394
+ if (rest && !mustBeMember) throw new Error(`fake pool: unmodelled label join — ${rest}`);
395
+ const keep = interpret(m[2], [
396
+ [/\bae\.github_id = \$(\d+)::bigint/, (x) => { const v = String(P(x[1])); return (e) => e.github_id === v; }],
397
+ [/\bae\.client_id = \$(\d+)/, (x) => { const v = P(x[1]); return (e) => e.client_id === v; }],
398
+ [/\bae\.applied_at > now\(\) - make_interval\(days => \$(\d+)::int\)/, (x) => { const d = Number(P(x[1])); return (e) => e.applied_at > Date.now() - d * DAY; }],
399
+ ], 'echo label');
400
+ const rows = ECHOES.filter(keep)
401
+ .filter((e) => e.guild_id != null)
402
+ .filter((e) => !mustBeMember || MEMBERS.some((mm) => mm.guild_id === String(e.guild_id) && mm.github_id === e.github_id))
403
+ .map((e) => GUILDS.find((g) => g.id === String(e.guild_id)))
404
+ .filter(Boolean)
405
+ .map((g) => ({ slug: g.slug, name: g.name, visibility: g.visibility, id: g.id }));
406
+ return { rows: rows.slice(0, 1) };
407
+ }
408
+
409
+ throw new Error(`fake pool: unsupported statement — ${sql.slice(0, 160)}`);
410
+ },
411
+ };
412
+
413
+ // ===========================================================================
414
+ // The routers, over the real domain, with the doorway pinned
415
+ // ===========================================================================
416
+
417
+ const api = require('../src/module-api.js');
418
+ let session = { id: 1, github_id: OWNER, github_login: 'gowner-login' };
419
+ api.pool = pool;
420
+ api.logger = () => ({ error() {}, warn() {}, info() {}, debug() {} });
421
+ api.requireBuilder = (req, _res, next) => { req.builder = { ...session }; next(); };
422
+ // The inviter gate's rank door, as project_invite_ownership.mjs stands it in: a
423
+ // holder passes, anyone else gets the route's one refusal.
424
+ api.requirePermission = (...required) => (req, res, next) => {
425
+ if (req.builder && req.builder.curate) return next();
426
+ return res.fail('permission_forbidden', {
427
+ status: 403, message: `missing permission: ${required.join(', ')} (floor: metic)`,
428
+ details: { required, held: [], floors: { [required[0]]: 'metic' } },
429
+ });
430
+ };
431
+ // Nobody owns a project through provisioning here, and no publish-identity port
432
+ // exists: the join relays, and the inviter gate falls to the rank door.
433
+ const realResolveOptional = api.resolveOptional;
434
+ api.resolveOptional = (name) => (name === 'provisioning.projectOwnedBy' || name === 'provisioning.projectPublishIdentity'
435
+ ? null : realResolveOptional(name));
436
+
437
+ const engagements = require('../modules/platform-identity/guild-engagements.js');
438
+ const guilds = require('../modules/platform-identity/guilds.js');
439
+ const { hallInviteUrl } = require('../modules/platform-identity/invite-notices.js');
440
+ const { APPLICATION_ECHO_WINDOW_DAYS } = require('../modules/platform-identity/application-echo.js');
441
+ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
442
+
443
+ // The project's own admission surface, scripted; `relayed` records every relay.
444
+ let relayed = [];
445
+ let relayAnswer = { status: 201, body: { ok: true } };
446
+ const realFetch = globalThis.fetch;
447
+ globalThis.fetch = async (url, init) => {
448
+ if (String(url).endsWith('/api/bongos/access-requests')) {
449
+ relayed.push({ url: String(url), body: JSON.parse(init.body) });
450
+ return { status: relayAnswer.status, json: async () => relayAnswer.body };
451
+ }
452
+ throw new Error(`unexpected fetch: ${url}`);
453
+ };
454
+
455
+ const app = express();
456
+ app.use(express.json());
457
+ app.use(attachFail);
458
+ app.use('/api/bongos', require('../modules/platform-identity/routes/guilds.js')());
459
+ app.use('/api/bongos', require('../modules/platform-identity/routes/projects.js')());
460
+ app.use('/api/bongos', require('../modules/platform-identity/routes/my-projects.js')());
461
+ const server = app.listen(0, '127.0.0.1');
462
+ await new Promise((r) => server.once('listening', r));
463
+ const { port } = server.address();
464
+ after(() => { server.close(); globalThis.fetch = realFetch; });
465
+
466
+ function call(method, path, body) {
467
+ return new Promise((resolve, reject) => {
468
+ const payload = body === undefined ? null : Buffer.from(JSON.stringify(body));
469
+ const req = http.request({
470
+ host: '127.0.0.1', port, method, path,
471
+ headers: payload ? { 'content-type': 'application/json', 'content-length': payload.length } : {},
472
+ }, (res) => {
473
+ const chunks = [];
474
+ res.on('data', (c) => chunks.push(c));
475
+ res.on('end', () => {
476
+ const text = Buffer.concat(chunks).toString('utf8');
477
+ let json = null; try { json = JSON.parse(text); } catch { /* empty */ }
478
+ resolve({ status: res.statusCode, json, text, headers: res.headers });
479
+ });
480
+ });
481
+ req.on('error', reject);
482
+ req.end(payload);
483
+ });
484
+ }
485
+ const as = (githubId, extra = {}) => { session = { id: Number(githubId), github_id: githubId, github_login: `${githubId}-login`, ...extra }; };
486
+ const engagementWrites = () => calls.filter((c) => /INSERT INTO platform_identity_guild_engagements/.test(c.sql));
487
+
488
+ beforeEach(() => { seed(); calls = []; relayed = []; relayAnswer = { status: 201, body: { ok: true } }; as(OWNER); });
489
+
490
+ // ===========================================================================
491
+ // 0. The instrument is calibrated before it is trusted
492
+ // ===========================================================================
493
+
494
+ test('the interpreter refuses what it cannot read: an unread placeholder, an unknown term, an AND turned OR', () => {
495
+ assert.throws(() => interpret('m.github_id = $1::bigint AND x = $2', ENG_TERMS, 't'), /unread predicate on \$2/);
496
+ assert.throws(() => interpret("m.github_id = $1::bigint AND g.visibility = 'public'", ENG_TERMS, 't'), /unmodelled boolean structure/);
497
+ assert.throws(() => interpret("m.github_id = $1::bigint OR c.status = 'active'", ENG_TERMS, 't'), /unmodelled boolean structure/,
498
+ 'an OR must never be read as the conjunction it replaced');
499
+ });
500
+
501
+ // ===========================================================================
502
+ // 1. The stored shape, and every statement agreeing with it
503
+ // ===========================================================================
504
+
505
+ const TABLE = MIGRATION.match(/CREATE TABLE IF NOT EXISTS platform_identity_guild_engagements \(([\s\S]*?)\n\);/)[1];
506
+ const TABLE_COLUMNS = [...TABLE.matchAll(/^\s+([a-z_]+)\s+(?:bigint|text|timestamptz)/gm)].map((x) => x[1]);
507
+
508
+ test('migration 030: the engagement table is (guild_id, client_id, kind, created_at) keyed on all three, no filer, no status', () => {
509
+ assert.deepEqual(TABLE_COLUMNS, ['guild_id', 'client_id', 'kind', 'created_at']);
510
+ assert.match(TABLE, /guild_id\s+bigint NOT NULL REFERENCES platform_identity_guilds\(id\) ON DELETE CASCADE,/,
511
+ 'a dissolved guild takes its engagements with it (the ADR 0336 D2 no-tombstone reason)');
512
+ assert.match(TABLE, /kind\s+text NOT NULL CHECK \(kind IN \('apply', 'invited'\)\),/);
513
+ assert.match(TABLE, /PRIMARY KEY \(guild_id, client_id, kind\)/);
514
+ assert.doesNotMatch(TABLE, /\bstatus\b/, 'no status column: the hub holds no admission fact about someone else\'s project (ADR 0201 D1.3)');
515
+ assert.doesNotMatch(TABLE, /filed_by|filer|created_by|github_id/, 'no filer column, as the guild row keeps no creator');
516
+ assert.match(MIGRATION, /CREATE INDEX IF NOT EXISTS platform_identity_guild_engagements_client\s+ON platform_identity_guild_engagements \(client_id\);/);
517
+ });
518
+
519
+ test('migration 030: the echo gains a nullable guild_id that is SET NULL on dissolve; additive and idempotent', () => {
520
+ assert.match(MIGRATION, /ALTER TABLE platform_identity_application_echoes\s+ADD COLUMN IF NOT EXISTS guild_id bigint REFERENCES platform_identity_guilds\(id\) ON DELETE SET NULL;/);
521
+ const code = MIGRATION.replace(/^--.*$/gm, '');
522
+ assert.match(code, /^\s*BEGIN;[\s\S]*COMMIT;\s*$/, 'one transaction');
523
+ assert.doesNotMatch(code, /\bDROP\b|\bRENAME\b|ALTER COLUMN/i, 'additive only (ADR 0083 §5)');
524
+ assert.doesNotMatch(code, /\brole\b/, 'ADR 0174 reserves `role` for the crafts');
525
+ assert.ok(/COMMENT ON TABLE platform_identity_guild_engagements IS[\s\S]*No filer column[\s\S]*No status column[\s\S]*30-day window/.test(MIGRATION),
526
+ 'the table comment states the three rules: no filer, no status, window read-time');
527
+ });
528
+
529
+ test('every engagement statement agrees with the migration: columns, conflict target, and kinds', () => {
530
+ const inserts = [...DOMAIN_SRC.matchAll(/INSERT INTO platform_identity_guild_engagements AS e \(([^)]*)\)[\s\S]*?ON CONFLICT \(([^)]*)\) DO UPDATE SET ([^\n`]*)/g)];
531
+ assert.equal(inserts.length, 2, 'two writers: the owner\'s apply and the project\'s invite');
532
+ for (const [, cols, conflict, set] of inserts) {
533
+ assert.deepEqual(cols.split(', '), ['guild_id', 'client_id', 'kind'], 'created_at comes from its default');
534
+ for (const c of cols.split(', ')) assert.ok(TABLE_COLUMNS.includes(c), `${c} is a real column`);
535
+ assert.equal(conflict, 'guild_id, client_id, kind', 'the conflict target IS the primary key');
536
+ assert.match(set.trim(), /^created_at = now\(\)/, 're-filing restarts the window and touches nothing else');
537
+ }
538
+ const kindsWritten = [...DOMAIN_SRC.matchAll(/target\.client_id, '(\w+)'|c\.client_id, '(\w+)'/g)].map((x) => x[1] || x[2]).sort();
539
+ assert.deepEqual(kindsWritten, ['apply', 'invited'], 'each writer writes exactly its own kind, both in the CHECK');
540
+ assert.equal(engagements.ENGAGEMENT_WINDOW_DAYS, 30, 'the 30-day read-time window (ADR 0365 D1)');
541
+ });
542
+
543
+ test('no statement reads or writes a status OF AN ENGAGEMENT or of the echo', () => {
544
+ const sqlOf = (src) => [...src.matchAll(/`([^`]*)`/g)].map((x) => x[1]).filter((s) => /platform_identity_/.test(s));
545
+ for (const s of sqlOf(DOMAIN_SRC)) {
546
+ assert.doesNotMatch(s, /\be\.status\b|\bae\.status\b/, 'an engagement and an echo have no status to read');
547
+ assert.doesNotMatch(s, /INSERT INTO platform_identity_guild_engagements[^(]*\([^)]*status/, 'nor one to write');
548
+ }
549
+ // The only statuses these statements read are the PROJECT's own catalog state —
550
+ // the naming rule (onMapSql) and the registration being live — never an outcome.
551
+ const statusReads = sqlOf(DOMAIN_SRC).flatMap((s) => [...s.matchAll(/\b(\w+)\.status\b/g)].map((x) => x[1]));
552
+ assert.ok(statusReads.every((a) => a === 'c' || a === 'c1'), `only the sso client's own status is spelled here: ${statusReads}`);
553
+ const echoWrite = ECHO_SRC.match(/INSERT INTO platform_identity_application_echoes \(([^)]*)\)[\s\S]*?ON CONFLICT \(([^)]*)\) DO UPDATE SET ([^`]*)`/);
554
+ assert.deepEqual(echoWrite[1].split(', '), ['github_id', 'client_id', 'applied_at', 'guild_id']);
555
+ for (const c of ['github_id', 'client_id', 'applied_at']) assert.match(ECHO_MIGRATION, new RegExp(`\\b${c}\\b`));
556
+ assert.equal(echoWrite[3].trim(), 'applied_at = now(), guild_id = EXCLUDED.guild_id',
557
+ 'the most recent application decides the label — a solo re-apply clears it (ADR 0365 D1)');
558
+ assert.doesNotMatch(echoWrite[0], /\bstatus\b/);
559
+ });
560
+
561
+ // ===========================================================================
562
+ // 2. Owner-only filing (D2)
563
+ // ===========================================================================
564
+
565
+ const fileApply = (slug, origin) => call('POST', `/api/bongos/guilds/${slug}/engagements`, { origin });
566
+
567
+ test('the owner files "this guild is applying to P"; the answer is exactly the contract shape', async () => {
568
+ const res = await fileApply('guild-a', 'https://pub.example/');
569
+ assert.equal(res.status, 200);
570
+ assert.deepEqual(Object.keys(res.json), ['engagement']);
571
+ const e = res.json.engagement;
572
+ assert.deepEqual(Object.keys(e).sort(), ['created_at', 'guild', 'kind', 'project']);
573
+ assert.deepEqual(e.guild, { slug: 'guild-a', name: 'Guild A' });
574
+ assert.equal(e.kind, 'apply');
575
+ assert.deepEqual(e.project, { name: 'Project pub', origin: 'https://pub.example' });
576
+ assert.ok(!res.text.includes('SENTINEL'), 'client_id never crosses');
577
+ assert.equal(res.headers['cache-control'], 'no-store');
578
+ const write = engagementWrites()[0];
579
+ assert.deepEqual(write.params, ['71', OWNER, 'https://pub.example'], 'the guild, the SESSION, and the origin with its trailing slash trimmed');
580
+ assert.ok(!relayed.length, 'filing applies for nobody — the owner included (ADR 0365 D2)');
581
+ assert.ok(!ECHOES.some((x) => x.github_id === OWNER), 'and writes no echo');
582
+ });
583
+
584
+ test('the owner test lives INSIDE the INSERT: called straight at the domain, a non-owner files nothing', async () => {
585
+ const before = ENGAGEMENTS.length;
586
+ const out = await engagements.fileEngagement({ guildId: '71', githubId: MEMBER, origin: 'https://lapsed.example' }, { pool });
587
+ assert.equal(out, null, 'a member who skipped the route\'s pre-check still cannot file');
588
+ assert.equal(ENGAGEMENTS.length, before);
589
+ const filed = calls.find((c) => /INSERT INTO platform_identity_guild_engagements/.test(c.sql));
590
+ const insert = filed.sql.split('INSERT INTO platform_identity_guild_engagements')[1].split(' RETURNING ')[0];
591
+ assert.match(insert, /EXISTS \(SELECT 1 FROM platform_identity_guild_members o WHERE o\.guild_id = g\.id AND o\.github_id = \$2::bigint AND o\.membership_kind = 'owner'\)/,
592
+ 'ownerExistsSql, re-asserted in the INSERT itself (guilds.js\'s shared fragment)');
593
+ assert.equal(filed.params[1], MEMBER);
594
+ });
595
+
596
+ test('a member who is not the owner is told plainly (403); a stranger gets the guild 404, byte-identical to no guild', async () => {
597
+ as(MEMBER);
598
+ const member = await fileApply('guild-a', 'https://pub.example');
599
+ assert.equal(member.status, 403);
600
+ assert.equal(member.json.error.code, 'guild_owner_only');
601
+ assert.equal(engagementWrites().length, 0, 'nothing reached the INSERT');
602
+
603
+ as(STRANGER);
604
+ const stranger = await fileApply('quiet-guild', 'https://pub.example');
605
+ const unknown = await fileApply('no-such-guild', 'https://pub.example');
606
+ assert.equal(stranger.status, 404);
607
+ assert.equal(stranger.json.error.code, 'guild_not_found');
608
+ assert.equal(stranger.text, unknown.text, 'a non-member cannot learn an unlisted guild exists');
609
+ assert.equal(engagementWrites().length, 0);
610
+ });
611
+
612
+ test('P unknown, inactive, private, stealth, dark matter or door-unknown: ONE byte-identical 404, and nothing written', async () => {
613
+ const bodies = [];
614
+ for (const origin of ['https://nowhere.example', 'https://dead.example', 'https://priv.example',
615
+ 'https://stealth.example', 'https://dark.example', 'https://unreported.example']) {
616
+ const before = ENGAGEMENTS.map((e) => `${e.guild_id}|${e.client_id}|${e.kind}|${e.created_at}`).join();
617
+ const r = await fileApply('guild-a', origin);
618
+ assert.equal(r.status, 404, origin);
619
+ assert.equal(r.json.error.code, 'unknown_project');
620
+ assert.equal(ENGAGEMENTS.map((e) => `${e.guild_id}|${e.client_id}|${e.kind}|${e.created_at}`).join(), before, `${origin}: nothing written or restarted`);
621
+ bodies.push(r.text);
622
+ }
623
+ assert.equal(new Set(bodies).size, 1, 'no oracle: a private project and no project answer the same bytes (ADR 0365 D4)');
624
+ });
625
+
626
+ test('re-filing restarts the window rather than piling up a row', async () => {
627
+ const old = ENGAGEMENTS.find((e) => e.guild_id === '71' && e.client_id === cid('lapsed') && e.kind === 'apply');
628
+ const was = old.created_at;
629
+ await fileApply('guild-a', 'https://lapsed.example');
630
+ const rows = ENGAGEMENTS.filter((e) => e.guild_id === '71' && e.client_id === cid('lapsed') && e.kind === 'apply');
631
+ assert.equal(rows.length, 1);
632
+ assert.ok(rows[0].created_at > was);
633
+ });
634
+
635
+ test('the body is strict: a smuggled field is refused before any SQL', async () => {
636
+ const r = await call('POST', '/api/bongos/guilds/guild-a/engagements', { origin: 'https://pub.example', github_id: MEMBER });
637
+ assert.equal(r.status, 400);
638
+ assert.equal(calls.length, 0);
639
+ });
640
+
641
+ // ===========================================================================
642
+ // 3. The inbox (GET /me/guild-engagements)
643
+ // ===========================================================================
644
+
645
+ const inbox = async (who) => { as(who); return call('GET', '/api/bongos/me/guild-engagements'); };
646
+ const keyOf = (e) => `${e.guild.slug}|${e.project.origin.replace('https://', '').replace('.example', '')}|${e.kind}`;
647
+
648
+ test('the inbox: my guilds only, inside the window, the naming rule, the exclusions — newest first, client_id never crossing', async () => {
649
+ const r = await inbox(MEMBER);
650
+ assert.equal(r.status, 200);
651
+ assert.equal(r.headers['cache-control'], 'no-store');
652
+ assert.deepEqual(r.json.engagements.map(keyOf), [
653
+ 'quiet-guild|pub|apply',
654
+ 'guild-a|pub|apply',
655
+ 'guild-a|pub|invited',
656
+ 'guild-a|lapsed|apply',
657
+ 'guild-a|stale-echo|apply',
658
+ ]);
659
+ assert.equal(r.json.count, r.json.engagements.length, 'count is the array\'s own length');
660
+ for (const e of r.json.engagements) {
661
+ assert.deepEqual(Object.keys(e).sort(), ['created_at', 'guild', 'kind', 'project']);
662
+ assert.deepEqual(Object.keys(e.guild).sort(), ['name', 'slug']);
663
+ assert.deepEqual(Object.keys(e.project).sort(), ['name', 'origin']);
664
+ }
665
+ assert.ok(!r.text.includes('SENTINEL') && !/client_id|guild_id|visibility/.test(r.text),
666
+ 'a literal whitelist: no client_id, no guild row id, no guild visibility');
667
+ });
668
+
669
+ test('the inbox drops every project it may not name, and each dropped row is in the world (not vacuous)', async () => {
670
+ const r = await inbox(MEMBER);
671
+ const seen = new Set(r.json.engagements.map((e) => e.project.origin));
672
+ for (const k of ['priv', 'stealth', 'dark', 'unreported', 'dead']) {
673
+ assert.ok(ENGAGEMENTS.some((e) => e.client_id === cid(k)), `${k} has an engagement row`);
674
+ assert.ok(!seen.has(`https://${k}.example`), `${k}: not on the map AND Public, so not named — and not shown anonymised either`);
675
+ }
676
+ assert.ok(!r.text.includes('Project priv') && !r.text.includes('priv.example'), 'a private project\'s invite reaches its own invitees and nobody else');
677
+ assert.ok(!seen.has('https://old.example'), 'a 31-day-old engagement has aged out (read-time)');
678
+ assert.ok(!r.json.engagements.some((e) => e.guild.slug === 'other-guild'), 'a guild I am not in is not mine to read');
679
+ });
680
+
681
+ test('the inbox excludes a project I build on, hold a LIVE invite notice for, or applied to inside the window — and only those', async () => {
682
+ const seen = new Set((await inbox(MEMBER)).json.engagements.map((e) => e.project.origin));
683
+ assert.ok(!seen.has('https://member.example'), 'already a member');
684
+ assert.ok(!seen.has('https://pending.example'), 'already invited (a live pending notice)');
685
+ assert.ok(!seen.has('https://echoed.example'), 'already applied (a live echo)');
686
+ assert.ok(seen.has('https://lapsed.example'), 'a LAPSED invite notice is no invite: the card shows');
687
+ assert.ok(seen.has('https://stale-echo.example'), 'an echo outside its window is no application: the card shows');
688
+ // the same rows for someone with no such history are all visible
689
+ const late = new Set((await inbox(LATE)).json.engagements.map((e) => e.project.origin));
690
+ for (const k of ['member', 'pending', 'echoed']) assert.ok(late.has(`https://${k}.example`), `${k} shows to a member with no history there`);
691
+ });
692
+
693
+ test('a member who joined AFTER a project invited the guild does not see that invite — but does see an apply', async () => {
694
+ const late = (await inbox(LATE)).json.engagements.map(keyOf);
695
+ assert.ok(late.includes('guild-a|pub|apply'), 'an apply engagement is for every member, whenever they joined');
696
+ assert.ok(!late.includes('guild-a|pub|invited'), 'nobody who joins later inherits the invite (ADR 0336 D7)');
697
+ assert.ok((await inbox(MEMBER)).json.engagements.map(keyOf).includes('guild-a|pub|invited'), 'control: an earlier member does');
698
+ });
699
+
700
+ test('the inbox statement carries every rule, bound to the session and the named constants', async () => {
701
+ await inbox(MEMBER);
702
+ const read = calls.find((c) => /FROM platform_identity_guild_members m JOIN platform_identity_guilds g/.test(c.sql));
703
+ assert.match(read.sql, /\(p\.status = 'active' AND \(btrim\(coalesce\(p\.name, ''\)\) <> '' AND btrim\(coalesce\(p\.description, ''\)\) <> ''\) AND \(p\.visibility_source = 'control-plane' OR p\.visibility_reported_at IS NOT NULL\)\) AND p\.visibility = 'public'/,
704
+ 'the naming rule: onMapSql AND Public');
705
+ assert.match(read.sql, /\(e\.kind = 'apply' OR m\.joined_at <= e\.created_at\)/, 'the joined_at rule, invites only');
706
+ assert.match(read.sql, /NOT EXISTS \(SELECT 1 FROM platform_identity_project_memberships pm/, 'member / live-invite exclusion');
707
+ assert.match(read.sql, /NOT EXISTS \(SELECT 1 FROM platform_identity_application_echoes ae/, 'live-echo exclusion');
708
+ assert.deepEqual(read.params, [MEMBER, 30, api.inviteExpiry.INVITE_EXPIRY_DAYS, APPLICATION_ECHO_WINDOW_DAYS, guilds.LIST_MAX],
709
+ 'the session, then each window by its own constant');
710
+ assert.doesNotMatch(read.sql.split(' FROM ')[0], /client_id/, 'client_id is not even selected');
711
+ });
712
+
713
+ test('a provisional account reads no inbox (the guild feature\'s consent floor)', async () => {
714
+ const r = await inbox(PROV);
715
+ assert.equal(r.status, 403);
716
+ assert.ok(!calls.some((c) => /platform_identity_guild_engagements/.test(c.sql)));
717
+ });
718
+
719
+ // ===========================================================================
720
+ // 4. A project invites a guild (POST /projects/invite-guild)
721
+ // ===========================================================================
722
+
723
+ const CURATOR = '6001';
724
+ const inviteGuild = (body) => call('POST', '/api/bongos/projects/invite-guild', body);
725
+
726
+ test('a non-inviter gets POST /projects/invite\'s unchanged refusal, and no guild is read', async () => {
727
+ as(STRANGER);
728
+ const guildRefusal = await inviteGuild({ client_id: cid('pub'), guild: 'guild-a' });
729
+ const plainRefusal = await call('POST', '/api/bongos/projects/invite', { client_id: cid('pub'), github_login: 'x' });
730
+ assert.equal(guildRefusal.status, 403);
731
+ assert.equal(guildRefusal.text, plainRefusal.text, 'the same gate, the same bytes');
732
+ const missing = await inviteGuild({ client_id: 'no-such-client', guild: 'guild-a' });
733
+ assert.equal(missing.text, guildRefusal.text, 'identical whether or not the project exists');
734
+ assert.ok(!calls.some((c) => /platform_identity_guild/.test(c.sql)), 'the refusal reads no guild and no roster');
735
+ });
736
+
737
+ test('an UNLISTED guild is the unknown-guild 404, byte for byte, and writes nothing', async () => {
738
+ as(CURATOR, { curate: true });
739
+ const unlisted = await inviteGuild({ client_id: cid('pub'), guild: 'quiet-guild' });
740
+ const unknown = await inviteGuild({ client_id: cid('pub'), guild: 'no-such-guild' });
741
+ const malformed = await inviteGuild({ client_id: cid('pub'), guild: 'Not A Slug!' });
742
+ assert.equal(unlisted.status, 404);
743
+ assert.equal(unlisted.json.error.code, 'guild_not_found');
744
+ assert.equal(unlisted.text, unknown.text, 'an unlisted guild cannot be addressed (ADR 0365 D3)');
745
+ assert.equal(malformed.text, unknown.text);
746
+ assert.equal(engagementWrites().length, 0);
747
+ assert.ok(!calls.some((c) => /JOIN platform_identity_accounts pa/.test(c.sql)), 'and no roster is read');
748
+ });
749
+
750
+ test('an inactive project is the invite route\'s unknown_project 404', async () => {
751
+ as(CURATOR, { curate: true });
752
+ const r = await inviteGuild({ client_id: cid('dead'), guild: 'guild-a' });
753
+ assert.equal(r.status, 404);
754
+ assert.equal(r.json.error.code, 'unknown_project');
755
+ assert.equal(engagementWrites().length, 0);
756
+ });
757
+
758
+ test('a public guild: one hall link per member on its PUBLIC roster, the `invited` row recorded, the contract shape', async () => {
759
+ as(CURATOR, { curate: true });
760
+ const r = await inviteGuild({ client_id: cid('pub'), guild: 'Guild-A' });
761
+ assert.equal(r.status, 200);
762
+ assert.deepEqual(Object.keys(r.json).sort(), ['count', 'guild', 'invited', 'invites']);
763
+ assert.equal(r.json.invited, false, 'the hub wrote no invite (ADR 0353)');
764
+ assert.deepEqual(r.json.guild, { slug: 'guild-a', name: 'Guild A' });
765
+ assert.equal(r.json.count, r.json.invites.length);
766
+ assert.deepEqual(r.json.invites.map((i) => i.handle), ['gowner', 'glate', 'gmember'],
767
+ 'owner first, then by handle — the public roster exactly; private, unshown and provisional members are absent');
768
+ for (const i of r.json.invites) {
769
+ assert.deepEqual(Object.keys(i).sort(), ['display_name', 'handle', 'invite_url']);
770
+ const acct = ACCOUNTS.find((a) => a.handle === i.handle);
771
+ assert.equal(i.invite_url, hallInviteUrl('https://pub.example', acct.github_login), 'the project\'s own hall, the login pre-filled');
772
+ }
773
+ for (const hidden of [PRIV, SHY, PROV]) {
774
+ const a = ACCOUNTS.find((x) => x.github_id === hidden);
775
+ assert.ok(!r.text.includes(a.handle) && !r.text.includes(a.github_login), `${a.handle} is not on the public roster, so no link`);
776
+ }
777
+ assert.ok(!/github_id|SENTINEL/.test(r.text), 'no account id and no client_id cross');
778
+ assert.ok(ENGAGEMENTS.some((e) => e.guild_id === '71' && e.client_id === cid('pub') && e.kind === 'invited' && e.created_at > NOW),
779
+ 'the invited engagement is recorded (re-inviting restarts its window)');
780
+ const roster = calls.find((c) => /JOIN platform_identity_accounts pa/.test(c.sql));
781
+ assert.match(roster.sql, /WHERE m\.guild_id = \$1 AND m\.shown_publicly AND \(\(pa\.profile_state = 'active' AND pa\.terms_accepted_at IS NOT NULL\) AND pa\.account_visibility = 'public'\)/,
782
+ 'shown_publicly AND accountVisibleSql — getPublicGuild\'s own roster predicate');
783
+ assert.deepEqual(roster.params, ['71', guilds.LIST_MAX], 'capped at the guild LIST_MAX');
784
+ });
785
+
786
+ test('a PRIVATE project may invite a guild\'s public roster, but leaves no notice for the members it could not see', async () => {
787
+ as(CURATOR, { curate: true });
788
+ ENGAGEMENTS = ENGAGEMENTS.filter((e) => e.client_id !== cid('priv'));
789
+ const r = await inviteGuild({ client_id: cid('priv'), guild: 'guild-a' });
790
+ assert.equal(r.status, 200);
791
+ assert.equal(r.json.count, 3, 'the inviter still gets the people it could see');
792
+ assert.ok(!ENGAGEMENTS.some((e) => e.client_id === cid('priv')),
793
+ 'no `invited` row: nothing could surface later, even if P went public inside the window (ADR 0365 D4)');
794
+ const write = engagementWrites()[0];
795
+ assert.match(write.sql, /AND p\.visibility = 'public'/, 'the naming rule rides the INSERT itself');
796
+ assert.match(write.sql, /EXISTS \(SELECT 1 FROM platform_identity_guilds pg WHERE pg\.id = \$1 AND pg\.visibility = 'public'\)/,
797
+ 'and the guild must still be public at the moment of writing');
798
+ });
799
+
800
+ // ===========================================================================
801
+ // 5. The join: apply WITH a guild (POST /my-projects/join { guild })
802
+ // ===========================================================================
803
+
804
+ const joinWith = (origin, guild) => call('POST', '/api/bongos/my-projects/join', guild === undefined ? { origin } : { origin, guild });
805
+
806
+ test('a guild that no longer resolves is a 409 guild_engagement_gone, and NOTHING is relayed or echoed', async () => {
807
+ as(MEMBER);
808
+ const cases = [
809
+ ['https://lapsed.example', 'other-guild'], // a guild I am not in
810
+ ['https://old.example', 'guild-a'], // aged out
811
+ ['https://priv.example', 'guild-a'], // the project stopped being named
812
+ ['https://lapsed.example', 'Not A Slug!'], // cannot even be a slug
813
+ ];
814
+ for (const [origin, guild] of cases) {
815
+ const r = await joinWith(origin, guild);
816
+ assert.equal(r.status, 409, `${origin} with ${guild}`);
817
+ assert.equal(r.json.error.code, 'guild_engagement_gone');
818
+ }
819
+ assert.equal(relayed.length, 0, 'the applicant chose to apply WITH the guild — the hub never files it without one');
820
+ assert.equal(ECHOES.length, 2, 'no echo written');
821
+ });
822
+
823
+ test('a later joiner cannot apply with an invite they never saw — the join and the inbox share one rule (no oracle)', async () => {
824
+ as(LATE);
825
+ ENGAGEMENTS = ENGAGEMENTS.filter((e) => !(e.guild_id === '71' && e.client_id === cid('pub') && e.kind === 'apply'));
826
+ const r = await joinWith('https://pub.example', 'guild-a');
827
+ assert.equal(r.status, 409, 'only the invited row is left, and LATE joined after it');
828
+ assert.equal(relayed.length, 0);
829
+ });
830
+
831
+ test('a guild that resolves is relayed as the caller\'s OWN application, and the echo carries the guild', async () => {
832
+ as(MEMBER);
833
+ const r = await joinWith('https://lapsed.example', 'guild-a');
834
+ assert.equal(r.status, 200);
835
+ assert.equal(r.json.status, 'requested');
836
+ assert.equal(relayed.length, 1);
837
+ assert.equal(relayed[0].body.github_login, `${MEMBER}-login`, 'the session\'s own login — nobody else\'s');
838
+ const check = calls.find((c) => /lower\(g\.slug\) = \$2/.test(c.sql));
839
+ assert.deepEqual(check.params, [MEMBER, 'guild-a', cid('lapsed'), 30], 'the session, the canonical slug, the resolved client');
840
+ const echo = ECHOES.find((e) => e.github_id === MEMBER && e.client_id === cid('lapsed'));
841
+ assert.equal(echo.guild_id, '71', 'the label the reviewer will see');
842
+ assert.ok(!r.text.includes('SENTINEL'));
843
+ });
844
+
845
+ test('a later SOLO application clears the label: the most recent application decides', async () => {
846
+ as(MEMBER);
847
+ await joinWith('https://lapsed.example', 'guild-a');
848
+ assert.equal(ECHOES.find((e) => e.client_id === cid('lapsed')).guild_id, '71');
849
+ await joinWith('https://lapsed.example');
850
+ assert.equal(ECHOES.find((e) => e.client_id === cid('lapsed')).guild_id, null);
851
+ });
852
+
853
+ test('an application the project REFUSED at the door leaves no echo, guild or not', async () => {
854
+ as(MEMBER);
855
+ relayAnswer = { status: 403, body: { error: { code: 'invite_only' } } };
856
+ const r = await joinWith('https://lapsed.example', 'guild-a');
857
+ assert.equal(r.json.status, 'invite_only');
858
+ assert.ok(!ECHOES.some((e) => e.client_id === cid('lapsed')));
859
+ });
860
+
861
+ // ===========================================================================
862
+ // 6. The label the reviewer reads (echoGuildLabel; the route is proved in
863
+ // tests/applicant_profile_boundary.mjs)
864
+ // ===========================================================================
865
+
866
+ test('the label: { slug, name } while still a member, slug null for an unlisted guild, null once they left', async () => {
867
+ ECHOES.push({ github_id: MEMBER, client_id: cid('pub'), applied_at: ago(1), guild_id: '71' });
868
+ assert.deepEqual(await engagements.echoGuildLabel({ githubId: MEMBER, clientId: cid('pub') }, { pool }), { slug: 'guild-a', name: 'Guild A' });
869
+ ECHOES.find((e) => e.client_id === cid('pub')).guild_id = '72';
870
+ assert.deepEqual(await engagements.echoGuildLabel({ githubId: MEMBER, clientId: cid('pub') }, { pool }), { slug: null, name: 'Quiet Guild' },
871
+ 'an unlisted guild has no public page for the reviewer to open');
872
+ ECHOES.push({ github_id: STRANGER, client_id: cid('pub'), applied_at: ago(1), guild_id: '71' });
873
+ assert.equal(await engagements.echoGuildLabel({ githubId: STRANGER, clientId: cid('pub') }, { pool }), null,
874
+ 'an applicant who is not (or no longer) in the guild carries no label');
875
+ assert.equal(await engagements.echoGuildLabel({ githubId: 'system:bfg', clientId: cid('pub') }, { pool }), null);
876
+ });
877
+
878
+ test('nothing here is authority (ADR 0016): no rank, permission, credit or admission symbol in the domain', () => {
879
+ const code = DOMAIN_SRC.replace(/\/\/.*$/gm, '');
880
+ assert.doesNotMatch(code, /requireRank|requirePermission|total_credits|\bkarma\b|builders\.rank|access_requests/);
881
+ });