@bongos/core 1.23.22 → 1.24.1

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 (424) hide show
  1. package/.bongos-core.json +902 -407
  2. package/.claude/skills/blocker-review/SKILL.md +9 -1
  3. package/.claude/skills/blocker-solve/SKILL.md +1 -1
  4. package/.claude/skills/builder-claim/SKILL.md +1 -1
  5. package/.claude/skills/builder-reauth/SKILL.md +1 -1
  6. package/.claude/skills/builder-redteam/SKILL.md +1 -1
  7. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  8. package/.claude/skills/builder-setup/SKILL.md +1 -1
  9. package/.claude/skills/builder-ship/SKILL.md +1 -1
  10. package/.claude/skills/collab-review/SKILL.md +5 -2
  11. package/.claude/skills/design/SKILL.md +1 -1
  12. package/.claude/skills/goal-create/SKILL.md +2 -2
  13. package/.claude/skills/grade-recover/SKILL.md +1 -1
  14. package/.claude/skills/merge-mode/SKILL.md +1 -1
  15. package/.claude/skills/owner-review/SKILL.md +1 -1
  16. package/.claude/skills/planning-session/SKILL.md +11 -11
  17. package/.claude/skills/read-session-export/SKILL.md +1 -1
  18. package/.claude/skills/recall/SKILL.md +1 -1
  19. package/.claude/skills/scan-before-install/SKILL.md +1 -1
  20. package/.claude/skills/session-handoff/SKILL.md +1 -1
  21. package/.claude/skills/strand-fix/SKILL.md +13 -0
  22. package/.claude/skills/worktree-clean/SKILL.md +1 -1
  23. package/AGENTS.md +15 -0
  24. package/GEMINI.md +5 -0
  25. package/clients/bongos-client/README.md +1 -1
  26. package/clients/bongos-client/bongos-client.global.js +90 -0
  27. package/clients/bongos-client/index.cjs +90 -0
  28. package/clients/bongos-client/index.d.ts +149 -4
  29. package/clients/bongos-client/index.mjs +90 -0
  30. package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
  31. package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
  32. package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
  33. package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
  34. package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
  35. package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
  36. package/docs/adr/README.md +5 -1
  37. package/docs/api/openapi.json +8555 -5876
  38. package/docs/api-reference.md +63 -10
  39. package/docs/architecture.md +37 -1
  40. package/docs/canonical-permissions.md +1 -1
  41. package/docs/copy-inventory.md +420 -337
  42. package/docs/copy-registry.json +1321 -500
  43. package/docs/file-map.md +1 -0
  44. package/docs/module-api-changelog.md +5 -0
  45. package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
  46. package/docs/onboarding/diagrams/README.md +1 -1
  47. package/docs/onboarding/drachmae-and-karma.md +15 -15
  48. package/docs/onboarding/primer.md +2 -2
  49. package/docs/page-inventory.json +55 -6
  50. package/docs/page-readings.json +1185 -1011
  51. package/docs/recipes/google-sign-in.md +60 -0
  52. package/docs/recipes/ops-gotchas.md +1 -1
  53. package/migrations/core_282_builders_lower_login_idx.sql +33 -0
  54. package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
  55. package/migrations/core_283_payout_approvals.sql +183 -0
  56. package/migrations/core_284_payout_gates.sql +179 -0
  57. package/migrations/core_285_task_needs.sql +74 -0
  58. package/modules/ai-studio/billing.js +67 -0
  59. package/modules/ai-studio/context.js +29 -0
  60. package/modules/ai-studio/db.js +172 -0
  61. package/modules/ai-studio/gemini.js +75 -0
  62. package/modules/ai-studio/generate.js +119 -0
  63. package/modules/ai-studio/keys.js +73 -0
  64. package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
  65. package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
  66. package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
  67. package/modules/ai-studio/openrouter.js +90 -0
  68. package/modules/ai-studio/payments.js +105 -0
  69. package/modules/ai-studio/pricing.js +92 -0
  70. package/modules/ai-studio/routes/ai-studio.js +235 -5
  71. package/modules/ai-studio/studio.js +121 -0
  72. package/modules/autonomy/fence.js +64 -1
  73. package/modules/builder-settings/builder-needs.js +27 -1
  74. package/modules/copy-desk/routes/copy-desk.js +5 -0
  75. package/modules/economy/achievements.js +22 -10
  76. package/modules/economy/cost-classify.js +9 -0
  77. package/modules/economy/cost-write-guard.js +52 -0
  78. package/modules/economy/cost.js +2 -1
  79. package/modules/economy/credits.js +18 -15
  80. package/modules/economy/goal-reward.js +161 -0
  81. package/modules/economy/reward.js +4 -0
  82. package/modules/economy/routes/cost.js +33 -7
  83. package/modules/government/catalog.js +6 -0
  84. package/modules/government/db.js +34 -0
  85. package/modules/government/docket.js +36 -0
  86. package/modules/government/migrations/government_035_payout_approve.sql +29 -0
  87. package/modules/government/pollers/board-expiry.js +108 -20
  88. package/modules/government/protected-surfaces.json +1 -0
  89. package/modules/government/routes/government.js +22 -0
  90. package/modules/government/routing.js +39 -5
  91. package/modules/government/waiting.js +74 -0
  92. package/modules/hall-ui/public/ai-studio-lib.js +268 -0
  93. package/modules/hall-ui/public/ai-studio.css +193 -0
  94. package/modules/hall-ui/public/ai-studio.html +98 -0
  95. package/modules/hall-ui/public/ai-studio.js +556 -0
  96. package/modules/hall-ui/public/ai-studio.states.json +94 -0
  97. package/modules/hall-ui/public/blockers-by-person.js +156 -0
  98. package/modules/hall-ui/public/blockers-lib.js +15 -1
  99. package/modules/hall-ui/public/blockers.html +8 -0
  100. package/modules/hall-ui/public/blockers.js +1 -1
  101. package/modules/hall-ui/public/claim-action.js +3 -0
  102. package/modules/hall-ui/public/collab.html +12 -0
  103. package/modules/hall-ui/public/craft-mode.js +4 -1
  104. package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
  105. package/modules/hall-ui/public/drachmae.html +1 -1
  106. package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
  107. package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
  108. package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
  109. package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
  110. package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
  111. package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
  112. package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
  113. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
  114. package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
  115. package/modules/hall-ui/public/goal-inbox.js +1 -1
  116. package/modules/hall-ui/public/goals-render.js +4 -0
  117. package/modules/hall-ui/public/goals.html +8 -0
  118. package/modules/hall-ui/public/gov-living.js +6 -2
  119. package/modules/hall-ui/public/hall-ask.css +5 -0
  120. package/modules/hall-ui/public/hall-ask.js +39 -3
  121. package/modules/hall-ui/public/notes-panel.css +20 -0
  122. package/modules/hall-ui/public/notes-panel.js +146 -0
  123. package/modules/hall-ui/public/project-settings.html +1 -1
  124. package/modules/hall-ui/public/project-settings.js +7 -5
  125. package/modules/hall-ui/public/settings-ai-keys.js +124 -0
  126. package/modules/hall-ui/public/settings.html +15 -0
  127. package/modules/hall-ui/public/street-deck.js +12 -3
  128. package/modules/hall-ui/public/studio.css +90 -8
  129. package/modules/hall-ui/public/studio.html +39 -10
  130. package/modules/hall-ui/public/studio.js +148 -18
  131. package/modules/hall-ui/public/task.html +2 -0
  132. package/modules/hall-ui/public/task.js +4 -0
  133. package/modules/hall-ui/public/tweak-editor.js +12 -3
  134. package/modules/hall-ui/public/welcome-boot.css +42 -6
  135. package/modules/hall-ui/public/welcome-boot.js +20 -6
  136. package/modules/hall-ui/public/work.js +20 -0
  137. package/modules/hall-ui/records/ai-studio.md +36 -0
  138. package/modules/hall-ui/records/blockers-people.md +24 -0
  139. package/modules/lifecycle/db-claims.js +32 -4
  140. package/modules/lifecycle/db-goals.js +18 -5
  141. package/modules/lifecycle/db-tasks.js +15 -6
  142. package/modules/lifecycle/done-when.js +23 -1
  143. package/modules/lifecycle/lifecycle.js +5 -0
  144. package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
  145. package/modules/lifecycle/module.json +2 -1
  146. package/modules/lifecycle/notes.js +219 -0
  147. package/modules/lifecycle/page-notice.js +124 -0
  148. package/modules/lifecycle/page-tweak-artist.js +18 -1
  149. package/modules/lifecycle/publish-reconciler.js +67 -20
  150. package/modules/lifecycle/routes/claims.js +5 -2
  151. package/modules/lifecycle/routes/goals.js +3 -1
  152. package/modules/lifecycle/routes/notes.js +91 -0
  153. package/modules/lifecycle/routes/project-settings.js +43 -35
  154. package/modules/lifecycle/routes/task-route-helpers.js +21 -0
  155. package/modules/lifecycle/routes/task-write-routes.js +21 -3
  156. package/modules/lifecycle/routes/tasks.js +12 -12
  157. package/modules/lifecycle/task-needs-gate.js +70 -0
  158. package/modules/market-watch/CLAUDE.md +15 -0
  159. package/modules/market-watch/db.js +340 -0
  160. package/modules/market-watch/key.js +58 -0
  161. package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
  162. package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
  163. package/modules/market-watch/module.json +26 -0
  164. package/modules/market-watch/pollers/hub-seed.js +54 -0
  165. package/modules/market-watch/pollers/weekly-run.js +100 -0
  166. package/modules/market-watch/research.js +350 -0
  167. package/modules/market-watch/run.js +245 -0
  168. package/modules/market-watch/seed/market-data.json +38 -0
  169. package/modules/market-watch/seed/partners-data.json +497 -0
  170. package/modules/market-watch/seed.js +252 -0
  171. package/modules/market-watch/store.js +230 -0
  172. package/modules/memory/routes/memory.js +14 -2
  173. package/modules/memory/routes/search.js +8 -2
  174. package/modules/platform-identity/account-logins.js +192 -0
  175. package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
  176. package/modules/platform-identity/platform-identity.js +5 -4
  177. package/modules/platform-identity/routes/my-projects.js +6 -0
  178. package/modules/platform-identity/routes/profile.js +18 -0
  179. package/modules/platform-identity/routes/public-profile.js +10 -1
  180. package/modules/platform-identity/routes/sso.js +26 -1
  181. package/modules/platform-identity/tests/platform-identity.mjs +2 -1
  182. package/modules/provisioning/credential-preflight.js +13 -3
  183. package/modules/provisioning/pollers/app-liveness.js +25 -5
  184. package/modules/provisioning/routes/provisioning.js +1 -1
  185. package/modules/provisioning/starter-bundles.js +6 -2
  186. package/modules/public-landing/public/account.html +10 -2
  187. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
  188. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  189. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  190. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
  191. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
  192. package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
  193. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
  194. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  195. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  196. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
  197. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
  198. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  199. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  200. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
  201. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
  202. package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
  203. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
  204. package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
  205. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
  206. package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
  207. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
  208. package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
  209. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
  210. package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
  211. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
  212. package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
  213. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
  214. package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
  215. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
  216. package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
  217. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
  218. package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
  219. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
  220. package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
  221. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
  222. package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
  223. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
  224. package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
  225. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
  226. package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
  227. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
  228. package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
  229. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
  230. package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
  231. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
  232. package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
  233. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
  234. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
  235. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
  236. package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
  237. package/modules/public-landing/public/community.html +1 -1
  238. package/modules/public-landing/public/connections.html +2 -2
  239. package/modules/public-landing/public/guild.html +1 -1
  240. package/modules/public-landing/public/invitations.html +2 -2
  241. package/modules/public-landing/public/projects.html +5 -5
  242. package/modules/raise/views/nda.html +2 -0
  243. package/modules/specialities/db.js +32 -1
  244. package/modules/specialities/routes/specialities.js +10 -6
  245. package/modules/status-ui/public/index.html +1 -1
  246. package/package-lock.json +2 -2
  247. package/package.json +1 -1
  248. package/release-notes.json +220 -0
  249. package/scripts/bongos/autobongos-tweak-lane.js +46 -1
  250. package/scripts/bongos/backfill-subagent-rewards.js +2 -2
  251. package/scripts/bongos/claim.js +22 -1
  252. package/scripts/bongos/db-backup-nightly.sh +29 -0
  253. package/scripts/bongos/deploy-divergence.js +7 -0
  254. package/scripts/bongos/doc-cli-guard.js +6 -0
  255. package/scripts/bongos/fitness.js +3 -2
  256. package/scripts/bongos/gen-diagrams.js +3 -3
  257. package/scripts/bongos/lib.sh +4 -5
  258. package/scripts/bongos/migration-namespace.js +13 -1
  259. package/scripts/bongos/plain-cards.js +4 -0
  260. package/scripts/bongos/provision-github-setup.js +3 -1
  261. package/scripts/bongos/provision-units.js +6 -1
  262. package/scripts/bongos/publish-manifest.js +3 -0
  263. package/scripts/bongos/release.js +2 -0
  264. package/scripts/bongos/run-unit-tests.js +268 -82
  265. package/scripts/bongos/seed-tasks.example.json +56 -0
  266. package/scripts/bongos/seed-tasks.js +222 -0
  267. package/scripts/bongos/seed-verify-criteria.js +1 -2
  268. package/scripts/bongos/ship-finish.js +2 -0
  269. package/scripts/bongos/ship-resume.js +2 -0
  270. package/scripts/bongos/strand-lease.js +250 -0
  271. package/scripts/bongos/strand-watch.js +26 -5
  272. package/scripts/bongos/version-literal-guard.js +2 -2
  273. package/scripts/discord/post.js +4 -3
  274. package/scripts/gds/seed-tasks.js +7 -0
  275. package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
  276. package/scripts/hall-preview/server.js +21 -0
  277. package/src/bongos/auth-config.js +29 -1
  278. package/src/bongos/auth-discord.js +3 -3
  279. package/src/bongos/auth-github.js +3 -6
  280. package/src/bongos/auth-google.js +217 -0
  281. package/src/bongos/device-approval.js +140 -0
  282. package/src/bongos/llm-pricing.js +17 -1
  283. package/src/bongos/metrics.js +48 -2
  284. package/src/bongos/middleware/rate-limit.js +30 -11
  285. package/src/bongos/module-scope-map.js +12 -1
  286. package/src/bongos/optimizer.js +15 -1
  287. package/src/bongos/payout-approvals.js +342 -0
  288. package/src/bongos/payout-gates.js +364 -0
  289. package/src/bongos/prelaunch-gate.js +7 -9
  290. package/src/bongos/prelaunch-switch.js +20 -0
  291. package/src/bongos/route-rank-check.js +3 -0
  292. package/src/bongos/routes/auth-google.js +246 -0
  293. package/src/bongos/routes/auth.js +38 -1
  294. package/src/bongos/routes/backup.js +33 -9
  295. package/src/bongos/routes/builders.js +57 -30
  296. package/src/bongos/routes/github.js +9 -0
  297. package/src/bongos/routes/me.js +18 -0
  298. package/src/bongos/routes/my-payouts.js +133 -0
  299. package/src/bongos/routes/payout-approvals.js +312 -0
  300. package/src/bongos/routes/payout-gates.js +179 -0
  301. package/src/bongos/routes.js +15 -1
  302. package/src/bongos/secret-box.js +18 -3
  303. package/src/bongos/serve-internal.js +47 -19
  304. package/src/bongos/sign-in-pages.js +138 -0
  305. package/src/bongos/task-needs.js +146 -0
  306. package/src/bongos-downloads.js +18 -1
  307. package/src/instance-config.js +28 -0
  308. package/src/module-api.js +14 -1
  309. package/tests/account_logins.mjs +326 -0
  310. package/tests/account_logins_db.mjs +161 -0
  311. package/tests/ai_studio_billing.mjs +253 -0
  312. package/tests/ai_studio_generate.mjs +282 -0
  313. package/tests/ai_studio_keys.mjs +159 -0
  314. package/tests/ai_studio_requests.mjs +319 -0
  315. package/tests/auth_backchannel_logout_route.mjs +201 -0
  316. package/tests/auto_satisfy_criteria.mjs +10 -1
  317. package/tests/autobongos_tweak_lane.mjs +144 -0
  318. package/tests/autobongos_tweak_pin_identity.mjs +137 -0
  319. package/tests/backfill_subagent_rewards.mjs +17 -1
  320. package/tests/backup_trigger_lock.mjs +119 -0
  321. package/tests/blockers_people.mjs +198 -0
  322. package/tests/bounded_rate_maps.mjs +184 -0
  323. package/tests/claimable_blocker_status.mjs +1 -1
  324. package/tests/claims_batch_routes.mjs +421 -0
  325. package/tests/config_read_cache.mjs +171 -0
  326. package/tests/copy_desk_page_editor.mjs +12 -0
  327. package/tests/cost_write_guard.mjs +200 -0
  328. package/tests/craft_achievements.mjs +33 -10
  329. package/tests/criterion_goal_attach.mjs +1 -1
  330. package/tests/db_backup_nightly.mjs +73 -1
  331. package/tests/deploy_divergence_line.mjs +7 -0
  332. package/tests/effective_visibility_predicate.mjs +12 -0
  333. package/tests/fitness.mjs +19 -0
  334. package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
  335. package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
  336. package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
  337. package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
  338. package/tests/github_one_trip.mjs +34 -0
  339. package/tests/github_repo_visibility.mjs +20 -0
  340. package/tests/goal_achievement.mjs +30 -1
  341. package/tests/goal_achievement_reward.mjs +205 -0
  342. package/tests/goal_sort_staleness.mjs +2 -1
  343. package/tests/goal_tier.mjs +11 -1
  344. package/tests/google_fonts_nonblocking.mjs +84 -0
  345. package/tests/google_sign_in.mjs +401 -0
  346. package/tests/government_board_expiry.mjs +94 -0
  347. package/tests/government_protected_surfaces.mjs +1 -1
  348. package/tests/government_routes.mjs +1 -1
  349. package/tests/hall_ai_studio.mjs +347 -0
  350. package/tests/hall_page_gate_map.mjs +3 -0
  351. package/tests/hall_studio_home.mjs +61 -2
  352. package/tests/home_attention_cards.mjs +1 -1
  353. package/tests/human_signal.mjs +141 -0
  354. package/tests/install_script_reader.mjs +51 -0
  355. package/tests/integration_fixture_ids.mjs +82 -0
  356. package/tests/invite_expiry_db.mjs +13 -1
  357. package/tests/lib_sh_resolution.mjs +0 -4
  358. package/tests/market_watch_data.mjs +244 -0
  359. package/tests/market_watch_data_db.mjs +308 -0
  360. package/tests/market_watch_module.mjs +114 -0
  361. package/tests/market_watch_run.mjs +503 -0
  362. package/tests/metrics.mjs +124 -0
  363. package/tests/migration_namespace.mjs +14 -0
  364. package/tests/module_api.mjs +2 -0
  365. package/tests/module_loader.mjs +1 -1
  366. package/tests/notes.mjs +182 -0
  367. package/tests/own_scope_guard.mjs +5 -0
  368. package/tests/page_tweak_marker_sql.mjs +76 -0
  369. package/tests/page_tweak_no_block.mjs +135 -0
  370. package/tests/page_tweak_notice.mjs +103 -0
  371. package/tests/payout_approvals.mjs +253 -0
  372. package/tests/payout_approvals_db.mjs +106 -0
  373. package/tests/payout_gates.mjs +322 -0
  374. package/tests/payout_gates_db.mjs +113 -0
  375. package/tests/planning_session_skill.mjs +13 -3
  376. package/tests/project_settings_page.mjs +29 -5
  377. package/tests/project_settings_spend_cap_route.mjs +235 -0
  378. package/tests/promotion_sweep.mjs +29 -1
  379. package/tests/provision.mjs +3 -2
  380. package/tests/provisioning_app_status.mjs +18 -0
  381. package/tests/provisioning_credential_preflight.mjs +53 -0
  382. package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
  383. package/tests/publish_reconciler_overlap.mjs +60 -0
  384. package/tests/rank_change_broadcast_port.mjs +151 -0
  385. package/tests/seed_tasks.mjs +176 -0
  386. package/tests/seed_verify_criteria.mjs +2 -6
  387. package/tests/site_classified_once.mjs +142 -0
  388. package/tests/speciality_db_shape.mjs +23 -0
  389. package/tests/speciality_session_skills.mjs +48 -2
  390. package/tests/strand_lease.mjs +249 -0
  391. package/tests/strand_watch.mjs +10 -0
  392. package/tests/task_criteria_routes.mjs +275 -0
  393. package/tests/task_criteria_unlink_db.mjs +123 -0
  394. package/tests/task_needs.mjs +233 -0
  395. package/tests/task_needs_db.mjs +110 -0
  396. package/tests/unit_runner_serial.mjs +266 -43
  397. package/tests/version_literals.mjs +5 -7
  398. package/tests/welcome_boot.mjs +16 -5
  399. package/tests/welcome_boot_browser.mjs +36 -4
  400. package/docs/recipes/bongos-cli-release.md +0 -66
  401. package/scripts/bongos/backfill-disciplines.js +0 -144
  402. package/scripts/bongos/backfill-session-earnings.js +0 -123
  403. package/scripts/bongos/backfill-session-types.js +0 -87
  404. package/scripts/bongos/build-bongos.js +0 -94
  405. package/scripts/bongos/cleanup-orphans.js +0 -74
  406. package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
  407. package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
  408. package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
  409. package/scripts/bongos/smoke-batch-claim.sh +0 -238
  410. package/scripts/bongos/smoke-batch-release.sh +0 -316
  411. package/scripts/bongos/smoke-claim-consistency.sh +0 -222
  412. package/scripts/bongos/smoke-dependencies.sh +0 -143
  413. package/scripts/gds/backfill-disciplines.js +0 -7
  414. package/scripts/gds/backfill-session-earnings.js +0 -7
  415. package/scripts/gds/backfill-session-types.js +0 -7
  416. package/scripts/gds/cleanup-orphans.js +0 -7
  417. package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
  418. package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
  419. package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
  420. package/scripts/gds/smoke-batch-claim.sh +0 -8
  421. package/scripts/gds/smoke-batch-release.sh +0 -8
  422. package/scripts/gds/smoke-claim-consistency.sh +0 -8
  423. package/scripts/gds/smoke-dependencies.sh +0 -8
  424. package/tests/smoke_dependencies_witness.mjs +0 -46
@@ -0,0 +1,198 @@
1
+ // tests/blockers_people.mjs — open blockers UNDER THE PEOPLE they wait on (task 1004999).
2
+ //
3
+ // WHAT IT GUARDS.
4
+ // * Every route the City carries now names the PEOPLE in its seat, not only the seat.
5
+ // * groupByPerson lists each open blocker under every person its seat resolves to,
6
+ // with the City's own rule (seatOf → resolveSeat): owner, a seated Lead, an
7
+ // unseated building falling to the owner, a board's members, and nobody.
8
+ // * The grouping and the City agree on every blocker — they are one rule.
9
+ // * Seating a different Lead moves the blocker with nothing on the blocker edited.
10
+ // * blockersByPerson reads the real contributed source, its asks and the seats.
11
+ // * The hall draws it: the viewer first as "You", the seat in words, "Nobody yet",
12
+ // escaping, and the City card's "waits on Lars, the owner".
13
+ //
14
+ // DB-free: the government db reads, the resolver and the asks read are stubbed.
15
+ // Run: node --test tests/blockers_people.mjs
16
+
17
+ import { test } from 'node:test';
18
+ import assert from 'node:assert/strict';
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { createRequire } from 'node:module';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const gov = (f) => path.join(ROOT, 'modules', 'government', f);
27
+ const routing = require(gov('routing.js'));
28
+ const waiting = require(gov('waiting.js'));
29
+ const leads = require(gov('leads.js'));
30
+ const people = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'blockers-by-person.js'));
31
+
32
+ const NOW = Date.parse('2026-10-29T12:00:00Z');
33
+ const ARCHONS = [{ id: 1, github_login: 'lars', display_name: 'Lars' }];
34
+ const owner = leads.ownerOf(ARCHONS);
35
+ const planningLead = { id: 9, building: 'planning', builder_id: 42, github_login: 'ana', display_name: 'Ana', removed_at: null };
36
+ const releaseLead = { id: 10, building: 'release', builder_id: 43, github_login: 'ben', display_name: 'Ben', removed_at: null };
37
+ const ask = (waitsOn) => ({ question: 'Allow it?', waits_on: waitsOn, if_nobody_answers: 'It stays stuck.' });
38
+ const blocker = (id, over = {}) => ({
39
+ id: `blockers:${id}`, source: 'blockers', building: 'planning', state: 'needs', title: `Blocker ${id}`,
40
+ opened_at: `2026-10-2${id % 10}T00:00:00Z`, deadline: null, href: `/blockers#blocker-${id}`, ref: { type: 'blocker', id: String(id) }, ...over,
41
+ });
42
+
43
+ test('a route names the people in its seat, not only the seat', () => {
44
+ const seats = { leads: [planningLead], owner, holders: { charter: [{ builder_id: 7, github_login: 'cy', display_name: 'Cy' }, { builder_id: 8, github_login: 'di' }] } };
45
+ const byOwner = routing.routeMatter(blocker(1, { ask: ask('owner') }), '1', seats, NOW);
46
+ assert.deepEqual(byOwner.people, [{ id: '1', login: 'lars', name: 'Lars' }]);
47
+ const byLead = routing.routeMatter(blocker(2), '1', seats, NOW);
48
+ assert.equal(byLead.seat, 'lead:planning');
49
+ assert.deepEqual(byLead.people, [{ id: '42', login: 'ana', name: 'Ana' }]);
50
+ const unseated = routing.routeMatter(blocker(3, { building: 'treasury' }), '1', seats, NOW);
51
+ assert.deepEqual([unseated.seat, unseated.fell_to_owner, unseated.people.map((p) => p.name)], ['owner', true, ['Lars']]);
52
+ const board = routing.routeMatter(blocker(4, { ask: ask('board:charter') }), '1', seats, NOW);
53
+ assert.deepEqual(board.people, [{ id: '7', login: 'cy', name: 'Cy' }, { id: '8', login: 'di', name: 'di' }]);
54
+ });
55
+
56
+ test('the name map is built once per request: routeMatter takes it, and routeAll gives the same routes', () => {
57
+ const seats = { leads: [planningLead], owner, holders: {} };
58
+ // a passed map is the one used — so routeAll can hand every matter the one it built
59
+ const dir = new Map([['42', { id: '42', login: 'x', name: 'From the map' }]]);
60
+ assert.deepEqual(routing.routeMatter(blocker(1), '1', seats, NOW, dir).people, [{ id: '42', login: 'x', name: 'From the map' }]);
61
+ const ms = [blocker(1), blocker(2, { ask: ask('owner') }), blocker(3, { building: 'treasury' })];
62
+ const one = ms.map((m) => routing.routeMatter(m, '42', seats, NOW));
63
+ const all = ms.map((m) => ({ ...m }));
64
+ routing.routeAll(all, '42', seats, NOW);
65
+ assert.deepEqual(all.map((m) => m.route), one);
66
+ const src = fs.readFileSync(gov('routing.js'), 'utf8');
67
+ const body = src.slice(src.indexOf('function routeAll'), src.indexOf('\n}', src.indexOf('function routeAll')));
68
+ assert.match(body, /const dir = directoryOf\(ctx\);[\s\S]*routeMatter\(m, viewerId, ctx, now, dir\)/, 'built once, outside the loop');
69
+ });
70
+
71
+ test('groupByPerson: each blocker under every person its seat resolves to; nobody is never dropped', () => {
72
+ const seats = { leads: [planningLead], owner, holders: { charter: [{ builder_id: 7, display_name: 'Cy' }, { builder_id: 42, display_name: 'Ana' }], rules: [] } };
73
+ const matters = [
74
+ blocker(1, { ask: ask('owner') }),
75
+ blocker(2), // ask-less → lead:planning → Ana
76
+ blocker(3, { building: 'treasury' }), // unseated → the owner
77
+ blocker(4, { ask: ask('board:charter') }), // two members
78
+ blocker(5, { ask: ask('board:rules') }), // nobody seated → falls to the owner
79
+ blocker(6, { state: 'settled', ask: ask('owner') }), // settled is not waiting
80
+ ];
81
+ const g = waiting.groupByPerson(matters, seats);
82
+ const ids = (name) => g.people.find((x) => x.person.name === name).items.map((i) => i.id);
83
+ assert.deepEqual(ids('Lars'), ['1', '3', '5']);
84
+ assert.deepEqual(ids('Ana'), ['2', '4']);
85
+ assert.deepEqual(ids('Cy'), ['4']);
86
+ assert.deepEqual(g.people.map((p) => [p.person.name, p.count]), [['Lars', 3], ['Ana', 2], ['Cy', 1]], 'most waiting first');
87
+ const two = g.people.find((x) => x.person.name === 'Ana').items.find((i) => i.id === '2');
88
+ assert.deepEqual([two.seat, two.as, two.asked, two.fell_to_owner], ['lead:planning', 'the Planning Lead', 'lead:planning', false]);
89
+ const three = g.people.find((x) => x.person.name === 'Lars').items.find((i) => i.id === '3');
90
+ assert.deepEqual([three.seat, three.asked, three.fell_to_owner], ['owner', 'lead:treasury', true]);
91
+ // no owner at all: an owner-seat blocker waits on nobody, and is listed there
92
+ const empty = waiting.groupByPerson([blocker(1, { ask: ask('owner') })], { leads: [], owner: leads.ownerOf([]), holders: {} });
93
+ assert.deepEqual([empty.people.length, empty.nobody.map((i) => i.id)], [0, ['1']]);
94
+ });
95
+
96
+ test('the grouping and the City agree on every blocker', () => {
97
+ const seats = { leads: [planningLead, releaseLead], owner, holders: { charter: [{ builder_id: 7, display_name: 'Cy' }] } };
98
+ const matters = [blocker(1), blocker(2, { ask: ask('owner') }), blocker(3, { ask: ask('lead:release') }), blocker(4, { ask: ask('board:charter') }), blocker(5, { building: 'people' }), blocker(6, { ask: ask('committee:release.ops') })];
99
+ const g = waiting.groupByPerson(matters, seats);
100
+ for (const m of matters) {
101
+ const city = routing.routeMatter(m, null, seats, NOW).people.map((p) => p.id).sort();
102
+ const here = g.people.filter((x) => x.items.some((i) => i.matter_id === m.id)).map((x) => x.person.id).sort();
103
+ assert.deepEqual(here, city, `${m.id}: the same people on both`);
104
+ }
105
+ });
106
+
107
+ test('seating a different Lead moves the blocker; nothing on the blocker changes', () => {
108
+ const m = [blocker(1)];
109
+ const before = waiting.groupByPerson(m, { leads: [planningLead], owner, holders: {} });
110
+ const after = waiting.groupByPerson(m, { leads: [{ ...planningLead, builder_id: 77, github_login: 'eve', display_name: 'Eve' }], owner, holders: {} });
111
+ assert.deepEqual(before.people.map((p) => p.person.name), ['Ana']);
112
+ assert.deepEqual(after.people.map((p) => p.person.name), ['Eve']);
113
+ const removed = waiting.groupByPerson(m, { leads: [{ ...planningLead, removed_at: '2026-10-28T00:00:00Z' }], owner, holders: {} });
114
+ assert.deepEqual(removed.people.map((p) => p.person.name), ['Lars'], 'a removed Lead hands it back to the owner');
115
+ });
116
+
117
+ test('forViewer: the viewer\'s own group, or an empty one', () => {
118
+ const g = waiting.groupByPerson([blocker(1), blocker(2, { ask: ask('owner') })], { leads: [planningLead], owner, holders: {} });
119
+ assert.deepEqual(waiting.forViewer(g, 42).items.map((i) => i.id), ['1']);
120
+ assert.deepEqual(waiting.forViewer(g, '1').items.map((i) => i.id), ['2']);
121
+ assert.deepEqual([waiting.forViewer(g, 5).count, waiting.forViewer(g, 5).items], [0, []]);
122
+ });
123
+
124
+ test('blockersByPerson reads the contributed blockers, their asks and the seats', async () => {
125
+ const docket = require(gov('docket.js'));
126
+ const resolver = require(gov('resolver.js'));
127
+ const asksDb = require(gov('asks-db.js'));
128
+ const db = require(gov('db.js'));
129
+ const real = { perms: resolver.resolveBuilderPermissions, asks: asksDb.asksFor, leads: db.listLeads, archons: db.listActiveArchons };
130
+ resolver.resolveBuilderPermissions = async () => ['board.vote.cast'];
131
+ asksDb.asksFor = async (ids) => new Map(ids.filter((id) => id === 'blockers:7').map((id) => [id, ask('owner')]));
132
+ db.listLeads = async () => [planningLead];
133
+ db.listActiveArchons = async () => ARCHONS;
134
+ const rows = [{ id: 7, title: 'Token needs the workflow scope', created_at: '2026-10-29T10:00:00Z' }, { id: 8, title: 'Pick a host', created_at: '2026-10-28T10:00:00Z' }];
135
+ const contributions = { sources: new Map([['blockers', { source: 'blockers', building: 'planning', gate: { anyBuilder: true }, read: async () => rows.map((b) => ({ id: b.id, title: b.title, state: 'needs', opened_at: b.created_at, href: `/blockers#blocker-${b.id}`, ref: { type: 'blocker', id: b.id } })) }]]) };
136
+ try {
137
+ const r = await docket.blockersByPerson({ id: 42, rank: 'metic' }, { contributions, now: NOW });
138
+ assert.equal(r.available, true);
139
+ assert.deepEqual(r.people.map((p) => [p.person.name, p.items.map((i) => i.id)]), [['Ana', ['8']], ['Lars', ['7']]]);
140
+ assert.deepEqual(r.yours.items.map((i) => i.id), ['8'], 'Ana sees her own');
141
+ const off = await docket.blockersByPerson({ id: 42 }, { contributions: { sources: new Map() }, now: NOW });
142
+ assert.deepEqual([off.available, off.reason, off.people], [false, 'off', []], 'no source is said, not shown as empty');
143
+ db.listLeads = async () => { throw new Error('db down'); };
144
+ const down = await docket.blockersByPerson({ id: 42 }, { contributions, now: NOW });
145
+ assert.deepEqual([down.available, down.reason], [false, 'unavailable'], 'a failed seats read groups nothing rather than guessing');
146
+ } finally {
147
+ resolver.resolveBuilderPermissions = real.perms; asksDb.asksFor = real.asks; db.listLeads = real.leads; db.listActiveArchons = real.archons;
148
+ }
149
+ });
150
+
151
+ const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
152
+ const fmtDate = (iso) => iso.slice(0, 10);
153
+
154
+ test('the Blockers page: the viewer first as "You", the seat in words, "Nobody yet", escaped', () => {
155
+ const g = waiting.groupByPerson([
156
+ blocker(1, { ask: ask('owner'), title: '<b>Token</b>' }),
157
+ blocker(2), blocker(3, { building: 'treasury' }),
158
+ blocker(4, { ask: ask('board:rules') }),
159
+ ], { leads: [planningLead], owner: leads.ownerOf([]), holders: { rules: [] } });
160
+ const html = people.peopleHtml({ available: true, ...g }, { esc, fmtDate, viewerId: 42, blockerHref: (id) => `#/blocker/${id}` });
161
+ assert.ok(html.indexOf('>You<') < html.indexOf('>Nobody yet<'), 'the viewer first');
162
+ assert.match(html, /as the Planning Lead · filed 2026-10-22/);
163
+ assert.match(html, /href="#\/blocker\/2"/);
164
+ assert.match(html, /&lt;b&gt;Token&lt;\/b&gt;/, 'a title is escaped');
165
+ assert.ok(!html.includes('<b>Token'), 'never raw');
166
+ assert.equal(people.peopleHtml({ available: false, people: [], nobody: [] }, { esc, fmtDate }), '', 'unavailable draws nothing');
167
+ assert.equal(people.peopleHtml({ available: true, people: [], nobody: [] }, { esc, fmtDate }), '', 'nothing open draws nothing');
168
+ assert.equal(people.asWords({ as: 'the owner', asked: 'lead:treasury', fell_to_owner: true }), 'as the owner, since Treasury has no Lead');
169
+ assert.equal(people.asWords({ as: 'the owner', asked: 'owner', fell_to_owner: false }), 'as the owner');
170
+ });
171
+
172
+ test('Collab\'s block draws only the viewer\'s own, and nothing when nothing waits', () => {
173
+ const mine = { available: true, count: 1, items: [{ id: '9', title: 'Pick a host', as: 'the Planning Lead', asked: 'lead:planning', opened_at: '2026-10-28T00:00:00Z' }] };
174
+ const html = people.mineHtml(mine, { esc, fmtDate, blockerHref: (id) => `#/blocker/${id}` });
175
+ assert.match(html, /Pick a host/);
176
+ assert.match(html, /as the Planning Lead/);
177
+ assert.equal(people.mineHtml({ available: true, count: 0, items: [] }, { esc, fmtDate }), '');
178
+ assert.equal(people.mineHtml(null, { esc, fmtDate }), '');
179
+ });
180
+
181
+ test('the pages carry the sections and load the script', () => {
182
+ const pub = (f) => fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', f), 'utf8');
183
+ assert.match(pub('blockers.html'), /id="blockers-people-scroll"[^>]*hidden/);
184
+ assert.match(pub('blockers.html'), /<script src="\/builders\/blockers-by-person.js"><\/script>/);
185
+ assert.match(pub('collab.html'), /id="cb-blockers-scroll"[^>]*hidden/);
186
+ assert.match(pub('collab.html'), /<script src="\/builders\/blockers-by-person.js"><\/script>/);
187
+ });
188
+
189
+ test('the City card says who: "waits on Lars, the owner"', () => {
190
+ const deck = require(path.join(ROOT, 'modules', 'hall-ui', 'public', 'street-deck.js'));
191
+ const who = deck.whoWords;
192
+ assert.equal(who({ label: 'the owner', people: [{ name: 'Lars' }] }), 'Lars, the owner');
193
+ assert.equal(who({ label: 'the board', people: [{ name: 'Cy' }, { login: 'di' }] }), 'Cy and di, the board');
194
+ assert.equal(who({ label: 'the board', people: [{ name: 'A' }, { name: 'B' }, { name: 'C' }] }), 'A and 2 others, the board');
195
+ assert.equal(who({ label: 'the owner', people: [] }), 'the owner', 'nobody named: the seat alone');
196
+ const foot = deck.footOf({ waits_on: { real: true, label: 'the Planning Lead', people: [{ name: 'Ana' }] } }, '');
197
+ assert.equal(foot.rest, 'It waits on Ana, the Planning Lead.');
198
+ });
@@ -0,0 +1,184 @@
1
+ // tests/bounded_rate_maps.mjs
2
+ //
3
+ // Task 1004964 (2026-10-28 server audit, item 10): every in-memory rate-limit and
4
+ // login map has a HARD size cap with oldest-first eviction, not only a time-based
5
+ // sweep that runs every five minutes. Each case floods a map past its cap through
6
+ // the real middleware (or the real tracker) and shows the size stays at the cap,
7
+ // the newest entries survive, and the oldest are the ones evicted.
8
+ // deviceFlows is also pruned on an unref'd timer, not only when a new flow starts.
9
+ //
10
+ // Hermetic: no DB, no network. Run: node tests/bounded_rate_maps.mjs
11
+
12
+ import { strict as assert } from 'node:assert';
13
+ import { createRequire } from 'node:module';
14
+ import { makeRunner } from './helpers.mjs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const { test, summary } = makeRunner();
18
+
19
+ const fakeRes = () => ({ set() { return this; }, fail() { return this; } });
20
+ const noop = () => {};
21
+
22
+ // Drive `mw` with `n` distinct callers; `reqFor(i)` builds the request for caller i.
23
+ function flood(mw, n, reqFor) {
24
+ for (let i = 0; i < n; i++) mw(reqFor(i), fakeRes(), noop);
25
+ }
26
+ const byIp = (method = 'GET') => (i) => ({ method, ip: `ip-${i}`, headers: {}, path: '/' });
27
+ const byBuilder = (i) => ({ method: 'GET', builder: { id: i }, headers: {} });
28
+
29
+ // The one assertion every case makes: capped size, newest kept, oldest gone.
30
+ function assertCapped(map, cap, n, keyFor) {
31
+ assert.ok(n > cap, 'the flood must overrun the cap');
32
+ assert.equal(map.size, cap, `map holds ${map.size}, cap is ${cap}`);
33
+ assert.ok(map.has(keyFor(n - 1)), 'the newest caller survives');
34
+ assert.ok(map.has(keyFor(n - cap)), 'the oldest still-in-cap caller survives');
35
+ assert.ok(!map.has(keyFor(n - cap - 1)), 'the caller just past the cap was evicted');
36
+ assert.ok(!map.has(keyFor(0)), 'the first caller was evicted');
37
+ }
38
+
39
+ const rl = require('../src/bongos/middleware/rate-limit.js');
40
+ const CAP = rl.MAX_TRACKED_KEYS;
41
+ const N = CAP + 25;
42
+
43
+ await test('the shared cap is about 50k', () => {
44
+ assert.equal(CAP, 50000);
45
+ });
46
+
47
+ await test('rate-limit.js per-IP write bucket map stays capped', () => {
48
+ const m = rl._bucketMaps.ipBuckets;
49
+ m.clear();
50
+ flood(rl.rateLimitMiddleware, N, byIp('POST'));
51
+ assertCapped(m, CAP, N, (i) => `ip-${i}`);
52
+ m.clear();
53
+ });
54
+
55
+ await test('rate-limit.js per-session write bucket map stays capped', () => {
56
+ const m = rl._bucketMaps.sessionBuckets;
57
+ m.clear();
58
+ // A fresh IP per request: the per-IP check runs first and would 429 one IP early.
59
+ rl._bucketMaps.ipBuckets.clear();
60
+ flood(rl.rateLimitMiddleware, N, (i) => ({
61
+ method: 'POST', ip: `s-${i}`, headers: { authorization: `Bearer tok-${i}` },
62
+ }));
63
+ assertCapped(m, CAP, N, (i) => `b:tok-${i}`);
64
+ m.clear();
65
+ rl._bucketMaps.ipBuckets.clear();
66
+ });
67
+
68
+ await test('rate-limit.js public-read bucket map stays capped', () => {
69
+ const m = rl._bucketMaps.publicReadBuckets;
70
+ m.clear();
71
+ flood(rl.publicReadRateLimitMiddleware, N, byIp());
72
+ assertCapped(m, CAP, N, (i) => `ip-${i}`);
73
+ m.clear();
74
+ });
75
+
76
+ await test('rate-limit.js account-existence bucket map stays capped', () => {
77
+ const m = rl._bucketMaps.accountExistenceBuckets;
78
+ m.clear();
79
+ flood(rl.accountExistenceReadRateLimit, N, byIp());
80
+ assertCapped(m, CAP, N, (i) => `ip-${i}`);
81
+ m.clear();
82
+ });
83
+
84
+ await test('rate-limit.js public-project-feed bucket map stays capped', () => {
85
+ const m = rl._bucketMaps.projectFeedBuckets;
86
+ m.clear();
87
+ flood(rl.publicProjectFeedRateLimit, N, byIp());
88
+ assertCapped(m, CAP, N, (i) => `ip-${i}`);
89
+ m.clear();
90
+ });
91
+
92
+ await test('rate-limit.js default-read map keeps its tighter 10k cap', () => {
93
+ const m = rl._bucketMaps.defaultReadBuckets;
94
+ m.clear();
95
+ const mw = rl.defaultReadRateLimit({ stack: [] });
96
+ const cap = rl.DEFAULT_READ_MAX_TRACKED_IPS;
97
+ flood(mw, cap + 25, byIp());
98
+ assertCapped(m, cap, cap + 25, (i) => `ip-${i}`);
99
+ m.clear();
100
+ });
101
+
102
+ await test('memory sync + read bucket maps stay capped', () => {
103
+ const { _internals: mem } = require('../modules/memory/routes/memory.js');
104
+ assert.equal(mem.MAX_TRACKED_KEYS, CAP);
105
+ for (const [mw, map] of [[mem.syncRateLimit, mem.syncBuckets], [mem.readRateLimit, mem.readBuckets]]) {
106
+ map.clear();
107
+ flood(mw, N, byBuilder);
108
+ assertCapped(map, CAP, N, (i) => String(i));
109
+ map.clear();
110
+ }
111
+ });
112
+
113
+ await test('memory search bucket map stays capped', () => {
114
+ const { _internals: s } = require('../modules/memory/routes/search.js');
115
+ assert.equal(s.MAX_TRACKED_KEYS, CAP);
116
+ s.buckets.clear();
117
+ flood(s.searchRateLimit, N, byBuilder);
118
+ assertCapped(s.buckets, CAP, N, (i) => String(i));
119
+ s.buckets.clear();
120
+ });
121
+
122
+ await test('SSO applicant-read bucket map stays capped', () => {
123
+ const { _internals: sso } = require('../modules/platform-identity/routes/sso.js');
124
+ assert.equal(sso.APPLICANT_MAX_TRACKED_IPS, CAP);
125
+ sso.applicantReadBuckets.clear();
126
+ flood(sso.applicantReadRateLimit, N, byIp());
127
+ assertCapped(sso.applicantReadBuckets, CAP, N, (i) => `ip-${i}`);
128
+ sso.applicantReadBuckets.clear();
129
+ });
130
+
131
+ await test('public-profile bucket map stays capped', () => {
132
+ const { _internals: pp } = require('../modules/platform-identity/routes/public-profile.js');
133
+ assert.equal(pp.MAX_TRACKED_IPS, CAP);
134
+ pp.buckets.clear();
135
+ flood(pp.profileReadRateLimit, N, byIp());
136
+ assertCapped(pp.buckets, CAP, N, (i) => `ip-${i}`);
137
+ pp.buckets.clear();
138
+ });
139
+
140
+ await test('deviceFlows stays capped with oldest-first eviction', () => {
141
+ const ac = require('../src/bongos/auth-config.js');
142
+ assert.equal(ac.DEVICE_FLOW_MAX, CAP);
143
+ ac.deviceFlows.clear();
144
+ const live = () => ({ interval: 5, expires_at: Date.now() + 15 * 60_000, last_polled_at: 0 });
145
+ for (let i = 0; i < N; i++) ac.trackDeviceFlow(`dc-${i}`, live());
146
+ assertCapped(ac.deviceFlows, CAP, N, (i) => `dc-${i}`);
147
+ ac.deviceFlows.clear();
148
+ });
149
+
150
+ await test('deviceFlows is pruned on an unref\'d timer, not only on a new flow', () => {
151
+ // Load auth-config fresh with setInterval spied, to see the timer it arms.
152
+ const realSetInterval = globalThis.setInterval;
153
+ const armed = [];
154
+ globalThis.setInterval = (fn, ms, ...rest) => {
155
+ const t = realSetInterval(fn, ms, ...rest);
156
+ const unref = t.unref.bind(t);
157
+ const rec = { fn, ms, unrefd: false, t };
158
+ t.unref = () => { rec.unrefd = true; return unref(); };
159
+ armed.push(rec);
160
+ return t;
161
+ };
162
+ const key = require.resolve('../src/bongos/auth-config.js');
163
+ const cached = require.cache[key];
164
+ try {
165
+ delete require.cache[key];
166
+ const fresh = require('../src/bongos/auth-config.js');
167
+ const timer = armed.find((r) => r.fn === fresh.pruneDeviceFlows);
168
+ assert.ok(timer, 'a prune timer is armed at load');
169
+ assert.equal(timer.ms, fresh.DEVICE_FLOW_PRUNE_MS);
170
+ assert.ok(timer.ms >= 60_000, 'the prune timer is not a per-second wake');
171
+ assert.ok(timer.unrefd, 'the prune timer is unref\'d');
172
+ // And what it runs drops expired flows while keeping live ones, with no new flow started.
173
+ fresh.deviceFlows.set('old', { expires_at: Date.now() - 1 });
174
+ fresh.deviceFlows.set('new', { expires_at: Date.now() + 60_000 });
175
+ timer.fn();
176
+ assert.deepEqual([...fresh.deviceFlows.keys()], ['new']);
177
+ } finally {
178
+ globalThis.setInterval = realSetInterval;
179
+ for (const r of armed) clearInterval(r.t);
180
+ require.cache[key] = cached;
181
+ }
182
+ });
183
+
184
+ summary();
@@ -35,7 +35,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
35
35
  const repoRoot = join(__dirname, '..');
36
36
  const read = (...p) => readFileSync(join(repoRoot, ...p), 'utf8');
37
37
 
38
- const MIG = 'core_264_claimable_respects_dependencies.sql';
38
+ const MIG = 'core_285_task_needs.sql';
39
39
  const mig = read('migrations', MIG);
40
40
  // Comments carry the rationale; strip them when asserting on the executable body
41
41
  // so a claim in prose can never stand in for the real SQL.