@bongos/core 1.19.416 → 1.19.418
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 +70 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +10 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/docs/adr/0205-federated-checkin-needs-the-hubs-own-signin-witness.md +79 -0
- package/docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md +59 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +185 -4
- package/docs/api-reference.md +5 -2
- package/docs/architecture.md +1 -0
- package/docs/copy-inventory.md +51 -36
- package/docs/copy-registry.json +175 -40
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +6 -0
- package/modules/hall-ui/public/settings-sessions.js +119 -0
- package/modules/hall-ui/public/settings.css +25 -0
- package/modules/hall-ui/public/settings.html +22 -0
- package/modules/platform-identity/migrations/platform_identity_014_membership_signin_proof.sql +41 -0
- package/modules/platform-identity/platform-identity.js +88 -15
- package/modules/platform-identity/routes/sso.js +54 -10
- package/modules/platform-identity/tests/platform-identity.mjs +25 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/db.js +46 -0
- package/src/bongos/route-rank-check.js +10 -0
- package/src/bongos/routes/my-sessions.js +128 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/hub_recent_ships.mjs +4 -1
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/own_session_revoke.mjs +410 -0
- package/tests/sso_checkin_identity_spoof.mjs +348 -0
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.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.418",
|
|
6
|
+
"core_contract": "1.19.418",
|
|
7
|
+
"source_commit": "e87d7993f31b4954beb97e7d65573bc9410f1192",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-29T16:
|
|
9
|
+
"built_at": "2026-08-29T16:52:59.816Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 357,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1704,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2084,
|
|
20
|
+
"tree_sha256": "8422d1d3312cf70f1ec09e532c01c0c368328ee68577712e0a6f98df0078f338",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -352,12 +352,12 @@
|
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/README.md",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "2d74abb8d89022d621f105f019b2d0e4c7cef399e14e54fab64fb443b0f49179"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
359
359
|
"mode": "0000644",
|
|
360
|
-
"sha256": "
|
|
360
|
+
"sha256": "02b57e81ee53386d1392c769fb39adda73440fe016699b0e6b3a3b01a554d060"
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -367,17 +367,17 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.cjs",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "07dc39d1991ca379b67de839752cfc4578036172f113d15b96d4a39b0eb7f9b4"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.d.ts",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "41363423243953f69a663ec425c439e61149f139ad7f0a3c721fd1f7a02047ef"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/index.mjs",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "9cd5857fc781c8518d9cfedfe690af7487dde2da53fef0861bb6c6cded4d9b2e"
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1524,25 +1524,35 @@
|
|
|
1524
1524
|
"mode": "0000644",
|
|
1525
1525
|
"sha256": "ac6a729436ff1e51414ccaf097b5e49c1360dd3bca1efd1ff73f38d4e5fca24d"
|
|
1526
1526
|
},
|
|
1527
|
+
{
|
|
1528
|
+
"path": "docs/adr/0205-federated-checkin-needs-the-hubs-own-signin-witness.md",
|
|
1529
|
+
"mode": "0000644",
|
|
1530
|
+
"sha256": "dedf9285b041a0d4c54982a8372aca573e57eb3d89a5abf9381a922a8784ff7f"
|
|
1531
|
+
},
|
|
1532
|
+
{
|
|
1533
|
+
"path": "docs/adr/0206-own-scoped-session-revocation-no-absolute-ceiling.md",
|
|
1534
|
+
"mode": "0000644",
|
|
1535
|
+
"sha256": "634404f2eb0d29386460e0ea008a826069d80bfbd42721aa7b2cd4d74cb4fa86"
|
|
1536
|
+
},
|
|
1527
1537
|
{
|
|
1528
1538
|
"path": "docs/adr/README.md",
|
|
1529
1539
|
"mode": "0000644",
|
|
1530
|
-
"sha256": "
|
|
1540
|
+
"sha256": "fb4ae772f6d0289385dc1caddd24a2271510d85f4b4eb6d039248310defc84d7"
|
|
1531
1541
|
},
|
|
1532
1542
|
{
|
|
1533
1543
|
"path": "docs/api-reference.md",
|
|
1534
1544
|
"mode": "0000644",
|
|
1535
|
-
"sha256": "
|
|
1545
|
+
"sha256": "05d778a5f5d44ef620fc266add8626f344308ff44f031b16a65441a1de38d67c"
|
|
1536
1546
|
},
|
|
1537
1547
|
{
|
|
1538
1548
|
"path": "docs/api/openapi.json",
|
|
1539
1549
|
"mode": "0000644",
|
|
1540
|
-
"sha256": "
|
|
1550
|
+
"sha256": "cc4545032233412b8938f8d399d24a3427789f5213dfdd29f33b3de1c4c3c3ac"
|
|
1541
1551
|
},
|
|
1542
1552
|
{
|
|
1543
1553
|
"path": "docs/architecture.md",
|
|
1544
1554
|
"mode": "0000644",
|
|
1545
|
-
"sha256": "
|
|
1555
|
+
"sha256": "d24b71683aab15bb19ffd110c3d214dade7f2be4c5cc81740153c92ea8ccc252"
|
|
1546
1556
|
},
|
|
1547
1557
|
{
|
|
1548
1558
|
"path": "docs/branding-contract.md",
|
|
@@ -1562,12 +1572,12 @@
|
|
|
1562
1572
|
{
|
|
1563
1573
|
"path": "docs/copy-inventory.md",
|
|
1564
1574
|
"mode": "0000644",
|
|
1565
|
-
"sha256": "
|
|
1575
|
+
"sha256": "3e70beb03c93a71f5068c407467deeeca3c69d5271372de04399a9094fa32dd9"
|
|
1566
1576
|
},
|
|
1567
1577
|
{
|
|
1568
1578
|
"path": "docs/copy-registry.json",
|
|
1569
1579
|
"mode": "0000644",
|
|
1570
|
-
"sha256": "
|
|
1580
|
+
"sha256": "ae4ffbc86e556ea5463b8289754b70e9465a581e93a40f8713204c34d433e00a"
|
|
1571
1581
|
},
|
|
1572
1582
|
{
|
|
1573
1583
|
"path": "docs/design/apex-pass-2-direction.md",
|
|
@@ -1902,7 +1912,7 @@
|
|
|
1902
1912
|
{
|
|
1903
1913
|
"path": "docs/file-map.md",
|
|
1904
1914
|
"mode": "0000644",
|
|
1905
|
-
"sha256": "
|
|
1915
|
+
"sha256": "2464bcb60656e263ef00002c928a2e6f0d4b812b7a7b8183c634535f1192e58d"
|
|
1906
1916
|
},
|
|
1907
1917
|
{
|
|
1908
1918
|
"path": "docs/handoff-template.md",
|
|
@@ -1912,7 +1922,7 @@
|
|
|
1912
1922
|
{
|
|
1913
1923
|
"path": "docs/module-api-changelog.md",
|
|
1914
1924
|
"mode": "0000644",
|
|
1915
|
-
"sha256": "
|
|
1925
|
+
"sha256": "11919db7fb659bf498544aa14b39cef44166ed3ad596784281e8f6cd7efd6fa5"
|
|
1916
1926
|
},
|
|
1917
1927
|
{
|
|
1918
1928
|
"path": "docs/modules-contract.md",
|
|
@@ -3222,7 +3232,7 @@
|
|
|
3222
3232
|
{
|
|
3223
3233
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
3224
3234
|
"mode": "0000644",
|
|
3225
|
-
"sha256": "
|
|
3235
|
+
"sha256": "07dc39d1991ca379b67de839752cfc4578036172f113d15b96d4a39b0eb7f9b4"
|
|
3226
3236
|
},
|
|
3227
3237
|
{
|
|
3228
3238
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -4139,15 +4149,20 @@
|
|
|
4139
4149
|
"mode": "0000644",
|
|
4140
4150
|
"sha256": "e99046a2c0e2cb6ce9f05711ddbe6ed9d2e7536086dca16496ef0e5e8cb9a041"
|
|
4141
4151
|
},
|
|
4152
|
+
{
|
|
4153
|
+
"path": "modules/hall-ui/public/settings-sessions.js",
|
|
4154
|
+
"mode": "0000644",
|
|
4155
|
+
"sha256": "c0c207769631f027ca35d219105e2189e2d259e7b45bfeaa1245fd749d7e91e5"
|
|
4156
|
+
},
|
|
4142
4157
|
{
|
|
4143
4158
|
"path": "modules/hall-ui/public/settings.css",
|
|
4144
4159
|
"mode": "0000644",
|
|
4145
|
-
"sha256": "
|
|
4160
|
+
"sha256": "d50ac6727dde53e91aef8daa5ce759edcc69924e43b786103800892d9b87fe4d"
|
|
4146
4161
|
},
|
|
4147
4162
|
{
|
|
4148
4163
|
"path": "modules/hall-ui/public/settings.html",
|
|
4149
4164
|
"mode": "0000644",
|
|
4150
|
-
"sha256": "
|
|
4165
|
+
"sha256": "88886109031ceb77f4a4efbe133af5a2ac8e26ec5dd55e2e71a0bbe5152887bc"
|
|
4151
4166
|
},
|
|
4152
4167
|
{
|
|
4153
4168
|
"path": "modules/hall-ui/public/settings.js",
|
|
@@ -4834,6 +4849,11 @@
|
|
|
4834
4849
|
"mode": "0000644",
|
|
4835
4850
|
"sha256": "50b993d4cdad3981b4ace6c60c4f9ee350076a2ee9b205b7fc7d80de255f492e"
|
|
4836
4851
|
},
|
|
4852
|
+
{
|
|
4853
|
+
"path": "modules/platform-identity/migrations/platform_identity_014_membership_signin_proof.sql",
|
|
4854
|
+
"mode": "0000644",
|
|
4855
|
+
"sha256": "cf29832de04b2b31e1d36b5d595489bbbb09d14b12c86da7c5ac7557917475bf"
|
|
4856
|
+
},
|
|
4837
4857
|
{
|
|
4838
4858
|
"path": "modules/platform-identity/module.json",
|
|
4839
4859
|
"mode": "0000644",
|
|
@@ -4842,7 +4862,7 @@
|
|
|
4842
4862
|
{
|
|
4843
4863
|
"path": "modules/platform-identity/platform-identity.js",
|
|
4844
4864
|
"mode": "0000644",
|
|
4845
|
-
"sha256": "
|
|
4865
|
+
"sha256": "16f86cd9b5a4b934b90a0332651b18c8bd8b1ccbfdbf0c5b23fffe50dbe8d8f8"
|
|
4846
4866
|
},
|
|
4847
4867
|
{
|
|
4848
4868
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -4882,12 +4902,12 @@
|
|
|
4882
4902
|
{
|
|
4883
4903
|
"path": "modules/platform-identity/routes/sso.js",
|
|
4884
4904
|
"mode": "0000644",
|
|
4885
|
-
"sha256": "
|
|
4905
|
+
"sha256": "5e1ca2aed13b91e275cc30725b05498a4332931af6ef08194afc8aff58352316"
|
|
4886
4906
|
},
|
|
4887
4907
|
{
|
|
4888
4908
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
4889
4909
|
"mode": "0000644",
|
|
4890
|
-
"sha256": "
|
|
4910
|
+
"sha256": "5403d391a22cffa58722923beee730db0bf4052ee933a84ab1e11fcbab9ea5e4"
|
|
4891
4911
|
},
|
|
4892
4912
|
{
|
|
4893
4913
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -5422,12 +5442,12 @@
|
|
|
5422
5442
|
{
|
|
5423
5443
|
"path": "package-lock.json",
|
|
5424
5444
|
"mode": "0000644",
|
|
5425
|
-
"sha256": "
|
|
5445
|
+
"sha256": "022b69ded283068fcd60758e3dbb676e64fd660c782700e3d52da3f7c00b8b1e"
|
|
5426
5446
|
},
|
|
5427
5447
|
{
|
|
5428
5448
|
"path": "package.json",
|
|
5429
5449
|
"mode": "0000644",
|
|
5430
|
-
"sha256": "
|
|
5450
|
+
"sha256": "39ac5d6d5119b7473a0247f6296c83d175af6323f9d6b3c23ad83f6ff89f332e"
|
|
5431
5451
|
},
|
|
5432
5452
|
{
|
|
5433
5453
|
"path": "public-docs/index.html",
|
|
@@ -7307,7 +7327,7 @@
|
|
|
7307
7327
|
{
|
|
7308
7328
|
"path": "src/bongos/db.js",
|
|
7309
7329
|
"mode": "0000644",
|
|
7310
|
-
"sha256": "
|
|
7330
|
+
"sha256": "feff54089b6bae5845558d262c07ec30d9532419d42c04f1f113bc2fc95d9173"
|
|
7311
7331
|
},
|
|
7312
7332
|
{
|
|
7313
7333
|
"path": "src/bongos/github-repos.js",
|
|
@@ -7407,12 +7427,12 @@
|
|
|
7407
7427
|
{
|
|
7408
7428
|
"path": "src/bongos/route-rank-check.js",
|
|
7409
7429
|
"mode": "0000644",
|
|
7410
|
-
"sha256": "
|
|
7430
|
+
"sha256": "96b884a69b7b709a256d220c97a972f9827d74a36a864c216a7b37db3fdf66be"
|
|
7411
7431
|
},
|
|
7412
7432
|
{
|
|
7413
7433
|
"path": "src/bongos/routes.js",
|
|
7414
7434
|
"mode": "0000644",
|
|
7415
|
-
"sha256": "
|
|
7435
|
+
"sha256": "074cded727667d864b1849d8d5505736895a69a6bdf7132375fd358d66c2e6fd"
|
|
7416
7436
|
},
|
|
7417
7437
|
{
|
|
7418
7438
|
"path": "src/bongos/routes/CLAUDE.md",
|
|
@@ -7479,6 +7499,11 @@
|
|
|
7479
7499
|
"mode": "0000644",
|
|
7480
7500
|
"sha256": "54a42c37d4afd403a13aa5e9fda6a7d95cb0fca70dfe3cb4daef318c099a5f00"
|
|
7481
7501
|
},
|
|
7502
|
+
{
|
|
7503
|
+
"path": "src/bongos/routes/my-sessions.js",
|
|
7504
|
+
"mode": "0000644",
|
|
7505
|
+
"sha256": "0e002104e2fedea2bb322826560aadf18b9885f21e214592c63b304b3ba9fd1e"
|
|
7506
|
+
},
|
|
7482
7507
|
{
|
|
7483
7508
|
"path": "src/bongos/routes/optimizer.js",
|
|
7484
7509
|
"mode": "0000644",
|
|
@@ -7542,7 +7567,7 @@
|
|
|
7542
7567
|
{
|
|
7543
7568
|
"path": "src/module-api.js",
|
|
7544
7569
|
"mode": "0000644",
|
|
7545
|
-
"sha256": "
|
|
7570
|
+
"sha256": "a0b22f52d4549706a689fe6e4c328f03f36d0ad340e90fc4a8bf6c555ea94b54"
|
|
7546
7571
|
},
|
|
7547
7572
|
{
|
|
7548
7573
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8932,7 +8957,7 @@
|
|
|
8932
8957
|
{
|
|
8933
8958
|
"path": "tests/hub_recent_ships.mjs",
|
|
8934
8959
|
"mode": "0000644",
|
|
8935
|
-
"sha256": "
|
|
8960
|
+
"sha256": "53c7c8ad2de098d29c9010ed37e43779e8ceda4790018527c7edf45d54e56e80"
|
|
8936
8961
|
},
|
|
8937
8962
|
{
|
|
8938
8963
|
"path": "tests/idea_capture_grades.mjs",
|
|
@@ -9452,7 +9477,12 @@
|
|
|
9452
9477
|
{
|
|
9453
9478
|
"path": "tests/own_scope_guard.mjs",
|
|
9454
9479
|
"mode": "0000644",
|
|
9455
|
-
"sha256": "
|
|
9480
|
+
"sha256": "da02342121dbf5be0cc31c009316f0473d010e592f1a06fc9276a184a3be2846"
|
|
9481
|
+
},
|
|
9482
|
+
{
|
|
9483
|
+
"path": "tests/own_session_revoke.mjs",
|
|
9484
|
+
"mode": "0000644",
|
|
9485
|
+
"sha256": "756920320b74c9094d1be12108efb8ddbb39341b8d122afd9454ffd084a2efe5"
|
|
9456
9486
|
},
|
|
9457
9487
|
{
|
|
9458
9488
|
"path": "tests/package_core.mjs",
|
|
@@ -10154,6 +10184,11 @@
|
|
|
10154
10184
|
"mode": "0000644",
|
|
10155
10185
|
"sha256": "70228aa4be345cc09664b3b725e30c3a1e0eacf4c39573db1304fb219d3245e7"
|
|
10156
10186
|
},
|
|
10187
|
+
{
|
|
10188
|
+
"path": "tests/sso_checkin_identity_spoof.mjs",
|
|
10189
|
+
"mode": "0000644",
|
|
10190
|
+
"sha256": "27bd2a87e001eec2bc58e13cfa58e53f88c73ebe7504b944c718182247d98ca7"
|
|
10191
|
+
},
|
|
10157
10192
|
{
|
|
10158
10193
|
"path": "tests/start_env_guard.mjs",
|
|
10159
10194
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 330 operations across 52 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -523,6 +523,12 @@ function createClient(opts = {}) {
|
|
|
523
523
|
getMeRender: (args) => request("GET", "/me/render", { hasBody: false }, args),
|
|
524
524
|
// PATCH /me/render — rank: any-builder — PATCH /me/render
|
|
525
525
|
patchMeRender: (args) => request("PATCH", "/me/render", { hasBody: true }, args),
|
|
526
|
+
// GET /me/sessions — rank: any-builder — GET /me/sessions
|
|
527
|
+
getMeSessions: (args) => request("GET", "/me/sessions", { hasBody: false }, args),
|
|
528
|
+
// POST /me/sessions/revoke — rank: any-builder — POST /me/sessions/revoke
|
|
529
|
+
postMeSessionsRevoke: (args) => request("POST", "/me/sessions/revoke", { hasBody: true }, args),
|
|
530
|
+
// POST /me/sessions/revoke-all — rank: any-builder — POST /me/sessions/revoke-all
|
|
531
|
+
postMeSessionsRevokeAll: (args) => request("POST", "/me/sessions/revoke-all", { hasBody: true }, args),
|
|
526
532
|
// GET /me/skill-prefs — rank: any-builder — GET /me/skill-prefs
|
|
527
533
|
getMeSkillPrefs: (args) => request("GET", "/me/skill-prefs", { hasBody: false }, args),
|
|
528
534
|
// PATCH /me/skill-prefs — rank: any-builder — PATCH /me/skill-prefs
|
|
@@ -522,6 +522,12 @@ function createClient(opts = {}) {
|
|
|
522
522
|
getMeRender: (args) => request("GET", "/me/render", { hasBody: false }, args),
|
|
523
523
|
// PATCH /me/render — rank: any-builder — PATCH /me/render
|
|
524
524
|
patchMeRender: (args) => request("PATCH", "/me/render", { hasBody: true }, args),
|
|
525
|
+
// GET /me/sessions — rank: any-builder — GET /me/sessions
|
|
526
|
+
getMeSessions: (args) => request("GET", "/me/sessions", { hasBody: false }, args),
|
|
527
|
+
// POST /me/sessions/revoke — rank: any-builder — POST /me/sessions/revoke
|
|
528
|
+
postMeSessionsRevoke: (args) => request("POST", "/me/sessions/revoke", { hasBody: true }, args),
|
|
529
|
+
// POST /me/sessions/revoke-all — rank: any-builder — POST /me/sessions/revoke-all
|
|
530
|
+
postMeSessionsRevokeAll: (args) => request("POST", "/me/sessions/revoke-all", { hasBody: true }, args),
|
|
525
531
|
// GET /me/skill-prefs — rank: any-builder — GET /me/skill-prefs
|
|
526
532
|
getMeSkillPrefs: (args) => request("GET", "/me/skill-prefs", { hasBody: false }, args),
|
|
527
533
|
// PATCH /me/skill-prefs — rank: any-builder — PATCH /me/skill-prefs
|
|
@@ -68,6 +68,7 @@ export interface GetMeCrossProjectResponse { hide_stats: boolean; profile_state:
|
|
|
68
68
|
export interface GetMeDisplayNameResponse { display_name: unknown; max_length: unknown }
|
|
69
69
|
export interface GetMeEventSoundPrefsResponse { prefs: unknown; effective: unknown; effective_wav: unknown; events: unknown }
|
|
70
70
|
export interface GetMeRecentShipsResponse { ships: unknown }
|
|
71
|
+
export interface GetMeSessionsResponse { sessions: unknown }
|
|
71
72
|
export interface GetMeSkillPrefsResponse { prefs: unknown; effective: unknown; defaults: unknown; known_skills: unknown; allowed_models: unknown }
|
|
72
73
|
export interface GetMeSoundPrefsResponse { prefs: unknown; effective: unknown; effective_map: unknown; volume: unknown; defaults: unknown; known_sounds: unknown }
|
|
73
74
|
export interface GetMemoryBfgBuildersBuilderIdFileResponse { builder_id: unknown; bfg_read: boolean; path: unknown; content: unknown; sha256: unknown; byte_len: unknown; mtime: unknown; updated_at: unknown; created_at: unknown }
|
|
@@ -283,6 +284,9 @@ export interface PostLlmCacheStoreRequest { spec?: unknown; result_stdout?: unkn
|
|
|
283
284
|
export interface PostLlmCacheStoreResponse { stored: boolean; result_cost_usd?: unknown }
|
|
284
285
|
export interface PostMeAnthropicLinkRequest { anthropic_email: string; anthropic_user_id: string }
|
|
285
286
|
export interface PostMeAnthropicLinkResponse { linked: unknown }
|
|
287
|
+
export interface PostMeSessionsRevokeAllResponse { ok: boolean; revoked: unknown }
|
|
288
|
+
export interface PostMeSessionsRevokeRequest { session_id?: StringifiableId }
|
|
289
|
+
export interface PostMeSessionsRevokeResponse { ok: boolean; revoked: unknown }
|
|
286
290
|
export interface PostMemoryBuildersBuilderIdBfgWriteRequest { kind?: string; slug?: string; title?: string; body_md?: string; rationale?: string; karma_delta?: unknown }
|
|
287
291
|
export interface PostMemoryBuildersBuilderIdBfgWriteResponse { ok: boolean; builder_id: unknown; path: unknown; kind: unknown; author: unknown; audit_id: unknown; karma_id: unknown; sha256: unknown; byte_len: unknown; updated_at: unknown }
|
|
288
292
|
export interface PostMemorySyncRequest { files?: unknown[] }
|
|
@@ -817,6 +821,12 @@ export interface BongosClient {
|
|
|
817
821
|
getMeRender(args?: RequestArgs): Promise<ApiResponse>;
|
|
818
822
|
/** PATCH /me/render — rank: any-builder */
|
|
819
823
|
patchMeRender(args?: RequestArgs): Promise<ApiResponse>;
|
|
824
|
+
/** GET /me/sessions — rank: any-builder */
|
|
825
|
+
getMeSessions(args?: RequestArgs): Promise<GetMeSessionsResponse>;
|
|
826
|
+
/** POST /me/sessions/revoke — rank: any-builder */
|
|
827
|
+
postMeSessionsRevoke(args?: RequestArgs & { body?: PostMeSessionsRevokeRequest }): Promise<PostMeSessionsRevokeResponse>;
|
|
828
|
+
/** POST /me/sessions/revoke-all — rank: any-builder */
|
|
829
|
+
postMeSessionsRevokeAll(args?: RequestArgs): Promise<PostMeSessionsRevokeAllResponse>;
|
|
820
830
|
/** GET /me/skill-prefs — rank: any-builder */
|
|
821
831
|
getMeSkillPrefs(args?: RequestArgs): Promise<GetMeSkillPrefsResponse>;
|
|
822
832
|
/** PATCH /me/skill-prefs — rank: any-builder */
|
|
@@ -519,6 +519,12 @@ export function createClient(opts = {}) {
|
|
|
519
519
|
getMeRender: (args) => request("GET", "/me/render", { hasBody: false }, args),
|
|
520
520
|
// PATCH /me/render — rank: any-builder — PATCH /me/render
|
|
521
521
|
patchMeRender: (args) => request("PATCH", "/me/render", { hasBody: true }, args),
|
|
522
|
+
// GET /me/sessions — rank: any-builder — GET /me/sessions
|
|
523
|
+
getMeSessions: (args) => request("GET", "/me/sessions", { hasBody: false }, args),
|
|
524
|
+
// POST /me/sessions/revoke — rank: any-builder — POST /me/sessions/revoke
|
|
525
|
+
postMeSessionsRevoke: (args) => request("POST", "/me/sessions/revoke", { hasBody: true }, args),
|
|
526
|
+
// POST /me/sessions/revoke-all — rank: any-builder — POST /me/sessions/revoke-all
|
|
527
|
+
postMeSessionsRevokeAll: (args) => request("POST", "/me/sessions/revoke-all", { hasBody: true }, args),
|
|
522
528
|
// GET /me/skill-prefs — rank: any-builder — GET /me/skill-prefs
|
|
523
529
|
getMeSkillPrefs: (args) => request("GET", "/me/skill-prefs", { hasBody: false }, args),
|
|
524
530
|
// PATCH /me/skill-prefs — rank: any-builder — PATCH /me/skill-prefs
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# ADR 0205 — A federated check-in may not speak for a person the hub never saw sign in: the membership carries the hub's own witness, and identity is never re-synced from client credentials
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Task:** [task 1003340](https://cloudbongos.com/builders#/task/1003340) (goal 1000045 — public builder profiles + account security)
|
|
6
|
+
- **Closes:** security report 1000027 (high, LIVE on the hub)
|
|
7
|
+
- **Relates to:** ADR 0141 §1/§3/§4 (identity federated, authority local — the check-in and rollup wire contracts), ADR 0171 D7 (the rollup impersonation hardening this completes), ADR 0016 (the trust boundary), ADR 0083 §5 (module-owned, namespaced migrations), ADR 0156 / ADR 0177 (the `display_name` source guard and the `membership_kind` cross-version window this must not break)
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
`POST /sso/membership/check-in` is how a federated instance tells the hub "this builder builds on my project". It authenticates with `client_id` + `client_secret` — the same constant-time check as `/sso/token` — and takes the **subject** (`github_id`, `github_login`, `avatar_url`) from the request body.
|
|
12
|
+
|
|
13
|
+
That is the whole defect, stated plainly: **client credentials say WHICH PROJECT is calling. They say nothing about WHO the `github_id` in the body is.** Any owner of any registered SSO client, holding only their own legitimate secret, could:
|
|
14
|
+
|
|
15
|
+
1. **Rewrite a real person's public identity.** `upsertAccount`'s `ON CONFLICT (github_id) DO UPDATE` set `github_login = EXCLUDED.github_login` and `avatar_url = EXCLUDED.avatar_url` unconditionally on the victim's existing row. Both are what `<redacted>` and core `GET /builders/:id/profile` render, and `avatar_url` was stored with no scheme check at all. (`display_name` was already source-guarded by the ADR 0156 mirror; these two were not, deliberately — "neither is user-editable", which was true and beside the point once a *third party* could write them.)
|
|
16
|
+
2. **Mint the row that unlocks a forged portfolio.** ADR 0171 D7 added a `membership_required` precheck to `POST /sso/activity/rollup` precisely to stop a client publishing credits/karma/rank for someone who does not build on its project. But the membership it demands was created by the *other* endpoint, on the caller's unproven say-so — so the gate was satisfiable by the very call it was meant to gate. Check in as the victim, then push arbitrary totals onto their public cross-project record. The same row also made the attacker's project appear in the victim's "My Projects".
|
|
17
|
+
|
|
18
|
+
D7 hardened the read side of a door whose key was being handed out at the other window.
|
|
19
|
+
|
|
20
|
+
The fact that makes a fix possible: **the hub is the identity provider.** Every federated sign-in passes through the hub itself, at one of exactly two places, and in both the pairing of *person* to *project* is the hub's own finding rather than a caller's claim:
|
|
21
|
+
|
|
22
|
+
- `POST /sso/token` — redeeming a single-use code that `/sso/authorize` minted under a **live hub session**, bound at mint time to `(client_id, github_id, redirect_uri)`;
|
|
23
|
+
- `POST /sso/device/poll` — a GitHub device approval, with the audience **bound at start from the typed origin** so the CLI never names the client (the ADR 0141 §1 confused-deputy fix).
|
|
24
|
+
|
|
25
|
+
And the instance sequences its calls: `src/bongos/auth-idp.js` runs the token exchange or assertion verification, *then* the check-in, *then* the rollup. A legitimate check-in is therefore always preceded, within the same sign-in, by a hub-authenticated moment.
|
|
26
|
+
|
|
27
|
+
## Decision
|
|
28
|
+
|
|
29
|
+
### D1 — The membership row carries `signin_verified_at`, and only the hub can stamp it
|
|
30
|
+
|
|
31
|
+
`<redacted>.sql` adds one nullable `timestamptz` to `<redacted>`. It is written by exactly one domain function, `recordFederatedSignin({ clientId, githubId })`, called from exactly the two hub routes above. **No request-body field reaches the statement** — the `client_id` is the authenticated client and the `github_id` comes from the redeemed code or GitHub's own device answer.
|
|
32
|
+
|
|
33
|
+
That is what makes it evidence rather than another self-report: it is a column the caller structurally cannot write.
|
|
34
|
+
|
|
35
|
+
### D2 — A check-in may only UPDATE a witnessed row; it can no longer create one
|
|
36
|
+
|
|
37
|
+
`upsertMembership` is replaced by `touchMembership`, an `UPDATE … WHERE client_id = $1 AND github_id = $2 AND signin_verified_at IS NOT NULL`. No match → the route answers `409 signin_required` and writes nothing at all. `recordFederatedSignin` is now the only birth path for a membership row, so a project cannot claim a person the hub never saw arrive.
|
|
38
|
+
|
|
39
|
+
Signing in also **accepts a standing invitation**: a `pending` row (pre-seeded by `POST /projects/invite`) becomes `member` when its invitee actually signs in, and any other stored `membership_kind` is left alone — the coarse kind stays the reporting instance's to say, through `touchMembership`, exactly as ADR 0141 §3 intends.
|
|
40
|
+
|
|
41
|
+
The check-in also gains the numeric-`github_id` guard the rollup already had, so a `system:*`-shaped principal is refused before it can hit the `bigint` column.
|
|
42
|
+
|
|
43
|
+
### D3 — A federated client gets an insert-only account writer
|
|
44
|
+
|
|
45
|
+
`ensureAccount` inserts the identity shell `ON CONFLICT (github_id) DO NOTHING` and returns the stored row. `upsertAccount` — whose conflict branch re-syncs identity — is now documented as belonging to hub-authenticated callers only (`/sso/authorize`, `/sso/device/poll`, the own-scoped profile route).
|
|
46
|
+
|
|
47
|
+
**Nothing is lost by removing the re-sync from check-in.** Both hub-authenticated paths already call `upsertAccount` on every single sign-in, from GitHub's own answer or the live hub session — so a renamed GitHub login and a changed avatar still land, on the pass where the hub actually knows whose they are. The check-in's identity write was pure redundancy that happened to be the attack surface.
|
|
48
|
+
|
|
49
|
+
### D4 — The rollup gate reads the witness, not the row's existence
|
|
50
|
+
|
|
51
|
+
One predicate, `membershipBacksRollup(membership)`: non-`pending` **and** `signin_verified_at` present. The gate ADR 0171 D7 describes finally means what it says.
|
|
52
|
+
|
|
53
|
+
### D5 — `avatar_url` is https-only, validated at the write boundary
|
|
54
|
+
|
|
55
|
+
`safeAvatarUrl` returns the value only when it parses as an absolute `https:` URL, and `null` otherwise — `http:`, `data:`, `javascript:`, `file:`, relative paths and malformed strings all collapse to null. It is applied inside **both** account writers rather than at each render, because the avatar is echoed into a public page and an `<img src>` from several surfaces and a per-render check is a rule someone eventually forgets. Every real GitHub avatar is already https, so nothing legitimate is refused.
|
|
56
|
+
|
|
57
|
+
### D6 — No backfill, and that is the deliberate part
|
|
58
|
+
|
|
59
|
+
Existing membership rows carry no witness, and there is **no predicate that separates a legitimate 2026 check-in row from a forged one** — grandfathering them would preserve precisely the rows the report is about. So the column ships NULL for every existing row and the gates fail closed on them.
|
|
60
|
+
|
|
61
|
+
The cost is bounded to near-zero because of D1's own sequencing: the first federated sign-in after this deploys stamps the witness *before* the check-in and rollup that follow it, so a real builder heals on their next login with no operator step. Already-published activity rows are untouched, so a public profile goes **stale until that login, never blank**.
|
|
62
|
+
|
|
63
|
+
`listMembershipsForAccount` ("My Projects", and the single-logout fan-out) is deliberately **not** retro-filtered on the witness. Once D2 makes new forgery impossible, filtering historical rows would empty every builder's project list for a marginal security gain against rows that were, in all likelihood, legitimate check-ins. The gates belong on the write and publish paths; this is a read of the past.
|
|
64
|
+
|
|
65
|
+
## Consequences
|
|
66
|
+
|
|
67
|
+
- The three attack steps in report 1000027 now stop at step 3: a client with valid credentials and no witnessed sign-in gets `409 signin_required`, having written nothing.
|
|
68
|
+
- `POST /sso/token` gains a write. It is inside the existing `try`, so a failure to store the witness fails the exchange rather than proceeding into a state where the client's own check-in would be the only evidence the sign-in happened. The code is single-use and already consumed at that point, so the user restarts sign-in — the fail-closed trade the report's severity justifies.
|
|
69
|
+
- **No wire-format change.** The check-in and rollup request schemas are byte-identical, `membership_kind`/`role` and the ADR 0177 cross-version window are untouched, and no federated instance needs to upgrade. The proof moved *inside* the hub, which is the only place it could live without asking every instance on its own schedule to carry a new field.
|
|
70
|
+
- New refusal codes: `signin_required` (409) and `invalid_github_id` (400) on check-in.
|
|
71
|
+
- Proof: `tests/sso_checkin_identity_spoof.mjs` — DB-free, in the default unit gate (a real-Postgres proof would self-skip in CI, which is where this one has to run). Ten of its fourteen assertions fail against the pre-fix code; the two "still works" rows are the control that the gate is a gate and not a wall.
|
|
72
|
+
|
|
73
|
+
## Rejected
|
|
74
|
+
|
|
75
|
+
- **Carrying the hub assertion in the check-in body.** The strongest-looking fix, and unshippable: `validateOrRespond` is strict, the callers are other instances on their own upgrade schedule (ADR 0141 §3), and requiring a new field would 400 every check-in from every un-upgraded instance. The same reasoning ADR 0201 D2 used to put its assertion in a header applies here — except here the hub already holds the evidence and needs no field at all.
|
|
76
|
+
- **Source-guarding `github_login`/`avatar_url` the way `display_name` is guarded (ADR 0156).** That guard answers "did the user set this themselves", which is the wrong question: neither field is user-editable, and the attacker is a third party, not the user. It would have left the membership half of the report wide open.
|
|
77
|
+
- **Backfilling `signin_verified_at = joined_at`.** Preserves exactly the rows that may be forged. See D6.
|
|
78
|
+
- **Refusing the check-in silently (200 with no write).** The caller swallows errors either way, but a silent success makes the hub's logs lie about what it did; `409 signin_required` says what happened, and matches the shape the rollup gate already uses.
|
|
79
|
+
- **Retro-filtering `listMembershipsForAccount`.** See D6.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 0206 — A leaked bearer's remedy is the account holder's own revoke, not an absolute session ceiling
|
|
2
|
+
|
|
3
|
+
- **Status:** Accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Deciders:** Claude, building the own-scoped half of the session self-service. The ceiling question is the one the task named as "worth a decision either way".
|
|
6
|
+
- **Task:** task 1003341 (goal 1000045 — public builder profiles + account security, criterion `i-control-my-own-access-and-exposure`)
|
|
7
|
+
- **Interacts with:** [ADR 0016](<redacted>.md) (the DB enforces; a session is checked fresh per request, never cached), [ADR 0053](0053-scoped-dev-box-session.md) / [ADR 0151](<redacted>.md) §3 (the session-scope registry — scope is capability on the token, orthogonal to rank), migration `<redacted>.sql` (the 24h TTL + refresh-on-use it introduced, and the deliberate backfill grace that came with it).
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
`db-kernel.js lookupSession` rewrites `expires_at = now() + 24h` on every authenticated request. That is refresh-on-use, and for a session in daily use it means the TTL never arrives: a CLI bearer sitting in `~/.config/otb/gds-session.json` is, in practice, immortal. If it leaks — committed to a repo, left on a shared dev box, copied off a stolen laptop — the holder keeps it alive simply by using it.
|
|
12
|
+
|
|
13
|
+
Until this task the account holder had no remedy of their own. `POST /auth/logout` revokes only the token presented on that same request, which is useless when the token you want dead is the one you no longer hold. The two endpoints that can kill somebody else's session — `POST /auth/revoke` and `POST /auth/revoke-all` — are gated on the admin `auth.session.revoke` atom, so the victim had to find an Archon and ask. On a self-hosted single-owner instance there may be no second Archon at all, which makes the remedy not merely slow but absent.
|
|
14
|
+
|
|
15
|
+
Two different fixes were on the table, and it matters that they are not the same fix:
|
|
16
|
+
|
|
17
|
+
1. **Give the holder the remedy.** List your own live sessions, kill one, or sign out everywhere — no rank, no permission, no intermediary.
|
|
18
|
+
2. **Cap the sliding window.** Stop `expires_at` at some absolute distance from `created_at`, so every session dies eventually whether anybody notices or not.
|
|
19
|
+
|
|
20
|
+
## Decision
|
|
21
|
+
|
|
22
|
+
**Ship (1). Do not ship (2) — for now, and for stated reasons rather than by omission.**
|
|
23
|
+
|
|
24
|
+
The own-scoped routes land as `GET /me/sessions`, `POST /me/sessions/revoke` and `POST /me/sessions/revoke-all`, in their own file (`src/bongos/routes/my-sessions.js`), gated on `requireBuilder` alone. Every statement they issue carries `builder_id = <the caller's own id>`, taken from the session the caller presented and never from the query string, the body or the path. `revoke-all` deliberately includes the session making the request: this is the button somebody presses when they believe a token has leaked, and a sweep that spared one session would leave them wondering which. The token value is never selected, never logged and never returned.
|
|
25
|
+
|
|
26
|
+
**Why no absolute ceiling.** A ceiling and a revoke are aimed at different things, and only one of them was the gap:
|
|
27
|
+
|
|
28
|
+
- A ceiling is **unconditional**: it signs out every builder, every dev box, every long-running autonomous session on a timer, whether or not anything has gone wrong. Migration 035 already met this problem once, in the small — it backfilled a 24h grace *specifically* so the migration would not kill the session running it — and a ceiling reintroduces that class of breakage permanently rather than once.
|
|
29
|
+
- A ceiling is **slow where it matters**: any cap loose enough not to be a daily nuisance (30 days, 90 days) leaves a leaked token live for weeks. The victim who knows their token leaked does not want it to expire eventually; they want it dead now. That is exactly what (1) gives them, in one request.
|
|
30
|
+
- A ceiling has **no owner-visible upside** once (1) exists. Its remaining value is against a leak nobody ever notices — real, but strictly weaker than the visibility this task also ships: the list surfaces `created_at` beside `last_used_at`, so a session that has been alive for three months is now something a person can *see* and act on, which is the compounding half of the fix.
|
|
31
|
+
- Rank changes already do the sweeping a ceiling would do. `setBuilderRank` calls `revokeAllSessionsForBuilder` on any demotion, and `requireBuilder` denies a deactivated builder on the hot path regardless of a live token, so the authority-drift case a ceiling is often justified by is already closed by other means.
|
|
32
|
+
|
|
33
|
+
The ceiling stays deliberately unbuilt, not forgotten. What would change the answer is stated below.
|
|
34
|
+
|
|
35
|
+
### Rejected
|
|
36
|
+
|
|
37
|
+
- **An absolute `created_at + N` cap now.** Breaks every long-lived box and autonomous session on a timer for a threat the revoke answers faster; and any N large enough to be tolerable is too large to be a remedy.
|
|
38
|
+
- **A shorter TTL.** Same objection, worse: it punishes ordinary use continuously and still does nothing the moment a token is in someone else's hands.
|
|
39
|
+
- **Three more routes inside `routes/auth.js`.** That file is the largest in `src/bongos/routes/` and its admin pair legitimately reads a caller-named target from the request body. Keeping the own-scoped half separate is what lets `tests/own_session_revoke.mjs` assert the no-caller-named-owner rule over a whole file, with no carve-out for a neighbour to hide behind.
|
|
40
|
+
- **A `keep_current` flag on revoke-all.** Configurability nobody asked for: somebody who wants to keep one session revokes the others individually — the list marks which one is current.
|
|
41
|
+
- **404 on a session id that is not yours.** It would confirm which ids exist. A miss answers `revoked: 0`, identically to an id that never existed.
|
|
42
|
+
- **Lowering the `auth.session.revoke` floor instead.** That widens an admin atom to reach *other people's* sessions, which is the opposite of what a self-serve remedy needs.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
- A builder at any rank — `xenos` included — can see and kill their own sessions without an Archon. The cross-builder admin pair is untouched and still permission-gated; this is an addition, not a replacement.
|
|
47
|
+
- A session in continuous use still never expires on its own. That is now a *visible* property (the list shows how old each session is) with a one-click remedy, rather than an invisible one with no remedy.
|
|
48
|
+
- The three routes are pinned to `any-builder` in `EXPECTED_RANKS`, so both a later rank gate *and* a later widening to public fail the route-rank auditor loudly.
|
|
49
|
+
- The hall's Settings page grows an "Active sessions" panel with a per-session revoke and a "Sign out everywhere" button. Signing out everywhere from the browser ends that browser session too, so the page sends the builder back to sign-in.
|
|
50
|
+
|
|
51
|
+
## When to revisit
|
|
52
|
+
|
|
53
|
+
Add the ceiling if any of these become true:
|
|
54
|
+
|
|
55
|
+
- Instances start issuing sessions to principals that cannot be asked to notice a leak (machine identities, embedded devices) — those want a cap, because nobody is watching the list.
|
|
56
|
+
- A compliance or hosting requirement names a maximum session age.
|
|
57
|
+
- The session list shows, in practice, that long-lived sessions accumulate faster than people prune them — i.e. the visibility half of this decision turns out not to work.
|
|
58
|
+
|
|
59
|
+
If it is added, it belongs on `created_at` (a true ceiling), not on the TTL, and it needs the same grace treatment migration 035 gave the 24h TTL, or it will sign out the session that deploys it.
|
package/docs/adr/README.md
CHANGED
|
@@ -292,5 +292,7 @@ This keeps the decision history honest and traceable.
|
|
|
292
292
|
| 0202 | [**A connection is one directed row on the platform account, and every degree label is computed over the graph the viewer is allowed to see** ([task 1002294](https://cloudbongos.com/builders#/task/1002294), goal 1000045 criterion C6 — the design spike for the connections wave). Criterion C6 asks for LinkedIn-shaped connections; the cross-project data plane it must sit on is account-global (ADR 0141) and governed by an owner-signed privacy matrix (`docs/specs/<redacted>.md` D1/D2), where ADR 0171 D4 already bans "a count plus a hidden total" as a leak by subtraction. Decisions: (1) **one directed request row** on a new `platform_identity_connections` — `requester`/`addressee` kept because the inbox, cancel and cooldown all need to know who asked, with `pair_lo`/`pair_hi` GENERATED from LEAST/GREATEST and a partial unique index over `pending|accepted` so a duplicate AND a simultaneous reciprocal request are database errors, not route bugs; module-owned and `platform_identity_`-namespaced (ADR 0083 §5). (2) The `goal_membership_requests` lifecycle (`pending → accepted|rejected|cancelled`, atomic resolve-and-act) with three deliberate divergences: a decline is **silent**, a re-ask inside its 90-day cooldown is **swallowed and answered as if it worked** (ADR 0194 §3 applied to a person — a 409 saying "declined" would disclose what the silence protects), and pending requests expire at **30 days read-time** (privacy spec D4), with expiry resolved in the same statement that inserts, since `now()` cannot live in an index predicate. Remove **hard-deletes** — a "A removed B" tombstone is data neither party consented to keep and no read path uses. (3) Degree = a **recursive CTE seeded at the viewer**, `DEGREE_MAX = 3` with `FANOUT_1`/`FANOUT_2` caps and a deterministic frontier order; past the caps the label is `null` ("out of network"), never a number the query did not establish. Envelope stated honestly: single-digit ms at hub scale, caps bite around 10⁴ accounts, and they convert a hub-heavy account's millions-wide 3-hop into a *missing label* on a page that still renders. (4) Connections get **no predicate of their own** — they consume task 1002967's single effective-visibility reader, published as JS and as `accountVisibleSql(alias)` (the ADR 0182 form); `profile_state='active'` floors the feature, a private account answers **404 not 403** and never appears in a public list, `hide_stats` suppresses the numbers beside an edge but not the edge, degree is an **own-scope read** so viewer-relative labels coexist with viewer-independent public responses — and every count is the length of the array the viewer may see. (5) "Builders you may know" from mutuals + shared projects + recency, one explainable linear score, a named project only where its catalogue row is publicly listed; shared goals refused (instance-local), guilds a reserved empty slot, and **Discord refused outright** (outside the trust boundary; the hub stores no Discord identity), as are the GitHub follower graph and address-book import. Rejected: two mirrored rows on accept; a symmetric edge with no requester; a core table beside `builders`; a materialised closure/adjacency table (privacy-filtered at write time = a cached visibility decision, which ADR 0171 D2 forbids); an in-process graph cache; a graph database. Deferred and named: a connections-specific hide toggle, blocking, what an accepted connection unlocks on a private account, and notifications beyond the existing attention card. Exposes to the guild/orb wave: `neighbours`, `degreeBetween`, `mutualCount` — **not** a stored strength scalar.](<redacted>.md) | platform identity / privacy / social graph |
|
|
293
293
|
| 0203 | [**The doorway changelog leaves `src/module-api.js` — the CI auto-patch carrier appends to a markdown ledger instead** ([task 1003335](https://cloudbongos.com/builders#/task/1003335)). Amends [ADR 0161](<redacted>.md) §2. The `CORE_VERSION` release history lived as a comment block inside the doorway file, and ADR 0161's carrier appends two lines to it on EVERY merge-triggered release, pushed by `github-actions[bot]` straight to `main` — bypassing the `unit` gate it breaks. Release 1.19.407 took the file to 1501 lines, one over the `oversized_file_count` ratchet: `unit` went red on `main` and every open PR inherited it, self-inflicting and monotonic. Decision: the ledger moves to `docs/module-api-changelog.md` — markdown, which `fitness-ratchets.js` (`git ls-files '*.js'`) structurally never counts, so it can grow forever. All 1222 lines / 448 versions moved verbatim (only the `// ` marker stripped, kept in a fenced block so the hanging indent survives); the `const CORE_VERSION` literal and its line shape do NOT move, so every reader that greps it is untouched. `bump-version.js` transforms both files before writing either and refuses a half-applied bump; `publish.yml` stages both, pinned by `tests/bump_version.mjs`; the file is registered in `PUBLISH_ALLOWLIST` so the history keeps shipping inside `@bongos/core`; `src/module-api.js` keeps a pointer doc-comment where the block was. Rejected: trimming entries in place (buys one release), raising the baseline (out of policy per `config/fitness-baselines.json`'s own `_comment`), exempting the file from the ratchet.](<redacted>.md) | build pipeline / distribution |
|
|
294
294
|
| 0204 | [**The apex is dark only; light is the builders' hall's** ([task 1003336](https://cloudbongos.com/builders#/task/1003336), goal 1000080). Supersedes in part [ADR 0196](<redacted>.md) (the light-mode half of the landing build) and the landing direction's 2026-08-27 pick of "light rebuilt to the dark standard". The owner, reviewing the planets: "only black or only white is a bit bland… leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall", confirmed as the WHOLE apex. Decision: `/`, `/projects`, `/privacy`, `/terms` and `/contact` pin `data-mode="dark"` on `<html>` — no pre-paint script, no `cb-color-mode`, no `?mode=`, no OS-preference read; the moon/sun glyph and its wiring leave both bars; the hall's own `hall-theme` switch is the platform's only light/dark control. The light tier of `world.css` / `cosmos.css` and the dark-twins rule STAY until the tier's own removal (the light-pair contrast tests keep measuring). The kit learns `"modes": ["dark"]` in a states file (validated; `render.js` takes it as the default). Rejected: a dark default with the toggle kept; deleting the light tier in the same diff; `--modes dark` on every kit call instead of the page's contract.](<redacted>.md) | apex / design system |
|
|
295
|
+
| 0205 | [**A federated check-in may not speak for a person the hub never saw sign in: the membership carries the hub's own witness, and identity is never re-synced from client credentials** ([task 1003340](https://cloudbongos.com/builders#/task/1003340), goal 1000045 — closes security report 1000027, high, LIVE on the hub). `POST /sso/membership/check-in` authenticated the CALLER (`client_id` + `client_secret`) and took the SUBJECT (`github_id`, `github_login`, `avatar_url`) from the request body — client credentials say which PROJECT is calling, never who the person is. So any registered SSO client could (1) rewrite a real person's public `github_login` and `avatar_url`, which `upsertAccount`'s `ON CONFLICT` re-synced unconditionally and stored with no scheme check, and (2) mint the `<redacted>` row that [ADR 0171](<redacted>.md) D7's `membership_required` precheck reads — so the anti-impersonation gate on `POST /sso/activity/rollup` was satisfiable by the very call it was meant to gate, and forged credits/karma/rank reached a public profile. Decisions: (1) migration `<redacted>` adds `signin_verified_at`, written ONLY by `recordFederatedSignin` from the two moments the HUB itself authenticated a human for a client — `/sso/token` (a code minted under a live hub session, bound to the pair) and `/sso/device/poll` (a GitHub approval, audience bound from the typed origin) — so no request-body field reaches the statement and the column is evidence a caller structurally cannot write; (2) `upsertMembership` becomes `touchMembership`, an UPDATE gated on that column — a check-in can no longer CREATE a membership, answering `409 signin_required` and writing nothing, while signing in accepts a standing `pending` invite and leaves any other coarse kind to the instance to report; (3) a federated client gets insert-only `ensureAccount` (`DO NOTHING`), and `upsertAccount`'s identity re-sync belongs to hub-authenticated callers only — nothing is lost, because both of those already re-sync on every sign-in from GitHub's own answer; (4) the rollup gate reads `membershipBacksRollup` (non-pending AND witnessed), not row-existence; (5) `safeAvatarUrl` admits absolute https only, applied inside both account writers rather than at each render; (6) NO backfill — no predicate separates a legitimate check-in row from a forged one, and the first sign-in after deploy heals a real membership because the witness is stamped BEFORE the check-in and rollup that follow it, so a profile goes stale, never blank. No wire-format change: the request schemas are byte-identical and the ADR 0177 `membership_kind` window is untouched, so no federated instance upgrades. Proof `tests/sso_checkin_identity_spoof.mjs` (DB-free, in the default unit gate; 10 of 14 assertions fail against the pre-fix code, 2 controls prove the gate is not a wall). Rejected: carrying the hub assertion in the check-in body (strict validation would 400 every un-upgraded instance); ADR 0156-style source-guarding (answers "did the user set this", the wrong question when the writer is a third party); backfilling `signin_verified_at = joined_at`; a silent 200; retro-filtering `listMembershipsForAccount`.](<redacted>.md) | platform identity / security |
|
|
296
|
+
| 0206 | [**A leaked bearer's remedy is the account holder's own revoke, not an absolute session ceiling** ([task 1003341](https://cloudbongos.com/builders#/task/1003341), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `lookupSession` rewrites `expires_at = now() + 24h` on every authenticated request, so a CLI bearer in daily use never expires; if it leaks, the holder keeps it alive simply by using it. `POST /auth/logout` kills only the token presented on that same request — useless when the token you want dead is the one you no longer hold — and the two endpoints that can kill another session are gated on the admin `auth.session.revoke` atom, so the victim had to find an Archon, and a single-owner self-hosted instance may have none. Decision: ship the OWN-scoped remedy (`GET /me/sessions`, `POST /me/sessions/revoke`, `POST /me/sessions/revoke-all` on `requireBuilder` alone, every statement keyed on the caller's own id from the presented session, the token value never selected or returned, `revoke-all` deliberately including the requesting session) and deliberately NOT an absolute ceiling on the sliding refresh: a ceiling is unconditional (migration 035 already had to backfill a 24h grace so it would not kill the session running it), any cap loose enough not to be a daily nuisance leaves a leaked token live for weeks, and the visibility this task ships — `created_at` beside `last_used_at` in the list — is the compounding half of the fix. A demotion already sweeps sessions, and a deactivated builder is denied on the hot path, so the authority-drift case is closed by other means. Rejected: a `created_at + N` cap now; a shorter TTL; three more routes inside the already-largest `routes/auth.js` (a separate file is what lets the no-caller-named-owner rule be asserted over a WHOLE file); a `keep_current` flag; 404 on a session id that is not yours (it would confirm which ids exist); lowering the `auth.session.revoke` floor. Revisit for machine principals, a compliance maximum, or evidence that people do not prune what the list shows them.](<redacted>.md) | auth / account security |
|
|
295
297
|
|
|
296
298
|
> ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
|