@bongos/core 1.23.4 → 1.23.6

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.
Files changed (41) hide show
  1. package/.bongos-core.json +67 -42
  2. package/clients/bongos-client/README.md +1 -1
  3. package/clients/bongos-client/bongos-client.global.js +4 -0
  4. package/clients/bongos-client/index.cjs +4 -0
  5. package/clients/bongos-client/index.d.ts +9 -1
  6. package/clients/bongos-client/index.mjs +4 -0
  7. package/docs/adr/0356-a-recruit-invite-expires-30-days-after-it-is-sent-read-time.md +2 -1
  8. package/docs/api/openapi.json +204 -7
  9. package/docs/api-reference.md +4 -2
  10. package/docs/copy-inventory.md +26 -24
  11. package/docs/copy-registry.json +43 -25
  12. package/docs/module-api-changelog.md +4 -0
  13. package/docs/modules-contract.md +2 -0
  14. package/docs/page-readings.json +8 -8
  15. package/migrations/core_281_module_version_withdraw_note.sql +25 -0
  16. package/modules/government/catalog.js +1 -1
  17. package/modules/hall-ui/public/modules.js +10 -1
  18. package/modules/onboarding/routes/access-requests.js +10 -9
  19. package/package-lock.json +2 -2
  20. package/package.json +1 -1
  21. package/release-notes.json +16 -0
  22. package/scripts/bongos/login.js +13 -14
  23. package/scripts/bongos/module-store-cli.js +182 -0
  24. package/scripts/bongos/module.js +60 -134
  25. package/scripts/gds/module-store-cli.js +6 -0
  26. package/src/bongos/auth-admission.js +3 -3
  27. package/src/bongos/middleware/rate-limit.js +1 -1
  28. package/src/bongos/module-listing.js +12 -2
  29. package/src/bongos/module-store.js +90 -1
  30. package/src/bongos/route-rank-check.js +6 -0
  31. package/src/bongos/routes/auth.js +7 -11
  32. package/src/bongos/routes/modules.js +76 -3
  33. package/src/module-api.js +1 -1
  34. package/tests/account_existence_rate_limit.mjs +7 -3
  35. package/tests/application_lifecycle.mjs +4 -3
  36. package/tests/bongos_login.mjs +15 -4
  37. package/tests/hall_catalog_world.mjs +1 -1
  38. package/tests/invite_expiry_db.mjs +1 -1
  39. package/tests/module_store_cli.mjs +1 -1
  40. package/tests/module_store_price.mjs +207 -0
  41. package/tests/module_store_withdraw.mjs +201 -0
@@ -3,9 +3,9 @@
3
3
  "generator": "scripts/bongos/copy-inventory.js",
4
4
  "goal": 1000074,
5
5
  "counts": {
6
- "strings": 2735,
7
- "unique_ids": 2323,
8
- "certain": 2735,
6
+ "strings": 2737,
7
+ "unique_ids": 2325,
8
+ "certain": 2737,
9
9
  "heuristic": 0,
10
10
  "duplicate_ids": 239
11
11
  },
@@ -13,7 +13,7 @@
13
13
  "builders-hall": {
14
14
  "at": "modules/hall-ui/public",
15
15
  "files": 153,
16
- "strings": 1916
16
+ "strings": 1918
17
17
  },
18
18
  "status-dashboard": {
19
19
  "at": "modules/status-ui/public",
@@ -5128,7 +5128,7 @@
5128
5128
  "surface": "builders-hall",
5129
5129
  "text": "Could not load modules.",
5130
5130
  "file": "modules/hall-ui/public/modules.js",
5131
- "line": 593,
5131
+ "line": 602,
5132
5132
  "origin": "js-notice",
5133
5133
  "confidence": "certain"
5134
5134
  },
@@ -5317,7 +5317,7 @@
5317
5317
  "surface": "builders-hall",
5318
5318
  "text": "Could not reach the submit endpoint.",
5319
5319
  "file": "modules/hall-ui/public/modules.js",
5320
- "line": 570,
5320
+ "line": 579,
5321
5321
  "origin": "js-notice",
5322
5322
  "confidence": "certain"
5323
5323
  },
@@ -9187,7 +9187,7 @@
9187
9187
  "surface": "builders-hall",
9188
9188
  "text": "No real-world data yet, so it is listed but not ranked.",
9189
9189
  "file": "modules/hall-ui/public/modules.js",
9190
- "line": 334,
9190
+ "line": 343,
9191
9191
  "origin": "js-markup",
9192
9192
  "confidence": "certain"
9193
9193
  },
@@ -9835,7 +9835,7 @@
9835
9835
  "surface": "builders-hall",
9836
9836
  "text": "Nothing to submit — every module here ships with the core.",
9837
9837
  "file": "modules/hall-ui/public/modules.js",
9838
- "line": 531,
9838
+ "line": 540,
9839
9839
  "origin": "js-text-assign",
9840
9840
  "confidence": "certain"
9841
9841
  },
@@ -12985,7 +12985,7 @@
12985
12985
  "surface": "builders-hall",
12986
12986
  "text": "Taken out of the store. Your copy keeps working, but it will get no more updates.",
12987
12987
  "file": "modules/hall-ui/public/modules.js",
12988
- "line": 125,
12988
+ "line": 134,
12989
12989
  "origin": "js-markup",
12990
12990
  "confidence": "certain"
12991
12991
  },
@@ -14470,7 +14470,7 @@
14470
14470
  "surface": "builders-hall",
14471
14471
  "text": "This project tests new versions first",
14472
14472
  "file": "modules/hall-ui/public/modules.js",
14473
- "line": 211,
14473
+ "line": 220,
14474
14474
  "origin": "js-markup",
14475
14475
  "confidence": "certain"
14476
14476
  },
@@ -16711,7 +16711,7 @@
16711
16711
  "surface": "builders-hall",
16712
16712
  "text": "delisted · no more updates",
16713
16713
  "file": "modules/hall-ui/public/modules.js",
16714
- "line": 125,
16714
+ "line": 134,
16715
16715
  "origin": "js-markup",
16716
16716
  "confidence": "certain"
16717
16717
  },
@@ -17161,7 +17161,7 @@
17161
17161
  "surface": "builders-hall",
17162
17162
  "text": "kept on general: {…}",
17163
17163
  "file": "modules/hall-ui/public/modules.js",
17164
- "line": 207,
17164
+ "line": 216,
17165
17165
  "origin": "js-markup",
17166
17166
  "confidence": "certain"
17167
17167
  },
@@ -17413,7 +17413,7 @@
17413
17413
  "surface": "builders-hall",
17414
17414
  "text": "not measured yet",
17415
17415
  "file": "modules/hall-ui/public/modules.js",
17416
- "line": 351,
17416
+ "line": 360,
17417
17417
  "origin": "js-markup",
17418
17418
  "confidence": "certain"
17419
17419
  },
@@ -17449,7 +17449,7 @@
17449
17449
  "surface": "builders-hall",
17450
17450
  "text": "not testing",
17451
17451
  "file": "modules/hall-ui/public/modules.js",
17452
- "line": 206,
17452
+ "line": 215,
17453
17453
  "origin": "js-markup",
17454
17454
  "confidence": "certain"
17455
17455
  },
@@ -17809,7 +17809,7 @@
17809
17809
  "surface": "builders-hall",
17810
17810
  "text": "restart it",
17811
17811
  "file": "modules/hall-ui/public/modules.js",
17812
- "line": 141,
17812
+ "line": 150,
17813
17813
  "origin": "js-markup",
17814
17814
  "confidence": "certain"
17815
17815
  },
@@ -18016,7 +18016,7 @@
18016
18016
  "surface": "builders-hall",
18017
18017
  "text": "strong at {…}",
18018
18018
  "file": "modules/hall-ui/public/modules.js",
18019
- "line": 340,
18019
+ "line": 349,
18020
18020
  "origin": "js-markup",
18021
18021
  "confidence": "certain"
18022
18022
  },
@@ -18025,7 +18025,7 @@
18025
18025
  "surface": "builders-hall",
18026
18026
  "text": "submit for review",
18027
18027
  "file": "modules/hall-ui/public/modules.js",
18028
- "line": 286,
18028
+ "line": 295,
18029
18029
  "origin": "js-markup",
18030
18030
  "confidence": "certain"
18031
18031
  },
@@ -18601,7 +18601,7 @@
18601
18601
  "surface": "builders-hall",
18602
18602
  "text": "waits for a restart",
18603
18603
  "file": "modules/hall-ui/public/modules.js",
18604
- "line": 139,
18604
+ "line": 148,
18605
18605
  "origin": "js-markup",
18606
18606
  "confidence": "certain"
18607
18607
  },
@@ -18619,7 +18619,7 @@
18619
18619
  "surface": "builders-hall",
18620
18620
  "text": "weak at {…}",
18621
18621
  "file": "modules/hall-ui/public/modules.js",
18622
- "line": 342,
18622
+ "line": 351,
18623
18623
  "origin": "js-markup",
18624
18624
  "confidence": "certain"
18625
18625
  },
@@ -18767,6 +18767,15 @@
18767
18767
  "origin": "js-markup",
18768
18768
  "confidence": "certain"
18769
18769
  },
18770
+ {
18771
+ "id": "caa8962d20c8",
18772
+ "surface": "builders-hall",
18773
+ "text": "withdrawn · update when a fix is out",
18774
+ "file": "modules/hall-ui/public/modules.js",
18775
+ "line": 127,
18776
+ "origin": "js-markup",
18777
+ "confidence": "certain"
18778
+ },
18770
18779
  {
18771
18780
  "id": "80707a00b301",
18772
18781
  "surface": "builders-hall",
@@ -18893,6 +18902,15 @@
18893
18902
  "origin": "js-markup",
18894
18903
  "confidence": "certain"
18895
18904
  },
18905
+ {
18906
+ "id": "8ebfbca38e77",
18907
+ "surface": "builders-hall",
18908
+ "text": "{…} Your copy keeps working; update when a newer version is out.",
18909
+ "file": "modules/hall-ui/public/modules.js",
18910
+ "line": 127,
18911
+ "origin": "js-markup",
18912
+ "confidence": "certain"
18913
+ },
18896
18914
  {
18897
18915
  "id": "c5623d60015d",
18898
18916
  "surface": "builders-hall",
@@ -18916,7 +18934,7 @@
18916
18934
  "surface": "builders-hall",
18917
18935
  "text": "{…} and above can enable or disable a module",
18918
18936
  "file": "modules/hall-ui/public/modules.js",
18919
- "line": 284,
18937
+ "line": 293,
18920
18938
  "origin": "js-markup",
18921
18939
  "confidence": "certain"
18922
18940
  },
@@ -18925,7 +18943,7 @@
18925
18943
  "surface": "builders-hall",
18926
18944
  "text": "{…} and above can switch this",
18927
18945
  "file": "modules/hall-ui/public/modules.js",
18928
- "line": 210,
18946
+ "line": 219,
18929
18947
  "origin": "js-markup",
18930
18948
  "confidence": "certain"
18931
18949
  },
@@ -19024,7 +19042,7 @@
19024
19042
  "surface": "builders-hall",
19025
19043
  "text": "{…} not measured yet",
19026
19044
  "file": "modules/hall-ui/public/modules.js",
19027
- "line": 325,
19045
+ "line": 334,
19028
19046
  "origin": "js-markup",
19029
19047
  "confidence": "certain"
19030
19048
  },
@@ -19231,7 +19249,7 @@
19231
19249
  "surface": "builders-hall",
19232
19250
  "text": "{…} {…} — how-to",
19233
19251
  "file": "modules/hall-ui/public/modules.js",
19234
- "line": 646,
19252
+ "line": 655,
19235
19253
  "origin": "js-text-assign",
19236
19254
  "confidence": "certain"
19237
19255
  },
@@ -19294,7 +19312,7 @@
19294
19312
  "surface": "builders-hall",
19295
19313
  "text": "{…} — how to use it",
19296
19314
  "file": "modules/hall-ui/public/modules.js",
19297
- "line": 648,
19315
+ "line": 657,
19298
19316
  "origin": "js-text-assign",
19299
19317
  "confidence": "certain"
19300
19318
  },
@@ -19420,7 +19438,7 @@
19420
19438
  "surface": "builders-hall",
19421
19439
  "text": "· version",
19422
19440
  "file": "modules/hall-ui/public/modules.js",
19423
- "line": 651,
19441
+ "line": 660,
19424
19442
  "origin": "js-markup",
19425
19443
  "confidence": "certain"
19426
19444
  },
@@ -3225,5 +3225,9 @@ is load-bearing: the script throws rather than guess if it is missing, and
3225
3225
  landed since 1.23.2 with no explicit bump. run 40402796050. (task 1002620)
3226
3226
  1.23.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3227
3227
  landed since 1.23.3 with no explicit bump. run 40406624494. (task 1002620)
3228
+ 1.23.5 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3229
+ landed since 1.23.4 with no explicit bump. run 40417462733. (task 1002620)
3230
+ 1.23.6 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3231
+ landed since 1.23.5 with no explicit bump. run 40420658396. (task 1002620)
3228
3232
  ---------------------------------------------------------------------------
3229
3233
  ```
@@ -391,6 +391,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/bo
391
391
  - The version is `module.json`'s `version`. Each version is published **once** and must be newer than the last — bump it to publish a fix.
392
392
  - Your first publish of a key makes you its author; only the author can publish later versions, and a delisted key takes none.
393
393
  - **Every new version goes to testers first** (task 1003803, [ADR 0344](adr/0344-a-tester-is-a-project-that-opts-in-once.md) D2). A published version, first release or update, is recorded on the `tester` channel (`store_module_version_channels`, reason `new_version`), and the publish response says so as `channel`. A publish cannot name a channel, so there is no flag that sends an untested version to every project. Moving it to `general` is a separate, later step: the promotion gate (task 1003804).
394
+ - **Taking a version back** (task 1004895). Once a version is on `general` the tester stage is behind it, so a Metic+ person can withdraw it: `POST /store/modules/:key/versions/:version/withdraw { reason }` (the `module.delist` permission, the same one that guards delist and release). It writes one `withdrawn` channel row (reason `withdrawn_by_person`, `set_by` the person; the reason text is kept by the audit log), and from then on acquire, install, update and the update check hand out the newest version still served instead; an exact request for the withdrawn one is refused by name. The reason is kept on that row (`note`, core_281). Nothing a project already holds is touched: it keeps running the version, and both `bongos upgrade` and the hall's Modules tab tell it the version was withdrawn, with the reason (`GET /store/modules/latest` returns a `withdrawn` map of `{ version, note }` beside `latest`, and each `GET /modules` row carries `store_withdrawn`). Because update only moves forward, the way off is a newer version: until the author publishes one, the upgrade report says so rather than offering the older release. Withdrawing the only served version is allowed and leaves nothing to hand out. A second withdraw of the same version is refused (`already_withdrawn`). There is no un-withdraw route.
394
395
  - **When a tester version reaches everyone** (task 1003804, [ADR 0344](adr/0344-a-tester-is-a-project-that-opts-in-once.md) D3; owner, 2026-10-23). A version on `tester` moves to `general` after **7 days**, or **from day 2** once **5 or more** testers report a clean install, as long as its Security gate has passed and its own tests pass (every test file, or no tests declared; ADR 0343). Any failed tester install or runtime error **holds** it on `tester` (reason `held_tester_errors`); a held version does not move at day 7, and only a Metic+ person releases it, with `POST /store/modules/:key/versions/:version/release { reason }` (the `module.delist` permission). A version is only ever held on evidence, so one nobody tests still moves at day 7. Each move is a new channel row: reason `promoted_after_7_days`, `promoted_early_clean_installs` or `released_by_person`, and `set_by` empty when the system moved it. An hourly sweep in the server applies the rule (`MODULE_PROMOTION_DISABLED=1` switches it off). `GET /store/modules/:key/versions/:version/channel` shows any signed-in builder a version's channel, who moved it and why, the rule in plain words and, on `tester`, the counts it is waiting on and the day it goes to everyone. The rule is `decidePromotion` in `src/bongos/module-promotion.js`.
395
396
  - The publish denylist applies (ADR 0098), plus a refusal of credential-named files (`.env*`, `*.pem`, `*.key`, `id_rsa`, …) anywhere in the module. The store re-checks every hash and the denylist itself.
396
397
  - Gate: Metic+ (`module.submit`) for now. This is not `bongos module submit`, which proposes a module *into* core.
@@ -399,6 +400,7 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/bo
399
400
  - **Tester feedback** (task 1003806, ADR 0344 D5). While a version is on the tester channel, what tester projects report about it becomes its Tester feedback part: the share of testers whose install worked and who hit no runtime errors, blended with ten neutral testers so a small sample cannot score high — `round(100 × (went well + 5) / (testers + 10))`. Reports from after the version left the tester channel, and the author's own, never count; under 3 testers it is not scored yet. The rule is `assessTesterFeedback` in `scripts/bongos/module-assess-installs.js`.
400
401
  - **What a module is strong and weak at** (task 1003799, ADR 0343 D4). Each catalog entry also carries `parts` (the current result of Tests, Clean installs, Reliability, Tester feedback and How-to) and a reading of them: `strengths` (a part scored **80 or more**), `weaknesses` (a part scored **below the floor**) and `unmeasured` (a part with no score yet, which is never counted as weak). Each strength and weakness has a one-sentence `detail` saying what the number rests on. Security is not among them: every listed version has passed it. The reading is `readParts` in `src/bongos/module-listing.js`; the hall's Modules tab shows it in its "In the store" group, so two modules with the same overall score can be told apart.
401
402
  - **Sorting by a part** (task 1003800). `GET /store/modules?sort=<part>` orders the listed modules by one part a buyer cares about: `tests`, `install`, `reliability`, `tester_feedback` or `docs` (the response lists them as `sort_parts`, and says which one it applied as `sort`). Modules with a score for that part come first, highest first; a tie goes to the higher overall score, then the module key. A module with no score for that part goes last, so missing data never wins or loses a place on its own. New modules are not sorted: they are never ranked. Any other `sort` value is refused with a 400. Without `sort`, the listed modules are in the ranking below; New stays in key order. The rule is `sortByPart` in `src/bongos/module-listing.js`.
403
+ - **Setting a price** (task 1004894). A module's author sets its price with `bongos module price <key> [--cents N] [--credits N]` (or the same two flags on `bongos module publish`, which sets the price right after the upload), or `POST /store/modules/:key/price { cents?, credits? }` (the `module.submit` permission, the same one that publishes, and only the author passes; a delisted module takes no new price). Each amount is a whole number of 0 or more, or null for "not offered in that currency"; at least one is given, and 0 in either makes the module free. Every change appends a row to the price history (in one transaction, the module row locked) naming who set it and when, so the history is the audit of what each price replaced, and the newest is current: it is what the catalog shows and what acquire charges. A paid price may be set before buying exists (owner, 2026-10-26): the catalog lists the module with its price and `buyable: false`, `bongos module store` prints it as not yet buyable, and acquire keeps refusing it without an entitlement. When buying ships (task 1003788), `BUYING_OPEN` in `src/bongos/module-listing.js` is the one switch to flip. Price is never an input to the score (ADR 0343 D6).
402
404
  - **The ranking, and a maximum price** (task 1003808, owner decision 2026-10-23). Without `sort`, listed modules are ranked **quality first**: highest overall score first; between equal scores, free modules first, then the lowest cash price, then the lowest credit price, then the module key. Price never buys a better place, it only orders modules the score could not tell apart (and it is never an input to the score, ADR 0343 D6). The response states the rule as `ranking`, and each entry carries its current `price` (`cents`, `credits`, `free`). A buyer may add `?max_cents=` and/or `?max_credits=` (whole numbers, 0 or more): modules that cost more are left out of both lists, free modules always stay, and a priced module stays when it is offered at or under a cap the buyer set. The applied caps come back as `max_price`. The rules are `rankListed` and `withinMaxPrice` in `src/bongos/module-listing.js`.
403
405
  - **The cap: at most ten per sub-category** (task 1003809, [ADR 0342](adr/0342-module-categories-are-eight-parents-with-approved-sub-categories.md) D2). Above the floor, at most **ten** modules are listed in each approved sub-category: the first ten in the ranking above, so a buyer's `sort` or maximum price reorders or narrows those ten but never lets the eleventh in. The cap is never applied to a parent. A module whose sub-category is only proposed is listed under its parent and counts toward no cap, and a New module is not ranked, so it neither takes a slot nor is held by one. The catalog returns the cap as `cap`. The rule is `applyCap` in `src/bongos/module-listing.js`.
404
406
  - **Why a module is not listed** (task 1003810). `GET /store/modules/:key/listing` says where one module stands, judging the same newest version the catalog does: `listed` (true or false), a `reason` (`not_assessed`, `security_not_passed`, `no_score`, `below_floor`, `delisted`, `no_version`, `over_cap`, `new` or `at_or_above_floor`), `why` in a sentence and `next_step` saying what would change it. A failed Security gate is told apart from one that has not finished, and names what it found to the module's author only (to anyone else it would point at a weakness not fixed yet); below the floor names the score, the floor and the weakest parts. It carries the same `parts`, `strengths`, `weaknesses` and `unmeasured` as the catalog, plus `security`. Any signed-in builder can read it. Past the cap it is `over_cap`, which names the score, the module's place in its sub-category and the cap, so an author can tell being eleventh apart from being below the floor. Both responses carry the module's `category` (`parent`, `parent_title`, and `sub` and `label` once its sub-category is approved). The rule is `explainPlacement` in `src/bongos/module-listing.js`.
@@ -1644,7 +1644,7 @@
1644
1644
  "surface": "builders",
1645
1645
  "title": "Modules",
1646
1646
  "states_read": ["browse","search-empty","lens","details","out"],
1647
- "files_hash": "d23744d56d3d6024",
1647
+ "files_hash": "29285c080d99ce0e",
1648
1648
  "reading_hash": "d544f93125c23b24",
1649
1649
  "counts": {"lines":58,"placed":26,"shared":8,"unplaced":24},
1650
1650
  "omitted": {"data":106},
@@ -1653,7 +1653,7 @@
1653
1653
  {"key":"L0001","section":"skip link","text":"Skip to content","placement":"shared","file":"modules/hall-ui/public/shell.js","line":868,"string_id":"87cee78ca134"},
1654
1654
  {"key":"L0002","section":"top bar","text":"Modules","placement":"shared"},
1655
1655
  {"key":"L0003","section":"top bar","text":"Jump to…","placement":"shared","file":"modules/hall-ui/public/shell.js","line":540,"string_id":"eba610d25523"},
1656
- {"key":"L0004","section":"top bar","text":"⌘K","placement":"shared"},
1656
+ {"key":"L0004","section":"top bar","text":"Ctrl K","placement":"shared"},
1657
1657
  {"key":"L0005","section":"top bar","text":"Building","placement":"shared"},
1658
1658
  {"key":"L0006","section":"top bar","text":"Artistry","placement":"shared"},
1659
1659
  {"key":"L0007","section":"top bar","text":"Ideating","placement":"shared"},
@@ -1665,7 +1665,7 @@
1665
1665
  {"key":"L0013","section":"Module catalog","text":"all","placement":"unplaced"},
1666
1666
  {"key":"L0014","section":"Module catalog","text":"enabled","placement":"unplaced","occurrences":16},
1667
1667
  {"key":"L0015","section":"Module catalog","text":"disabled","placement":"unplaced","occurrences":4},
1668
- {"key":"L0016","section":"Module catalog","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"search-empty","occurrences":3},
1668
+ {"key":"L0016","section":"Module catalog","text":"{…} of {…}","placement":"placed","file":"modules/hall-ui/public/sky-panel.js","line":120,"string_id":"73c1372b43d9","state":"lens","occurrences":4},
1669
1669
  {"key":"L0017","section":"Module catalog","text":"modules","placement":"unplaced"},
1670
1670
  {"key":"L0018","section":"Module catalog","text":"16 from the {…}","placement":"unplaced"},
1671
1671
  {"key":"L0019","section":"Module catalog","text":"of {…}","placement":"placed","file":"modules/hall-ui/public/roadmap.js","line":311,"string_id":"5e7a2220c6a9"},
@@ -1683,20 +1683,20 @@
1683
1683
  {"key":"L0031","section":"Module catalog","text":"{…}-loaded","placement":"unplaced"},
1684
1684
  {"key":"L0032","section":"Module catalog","text":"sessions","placement":"placed","file":"modules/hall-ui/public/task.js","line":483,"string_id":"732b4c935e36"},
1685
1685
  {"key":"L0033","section":"Module catalog","text":"Built by this project","placement":"placed","file":"modules/hall-ui/public/modules.html","line":67,"string_id":"b51edc826cbe"},
1686
- {"key":"L0034","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":286,"string_id":"0215b2cdca8c"},
1686
+ {"key":"L0034","section":"Module catalog","text":"submit for review","placement":"placed","file":"modules/hall-ui/public/modules.js","line":295,"string_id":"0215b2cdca8c"},
1687
1687
  {"key":"L0035","section":"Module catalog","text":"A module built here can be sent for review to join the shared catalog, credited to you and this project.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":71,"string_id":"25442a59064e"},
1688
1688
  {"key":"L0036","section":"Module catalog","text":"Testing new versions","placement":"placed","file":"modules/hall-ui/public/modules.html","line":79,"string_id":"d52c87fba7da"},
1689
- {"key":"L0037","section":"Module catalog","text":"This project tests new versions first","placement":"placed","file":"modules/hall-ui/public/modules.js","line":211,"string_id":"1116d2cad470"},
1689
+ {"key":"L0037","section":"Module catalog","text":"This project tests new versions first","placement":"placed","file":"modules/hall-ui/public/modules.js","line":220,"string_id":"1116d2cad470"},
1690
1690
  {"key":"L0038","section":"Module catalog","text":"testing","placement":"unplaced"},
1691
- {"key":"L0039","section":"Module catalog","text":"kept on general: {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":207,"string_id":"cf5047ea06d8"},
1691
+ {"key":"L0039","section":"Module catalog","text":"kept on general: {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":216,"string_id":"cf5047ea06d8"},
1692
1692
  {"key":"L0040","section":"Module catalog","text":"A testing project gets new module versions first, and what it finds feeds their scores. It is never switched on for you. To keep one module on the versions everyone gets, open its details.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":82,"string_id":"a517246720d9"},
1693
1693
  {"key":"L0041","section":"Module catalog","text":"In the store","placement":"placed","file":"modules/hall-ui/public/modules.html","line":91,"string_id":"ecfebcd52894"},
1694
1694
  {"key":"L0042","section":"Module catalog","text":"Sort by","placement":"placed","file":"modules/hall-ui/public/modules.html","line":95,"string_id":"70383b194ec0"},
1695
1695
  {"key":"L0043","section":"Module catalog","text":"Name","placement":"unplaced"},
1696
1696
  {"key":"L0044","section":"Module catalog","text":"No store module matches that {…}.","placement":"unplaced","state":"search-empty"},
1697
1697
  {"key":"L0045","section":"Module catalog","text":"scores","placement":"unplaced","occurrences":2},
1698
- {"key":"L0046","section":"Module catalog","text":"strong at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":340,"string_id":"e294c3429c2e","occurrences":3},
1699
- {"key":"L0047","section":"Module catalog","text":"weak at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":342,"string_id":"20318b6da94e"},
1698
+ {"key":"L0046","section":"Module catalog","text":"strong at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":349,"string_id":"e294c3429c2e","occurrences":3},
1699
+ {"key":"L0047","section":"Module catalog","text":"weak at {…}","placement":"placed","file":"modules/hall-ui/public/modules.js","line":351,"string_id":"20318b6da94e"},
1700
1700
  {"key":"L0048","section":"Module catalog","text":"A part is strong at 80 of 100 or more, and weak below the listing floor. Missing data never counts against a module, and a new module is not ranked.","placement":"placed","file":"modules/hall-ui/public/modules.html","line":98,"string_id":"01857ee8cbe3"},
1701
1701
  {"key":"L0049","section":"footer","text":"Sign out","placement":"shared","file":"modules/hall-ui/public/shell.js","line":516,"string_id":"1883ad7f07fc"},
1702
1702
  {"key":"L0050","section":"Module catalog","text":"Builders hall · this session","placement":"placed","file":"modules/hall-ui/public/craft-mode.js","line":670,"string_id":"fd1903586e8e"},
@@ -0,0 +1,25 @@
1
+ -- core_281_module_version_withdraw_note.sql — the reason a person gave when they took
2
+ -- a module version back (task 1004895, criterion wa5-staged-rollout).
3
+ --
4
+ -- WHAT IT IS. One nullable column on the channel history (core_276). A withdraw
5
+ -- writes a 'withdrawn' row; `note` holds the reason the Metic+ person gave, so a
6
+ -- project still running that version is told WHY ("crashes on start", "security
7
+ -- flaw, update now") in `bongos upgrade` and the hall's Modules tab, not just that
8
+ -- it happened. `reason` stays the machine code it always was (^[a-z][a-z0-9_]*$);
9
+ -- `note` is the person's words. Every other move (publish, promote, release) leaves
10
+ -- it NULL, so no existing row or writer changes.
11
+ --
12
+ -- Additive only (one nullable column), so forward-safe: an older release that does
13
+ -- not know the column keeps reading and writing the table as before.
14
+ -- Idempotent: safe to re-run.
15
+ -- Approved by the owner on 2026-10-26 (in chat: "Yes, show the reason").
16
+
17
+ BEGIN;
18
+
19
+ ALTER TABLE store_module_version_channels
20
+ ADD COLUMN IF NOT EXISTS note text CHECK (note IS NULL OR length(note) BETWEEN 1 AND 2000);
21
+
22
+ INSERT INTO schema_migrations (version) VALUES ('core_281_module_version_withdraw_note')
23
+ ON CONFLICT (version) DO NOTHING;
24
+
25
+ COMMIT;
@@ -240,7 +240,7 @@ const PERMISSIONS = [
240
240
  { key: 'module.subcategory.review', system: false, floor: 'metic', guards: 'GET /store/subcategories/queue, POST /store/subcategories/:id/approve, POST /store/subcategories/:id/merge' },
241
241
  // Take a module out of the store (task 1003797, ADR 0338 D2). Stops new installs,
242
242
  // versions and downloads; never reaches an instance that already holds it.
243
- { key: 'module.delist', system: false, floor: 'metic', guards: 'POST /store/modules/:key/delist, POST /store/modules/:key/versions/:version/release (a version the promotion gate held on tester — task 1003804)' },
243
+ { key: 'module.delist', system: false, floor: 'metic', guards: 'POST /store/modules/:key/delist, POST /store/modules/:key/versions/:version/release (a version the promotion gate held on tester — task 1003804), POST /store/modules/:key/versions/:version/withdraw (take a version back — task 1004895)' },
244
244
  { key: 'security.report.file', system: false, floor: 'metic', guards: 'POST /security/reports, GET /security/adr' },
245
245
  { key: 'llm_cache.use', system: false, floor: 'metic', guards: 'POST /llm-cache/{lookup,store}' },
246
246
  { key: 'backup.manage', system: false, floor: 'metic', guards: 'GET /backup/status, POST /backup/trigger' },
@@ -118,6 +118,15 @@
118
118
  return `<a class="ov-fact mod-howto" href="${escapeHtml(href)}">how-to</a>`;
119
119
  }
120
120
 
121
+ // A store module this instance runs at a version the store took back (task
122
+ // 1004895): it keeps working here, and the reason the person gave is the tooltip.
123
+ function withdrawnFact(m) {
124
+ const w = m.store_withdrawn;
125
+ if (!w || typeof w !== 'object') return '';
126
+ const why = w.note ? `Taken back from the store: ${w.note}` : 'Taken back from the store.';
127
+ return `<span class="fact-pill fact-pill--warn" title="${escapeHtml(why)} Your copy keeps working; update when a newer version is out.">withdrawn · update when a fix is out</span>`;
128
+ }
129
+
121
130
  // A store module this instance holds that the store has since delisted (task
122
131
  // 1003797, ADR 0338 D2): it keeps working here, but no more versions will come.
123
132
  function delistedFact(m) {
@@ -296,7 +305,7 @@
296
305
  <div class="ov-row__main">
297
306
  <span class="ov-row__title">${escapeHtml(m.title || m.key)}<span class="ov-mono">${escapeHtml(m.key)}</span></span>
298
307
  ${desc ? `<p class="ov-row__sub mod-row__desc">${escapeHtml(desc)}</p>` : ''}
299
- <div class="ov-row__facts">${provenancePill(m)}${categoryFact(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}${restartFact(m)}</div>
308
+ <div class="ov-row__facts">${provenancePill(m)}${categoryFact(m)}${attributionFacts(m)}${upkeepFact(m)}${howtoFact(m)}${delistedFact(m)}${withdrawnFact(m)}${restartFact(m)}</div>
300
309
  ${lines ? `<details class="mod-more"${openKeys.has(m.key) ? ' open' : ''}><summary aria-label="Details for ${escapeHtml(m.title || m.key)}"></summary>${lines}</details>` : ''}
301
310
  </div>
302
311
  <div class="ov-row__right mod-row__right">${rightHtml(m)}</div>
@@ -357,9 +357,12 @@ module.exports = function buildAccessRequestsRouter() {
357
357
  // (the device_code is spent), so the CLI can't re-poll the flow — it polls this
358
358
  // instead, then restarts sign-in once admitted. Exposes nothing the public POST
359
359
  // above doesn't already reveal through its 409s (already_member / already_approved).
360
- // status ∈ member | invited | expired | pending | dismissed | none; admitted =
361
- // member||invited. `expired` is an invite past its 30 days (task 1002969): the door
362
- // is shut, and the CLI stops waiting rather than polling a door that will not open.
360
+ // status ∈ member | invited | not_admitted; admitted = member||invited (ADR 0217 D4,
361
+ // task 1003359). `not_admitted` covers pending, declined, a lapsed invite and no
362
+ // request at all, so a caller who is not the subject cannot learn that someone
363
+ // applied here or was turned down. It does NOT close the membership oracle —
364
+ // `admitted` still answers that, and the limiter below is what bounds enumeration.
365
+ // A lapsed invite answering `not_admitted` rather than `expired` amends ADR 0356 D4.
363
366
  // rank: public — a would-be builder is by definition outside the system.
364
367
  // Declared BEFORE `GET /access-requests` so the literal `/status` path is matched
365
368
  // as its own route (not swallowed as a query on the list route).
@@ -370,9 +373,7 @@ module.exports = function buildAccessRequestsRouter() {
370
373
  // per-IP budget: core's GET /auth/web/admission-status (the boolean twin of
371
374
  // this answer) mounts the very same instance, so the two cannot be alternated
372
375
  // for double the rate. The limiter runs before the queries, so a refused probe
373
- // costs no DB work. Whether the ANSWER should also collapse — so a caller who
374
- // is not the subject cannot tell pending/dismissed/none apart — is the owner's
375
- // call and is filed as a blocker, not decided here.
376
+ // costs no DB work.
376
377
  router.get('/access-requests/status', accountExistenceReadRateLimit, async (req, res) => {
377
378
  const login = normalizeLogin(req.query.github_login);
378
379
  if (!GITHUB_LOGIN_RE.test(login)) {
@@ -386,7 +387,7 @@ module.exports = function buildAccessRequestsRouter() {
386
387
  if (member.rows.length > 0) {
387
388
  return res.json({ github_login: login, status: 'member', admitted: true });
388
389
  }
389
- // Prefer an invite that still admits, else the most recent request.
390
+ // A live invite sorts first, so the top row admits only if it is one.
390
391
  const ar = await pool.query(
391
392
  `SELECT status, (status = 'invited' AND ${inviteExpiredSql()}) AS expired
392
393
  FROM access_requests
@@ -396,8 +397,8 @@ module.exports = function buildAccessRequestsRouter() {
396
397
  [login]
397
398
  );
398
399
  const top = ar.rows[0];
399
- const status = !top ? 'none' : top.expired ? 'expired' : top.status;
400
- return res.json({ github_login: login, status, admitted: status === 'invited' });
400
+ const invited = !!top && top.status === 'invited' && !top.expired;
401
+ return res.json({ github_login: login, status: invited ? 'invited' : 'not_admitted', admitted: invited });
401
402
  } catch (err) {
402
403
  console.error('[gds] GET /access-requests/status', err);
403
404
  return res.fail('status_failed', { status: 500, message: 'internal error' });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.4",
3
+ "version": "1.23.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.23.4",
9
+ "version": "1.23.6",
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.4",
3
+ "version": "1.23.6",
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",
@@ -10407,5 +10407,21 @@
10407
10407
  "id": "1003811",
10408
10408
  "text": "Builders can now browse the module store from the terminal with bongos module store, and it always shows the same modules in the same order as the hall."
10409
10409
  }
10410
+ ],
10411
+ "1.23.5": [
10412
+ {
10413
+ "id": "1004895",
10414
+ "text": "A broken module version can now be pulled back from the store, so new installs and updates stop getting it while projects that already have it are told."
10415
+ }
10416
+ ],
10417
+ "1.23.6": [
10418
+ {
10419
+ "id": "1004894",
10420
+ "text": "Module authors can now set a price for their module, in money or credits; paid modules show their price and are marked not yet buyable until buying opens."
10421
+ },
10422
+ {
10423
+ "id": "1003359",
10424
+ "text": "People checking whether someone has applied to a project can no longer tell a pending, declined or never-filed request apart, and the sign-in tool no longer promises an approval that may never come."
10425
+ }
10410
10426
  ]
10411
10427
  }
@@ -43,17 +43,24 @@ function normalizeInstanceBase(raw) {
43
43
  return s;
44
44
  }
45
45
 
46
- // Decide what an /access-requests/status poll means for the sign-in loop.
46
+ // Decide what an /access-requests/status poll means for the sign-in loop. The probe
47
+ // answers only admitted or not (ADR 0217 D4), so anything else is a wait — even an
48
+ // older instance's `dismissed`/`expired`, which the loop no longer reads.
47
49
  function pollDecision(body) {
48
50
  if (!body || typeof body !== 'object') return 'wait';
49
51
  if (body.admitted === true) return 'admitted';
50
52
  if (body.status === 'member' || body.status === 'invited') return 'admitted';
51
- if (body.status === 'dismissed') return 'dismissed';
52
- // An invite past its 30 days (task 1002969): the door is shut until they invite again.
53
- if (body.status === 'expired') return 'expired';
54
53
  return 'wait';
55
54
  }
56
55
 
56
+ // What the loop says when it stops waiting. It cannot know whether the request is
57
+ // still pending, was declined, or rode a lapsed invite, so it promises nothing.
58
+ function waitTimeoutMessage(base) {
59
+ return "✖ Stopped waiting after 30 minutes — you haven't been let in yet. If your request was turned down "
60
+ + "or your invite has run out, waiting longer won't change that. Ask the project's owner, "
61
+ + `then re-run \`bongos login ${base}\`.`;
62
+ }
63
+
57
64
  // --- I/O helpers -------------------------------------------------------------
58
65
 
59
66
  async function fetchJson(url, opts = {}) {
@@ -300,16 +307,8 @@ async function main() {
300
307
  log('\n ✓ Approved! Finishing sign-in — approve once more in your browser.');
301
308
  break;
302
309
  }
303
- if (decision === 'dismissed') {
304
- console.error('\n✖ your access request was declined.');
305
- process.exit(1);
306
- }
307
- if (decision === 'expired') {
308
- console.error('\n✖ your invite has expired — ask the project to invite you again.');
309
- process.exit(1);
310
- }
311
310
  if (Date.now() > admitDeadline) {
312
- console.error('\n✖ timed out waiting for approval — re-run `bongos login` once an Archon has approved you.');
311
+ console.error(`\n${waitTimeoutMessage(base)}`);
313
312
  process.exit(1);
314
313
  }
315
314
  process.stdout.write('.');
@@ -357,4 +356,4 @@ if (require.main === module) {
357
356
  });
358
357
  }
359
358
 
360
- module.exports = { normalizeInstanceBase, pollDecision, runDeviceFlow, logOtherInstances };
359
+ module.exports = { normalizeInstanceBase, pollDecision, waitTimeoutMessage, runDeviceFlow, logOtherInstances };