@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.
@@ -0,0 +1,38 @@
1
+ {
2
+ "_": "The guild page's states for modules/ui-design/kit/render.js (task 1002301, R25; ADR 0336 D4, ADR 0337 D4.4). One shell serves every /g/<slug>; the stub answers three guilds from fixtures/guilds.json. Signed out: forge-crew is the public page (four listed members, the rounded record), and night-shift — unlisted — is the same 'no guild here' an unknown slug gets, with sign-in. Signed in as the fixture builder: forge-crew is the members' view (everyone, six, with the prompt to be listed because they are a member who is not on its public page; the pair for that switch waits until the prompt is answered), night-shift is their unlisted guild (no public page, no published record), open-hands is a public guild they are not in (the public view, not counted yet), and an unknown slug is 'no guild here' without the sign-in. Dark only, like every apex page (ADR 0204).",
3
+ "page": "/guild",
4
+ "surface": "public-landing",
5
+ "modes": ["dark"],
6
+ "modeQuery": false,
7
+ "ignoreRequests": ["/api/(gds|bongos)/me$", "/api/(gds|bongos)/guilds/[a-z0-9-]+(/members)?$"],
8
+ "ignoreConsole": ["/api/(gds|bongos)/me", "/api/(gds|bongos)/guilds/", "\\b401\\b", "\\b404\\b", "Unauthorized", "Not Found"],
9
+ "ignoreControls": [".vh"],
10
+ "states": {
11
+ "out-public": { "url": "/g/forge-crew", "auth": false, "actions": [["wait", 400]],
12
+ "expect": { "visible": ["#guildBody", "#gName", "#gRoster", "#gFigs", "#signinTop"], "hidden": ["#gYou", "#missing", "#loading", "#who"],
13
+ "text": [["#gName", "Forge Crew"], ["#gKicker", "Guild"], ["#gMeta", "4 members · since Sep 2026"], ["#tShipped", "100+"], ["#tCredits", "2,000+"], ["#tKarma", "50+"], ["#tProjects", "under 10"], ["#gRecordNote", "Counted on 1 Oct 2026. Projects are the public projects its listed members build on."]],
14
+ "count": [["#gRoster li", 4]] } },
15
+ "out-unlisted": { "url": "/g/night-shift", "auth": false, "actions": [["wait", 400]],
16
+ "expect": { "visible": ["#missing", "#missingSignin"], "hidden": ["#guildBody", "#loading"],
17
+ "text": [["#missingWhy", "There is no public guild at this address. If it is an unlisted guild you belong to, sign in to see it."]] } },
18
+ "in-member": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500]],
19
+ "expect": { "visible": ["#guildBody", "#gYou", "#showAsk", "#countWhat", "#who"], "hidden": ["#shownBand", "#shownUnlisted", "#missing", "#signinTop"],
20
+ "text": [["#gKicker", "Guild · public"], ["#gMeta", "6 members · 4 on the public page · since Sep 2026"], ["#gYouSide", "You’re a member of this guild. Only you see this, and only you can change it."]],
21
+ "checked": "input[name=shown][value=off]",
22
+ "count": [["#gRoster li", 6]] } },
23
+ "in-unlisted-owner": { "url": "/g/night-shift", "auth": true, "actions": [["wait", 500]],
24
+ "expect": { "visible": ["#guildBody", "#gYou", "#shownUnlisted"], "hidden": ["#showAsk", "#shownBand", "#gFigs", "#gDesc"],
25
+ "text": [["#gKicker", "Guild · unlisted"], ["#shownUnlisted", "This guild is unlisted, so it has no public page. If you make it public, you’ll be listed on it as its owner, and each member will be asked before they are."], ["#gMeta", "3 members"], ["#gRecordNote", "This guild is unlisted, so its record isn’t published anywhere."], ["#countWhat", "Your numbers are left out of this guild’s totals, from its next weekly count."]],
26
+ "checked": "input[name=counted][value=off]" } },
27
+ "in-not-member": { "url": "/g/open-hands", "auth": true, "actions": [["wait", 500]],
28
+ "expect": { "visible": ["#guildBody", "#gRoster"], "hidden": ["#gYou", "#missing"],
29
+ "text": [["#gKicker", "Guild"], ["#gMeta", "2 members · since Oct 2026"], ["#tShipped", "—"], ["#gRecordNote", "Not counted yet. A guild’s record is counted once a week."]],
30
+ "count": [["#gRoster li", 2]] } },
31
+ "in-missing": { "url": "/g/no-such-guild", "auth": true, "actions": [["wait", 500]],
32
+ "expect": { "visible": ["#missing"], "hidden": ["#guildBody", "#missingSignin"],
33
+ "text": [["#missingWhy", "There is no guild at this address that you can see. A guild that is unlisted is shown only to its members."]] } },
34
+ "in-profile-band": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 500], ["click", "#who"], ["wait", 300]],
35
+ "expect": { "visible": ["#menuProfile"], "hidden": ["#menuProjects"], "text": [["#bandWhoLogin", "builder"]] } }
36
+ },
37
+ "reducedMotion": { "url": "/g/forge-crew", "auth": true, "actions": [["wait", 400]] }
38
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "forge-crew": {
3
+ "public": {
4
+ "guild": {
5
+ "slug": "forge-crew",
6
+ "name": "Forge Crew",
7
+ "description": "Builders who like the unglamorous parts: migrations, test harnesses, the deploy path. We pick up the work nobody else claims.",
8
+ "created_at": "2026-09-12T10:20:00.000Z"
9
+ },
10
+ "members": [
11
+ { "handle": "mara-quinn", "display_name": "Mara Quinn", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
12
+ { "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
13
+ { "handle": "kip", "display_name": "Kip", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
14
+ { "handle": "theo-park", "display_name": "Theo Park", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" }
15
+ ],
16
+ "size": 4,
17
+ "totals": { "credits": 2000, "works_shipped": 100, "karma": 50, "projects": 0, "as_of": "2026-10-01T03:00:00.000Z" }
18
+ },
19
+ "members": {
20
+ "guild": {
21
+ "slug": "forge-crew",
22
+ "name": "Forge Crew",
23
+ "description": "Builders who like the unglamorous parts: migrations, test harnesses, the deploy path. We pick up the work nobody else claims.",
24
+ "visibility": "public"
25
+ },
26
+ "you": { "membership_kind": "member", "shown_publicly": false, "counted_in_totals": true },
27
+ "members": [
28
+ { "handle": "mara-quinn", "display_name": "Mara Quinn", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
29
+ { "handle": "builder", "display_name": "A Builder", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
30
+ { "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
31
+ { "handle": "kip", "display_name": "Kip", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" },
32
+ { "handle": "sol-reyes", "display_name": "Sol Reyes", "avatar_url": null, "membership_kind": "member" },
33
+ { "handle": "theo-park", "display_name": "Theo Park", "avatar_url": "/__kit/avatar.svg", "membership_kind": "member" }
34
+ ],
35
+ "count": 6
36
+ }
37
+ },
38
+ "night-shift": {
39
+ "public": null,
40
+ "members": {
41
+ "guild": {
42
+ "slug": "night-shift",
43
+ "name": "Night Shift",
44
+ "description": "",
45
+ "visibility": "unlisted"
46
+ },
47
+ "you": { "membership_kind": "owner", "shown_publicly": false, "counted_in_totals": false },
48
+ "members": [
49
+ { "handle": "builder", "display_name": "A Builder", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
50
+ { "handle": "ines-duarte", "display_name": "Ines Duarte", "avatar_url": null, "membership_kind": "member" },
51
+ { "handle": "sol-reyes", "display_name": "Sol Reyes", "avatar_url": null, "membership_kind": "member" }
52
+ ],
53
+ "count": 3
54
+ }
55
+ },
56
+ "open-hands": {
57
+ "public": {
58
+ "guild": {
59
+ "slug": "open-hands",
60
+ "name": "Open Hands",
61
+ "description": "A newer guild for first-time builders. Ask anything.",
62
+ "created_at": "2026-10-03T16:05:00.000Z"
63
+ },
64
+ "members": [
65
+ { "handle": "ada-ng", "display_name": "Ada Ng", "avatar_url": "/__kit/avatar.svg", "membership_kind": "owner" },
66
+ { "handle": "ruth-okafor", "display_name": "Ruth Okafor", "avatar_url": null, "membership_kind": "member" }
67
+ ],
68
+ "size": 2,
69
+ "totals": null
70
+ },
71
+ "members": null
72
+ }
73
+ }
@@ -436,6 +436,44 @@ function handlePatch(res, body) {
436
436
  return json(res, 200, { handle: h, previous_handle: previous || null });
437
437
  }
438
438
 
439
+ // The hub's guild page (task 1002301): GET /guilds/:slug (public), GET /guilds/:slug/members
440
+ // and PATCH /guilds/:slug/membership (own-scope), answered from ONE set of guilds held for the
441
+ // run (guilds.json, or the file a states file's stub.fixtures names), keyed by slug, each with
442
+ // the public read's body (null = the 404 an unlisted or unknown guild answers) and the
443
+ // signed-in builder's members read (null = not a member: the same 404). The PATCH mirrors the
444
+ // route's schema — the two booleans, at least one — so a save the page re-reads comes back saved.
445
+ let GUILDS = null;
446
+ function guildsHeld() {
447
+ if (!GUILDS) {
448
+ const name = 'guilds';
449
+ GUILDS = JSON.parse(fs.readFileSync(path.join(FIXTURES, (ALIAS[name] || name) + '.json'), 'utf8'));
450
+ }
451
+ return GUILDS;
452
+ }
453
+ // a root-relative avatar is this stub's own (/__kit/avatar.svg), absolutised as me() does
454
+ function withAvatars(body, origin) {
455
+ if (!body || !Array.isArray(body.members)) return body;
456
+ return { ...body, members: body.members.map((m) => (typeof m.avatar_url === 'string' && m.avatar_url.startsWith('/') ? { ...m, avatar_url: origin + m.avatar_url } : m)) };
457
+ }
458
+ const PAGE_ALIAS = { g: 'guild' };
459
+ const GUILD_NOT_FOUND ={ error: { code: 'guild_not_found', message: 'guild_not_found' } };
460
+ function guildRead(res, slug, half, origin) {
461
+ const g = guildsHeld()[String(slug).toLowerCase()];
462
+ const body = g && g[half];
463
+ return body ? json(res, 200, withAvatars(body, origin)) : json(res, 404, GUILD_NOT_FOUND);
464
+ }
465
+ function guildMembershipPatch(res, slug, body) {
466
+ const keys = Object.keys(body);
467
+ const bad = keys.find((k) => !['shown_publicly', 'counted_in_totals'].includes(k));
468
+ if (bad) return refuse(res, 400, 'validation_failed', `unknown field: ${bad}`);
469
+ for (const k of keys) if (typeof body[k] !== 'boolean') return refuse(res, 400, 'validation_failed', `${k} must be a boolean`);
470
+ if (!keys.length) return refuse(res, 400, 'nothing_to_update', 'send shown_publicly and/or counted_in_totals');
471
+ const g = guildsHeld()[String(slug).toLowerCase()];
472
+ if (!g || !g.members) return json(res, 404, GUILD_NOT_FOUND);
473
+ Object.assign(g.members.you, body);
474
+ return json(res, 200, { shown_publicly: g.members.you.shown_publicly, counted_in_totals: g.members.you.counted_in_totals });
475
+ }
476
+
439
477
  const server = http.createServer((req, res) => {
440
478
  const u = new URL(req.url, 'http://x');
441
479
  const p = u.pathname;
@@ -453,6 +491,10 @@ const server = http.createServer((req, res) => {
453
491
  if (rest === '/me/cross-project' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => crossProjectPatch(res, body)) : json(res, 401, UNAUTH);
454
492
  if (rest === '/me/profile' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => profilePatch(res, body)) : json(res, 401, UNAUTH);
455
493
  if (rest === '/me/handle' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => handlePatch(res, body)) : json(res, 401, UNAUTH);
494
+ const guild = rest.match(/^\/guilds\/([^/]+)(\/members|\/membership)?$/);
495
+ if (guild && !guild[2] && req.method === 'GET') return guildRead(res, guild[1], 'public', origin);
496
+ if (guild && guild[2] === '/members' && req.method === 'GET') return AUTH ? guildRead(res, guild[1], 'members', origin) : json(res, 401, UNAUTH);
497
+ if (guild && guild[2] === '/membership' && req.method === 'PATCH') return AUTH ? readBody(req, (body) => guildMembershipPatch(res, guild[1], body)) : json(res, 401, UNAUTH);
456
498
  if (rest === '/provisioning/instances' && req.method === 'GET') return AUTH ? instances(res, null, false) : json(res, 401, UNAUTH);
457
499
  // the planet-physics questions' pre-create check (task 1004418): nothing stops here —
458
500
  // the kit carries no screen. Of the soft-limit table it mirrors only the three hide
@@ -550,7 +592,10 @@ const server = http.createServer((req, res) => {
550
592
  // TASK_DETAIL_PAGE_RE / IDEA_DETAIL_PAGE_RE routes). Without this the states
551
593
  // for those pages could only be rendered at a URL production never serves, and
552
594
  // a page script that reads its id out of location.pathname would see none.
553
- const name = under2 === '' ? 'index' : (fs.existsSync(path.join(DIR, under2 + '.html')) ? under2 : under2.replace(/\/[^/]+$/, ''));
595
+ // A namespace word may name its page differently: the hub serves /g/<slug> from
596
+ // guild.html (platform-server.js's /g/:slug route, task 1002301).
597
+ const base = under2 === '' ? 'index' : (fs.existsSync(path.join(DIR, under2 + '.html')) ? under2 : under2.replace(/\/[^/]+$/, ''));
598
+ const name = PAGE_ALIAS[base] && fs.existsSync(path.join(DIR, PAGE_ALIAS[base] + '.html')) ? PAGE_ALIAS[base] : base;
554
599
  if (!name.includes('/') && fs.existsSync(path.join(DIR, name + '.html'))) {
555
600
  try { res.writeHead(200, { 'content-type': MIME['.html'], 'cache-control': 'no-store' }); return res.end(page(name + '.html').html); }
556
601
  catch (e) { res.writeHead(500, { 'content-type': 'text/plain' }); return res.end('render failed: ' + e.stack); }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.56",
3
+ "version": "1.21.57",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.56",
9
+ "version": "1.21.57",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.56",
3
+ "version": "1.21.57",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9108,5 +9108,11 @@
9108
9108
  "id": "1004581",
9109
9109
  "text": "The street no longer shows the small grey line of words above the buildings. The owner wants the city to read at a glance from its look alone, and the line was also wrong in light mode. It is gone in light and dark and on phon"
9110
9110
  }
9111
+ ],
9112
+ "1.21.57": [
9113
+ {
9114
+ "id": "1002301",
9115
+ "text": "Guilds now have their own page on cloudbongos.com — who is in it and what it has shipped together — and members can choose whether they appear on it and count toward its totals."
9116
+ }
9111
9117
  ]
9112
9118
  }
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.56'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.57'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -305,6 +305,19 @@ function buildPlatformApp() {
305
305
  if (!html) return next();
306
306
  res.type('html').send(html);
307
307
  });
308
+ // THE GUILD PAGE (task 1002301, R25; ADR 0336 D4/D6): /g/<slug>, the namespace
309
+ // D6 reserved. Hub-only for the /account reason — every read it makes is a
310
+ // platform-identity route. ONE shell for every slug: the page reads the public
311
+ // guild and, signed in, the members' own-scope read, so whether a guild exists
312
+ // (and whether an unlisted one is yours) is answered by those routes and never
313
+ // by this one. A segment that cannot be a slug falls through to the 404 tail.
314
+ app.get('/g/:slug', (req, res, next) => {
315
+ if (!isApexHost(req) || !isModuleEnabled('platform-identity')) return next();
316
+ if (!/^[A-Za-z0-9-]{2,39}$/.test(req.params.slug)) return next();
317
+ const html = landingPage('guild.html');
318
+ if (!html) return next();
319
+ res.type('html').send(html);
320
+ });
308
321
  // Assets fall through to the 404 tail when no module claims the apex root — the
309
322
  // handler is not mounted at all rather than pointed at a missing directory.
310
323
  const landingAssets = landingDir() && express.static(landingDir(), { index: false, fallthrough: true });