@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 +18 -18
- package/docs/copy-inventory.md +3 -3
- package/docs/copy-registry.json +6 -6
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +5 -5
- package/modules/platform-identity/routes/sso.js +10 -2
- package/modules/platform-identity/terms-acceptance.js +11 -10
- package/modules/public-landing/public/terms.html +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/src/bongos/auth-github.js +22 -0
- package/src/module-api.js +1 -1
- package/tests/terms_acceptance.mjs +174 -30
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.
|
|
6
|
-
"core_contract": "1.23.
|
|
7
|
-
"source_commit": "
|
|
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-
|
|
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": "
|
|
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": "
|
|
2425
|
+
"sha256": "db72d41e471428058a50aa84790a1a82260e4a05695f8b3ace417962a68edbb2"
|
|
2426
2426
|
},
|
|
2427
2427
|
{
|
|
2428
2428
|
"path": "docs/copy-registry.json",
|
|
2429
2429
|
"mode": "0000644",
|
|
2430
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
10840
|
+
"sha256": "c2355516436464dd695de67ba3e9fc57f4cc82782f63716f694b4296f83a7200"
|
|
10841
10841
|
},
|
|
10842
10842
|
{
|
|
10843
10843
|
"path": "package.json",
|
|
10844
10844
|
"mode": "0000644",
|
|
10845
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
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": "
|
|
20635
|
+
"sha256": "56b6c06ada67f3cff5ae2bc05182cdf11ffa9028b8725d0eeb35267bd9876e76"
|
|
20636
20636
|
},
|
|
20637
20637
|
{
|
|
20638
20638
|
"path": "tests/terms_grandfather_db.mjs",
|
package/docs/copy-inventory.md
CHANGED
|
@@ -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
|
-
| `
|
|
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
|
-
| `
|
|
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
|
-
| `
|
|
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` |
|
package/docs/copy-registry.json
CHANGED
|
@@ -21657,9 +21657,9 @@
|
|
|
21657
21657
|
"confidence": "certain"
|
|
21658
21658
|
},
|
|
21659
21659
|
{
|
|
21660
|
-
"id": "
|
|
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
|
|
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": "
|
|
22434
|
+
"id": "b43bc7940904",
|
|
22435
22435
|
"surface": "landing",
|
|
22436
|
-
"text": "Once you have signed in
|
|
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": "
|
|
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
|
|
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
|
```
|
package/docs/page-readings.json
CHANGED
|
@@ -5579,8 +5579,8 @@
|
|
|
5579
5579
|
"surface": "landing",
|
|
5580
5580
|
"title": "{{productName}} — Terms",
|
|
5581
5581
|
"states_read": ["default"],
|
|
5582
|
-
"files_hash": "
|
|
5583
|
-
"reading_hash": "
|
|
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
|
|
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
|
|
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
|
|
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` —
|
|
311
|
-
//
|
|
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,
|
|
27
|
-
//
|
|
28
|
-
// account to sign in to a PROJECT
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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
|
|
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’s own standing for you, as that project reported it — the platform holds no rank of its own.</p>
|
|
116
|
-
<p><b>What is public.</b> Once you have signed in
|
|
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’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’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
|
|
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
|
|
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 — 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 <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.
|
|
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.
|
|
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.
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -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.
|
|
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).
|
|
8
|
-
// platform account to sign in to a PROJECT
|
|
9
|
-
//
|
|
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
|
|
26
|
-
// statement, reached from exactly
|
|
27
|
-
// `activate: true`
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
// (task 1004265
|
|
125
|
-
// command-line builder why.
|
|
126
|
-
|
|
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.
|
|
130
|
-
'
|
|
131
|
-
assert.match(section, /
|
|
132
|
-
'
|
|
133
|
-
assert.match(section, /
|
|
134
|
-
'
|
|
135
|
-
assert.
|
|
136
|
-
'the
|
|
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
|
|
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
|
-
|
|
398
|
-
|
|
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
|
|
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
|
// ===========================================================================
|