@bongos/core 1.18.0 → 1.18.18

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 (553) hide show
  1. package/.bongos-core.json +1740 -640
  2. package/.claude/skills/builder-box/SKILL.md +6 -0
  3. package/.claude/skills/builder-sequence/SKILL.md +97 -0
  4. package/.claude/skills/builder-ship/SKILL.md +8 -2
  5. package/.claude/skills/builder-start/SKILL.md +11 -0
  6. package/.claude/skills/builder-sync/SKILL.md +41 -0
  7. package/.claude/skills/fix-task/SKILL.md +64 -0
  8. package/.claude/skills/goal-create/SKILL.md +158 -0
  9. package/.claude/skills/goal-review/SKILL.md +3 -1
  10. package/.claude/skills/idea-triage/SKILL.md +14 -8
  11. package/.claude/skills/merge-mode/SKILL.md +2 -2
  12. package/.claude/skills/new-project/SKILL.md +1 -0
  13. package/.claude/skills/planning-session/SKILL.md +45 -18
  14. package/.env.local.example +4 -3
  15. package/README.md +5 -3
  16. package/bin/bongos.js +1 -0
  17. package/clients/bongos-client/README.md +2 -2
  18. package/clients/bongos-client/bongos-client.global.js +82 -18
  19. package/clients/bongos-client/index.cjs +82 -18
  20. package/clients/bongos-client/index.d.ts +131 -29
  21. package/clients/bongos-client/index.mjs +82 -18
  22. package/config/branding.neutral.json +11 -2
  23. package/config/governance.neutral.json +21 -0
  24. package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
  25. package/docs/adr/0090-metic-task-authoring.md +1 -1
  26. package/docs/adr/0142-project-context-is-prose-only.md +41 -0
  27. package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
  28. package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
  29. package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
  30. package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
  31. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
  32. package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
  33. package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
  34. package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
  35. package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
  36. package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
  37. package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
  38. package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
  39. package/docs/adr/0152-metic-task-abandonment.md +67 -0
  40. package/docs/adr/0153-sequential-chain-runner.md +51 -0
  41. package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
  42. package/docs/adr/README.md +15 -2
  43. package/docs/api/openapi.json +2648 -483
  44. package/docs/api-reference.md +73 -13
  45. package/docs/architecture.md +4 -2
  46. package/docs/branding-contract.md +3 -1
  47. package/docs/canonical-permissions.md +21 -8
  48. package/docs/file-map.md +15 -7
  49. package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
  50. package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
  51. package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
  52. package/docs/onboarding/diagrams/assertions.json +4 -7
  53. package/docs/onboarding/drachmae-and-karma.md +3 -3
  54. package/docs/onboarding/primer.md +17 -16
  55. package/docs/onboarding/slash-commands.md +124 -0
  56. package/docs/recipes/builder-box-lifecycle.md +23 -0
  57. package/docs/recipes/devbox-app-release.md +14 -4
  58. package/docs/recipes/instance-first-run.md +16 -0
  59. package/docs/recipes/local-dev.md +81 -0
  60. package/docs/recipes/ops-gotchas.md +19 -1
  61. package/docs/recipes/overnight-builder.md +1 -1
  62. package/docs/recipes/packaging-the-core.md +13 -0
  63. package/docs/recipes/parallel-goal-waves.md +174 -0
  64. package/docs/recipes/ship-and-regrade.md +2 -2
  65. package/docs/recipes/standalone-live-docs.md +48 -0
  66. package/docs/recipes/upgrading-the-core.md +5 -2
  67. package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
  68. package/migrations/003_pms.sql +6 -7
  69. package/migrations/006_pms_v2.sql +3 -31
  70. package/migrations/039_newcomer_friendly_column.sql +5 -0
  71. package/migrations/102_remove_box_first_provision_approval.sql +4 -0
  72. package/migrations/core_192_github_oauth_tokens.sql +33 -0
  73. package/migrations/core_193_task_needs_rebase.sql +28 -0
  74. package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
  75. package/migrations/core_195_idea_triage_provenance.sql +57 -0
  76. package/migrations/core_196_session_reward_topup.sql +69 -0
  77. package/modules/autonomy/pollers/routine-timers.js +9 -8
  78. package/modules/builder-settings/wandering-prefs.js +61 -18
  79. package/modules/dev-box/app/README.md +73 -0
  80. package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
  81. package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
  82. package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
  83. package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
  84. package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
  85. package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
  86. package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
  87. package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
  88. package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
  89. package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
  90. package/modules/dev-box/app/assets/tray/win-off.png +0 -0
  91. package/modules/dev-box/app/assets/tray/win-on.png +0 -0
  92. package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
  93. package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
  94. package/modules/dev-box/app/build-resources/icon.png +0 -0
  95. package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
  96. package/modules/dev-box/app/electron-builder.yml +95 -0
  97. package/modules/dev-box/app/package-lock.json +5291 -0
  98. package/modules/dev-box/app/package.json +30 -0
  99. package/modules/dev-box/app/renderer/app.js +319 -0
  100. package/modules/dev-box/app/renderer/index.html +45 -0
  101. package/modules/dev-box/app/renderer/styles.css +246 -0
  102. package/modules/dev-box/app/renderer/theme.generated.css +16 -0
  103. package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
  104. package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
  105. package/modules/dev-box/app/scripts/gen-brand.js +138 -0
  106. package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
  107. package/modules/dev-box/app/scripts/gen-icons.js +286 -0
  108. package/modules/dev-box/app/scripts/vendor-client.js +23 -0
  109. package/modules/dev-box/app/src/box-controller.js +287 -0
  110. package/modules/dev-box/app/src/brand.generated.json +13 -0
  111. package/modules/dev-box/app/src/brand.js +16 -0
  112. package/modules/dev-box/app/src/config.js +19 -0
  113. package/modules/dev-box/app/src/gds-client.js +90 -0
  114. package/modules/dev-box/app/src/main.js +610 -0
  115. package/modules/dev-box/app/src/pairing.js +57 -0
  116. package/modules/dev-box/app/src/preload.js +29 -0
  117. package/modules/dev-box/app/src/session-store.js +58 -0
  118. package/modules/dev-box/app/src/ssh-setup.js +569 -0
  119. package/modules/dev-box/app/src/task-picker.js +70 -0
  120. package/modules/dev-box/app/src/tray.js +99 -0
  121. package/modules/dev-box/app/src/updater.js +55 -0
  122. package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
  123. package/modules/dev-box/app/test/unit.mjs +526 -0
  124. package/modules/dev-box/box-access.js +121 -4
  125. package/modules/dev-box/boxes.js +20 -0
  126. package/modules/dev-box/module.json +1 -1
  127. package/modules/dev-box/routes/box.js +87 -3
  128. package/modules/economy/credits.js +116 -25
  129. package/modules/economy/reward.js +8 -0
  130. package/modules/economy/streak.js +2 -1
  131. package/modules/governance/CLAUDE.md +15 -0
  132. package/modules/governance/catalog.js +419 -0
  133. package/modules/governance/config.js +174 -0
  134. package/modules/governance/db.js +125 -0
  135. package/modules/governance/migrations/governance_001_tables.sql +81 -0
  136. package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
  137. package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
  138. package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
  139. package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
  140. package/modules/governance/module.json +10 -0
  141. package/modules/governance/protected-surfaces.js +88 -0
  142. package/modules/governance/protected-surfaces.json +80 -0
  143. package/modules/governance/rank-roles.js +83 -0
  144. package/modules/governance/resolver.js +221 -0
  145. package/modules/governance/routes/governance.js +89 -0
  146. package/modules/governance/routes/role-sync.js +49 -0
  147. package/modules/governance/session-scopes.js +127 -0
  148. package/modules/governance/session-scopes.json +38 -0
  149. package/modules/hall-ui/public/atlas.css +117 -0
  150. package/modules/hall-ui/public/atlas.html +3 -114
  151. package/modules/hall-ui/public/brand-apply.js +34 -6
  152. package/modules/hall-ui/public/builders.js +187 -219
  153. package/modules/hall-ui/public/diagrams.html +2 -0
  154. package/modules/hall-ui/public/dom-utils.js +27 -0
  155. package/modules/hall-ui/public/drachmae.html +2 -0
  156. package/modules/hall-ui/public/drachmae.js +54 -1
  157. package/modules/hall-ui/public/gate.css +98 -0
  158. package/modules/hall-ui/public/gate.html +3 -95
  159. package/modules/hall-ui/public/goal-inbox.js +4 -1
  160. package/modules/hall-ui/public/goals-page.js +626 -114
  161. package/modules/hall-ui/public/goals.css +79 -1
  162. package/modules/hall-ui/public/goals.html +38 -11
  163. package/modules/hall-ui/public/hall-widgets.js +62 -52
  164. package/modules/hall-ui/public/harbor.css +123 -0
  165. package/modules/hall-ui/public/harbor.html +3 -120
  166. package/modules/hall-ui/public/index.html +11 -4
  167. package/modules/hall-ui/public/instances-panel.js +4 -1
  168. package/modules/hall-ui/public/modules.css +63 -0
  169. package/modules/hall-ui/public/modules.html +3 -60
  170. package/modules/hall-ui/public/my-projects.html +78 -0
  171. package/modules/hall-ui/public/my-projects.js +142 -0
  172. package/modules/hall-ui/public/new-project-wizard.js +7 -3
  173. package/modules/hall-ui/public/not-ready.html +2 -0
  174. package/modules/hall-ui/public/pair.css +73 -0
  175. package/modules/hall-ui/public/pair.html +3 -70
  176. package/modules/hall-ui/public/palette.js +302 -0
  177. package/modules/hall-ui/public/primer.css +13 -0
  178. package/modules/hall-ui/public/primer.html +2 -0
  179. package/modules/hall-ui/public/profile.html +2 -0
  180. package/modules/hall-ui/public/ranks.html +2 -0
  181. package/modules/hall-ui/public/roadmap.css +179 -0
  182. package/modules/hall-ui/public/roadmap.html +9 -172
  183. package/modules/hall-ui/public/roadmap.js +329 -316
  184. package/modules/hall-ui/public/scouting.html +45 -0
  185. package/modules/hall-ui/public/scouting.js +73 -0
  186. package/modules/hall-ui/public/sessions.css +16 -0
  187. package/modules/hall-ui/public/sessions.html +3 -13
  188. package/modules/hall-ui/public/settings.html +2 -0
  189. package/modules/hall-ui/public/settings.js +4 -4
  190. package/modules/hall-ui/public/shell.js +143 -18
  191. package/modules/hall-ui/public/style.css +231 -39
  192. package/modules/hall-ui/public/task-detail.js +347 -26
  193. package/modules/hall-ui/public/task.html +6 -1
  194. package/modules/hall-ui/public/watch.html +2 -0
  195. package/modules/hall-ui/public/work.html +8 -6
  196. package/modules/hall-ui/public/work.js +1 -1
  197. package/modules/ideas/inbox.js +61 -11
  198. package/modules/ideas/routes/inbox.js +23 -0
  199. package/modules/lifecycle/claim-eligibility.js +72 -0
  200. package/modules/lifecycle/db.js +519 -139
  201. package/modules/lifecycle/done-when.js +18 -3
  202. package/modules/lifecycle/github-push.js +19 -2
  203. package/modules/lifecycle/goal-advisory.js +114 -0
  204. package/modules/lifecycle/goal-authz.js +62 -0
  205. package/modules/lifecycle/lifecycle.js +9 -0
  206. package/modules/lifecycle/publish-reconciler.js +109 -6
  207. package/modules/lifecycle/routes/claims.js +68 -23
  208. package/modules/lifecycle/routes/done-when.js +110 -15
  209. package/modules/lifecycle/routes/goals.js +8 -38
  210. package/modules/lifecycle/routes/tasks.js +38 -6
  211. package/modules/lifecycle/ship-card.js +6 -0
  212. package/modules/onboarding/onboarding-state.js +1 -1
  213. package/modules/platform-identity/CLAUDE.md +15 -0
  214. package/modules/platform-identity/hub-device.js +147 -0
  215. package/modules/platform-identity/hub-keys.js +151 -0
  216. package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
  217. package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
  218. package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
  219. package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
  220. package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
  221. package/modules/platform-identity/module.json +18 -0
  222. package/modules/platform-identity/platform-identity.js +531 -0
  223. package/modules/platform-identity/pollers/code-sweep.js +43 -0
  224. package/modules/platform-identity/routes/my-projects.js +140 -0
  225. package/modules/platform-identity/routes/projects.js +107 -0
  226. package/modules/platform-identity/routes/scouting.js +35 -0
  227. package/modules/platform-identity/routes/sso.js +402 -0
  228. package/modules/platform-identity/tests/platform-identity.mjs +388 -0
  229. package/modules/platform-identity/ui/.gitkeep +0 -0
  230. package/modules/security/routes/reports.js +10 -0
  231. package/modules/sessions/db.js +166 -21
  232. package/modules/sessions/module.json +1 -1
  233. package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
  234. package/modules/sessions/routes/sessions.js +33 -0
  235. package/modules/status-ui/public/explore.html +54 -0
  236. package/modules/status-ui/public/explore.js +67 -0
  237. package/package-lock.json +2 -2
  238. package/package.json +1 -1
  239. package/public-landing/assets/anim/idle/frame_00.webp +0 -0
  240. package/public-landing/assets/anim/idle/frame_01.webp +0 -0
  241. package/public-landing/assets/anim/idle/frame_02.webp +0 -0
  242. package/public-landing/assets/anim/idle/frame_03.webp +0 -0
  243. package/public-landing/assets/anim/idle/frame_04.webp +0 -0
  244. package/public-landing/assets/anim/idle/frame_05.webp +0 -0
  245. package/public-landing/assets/anim/idle/frame_06.webp +0 -0
  246. package/public-landing/assets/anim/idle/frame_07.webp +0 -0
  247. package/public-landing/assets/anim/idle/frame_08.webp +0 -0
  248. package/public-landing/assets/anim/idle/frame_09.webp +0 -0
  249. package/public-landing/assets/anim/idle/frame_10.webp +0 -0
  250. package/public-landing/assets/anim/idle/frame_11.webp +0 -0
  251. package/public-landing/assets/anim/idle/frame_12.webp +0 -0
  252. package/public-landing/assets/anim/idle/frame_13.webp +0 -0
  253. package/public-landing/assets/anim/idle/frame_14.webp +0 -0
  254. package/public-landing/assets/anim/idle/frame_15.webp +0 -0
  255. package/public-landing/assets/anim/idle/frame_16.webp +0 -0
  256. package/public-landing/assets/anim/idle/frame_17.webp +0 -0
  257. package/public-landing/assets/anim/idle/frame_18.webp +0 -0
  258. package/public-landing/assets/anim/idle/frame_19.webp +0 -0
  259. package/public-landing/assets/anim/idle/frame_20.webp +0 -0
  260. package/public-landing/assets/anim/idle/frame_21.webp +0 -0
  261. package/public-landing/assets/anim/idle/frame_22.webp +0 -0
  262. package/public-landing/assets/anim/idle/frame_23.webp +0 -0
  263. package/public-landing/assets/anim/idle/frame_24.webp +0 -0
  264. package/public-landing/assets/anim/idle/frame_25.webp +0 -0
  265. package/public-landing/assets/anim/idle/frame_26.webp +0 -0
  266. package/public-landing/assets/anim/idle/frame_27.webp +0 -0
  267. package/public-landing/assets/anim/idle/frame_28.webp +0 -0
  268. package/public-landing/assets/anim/idle/frame_29.webp +0 -0
  269. package/public-landing/assets/anim/idle/frame_30.webp +0 -0
  270. package/public-landing/assets/anim/idle/frame_31.webp +0 -0
  271. package/public-landing/assets/anim/idle/frame_32.webp +0 -0
  272. package/public-landing/assets/anim/idle/frame_33.webp +0 -0
  273. package/public-landing/assets/anim/idle/frame_34.webp +0 -0
  274. package/public-landing/assets/anim/idle/frame_35.webp +0 -0
  275. package/public-landing/assets/anim/idle/frame_36.webp +0 -0
  276. package/public-landing/assets/anim/idle/frame_37.webp +0 -0
  277. package/public-landing/assets/anim/idle/frame_38.webp +0 -0
  278. package/public-landing/assets/anim/idle/frame_39.webp +0 -0
  279. package/public-landing/assets/anim/idle/frame_40.webp +0 -0
  280. package/public-landing/assets/anim/idle/frame_41.webp +0 -0
  281. package/public-landing/assets/anim/idle/frame_42.webp +0 -0
  282. package/public-landing/assets/anim/idle/frame_43.webp +0 -0
  283. package/public-landing/create.html +252 -62
  284. package/public-landing/gate/README.md +137 -0
  285. package/public-landing/gate/dispmaps/bar.png +0 -0
  286. package/public-landing/gate/dispmaps/flat.png +0 -0
  287. package/public-landing/gate/dispmaps/key.png +0 -0
  288. package/public-landing/gate/index.html +587 -0
  289. package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
  290. package/public-landing/gate/previews/off-the-boats.jpg +0 -0
  291. package/public-landing/gate/space.css +799 -0
  292. package/public-landing/gate/space.js +1826 -0
  293. package/public-landing/gate/sprites/enter.png +0 -0
  294. package/public-landing/gate/sprites/search.png +0 -0
  295. package/public-landing/gate/sprites/terminal-blank.png +0 -0
  296. package/public-landing/gate/sprites/tile.png +0 -0
  297. package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
  298. package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
  299. package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
  300. package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
  301. package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
  302. package/public-landing/world/assets/plates/scene-establishing.png +0 -0
  303. package/scripts/gds/abandon.js +6 -4
  304. package/scripts/gds/api.js +10 -5
  305. package/scripts/gds/api.ps1 +26 -0
  306. package/scripts/gds/api.sh +6 -0
  307. package/scripts/gds/audit-ships.js +7 -1
  308. package/scripts/gds/backfill-sessions.js +78 -18
  309. package/scripts/gds/backup.js +23 -1
  310. package/scripts/gds/box-connect-lib.js +124 -31
  311. package/scripts/gds/box-sync.js +66 -0
  312. package/scripts/gds/box.js +225 -53
  313. package/scripts/gds/cf-tunnel.js +145 -6
  314. package/scripts/gds/claim.js +34 -3
  315. package/scripts/gds/cli-lib.js +161 -21
  316. package/scripts/gds/code.js +6 -2
  317. package/scripts/gds/deploy-divergence.js +209 -0
  318. package/scripts/gds/dev-lib.js +309 -0
  319. package/scripts/gds/dev.js +322 -0
  320. package/scripts/gds/doctor.js +90 -61
  321. package/scripts/gds/fetch-art-key.js +1 -1
  322. package/scripts/gds/fetch-sound-prefs.js +2 -2
  323. package/scripts/gds/fitness.js +228 -12
  324. package/scripts/gds/gds-db-backup.service +16 -1
  325. package/scripts/gds/gds-db-offsite.service +9 -1
  326. package/scripts/gds/gen-api-client.js +1 -1
  327. package/scripts/gds/gen-api-docs.js +36 -14
  328. package/scripts/gds/gen-diagrams.js +45 -28
  329. package/scripts/gds/gen-file-map.js +2 -1
  330. package/scripts/gds/gen-freshness.js +32 -0
  331. package/scripts/gds/gen-repo-map.js +3 -2
  332. package/scripts/gds/gen-session-index.js +14 -5
  333. package/scripts/gds/hooks/pre-push.js +2 -2
  334. package/scripts/gds/init.js +60 -3
  335. package/scripts/gds/install-git-hooks.js +1 -1
  336. package/scripts/gds/key-card.js +15 -6
  337. package/scripts/gds/key-status.js +1 -1
  338. package/scripts/gds/linkify-refs.js +35 -9
  339. package/scripts/gds/lint-migrations.js +102 -0
  340. package/scripts/gds/login.js +58 -11
  341. package/scripts/gds/mirror-redact.js +5 -0
  342. package/scripts/gds/package-core.js +263 -4
  343. package/scripts/gds/paste-token.js +7 -1
  344. package/scripts/gds/preflight.js +126 -0
  345. package/scripts/gds/provision.js +268 -19
  346. package/scripts/gds/publish-manifest.js +22 -1
  347. package/scripts/gds/reauth.js +7 -3
  348. package/scripts/gds/regen-instance-docs.js +47 -0
  349. package/scripts/gds/run-unit-tests.js +42 -16
  350. package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
  351. package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
  352. package/scripts/gds/sequence.js +425 -0
  353. package/scripts/gds/session-cost.js +10 -1
  354. package/scripts/gds/setup.js +72 -5
  355. package/scripts/gds/shell.js +5 -3
  356. package/scripts/gds/ship.js +433 -59
  357. package/scripts/gds/smoke-gds.js +139 -8
  358. package/scripts/gds/start.js +171 -22
  359. package/scripts/gds/status.js +1 -1
  360. package/scripts/gds/triage.js +10 -9
  361. package/scripts/gds/upgrade.js +271 -7
  362. package/scripts/migrate.sh +10 -0
  363. package/scripts/status-mirror-sync.js +114 -84
  364. package/src/bongos/api-errors.js +1 -0
  365. package/src/bongos/auth.js +809 -107
  366. package/src/bongos/db-kernel.js +27 -0
  367. package/src/bongos/db.js +105 -0
  368. package/src/bongos/github-repos.js +93 -0
  369. package/src/bongos/module-scope-map.js +27 -1
  370. package/src/bongos/permission-path-check.js +136 -102
  371. package/src/bongos/repo-info.js +47 -11
  372. package/src/bongos/route-rank-check.js +429 -46
  373. package/src/bongos/routes/_helpers.js +12 -1
  374. package/src/bongos/routes/auth.js +312 -13
  375. package/src/bongos/routes/backup.js +58 -2
  376. package/src/bongos/routes/github.js +90 -0
  377. package/src/bongos/routes/instance.js +15 -3
  378. package/src/bongos/routes/me.js +35 -1
  379. package/src/bongos/routes.js +2 -0
  380. package/src/bongos/serve-internal.js +84 -15
  381. package/src/branding.js +49 -2
  382. package/src/instance-config.js +46 -0
  383. package/src/module-api.js +140 -1
  384. package/src/platform-server.js +86 -9
  385. package/tests/abandon_task.mjs +59 -3
  386. package/tests/api_cli_flags.mjs +53 -0
  387. package/tests/auth_page_gate.mjs +13 -0
  388. package/tests/backfill_sessions.mjs +127 -1
  389. package/tests/backup_cli.mjs +54 -0
  390. package/tests/bongos_box_connect.mjs +77 -0
  391. package/tests/box_access.mjs +130 -1
  392. package/tests/box_credential.mjs +125 -0
  393. package/tests/box_host_keys.mjs +24 -0
  394. package/tests/box_infra_resolve.mjs +149 -0
  395. package/tests/box_scope_session.mjs +6 -2
  396. package/tests/box_sync.mjs +55 -0
  397. package/tests/box_task_scope.mjs +99 -0
  398. package/tests/branding.mjs +41 -23
  399. package/tests/cf_ruleset_preflight.mjs +142 -0
  400. package/tests/cf_tunnel.mjs +37 -0
  401. package/tests/claim_gate_rebase.mjs +98 -0
  402. package/tests/clawback_confirmed_backlog.mjs +103 -0
  403. package/tests/cli_lib_formatters.mjs +20 -0
  404. package/tests/confirm_attribution.mjs +30 -46
  405. package/tests/credit_grant.mjs +198 -10
  406. package/tests/criterion_authoring_gate.mjs +272 -0
  407. package/tests/criterion_progress.mjs +33 -1
  408. package/tests/dev_lib.mjs +253 -0
  409. package/tests/devbox_artifact_gate.mjs +75 -0
  410. package/tests/devbox_dmg_backdrop.mjs +137 -0
  411. package/tests/devbox_icons.mjs +304 -0
  412. package/tests/devbox_installer_brand.mjs +232 -0
  413. package/tests/devbox_key_register.mjs +234 -0
  414. package/tests/docs_openapi_route.mjs +110 -0
  415. package/tests/doctor_hooks.mjs +141 -0
  416. package/tests/extract_json.mjs +140 -0
  417. package/tests/fitness.mjs +89 -0
  418. package/tests/gen_freshness.mjs +96 -0
  419. package/tests/github_repos_helpers.mjs +91 -0
  420. package/tests/github_token_store.mjs +121 -0
  421. package/tests/goal_advisory.mjs +251 -0
  422. package/tests/goal_criterion_confirm_ui.mjs +269 -0
  423. package/tests/goal_routes.mjs +59 -4
  424. package/tests/goal_tier.mjs +12 -3
  425. package/tests/governance_catalog.mjs +129 -0
  426. package/tests/governance_claim_eligibility.mjs +262 -0
  427. package/tests/governance_config.mjs +187 -0
  428. package/tests/governance_ownership_scope.mjs +410 -0
  429. package/tests/governance_page_gates.mjs +324 -0
  430. package/tests/governance_parity.mjs +336 -0
  431. package/tests/governance_principals.mjs +232 -0
  432. package/tests/governance_protected_surfaces.mjs +208 -0
  433. package/tests/governance_require_permission.mjs +223 -0
  434. package/tests/governance_resolver.mjs +84 -0
  435. package/tests/governance_role_sync.mjs +231 -0
  436. package/tests/governance_seed.mjs +127 -0
  437. package/tests/governance_session_scope.mjs +434 -0
  438. package/tests/governance_vanilla_boot.mjs +203 -0
  439. package/tests/grade_attribution.mjs +7 -7
  440. package/tests/hall_audit.mjs +223 -0
  441. package/tests/hall_nav.mjs +191 -0
  442. package/tests/hall_palette.mjs +206 -0
  443. package/tests/hall_signout.mjs +124 -0
  444. package/tests/hook_user_agents.mjs +10 -0
  445. package/tests/idea_triage.mjs +162 -0
  446. package/tests/idp_device_exchange.mjs +198 -0
  447. package/tests/idp_federation.mjs +100 -0
  448. package/tests/init.mjs +51 -0
  449. package/tests/instance_config.mjs +65 -0
  450. package/tests/instance_mismatch_guard.mjs +23 -6
  451. package/tests/instance_release_config.mjs +104 -0
  452. package/tests/linkify_refs.mjs +15 -2
  453. package/tests/migrations_forward_safe.mjs +86 -0
  454. package/tests/module_api.mjs +1 -1
  455. package/tests/module_contributions.mjs +55 -0
  456. package/tests/module_loader.mjs +10 -5
  457. package/tests/module_scope_active_claims.mjs +94 -0
  458. package/tests/needs_rebase.mjs +92 -0
  459. package/tests/one_active_claim_per_session.mjs +76 -0
  460. package/tests/override_attribution.mjs +10 -8
  461. package/tests/override_request_ownership.mjs +8 -4
  462. package/tests/package_core.mjs +246 -1
  463. package/tests/parse_cookie_decode.mjs +42 -0
  464. package/tests/pay_on_land.mjs +129 -0
  465. package/tests/platform_boot.mjs +100 -1
  466. package/tests/post_login_landing.mjs +42 -0
  467. package/tests/preflight_checks.mjs +72 -0
  468. package/tests/provision.mjs +289 -7
  469. package/tests/pruned_lockfile_integrity.mjs +79 -0
  470. package/tests/publish_branch_route.mjs +53 -0
  471. package/tests/publish_reconciler.mjs +187 -2
  472. package/tests/repo_info_binding.mjs +248 -0
  473. package/tests/reward_gate.mjs +11 -2
  474. package/tests/reward_mode_cost_plus_only.mjs +116 -0
  475. package/tests/reward_mode_surfacing.mjs +168 -0
  476. package/tests/route_permission_check.mjs +301 -0
  477. package/tests/route_rank_check.mjs +254 -12
  478. package/tests/route_validation.mjs +30 -0
  479. package/tests/sequence.mjs +267 -0
  480. package/tests/session_digest.mjs +44 -0
  481. package/tests/session_index.mjs +18 -6
  482. package/tests/session_records.mjs +39 -1
  483. package/tests/session_reward_on_land.mjs +381 -0
  484. package/tests/session_reward_reconcile.mjs +229 -0
  485. package/tests/ship_api_client_pathspec.mjs +100 -0
  486. package/tests/ship_base_flag.mjs +93 -0
  487. package/tests/ship_ci_deploy.mjs +140 -3
  488. package/tests/ship_upstream_deletion.mjs +57 -0
  489. package/tests/smoke_target.mjs +233 -0
  490. package/tests/start_env_guard.mjs +87 -0
  491. package/tests/start_rebase_warning.mjs +103 -0
  492. package/tests/status_mirror_sync.mjs +122 -0
  493. package/tests/stop_refcheck.mjs +19 -1
  494. package/tests/task_goal_link.mjs +15 -6
  495. package/tests/upgrade.mjs +393 -5
  496. package/tests/web_admission_status.mjs +162 -0
  497. package/tests/write_validation_gaps.mjs +110 -0
  498. package/.gitignore +0 -91
  499. package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
  500. package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
  501. package/scripts/hall-preview/fixtures/access-requests.json +0 -20
  502. package/scripts/hall-preview/fixtures/achievements.json +0 -76
  503. package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
  504. package/scripts/hall-preview/fixtures/audit-log.json +0 -68
  505. package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
  506. package/scripts/hall-preview/fixtures/box__me.json +0 -5
  507. package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
  508. package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
  509. package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
  510. package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
  511. package/scripts/hall-preview/fixtures/goals.json +0 -502
  512. package/scripts/hall-preview/fixtures/goals__25.json +0 -96
  513. package/scripts/hall-preview/fixtures/goals__26.json +0 -67
  514. package/scripts/hall-preview/fixtures/goals__3.json +0 -103
  515. package/scripts/hall-preview/fixtures/goals__35.json +0 -49
  516. package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
  517. package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
  518. package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
  519. package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
  520. package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
  521. package/scripts/hall-preview/fixtures/inbox.json +0 -330
  522. package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
  523. package/scripts/hall-preview/fixtures/instance.json +0 -124
  524. package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
  525. package/scripts/hall-preview/fixtures/me.json +0 -476
  526. package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
  527. package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
  528. package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
  529. package/scripts/hall-preview/fixtures/me__render.json +0 -5
  530. package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
  531. package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
  532. package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
  533. package/scripts/hall-preview/fixtures/override-requests.json +0 -15
  534. package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
  535. package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
  536. package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
  537. package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
  538. package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
  539. package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
  540. package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
  541. package/scripts/hall-preview/fixtures/public__grades.json +0 -50
  542. package/scripts/hall-preview/fixtures/public__progress.json +0 -124
  543. package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
  544. package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
  545. package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
  546. package/scripts/hall-preview/fixtures/security__docs.json +0 -10
  547. package/scripts/hall-preview/fixtures/security__reports.json +0 -31
  548. package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
  549. package/scripts/hall-preview/fixtures/tasks.json +0 -2420
  550. package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
  551. package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
  552. package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
  553. package/scripts/hall-preview/fixtures/versions.json +0 -89
@@ -0,0 +1,402 @@
1
+ // modules/platform-identity/routes/sso.js — the hub SSO login-handoff surface
2
+ // (ADR 0141 §1/§2, task #1002106). Two endpoints:
3
+ //
4
+ // GET /sso/authorize — the browser leg. Gated on a LIVE HUB SESSION (resolved
5
+ // inline; bounces to the hub's own sign-in when absent).
6
+ // Validates the client + redirect_uri, mints a single-use
7
+ // code, and 302s back to the instance's redirect_uri.
8
+ // POST /sso/token — the server-to-server leg. Authenticated by client_id +
9
+ // client_secret (constant-time). Atomically redeems the
10
+ // code and returns IDENTITY ONLY — never a rank.
11
+ //
12
+ // Reaches core ONLY through the doorway (src/module-api.js, the one-way rule,
13
+ // ADR 0083); requires this module's own platform-identity.js (same package),
14
+ // never a sibling module.
15
+ 'use strict';
16
+
17
+ const express = require('express');
18
+ const api = require('../../../src/module-api');
19
+ const pi = require('../platform-identity');
20
+ const hubKeys = require('../hub-keys');
21
+ const hubDevice = require('../hub-device');
22
+
23
+ const { pool, validateOrRespond, resolveSession } = api;
24
+
25
+ // Minimal standalone error page for the browser leg (no template engine / deps).
26
+ // The /sso/authorize failures are pre-redirect (bad client / bad redirect_uri), so
27
+ // we must render our OWN page rather than bounce to an unvalidated URI.
28
+ function errorPage(detail) {
29
+ const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => (
30
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]
31
+ ));
32
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8">`
33
+ + `<meta name="viewport" content="width=device-width, initial-scale=1">`
34
+ + `<title>Sign-in handoff failed</title></head><body>`
35
+ + `<h1>Sign-in handoff failed</h1><p>${esc(detail)}</p>`
36
+ + `<p>This link came from a project trying to sign you in through Cloud Bongos. `
37
+ + `If it keeps happening, the project's sign-in may be misconfigured.</p></body></html>`;
38
+ }
39
+
40
+ // --- Federated single-logout fan-out (ADR 0144, task 1002238) --------------
41
+ // When a builder signs out of the HUB, core emits a `builder.logout` seam event;
42
+ // this module (enabled only on the hub) reacts by sending a hub-signed logout
43
+ // token to every federated project the builder entered, so each revokes its own
44
+ // local session too. Back-channel (server-to-server) so sign-out sticks even if
45
+ // the browser closes the tab, and doesn't depend on third-party-cookie rules.
46
+
47
+ const SLO_TIMEOUT_MS = 4000; // per-instance notify budget — a slow/down project must never wedge the sweep
48
+
49
+ // Registered once (guarded against a route rebuild, e.g. in tests) at first mount.
50
+ let _logoutListenerOff = null;
51
+
52
+ // POST one hub-signed logout token to a single federated instance, bounded by a
53
+ // short timeout. NEVER throws (the caller runs it under Promise.allSettled + this
54
+ // try/catch), so one unreachable project can't sink the others.
55
+ async function notifyInstanceLogout(membership, githubId) {
56
+ const origin = String(membership.project_origin).replace(/\/+$/, '');
57
+ try {
58
+ const token = hubKeys.mintHubLogoutToken({ clientId: membership.client_id, githubId });
59
+ const ac = new AbortController();
60
+ const timer = setTimeout(() => ac.abort(), SLO_TIMEOUT_MS);
61
+ try {
62
+ const resp = await fetch(`${origin}/api/gds/auth/backchannel-logout`, {
63
+ method: 'POST',
64
+ headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
65
+ body: JSON.stringify({ logout_token: token }),
66
+ signal: ac.signal,
67
+ });
68
+ if (!resp.ok) console.warn(`[platform-identity] SLO: ${origin} returned ${resp.status}`);
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ } catch (err) {
73
+ console.warn(`[platform-identity] SLO: notifying ${origin} failed (non-blocking):`, err && err.message ? err.message : err);
74
+ }
75
+ }
76
+
77
+ // Fan a back-channel logout out to every federated project the builder entered.
78
+ // Runs off a fire-and-forget event, so it MUST NOT throw (a floating rejection
79
+ // would be an unhandledRejection). Over-notifying is safe: an instance with no
80
+ // live session for the builder just revokes nothing (idempotent). Uses the
81
+ // builder's known memberships as the entered-instance list (ADR 0141 §3 rows) —
82
+ // the task's sanctioned V1 signal; a precise per-session entry ledger is a follow-up.
83
+ async function fanOutBackchannelLogout(githubId) {
84
+ try {
85
+ if (githubId == null) return;
86
+ // Only the hub mints logout tokens; no signing key ⇒ not the hub ⇒ nothing to do
87
+ // (defense-in-depth beyond the module being hub-only).
88
+ if (!hubKeys.hubSigningConfigured()) return;
89
+ const memberships = await pi.listMembershipsForAccount(githubId, { pool });
90
+ const targets = (memberships || []).filter((m) => m.project_origin && m.client_id);
91
+ await Promise.allSettled(targets.map((m) => notifyInstanceLogout(m, githubId)));
92
+ } catch (err) {
93
+ console.error('[platform-identity] SLO fan-out failed (non-blocking):', err && err.message ? err.message : err);
94
+ }
95
+ }
96
+
97
+ module.exports = function ssoRoutes() {
98
+ const router = express.Router();
99
+
100
+ // Wire the federated single-logout fan-out to core's `builder.logout` event,
101
+ // once. The handler is deliberately fire-and-forget (fanOut self-isolates), so
102
+ // the hub's own /auth/logout returns immediately and the sweep runs server-side.
103
+ if (!_logoutListenerOff) {
104
+ _logoutListenerOff = api.on('builder.logout', ({ githubId }) => { fanOutBackchannelLogout(githubId); });
105
+ }
106
+
107
+ // GET /sso/authorize?client_id=&redirect_uri=&state= — the browser authorize leg.
108
+ //
109
+ // rank: public — this is an authentication entry point, not an authorization
110
+ // surface. It is gated on a LIVE HUB SESSION (resolved inline via
111
+ // resolveSession; a missing session 302s to the hub's own web sign-in and
112
+ // returns here), NOT on a builder RANK. It mints identity-only codes.
113
+ router.get('/sso/authorize', async (req, res) => {
114
+ const clientId = String(req.query.client_id || '');
115
+ const redirectUri = String(req.query.redirect_uri || '');
116
+ const state = req.query.state != null ? String(req.query.state) : '';
117
+
118
+ if (!clientId || !redirectUri) {
119
+ return res.status(400).type('text/html').send(errorPage('Missing client_id or redirect_uri.'));
120
+ }
121
+ // Bound the query params (the POST leg validates via validateOrRespond; GET
122
+ // params are read raw, so cap them here). An oversized value is never useful
123
+ // and only amplifies work / the reflected Location header.
124
+ if (clientId.length > 200 || redirectUri.length > 2048 || state.length > 512) {
125
+ return res.status(400).type('text/html').send(errorPage('A sign-in parameter was too long.'));
126
+ }
127
+ // Validate the client + redirect_uri BEFORE doing anything else, and never
128
+ // redirect to an unvalidated URI (open-redirect guard, ADR 0141 §2 #3).
129
+ let client;
130
+ try {
131
+ client = await pi.getClient(clientId, { pool });
132
+ } catch (err) {
133
+ console.error('[platform-identity] /sso/authorize getClient', err);
134
+ return res.status(500).type('text/html').send(errorPage('Internal error resolving the project.'));
135
+ }
136
+ if (!client || client.status !== 'active') {
137
+ return res.status(400).type('text/html').send(errorPage('Unknown or suspended project (client_id).'));
138
+ }
139
+ if (!pi.redirectUriAllowed(client, redirectUri)) {
140
+ return res.status(400).type('text/html').send(errorPage('The redirect_uri is not registered for this project.'));
141
+ }
142
+
143
+ // Gate on a live hub session; bounce to the hub's own sign-in if absent, then
144
+ // return here to complete. safeReturnTo on the web flow validates the relative
145
+ // return path, so req.originalUrl (same-host, relative) is accepted.
146
+ //
147
+ // resolveSession is the RAW resolver (so we can redirect rather than 401), so it
148
+ // does NOT apply requireBuilder's deny gates — we must. sessionUsableForFederation
149
+ // rejects a deactivated account, a dev-box-scoped token (outside the ADR 0016
150
+ // trust boundary), and service principals: none may mint a federated identity code.
151
+ let session;
152
+ try {
153
+ session = await resolveSession(req);
154
+ } catch (err) {
155
+ console.error('[platform-identity] /sso/authorize resolveSession', err);
156
+ return res.status(500).type('text/html').send(errorPage('Internal error checking your hub session.'));
157
+ }
158
+ const usable = pi.sessionUsableForFederation(session);
159
+ if (!usable.ok) {
160
+ if (usable.reason === 'no_session') {
161
+ return res.redirect(302, `/api/gds/auth/web/start?return=${encodeURIComponent(req.originalUrl)}`);
162
+ }
163
+ const msg = usable.reason === 'inactive'
164
+ ? 'This account is no longer active. Ask an Archon on the hub if you believe this is an error.'
165
+ : usable.reason === 'box_scope'
166
+ ? 'A dev-box session cannot be used for cross-project sign-in. Sign in from a browser or run /builder-reauth.'
167
+ : 'This session type cannot be used for cross-project sign-in.';
168
+ return res.status(403).type('text/html').send(errorPage(msg));
169
+ }
170
+
171
+ // A live hub session — record/refresh the platform account and mint a
172
+ // single-use code bound to (client_id, github_id, redirect_uri).
173
+ try {
174
+ await pi.upsertAccount({
175
+ githubId: session.github_id,
176
+ githubLogin: session.github_login,
177
+ displayName: session.display_name,
178
+ avatarUrl: session.avatar_url,
179
+ }, { pool });
180
+ const code = await pi.mintAuthCode({
181
+ clientId,
182
+ githubId: session.github_id,
183
+ githubLogin: session.github_login,
184
+ displayName: session.display_name,
185
+ avatarUrl: session.avatar_url,
186
+ redirectUri,
187
+ }, { pool });
188
+ const u = new URL(redirectUri);
189
+ u.searchParams.set('code', code);
190
+ if (state) u.searchParams.set('state', state);
191
+ return res.redirect(302, u.toString());
192
+ } catch (err) {
193
+ console.error('[platform-identity] /sso/authorize mint', err);
194
+ return res.status(500).type('text/html').send(errorPage('Could not complete the sign-in handoff.'));
195
+ }
196
+ });
197
+
198
+ // POST /sso/token — server-to-server code redemption.
199
+ //
200
+ // rank: public — authenticated by client_id + client_secret (constant-time hash
201
+ // compare in clientSecretValid), NOT a builder session/rank. This is the
202
+ // confidential-client leg; the instance calls it from its server, never the
203
+ // browser. Returns IDENTITY ONLY (github_id, login, display_name, avatar_url).
204
+ router.post('/sso/token', async (req, res) => {
205
+ if (validateOrRespond(req, res, {
206
+ client_id: { required: true, type: 'string', maxLength: 200 },
207
+ client_secret: { required: true, type: 'string', maxLength: 512 },
208
+ code: { required: true, type: 'string', maxLength: 512 },
209
+ redirect_uri: { required: true, type: 'string', maxLength: 2048 },
210
+ })) return;
211
+
212
+ const { client_id, client_secret, code, redirect_uri } = req.body || {};
213
+ try {
214
+ const client = await pi.getClient(client_id, { pool });
215
+ // ALWAYS run the constant-work secret check (clientSecretValid hashes +
216
+ // constant-time-compares against a decoy when the client is missing), so an
217
+ // unknown/suspended client_id is timing-indistinguishable from a wrong secret.
218
+ // Then fold in the active check. One generic invalid_client for every failure.
219
+ const secretOk = pi.clientSecretValid(client, client_secret);
220
+ const active = !!client && client.status === 'active';
221
+ if (!active || !secretOk) {
222
+ return res.fail('invalid_client', 401);
223
+ }
224
+ // Atomic single-use redeem, bound to this client.
225
+ const identity = await pi.redeemAuthCode({ code, clientId: client_id }, { pool });
226
+ if (!identity) {
227
+ return res.fail('invalid_grant', 400);
228
+ }
229
+ // The redeem is also bound to the redirect_uri the code was minted against.
230
+ if (identity.redirect_uri !== redirect_uri) {
231
+ return res.fail('invalid_grant', 400);
232
+ }
233
+ // IDENTITY ONLY — never a rank, credits, or a session token (ADR 0016 / 0141).
234
+ return res.json({
235
+ github_id: Number(identity.github_id),
236
+ github_login: identity.github_login,
237
+ display_name: identity.display_name,
238
+ avatar_url: identity.avatar_url,
239
+ });
240
+ } catch (err) {
241
+ console.error('[platform-identity] POST /sso/token', err);
242
+ return res.fail('token_exchange_failed', { status: 500, message: 'internal error' });
243
+ }
244
+ });
245
+
246
+ // GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). An
247
+ // instance provisioned WITHOUT the key in env can fetch it here; the value is
248
+ // public by design (it only VERIFIES assertions, never signs).
249
+ //
250
+ // rank: public — a public key is not a secret; no auth needed.
251
+ router.get('/sso/pubkey', (_req, res) => {
252
+ const publicKeyB64 = hubKeys.hubPublicKeyB64();
253
+ if (!publicKeyB64) return res.fail('idp_signing_unconfigured', 503);
254
+ res.json({ alg: 'EdDSA', format: 'spki-der-base64', public_key_b64: publicKeyB64 });
255
+ });
256
+
257
+ // POST /sso/device/start — begin CLI federation. The client is resolved from the
258
+ // USER-TYPED instance ORIGIN via the registry (getClientByOrigin) and BOUND to the
259
+ // device flow — the CLI never names the audience. This closes the confused-deputy
260
+ // where a malicious instance manifest could steer the mint at a victim client_id
261
+ // (ADR 0141 §1 security fix). Runs the GitHub device flow against the HUB's app and
262
+ // returns the codes. Identity is established as a platform ACCOUNT at poll time —
263
+ // never a hub-builder session (ADR 0141: identity federated, authority local).
264
+ //
265
+ // rank: public — an authentication entry point; the origin is public, and the
266
+ // poll's GitHub approval is the real gate.
267
+ router.post('/sso/device/start', async (req, res) => {
268
+ if (validateOrRespond(req, res, {
269
+ origin: { required: true, type: 'string', maxLength: 512 },
270
+ })) return;
271
+ if (!hubKeys.hubSigningConfigured() || !hubDevice.hubDeviceConfigured()) {
272
+ return res.fail('idp_signing_unconfigured', 503);
273
+ }
274
+ try {
275
+ // Resolve the client from the user-typed origin — NOT from any client_id the
276
+ // caller supplies. An unregistered origin has no client → refuse (correct default).
277
+ const client = await pi.getClientByOrigin(req.body.origin, { pool });
278
+ if (!client || client.status !== 'active') return res.fail('invalid_client', 401);
279
+ const out = await hubDevice.startHubGithubDevice(client.client_id);
280
+ res.json(out);
281
+ } catch (err) {
282
+ console.error('[platform-identity] /sso/device/start', err);
283
+ res.fail('device_start_failed', { status: 502, message: String(err.message || 'error').slice(0, 200) });
284
+ }
285
+ });
286
+
287
+ // POST /sso/device/poll — poll the hub device flow. On GitHub approval, upsert the
288
+ // platform account and mint an audience-scoped EdDSA assertion for the client that
289
+ // was BOUND at start (from the origin) — the poll takes NO client_id, so the CLI
290
+ // cannot influence the audience. Returns the SAME poll states as the core device
291
+ // flow, plus { state:'ok', assertion } on success.
292
+ //
293
+ // rank: public — the device_code (GitHub's) is the bearer; the assertion carries
294
+ // identity ONLY and is useless to anyone but the origin-bound client (aud-scoped).
295
+ router.post('/sso/device/poll', async (req, res) => {
296
+ if (validateOrRespond(req, res, {
297
+ device_code: { required: true, type: 'string', maxLength: 512 },
298
+ })) return;
299
+ if (!hubKeys.hubSigningConfigured() || !hubDevice.hubDeviceConfigured()) {
300
+ return res.fail('idp_signing_unconfigured', 503);
301
+ }
302
+ try {
303
+ const out = await hubDevice.pollHubGithubDevice(req.body.device_code);
304
+ if (out.state !== 'ok') return res.json(out); // pending | slow_down | expired | denied
305
+ if (!out.client_id) return res.fail('device_poll_failed', { status: 500, message: 'internal error' });
306
+ const id = out.identity;
307
+ await pi.upsertAccount({
308
+ githubId: id.github_id,
309
+ githubLogin: id.github_login,
310
+ displayName: id.display_name,
311
+ avatarUrl: id.avatar_url,
312
+ }, { pool });
313
+ const assertion = hubKeys.mintHubAssertion({
314
+ clientId: out.client_id, // BOUND at start from the origin — not caller-supplied
315
+ githubId: id.github_id,
316
+ githubLogin: id.github_login,
317
+ displayName: id.display_name,
318
+ avatarUrl: id.avatar_url,
319
+ });
320
+ res.json({ state: 'ok', assertion });
321
+ } catch (err) {
322
+ console.error('[platform-identity] /sso/device/poll', err);
323
+ res.fail('device_poll_failed', { status: 500, message: 'internal error' });
324
+ }
325
+ });
326
+
327
+ // POST /sso/membership/check-in — a federated instance reports, on every sign-in,
328
+ // that a builder builds on its project (ADR 0141 §3). Server-to-server, authed by
329
+ // client_id + client_secret (same constant-time check as /sso/token). Upserts the
330
+ // account (so a CLI-only builder still appears in My Projects) + the membership
331
+ // with a COARSE role. The client_id is taken from the AUTHENTICATED client — the
332
+ // caller cannot report membership for a project it doesn't hold the secret for.
333
+ //
334
+ // rank: public — authenticated by client_id + client_secret, not a builder rank.
335
+ router.post('/sso/membership/check-in', async (req, res) => {
336
+ if (validateOrRespond(req, res, {
337
+ client_id: { required: true, type: 'string', maxLength: 200 },
338
+ client_secret: { required: true, type: 'string', maxLength: 512 },
339
+ github_id: { required: true },
340
+ github_login: { required: true, type: 'string', maxLength: 200 },
341
+ display_name: { type: 'string', maxLength: 400 },
342
+ avatar_url: { type: 'string', maxLength: 2048 },
343
+ role: { type: 'string', maxLength: 16 },
344
+ })) return;
345
+ const { client_id, client_secret, github_id, github_login, display_name, avatar_url, role } = req.body || {};
346
+ try {
347
+ const client = await pi.getClient(client_id, { pool });
348
+ if (!client || client.status !== 'active' || !pi.clientSecretValid(client, client_secret)) {
349
+ return res.fail('invalid_client', 401);
350
+ }
351
+ // Upsert the account first so a CLI-only builder is known to the hub, then the
352
+ // membership — keyed to the AUTHENTICATED client_id, never a caller-named one.
353
+ await pi.upsertAccount({ githubId: github_id, githubLogin: github_login, displayName: display_name, avatarUrl: avatar_url }, { pool });
354
+ await pi.upsertMembership({ clientId: client_id, githubId: github_id, role: role || 'builder' }, { pool });
355
+ res.json({ ok: true });
356
+ } catch (err) {
357
+ console.error('[platform-identity] /sso/membership/check-in', err);
358
+ res.fail('checkin_failed', { status: 500, message: 'internal error' });
359
+ }
360
+ });
361
+
362
+ // POST /sso/activity/rollup — a federated instance reports a builder's activity
363
+ // snapshot (ADR 0141 §4). Server-to-server, client_id+client_secret auth. Keyed to
364
+ // the AUTHENTICATED client_id. Descriptive analytics for the Archon-only scouting
365
+ // view — never authority.
366
+ //
367
+ // rank: public — authenticated by client_id + client_secret, not a builder rank.
368
+ router.post('/sso/activity/rollup', async (req, res) => {
369
+ if (validateOrRespond(req, res, {
370
+ client_id: { required: true, type: 'string', maxLength: 200 },
371
+ client_secret: { required: true, type: 'string', maxLength: 512 },
372
+ github_id: { required: true },
373
+ credits: { type: 'number' },
374
+ tasks_shipped: { type: 'number' },
375
+ karma: { type: 'number' },
376
+ rank: { type: 'string', maxLength: 32 },
377
+ disciplines: {},
378
+ })) return;
379
+ const b = req.body || {};
380
+ try {
381
+ const client = await pi.getClient(b.client_id, { pool });
382
+ if (!client || client.status !== 'active' || !pi.clientSecretValid(client, b.client_secret)) {
383
+ return res.fail('invalid_client', 401);
384
+ }
385
+ await pi.upsertActivity({
386
+ clientId: b.client_id,
387
+ githubId: b.github_id,
388
+ credits: b.credits,
389
+ tasksShipped: b.tasks_shipped,
390
+ karma: b.karma,
391
+ rank: b.rank || null,
392
+ disciplines: Array.isArray(b.disciplines) ? b.disciplines : [],
393
+ }, { pool });
394
+ res.json({ ok: true });
395
+ } catch (err) {
396
+ console.error('[platform-identity] /sso/activity/rollup', err);
397
+ res.fail('rollup_failed', { status: 500, message: 'internal error' });
398
+ }
399
+ });
400
+
401
+ return router;
402
+ };