@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.
- package/.bongos-core.json +76 -41
- package/.claude/skills/builder-reauth/SKILL.md +5 -3
- package/README.md +1 -1
- package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +2 -0
- package/docs/adr/0294-cli-sessions-do-not-expire.md +56 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +90 -90
- package/docs/copy-registry.json +109 -109
- package/docs/module-api-changelog.md +4 -0
- package/migrations/core_243_cli_sessions_never_expire.sql +58 -0
- package/modules/hall-ui/public/hall-kit.js +7 -65
- package/modules/hall-ui/public/hall-render.js +1 -1
- package/modules/hall-ui/public/hall-visual.js +85 -0
- package/modules/hall-ui/public/index.html +1 -0
- package/modules/hall-ui/public/profile.html +1 -0
- package/modules/hall-ui/public/profile.js +2 -2
- package/modules/hall-ui/public/settings.js +5 -2
- package/modules/hall-ui/public/studio.css +1 -1
- package/modules/hall-ui/public/studio.html +1 -0
- package/modules/hall-ui/public/studio.js +4 -3
- package/modules/hall-ui/public/style.css +0 -16
- package/modules/hall-ui/public/task.html +1 -0
- package/modules/hall-ui/public/task.js +10 -13
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/scripts/gds/accessibility-replay.js +195 -0
- package/scripts/gds/box-auth-check.js +1 -1
- package/scripts/gds/cli-lib.js +2 -2
- package/scripts/gds/reauth.js +9 -7
- package/scripts/gds/run-unit-tests.js +7 -0
- package/src/bongos/auth-github.js +2 -0
- package/src/bongos/db-kernel.js +29 -11
- package/src/bongos/db.js +33 -24
- package/src/bongos/routes/auth.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/accessibility_replay.mjs +201 -0
- package/tests/auth_session_expiry_db.mjs +183 -0
- package/tests/auth_session_ttl.mjs +124 -11
- package/tests/fixtures/accessibility-replay.json +93 -0
- package/tests/hall_studio_world.mjs +19 -11
- 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
|
|
110
|
-
// "the picture is the card" — and drawn by the
|
|
111
|
-
//
|
|
112
|
-
|
|
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-
|
|
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 \|\| !
|
|
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
|
|
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
|
|
219
|
-
// and a stub here would make the degrade tests below pass for the wrong
|
|
220
|
-
// (no
|
|
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
|
-
|
|
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 }) };
|
package/tests/task_visuals.mjs
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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-
|
|
174
|
-
assert.equal(
|
|
175
|
-
assert.equal(
|
|
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(
|
|
179
|
-
assert.equal(
|
|
180
|
-
assert.equal(
|
|
181
|
-
assert.equal(
|
|
182
|
-
assert.equal(
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
await t('hall-
|
|
186
|
-
assert.equal(
|
|
187
|
-
assert.equal(
|
|
188
|
-
assert.equal(
|
|
189
|
-
const html =
|
|
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('"before"'));
|
|
@@ -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-
|
|
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(
|
|
200
|
-
assert.equal(
|
|
201
|
-
assert.equal(
|
|
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(
|
|
205
|
-
assert.equal(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|