@bongos/core 1.19.578 → 1.19.580
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 +68 -38
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0255-a-public-lists-ordering-is-part-of-its-payload.md +4 -1
- package/docs/adr/0259-a-projects-departure-from-the-public-list-is-public.md +128 -0
- package/docs/adr/0260-the-application-is-the-consent-and-the-echo-is-the-gate.md +146 -0
- package/docs/adr/README.md +2 -0
- package/docs/api/openapi.json +73 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +23 -21
- package/docs/copy-registry.json +51 -33
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/hall-ui/public/oversight.css +7 -0
- package/modules/hall-ui/public/watch.js +75 -0
- package/modules/onboarding/routes/access-requests.js +56 -1
- package/modules/platform-identity/applicant-profile.js +158 -0
- package/modules/platform-identity/application-echo.js +50 -0
- package/modules/platform-identity/recruiter-sliver.js +19 -0
- package/modules/platform-identity/routes/sso.js +126 -0
- package/modules/public-landing/public/projects.html +2 -2
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/auth-admission.js +61 -0
- package/src/bongos/auth.js +2 -1
- package/src/module-api.js +11 -1
- package/tests/adr_renumber_integrity.mjs +84 -0
- package/tests/applicant_profile_boundary.mjs +641 -0
- package/tests/applicant_profile_read_bounds.mjs +95 -0
- package/tests/module_api.mjs +1 -0
- package/tests/projects_hub.mjs +24 -0
- package/tests/recruiter_sliver_boundary.mjs +35 -2
- package/tests/watch_applications_queue.mjs +94 -0
package/docs/copy-registry.json
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"generator": "scripts/gds/copy-inventory.js",
|
|
4
4
|
"goal": 1000074,
|
|
5
5
|
"counts": {
|
|
6
|
-
"strings":
|
|
7
|
-
"unique_ids":
|
|
8
|
-
"certain":
|
|
6
|
+
"strings": 1709,
|
|
7
|
+
"unique_ids": 1478,
|
|
8
|
+
"certain": 1709,
|
|
9
9
|
"heuristic": 0,
|
|
10
10
|
"duplicate_ids": 162
|
|
11
11
|
},
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"builders-hall": {
|
|
14
14
|
"at": "modules/hall-ui/public",
|
|
15
15
|
"files": 78,
|
|
16
|
-
"strings":
|
|
16
|
+
"strings": 1237
|
|
17
17
|
},
|
|
18
18
|
"status-dashboard": {
|
|
19
19
|
"at": "modules/status-ui/public",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"landing": {
|
|
24
24
|
"at": "modules/public-landing/public",
|
|
25
25
|
"files": 5,
|
|
26
|
-
"strings":
|
|
26
|
+
"strings": 372
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"blind_spots": [
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
"modules/hall-ui/public/hall-render.js:605",
|
|
160
160
|
"modules/hall-ui/public/idea.js:207",
|
|
161
161
|
"modules/hall-ui/public/ideas.js:476",
|
|
162
|
-
"modules/hall-ui/public/watch.js:
|
|
162
|
+
"modules/hall-ui/public/watch.js:1212"
|
|
163
163
|
]
|
|
164
164
|
},
|
|
165
165
|
{
|
|
@@ -243,7 +243,7 @@
|
|
|
243
243
|
"modules/hall-ui/public/gate.js:219",
|
|
244
244
|
"modules/hall-ui/public/government.js:1104",
|
|
245
245
|
"modules/hall-ui/public/harbor.js:337",
|
|
246
|
-
"modules/hall-ui/public/watch.js:
|
|
246
|
+
"modules/hall-ui/public/watch.js:1241"
|
|
247
247
|
]
|
|
248
248
|
},
|
|
249
249
|
{
|
|
@@ -504,7 +504,7 @@
|
|
|
504
504
|
"occurrences": 2,
|
|
505
505
|
"at": [
|
|
506
506
|
"modules/hall-ui/public/collab.js:41",
|
|
507
|
-
"modules/hall-ui/public/watch.js:
|
|
507
|
+
"modules/hall-ui/public/watch.js:464"
|
|
508
508
|
]
|
|
509
509
|
},
|
|
510
510
|
{
|
|
@@ -1134,7 +1134,7 @@
|
|
|
1134
1134
|
"occurrences": 2,
|
|
1135
1135
|
"at": [
|
|
1136
1136
|
"modules/hall-ui/public/drachmae.js:148",
|
|
1137
|
-
"modules/hall-ui/public/watch.js:
|
|
1137
|
+
"modules/hall-ui/public/watch.js:1102"
|
|
1138
1138
|
]
|
|
1139
1139
|
},
|
|
1140
1140
|
{
|
|
@@ -1713,7 +1713,7 @@
|
|
|
1713
1713
|
"text": "{…} {…}{…}",
|
|
1714
1714
|
"occurrences": 2,
|
|
1715
1715
|
"at": [
|
|
1716
|
-
"modules/hall-ui/public/watch.js:
|
|
1716
|
+
"modules/hall-ui/public/watch.js:450",
|
|
1717
1717
|
"modules/hall-ui/public/work.js:223"
|
|
1718
1718
|
]
|
|
1719
1719
|
},
|
|
@@ -2233,6 +2233,15 @@
|
|
|
2233
2233
|
"origin": "html-text",
|
|
2234
2234
|
"confidence": "certain"
|
|
2235
2235
|
},
|
|
2236
|
+
{
|
|
2237
|
+
"id": "74cc57f76b42",
|
|
2238
|
+
"surface": "builders-hall",
|
|
2239
|
+
"text": "Active recently",
|
|
2240
|
+
"file": "modules/hall-ui/public/watch.js",
|
|
2241
|
+
"line": 411,
|
|
2242
|
+
"origin": "js-markup",
|
|
2243
|
+
"confidence": "certain"
|
|
2244
|
+
},
|
|
2236
2245
|
{
|
|
2237
2246
|
"id": "524188fe0078",
|
|
2238
2247
|
"surface": "builders-hall",
|
|
@@ -2454,7 +2463,7 @@
|
|
|
2454
2463
|
"surface": "builders-hall",
|
|
2455
2464
|
"text": "Asked {…}",
|
|
2456
2465
|
"file": "modules/hall-ui/public/watch.js",
|
|
2457
|
-
"line":
|
|
2466
|
+
"line": 464,
|
|
2458
2467
|
"origin": "js-markup",
|
|
2459
2468
|
"confidence": "certain"
|
|
2460
2469
|
},
|
|
@@ -3453,7 +3462,7 @@
|
|
|
3453
3462
|
"surface": "builders-hall",
|
|
3454
3463
|
"text": "Could not change rank: {…}",
|
|
3455
3464
|
"file": "modules/hall-ui/public/watch.js",
|
|
3456
|
-
"line":
|
|
3465
|
+
"line": 795,
|
|
3457
3466
|
"origin": "js-notice",
|
|
3458
3467
|
"confidence": "certain"
|
|
3459
3468
|
},
|
|
@@ -3462,7 +3471,7 @@
|
|
|
3462
3471
|
"surface": "builders-hall",
|
|
3463
3472
|
"text": "Could not change standing: {…}",
|
|
3464
3473
|
"file": "modules/hall-ui/public/watch.js",
|
|
3465
|
-
"line":
|
|
3474
|
+
"line": 821,
|
|
3466
3475
|
"origin": "js-notice",
|
|
3467
3476
|
"confidence": "certain"
|
|
3468
3477
|
},
|
|
@@ -3678,7 +3687,7 @@
|
|
|
3678
3687
|
"surface": "builders-hall",
|
|
3679
3688
|
"text": "Could not load this page.",
|
|
3680
3689
|
"file": "modules/hall-ui/public/watch.js",
|
|
3681
|
-
"line":
|
|
3690
|
+
"line": 1287,
|
|
3682
3691
|
"origin": "js-notice",
|
|
3683
3692
|
"confidence": "certain"
|
|
3684
3693
|
},
|
|
@@ -3750,7 +3759,7 @@
|
|
|
3750
3759
|
"surface": "builders-hall",
|
|
3751
3760
|
"text": "Could not read the report: {…}",
|
|
3752
3761
|
"file": "modules/hall-ui/public/watch.js",
|
|
3753
|
-
"line":
|
|
3762
|
+
"line": 616,
|
|
3754
3763
|
"origin": "js-markup",
|
|
3755
3764
|
"confidence": "certain"
|
|
3756
3765
|
},
|
|
@@ -4020,7 +4029,7 @@
|
|
|
4020
4029
|
"surface": "builders-hall",
|
|
4021
4030
|
"text": "Could not verify your access. Go back and try again.",
|
|
4022
4031
|
"file": "modules/hall-ui/public/watch.js",
|
|
4023
|
-
"line":
|
|
4032
|
+
"line": 1286,
|
|
4024
4033
|
"origin": "js-text-assign",
|
|
4025
4034
|
"confidence": "certain"
|
|
4026
4035
|
},
|
|
@@ -7026,7 +7035,7 @@
|
|
|
7026
7035
|
"surface": "builders-hall",
|
|
7027
7036
|
"text": "Over the last {…} days",
|
|
7028
7037
|
"file": "modules/hall-ui/public/watch.js",
|
|
7029
|
-
"line":
|
|
7038
|
+
"line": 1129,
|
|
7030
7039
|
"origin": "js-markup",
|
|
7031
7040
|
"confidence": "certain"
|
|
7032
7041
|
},
|
|
@@ -7255,6 +7264,15 @@
|
|
|
7255
7264
|
"origin": "html-title",
|
|
7256
7265
|
"confidence": "certain"
|
|
7257
7266
|
},
|
|
7267
|
+
{
|
|
7268
|
+
"id": "92d8fad6dc91",
|
|
7269
|
+
"surface": "builders-hall",
|
|
7270
|
+
"text": "Private account — this is all they share by applying.",
|
|
7271
|
+
"file": "modules/hall-ui/public/watch.js",
|
|
7272
|
+
"line": 412,
|
|
7273
|
+
"origin": "js-markup",
|
|
7274
|
+
"confidence": "certain"
|
|
7275
|
+
},
|
|
7258
7276
|
{
|
|
7259
7277
|
"id": "2b88c01d1697",
|
|
7260
7278
|
"surface": "builders-hall",
|
|
@@ -7431,7 +7449,7 @@
|
|
|
7431
7449
|
"surface": "builders-hall",
|
|
7432
7450
|
"text": "Rank set to {…}.",
|
|
7433
7451
|
"file": "modules/hall-ui/public/watch.js",
|
|
7434
|
-
"line":
|
|
7452
|
+
"line": 789,
|
|
7435
7453
|
"origin": "js-notice",
|
|
7436
7454
|
"confidence": "certain"
|
|
7437
7455
|
},
|
|
@@ -7503,7 +7521,7 @@
|
|
|
7503
7521
|
"surface": "builders-hall",
|
|
7504
7522
|
"text": "Read it as a table",
|
|
7505
7523
|
"file": "modules/hall-ui/public/watch.js",
|
|
7506
|
-
"line":
|
|
7524
|
+
"line": 1102,
|
|
7507
7525
|
"origin": "js-markup",
|
|
7508
7526
|
"confidence": "certain"
|
|
7509
7527
|
},
|
|
@@ -7818,7 +7836,7 @@
|
|
|
7818
7836
|
"surface": "builders-hall",
|
|
7819
7837
|
"text": "Report {…}.",
|
|
7820
7838
|
"file": "modules/hall-ui/public/watch.js",
|
|
7821
|
-
"line":
|
|
7839
|
+
"line": 550,
|
|
7822
7840
|
"origin": "js-notice",
|
|
7823
7841
|
"confidence": "certain"
|
|
7824
7842
|
},
|
|
@@ -7971,7 +7989,7 @@
|
|
|
7971
7989
|
"surface": "builders-hall",
|
|
7972
7990
|
"text": "Restricted.",
|
|
7973
7991
|
"file": "modules/hall-ui/public/watch.js",
|
|
7974
|
-
"line":
|
|
7992
|
+
"line": 1241,
|
|
7975
7993
|
"origin": "js-text-assign",
|
|
7976
7994
|
"confidence": "certain"
|
|
7977
7995
|
},
|
|
@@ -8745,7 +8763,7 @@
|
|
|
8745
8763
|
"surface": "builders-hall",
|
|
8746
8764
|
"text": "System oversight: people, operations and security.",
|
|
8747
8765
|
"file": "modules/hall-ui/public/watch.js",
|
|
8748
|
-
"line":
|
|
8766
|
+
"line": 1300,
|
|
8749
8767
|
"origin": "js-text-assign",
|
|
8750
8768
|
"confidence": "certain"
|
|
8751
8769
|
},
|
|
@@ -9442,6 +9460,15 @@
|
|
|
9442
9460
|
"origin": "js-markup",
|
|
9443
9461
|
"confidence": "certain"
|
|
9444
9462
|
},
|
|
9463
|
+
{
|
|
9464
|
+
"id": "b7c288328f79",
|
|
9465
|
+
"surface": "builders-hall",
|
|
9466
|
+
"text": "This builder hides their cross-project record.",
|
|
9467
|
+
"file": "modules/hall-ui/public/watch.js",
|
|
9468
|
+
"line": 393,
|
|
9469
|
+
"origin": "js-markup",
|
|
9470
|
+
"confidence": "certain"
|
|
9471
|
+
},
|
|
9445
9472
|
{
|
|
9446
9473
|
"id": "ed313d96b419",
|
|
9447
9474
|
"surface": "builders-hall",
|
|
@@ -11787,7 +11814,7 @@
|
|
|
11787
11814
|
"surface": "builders-hall",
|
|
11788
11815
|
"text": "task {…}",
|
|
11789
11816
|
"file": "modules/hall-ui/public/watch.js",
|
|
11790
|
-
"line":
|
|
11817
|
+
"line": 1212,
|
|
11791
11818
|
"origin": "js-markup",
|
|
11792
11819
|
"confidence": "certain"
|
|
11793
11820
|
},
|
|
@@ -12417,7 +12444,7 @@
|
|
|
12417
12444
|
"surface": "builders-hall",
|
|
12418
12445
|
"text": "{…} {…}{…}",
|
|
12419
12446
|
"file": "modules/hall-ui/public/watch.js",
|
|
12420
|
-
"line":
|
|
12447
|
+
"line": 450,
|
|
12421
12448
|
"origin": "js-markup",
|
|
12422
12449
|
"confidence": "certain"
|
|
12423
12450
|
},
|
|
@@ -16093,15 +16120,6 @@
|
|
|
16093
16120
|
"origin": "html-text",
|
|
16094
16121
|
"confidence": "certain"
|
|
16095
16122
|
},
|
|
16096
|
-
{
|
|
16097
|
-
"id": "78a1684630fa",
|
|
16098
|
-
"surface": "landing",
|
|
16099
|
-
"text": "— the map shows only that something is there; its name, and everything else about it, stay yours, and nobody can ask to join. Whichever you pick, a project reaches the map only once it has a name and a description. This is separate from “Who can see it” above — that one is the door on the project’s own pages. Changing it restarts the project to pick it up.",
|
|
16100
|
-
"file": "modules/public-landing/public/projects.html",
|
|
16101
|
-
"line": 828,
|
|
16102
|
-
"origin": "html-text",
|
|
16103
|
-
"confidence": "certain"
|
|
16104
|
-
},
|
|
16105
16123
|
{
|
|
16106
16124
|
"id": "b5bac008ba11",
|
|
16107
16125
|
"surface": "landing",
|
package/docs/file-map.md
CHANGED
|
@@ -365,6 +365,7 @@ tests/
|
|
|
365
365
|
├── scouting_owner_access_and_reach.mjs ← (task 1002290, goal 1000045 C5, ADR 0210): the R14 proof — WHO may read the cross-project recruiting directory, and WHO appears in it. A project owner (an instance-asserted, hub-witnessed `membership_kind='owner'`) reads `GET /scouting` with no rank at all; a builder who is neither owner nor `project.curate` holder gets the unchanged 403; an un-witnessed owner claim is refused; a broken ownership read falls closed onto the permission gate rather than admitting or 500ing. And the recruiting opt-out finally bites: a public, unhidden, active account that opted out is absent from `/scouting` and the hall's `/people` (the same endpoint), returns when it opts back in, and the opt-out grants and denies no permission — an opted-out owner still reads the directory while being absent from their own copy of it. Fake pool filtered by each statement's own WHERE clause, real route, real `requirePermission` over the real government catalog, real HTTP. Mutation-checked three ways. DB-free `unit` lane.
|
|
366
366
|
├── scouting_page_gate.mjs ← (task 1002290, goal 1000045 C5, ADR 0210 D3): the OTHER half of R14 — the `/builders/people` SHELL must admit exactly the two audiences `GET /scouting` admits. A project owner holding no permission reaches it; everyone the route refuses gets the byte-identical `?denied=project.curate` redirect; and each of six ways the `alsoAdmits` door can fail (module absent, function absent, throw, reject, non-boolean, prototype-only) fails CLOSED. Also pins that the other four page gates never consult the seam at all, and that exactly ONE gate declares a second door. Its own file because the route proof's ESM imports pull `auth.js` in before a session double could be installed — this one installs it first, the `government_page_gates.mjs` pattern. DB-free `unit` lane.
|
|
367
367
|
├── terms_acceptance.mjs ← (task 1002913, goal 1000045, ADR 0213): the platform T&C acceptance record. Proves the four properties that make it evidence rather than a claim — the /terms page carries the cross-project consent ADR 0141 §4 deferred to it and it is apex-only; acceptance is written ONLY by the hub's own /sso/authorize (driven over the real router with an attacker-controlled query string, so a forged timestamp/version/activate flag reaches nothing); it is written ONCE (the fake pool interprets the upsert's own column assignments, so COALESCE is executed, not string-matched, and an assignment shape it cannot read throws instead of reading as "unchanged"); and consent is two columns in the shared predicate, so an account the hub witnessed before the terms existed publishes nothing. Plus the projection rule — every read whose row a JS twin judges must select both consent columns, or the twin denies everyone silently. DB-free `unit` lane, asserts its own lane membership.
|
|
368
|
+
├── applicant_profile_boundary.mjs ← (task 1002972, goal 1000045 C4, privacy spec D7, ADR 0241): THE D7 BOUNDARY proof for the applicant view a reviewer sees. Whitelists read from the MODULES’ own exports — the private branch against `recruiter-sliver`’s `SLIVER_KEYS`/`SLIVER_KEYS_HIDDEN`, the public branch against the public-page payload’s key list — so a third shape (the “new disclosure class” D7 forbids) reds this file. Pins THE CONSENT SWAP in both directions on the same account: a private builder who never opted into recruiting gets an applicant view and gets `null` from `getRecruiterSliver`, so a refactor routing the port through the recruiter sliver cannot pass quietly. `hide_stats` beats the application in both branches; provisional and terms-never-accepted disclose nothing; planted sentinels asserted absent from the whole body with the avatar-embedded `github_id` pinned as the one honest exception. Plus the federated gate (ADR 0205): no echo → 404 and NO account row read (asserted on the statement log), the witness matched against the AUTHENTICATED client, an out-of-window echo stops working, and `Cache-Control: no-store`. Real router + real HTTP over in-memory tables filtered by each statement’s own WHERE clause. DB-free `unit` lane, asserted in the file itself.
|
|
368
369
|
├── recruiter_sliver_boundary.mjs ← (task 1002968, goal 1000045 C2/C5, privacy spec D3, ADR 0215): THE PRE-ACCEPT BOUNDARY proof for the D3 recruiter sliver. A WHITELIST over the module's own exported key list (a sixth key fails whether or not this file was told about it), planted improbable sentinels asserted absent from the WHOLE response body with two controls against vacuity, the ceiling assertion (the handle a recruiter is handed 404s at `GET /profiles/:handle`, with a 200 control) and the gate (both recruiting routes admit the same two audiences and refuse byte-identically). Real router + real HTTP over an in-memory table filtered by each statement's own WHERE clause. DB-free `unit` lane, asserted in the file itself.
|
|
369
370
|
└── public_task_resolver.mjs ← (#604): the public `/public/tasks/:id` projection — title/value_summary exposed only for shipped|confirmed; withheld for in-flight so unshipped/security task names can't leak.
|
|
370
371
|
```
|
|
@@ -1605,5 +1605,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
1605
1605
|
landed since 1.19.576 with no explicit bump. run 34142767912. (task 1002620)
|
|
1606
1606
|
1.19.578 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1607
1607
|
landed since 1.19.577 with no explicit bump. run 34146835487. (task 1002620)
|
|
1608
|
+
1.19.579 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1609
|
+
landed since 1.19.578 with no explicit bump. run 34149317186. (task 1002620)
|
|
1610
|
+
1.19.580 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1611
|
+
landed since 1.19.579 with no explicit bump. run 34153217035. (task 1002620)
|
|
1608
1612
|
---------------------------------------------------------------------------
|
|
1609
1613
|
```
|
|
@@ -792,6 +792,8 @@ function createClient(opts = {}) {
|
|
|
792
792
|
"sso": {
|
|
793
793
|
// POST /sso/activity/rollup — rank: public — POST /sso/activity/rollup
|
|
794
794
|
postSsoActivityRollup: (args) => request("POST", "/sso/activity/rollup", { hasBody: true }, args),
|
|
795
|
+
// POST /sso/applicant-profile — rank: public — POST /sso/applicant-profile
|
|
796
|
+
postSsoApplicantProfile: (args) => request("POST", "/sso/applicant-profile", { hasBody: true }, args),
|
|
795
797
|
// GET /sso/authorize — rank: public — GET /sso/authorize
|
|
796
798
|
getSsoAuthorize: (args) => request("GET", "/sso/authorize", { hasBody: false }, args),
|
|
797
799
|
// POST /sso/device/poll — rank: public — POST /sso/device/poll
|
|
@@ -119,6 +119,13 @@ a.ov-row__title:hover { color: var(--accent-ink); text-decoration: underline; te
|
|
|
119
119
|
.ov-row__sub { display: block; margin: 4px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--ink-soft); max-width: var(--measure); overflow-wrap: anywhere; }
|
|
120
120
|
.ov-row__facts { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin: 10px 0 0; }
|
|
121
121
|
.ov-row__sub + .ov-row__facts { margin-top: 8px; }
|
|
122
|
+
/* The applicant's own profile inside an access-request row (task 1002972, D7).
|
|
123
|
+
A rule on the inline edge rather than a card: it is the APPLICANT speaking
|
|
124
|
+
through their own privacy settings, and it must read as evidence attached to
|
|
125
|
+
their ask — never as another decision surface competing with the buttons. */
|
|
126
|
+
.ov-row__profile { margin: 10px 0 0; padding-left: 12px; border-left: 2px solid var(--rule); }
|
|
127
|
+
.ov-row__profile .ov-row__sub { margin-top: 0; }
|
|
128
|
+
.ov-row__profile .ov-row__facts { margin-top: 6px; }
|
|
122
129
|
.ov-row__right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; text-align: right; }
|
|
123
130
|
.ov-row__actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; }
|
|
124
131
|
.ov-row__when { font-size: 12.5px; color: var(--ink-faint); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
@@ -350,6 +350,77 @@
|
|
|
350
350
|
: '<span class="fact-pill fact-pill--warn" title="Nobody vouched for this name — anyone can file a request naming anyone. Confirm who is asking before you admit them.">Name unvouched</span>';
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
// ---- the applicant's own profile, beside their ask (task 1002972, D7) -----
|
|
354
|
+
//
|
|
355
|
+
// The reviewer sees what the APPLICANT'S OWN privacy rules would show them —
|
|
356
|
+
// resolved live by the server on every render, never stored on the request. So
|
|
357
|
+
// this renderer decides nothing about disclosure: it draws whichever of the
|
|
358
|
+
// two shapes the server sent, and draws nothing at all when it sent null. A
|
|
359
|
+
// null covers "not vouched", "no hub", "hub unreachable", "account hidden" and
|
|
360
|
+
// "no such account" alike, and it must stay that way on screen too — the row
|
|
361
|
+
// simply carries no profile block, and the vouch pill above already says
|
|
362
|
+
// whether anyone confirmed the name.
|
|
363
|
+
//
|
|
364
|
+
// NOTHING HERE IS AUTHORITY (ADR 0016). A rich record is not a reason to admit
|
|
365
|
+
// and an empty one is not a reason to refuse; the two buttons are unchanged
|
|
366
|
+
// and the server's admission gate never reads any of this.
|
|
367
|
+
|
|
368
|
+
// The bio, under the applicant's name. ESCAPED AND NEVER LINKIFIED — the
|
|
369
|
+
// render contract the hub's own profile payload carries, because a bio is user
|
|
370
|
+
// content on a page an Archon reads. escapeHtml is the whole defence; do not
|
|
371
|
+
// "improve" this into anchor-building.
|
|
372
|
+
function profileBio(p) {
|
|
373
|
+
var bio = p.account && p.account.bio;
|
|
374
|
+
return bio ? '<p class="ov-row__sub">' + escapeHtml(bio) + '</p>' : '';
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Disciplines as pills — the one field both shapes carry, so it is read off
|
|
378
|
+
// whichever half is present rather than branched on twice.
|
|
379
|
+
function disciplinePills(list) {
|
|
380
|
+
if (!Array.isArray(list) || !list.length) return '';
|
|
381
|
+
return list.slice(0, 6).map(function (d) {
|
|
382
|
+
return '<span class="fact-pill fact-pill--quiet">' + escapeHtml(d) + '</span>';
|
|
383
|
+
}).join('');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// The PUBLIC shape: the cross-project rollup as one plain sentence. A null
|
|
387
|
+
// rollup is not an error and not an absence of the person — it is the builder
|
|
388
|
+
// having hidden their numbers, and saying so is what stops a reviewer reading
|
|
389
|
+
// the blank as "this applicant has done nothing".
|
|
390
|
+
function publicProfileFacts(p) {
|
|
391
|
+
var x = p.cross_project;
|
|
392
|
+
if (!x) {
|
|
393
|
+
return '<span class="ov-row__when">This builder hides their cross-project record.</span>';
|
|
394
|
+
}
|
|
395
|
+
var t = x.totals || {};
|
|
396
|
+
var parts = [];
|
|
397
|
+
if (num(t.projects)) parts.push(fmtInt(t.projects) + (t.projects === 1 ? ' project' : ' projects'));
|
|
398
|
+
if (num(t.works_shipped)) parts.push(fmtInt(t.works_shipped) + ' shipped');
|
|
399
|
+
if (num(t.credits)) parts.push(fmtCompact(t.credits) + ' credits');
|
|
400
|
+
return disciplinePills(x.disciplines)
|
|
401
|
+
+ (parts.length ? '<span class="ov-row__when">' + escapeHtml(parts.join(' · ')) + '</span>' : '')
|
|
402
|
+
+ (x.last_active ? '<span class="ov-row__when">Last active ' + escapeHtml(fmtDate(x.last_active)) + '</span>' : '');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// The SLIVER shape: a private account that chose to apply anyway. The sentence
|
|
406
|
+
// is there so a reviewer does not read the thinness as a poor record — it is a
|
|
407
|
+
// deliberate boundary, and applying is what opened even this much.
|
|
408
|
+
function sliverProfileFacts(p) {
|
|
409
|
+
var s = p.sliver || {};
|
|
410
|
+
return disciplinePills(s.disciplines)
|
|
411
|
+
+ (s.active_recently ? '<span class="fact-pill fact-pill--ok">Active recently</span>' : '')
|
|
412
|
+
+ '<span class="ov-row__when">Private account — this is all they share by applying.</span>';
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function profileHtml(p) {
|
|
416
|
+
if (!p || (p.view !== 'public' && p.view !== 'sliver')) return '';
|
|
417
|
+
var facts = p.view === 'public' ? publicProfileFacts(p) : sliverProfileFacts(p);
|
|
418
|
+
return '<div class="ov-row__profile">'
|
|
419
|
+
+ (p.view === 'public' ? profileBio(p) : '')
|
|
420
|
+
+ (facts ? '<div class="ov-row__facts">' + facts + '</div>' : '')
|
|
421
|
+
+ '</div>';
|
|
422
|
+
}
|
|
423
|
+
|
|
353
424
|
// What this row still offers. A waiting request has two decisions; an admitted
|
|
354
425
|
// one can still be rescinded (until they sign in, after which the server
|
|
355
426
|
// refuses and says so); a declined one is terminal here — re-admitting is the
|
|
@@ -384,6 +455,10 @@
|
|
|
384
455
|
'<div class="ov-row__main">' +
|
|
385
456
|
`<span class="ov-row__title">${who}</span>` +
|
|
386
457
|
note +
|
|
458
|
+
// The applicant's own profile sits between what THEY wrote and what the
|
|
459
|
+
// SYSTEM knows — their words first, their record second, the decision
|
|
460
|
+
// facts last. Empty string when the server resolved nothing.
|
|
461
|
+
profileHtml(r.profile) +
|
|
387
462
|
'<div class="ov-row__facts">' +
|
|
388
463
|
vouchPill(!!r.vouched) +
|
|
389
464
|
`<span class="ov-row__when">Asked ${escapeHtml(fmtDate(r.created_at))}</span>` +
|
|
@@ -80,6 +80,57 @@ function normalizeLogin(raw) {
|
|
|
80
80
|
return String(raw || '').trim().replace(/^@/, '');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// How many applicant profiles one queue render may resolve from the hub (privacy
|
|
84
|
+
// spec D7, task 1002972). The list itself returns up to 200 rows, and a render
|
|
85
|
+
// that fired 200 cross-instance round-trips would make a reviewer wait on the
|
|
86
|
+
// hub to read a queue that is perfectly readable without it.
|
|
87
|
+
//
|
|
88
|
+
// The cap bites on VOUCHED rows only — an unvouched row has no account to
|
|
89
|
+
// resolve and costs nothing — and it is applied to the list AS ORDERED, newest
|
|
90
|
+
// first, so the rows a reviewer actually acts on are the ones that carry a
|
|
91
|
+
// profile. Beyond it, `profile` is null and the row renders exactly as an
|
|
92
|
+
// unvouched one does: the queue never degrades into an error, it degrades into
|
|
93
|
+
// less detail. Raising this number buys detail and costs latency; it changes
|
|
94
|
+
// nothing about WHAT may be disclosed.
|
|
95
|
+
const MAX_LIVE_PROFILE_READS = 25;
|
|
96
|
+
|
|
97
|
+
// attachApplicantProfiles(rows) — the LIVE half of D7, and "live" is the whole
|
|
98
|
+
// design rather than a performance note. Nothing about the applicant is stored
|
|
99
|
+
// on the access_requests row; the profile is resolved from the hub ON EVERY
|
|
100
|
+
// RENDER, so an applicant who goes private, hides their stats, or loses their
|
|
101
|
+
// account between applying and being reviewed is answered by the NEXT read of
|
|
102
|
+
// this queue (criterion C2's "immediately"). A snapshot taken at apply time
|
|
103
|
+
// would keep publishing a consent that had since been withdrawn.
|
|
104
|
+
//
|
|
105
|
+
// KEYED ON THE VOUCH, NEVER ON github_login. `applicant_github_id` is the hub's
|
|
106
|
+
// own assertion about who filed this request (core_230); the login is a string
|
|
107
|
+
// anybody may type into the one PUBLIC unauthenticated write on this API.
|
|
108
|
+
// Resolving a cross-project record against an unvouched login is precisely the
|
|
109
|
+
// impersonation surface the vouch exists to close — file a request naming a
|
|
110
|
+
// well-known builder, let the owner read that builder's excellent record, and
|
|
111
|
+
// walk in. So an unvouched row gets `profile: null`, always, and the queue's own
|
|
112
|
+
// "Name unvouched" pill already tells the reviewer why.
|
|
113
|
+
//
|
|
114
|
+
// Each read fails soft to null independently (the doorway port swallows), so one
|
|
115
|
+
// slow or missing account cannot fail the others or the listing.
|
|
116
|
+
async function attachApplicantProfiles(rows) {
|
|
117
|
+
// The budget is spent UP FRONT, by index, rather than by decrementing a shared
|
|
118
|
+
// counter inside the concurrent callbacks. Both are correct today — `.map`
|
|
119
|
+
// invokes each async callback synchronously up to its first `await`, so the
|
|
120
|
+
// decrements happen in list order before any hub call starts — but resting a
|
|
121
|
+
// disclosure-shaped cap on that guarantee is the kind of correctness that
|
|
122
|
+
// breaks silently the day someone reaches for a concurrency helper here. This
|
|
123
|
+
// form is correct for a reason a reader can see without knowing that rule.
|
|
124
|
+
const resolvable = new Set();
|
|
125
|
+
rows.forEach((r, i) => {
|
|
126
|
+
if (r.applicant_github_id && resolvable.size < MAX_LIVE_PROFILE_READS) resolvable.add(i);
|
|
127
|
+
});
|
|
128
|
+
return Promise.all(rows.map(async (r, i) => ({
|
|
129
|
+
...r,
|
|
130
|
+
profile: resolvable.has(i) ? await api.readApplicantProfileFromHub(r.applicant_github_id) : null,
|
|
131
|
+
})));
|
|
132
|
+
}
|
|
133
|
+
|
|
83
134
|
module.exports = function buildAccessRequestsRouter() {
|
|
84
135
|
const router = express.Router();
|
|
85
136
|
|
|
@@ -235,7 +286,11 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
235
286
|
LIMIT 200`,
|
|
236
287
|
[status]
|
|
237
288
|
);
|
|
238
|
-
|
|
289
|
+
// C4's other half (privacy spec D7, task 1002972): the applicant's own
|
|
290
|
+
// profile, as their OWN privacy rules would show it, resolved from the hub
|
|
291
|
+
// at render. See attachApplicantProfiles — it is keyed on the vouch, never
|
|
292
|
+
// on the login, and every row it cannot resolve carries `profile: null`.
|
|
293
|
+
res.json({ requests: await attachApplicantProfiles(rows) });
|
|
239
294
|
} catch (err) {
|
|
240
295
|
log.error('[gds] GET /access-requests', err);
|
|
241
296
|
res.fail('list_failed', { status: 500, message: 'internal error' });
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// modules/platform-identity/applicant-profile.js — THE D7 RULE: what a reviewer
|
|
2
|
+
// sees of the person who applied to their project (privacy spec D7, task
|
|
3
|
+
// 1002972, goal 1000045).
|
|
4
|
+
//
|
|
5
|
+
// ONE RULE, NO NEW DISCLOSURE CLASS. That is D7's whole claim, and this file is
|
|
6
|
+
// where it is true or not:
|
|
7
|
+
//
|
|
8
|
+
// PUBLIC account → the SAME payload GET /profiles/:handle already serves —
|
|
9
|
+
// the identity band plus the cross-project rollup, which is
|
|
10
|
+
// null when `hide_stats` is set. Not a copy of that shape:
|
|
11
|
+
// the rollup half is getPublicProfileExtras, the very port
|
|
12
|
+
// the public page calls.
|
|
13
|
+
// PRIVATE account → the D3 sliver's projection, built by recruiter-sliver's
|
|
14
|
+
// own sliverShapeFor — again the same code, not a similar
|
|
15
|
+
// one. Name-only under `hide_stats`, by that file's rule.
|
|
16
|
+
// PROVISIONAL, or an account that never accepted the terms → NOTHING (null).
|
|
17
|
+
//
|
|
18
|
+
// So a reviewer never sees a field that some existing surface would not already
|
|
19
|
+
// have shown them. A THIRD shape here would BE the new disclosure class D7
|
|
20
|
+
// forbids, which is why neither branch builds its own object.
|
|
21
|
+
//
|
|
22
|
+
// EVALUATED LIVE, AND THAT IS A REQUIREMENT RATHER THAN AN IMPLEMENTATION NOTE
|
|
23
|
+
// (spec C2, D7). Nothing here is stored, cached or snapshotted at apply time. An
|
|
24
|
+
// applicant who flips `hide_stats`, goes private, or has their account
|
|
25
|
+
// deactivated between applying and being reviewed is answered by the NEXT render
|
|
26
|
+
// — because the next render is the only place the answer exists. A snapshot
|
|
27
|
+
// taken at apply time would keep publishing a withdrawn consent for as long as
|
|
28
|
+
// the request sat in the queue, which is exactly the failure C2's "immediately"
|
|
29
|
+
// is written against.
|
|
30
|
+
//
|
|
31
|
+
// THE CONSENT IS THE APPLICATION, AND THAT IS THE ONE PLACE THIS DIFFERS FROM
|
|
32
|
+
// THE RECRUITER SLIVER — stated out loud because it is the decision a reader
|
|
33
|
+
// will want to check. getRecruiterSliver floors on recruiterSliverVisibleSql =
|
|
34
|
+
// consent AND *recruiting reach*, because a recruiter browsing a directory has
|
|
35
|
+
// been invited by nobody. An applicant has done something a browsing recruiter
|
|
36
|
+
// has not: spec D7 says it in those words — "applying is an explicit act". So
|
|
37
|
+
// reusing getRecruiterSliver verbatim here would be WRONG in the direction that
|
|
38
|
+
// looks safe: `recruiter_discoverable` defaults to FALSE for a private account,
|
|
39
|
+
// so a private builder who deliberately applied would show their reviewer
|
|
40
|
+
// nothing at all, and criterion C4 would be satisfied by an empty box. The reach
|
|
41
|
+
// opt-out governs the RECRUITING SURFACE (account-visibility.js says exactly
|
|
42
|
+
// that of scoutingListableSql); it was never a gate on a door the account itself
|
|
43
|
+
// knocked on.
|
|
44
|
+
//
|
|
45
|
+
// WHAT STILL FLOORS IT, so "the application is the consent" cannot be read as
|
|
46
|
+
// "applying opts you into everything":
|
|
47
|
+
// * accountActiveSql — profile_state='active' AND terms_accepted_at IS NOT
|
|
48
|
+
// NULL. A provisional account (no human ever witnessed at the hub) and an
|
|
49
|
+
// account that never accepted the platform terms disclose NOTHING here, the
|
|
50
|
+
// same as on every other surface. This is the shared predicate, called, not
|
|
51
|
+
// re-spelled (tests/effective_visibility_predicate.mjs enforces that).
|
|
52
|
+
// * hide_stats — untouched, in both branches, by the two ports themselves.
|
|
53
|
+
// * the existence axis is deliberately NOT a floor, for the same reason the D3
|
|
54
|
+
// sliver drops it: the private account is precisely this rule's subject, and
|
|
55
|
+
// flooring on accountVisibleSql would empty the private branch of everything
|
|
56
|
+
// it exists to carry.
|
|
57
|
+
//
|
|
58
|
+
// NOT AUTHORITY (ADR 0016). A reviewer holding this view holds a description,
|
|
59
|
+
// never a permission: nothing here says the applicant may enter, and admission
|
|
60
|
+
// still reads status='invited' and nothing else (hasInvitedAccessRequest). It
|
|
61
|
+
// does not even say they applied — the CALLER must prove that first; see
|
|
62
|
+
// applicationBacksProfileRead in ./application-echo.js, which is the gate the
|
|
63
|
+
// one route over this port is built on.
|
|
64
|
+
//
|
|
65
|
+
// WHO MAY CALL IT is not this file's question (routes/sso.js gates the one
|
|
66
|
+
// surface). WHAT crosses is decided here, and by the two ports it composes.
|
|
67
|
+
//
|
|
68
|
+
// tests/applicant_profile_boundary.mjs is the proof.
|
|
69
|
+
'use strict';
|
|
70
|
+
|
|
71
|
+
const pi = require('./platform-identity');
|
|
72
|
+
const recruiterSliver = require('./recruiter-sliver');
|
|
73
|
+
const { accountActiveSql, isAccountVisible } = require('./account-visibility');
|
|
74
|
+
|
|
75
|
+
// The two shapes this port may answer with, as a closed vocabulary. Exported so
|
|
76
|
+
// the proof asserts against these constants rather than a copy of the strings —
|
|
77
|
+
// a third view added here and quietly added to the test's own expectation is the
|
|
78
|
+
// failure a whitelist is supposed to make impossible.
|
|
79
|
+
const VIEW_PUBLIC = 'public';
|
|
80
|
+
const VIEW_SLIVER = 'sliver';
|
|
81
|
+
const APPLICANT_VIEWS = [VIEW_PUBLIC, VIEW_SLIVER];
|
|
82
|
+
|
|
83
|
+
// readApplicantAccount — the ONE account read behind the port: the identity band
|
|
84
|
+
// of an account that clears the CONSENT floor, plus the columns the branch below
|
|
85
|
+
// needs to decide the shape. Null for anyone who does not clear it.
|
|
86
|
+
//
|
|
87
|
+
// It selects `account_visibility` and the consent columns deliberately: the
|
|
88
|
+
// EXISTENCE decision is made in JS by isAccountVisible, on this row, rather than
|
|
89
|
+
// by a second query with a second predicate — one read, one row, one branch.
|
|
90
|
+
async function readApplicantAccount(githubId, { pool }) {
|
|
91
|
+
const { rows } = await pool.query(
|
|
92
|
+
`SELECT github_id, github_login, display_name, avatar_url, handle, bio, links,
|
|
93
|
+
created_at, profile_state, terms_accepted_at, account_visibility,
|
|
94
|
+
recruiter_discoverable, hide_stats
|
|
95
|
+
FROM platform_identity_accounts
|
|
96
|
+
WHERE ${accountActiveSql()} AND github_id = $1::bigint
|
|
97
|
+
LIMIT 1`,
|
|
98
|
+
[String(githubId)],
|
|
99
|
+
);
|
|
100
|
+
return rows[0] || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// publicApplicantView(account, { pool }) — the PUBLIC branch, assembled from the
|
|
104
|
+
// same two pieces GET /profiles/:handle serves: the identity band off the row,
|
|
105
|
+
// and the cross-project rollup off getPublicProfileExtras — which returns null
|
|
106
|
+
// for a `hide_stats` account, so hiding is honoured by the port rather than by a
|
|
107
|
+
// rule restated here.
|
|
108
|
+
//
|
|
109
|
+
// github_id and profile_state never cross, exactly as on the public page: the
|
|
110
|
+
// join key stays internal and the answer itself is the addressability signal.
|
|
111
|
+
async function publicApplicantView(account, { pool }) {
|
|
112
|
+
return {
|
|
113
|
+
view: VIEW_PUBLIC,
|
|
114
|
+
account: {
|
|
115
|
+
handle: account.handle ?? null,
|
|
116
|
+
github_login: account.github_login,
|
|
117
|
+
display_name: account.display_name ?? null,
|
|
118
|
+
avatar_url: account.avatar_url ?? null,
|
|
119
|
+
member_since: account.created_at,
|
|
120
|
+
// RENDER CONTRACT, inherited verbatim from getPublicProfileByHandle:
|
|
121
|
+
// escape the bio, never linkify it, and emit links with
|
|
122
|
+
// rel="nofollow ugc noopener". Validated at write; still user content.
|
|
123
|
+
bio: account.bio ?? null,
|
|
124
|
+
links: account.links ?? null,
|
|
125
|
+
},
|
|
126
|
+
cross_project: await pi.getPublicProfileExtras(account.github_id, { pool }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// getApplicantProfileView(githubId, { pool }) — THE port D7 names.
|
|
131
|
+
//
|
|
132
|
+
// Answers null | { view:'public', account, cross_project } | { view:'sliver',
|
|
133
|
+
// sliver }. A non-numeric principal (core's 'system:bfg' shape) answers null
|
|
134
|
+
// rather than throwing a bigint cast mid-request, like every other read in this
|
|
135
|
+
// module.
|
|
136
|
+
//
|
|
137
|
+
// FAIL-CLOSED IN ONE DIRECTION ONLY: everything that is not disclosable is the
|
|
138
|
+
// SAME null — absent account, provisional account, terms never accepted, and an
|
|
139
|
+
// unusable principal are indistinguishable to a caller, which is the ADR 0171 D4
|
|
140
|
+
// rule. The caller renders "no record", never a reason.
|
|
141
|
+
async function getApplicantProfileView(githubId, { pool } = {}) {
|
|
142
|
+
if (!pi.isNumericGithubId(githubId)) return null;
|
|
143
|
+
const account = await readApplicantAccount(githubId, { pool });
|
|
144
|
+
if (!account) return null;
|
|
145
|
+
if (isAccountVisible(account)) return publicApplicantView(account, { pool });
|
|
146
|
+
// PRIVATE: the D3 projection, built by the sliver's own code. sliverShapeFor
|
|
147
|
+
// takes the row this function already holds and applies the `hide_stats`
|
|
148
|
+
// shape rule — the CONSENT predicate is not re-applied there, which is why
|
|
149
|
+
// the floor above is this function's own responsibility and is not optional.
|
|
150
|
+
return { view: VIEW_SLIVER, sliver: await recruiterSliver.sliverShapeFor(account, { pool }) };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = {
|
|
154
|
+
APPLICANT_VIEWS,
|
|
155
|
+
VIEW_PUBLIC,
|
|
156
|
+
VIEW_SLIVER,
|
|
157
|
+
getApplicantProfileView,
|
|
158
|
+
};
|