@bongos/core 1.19.428 → 1.19.429

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.
@@ -18,6 +18,7 @@ const express = require('express');
18
18
  const api = require('../../../src/module-api');
19
19
  const pi = require('../platform-identity');
20
20
  const scoutingAuthz = require('../scouting-authz');
21
+ const canonicalProfile = require('../canonical-profile');
21
22
  const hubKeys = require('../hub-keys');
22
23
  const hubDevice = require('../hub-device');
23
24
  // task 1003208: structured logging (pino via the doorway) — was console.*.
@@ -120,6 +121,12 @@ function registerSeams() {
120
121
  // The ownership fact lives in this module, so the answer does too; core
121
122
  // asks through the port and treats an absent one as a denial.
122
123
  isProjectOwner: (githubId) => scoutingAuthz.hasOwnerMembership(githubId, { pool }),
124
+ // task 1002804 (P08, ADR 0214 D3): the account's PUBLIC handle — the
125
+ // canonical profile address — so the numeric profile route can name a
126
+ // builder by the rename-proof key instead of a mutable github_login.
127
+ // Floors on the EXISTENCE predicate, not the rollup one: hide_stats
128
+ // suppresses numbers, never addressability.
129
+ getPublicHandle: (githubId) => canonicalProfile.getPublicHandle(githubId, { pool }),
123
130
  });
124
131
  }
125
132
  }
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.428",
3
+ "version": "1.19.429",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.428",
9
+ "version": "1.19.429",
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.19.428",
3
+ "version": "1.19.429",
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",
@@ -32,6 +32,7 @@ const { validateOrRespond, LIMITS, parseId } = require('./_helpers');
32
32
  // 0093 §1): GET /builders/:id/onboarding resolves the `onboarding` kernel port
33
33
  // for the roster-visible checklist (read/shape) — never a direct require.
34
34
  const { pool } = require('../pool');
35
+ const { publicReadRateLimitMiddleware } = require('../middleware/rate-limit');
35
36
  const seams = require('../../module-seams');
36
37
  const { branding } = require('../../branding');
37
38
  // task 1003208: structured logging — was console.*. A core file logs through
@@ -480,7 +481,17 @@ module.exports = function buildBuildersRouter() {
480
481
  // monthly_budget_usd / system_role / box_blocked*) are NEVER spread into the
481
482
  // JSON. A leak-prevention unit test (tests/profile_route.mjs) asserts their
482
483
  // absence + the 404-on-unknown-id behaviour.
483
- router.get('/builders/:id/profile', async (req, res) => {
484
+ // THROTTLED since task 1002804. The global limiter meters WRITES only
485
+ // (rateLimitMiddleware returns early for GET) and the public read limiter is
486
+ // mounted under /public/*, which this is not — so this route answered at line
487
+ // rate. That was tolerable while it re-presented leaderboard data; it is not
488
+ // now that it also carries `handle`, because an id walk then yields the
489
+ // builder → canonical-profile-handle map in one call per builder.
490
+ // modules/platform-identity/routes/public-profile.js caps /profiles/:handle
491
+ // for exactly that mapping, so the two routes that publish it get the same
492
+ // ceiling. The EXISTING shared /public/* budget (120/60s per IP), not a new
493
+ // one — ADR 0209's rule that routes answering one question spend one budget.
494
+ router.get('/builders/:id/profile', publicReadRateLimitMiddleware, async (req, res) => {
484
495
  const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
485
496
  if (targetId === null) return;
486
497
  try {
@@ -493,11 +504,21 @@ module.exports = function buildBuildersRouter() {
493
504
  // shelf degrades to empty and the cross_project section is absent, not a 500.
494
505
  const reward = seams.resolveOptional('reward');
495
506
  const hub = seams.resolveOptional('platform-identity.hub');
496
- const [shippedCount, recentShips, achievements, crossProject] = await Promise.all([
507
+ // handle: the account's PUBLIC canonical-profile address (P08, task
508
+ // 1002804 / ADR 0214 D3). It rides a DIFFERENT predicate from
509
+ // cross_project on purpose — existence, not the rollup — because
510
+ // hide_stats suppresses numbers and never addressability, so a builder
511
+ // who hides their stats still has a canonical URL and a connect button.
512
+ // Absent (null) for no account, provisional and private alike, which is
513
+ // the same indistinguishable answer cross_project already gives.
514
+ // Optional-chained so an older hub provider without the function degrades
515
+ // to null rather than throwing across a version window.
516
+ const [shippedCount, recentShips, achievements, crossProject, handle] = await Promise.all([
497
517
  lc().countShippedTasksForBuilder(targetId),
498
518
  lc().recentShippedTasksByBuilder(targetId, 5),
499
519
  reward ? reward.getBuilderAchievements(targetId) : [],
500
520
  hub ? hub.getPublicProfileExtras(b.github_id) : null,
521
+ hub && hub.getPublicHandle ? hub.getPublicHandle(b.github_id) : null,
501
522
  ]);
502
523
  // WHITELIST — only the leaderboard-equivalent public field set. Never spread
503
524
  // the raw row: it carries anthropic_email / anthropic_user_id /
@@ -531,6 +552,7 @@ module.exports = function buildBuildersRouter() {
531
552
  recent_ships: recentShips,
532
553
  achievements,
533
554
  cross_project: crossProject,
555
+ handle: handle || null,
534
556
  });
535
557
  }
536
558
  res.json({
@@ -41,6 +41,7 @@ const { attachFail } = require('./middleware/error-envelope');
41
41
  const metrics = require('./metrics');
42
42
  const { logger } = require('./logger');
43
43
  const gdsAuth = require('./auth');
44
+ const { loadIdpConfig } = require('./auth-config');
44
45
  const { buildStampedHtml } = require('../asset-version');
45
46
  const { buildInfo } = require('../build-info');
46
47
  const { buildDevboxDownloads } = require('../devbox-downloads');
@@ -164,7 +165,7 @@ function injectBrandingGlobal(html, brand = clientBranding(), mods = clientModul
164
165
  // fill cannot reach them, and a baked one would make every instance advertise
165
166
  // whichever instance happened to author the file (task 1002748, direction C5/C6).
166
167
  // Pure + exported for the test; both origins are injectable so it drives with zero I/O.
167
- function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl(), instanceBase = safeInstanceBase()) {
168
+ function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOrigin(), hallOrigin = safeBuildersOrigin(), sourceRepo = safeSourceRepoUrl(), instanceBase = safeInstanceBase(), hubOrigin = safeHubOrigin()) {
168
169
  const world = (brand.identity && brand.identity.worldName) || '';
169
170
  const product = (brand.identity && brand.identity.productName) || '';
170
171
  const tagline = (brand.copy && brand.copy.tagline) || '';
@@ -182,7 +183,13 @@ function applyBrandTokens(html, brand = clientBranding(), origin = safePublicOri
182
183
  // whichever instance authored the file
183
184
  .split('{{buildersOrigin}}').join(esc(hall))
184
185
  .split('{{sourceRepoUrl}}').join(esc(sourceRepo || ''))
185
- .split('{{instanceBase}}').join(esc(instanceBase || ''));
186
+ .split('{{instanceBase}}').join(esc(instanceBase || ''))
187
+ // The PLATFORM HUB's apex — where a builder's canonical cross-project
188
+ // profile lives (P08, task 1002804 / ADR 0214 D2). Empty on a vanilla
189
+ // instance, and a page that reads it MUST render nothing rather than fall
190
+ // back: never write this token straight into an href, because an empty
191
+ // value would leave a same-host relative URL claiming to be the hub.
192
+ .split('{{hubOrigin}}').join(esc(hubOrigin || ''));
186
193
  }
187
194
  // A link whose href is a BARE origin token that resolves empty is dropped whole,
188
195
  // server-side (task 1003242; the branding contract): a pack that names no domains
@@ -231,6 +238,25 @@ function safeBuildersOrigin() {
231
238
  function safeInstanceBase() {
232
239
  try { return branding().domains.instanceBase || ''; } catch { return ''; }
233
240
  }
241
+ // The PLATFORM HUB's apex — the origin a builder's canonical cross-project
242
+ // profile is addressed on (P08, task 1002804; ADR 0171 D1 named this gap, ADR
243
+ // 0214 D2 fills it). Three cases, in the order they are asked:
244
+ // * THIS instance IS the hub — `platform-identity` enabled is the hub flag
245
+ // itself (src/platform-server.js), not a proxy for it — so the hub apex is
246
+ // its own publicOrigin;
247
+ // * a FEDERATED instance — the hub it signs in through, from the same
248
+ // `<PREFIX>_HUB_ORIGIN` env that auth-config's loadIdpConfig resolves; there
249
+ // is no second source of truth for "which hub am I on";
250
+ // * a VANILLA instance — no hub, so '' and every consumer renders nothing.
251
+ // Best-effort like its four siblings above: a branding or env hiccup must resolve
252
+ // to "no hub", never break every served page.
253
+ function safeHubOrigin() {
254
+ try {
255
+ if (isModuleEnabled('platform-identity')) return safePublicOrigin();
256
+ const idp = loadIdpConfig();
257
+ return (idp && idp.origin) || '';
258
+ } catch { return ''; }
259
+ }
234
260
  // The instance's own source repo, from the pack's `repo: {owner, name}` binding —
235
261
  // another server-only field (the branding contract keeps repo off clientBranding
236
262
  // alongside domains). A page that wants to link "read the source" must take it
package/src/module-api.js CHANGED
@@ -33,7 +33,7 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
33
33
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
34
34
  // the entry to that file. Look for a version's history there, not here.
35
35
  // ---------------------------------------------------------------------------
36
- const CORE_VERSION = '1.19.428'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
36
+ const CORE_VERSION = '1.19.429'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
37
37
 
38
38
  // A namespaced logger so a module's log lines are attributable + consistent.
39
39
  // Usage: const log = api.logger('dev-box'); log.info('mounted');