@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.
- package/.bongos-core.json +47 -22
- package/docs/copy-inventory.md +57 -19
- package/docs/copy-registry.json +450 -83
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +32 -4
- package/docs/page-readings.json +69 -1
- package/modules/public-landing/public/guild.html +571 -0
- package/modules/public-landing/public/guild.probes.json +53 -0
- package/modules/public-landing/public/guild.states.json +38 -0
- package/modules/ui-design/kit/fixtures/guilds.json +73 -0
- package/modules/ui-design/kit/serve.js +46 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +13 -0
- package/tests/hub_guild_page.mjs +537 -0
- package/tests/privacy_matrix.mjs +126 -14
- package/tests/render_check.mjs +2 -2
- package/tests/ui_design_kit.mjs +3 -3
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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.
|
|
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');
|
package/src/platform-server.js
CHANGED
|
@@ -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 });
|