@bongos/core 1.19.790 → 1.19.792

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.
Files changed (41) hide show
  1. package/.bongos-core.json +76 -41
  2. package/.claude/skills/builder-reauth/SKILL.md +5 -3
  3. package/README.md +1 -1
  4. package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +2 -0
  5. package/docs/adr/0294-cli-sessions-do-not-expire.md +56 -0
  6. package/docs/adr/README.md +1 -0
  7. package/docs/copy-inventory.md +90 -90
  8. package/docs/copy-registry.json +109 -109
  9. package/docs/module-api-changelog.md +4 -0
  10. package/migrations/core_243_cli_sessions_never_expire.sql +58 -0
  11. package/modules/hall-ui/public/hall-kit.js +7 -65
  12. package/modules/hall-ui/public/hall-render.js +1 -1
  13. package/modules/hall-ui/public/hall-visual.js +85 -0
  14. package/modules/hall-ui/public/index.html +1 -0
  15. package/modules/hall-ui/public/profile.html +1 -0
  16. package/modules/hall-ui/public/profile.js +2 -2
  17. package/modules/hall-ui/public/settings.js +5 -2
  18. package/modules/hall-ui/public/studio.css +1 -1
  19. package/modules/hall-ui/public/studio.html +1 -0
  20. package/modules/hall-ui/public/studio.js +4 -3
  21. package/modules/hall-ui/public/style.css +0 -16
  22. package/modules/hall-ui/public/task.html +1 -0
  23. package/modules/hall-ui/public/task.js +10 -13
  24. package/package-lock.json +2 -2
  25. package/package.json +1 -1
  26. package/scripts/gds/accessibility-replay.js +195 -0
  27. package/scripts/gds/box-auth-check.js +1 -1
  28. package/scripts/gds/cli-lib.js +2 -2
  29. package/scripts/gds/reauth.js +9 -7
  30. package/scripts/gds/run-unit-tests.js +7 -0
  31. package/src/bongos/auth-github.js +2 -0
  32. package/src/bongos/db-kernel.js +29 -11
  33. package/src/bongos/db.js +33 -24
  34. package/src/bongos/routes/auth.js +3 -2
  35. package/src/module-api.js +1 -1
  36. package/tests/accessibility_replay.mjs +201 -0
  37. package/tests/auth_session_expiry_db.mjs +183 -0
  38. package/tests/auth_session_ttl.mjs +124 -11
  39. package/tests/fixtures/accessibility-replay.json +93 -0
  40. package/tests/hall_studio_world.mjs +19 -11
  41. package/tests/task_visuals.mjs +91 -27
@@ -106,15 +106,17 @@ test('the verdict is offered only to a viewer the routes will accept', () => {
106
106
  assert.match(JS, /studio-verdict__sealed/, 'a viewer below the floor gets a sentence, not three dead buttons');
107
107
  });
108
108
 
109
- test('the review card shows the reviewed work\'s picture, through the kit', () => {
110
- // "the picture is the card" — and drawn by the kit's ONE full-size renderer
111
- // rather than a third hand-rolled <figure> (the task's own instruction).
112
- assert.match(JS, /kit\.taskVisualFigureHtml\(/, 'the plate comes from the kit renderer');
109
+ test('the review card shows the reviewed work\'s picture, through the shared renderer', () => {
110
+ // "the picture is the card" — and drawn by the ONE full-size renderer rather
111
+ // than a hand-rolled <figure>. That renderer moved out of hall-kit.js into
112
+ // hall-visual.js in task 1003835, so the Studio now reaches for OTBVisual; the
113
+ // rule this pins is unchanged, only where the one copy lives.
114
+ assert.match(JS, /vis\.taskVisualFigureHtml\(/, 'the plate comes from the shared renderer');
113
115
  assert.ok(!/<figure/.test(JS), 'studio.js builds no <figure> of its own');
114
- assert.match(HTML, /hall-kit\.js/, 'and the page loads the kit that owns it');
116
+ assert.match(HTML, /hall-visual\.js/, 'and the page loads the file that owns it');
115
117
  // The degrade: a review with no visual (or one that could not be read) draws
116
118
  // nothing at all rather than an empty frame, so it still reads as a review.
117
- assert.match(JS, /if \(!p\.shipped \|\| !kit/, 'no reviewed task, no kit → no plate');
119
+ assert.match(JS, /if \(!p\.shipped \|\| !vis/, 'no reviewed task, no renderer → no plate');
118
120
  // The reviewed task is read for the card in focus, once — not 50 times up front.
119
121
  assert.match(JS, /p\.hydrated = true;/, 'the read is made once per review');
120
122
  assert.match(JS, /\$\{API\}\/tasks\/\$\{encodeURIComponent\(p\.shippedId\)\}/, 'and it reads the SHIPPED task the review names');
@@ -204,7 +206,8 @@ test('the accent is spent only where the rule allows', () => {
204
206
  });
205
207
 
206
208
  // ---------------------------------------------------------------------------
207
- // Boot the REAL studio.js under a DOM stub, with the REAL hall-kit.js beside it
209
+ // Boot the REAL studio.js under a DOM stub, with the REAL hall-kit.js and
210
+ // hall-visual.js beside it
208
211
  // (the bootShell pattern below, pointed at the page script). The static-source
209
212
  // assertions above pin the wiring; these render the card and read what it
210
213
  // actually says, which is the only way to pin a DEGRADE — "no picture" and
@@ -215,11 +218,15 @@ test('the accent is spent only where the rule allows', () => {
215
218
  // NEW_TASK_ID so the "needs work" pair can be followed to its merge.
216
219
  const NEW_TASK_ID = '2001';
217
220
  async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags = null, onWrite = null } = {}) {
218
- // The REAL kit, not a stub — the plate's URL guard and escaping are its code,
219
- // and a stub here would make the degrade tests below pass for the wrong reason
220
- // (no kit also means no plate). It is node-require-safe by its own contract.
221
+ // The REAL modules, not stubs — the plate's URL guard and escaping are their
222
+ // code, and a stub here would make the degrade tests below pass for the wrong
223
+ // reason (no renderer also means no plate). Both are node-require-safe by their
224
+ // own contract. hall-visual.js carries the renderer since task 1003835; the kit
225
+ // is still loaded because the page still uses the rest of it.
221
226
  const kit = (await import(`file://${path.join(HALL, 'hall-kit.js').replace(/\\/g, '/')}`)).default;
222
- assert.equal(typeof kit.taskVisualFigureHtml, 'function', 'the real kit loaded');
227
+ const vis = (await import(`file://${path.join(HALL, 'hall-visual.js').replace(/\\/g, '/')}`)).default;
228
+ assert.equal(typeof vis.taskVisualFigureHtml, 'function', 'the real visual module loaded');
229
+ assert.equal(typeof kit.taskVisualFigureHtml, 'undefined', 'and the kit no longer carries a second copy');
223
230
  const els = new Map();
224
231
  const el = (id) => {
225
232
  if (!els.has(id)) els.set(id, { id, innerHTML: '', textContent: '', hidden: false, value: '' });
@@ -258,6 +265,7 @@ async function bootStudio({ reviews = [], shipped = {}, rank = 'metic', flags =
258
265
  errMessage: (d) => (d && d.error && d.error.message) || '',
259
266
  },
260
267
  OTBKit: kit,
268
+ OTBVisual: vis,
261
269
  scrollTo: () => {},
262
270
  };
263
271
  const document = { addEventListener: (t, h) => handlers.push({ t, h }) };
@@ -13,13 +13,16 @@ import { createRequire } from 'node:module';
13
13
  import os from 'node:os';
14
14
  import path from 'node:path';
15
15
  import fs from 'node:fs/promises';
16
+ import fsSync from 'node:fs';
16
17
  const require = createRequire(import.meta.url);
17
18
 
18
19
  // Point storage at a throwaway dir BEFORE requiring the module.
19
20
  const TMP = path.join(os.tmpdir(), `otb-taskvisual-${process.pid}`);
20
21
  process.env.TASK_VISUALS_DIR = TMP;
21
22
  const tv = require('../modules/lifecycle/task-visuals.js');
22
- const kit = require('../modules/hall-ui/public/hall-kit.js');
23
+ // The renderers left hall-kit.js for hall-visual.js (task 1003835) — the task
24
+ // record page draws a visual and needs nothing else the kit holds.
25
+ const vis = require('../modules/hall-ui/public/hall-visual.js');
23
26
 
24
27
  let passed = 0, failed = 0;
25
28
  function t(name, fn) {
@@ -140,7 +143,7 @@ await t('persistImageBuffer: writes a name the serve route accepts, under the ri
140
143
  assert.equal(tv.taskIdFromName(r.file), 1003109);
141
144
  assert.equal(r.url, `/api/bongos/task-visuals/${r.file}`);
142
145
  // Both prefixes are permanent aliases, so a URL minted under either resolves.
143
- assert.equal(kit.isTaskVisualUrl(r.url), true);
146
+ assert.equal(vis.isTaskVisualUrl(r.url), true);
144
147
  assert.equal(r.bytes, PNG.length);
145
148
  const onDisk = await fs.readFile(path.join(TMP, r.file));
146
149
  assert.deepEqual(onDisk, PNG);
@@ -170,23 +173,23 @@ await t('deleteStored: removes by name or URL, is idempotent, refuses a traversa
170
173
  });
171
174
 
172
175
  // ---- the client-side URL guard --------------------------------------------
173
- await t('hall-kit: only a server-minted visual URL is ever rendered', () => {
174
- assert.equal(kit.isTaskVisualUrl('/api/gds/task-visuals/task-1003109-0011223344556677.png'), true);
175
- assert.equal(kit.isTaskVisualUrl('/api/bongos/task-visuals/task-1-aabbccddeeff0011.webp'), true);
176
+ await t('hall-visual: only a server-minted visual URL is ever rendered', () => {
177
+ assert.equal(vis.isTaskVisualUrl('/api/gds/task-visuals/task-1003109-0011223344556677.png'), true);
178
+ assert.equal(vis.isTaskVisualUrl('/api/bongos/task-visuals/task-1-aabbccddeeff0011.webp'), true);
176
179
  // Anything that is not that exact shape renders nothing — including an
177
180
  // absolute URL to another origin, and a javascript: payload.
178
- assert.equal(kit.isTaskVisualUrl('https://evil.example/x.png'), false);
179
- assert.equal(kit.isTaskVisualUrl('javascript:alert(1)'), false);
180
- assert.equal(kit.isTaskVisualUrl('/api/gds/task-visuals/../../secrets'), false);
181
- assert.equal(kit.isTaskVisualUrl(''), false);
182
- assert.equal(kit.isTaskVisualUrl(null), false);
183
- });
184
-
185
- await t('hall-kit: no visual renders nothing at all — never a placeholder', () => {
186
- assert.equal(kit.shipVisualThumbHtml({ id: 1 }), '');
187
- assert.equal(kit.shipVisualThumbHtml({ id: 1, visual_url: null }), '');
188
- assert.equal(kit.shipVisualThumbHtml(null), '');
189
- const html = kit.shipVisualThumbHtml({ id: 1, visual_url: '/api/gds/task-visuals/task-1-0011223344556677.png', visual_alt: 'a "before" shot' });
181
+ assert.equal(vis.isTaskVisualUrl('https://evil.example/x.png'), false);
182
+ assert.equal(vis.isTaskVisualUrl('javascript:alert(1)'), false);
183
+ assert.equal(vis.isTaskVisualUrl('/api/gds/task-visuals/../../secrets'), false);
184
+ assert.equal(vis.isTaskVisualUrl(''), false);
185
+ assert.equal(vis.isTaskVisualUrl(null), false);
186
+ });
187
+
188
+ await t('hall-visual: no visual renders nothing at all — never a placeholder', () => {
189
+ assert.equal(vis.shipVisualThumbHtml({ id: 1 }), '');
190
+ assert.equal(vis.shipVisualThumbHtml({ id: 1, visual_url: null }), '');
191
+ assert.equal(vis.shipVisualThumbHtml(null), '');
192
+ const html = vis.shipVisualThumbHtml({ id: 1, visual_url: '/api/gds/task-visuals/task-1-0011223344556677.png', visual_alt: 'a "before" shot' });
190
193
  assert.ok(html.includes('ship-visual-thumb'));
191
194
  // The caption is attacker-adjacent text (a builder wrote it) — it must be escaped.
192
195
  assert.ok(html.includes('&quot;before&quot;'));
@@ -194,18 +197,18 @@ await t('hall-kit: no visual renders nothing at all — never a placeholder', ()
194
197
  });
195
198
 
196
199
  // ---- the full-size figure (task 1003829) -----------------------------------
197
- await t('hall-kit: the full-size figure is the same guard, at reading size', () => {
200
+ await t('hall-visual: the full-size figure is the same guard, at reading size', () => {
198
201
  // Same no-placeholder rule as the thumbnail: nothing at all without a visual.
199
- assert.equal(kit.taskVisualFigureHtml({ id: 1 }), '');
200
- assert.equal(kit.taskVisualFigureHtml({ id: 1, visual_url: null }), '');
201
- assert.equal(kit.taskVisualFigureHtml(null), '');
202
+ assert.equal(vis.taskVisualFigureHtml({ id: 1 }), '');
203
+ assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: null }), '');
204
+ assert.equal(vis.taskVisualFigureHtml(null), '');
202
205
  // And the same URL wall — an off-origin src or a javascript: payload is not
203
206
  // "rendered without a caption", it is not rendered.
204
- assert.equal(kit.taskVisualFigureHtml({ id: 1, visual_url: 'https://evil.example/x.png' }), '');
205
- assert.equal(kit.taskVisualFigureHtml({ id: 1, visual_url: 'javascript:alert(1)' }), '');
207
+ assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: 'https://evil.example/x.png' }), '');
208
+ assert.equal(vis.taskVisualFigureHtml({ id: 1, visual_url: 'javascript:alert(1)' }), '');
206
209
 
207
210
  const url = '/api/gds/task-visuals/task-1-0011223344556677.png';
208
- const html = kit.taskVisualFigureHtml({ id: 1, visual_url: url, visual_alt: 'a "before" shot' });
211
+ const html = vis.taskVisualFigureHtml({ id: 1, visual_url: url, visual_alt: 'a "before" shot' });
209
212
  assert.ok(html.includes('<figure class="task-visual">'));
210
213
  assert.ok(html.includes('class="task-visual__img"'));
211
214
  // The caption is the SHIPPER's text — attacker-adjacent, so escaped in both
@@ -215,19 +218,80 @@ await t('hall-kit: the full-size figure is the same guard, at reading size', ()
215
218
 
216
219
  // The caller owns the size, so it owns the class base; anything but
217
220
  // [A-Za-z0-9_-] is stripped rather than interpolated into the attribute.
218
- const studio = kit.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'studio-plate' });
221
+ const studio = vis.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'studio-plate' });
219
222
  assert.ok(studio.includes('<figure class="studio-plate">'));
220
223
  assert.ok(studio.includes('class="studio-plate__img"'));
221
- const hostile = kit.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'x" onload="a' });
224
+ const hostile = vis.taskVisualFigureHtml({ id: 1, visual_url: url }, { className: 'x" onload="a' });
222
225
  assert.ok(hostile.includes('<figure class="xonloada">'), hostile);
223
226
 
224
227
  // No written alt → no caption at all. The fallback alt is a machine sentence
225
228
  // for a screen reader, never a caption shown to a reader.
226
- const bare = kit.taskVisualFigureHtml({ id: 7, visual_url: url });
229
+ const bare = vis.taskVisualFigureHtml({ id: 7, visual_url: url });
227
230
  assert.ok(!bare.includes('figcaption'));
228
231
  assert.ok(bare.includes('alt="Visual for task 7"'));
229
232
  });
230
233
 
234
+ // ---- the convergence is the deliverable (task 1003835) ---------------------
235
+ //
236
+ // Reading the source is the right altitude here: the defect this task closes is
237
+ // a SECOND definition existing, which no behavioural assertion against one of
238
+ // them can see. A duplicate renderer passes every test above.
239
+
240
+ const HALL = (f) => fsSync.readFileSync(new URL(`../modules/hall-ui/public/${f}`, import.meta.url), 'utf8');
241
+
242
+ await t('hall-kit no longer defines a visual renderer', () => {
243
+ const kitSrc = HALL('hall-kit.js');
244
+ // The function DEFINITIONS are what must be gone — the pointer comment naming
245
+ // them is the thing that tells the next reader where they went, so match on
246
+ // the definition syntax rather than on the bare name.
247
+ assert.ok(!/function\s+taskVisualFigureHtml\s*\(/.test(kitSrc), 'hall-kit.js still defines taskVisualFigureHtml');
248
+ assert.ok(!/function\s+shipVisualThumbHtml\s*\(/.test(kitSrc), 'hall-kit.js still defines shipVisualThumbHtml');
249
+ assert.ok(!/function\s+isTaskVisualUrl\s*\(/.test(kitSrc), 'hall-kit.js still defines isTaskVisualUrl');
250
+ // …and it must not re-export them either, or callers keep two ways in.
251
+ const kitApi = require('../modules/hall-ui/public/hall-kit.js');
252
+ assert.equal(kitApi.taskVisualFigureHtml, undefined);
253
+ assert.equal(kitApi.shipVisualThumbHtml, undefined);
254
+ assert.equal(kitApi.isTaskVisualUrl, undefined);
255
+ });
256
+
257
+ await t('task.js renders through the shared module, not its own <figure>', () => {
258
+ const src = HALL('task.js');
259
+ assert.ok(src.includes('vis.taskVisualFigureHtml(t, { className: \'tk-visual\' })'),
260
+ 'task.js should delegate to OTBVisual with its own class base');
261
+ // The hand-rolled markup is the thing that came back three times. If any of it
262
+ // reappears in this file, the convergence has been undone.
263
+ assert.ok(!src.includes('<figure class="tk-visual">'), 'task.js hand-rolls the figure again');
264
+ assert.ok(!src.includes('tk-visual__img'), 'task.js hand-rolls the img class again');
265
+ assert.ok(!src.includes('task-visuals/task-'), 'task.js carries its own copy of the URL guard again');
266
+ });
267
+
268
+ await t('every page whose script draws a visual actually loads hall-visual.js', () => {
269
+ // A missing <script> is the one failure this split introduces, and it is
270
+ // silent: the guard in each caller degrades to rendering nothing, so a visual
271
+ // would simply stop appearing with no error anywhere.
272
+ for (const page of ['index.html', 'profile.html', 'studio.html', 'task.html']) {
273
+ assert.ok(HALL(page).includes('/builders/hall-visual.js'), `${page} does not load hall-visual.js`);
274
+ }
275
+ });
276
+
277
+ await t('the callers reach for OTBVisual, and no longer for the kit', () => {
278
+ for (const f of ['hall-render.js', 'profile.js', 'studio.js', 'task.js']) {
279
+ const src = HALL(f);
280
+ assert.ok(!/OTBKit(\.|\[')?\s*(shipVisualThumbHtml|taskVisualFigureHtml|isTaskVisualUrl)/.test(src),
281
+ `${f} still reads a visual renderer off OTBKit`);
282
+ }
283
+ assert.ok(HALL('hall-render.js').includes('window.OTBVisual.shipVisualThumbHtml'));
284
+ assert.ok(HALL('studio.js').includes('vis.taskVisualFigureHtml('));
285
+ });
286
+
287
+ await t('the dead .td-visual rules are gone, and nothing emits the class', () => {
288
+ // task 1003829's comment and this task's own brief both asserted a second
289
+ // renderer in task-detail.js. `git log -S` shows that file never carried one:
290
+ // the CSS was the fossil of a renderer that was planned and not built.
291
+ assert.ok(!HALL('style.css').includes('.td-visual'), 'style.css still carries the dead rules');
292
+ assert.ok(!HALL('task-detail.js').includes('td-visual'));
293
+ });
294
+
231
295
  // ---- cleanup ---------------------------------------------------------------
232
296
  await fs.rm(TMP, { recursive: true, force: true });
233
297