@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.
- package/.bongos-core.json +46 -26
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0214-canonical-profile-url-one-resolver-dark-until-the-page-exists.md +90 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +3 -2
- package/docs/copy-inventory.md +24 -23
- package/docs/copy-registry.json +37 -28
- package/docs/file-map.md +2 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/hall-ui/public/profile-canonical.js +137 -0
- package/modules/hall-ui/public/profile-connections.js +20 -9
- package/modules/hall-ui/public/profile.html +9 -0
- package/modules/hall-ui/public/profile.js +5 -0
- package/modules/platform-identity/canonical-profile.js +143 -0
- package/modules/platform-identity/routes/sso.js +7 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/routes/builders.js +24 -2
- package/src/bongos/serve-internal.js +28 -2
- package/src/module-api.js +1 -1
- package/tests/canonical_profile_url.mjs +628 -0
- package/tests/profile_connections_ui.mjs +38 -0
- package/tests/profile_route.mjs +30 -5
|
@@ -544,6 +544,44 @@ test('a 2nd-degree connection names the mutuals it was computed from', async ()
|
|
|
544
544
|
assert.match(page.html(), /data-handle="bo"/, 'the write must address the canonical handle, never the login the page happened to hold');
|
|
545
545
|
});
|
|
546
546
|
|
|
547
|
+
// The gap ADR 0212 stated honestly and left to task 1002804: the login hop
|
|
548
|
+
// resolves against the current handle or ONE previous-handle slot, so a builder
|
|
549
|
+
// who renamed twice matched neither and got no section at all. Now that
|
|
550
|
+
// GET /builders/:id/profile carries the canonical `handle` (ADR 0214 D3), the
|
|
551
|
+
// page addresses by that and the login is only the fallback.
|
|
552
|
+
test('the canonical handle from the profile response is what gets addressed, not the login', async () => {
|
|
553
|
+
reset();
|
|
554
|
+
const page = boot();
|
|
555
|
+
// A twice-renamed builder: 'bo' is current, 'bo-was' is the single previous
|
|
556
|
+
// slot, and the login the page holds ('bo-ancient') matches NEITHER.
|
|
557
|
+
await page.render({ builder: { id: 12, github_login: 'bo-ancient' }, handle: 'bo' }, 99);
|
|
558
|
+
assert.equal(page.$('#conn-scroll').hidden, false,
|
|
559
|
+
'a twice-renamed builder used to get no connections section at all — that is the hole the widened response closes');
|
|
560
|
+
assert.match(page.html(), />2nd</);
|
|
561
|
+
assert.ok(asked.some((p) => p.includes('/connections/degree/bo')),
|
|
562
|
+
'the canonical handle must be what is asked for');
|
|
563
|
+
assert.ok(!asked.some((p) => p.includes('bo-ancient')),
|
|
564
|
+
'the stale login must not be sent once the response names the builder canonically');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test('a profile response with no handle still falls back to the login hop', async () => {
|
|
568
|
+
reset();
|
|
569
|
+
const page = boot();
|
|
570
|
+
// An instance whose server predates the widening sends no `handle` key at all.
|
|
571
|
+
await page.render(profileOf('bo-was', 12), 99);
|
|
572
|
+
assert.match(page.html(), />2nd</);
|
|
573
|
+
assert.ok(asked.some((p) => p.includes('/connections/degree/bo-was')),
|
|
574
|
+
'without a handle the page must still hand over the login it holds');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('an empty-string handle is not treated as an address', async () => {
|
|
578
|
+
reset();
|
|
579
|
+
const page = boot();
|
|
580
|
+
await page.render({ builder: { id: 12, github_login: 'bo-was' }, handle: '' }, 99);
|
|
581
|
+
assert.ok(asked.some((p) => p.includes('/connections/degree/bo-was')),
|
|
582
|
+
'a falsy handle must fall through to the login, never be sent as an empty path segment');
|
|
583
|
+
});
|
|
584
|
+
|
|
547
585
|
test('a 3rd-degree connection renders "3rd"', async () => {
|
|
548
586
|
reset();
|
|
549
587
|
const page = boot();
|
package/tests/profile_route.mjs
CHANGED
|
@@ -470,11 +470,36 @@ await t('R06.1 the record is viewer-INDEPENDENT — another account sees the sam
|
|
|
470
470
|
await t('R06.1 the profile read takes NO auth middleware — the portfolio is shareable', () => {
|
|
471
471
|
// A future PR walling this behind requireBuilder would break "another account
|
|
472
472
|
// can see it" while every assertion above still passed, so pin the shape.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
473
|
+
//
|
|
474
|
+
// Asserted against the REAL ROUTER STACK rather than the registration line
|
|
475
|
+
// (task 1002804). The old line-regex demanded the handler be the only thing on
|
|
476
|
+
// the route, which caught an inline `requireBuilder` — but missed one mounted
|
|
477
|
+
// by `router.use`, and could not tell an auth gate from a rate limiter. This
|
|
478
|
+
// form catches both mounting styles and names what it forbids: AUTH. Throttling
|
|
479
|
+
// is not authentication — /public/* is rate-limited and public — so the shared
|
|
480
|
+
// public read limiter is allowed BY IDENTITY (a private copy would be a second
|
|
481
|
+
// budget, ADR 0209), and nothing else is.
|
|
482
|
+
const auth = require('../src/bongos/auth.js');
|
|
483
|
+
const { publicReadRateLimitMiddleware } = require('../src/bongos/middleware/rate-limit.js');
|
|
484
|
+
const router = buildBuildersRouter();
|
|
485
|
+
const layer = router.stack.find(
|
|
486
|
+
(l) => l.route && l.route.path === '/builders/:id/profile' && l.route.methods.get,
|
|
487
|
+
);
|
|
488
|
+
assert.ok(layer, 'the profile route registration is findable');
|
|
489
|
+
|
|
490
|
+
const bare = router.stack.filter((l) => !l.route).map((l) => l.handle);
|
|
491
|
+
assert.deepEqual(bare, [], 'a router-level middleware would gate EVERY route here, this one included');
|
|
492
|
+
|
|
493
|
+
const handlers = layer.route.stack.map((s) => s.handle);
|
|
494
|
+
const authGates = new Set(Object.values(auth).filter((v) => typeof v === 'function'));
|
|
495
|
+
for (const h of handlers) {
|
|
496
|
+
assert.ok(!authGates.has(h),
|
|
497
|
+
'an auth gate sits on the profile read — that is what would stop another account seeing the record');
|
|
498
|
+
}
|
|
499
|
+
const ALLOWED = new Set([publicReadRateLimitMiddleware]);
|
|
500
|
+
const extra = handlers.slice(0, -1).filter((h) => !ALLOWED.has(h));
|
|
501
|
+
assert.deepEqual(extra, [],
|
|
502
|
+
'only the shared public read limiter may sit between the path and the handler — anything else must justify itself here first');
|
|
478
503
|
});
|
|
479
504
|
|
|
480
505
|
await t('R06.4 NEGATIVE CONTROL: the leak guard goes red when a private field is planted', async () => {
|