@bongos/core 1.19.389 → 1.19.390

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.19.389",
6
- "core_contract": "1.19.389",
7
- "source_commit": "6025dfdf6c2020fdf22152cb9edf610790cc83b0",
5
+ "core_version": "1.19.390",
6
+ "core_contract": "1.19.390",
7
+ "source_commit": "fca78267668e7bfe1241ef5502900854cb91a59d",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-08-27T23:26:50.061Z",
9
+ "built_at": "2026-08-28T01:40:58.750Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 339,
13
13
  "agent_docs_stubbed": 22,
14
- "functional_verbatim": 1620,
14
+ "functional_verbatim": 1622,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 1981,
20
- "tree_sha256": "1cb4d393dd04fb49bb9f7aaaea5ae98dc3b4d9ce595c3a00b3aa0d86c7db7c95",
19
+ "file_count": 1983,
20
+ "tree_sha256": "cfb7d40bbbc5a242cfd992804860f21603eb1faa51af33d4dd6739464e971005",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -5122,12 +5122,12 @@
5122
5122
  {
5123
5123
  "path": "package-lock.json",
5124
5124
  "mode": "0000644",
5125
- "sha256": "eea9d3bbfd5303579b774edd548952fb8faa1b88acc2d21a1091409a247ec3f0"
5125
+ "sha256": "6df325de41dee29b93290e06d42cba92acbc357b5475c2bbefa70c8f9d96df1b"
5126
5126
  },
5127
5127
  {
5128
5128
  "path": "package.json",
5129
5129
  "mode": "0000644",
5130
- "sha256": "f012bfc1226d3d1d3dbe511bdb4c734f2e6731c37f220126f773b88ef3303a8c"
5130
+ "sha256": "1f4bb5de0e76f44c4588ed68adc9731eb8aa0b5e22fd21d26b4f31880a655de0"
5131
5131
  },
5132
5132
  {
5133
5133
  "path": "public-docs/index.html",
@@ -6737,17 +6737,22 @@
6737
6737
  {
6738
6738
  "path": "scripts/hall-preview/README.md",
6739
6739
  "mode": "0000644",
6740
- "sha256": "12318c5928131ceb5dd7aa1322de0d4fd8116f80f1d41a84c0e1fd6b1f12a2c1"
6740
+ "sha256": "880fb5ec7749e6e3092d5869b1f6baa64adbb956933f139eabfaf25347e3b9d8"
6741
+ },
6742
+ {
6743
+ "path": "scripts/hall-preview/live-identity.js",
6744
+ "mode": "0000644",
6745
+ "sha256": "69edca9e3fd584f5e8f29aaf7451611e80c663be32114dc03cce1c5da22a9c5d"
6741
6746
  },
6742
6747
  {
6743
6748
  "path": "scripts/hall-preview/refresh-fixtures.js",
6744
6749
  "mode": "0000644",
6745
- "sha256": "ad8a601e499bc3c3b1fbd8d7180e8a2489461941e659169a7591899a778f7261"
6750
+ "sha256": "b5819ccfabd209ced41613c6f9109faa2723a49c017c115a27383d907f777f3b"
6746
6751
  },
6747
6752
  {
6748
6753
  "path": "scripts/hall-preview/server.js",
6749
6754
  "mode": "0000644",
6750
- "sha256": "eb2c9bde186fcd050c41d68697945f46b9d891e545cc65f81db71b647a7cb3d9"
6755
+ "sha256": "a55d67cab28ca941ad47a016bb5ed66d2fe1f410d0557544ee79ab57adc70006"
6751
6756
  },
6752
6757
  {
6753
6758
  "path": "scripts/migrate.sh",
@@ -7107,7 +7112,7 @@
7107
7112
  {
7108
7113
  "path": "src/module-api.js",
7109
7114
  "mode": "0000644",
7110
- "sha256": "6daab593739c8b3f37576399b8ba4cdcb6fdf82f8ce914ce1cdb4322ac8d1f24"
7115
+ "sha256": "b0af1d77cee68628e7693a9beab490c3c66d9aa6ff091bc6da61fef4fe0f48a7"
7111
7116
  },
7112
7117
  {
7113
7118
  "path": "src/module-loader/catalog.js",
@@ -8449,6 +8454,11 @@
8449
8454
  "mode": "0000644",
8450
8455
  "sha256": "a53080637382b2313c6ed16d013fec6102a604fe7b8c72260945b5c85cb9303b"
8451
8456
  },
8457
+ {
8458
+ "path": "tests/hall_preview_identity.mjs",
8459
+ "mode": "0000644",
8460
+ "sha256": "4c2dadec059bd6ae7c1f16ec67f9ce6dd1d39eaa10aa7d1ad6219619947ffab4"
8461
+ },
8452
8462
  {
8453
8463
  "path": "tests/hall_signout.mjs",
8454
8464
  "mode": "0000644",
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.389",
3
+ "version": "1.19.390",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.389",
9
+ "version": "1.19.390",
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.389",
3
+ "version": "1.19.390",
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",
@@ -21,11 +21,43 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
21
21
  | `--brand config/branding.json` | serve the instance pack (default: `config/branding.neutral.json`, the vanilla skin) |
22
22
  | `--modules-off dev-box,provisioning` | report those modules disabled in `window.__MODULES__` — verifies `moduleOn()` gating (Harbor nav item, Instances/Start-a-project widgets must vanish) |
23
23
  | `--empty` | drain every array in every fixture — the empty-state screenshot pass |
24
+ | `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
24
25
 
25
26
  Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly`.
26
27
  A GET with no fixture answers 404 and logs
27
28
  `no fixture: <path>` — that log line names the exact file to add.
28
29
 
30
+ ## The account is yours (task 1003315)
31
+
32
+ The fixtures are one particular builder's snapshot, so the harness used to greet
33
+ everyone as that builder — their name, credits, achievements, goals and **rank**.
34
+ A metic snapshot also sealed the Archon-gated pages, so a UI change to Watch /
35
+ Harbor / Gate / Permissions could not be reviewed at all.
36
+
37
+ So at boot the harness reads **your** account: if this machine holds a GDS CLI
38
+ session, the me-scoped endpoints (`/me`, `/me/*`, `/builders/<you>/…`,
39
+ `/analytics/builder/<you>`, `/inbox/by-builder/<you>`, `/inbox/awaiting-nod`) are
40
+ fetched live — read-only GETs — and layered over the fixtures in memory. The boot
41
+ banner names whose hall you are looking at:
42
+
43
+ ```
44
+ account: @scootermack (archon) — your own, live
45
+ ```
46
+
47
+ Nothing is written to `fixtures/` (that's `refresh-fixtures.js`'s job), so the
48
+ checked-in snapshot stays the fallback. It serves whenever the live read can't
49
+ happen — signed out, expired session, offline, or an endpoint your rank can't
50
+ call (that one keeps its own fixture and the rest of your identity still lands).
51
+ Every fallback says why, and says the hall is about to show you someone else:
52
+
53
+ ```
54
+ account: @jaxri (metic) — NOT YOURS, the checked-in fixture: this machine has no Bongos CLI session — run /builder-setup
55
+ ```
56
+
57
+ **`--fixture-me` pins the snapshot.** Use it for the screenshot matrix below —
58
+ a live account makes the images vary run to run — and to review the signed-out
59
+ fallback deliberately. `--rank <rank>` still overrides whichever `/me` is served.
60
+
29
61
  ## Fixtures
30
62
 
31
63
  `fixtures/<path>.json`, slashes as `__` (`/me/recent-ships` → `me__recent-ships.json`).
@@ -46,7 +78,8 @@ Box/terminal endpoints are deliberately never snapshotted (connection details).
46
78
 
47
79
  Screenshot every changed page at **{light, dark} × {1280w, 390w}**, plus one
48
80
  instance-pack run (`--brand config/branding.json`) proving the theming contract
49
- survives. With the Playwright MCP (`npx playwright install chrome` once):
81
+ survives. Run the harness with **`--fixture-me`** for these — the pinned snapshot
82
+ is what makes two runs comparable. With the Playwright MCP (`npx playwright install chrome` once):
50
83
 
51
84
  1. `browser_navigate` → the page. `browser_console_messages` must show **0 errors**.
52
85
  2. 1280w: `browser_resize 1280×900` → `browser_take_screenshot` (fullPage for tall pages).
@@ -0,0 +1,121 @@
1
+ 'use strict';
2
+ // scripts/hall-preview/live-identity.js — serve the SIGNED-IN builder's own
3
+ // account in the hall preview harness (task 1003315).
4
+ //
5
+ // The harness answers every read from checked-in fixtures, and fixtures/me.json
6
+ // is one particular builder's snapshot. So every preview greeted whoever ran it
7
+ // as somebody else ("Welcome back, Jackson Rini"), with that builder's credits,
8
+ // achievements, goals and RANK — and because that snapshot is a metic, the
9
+ // Archon-gated pages rendered their sealed notice, so a UI change to them could
10
+ // not be reviewed at all. Reviewing your own UI change against someone else's
11
+ // account is the bug.
12
+ //
13
+ // The fix: at boot, when the machine has a GDS CLI session, the me-scoped reads
14
+ // are fetched LIVE (read-only GETs) and held in memory as overrides layered on
15
+ // top of the fixtures. Nothing is ever written to fixtures/ — the checked-in
16
+ // snapshot stays the signed-out fallback and the deterministic pass for the
17
+ // screenshot matrix (`--fixture-me` pins it).
18
+ //
19
+ // Every dependency is injected, so this file is testable with no disk, no
20
+ // network and no session (tests/hall_preview_identity.mjs).
21
+
22
+ // The me-scoped reads — everything the hall renders as "you". ':id' expands to
23
+ // the signed-in builder's id. Box/terminal endpoints are deliberately absent,
24
+ // the same rule refresh-fixtures.js follows: they carry connection details.
25
+ const ME_SCOPED_PATHS = [
26
+ 'me',
27
+ 'me/recent-ships?limit=30',
28
+ 'me/cross-project',
29
+ 'me/profile-nudge',
30
+ 'me/render',
31
+ 'me/wandering',
32
+ 'me/skill-prefs',
33
+ 'me/sound-prefs',
34
+ 'me/event-sound-prefs',
35
+ 'me/art-key/own',
36
+ 'builders/:id/profile',
37
+ 'builders/:id/active-claims',
38
+ 'builders/:id/activity?section=tasks',
39
+ 'builders/:id/activity?section=goals',
40
+ 'analytics/builder/:id?days=90',
41
+ 'inbox/by-builder/:id',
42
+ // Permission-gated (idea.triage, Metic+), so whether this queue exists at all
43
+ // is a fact about YOUR rank — the needs-attention inbox renders it or not.
44
+ 'inbox/awaiting-nod?limit=20',
45
+ ];
46
+
47
+ // The key an override is stored under, and the key a request looks itself up
48
+ // by. The api path alone — EXCEPT where ?section= picks the row shape rather
49
+ // than the path (the profile's activity log), which is the same discrimination
50
+ // the harness's FIXTURE_FALLBACKS already makes for that one endpoint.
51
+ function overrideKey(apiPath, query = {}) {
52
+ const p = String(apiPath || '').replace(/\/$/, '');
53
+ return query && query.section ? `${p}?section=${query.section}` : p;
54
+ }
55
+
56
+ // A ':id' template → the concrete request path plus the key it lands under.
57
+ function expandPath(template, id) {
58
+ const [rawPath, search = ''] = String(template).split('?');
59
+ const p = rawPath.replace(':id', encodeURIComponent(String(id)));
60
+ const query = Object.fromEntries(new URLSearchParams(search));
61
+ return { key: overrideKey(p, query), url: search ? `${p}?${search}` : p };
62
+ }
63
+
64
+ // Why we fell back, in the builder's words rather than an HTTP status — this
65
+ // line is printed in the boot banner and is the only signal that the hall is
66
+ // about to greet them as someone else.
67
+ function fallbackReason(status) {
68
+ if (status === 401 || status === 403) return 'your CLI session is no longer valid — run /builder-reauth';
69
+ return `the instance answered GET /me with ${status}`;
70
+ }
71
+
72
+ // resolveLiveIdentity — fetch the caller's own me-scoped reads, or explain why
73
+ // not. NEVER throws and never exits: a preview harness that dies because the
74
+ // laptop is offline is worse than one showing fixture data, so every failure
75
+ // path degrades to { ok: false, why } and the fixtures keep serving.
76
+ async function resolveLiveIdentity({
77
+ session,
78
+ fetchImpl = globalThis.fetch,
79
+ apiPrefix = '/api/bongos/v1',
80
+ timeoutMs = 8000,
81
+ } = {}) {
82
+ if (!session || !session.token) {
83
+ return { ok: false, why: 'this machine has no Bongos CLI session — run /builder-setup' };
84
+ }
85
+ const base = String(session.apiBase || '').replace(/\/+$/, '');
86
+ if (!base) return { ok: false, why: 'the CLI session names no instance to read from' };
87
+
88
+ const get = async (url) => {
89
+ try {
90
+ const res = await fetchImpl(`${base}${apiPrefix}/${url}`, {
91
+ headers: { authorization: `Bearer ${session.token}`, accept: 'application/json' },
92
+ signal: AbortSignal.timeout(timeoutMs),
93
+ });
94
+ if (!res.ok) return { ok: false, status: res.status };
95
+ return { ok: true, data: await res.json() };
96
+ } catch (e) {
97
+ return { ok: false, status: 0, error: e && e.message };
98
+ }
99
+ };
100
+
101
+ const me = await get('me');
102
+ if (!me.ok) return { ok: false, why: me.status ? fallbackReason(me.status) : `could not reach ${base} (${me.error || 'network error'})` };
103
+ const builder = me.data && me.data.builder;
104
+ if (!builder || !builder.id) return { ok: false, why: 'the instance returned no builder on GET /me' };
105
+
106
+ const overrides = new Map([['me', me.data]]);
107
+ const skipped = [];
108
+ const rest = ME_SCOPED_PATHS.filter((t) => t !== 'me').map((t) => expandPath(t, builder.id));
109
+ const results = await Promise.all(rest.map(({ url }) => get(url)));
110
+ rest.forEach(({ key }, i) => {
111
+ // A read this builder's rank can't make (or one this instance doesn't
112
+ // serve) simply keeps its fixture — a partial identity still shows YOUR
113
+ // name, rank and credits, which is the whole point.
114
+ if (results[i].ok) overrides.set(key, results[i].data);
115
+ else skipped.push(key);
116
+ });
117
+
118
+ return { ok: true, builder, base, overrides, skipped };
119
+ }
120
+
121
+ module.exports = { ME_SCOPED_PATHS, overrideKey, expandPath, fallbackReason, resolveLiveIdentity };
@@ -24,6 +24,7 @@ const SNAPSHOTS = [
24
24
  ['versions', 'versions'],
25
25
  ['leaderboard', 'leaderboard'],
26
26
  ['inbox', 'inbox', trimList('ideas', 25)],
27
+ ['inbox/awaiting-nod?limit=20', 'inbox__awaiting-nod', trimList('awaiting', 25)],
27
28
  ['goals', 'goals'],
28
29
  ['goals/inbox', 'goals__inbox'],
29
30
  // goal details: a handful of REAL ids (exact-match fixtures) + goal 25
@@ -19,8 +19,16 @@
19
19
  // window.__MODULES__ (verifies moduleOn() gating)
20
20
  // --empty serve every fixture with its arrays emptied — the
21
21
  // empty-state screenshot pass
22
- // --rank archon override the /me fixture's builder.rank so the Archon-gated
23
- // pages (watch, harbor, gate) render their content in preview
22
+ // --rank archon override the served /me builder.rank (live or fixture) so the
23
+ // Archon-gated pages (watch, harbor, gate) render in preview
24
+ // --fixture-me do NOT read your own account live; serve the checked-in
25
+ // fixtures/me.json snapshot (deterministic screenshot passes,
26
+ // and the offline/signed-out path) — task 1003315
27
+ //
28
+ // The account is YOURS: when this machine holds a GDS CLI session the me-scoped
29
+ // reads are fetched live at boot and layered over the fixtures, so the hall
30
+ // greets the builder previewing the change rather than whoever's snapshot is
31
+ // checked in (task 1003315). Signed out, offline, or --fixture-me → fixtures.
24
32
  //
25
33
  // Browse http://builders.localhost:<port>/ — Chromium resolves *.localhost,
26
34
  // and the `builders.` host prefix exercises the real subdomain serving path.
@@ -41,9 +49,11 @@ function arg(name) {
41
49
  }
42
50
  const PORT = Number(arg('--port')) || 4600;
43
51
  const EMPTY = process.argv.includes('--empty');
44
- // --rank: view the hall as a different rank — the /me fixture's builder.rank is
52
+ // --rank: view the hall as a different rank — the served /me builder.rank is
45
53
  // overridden in the response, so the Archon-gated pages (watch, harbor, gate)
46
- // render their real content instead of the sealed notice. Preview-only theatre:
54
+ // render their real content instead of the sealed notice. Since task 1003315 the
55
+ // served /me is normally your OWN account, so this is now only needed to view
56
+ // the hall as a rank you are not. Preview-only theatre:
47
57
  // the real pages are server-gated (requireGovernmentPage / ADR 0016).
48
58
  const RANK_OVERRIDE = arg('--rank');
49
59
  const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.trim()).filter(Boolean);
@@ -51,6 +61,10 @@ const MODULES_OFF = String(arg('--modules-off') || '').split(',').map((s) => s.t
51
61
  // often lacks prod's instance modules (provisioning, dev-box), and their
52
62
  // widgets can't be reviewed without it (task 2169).
53
63
  const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.trim()).filter(Boolean);
64
+ // --fixture-me: pin the checked-in /me snapshot instead of reading the caller's
65
+ // own account live. The screenshot matrix in the README wants a stable subject,
66
+ // and it is the explicit way to preview the signed-out fallback (task 1003315).
67
+ const FIXTURE_ME = process.argv.includes('--fixture-me');
54
68
  const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
55
69
  process.env.GDS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
56
70
 
@@ -65,6 +79,21 @@ catch (_) { express = require('express'); }
65
79
  const { buildStampedHtml } = require(path.join(ROOT, 'src', 'asset-version.js'));
66
80
  const { injectBrandingGlobal, mountInternalSurfaces } = require(path.join(ROOT, 'src', 'bongos', 'serve-internal.js'));
67
81
  const { clientModules } = require(path.join(ROOT, 'src', 'modules.js'));
82
+ const { overrideKey, resolveLiveIdentity } = require('./live-identity');
83
+
84
+ // The session is the ONE thing this harness reads from outside its own tree, and
85
+ // it reads it through the FAIL-OPEN helper on purpose: apiCall/cliClient exit the
86
+ // process on a 401, which would kill a preview over a cosmetic feature.
87
+ function readSession() {
88
+ try { return require(path.join(ROOT, 'scripts', 'gds', 'cli-lib')).readSessionToken(); }
89
+ catch (_) { return null; }
90
+ }
91
+
92
+ // Live me-scoped payloads, keyed by overrideKey(); empty until boot resolves
93
+ // them, and empty forever when signed out / offline / --fixture-me.
94
+ let liveOverrides = new Map();
95
+ let liveBuilder = null;
96
+ let liveWhy = null;
68
97
 
69
98
  // ---- fixtures ---------------------------------------------------------------
70
99
  // File naming: the API path with slashes as "__" — /me/recent-ships →
@@ -151,7 +180,11 @@ app.use((req, res, next) => {
151
180
  if (req.method !== 'GET') {
152
181
  return res.status(501).json({ error: { code: 'preview_readonly', message: 'the preview harness serves fixtures; writes are not wired' } });
153
182
  }
154
- const fix = fixtureFor(apiPath, req.query || {});
183
+ // Your own account wins over the checked-in snapshot; a me-scoped read that
184
+ // wasn't resolved live falls through to its fixture exactly as before.
185
+ const q = req.query || {};
186
+ const live = liveOverrides.get(overrideKey(apiPath, q)) ?? liveOverrides.get(apiPath);
187
+ const fix = live !== undefined ? live : fixtureFor(apiPath, q);
155
188
  if (fix === undefined) {
156
189
  console.log(` no fixture: ${apiPath} (add scripts/hall-preview/fixtures/${apiPath.replace(/\//g, '__')}.json)`);
157
190
  return res.status(404).json({ error: { code: 'no_fixture', message: apiPath } });
@@ -197,10 +230,41 @@ app.get(/^\/([a-z-]+)$/, (req, res, next) => {
197
230
  // the page routes we already served.
198
231
  mountInternalSurfaces(app);
199
232
 
233
+ // Whose hall this is, said plainly in the banner. When it is NOT yours, that is
234
+ // the line that says so — the old harness printed nothing and the substitution
235
+ // was only visible as a stranger's name on the page (task 1003315).
236
+ function accountLine() {
237
+ if (liveBuilder) {
238
+ const who = liveBuilder.github_login ? `@${liveBuilder.github_login}` : liveBuilder.display_name;
239
+ return ` account: ${who} (${liveBuilder.rank}) — your own, live${RANK_OVERRIDE ? ` [rank forced to ${RANK_OVERRIDE}]` : ''}`;
240
+ }
241
+ const why = FIXTURE_ME ? '--fixture-me' : liveWhy;
242
+ // A published core carves fixtures/ out (publish-manifest.js) — there the
243
+ // fallback is not "someone else's account", it is NO account, and the hall
244
+ // will 404 its way through /me. Say which of the two this is.
245
+ const snap = (loadFixture('me') || {}).builder;
246
+ if (!snap) return ` account: NONE — no live read (${why}) and this checkout ships no me fixture`;
247
+ const who = snap.github_login ? `@${snap.github_login}` : (snap.display_name || 'a checked-in snapshot');
248
+ return ` account: ${who} (${RANK_OVERRIDE || snap.rank}) — NOT YOURS, the checked-in fixture: ${why}`;
249
+ }
250
+
200
251
  // Loopback only — this is a pre-auth preview of hall pages with fixture data;
201
252
  // *.localhost resolves to 127.0.0.1 so nothing else is needed.
202
- app.listen(PORT, '127.0.0.1', () => {
203
- console.log(`hall-preview: http://builders.localhost:${PORT}/`);
204
- console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
205
- console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
206
- });
253
+ (async () => {
254
+ if (!FIXTURE_ME) {
255
+ const res = await resolveLiveIdentity({ session: readSession() });
256
+ if (res.ok) {
257
+ liveOverrides = res.overrides;
258
+ liveBuilder = res.builder;
259
+ if (res.skipped.length) console.log(` (kept fixtures for: ${res.skipped.join(', ')})`);
260
+ } else {
261
+ liveWhy = res.why;
262
+ }
263
+ }
264
+ app.listen(PORT, '127.0.0.1', () => {
265
+ console.log(`hall-preview: http://builders.localhost:${PORT}/`);
266
+ console.log(accountLine());
267
+ console.log(` brand: ${BRAND_FILE}${EMPTY ? ' (EMPTY fixtures)' : ''}${MODULES_OFF.length ? ` modules off: ${MODULES_OFF.join(',')}` : ''}`);
268
+ console.log(' pages:', Object.keys(stamped).map((f) => f.replace(/\.html$/, '')).sort().join(' '));
269
+ });
270
+ })();
package/src/module-api.js CHANGED
@@ -1218,8 +1218,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1218
1218
  // landed since 1.19.387 with no explicit bump. run 33125589814. (task 1002620)
1219
1219
  // 1.19.389 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1220
1220
  // landed since 1.19.388 with no explicit bump. run 33126279278. (task 1002620)
1221
+ // 1.19.390 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1222
+ // landed since 1.19.389 with no explicit bump. run 33133575931. (task 1002620)
1221
1223
  // ---------------------------------------------------------------------------
1222
- const CORE_VERSION = '1.19.389'; // CI auto-patch carrier (ADR 0161); see changelog above
1224
+ const CORE_VERSION = '1.19.390'; // CI auto-patch carrier (ADR 0161); see changelog above
1223
1225
 
1224
1226
  // A namespaced logger so a module's log lines are attributable + consistent.
1225
1227
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,163 @@
1
+ // tests/hall_preview_identity.mjs — hermetic unit test for the hall preview
2
+ // harness's live-identity layer (task 1003315: the preview greeted every
3
+ // builder as whoever's snapshot was checked into fixtures/me.json).
4
+ //
5
+ // NO disk, NO network, NO session: the session object and fetch are injected.
6
+ //
7
+ // Run: node tests/hall_preview_identity.mjs
8
+
9
+ import { strict as assert } from 'node:assert';
10
+ import { createRequire } from 'node:module';
11
+
12
+ const require = createRequire(import.meta.url);
13
+ const lib = require('../scripts/hall-preview/live-identity.js');
14
+
15
+ let passed = 0, failed = 0;
16
+ function t(name, fn) {
17
+ try { fn(); passed++; console.log(` PASS ${name}`); }
18
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
19
+ }
20
+ async function ta(name, fn) {
21
+ try { await fn(); passed++; console.log(` PASS ${name}`); }
22
+ catch (e) { failed++; console.log(` FAIL ${name}\n ${e.stack || e.message}`); }
23
+ }
24
+
25
+ const SESSION = { token: 'tok', apiBase: 'https://example.test' };
26
+ const ME = { builder: { id: '1000094', github_login: 'scootermack', rank: 'archon' } };
27
+
28
+ // A fetch stub: routes are keyed by the api path after /api/bongos/v1/. A number
29
+ // means "answer that status"; a missing key means 404.
30
+ function stubFetch(routes, { record } = {}) {
31
+ return async (url) => {
32
+ const p = String(url).split('/api/bongos/v1/')[1];
33
+ if (record) record.push(p);
34
+ const hit = Object.prototype.hasOwnProperty.call(routes, p) ? routes[p] : undefined;
35
+ if (hit === undefined) return { ok: false, status: 404, json: async () => ({}) };
36
+ if (typeof hit === 'number') return { ok: false, status: hit, json: async () => ({}) };
37
+ return { ok: true, status: 200, json: async () => hit };
38
+ };
39
+ }
40
+ const allOk = (extra = {}) => stubFetch(Object.assign({ me: ME }, extra));
41
+
42
+ // ---------- key derivation ----------
43
+ t('overrideKey: the path alone, except where ?section= picks the row shape', () => {
44
+ assert.equal(lib.overrideKey('me'), 'me');
45
+ assert.equal(lib.overrideKey('me/'), 'me', 'a trailing slash is the same key');
46
+ assert.equal(lib.overrideKey('builders/7/activity', { section: 'goals' }), 'builders/7/activity?section=goals');
47
+ assert.equal(lib.overrideKey('builders/7/activity', { limit: '20' }), 'builders/7/activity',
48
+ 'a query that is NOT ?section= does not fork the key');
49
+ });
50
+
51
+ t('expandPath: :id expands, and the key drops the non-discriminating query', () => {
52
+ assert.deepEqual(lib.expandPath('builders/:id/profile', '1000094'),
53
+ { key: 'builders/1000094/profile', url: 'builders/1000094/profile' });
54
+ assert.deepEqual(lib.expandPath('me/recent-ships?limit=30', '1'),
55
+ { key: 'me/recent-ships', url: 'me/recent-ships?limit=30' },
56
+ 'limit is a fetch detail, not part of the key the harness looks up');
57
+ assert.deepEqual(lib.expandPath('builders/:id/activity?section=goals', '9'),
58
+ { key: 'builders/9/activity?section=goals', url: 'builders/9/activity?section=goals' });
59
+ });
60
+
61
+ t('ME_SCOPED_PATHS: /me leads, and no box/terminal read is snapshotted', () => {
62
+ assert.equal(lib.ME_SCOPED_PATHS[0], 'me');
63
+ const box = lib.ME_SCOPED_PATHS.filter((p) => /(^|\/)box(\/|$)|terminal/.test(p));
64
+ assert.deepEqual(box, [], 'box endpoints carry connection details and are never fetched here');
65
+ });
66
+
67
+ // ---------- the fail-open paths (a preview must never die over this) ----------
68
+ await ta('no session: falls back, and never touches the network', async () => {
69
+ let called = 0;
70
+ const res = await lib.resolveLiveIdentity({ session: null, fetchImpl: async () => { called += 1; } });
71
+ assert.equal(res.ok, false);
72
+ assert.equal(called, 0);
73
+ assert.match(res.why, /builder-setup/);
74
+ });
75
+
76
+ await ta('a session with no apiBase: falls back rather than fetching a bare path', async () => {
77
+ const res = await lib.resolveLiveIdentity({
78
+ session: { token: 'tok' },
79
+ fetchImpl: async () => { throw new Error('should not fetch'); },
80
+ });
81
+ assert.equal(res.ok, false);
82
+ assert.match(res.why, /names no instance/);
83
+ });
84
+
85
+ await ta('401: falls back with the reauth line, NOT an exit (cli-lib apiCall would exit)', async () => {
86
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: stubFetch({ me: 401 }) });
87
+ assert.equal(res.ok, false);
88
+ assert.match(res.why, /builder-reauth/);
89
+ });
90
+
91
+ await ta('offline: falls back with the unreachable instance named', async () => {
92
+ const res = await lib.resolveLiveIdentity({
93
+ session: SESSION,
94
+ fetchImpl: async () => { throw new Error('ECONNREFUSED'); },
95
+ });
96
+ assert.equal(res.ok, false);
97
+ assert.match(res.why, /example\.test/);
98
+ assert.match(res.why, /ECONNREFUSED/);
99
+ });
100
+
101
+ await ta('a /me with no builder: falls back (a half-identity is worse than the fixture)', async () => {
102
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: stubFetch({ me: { builder: null } }) });
103
+ assert.equal(res.ok, false);
104
+ assert.match(res.why, /no builder/);
105
+ });
106
+
107
+ // ---------- the happy path ----------
108
+ await ta('signed in: /me is YOUR account, keyed for the harness lookup', async () => {
109
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl: allOk() });
110
+ assert.equal(res.ok, true);
111
+ assert.equal(res.builder.github_login, 'scootermack');
112
+ assert.equal(res.builder.rank, 'archon', 'the RANK is what unseals the Archon-gated pages in preview');
113
+ assert.deepEqual(res.overrides.get('me'), ME);
114
+ });
115
+
116
+ await ta('id-bearing reads are fetched for YOUR id, never the fixture builder id', async () => {
117
+ const record = [];
118
+ const fetchImpl = stubFetch({ me: ME, 'builders/1000094/profile': { bio: 'mine' } }, { record });
119
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
120
+ assert.deepEqual(res.overrides.get('builders/1000094/profile'), { bio: 'mine' });
121
+ assert.ok(record.includes('builders/1000094/profile'));
122
+ assert.ok(!record.some((p) => p.includes('/95/')), 'never the checked-in fixture builder id');
123
+ });
124
+
125
+ await ta('the two activity sections land under DIFFERENT keys (row shapes differ)', async () => {
126
+ const fetchImpl = stubFetch({
127
+ me: ME,
128
+ 'builders/1000094/activity?section=tasks': { rows: ['task'] },
129
+ 'builders/1000094/activity?section=goals': { rows: ['goal'] },
130
+ });
131
+ const res = await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
132
+ assert.deepEqual(res.overrides.get('builders/1000094/activity?section=tasks'), { rows: ['task'] });
133
+ assert.deepEqual(res.overrides.get('builders/1000094/activity?section=goals'), { rows: ['goal'] });
134
+ });
135
+
136
+ await ta('a read this rank cannot make keeps its fixture; the rest still resolves', async () => {
137
+ const res = await lib.resolveLiveIdentity({
138
+ session: SESSION,
139
+ fetchImpl: stubFetch({ me: ME, 'inbox/awaiting-nod?limit=20': 403 }),
140
+ });
141
+ assert.equal(res.ok, true, 'a partial identity still shows YOUR name, rank and credits');
142
+ assert.ok(res.skipped.includes('inbox/awaiting-nod'), 'skipped, so the fixture serves it');
143
+ assert.equal(res.overrides.has('inbox/awaiting-nod'), false);
144
+ });
145
+
146
+ await ta('every request carries the session bearer, is a GET, and is time-boxed', async () => {
147
+ const seen = [];
148
+ const fetchImpl = async (url, init) => {
149
+ seen.push(init);
150
+ const p = String(url).split('/api/bongos/v1/')[1];
151
+ return { ok: p === 'me', status: p === 'me' ? 200 : 404, json: async () => ME };
152
+ };
153
+ await lib.resolveLiveIdentity({ session: SESSION, fetchImpl });
154
+ assert.ok(seen.length > 1);
155
+ for (const init of seen) {
156
+ assert.equal(init.headers.authorization, 'Bearer tok');
157
+ assert.equal(init.method, undefined, 'no method set means GET; the harness never writes');
158
+ assert.ok(init.signal, 'every read is time-boxed so boot cannot hang');
159
+ }
160
+ });
161
+
162
+ console.log(`\nhall_preview_identity: ${passed} passed, ${failed} failed`);
163
+ process.exit(failed ? 1 : 0);