@bongos/core 1.23.7 → 1.23.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.bongos-core.json CHANGED
@@ -2,11 +2,11 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.23.7",
6
- "core_contract": "1.23.7",
7
- "source_commit": "b3d8fd1d43987850a49b0262cddfb454592c3baa",
5
+ "core_version": "1.23.8",
6
+ "core_contract": "1.23.8",
7
+ "source_commit": "71abc904d0973c3f46522ec7fd473ad9dcbe3740",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-26T23:49:17.830Z",
9
+ "built_at": "2026-10-27T00:17:49.705Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 616,
@@ -17,7 +17,7 @@
17
17
  "gate": "passed"
18
18
  },
19
19
  "file_count": 4206,
20
- "tree_sha256": "a3771c367fe15a46a3890c73c3a51d5233d9722c445cef33b41f0c5c2689906a",
20
+ "tree_sha256": "bc6631716475089c36b86b3553193208ab3f55a824cc34fcc9739e5b46f1dc0f",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2422,12 +2422,12 @@
2422
2422
  {
2423
2423
  "path": "docs/copy-inventory.md",
2424
2424
  "mode": "0000644",
2425
- "sha256": "698489ed0ecdf125ef705bbcb953830453c22ae8632edb437d8f3ddd685d3561"
2425
+ "sha256": "db72d41e471428058a50aa84790a1a82260e4a05695f8b3ace417962a68edbb2"
2426
2426
  },
2427
2427
  {
2428
2428
  "path": "docs/copy-registry.json",
2429
2429
  "mode": "0000644",
2430
- "sha256": "aea042d0a416623e897295c7b959d0eeed8c4d2759d859e35e4715d5c18eb936"
2430
+ "sha256": "aad1e5ab6a606bec9e29f2e80eebfb072e36b3ad3cfd71ba2e8b58488676bd67"
2431
2431
  },
2432
2432
  {
2433
2433
  "path": "docs/design/apex-pass-2-direction.md",
@@ -2937,7 +2937,7 @@
2937
2937
  {
2938
2938
  "path": "docs/module-api-changelog.md",
2939
2939
  "mode": "0000644",
2940
- "sha256": "91bc98777d143a46a4acdb3afc7662f9b6069fd5d2954f6b9aefd03d1d327f87"
2940
+ "sha256": "6b3ccce147bbb46647a2bd411fb35559bf0c7c0a3d268bcb5f5bb2996641d8dd"
2941
2941
  },
2942
2942
  {
2943
2943
  "path": "docs/modules-contract.md",
@@ -3027,7 +3027,7 @@
3027
3027
  {
3028
3028
  "path": "docs/page-readings.json",
3029
3029
  "mode": "0000644",
3030
- "sha256": "3fa8c79139ee97fb0bc2b7ab3ce5d194984797f294ba9cedb78e322e95d6fa8d"
3030
+ "sha256": "a659b466d5f83465efc73e9bb28ce33ef4bd67b543b1ec998be10b39e697d992"
3031
3031
  },
3032
3032
  {
3033
3033
  "path": "docs/project-context.template.md",
@@ -8672,7 +8672,7 @@
8672
8672
  {
8673
8673
  "path": "modules/platform-identity/routes/sso.js",
8674
8674
  "mode": "0000644",
8675
- "sha256": "e6896461ed1359d8c71c4af01cdceba2df3d644d31ebd807bff3bcdfa88697fb"
8675
+ "sha256": "84beda2da932dec7f94056d78ca766ec4da4ffbe5a422e4290e0fbd318a5c8a6"
8676
8676
  },
8677
8677
  {
8678
8678
  "path": "modules/platform-identity/scouting-authz.js",
@@ -8687,7 +8687,7 @@
8687
8687
  {
8688
8688
  "path": "modules/platform-identity/terms-acceptance.js",
8689
8689
  "mode": "0000644",
8690
- "sha256": "00d3d1460d1fa0d224e487a1701309b4725f2872a36a0cfe283cbd21b96d1e53"
8690
+ "sha256": "bc9be6664fef1f8854bf6cdbfddd872c4dee04d7a7df076adc63ff2b7d4125ce"
8691
8691
  },
8692
8692
  {
8693
8693
  "path": "modules/platform-identity/tests/activity-snapshots.mjs",
@@ -9947,7 +9947,7 @@
9947
9947
  {
9948
9948
  "path": "modules/public-landing/public/terms.html",
9949
9949
  "mode": "0000644",
9950
- "sha256": "54a9a74ee0398d5e309e3ccf1b657a612b2785bd9295304670a1e2cb7a2bc93a"
9950
+ "sha256": "ea71459f4cd48240d7607d2ed9538cdedcdfc71ec80ae25c8960596aab2ffc37"
9951
9951
  },
9952
9952
  {
9953
9953
  "path": "modules/raise/CLAUDE.md",
@@ -10837,12 +10837,12 @@
10837
10837
  {
10838
10838
  "path": "package-lock.json",
10839
10839
  "mode": "0000644",
10840
- "sha256": "b2707b768f0b92534351f237e4c1f2ad46894471f0f639f770bfa0b361fa584f"
10840
+ "sha256": "c2355516436464dd695de67ba3e9fc57f4cc82782f63716f694b4296f83a7200"
10841
10841
  },
10842
10842
  {
10843
10843
  "path": "package.json",
10844
10844
  "mode": "0000644",
10845
- "sha256": "2e911efb9044f425ef86de18b03ccadf10913f7db81eac836fc07ab901bd2011"
10845
+ "sha256": "fcb3c2902fb41d0a2cb4effa8cb28d614664f390c0e7a9bdded7b472069f3fd5"
10846
10846
  },
10847
10847
  {
10848
10848
  "path": "public-docs/index.html",
@@ -10862,7 +10862,7 @@
10862
10862
  {
10863
10863
  "path": "release-notes.json",
10864
10864
  "mode": "0000644",
10865
- "sha256": "56aff299cc54f61132be2b6d1877e50cc35282ed1e759726246d0ae0f131f343"
10865
+ "sha256": "2283bdaf35c46a9079b03e2a9dff5fd44153ec42ad3ece5045b123ce193523e4"
10866
10866
  },
10867
10867
  {
10868
10868
  "path": "scripts/bongos/CLAUDE.md",
@@ -14337,7 +14337,7 @@
14337
14337
  {
14338
14338
  "path": "src/bongos/auth-github.js",
14339
14339
  "mode": "0000644",
14340
- "sha256": "00ad42eac32546ed08948c89ade5b8a4ebaada1841a651bb84c124d659fbbbc8"
14340
+ "sha256": "8d02a4f4fd3281fb65c926d4e837499db5050d47979c1ff4ec5e9b441994610e"
14341
14341
  },
14342
14342
  {
14343
14343
  "path": "src/bongos/auth-idp.js",
@@ -14737,7 +14737,7 @@
14737
14737
  {
14738
14738
  "path": "src/module-api.js",
14739
14739
  "mode": "0000644",
14740
- "sha256": "6b5b56cd550854c711eef1cd99f72c7c7f52a781bb0b6391845ff8a3fb1f78c9"
14740
+ "sha256": "50b0b05cd25ea29cc845c537b33150b22115f65e680d4727a04673526f0da98e"
14741
14741
  },
14742
14742
  {
14743
14743
  "path": "src/module-loader/catalog.js",
@@ -20632,7 +20632,7 @@
20632
20632
  {
20633
20633
  "path": "tests/terms_acceptance.mjs",
20634
20634
  "mode": "0000644",
20635
- "sha256": "7d30b0a0545c630157ff1b2a642ab588397e316575375a105d94f3ad82fbba70"
20635
+ "sha256": "56b6c06ada67f3cff5ae2bc05182cdf11ffa9028b8725d0eeb35267bd9876e76"
20636
20636
  },
20637
20637
  {
20638
20638
  "path": "tests/terms_grandfather_db.mjs",
@@ -2300,7 +2300,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2300
2300
  | `a86a02b0ded9` | certain | How your project’s own hall looks: its colours in light and dark mode, whether its dark mode takes a tint of y | `modules/public-landing/public/projects.html:1447` |
2301
2301
  | `23febbbd53e2` | certain | I can sign for this project, and I accept the | `modules/public-landing/public/projects.html:2056` |
2302
2302
  | `915b187df57e` | certain | If you grant repository access while creating a project, that permission is used to create or adopt your repos | `modules/public-landing/public/privacy.html:103` |
2303
- | `d37e2fbde8c6` | certain | If your account already existed when this version took effect, the platform accepted it on your behalf, rather | `modules/public-landing/public/terms.html:119` |
2303
+ | `f11a5798d212` | certain | If your account already existed when this version took effect, the platform accepted it on your behalf, rather | `modules/public-landing/public/terms.html:119` |
2304
2304
  | `fa123fc3eda3` | certain | In a sentence | `modules/public-landing/public/projects.html:1627` |
2305
2305
  | `4b4238c12e62` | certain | In this guild’s totals | `modules/public-landing/public/guild.html:212` |
2306
2306
  | `ba0c71b83a99` | certain | Install it on your repo ↗ | `modules/public-landing/public/projects.html:2110` |
@@ -2386,7 +2386,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2386
2386
  | `1bc11853e893` | certain | On the map | `modules/public-landing/public/projects.html:1303` |
2387
2387
  | `b4ac8f44710c` | certain | On the public map | `modules/public-landing/public/projects.html:1225` |
2388
2388
  | `d7d88100f4a8` | certain | On this guild’s public page | `modules/public-landing/public/guild.html:204` |
2389
- | `1fdda2903cf9` | certain | Once you have signed in to a project in your browser with your platform account and accepted these terms, your | `modules/public-landing/public/terms.html:116` |
2389
+ | `b43bc7940904` | certain | Once you have signed in with your platform account in your browser, here or to a project, and accepted these t | `modules/public-landing/public/terms.html:116` |
2390
2390
  | `be2e114ee93f` | certain | One fewer | `modules/public-landing/public/projects.html:1848` |
2391
2391
  | `84eccbf0c014` | certain | One fewer person | `modules/public-landing/public/projects.html:1579` |
2392
2392
  | `a8fa42b47d99` | certain | One hour less | `modules/public-landing/public/projects.html:1586` |
@@ -2560,7 +2560,7 @@ Identical copy within one surface. Sometimes right (a repeated button), sometime
2560
2560
  | `a954f0dce9a3` | certain | The platform | `modules/public-landing/public/contact.html:113` |
2561
2561
  | `82d751a27370` | certain | The platform first gives this project its own read-only deploy key and switches its deploys over to it, and on | `modules/public-landing/public/projects.html:1257` |
2562
2562
  | `4fd8f5746cc5` | certain | The platform is run as a non-profit, capture-resistant commons. | `modules/public-landing/public/terms.html:98` |
2563
- | `768a6990ebfc` | certain | The platform stores the date you accepted these terms, and which version, on your account. It is written when | `modules/public-landing/public/terms.html:118` |
2563
+ | `f4e546b36564` | certain | The platform stores the date you accepted these terms, and which version, on your account. It is written when | `modules/public-landing/public/terms.html:118` |
2564
2564
  | `20fcebaa85c4` | certain | The project hub needs JavaScript — the map, your projects, and starting a new one all live here. You can read | `modules/public-landing/public/projects.html:888` |
2565
2565
  | `4a740b5afe73` | certain | The project map | `modules/public-landing/public/projects.html:898` |
2566
2566
  | `d4e91fc9e8ce` | certain | The projects you build on, and leaving one, are on your account page. | `modules/public-landing/public/projects.html:1090` |
@@ -21657,9 +21657,9 @@
21657
21657
  "confidence": "certain"
21658
21658
  },
21659
21659
  {
21660
- "id": "d37e2fbde8c6",
21660
+ "id": "f11a5798d212",
21661
21661
  "surface": "landing",
21662
- "text": "If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in to a project in your browser.",
21662
+ "text": "If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in that way.",
21663
21663
  "file": "modules/public-landing/public/terms.html",
21664
21664
  "line": 119,
21665
21665
  "origin": "html-text",
@@ -22431,9 +22431,9 @@
22431
22431
  "confidence": "certain"
22432
22432
  },
22433
22433
  {
22434
- "id": "1fdda2903cf9",
22434
+ "id": "b43bc7940904",
22435
22435
  "surface": "landing",
22436
- "text": "Once you have signed in to a project in your browser with your platform account and accepted these terms, your profile and that record are public by default: at your handle’s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in to a project that way stays provisional and is published nowhere.",
22436
+ "text": "Once you have signed in with your platform account in your browser, here or to a project, and accepted these terms, your profile and that record are public by default: at your handle’s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in that way stays provisional and is published nowhere.",
22437
22437
  "file": "modules/public-landing/public/terms.html",
22438
22438
  "line": 116,
22439
22439
  "origin": "html-text",
@@ -23997,9 +23997,9 @@
23997
23997
  "confidence": "certain"
23998
23998
  },
23999
23999
  {
24000
- "id": "768a6990ebfc",
24000
+ "id": "f4e546b36564",
24001
24001
  "surface": "landing",
24002
- "text": "The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in to a project in your browser with your platform account, and only once — signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub’s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in to a project in your browser once. This is version",
24002
+ "text": "The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in with your platform account in your browser, here or to a project, and only once — signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub’s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in once in your browser. This is version",
24003
24003
  "file": "modules/public-landing/public/terms.html",
24004
24004
  "line": 118,
24005
24005
  "origin": "html-text",
@@ -3231,5 +3231,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3231
3231
  landed since 1.23.5 with no explicit bump. run 40420658396. (task 1002620)
3232
3232
  1.23.7 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3233
3233
  landed since 1.23.6 with no explicit bump. run 40422218359. (task 1002620)
3234
+ 1.23.8 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3235
+ landed since 1.23.7 with no explicit bump. run 40425356400. (task 1002620)
3234
3236
  ---------------------------------------------------------------------------
3235
3237
  ```
@@ -5579,8 +5579,8 @@
5579
5579
  "surface": "landing",
5580
5580
  "title": "{{productName}} — Terms",
5581
5581
  "states_read": ["default"],
5582
- "files_hash": "43d9344847eeced2",
5583
- "reading_hash": "4c8c1d349a6a79c2",
5582
+ "files_hash": "1a299c86554b3ec1",
5583
+ "reading_hash": "fc023f0ba4f509c1",
5584
5584
  "counts": {"lines":46,"placed":31,"shared":10,"unplaced":5},
5585
5585
  "omitted": {"data":1},
5586
5586
  "notes": [],
@@ -5605,13 +5605,13 @@
5605
5605
  {"key":"L0018","section":"Your work across projects","text":"These numbers describe; they do not govern.","placement":"placed","file":"modules/public-landing/public/terms.html","line":115,"string_id":"02e596cc4569"},
5606
5606
  {"key":"L0019","section":"Your work across projects","text":"Your cross-project record is descriptive analytics and never authority. What you may do inside any project is decided by that project alone, from its own records, at the moment you ask. A large record grants nothing and a hidden one takes nothing away; no project is required to read it, and none may treat it as permission. A rank shown here is that project’s own standing for you, as that project reported it — the platform holds no rank of its own.","placement":"placed","file":"modules/public-landing/public/terms.html","line":115,"string_id":"1e6d91f484e4"},
5607
5607
  {"key":"L0020","section":"Your work across projects","text":"What is public.","placement":"placed","file":"modules/public-landing/public/terms.html","line":116,"string_id":"9b3312899fb1"},
5608
- {"key":"L0021","section":"Your work across projects","text":"Once you have signed in to a project in your browser with your platform account and accepted these terms, your profile and that record are public by default: at your handle’s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in to a project that way stays provisional and is published nowhere.","placement":"placed","file":"modules/public-landing/public/terms.html","line":116,"string_id":"1fdda2903cf9"},
5608
+ {"key":"L0021","section":"Your work across projects","text":"Once you have signed in with your platform account in your browser, here or to a project, and accepted these terms, your profile and that record are public by default: at your handle’s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in that way stays provisional and is published nowhere.","placement":"placed","file":"modules/public-landing/public/terms.html","line":116,"string_id":"b43bc7940904"},
5609
5609
  {"key":"L0022","section":"Your work across projects","text":"What you control.","placement":"placed","file":"modules/public-landing/public/terms.html","line":117,"string_id":"6130dcb60532"},
5610
5610
  {"key":"L0023","section":"Your work across projects","text":"From your settings you can hide your statistics, drop out of the recruiting directory, or make your account private. Each takes effect immediately, everywhere, including on a page someone already has open. Hiding removes the platform’s combined view; it does not retract the per-project numbers each project publishes itself, so a determined reader could still add them up the hard way.","placement":"placed","file":"modules/public-landing/public/terms.html","line":117,"string_id":"0808df6e434c"},
5611
5611
  {"key":"L0024","section":"Your work across projects","text":"Your acceptance is recorded.","placement":"placed","file":"modules/public-landing/public/terms.html","line":118,"string_id":"0f06f7e72ea7"},
5612
- {"key":"L0025","section":"Your work across projects","text":"The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in to a project in your browser with your platform account, and only once — signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub’s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in to a project in your browser once. This is version","placement":"placed","file":"modules/public-landing/public/terms.html","line":118,"string_id":"768a6990ebfc"},
5612
+ {"key":"L0025","section":"Your work across projects","text":"The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in with your platform account in your browser, here or to a project, and only once — signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub’s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in once in your browser. This is version","placement":"placed","file":"modules/public-landing/public/terms.html","line":118,"string_id":"f4e546b36564"},
5613
5613
  {"key":"L0026","section":"Your work across projects","text":"Accounts from before these terms.","placement":"placed","file":"modules/public-landing/public/terms.html","line":119,"string_id":"9b02ec8a2e21"},
5614
- {"key":"L0027","section":"Your work across projects","text":"If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in to a project in your browser.","placement":"placed","file":"modules/public-landing/public/terms.html","line":119,"string_id":"d37e2fbde8c6"},
5614
+ {"key":"L0027","section":"Your work across projects","text":"If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in that way.","placement":"placed","file":"modules/public-landing/public/terms.html","line":119,"string_id":"f11a5798d212"},
5615
5615
  {"key":"L0028","section":"Fair use","text":"Fair use","placement":"placed","file":"modules/public-landing/public/terms.html","line":121,"string_id":"2585087f4873"},
5616
5616
  {"key":"L0029","section":"Fair use","text":"Don't use {{productName}} to break the law, to attack this site or other people's projects, to mine, spam or otherwise abuse hosted resources, or to harass other builders. The owner may deactivate a builder or take a project offline for breaking these, and may set further house rules inside the hall.","placement":"placed","file":"modules/public-landing/public/terms.html","line":122,"string_id":"8d812912c392"},
5617
5617
  {"key":"L0030","section":"Liability","text":"Liability","placement":"unplaced"},
@@ -225,6 +225,13 @@ function registerSeams() {
225
225
  // when a founder ticks the create wizard's terms box. A READ of the constant only —
226
226
  // it stamps nothing on any account (that stays terms-acceptance.js's alone).
227
227
  termsVersion: () => terms.TERMS_VERSION,
228
+ // task 1003361 (ADR 0217 D3): the hub's OWN browser sign-in is the second place
229
+ // a human accepts the platform terms, beside /sso/authorize. Core passes the
230
+ // identity GitHub returned for the code it exchanged, never a request field,
231
+ // and `activate: true` is a source literal here exactly as it is there.
232
+ recordHubSignin: ({ githubId, githubLogin, displayName, avatarUrl }) => pi.upsertAccount({
233
+ githubId, githubLogin, displayName, avatarUrl, activate: true,
234
+ }, { pool }),
228
235
  });
229
236
  }
230
237
  }
@@ -307,8 +314,9 @@ module.exports = function ssoRoutes() {
307
314
 
308
315
  // A live hub session — record/refresh the platform account and mint a
309
316
  // single-use code bound to (client_id, github_id, redirect_uri).
310
- // `activate: true` — this is THE consent upgrade path (ADR 0171 D3): a
311
- // hub-session authorize is the one place a human is demonstrably at the hub,
317
+ // `activate: true` — a consent upgrade path (ADR 0171 D3): a hub-session
318
+ // authorize is a place a human is demonstrably at the hub (the hub's own web
319
+ // sign-in is the other, through recordHubSignin above — task 1003361),
312
320
  // so the account's profile_state lazily flips provisional → active here.
313
321
  try {
314
322
  await pi.upsertAccount({
@@ -23,15 +23,16 @@
23
23
  // predicate, and deliberately does not match a write.
24
24
  //
25
25
  // WHY THE CALLER CANNOT FORGE ONE. These fragments have exactly one call site:
26
- // upsertAccount's `activate` branch, whose only caller is /sso/authorize
27
- // (routes/sso.js) — the leg a builder passes through when they use their platform
28
- // account to sign in to a PROJECT. Signing in to the hub itself does not reach it
29
- // and records nothing (blocker 1000124; task 1003361 adds that capture point),
30
- // which is why /terms names a project sign-in. There `activate: true` is a SOURCE LITERAL and every other
31
- // field comes from the hub's own resolved session — the ADR 0205 D3 property
32
- // established for recordFederatedSignin by task 1003340. No request-body field
33
- // reaches these statements, which is the whole point: an acceptance record a
34
- // caller could assert would be evidence of nothing.
26
+ // upsertAccount's `activate` branch, which has two callers, both browser sign-ins
27
+ // and both in routes/sso.js — /sso/authorize, the leg a builder passes through when
28
+ // they use their platform account to sign in to a PROJECT, and the hub port's
29
+ // recordHubSignin, which core calls from the hub's own web sign-in (task 1003361;
30
+ // before it a hub-only builder was never stamped, blocker 1000124). In both
31
+ // `activate: true` is a SOURCE LITERAL; the identity is the hub's own resolved
32
+ // session on the first (the ADR 0205 D3 property established for
33
+ // recordFederatedSignin by task 1003340) and GitHub's answer for the exchanged code
34
+ // on the second. No request field reaches these statements, which is the whole
35
+ // point: an acceptance record a caller could assert would be evidence of nothing.
35
36
  //
36
37
  // THE FIRST ACCEPTANCE A HUMAN GAVE STANDS. The conflict branch keeps an existing
37
38
  // acceptance (COALESCE), so a second sign-in never rewrites or duplicates one.
@@ -43,7 +44,7 @@
43
44
  // accounts that already existed were accepted on their owner's behalf, and the
44
45
  // record says so rather than passing that off as a sign-in. A grandfather is not
45
46
  // an acceptance its owner gave, so it is the one acceptance that does not
46
- // "stand": the owner's next project sign-in replaces it with their own. This file only
47
+ // "stand": the owner's next browser sign-in replaces it with their own. This file only
47
48
  // ever READS the marker. tests/terms_acceptance.mjs holds both rules, and fails
48
49
  // any other server file that writes these columns or names the marker.
49
50
  //
@@ -113,10 +113,10 @@
113
113
  <h2>Your work across projects</h2>
114
114
  <p>Each {{productName}} project you build in reports a summary of your work there to the platform: the {{currency}} you earned, the works you shipped, your karma, the rank that project gave you, the roles you worked in, and when you were last active. The platform adds these up across every project into one cross-project record. Projects report this themselves — the platform does not read their databases.</p>
115
115
  <p><b>These numbers describe; they do not govern.</b> Your cross-project record is descriptive analytics and never authority. What you may do inside any project is decided by that project alone, from its own records, at the moment you ask. A large record grants nothing and a hidden one takes nothing away; no project is required to read it, and none may treat it as permission. A rank shown here is that project&rsquo;s own standing for you, as that project reported it &mdash; the platform holds no rank of its own.</p>
116
- <p><b>What is public.</b> Once you have signed in to a project in your browser with your platform account and accepted these terms, your profile and that record are public by default: at your handle&rsquo;s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in to a project that way stays provisional and is published nowhere.</p>
116
+ <p><b>What is public.</b> Once you have signed in with your platform account in your browser, here or to a project, and accepted these terms, your profile and that record are public by default: at your handle&rsquo;s page, in the people directory, to project owners looking for people to work with, and to search engines. An account that has never signed in that way stays provisional and is published nowhere.</p>
117
117
  <p><b>What you control.</b> From your settings you can hide your statistics, drop out of the recruiting directory, or make your account private. Each takes effect immediately, everywhere, including on a page someone already has open. Hiding removes the platform&rsquo;s combined view; it does not retract the per-project numbers each project publishes itself, so a determined reader could still add them up the hard way.</p>
118
- <p><b>Your acceptance is recorded.</b> The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in to a project in your browser with your platform account, and only once &mdash; signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub&rsquo;s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in to a project in your browser once. This is version <b>2026-08-29</b>.</p>
119
- <p><b>Accounts from before these terms.</b> If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in to a project in your browser.</p>
118
+ <p><b>Your acceptance is recorded.</b> The platform stores the date you accepted these terms, and which version, on your account. It is written when you sign in with your platform account in your browser, here or to a project, and only once &mdash; signing in again does not change the date you accepted. Signing in from the command line does not record it: you approve that sign-in on GitHub&rsquo;s page, where these terms are not shown, so an account that has only signed in that way stays unpublished until you sign in once in your browser. This is version <b>2026-08-29</b>.</p>
119
+ <p><b>Accounts from before these terms.</b> If your account already existed when this version took effect, the platform accepted it on your behalf, rather than waiting for you to sign in, and dated it the day that was done. The record marks that acceptance as grandfathered, so it is never mistaken for one you gave, and your own acceptance replaces it the next time you sign in that way.</p>
120
120
  <!--apex-only:end-->
121
121
  <h2>Fair use</h2>
122
122
  <p>Don't use {{productName}} to break the law, to attack this site or other people's projects, to mine, spam or otherwise abuse hosted resources, or to harass other builders. The owner may deactivate a builder or take a project offline for breaking these, and may set further house rules inside the hall.</p>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.7",
3
+ "version": "1.23.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.23.7",
9
+ "version": "1.23.8",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.7",
3
+ "version": "1.23.8",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -10429,5 +10429,11 @@
10429
10429
  "id": "1004896",
10430
10430
  "text": "Builders can now take a module they installed from the store back off their project with one command; its folder is kept aside so nothing is lost."
10431
10431
  }
10432
+ ],
10433
+ "1.23.8": [
10434
+ {
10435
+ "id": "1003361",
10436
+ "text": "Someone who signs in at cloudbongos.com and never joins a project now gets their profile published and can make connections, instead of staying invisible forever."
10437
+ }
10432
10438
  ]
10433
10439
  }
@@ -26,6 +26,7 @@ const seams = require('../module-seams');
26
26
  const { branding, userAgent } = require('../branding');
27
27
  const { firstTimeAdmissionBlocked, maybeSeatFirstAdmin, openEnrollmentEnabled, safeSyncRankRole } = require('./auth-admission.js');
28
28
  const { SCOPE, deviceFlows, loadGithubCreds, scopeForWebFlow } = require('./auth-config.js');
29
+ const log = require('./logger').logger.child({ src: 'auth-github' });
29
30
 
30
31
  function newSessionToken() {
31
32
  return crypto.randomBytes(32).toString('hex');
@@ -240,6 +241,26 @@ async function pollDeviceFlow(deviceCode, userAgent) {
240
241
  // Web Flow (browser)
241
242
  // -------------------------------------------------------------------------
242
243
 
244
+ // The hub's own browser sign-in records the platform terms acceptance (task 1003361,
245
+ // ADR 0217 D3), so a builder who never joins a project is still published. Only the
246
+ // hub provides the function, so every other instance skips it. The identity is
247
+ // GitHub's answer for the exchanged code, never a request field. Best-effort: a
248
+ // failed stamp must not cost the sign-in, and the next sign-in writes it.
249
+ async function recordHubSignin(ghUser) {
250
+ try {
251
+ const hub = seams.resolveOptional('platform-identity.hub');
252
+ if (!hub || typeof hub.recordHubSignin !== 'function') return;
253
+ await hub.recordHubSignin({
254
+ githubId: String(ghUser.id),
255
+ githubLogin: ghUser.login,
256
+ displayName: ghUser.name || ghUser.login,
257
+ avatarUrl: ghUser.avatar_url,
258
+ });
259
+ } catch (err) {
260
+ log.error({ err: err && err.message }, 'hub sign-in terms acceptance not recorded');
261
+ }
262
+ }
263
+
243
264
  function buildAuthorizeUrl(state, redirectUri, { repos = false, switchAccount = false } = {}) {
244
265
  const creds = loadGithubCreds();
245
266
  if (!creds) throw new Error('GitHub OAuth credentials not configured');
@@ -327,6 +348,7 @@ async function completeWebFlow({ code, redirectUri, userAgent, captureRepoToken
327
348
  });
328
349
  // V3.R75 (#294): tick the onboarding state machine. Best-effort.
329
350
  await seams.resolveOptional('onboarding')?.safeMarkStage(pool, builder.id, 'auth_complete', builder);
351
+ await recordHubSignin(ghUser);
330
352
  // Wizard opt-in only (task 1002099 / ADR 0143): when the caller asked to capture
331
353
  // repo access, hand the GitHub access token + granted scope back so the web
332
354
  // callback can persist it (encrypted, short-TTL) for the repo picker/creator.
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.23.7'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.23.8'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -4,9 +4,10 @@
4
4
  // only thing that can say a human accepted the platform terms, it says so once,
5
5
  // and nothing publishes without it" (ADR 0213, task 1002913; the artifact ADR
6
6
  // 0141 §4 deferred cross-project-aggregation consent to and ADR 0171 D3 reserved
7
- // a place for). That sign-in is GET /sso/authorize — a builder using their
8
- // platform account to sign in to a PROJECT. Signing in to the hub itself stamps
9
- // nothing yet (blocker 1000124); task 1003361 adds it as a second capture point.
7
+ // a place for). There are two such sign-ins, both in a browser: GET /sso/authorize
8
+ // (a builder using their platform account to sign in to a PROJECT) and the hub's
9
+ // own web sign-in (task 1003361 — before it, a builder who signed in only at the
10
+ // hub was never stamped, so never published; blocker 1000124).
10
11
  //
11
12
  // WHY THIS EXISTS. An acceptance record is only worth the evidence behind it. If
12
13
  // a caller can assert one — a body field, a query param, a header — then the
@@ -22,13 +23,15 @@
22
23
  // a claim about the platform must not ship inside a child project's page
23
24
  // (task 1002748). Run through the real stripApexOnly, not asserted by eye.
24
25
  //
25
- // 2. THE CAPTURE POINT. The acceptance columns are written by exactly one
26
- // statement, reached from exactly one route — /sso/authorize, where
27
- // `activate: true` is a SOURCE LITERAL and every other field comes from the
28
- // hub's own resolved session (the ADR 0205 D3 property task 1003340
29
- // established for recordFederatedSignin). The test drives the REAL router
30
- // over real HTTP with an attacker-controlled query string and body, and
31
- // proves neither reaches the statement.
26
+ // 2. THE CAPTURE POINTS. The acceptance columns are written by exactly one
27
+ // statement, reached from exactly two routes. /sso/authorize passes
28
+ // `activate: true` as a SOURCE LITERAL with every other field from the hub's
29
+ // own resolved session (the ADR 0205 D3 property task 1003340 established
30
+ // for recordFederatedSignin); the hub's /auth/web/callback reaches the same
31
+ // literal through the `platform-identity.hub` port with the identity GitHub
32
+ // returned for the exchanged code. The test drives BOTH real routes with an
33
+ // attacker-controlled query string, and proves nothing it carries reaches
34
+ // the statement.
32
35
  //
33
36
  // 3. IDEMPOTENCE, EXECUTED. The fake pool below is a real little
34
37
  // platform_identity_accounts table plus an interpreter that reads the
@@ -117,23 +120,22 @@ t('the acceptance the page promises is the one the code actually keeps', () => {
117
120
  'and that it is written once — the property the COALESCE below actually provides');
118
121
  });
119
122
 
120
- // The capture point is GET /sso/authorize — reached when a builder uses their
121
- // platform account to sign in to a PROJECT, IN A BROWSER. Signing in to the hub
122
- // itself stamps nothing (blocker 1000124, task 1003353), so the page must not say
123
- // "here"; and the CLI's project sign-in (the device poll) stamps nothing either
124
- // (task 1004265, executed in §3), so the page must say "in your browser" and tell a
125
- // command-line builder why. Task 1003361 adds the hub's own sign-in as a second
126
- // capture point; when it lands, this wording and §3 below change together.
127
- t('the page names the sign-in the code actually watches — a project sign-in in the browser, not "here" (blocker 1000124)', () => {
123
+ // The capture points are both BROWSER sign-ins with the platform account: the hub's
124
+ // own (task 1003361) and a project's, through GET /sso/authorize — both executed in
125
+ // §3. The page once named only the project sign-in, because the hub's recorded
126
+ // nothing (blocker 1000124, task 1003353); now it names both. The CLI's sign-in (the
127
+ // device poll) stamps nothing (task 1004265), so the page must still say "in your
128
+ // browser" and tell a command-line builder why.
129
+ t('the page names both sign-ins the code watches — a browser sign-in here or to a project (task 1003361)', () => {
128
130
  const section = termsHtml.slice(termsHtml.indexOf('Your work across projects'), termsHtml.indexOf('Fair use'));
129
- assert.doesNotMatch(section, /sign(?:ed|ing)? in here/i,
130
- 'the page promises that signing in to this site records acceptance — the code never stamps that sign-in');
131
- assert.match(section, /Once you have signed in to a project in your browser with your platform account and accepted these terms/,
132
- 'what makes a profile public must name the moment the code captures');
133
- assert.match(section, /never signed in to a project that way stays provisional/,
134
- 'and the provisional case must be the same moment, negated');
135
- assert.match(section, /written when you sign in to a project in your browser with your platform account, and only once/,
136
- 'the acceptance paragraph must name the same capture point');
131
+ assert.match(section, /Once you have signed in with your platform account in your browser, here or to a project, and accepted these terms/,
132
+ 'what makes a profile public must name the moments the code captures');
133
+ assert.match(section, /never signed in that way stays provisional/,
134
+ 'and the provisional case must be the same moments, negated');
135
+ assert.match(section, /written when you sign in with your platform account in your browser, here or to a project, and only once/,
136
+ 'the acceptance paragraph must name the same capture points');
137
+ assert.doesNotMatch(section, /sign(?:ed)? in to a project in your browser/,
138
+ 'the page still names only the project sign-in — since task 1003361 the hub’s own browser sign-in records it too');
137
139
  assert.doesNotMatch(section, /sign(?:ed)? in to a project with your platform account/,
138
140
  'an unqualified "project sign-in" also covers the command line, which records nothing (task 1004265)');
139
141
  });
@@ -151,7 +153,7 @@ t('the page says which acceptances were given on a builder’s behalf, and that
151
153
  'the owner chose to grandfather AND to say so on the page — the page must tell a pre-existing builder what was recorded for them');
152
154
  assert.match(termsHtml, /marks that acceptance as grandfathered/i,
153
155
  'and that the record keeps it distinguishable from one they gave');
154
- assert.match(termsHtml, /your own acceptance replaces it the next time you sign in to a project in your browser/i,
156
+ assert.match(termsHtml, /your own acceptance replaces it the next time you sign in that way/i,
155
157
  'and what happens when they do sign in — the conflict branch below really does replace it');
156
158
  assert.doesNotMatch(termsHtml, /written only by signing in/i,
157
159
  'the page still says acceptance is written ONLY by signing in, which is false for every grandfathered account');
@@ -382,6 +384,7 @@ function makePool() {
382
384
  const noopLog = { info() {}, warn() {}, error() {}, debug() {} };
383
385
  const moduleApiPath = require.resolve('../src/module-api.js');
384
386
  const helpers = require('../src/bongos/routes/_helpers.js');
387
+ const seams = require('../src/module-seams.js');
385
388
  const pool = makePool();
386
389
  let SESSION = null;
387
390
 
@@ -394,8 +397,10 @@ require.cache[moduleApiPath] = {
394
397
  // session; nothing a caller sends can change what it returns here.
395
398
  resolveSession: async () => SESSION,
396
399
  logger: () => noopLog,
397
- hasProvider: () => true,
398
- registerProvider: () => {},
400
+ // The REAL switchboard, so the `platform-identity.hub` port sso.js registers is
401
+ // the one core's completeWebFlow resolves (the hub sign-in capture point, §3).
402
+ hasProvider: seams.hasProvider,
403
+ registerProvider: seams.registerProvider,
399
404
  on: () => () => {},
400
405
  resolveEnv: () => null,
401
406
  userAgent: () => 'cloud-bongos-tests',
@@ -445,7 +450,8 @@ const authorize = (qs = '') => request(
445
450
  );
446
451
 
447
452
  // ===========================================================================
448
- // 3. The capture point — the project sign-in leg (/sso/authorize), and nothing else
453
+ // 3. The capture points — a browser sign-in to a project (/sso/authorize) or to
454
+ // the hub itself (/auth/web/callback), and nothing else
449
455
  // ===========================================================================
450
456
 
451
457
  console.log('\nthe capture point (only the hub can say a human accepted):');
@@ -610,6 +616,144 @@ await ta('a CLI-only builder who later signs in to a project in the browser IS s
610
616
  assert.equal(row.profile_state, 'active');
611
617
  });
612
618
 
619
+ // ---- the second capture point: the hub's OWN browser sign-in (task 1003361) ----
620
+ // Before it, a builder who signed in at the hub and never joined a project was never
621
+ // stamped — so isAccountActive was false, their profile invisible everywhere and
622
+ // their connections denied, for good (blocker 1000124). This drives the REAL
623
+ // /auth/web/callback over the REAL completeWebFlow, which reaches the port sso.js
624
+ // registered above. Only GitHub is stubbed, and the builders table is a small fake
625
+ // on core's pool; the account write runs through the same interpreter as §3's.
626
+ const corePool = require('../src/bongos/pool.js').pool;
627
+ const { envPrefix } = require('../src/instance-config.js');
628
+ process.env[`${envPrefix()}_GITHUB_CLIENT_ID`] = 'test-client';
629
+ process.env[`${envPrefix()}_GITHUB_CLIENT_SECRET`] = 'test-secret';
630
+
631
+ const HUB_ONLY = { id: 4343, login: 'hub-only', name: 'Hub Only', avatar_url: null };
632
+ let hubBuilder = null; // the hub's own builders row for HUB_ONLY, or null for a stranger
633
+ const pickBuilder = (list) => Object.fromEntries(list.split(',').map((c) => c.trim()).map((c) => [c, hubBuilder[c]]));
634
+ corePool.query = async (sql) => {
635
+ if (/INSERT INTO builders\b/.test(sql)) return { rows: [pickBuilder(sql.split('RETURNING')[1])] };
636
+ if (/FROM builders WHERE github_id/.test(sql)) return { rows: hubBuilder ? [pickBuilder(sql.match(/SELECT([\s\S]*?)FROM/)[1])] : [] };
637
+ return { rows: [], rowCount: 0 }; // no invite, no session row to read back
638
+ };
639
+ globalThis.fetch = async (url) => {
640
+ if (String(url) === 'https://github.com/login/oauth/access_token') return { ok: true, json: async () => ({ access_token: 'gho_t', scope: 'read:user' }) };
641
+ if (String(url) === 'https://api.github.com/user') return { ok: true, json: async () => ({ ...HUB_ONLY }) };
642
+ throw new Error(`unexpected fetch in test: ${url}`);
643
+ };
644
+
645
+ const buildAuthRouter = require('../src/bongos/routes/auth.js');
646
+ const webCallback = buildAuthRouter().stack
647
+ .find((l) => l.route?.path === '/auth/web/callback' && l.route.methods.get).route.stack[0].handle;
648
+ async function hubSignIn(extraQuery = {}) {
649
+ const req = {
650
+ query: { ...extraQuery, state: 'st8', code: 'c0de' }, secure: false,
651
+ headers: { cookie: 'gds_oauth_state=st8', host: 'localhost', 'user-agent': 'test' },
652
+ };
653
+ const res = {
654
+ redirectedTo: null, statusCode: 200, cookies: {},
655
+ cookie(name, value) { this.cookies[name] = value; return this; },
656
+ clearCookie() { return this; },
657
+ redirect(to) { this.redirectedTo = to; },
658
+ status(code) { this.statusCode = code; return this; },
659
+ type() { return this; },
660
+ send() { return this; },
661
+ };
662
+ await webCallback(req, res);
663
+ return res;
664
+ }
665
+ const HUB_BUILDER = {
666
+ id: 91, github_id: '4343', github_login: 'hub-only', display_name: 'Hub Only', avatar_url: null,
667
+ total_credits: 0, created_at: new Date(0), rank: 'xenos', preferred_disciplines: null, status: 'active',
668
+ };
669
+
670
+ await ta('CONTROL: a hub-only builder’s browser sign-in records the acceptance, publishes them, and their connections resolve', async () => {
671
+ hubBuilder = HUB_BUILDER;
672
+ pool.accounts.clear();
673
+ pool.setClock(NOW);
674
+ const res = await hubSignIn();
675
+ assert.ok(res.redirectedTo, `CONTROL: the sign-in must succeed (status ${res.statusCode}), or the stamp below proves nothing`);
676
+ const row = pool.accounts.get('4343');
677
+ assert.ok(row, 'the hub’s own sign-in wrote no platform account — the hub-only builder stays unpublished (blocker 1000124)');
678
+ assert.equal(row.terms_accepted_at, NOW, 'the hub’s own browser sign-in did not stamp the acceptance');
679
+ assert.equal(row.terms_version, TERMS_VERSION);
680
+ assert.equal(row.terms_accepted_source, 'signin', 'a hub sign-in is a sign-in, not a grandfather');
681
+ assert.equal(row.profile_state, 'active');
682
+ assert.ok(isAccountActive(row), 'connections floor on consent — a hub-only builder must now pass it');
683
+ assert.ok(isAccountVisible(row), 'and their public profile must exist');
684
+ });
685
+
686
+ await ta('the hub sign-in’s acceptance cannot be forged — only GitHub’s identity for the code reaches the statement', async () => {
687
+ hubBuilder = HUB_BUILDER;
688
+ pool.accounts.clear();
689
+ pool.calls.length = 0;
690
+ pool.setClock(NOW);
691
+ const res = await hubSignIn({
692
+ terms_accepted_at: '1999-01-01T00:00:00.000Z', terms_version: 'FORGED', activate: 'true',
693
+ terms_accepted_source: 'grandfathered', profile_state: 'active', github_id: '9999', login: 'evil-login',
694
+ });
695
+ assert.ok(res.redirectedTo, 'CONTROL: the sign-in must succeed');
696
+ assert.deepEqual([...pool.accounts.keys()], ['4343'], 'an account other than the one GitHub vouched for was written');
697
+ const upserts = pool.calls.filter((c) => /INSERT INTO platform_identity_accounts/i.test(c.sql) && /DO UPDATE/i.test(c.sql));
698
+ assert.equal(upserts.length, 1, `expected exactly one account upsert, got ${upserts.length}`);
699
+ const [p] = upserts.map((c) => c.params);
700
+ assert.equal(p.length, 6, 'the statement grew a parameter — every one must come from GitHub or a constant');
701
+ assert.equal(String(p[0]), '4343', 'the account id must be GitHub’s answer for the exchanged code');
702
+ assert.equal(p[1], 'hub-only', 'and so must the login');
703
+ assert.equal(p[4], true, 'activate must be the source literal the port passes');
704
+ assert.equal(p[5], TERMS_VERSION, 'the version must be the server constant');
705
+ assert.doesNotMatch(JSON.stringify(p), /FORGED|1999|9999|evil-login|grandfathered/, 'a caller-controlled value was bound into the acceptance statement');
706
+ });
707
+
708
+ await ta('signing in at the hub again keeps the first acceptance date', async () => {
709
+ hubBuilder = HUB_BUILDER;
710
+ pool.accounts.clear();
711
+ pool.setClock(NOW);
712
+ await hubSignIn();
713
+ pool.setClock(LATER);
714
+ await hubSignIn();
715
+ assert.equal(pool.accounts.get('4343').terms_accepted_at, NOW, 'a second hub sign-in restamped the acceptance');
716
+ });
717
+
718
+ await ta('a sign-in the admission gate refuses records no acceptance', async () => {
719
+ hubBuilder = null; // a stranger with no invite
720
+ pool.accounts.clear();
721
+ const res = await hubSignIn();
722
+ assert.equal(res.redirectedTo, null, 'CONTROL: the gate must refuse this sign-in');
723
+ assert.equal(pool.accounts.size, 0, 'a refused sign-in recorded a platform T&C acceptance');
724
+ });
725
+
726
+ await ta('a failed acceptance write never fails the hub sign-in — the next sign-in records it', async () => {
727
+ hubBuilder = HUB_BUILDER;
728
+ pool.accounts.clear();
729
+ const realQuery = pool.query;
730
+ pool.query = async (text, params) => {
731
+ if (/INSERT INTO platform_identity_accounts/i.test(String(text))) throw new Error('db down');
732
+ return realQuery(text, params);
733
+ };
734
+ try {
735
+ const res = await hubSignIn();
736
+ assert.ok(res.redirectedTo, 'a failed acceptance write turned a good sign-in into an error');
737
+ assert.equal(pool.accounts.size, 0);
738
+ } finally { pool.query = realQuery; }
739
+ await hubSignIn();
740
+ assert.ok(pool.accounts.get('4343')?.terms_accepted_at, 'the next sign-in did not record it');
741
+ });
742
+
743
+ await ta('an instance without the hub function (every child project) signs in exactly as before and writes no account', async () => {
744
+ hubBuilder = HUB_BUILDER;
745
+ pool.accounts.clear();
746
+ const hub = seams.resolveOptional('platform-identity.hub');
747
+ assert.equal(typeof hub?.recordHubSignin, 'function', 'CONTROL: the hub port must carry recordHubSignin');
748
+ const saved = hub.recordHubSignin;
749
+ delete hub.recordHubSignin;
750
+ try {
751
+ const res = await hubSignIn();
752
+ assert.ok(res.redirectedTo, 'a sign-in without the hub function must still succeed');
753
+ assert.equal(pool.accounts.size, 0, 'an instance without the hub function wrote a platform account');
754
+ } finally { hub.recordHubSignin = saved; }
755
+ });
756
+
613
757
  // ===========================================================================
614
758
  // 4. Idempotence — executed, not asserted against a string
615
759
  // ===========================================================================