@bongos/core 1.21.56 → 1.21.57

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.
@@ -153,14 +153,21 @@ function identitySentinels(acct) {
153
153
  // directory GET /scouting Recruiter — 'full' | 'name-only' | 'absent'
154
154
  // sliver GET /scouting/:handle Recruiter — 'full' (D3) | 'name-only' (D3, hidden) | 404
155
155
  // self GET /me/cross-project Self — 'own' (own numbers, any flags) | 'n/a' (no session exists)
156
+ // guild GET /guilds/:slug Anon · Builder · Recruiter · Self — the guild page's PUBLIC
157
+ // roster (task 1002301; ADR 0336 D4), viewer-independent: 'listed' | 'absent'.
158
+ // Every fixture account is a member with shown_publicly ON, so the account
159
+ // predicates alone decide; hide_stats never hides membership (D4 §2)
160
+ // members GET /guilds/:slug/members a fellow Member — the own-scope roster: 'listed' |
161
+ // 'absent'. Joining is consent to be seen BY THE GUILD (D4 §1), so a private
162
+ // member is listed here and nowhere public; a provisional account is never
156
163
  const MATRIX = {
157
- PROV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'n/a' },
158
- PUB: { profile: 'full', search: 'listed', board: 'listed', directory: 'full', sliver: 'full', self: 'own' },
159
- SHY: { profile: 'identity', search: 'name-only', board: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own' },
160
- PRIV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'own' },
161
- PRIVD: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'full', self: 'own' },
162
- PRIVDH: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'name-only', self: 'own' },
163
- OPTOUT: { profile: 'full', search: 'listed', board: 'listed', directory: 'absent', sliver: 404, self: 'own' },
164
+ PROV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'n/a', guild: 'absent', members: 'absent' },
165
+ PUB: { profile: 'full', search: 'listed', board: 'listed', directory: 'full', sliver: 'full', self: 'own', guild: 'listed', members: 'listed' },
166
+ SHY: { profile: 'identity', search: 'name-only', board: 'absent', directory: 'name-only', sliver: 'name-only', self: 'own', guild: 'listed', members: 'listed' },
167
+ PRIV: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 404, self: 'own', guild: 'absent', members: 'listed' },
168
+ PRIVD: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'full', self: 'own', guild: 'absent', members: 'listed' },
169
+ PRIVDH: { profile: 404, search: 'absent', board: 'absent', directory: 'absent', sliver: 'name-only', self: 'own', guild: 'absent', members: 'listed' },
170
+ OPTOUT: { profile: 'full', search: 'listed', board: 'listed', directory: 'absent', sliver: 404, self: 'own', guild: 'listed', members: 'listed' },
164
171
  };
165
172
 
166
173
  // ===========================================================================
@@ -243,10 +250,59 @@ function activityScope(sql, params) {
243
250
  return String(params[Number(m[1]) - 1]);
244
251
  }
245
252
 
253
+ // THE GUILDS (task 1002301): one public and one unlisted guild, each with every
254
+ // fixture account as a member and shown_publicly ON — the most permissive guild
255
+ // flag, so the account predicates alone decide each cell. PUB owns both.
256
+ const GUILDS = [
257
+ { id: '880001', slug: 'mx-guild', name: 'Matrix Guild', description: 'mx-guild-desc', visibility: 'public', created_at: 'T0' },
258
+ { id: '880002', slug: 'mx-unlisted', name: 'Matrix Unlisted', description: 'mx-unlisted-desc', visibility: 'unlisted', created_at: 'T0' },
259
+ ];
260
+ const GUILD_MEMBERS = GUILDS.flatMap((g) => ACCOUNTS.map((a) => ({
261
+ guild_id: g.id, github_id: a.github_id, membership_kind: a.key === 'PUB' ? 'owner' : 'member', shown_publicly: true, counted_in_totals: true,
262
+ })));
263
+ /** The members-of-a-guild statements: the guild and shown terms consumed here, the rest read as an account filter. */
264
+ function guildRosterFilter(sql, params) {
265
+ let where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = m\.github_id WHERE (.+?) ORDER BY/, 'guild roster');
266
+ if (!/\bm\.guild_id = \$1\b/.test(where)) throw new Error(`fake pool: a guild roster with no guild scope — ${where}`);
267
+ where = where.replace(/\bm\.guild_id = \$1\b/, '');
268
+ const shownOnly = /\bm\.shown_publicly\b/.test(where);
269
+ where = where.replace(/\bm\.shown_publicly\b/, '');
270
+ const byAccount = accountFilter(where, []);
271
+ return (m) => m.guild_id === String(params[0]) && (!shownOnly || m.shown_publicly) && byAccount(ACCOUNTS.find((a) => a.github_id === m.github_id));
272
+ }
273
+
246
274
  const pool = {
247
275
  query: async (text, params = []) => {
248
276
  const sql = String(text).replace(/\s+/g, ' ').trim();
249
277
 
278
+ // THE GUILD PAGE's reads. The public guild: its WHERE is read, not assumed.
279
+ if (/^SELECT id, slug, name, description, created_at FROM platform_identity_guilds WHERE /.test(sql)) {
280
+ if (!/WHERE lower\(slug\) = \$1 AND visibility = 'public'$/.test(sql)) throw new Error(`fake pool: the public guild read moved — ${sql}`);
281
+ const rows = GUILDS.filter((g) => g.slug === String(params[0]) && g.visibility === 'public').map(({ visibility, ...g }) => g);
282
+ return { rows, rowCount: rows.length };
283
+ }
284
+ // ... a member's own row in a guild (the own-scope door).
285
+ if (/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$/.test(sql)) {
286
+ const g = GUILDS.find((x) => x.slug === String(params[0]));
287
+ const m = g && GUILD_MEMBERS.find((x) => x.guild_id === g.id && x.github_id === String(params[1]));
288
+ const rows = m ? [{ ...g, ...m, joined_at: 'T2' }] : [];
289
+ return { rows, rowCount: rows.length };
290
+ }
291
+ // ... both rosters: public (shown AND visible) and members' (active).
292
+ if (/FROM platform_identity_guild_members m JOIN platform_identity_accounts pa/.test(sql)) {
293
+ // the projection is pinned: a new column here is a new disclosure, and this fails first
294
+ const cols = (/^SELECT (.+?) FROM /.exec(sql) || [, ''])[1];
295
+ if (cols !== 'pa.handle, pa.display_name, pa.avatar_url, m.membership_kind') throw new Error(`fake pool: the guild roster selects something new — ${cols}`);
296
+ const keep = guildRosterFilter(sql, params);
297
+ const rows = GUILD_MEMBERS.filter(keep).map((m) => {
298
+ const a = ACCOUNTS.find((x) => x.github_id === m.github_id);
299
+ return { handle: a.handle, display_name: a.display_name, avatar_url: a.avatar_url, membership_kind: m.membership_kind };
300
+ });
301
+ return { rows, rowCount: rows.length };
302
+ }
303
+ // ... and the weekly totals: never computed on a read (guild-totals.js); none stored here.
304
+ if (/FROM platform_identity_guild_totals WHERE guild_id = \$1$/.test(sql)) return { rows: [], rowCount: 0 };
305
+
250
306
  // THE LEADERBOARD's totals: the rollup floor sits on the act CTE's join.
251
307
  if (/platform_identity_activity_snapshots/.test(sql)) {
252
308
  const where = lift(sql, /JOIN platform_identity_accounts pa ON pa\.github_id = a\.github_id WHERE (.+?) \), per_project AS/, 'leaderboard totals');
@@ -346,6 +402,8 @@ const ANON = null;
346
402
  const STRANGER = { name: 'Builder', github_id: '7100001', rank: 'xenos', curate: false };
347
403
  const RECRUITER = { name: 'Recruiter', github_id: '7100002', rank: 'archon', curate: true };
348
404
  const selfOf = (k) => ({ name: 'Self', github_id: ACCT[k].github_id, rank: 'xenos', curate: false });
405
+ // a fellow guild member: PUB's session, which owns both fixture guilds
406
+ const MEMBER = { name: 'Member', github_id: ACCT.PUB.github_id, rank: 'xenos', curate: false };
349
407
 
350
408
  const moduleApiPath = require.resolve('../src/module-api.js');
351
409
  const helpers = require('../src/bongos/routes/_helpers.js');
@@ -413,7 +471,7 @@ const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
413
471
  const app = express();
414
472
  app.use(express.json());
415
473
  app.use(attachFail);
416
- for (const file of ['public-profile', 'profile', 'community-search', 'community-leaderboard', 'scouting']) {
474
+ for (const file of ['public-profile', 'profile', 'community-search', 'community-leaderboard', 'scouting', 'guilds-public', 'guilds']) {
417
475
  app.use('/api/bongos', require(`../modules/platform-identity/routes/${file}.js`)());
418
476
  }
419
477
  const server = app.listen(0, '127.0.0.1');
@@ -535,6 +593,52 @@ for (const k of KEYS) {
535
593
  });
536
594
  }
537
595
 
596
+ // The guild page's two rosters (task 1002301; ADR 0336 D4). One read per viewer
597
+ // serves every row, so each row's cell is checked against the same bodies.
598
+ const GUILD_PATH = '/api/bongos/guilds/mx-guild';
599
+ const ROSTER_KEYS = ['avatar_url', 'display_name', 'handle', 'membership_kind'];
600
+ const ALL_VIEWERS = () => [ANON, STRANGER, RECRUITER, MEMBER, ...KEYS.map(selfOf)];
601
+
602
+ test('the guild page’s public roster — the same bytes to Anon, Builder, Recruiter, a Member and every Self; listed or absent per the matrix', async () => {
603
+ const seen = [];
604
+ for (const who of ALL_VIEWERS()) seen.push(await get(who, GUILD_PATH));
605
+ for (const r of seen.slice(1)) {
606
+ assert.equal(r.status, seen[0].status, `${r.who} got a different status — the public guild read must not vary by viewer (ADR 0336 D4)`);
607
+ assert.equal(r.text, seen[0].text, `${r.who} got a different body — the public guild read must not vary by viewer (ADR 0336 D4)`);
608
+ }
609
+ const [r] = seen;
610
+ assert.equal(r.status, 200);
611
+ const listed = new Set(r.json.members.map((m) => m.handle));
612
+ for (const k of KEYS) assert.equal(listed.has(ACCT[k].handle), MATRIX[k].guild === 'listed', `${k} on the public roster: expected ${MATRIX[k].guild}`);
613
+ assert.equal(r.json.size, r.json.members.length, 'size is the roster’s own length — never a "+N more" (ADR 0336 D4)');
614
+ for (const m of r.json.members) assert.deepEqual(Object.keys(m).sort(), ROSTER_KEYS, 'handle, name, avatar, kind — never a GitHub id or login');
615
+ });
616
+
617
+ test('the guild members’ roster — a fellow member sees every active member, private ones included; Anon is refused', async () => {
618
+ const r = await get(MEMBER, `${GUILD_PATH}/members`);
619
+ assert.equal(r.status, 200);
620
+ const listed = new Set(r.json.members.map((m) => m.handle));
621
+ for (const k of KEYS) assert.equal(listed.has(ACCT[k].handle), MATRIX[k].members === 'listed', `${k} on the members’ roster: expected ${MATRIX[k].members}`);
622
+ assert.equal(r.json.count, r.json.members.length);
623
+ for (const m of r.json.members) assert.deepEqual(Object.keys(m).sort(), ROSTER_KEYS);
624
+ assert.deepEqual(Object.keys(r.json.you).sort(), ['counted_in_totals', 'membership_kind', 'shown_publicly'], 'the reader’s own two switches, and nobody else’s');
625
+ assert.equal((await get(ANON, `${GUILD_PATH}/members`)).status, 401);
626
+ });
627
+
628
+ test('an unlisted guild’s public read is a 404 to everyone, its own members included, byte-identical to a slug nobody holds; members reach it through the own-scope door', async () => {
629
+ const none = await get(ANON, '/api/bongos/guilds/mx-nobody');
630
+ assert.equal(none.status, 404);
631
+ for (const who of ALL_VIEWERS()) {
632
+ const r = await get(who, '/api/bongos/guilds/mx-unlisted');
633
+ assert.equal(r.status, 404, `${r.who}: an unlisted guild has no public existence (ADR 0336 D4)`);
634
+ assert.equal(r.text.replaceAll('mx-unlisted', 'S'), none.text.replaceAll('mx-nobody', 'S'), `${r.who}: no existence oracle`);
635
+ }
636
+ const own = await get(MEMBER, '/api/bongos/guilds/mx-unlisted/members');
637
+ assert.equal(own.status, 200);
638
+ const listed = new Set(own.json.members.map((m) => m.handle));
639
+ for (const k of KEYS) assert.equal(listed.has(ACCT[k].handle), MATRIX[k].members === 'listed', `${k} on the unlisted guild’s members’ roster`);
640
+ });
641
+
538
642
  test('the sliver’s key lists are D3’s, transcribed: handle, name, avatar, disciplines, a recent-activity boolean — or the first three', () => {
539
643
  // The cells above compare a response to the MODULE's lists; this pins those
540
644
  // lists to the spec, so a sixth key added to the module cannot pass with them.
@@ -594,6 +698,10 @@ test('the recruiting directory — the recruiter sees the full and name-only tie
594
698
  // ===========================================================================
595
699
 
596
700
  const isOwnRead = (r, a) => r.whoId === a.github_id && r.path === '/api/bongos/me/cross-project';
701
+ // The one place ADR 0336 D4 lets a private member be named: their own guild's
702
+ // members' roster, read by a fellow member — joining is consent to be seen BY THE
703
+ // GUILD, never by the world. A provisional account is not on it (accountActiveSql).
704
+ const isFellowMemberRead = (r) => r.who === 'Member' && /^\/api\/bongos\/guilds\/[^/?]+\/members$/.test(r.path);
597
705
  // What a response DISCLOSES: its body minus the fields that only echo the
598
706
  // caller's own request back (search's `query` and `discipline`). A discipline
599
707
  // the caller typed into the URL is not the server revealing it; the results
@@ -608,7 +716,7 @@ function disclosed(r) {
608
716
  }
609
717
 
610
718
  test('the walk covered every surface for every row (so the claims below are about something)', () => {
611
- for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/scouting']) {
719
+ for (const p of ['/profiles/', '/me/cross-project', '/community/search', '/community/leaderboard', '/scouting', '/guilds/mx-guild', '/guilds/mx-guild/members']) {
612
720
  assert.ok(SEEN.some((r) => r.path.includes(p) && r.status === 200), `no 200 from ${p} — the sweep below would be vacuous for it`);
613
721
  }
614
722
  assert.ok(SEEN.length >= KEYS.length * 6, `only ${SEEN.length} responses recorded`);
@@ -630,21 +738,25 @@ test('hide_stats beats everything: no hidden account’s numbers reach anyone bu
630
738
  assert.ok(SEEN.some((r) => r.who === 'Anon' && r.text.includes(String(ROLLUP[ACCT.PUB.github_id].credits))), 'a shown record must reach Anon, or absence above is vacuous');
631
739
  });
632
740
 
633
- test('provisional and private (not discoverable) are absent from every cross-project surface, for every viewer but Self', () => {
741
+ test('provisional and private (not discoverable) are absent from every cross-project surface, for every viewer but Self — a private member’s own guild aside', () => {
634
742
  for (const k of ['PROV', 'PRIV']) {
635
743
  const a = ACCT[k];
636
- for (const r of SEEN.filter((x) => !isOwnRead(x, a) && !x.path.endsWith(`/profiles/${a.handle}`) && !x.path.endsWith(`/scouting/${a.handle}`))) {
744
+ const ownGuild = (x) => k === 'PRIV' && isFellowMemberRead(x);
745
+ for (const r of SEEN.filter((x) => !isOwnRead(x, a) && !ownGuild(x) && !x.path.endsWith(`/profiles/${a.handle}`) && !x.path.endsWith(`/scouting/${a.handle}`))) {
637
746
  for (const s of identitySentinels(a)) assert.ok(!disclosed(r).includes(s), `${k}'s "${s}" reached ${r.who} through GET ${r.path}`);
638
747
  }
639
748
  }
640
749
  });
641
750
 
642
- test('private · discoverable reaches the recruiter’s sliver and nowhere else', () => {
751
+ test('private · discoverable reaches the recruiter’s sliver and, as a member, its own guilds’ members’ rosters — nowhere else', () => {
643
752
  for (const k of ['PRIVD', 'PRIVDH']) {
644
753
  const a = ACCT[k];
645
754
  const leaks = SEEN.filter((r) => !isOwnRead(r, a) && identitySentinels(a).some((s) => disclosed(r).includes(s)));
646
- assert.deepEqual(leaks.map((r) => `${r.who} ${r.path}`), [`Recruiter /api/bongos/scouting/${a.handle}`],
647
- `${k} may be named only by the sliver, to the recruiter (D3)`);
755
+ assert.deepEqual([...new Set(leaks.map((r) => `${r.who} ${r.path}`))].sort(), [
756
+ 'Member /api/bongos/guilds/mx-guild/members',
757
+ 'Member /api/bongos/guilds/mx-unlisted/members',
758
+ `Recruiter /api/bongos/scouting/${a.handle}`,
759
+ ], `${k} may be named only by the sliver, to the recruiter (D3), and to its fellow guild members (ADR 0336 D4)`);
648
760
  }
649
761
  });
650
762
 
@@ -24,8 +24,8 @@ test('a shared asset reaches every page in its dir whose HTML names it', () => {
24
24
  // sky-data.js is shared by the two doors of one page (task 1004232): /thinking and /goal-map.
25
25
  assert.deepEqual(names(['modules/hall-ui/public/sky-data.js']), ['goal-map.states.json', 'thinking.states.json']);
26
26
  assert.deepEqual(names(['modules/hall-ui/public/text-fit.js']), ['goals.states.json']);
27
- // the account page links the hub's sheet too since task 1004259
28
- assert.deepEqual(names(['modules/public-landing/public/assets/cosmos.css']), ['account.states.json', 'projects.states.json']);
27
+ // the account page links the hub's sheet too since task 1004259, and the guild page since task 1002301
28
+ assert.deepEqual(names(['modules/public-landing/public/assets/cosmos.css']), ['account.states.json', 'guild.states.json', 'projects.states.json']);
29
29
  });
30
30
 
31
31
  test('the page a builder changed comes before the pages a shared file reaches, so the render cap never drops it', () => {
@@ -34,7 +34,7 @@ test('the platform floors are a pinned list — a floor cannot vanish quietly',
34
34
  });
35
35
 
36
36
  test('the shipped states files validate and sit beside the page they name', () => {
37
- for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/hall-ui/public/ranks.states.json']) {
37
+ for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/public-landing/public/guild.states.json', 'modules/hall-ui/public/ranks.states.json']) {
38
38
  const json = readJson(rel);
39
39
  const v = lib.validateStatesFile(json);
40
40
  assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
@@ -43,14 +43,14 @@ test('the shipped states files validate and sit beside the page they name', () =
43
43
  }
44
44
  // the apex is dark only (task 1003336): its states files name the one mode, and
45
45
  // render.js reads it as the default so a plain `--page /` never renders light
46
- for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json']) {
46
+ for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/guild.states.json']) {
47
47
  assert.deepEqual(readJson(rel).modes, ['dark'], `${rel}: the apex renders dark only`);
48
48
  assert.equal(readJson(rel).modeQuery, false, `${rel}: no query walks a second mode`);
49
49
  }
50
50
  });
51
51
 
52
52
  test('the shipped probes files validate', () => {
53
- for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json']) {
53
+ for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json', 'modules/public-landing/public/guild.probes.json']) {
54
54
  const v = lib.validateProbesFile(readJson(rel));
55
55
  assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
56
56
  }