@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
@@ -80,6 +80,78 @@ function renderAuthErrorPage(heading, detail, opts = {}) {
80
80
  </body></html>`;
81
81
  }
82
82
 
83
+ // renderAccessPendingPage — the calm "hang tight" page shown instead of the red
84
+ // dead-end when admission is plausibly imminent (task 1002097): the configured
85
+ // firstAdmin during the bootstrap window, or a login with a pending request.
86
+ //
87
+ // It polls GET /auth/web/admission-status and, the moment the answer flips to
88
+ // admitted, sends the visitor back through /auth/web/start for a FRESH sign-in.
89
+ // A fresh start is required, not optional: the OAuth code that brought them
90
+ // here is already spent, so re-running the callback would fail on state
91
+ // mismatch. The owner chose auto-walk-in, hence no button to click.
92
+ //
93
+ // Branding resolves defensively, exactly as renderAuthErrorPage does — a
94
+ // branding hiccup must degrade to neutral copy, never crash the page.
95
+ function renderAccessPendingPage(login) {
96
+ const worldName = (() => { try { return branding().identity?.worldName || 'The city'; } catch { return 'The city'; } })();
97
+ const epigraph = (() => { try { return branding().copy?.authError?.epigraph || '— access —'; } catch { return '— access —'; } })();
98
+ // The probe takes the login as a query param; everything interpolated into
99
+ // the page is escaped, and the JS reads it from a data- attribute rather than
100
+ // being built by string concatenation into a script body.
101
+ const safeLogin = escapeHtml(String(login || ''));
102
+ return `<!doctype html>
103
+ <html lang="en"><head>
104
+ <meta charset="utf-8">
105
+ <meta name="viewport" content="width=device-width, initial-scale=1">
106
+ <title>${escapeHtml(worldName)} — confirming access</title>
107
+ <link rel="preconnect" href="https://fonts.googleapis.com">
108
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
109
+ <link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&family=EB+Garamond:ital,wght@0,400;0,500&display=swap" rel="stylesheet">
110
+ <link rel="stylesheet" href="/builders/style.css">
111
+ </head><body>
112
+ <header class="hall">
113
+ <p class="hall__epigraph">${escapeHtml(epigraph)}</p>
114
+ <h1>Confirming your access</h1>
115
+ <p class="hall__sub">Hang tight — this usually takes a few seconds.</p>
116
+ </header>
117
+ <main>
118
+ <section class="scroll">
119
+ <div class="landing" id="pending" data-login="${safeLogin}">
120
+ <p>We're checking your access now. You'll be signed in automatically as soon as it clears — no need to refresh.</p>
121
+ <p class="landing__note" id="pending-note">Checking…</p>
122
+ </div>
123
+ </section>
124
+ </main>
125
+ <script>
126
+ (function () {
127
+ var el = document.getElementById('pending');
128
+ var note = document.getElementById('pending-note');
129
+ var login = el ? el.getAttribute('data-login') : '';
130
+ var EVERY_MS = 3000;
131
+ var GIVE_UP_MS = 5 * 60 * 1000;
132
+ var started = Date.now();
133
+
134
+ function land() { window.location.href = '/api/gds/auth/web/start'; }
135
+
136
+ function tick() {
137
+ if (Date.now() - started > GIVE_UP_MS) {
138
+ note.textContent = 'Still waiting on approval. You can leave this page open, or try signing in again later.';
139
+ return;
140
+ }
141
+ fetch('/api/gds/auth/web/admission-status?login=' + encodeURIComponent(login), { credentials: 'omit' })
142
+ .then(function (r) { return r.ok ? r.json() : null; })
143
+ .then(function (j) {
144
+ if (j && j.admitted) { land(); return; }
145
+ setTimeout(tick, EVERY_MS);
146
+ })
147
+ .catch(function () { setTimeout(tick, EVERY_MS); });
148
+ }
149
+ setTimeout(tick, EVERY_MS);
150
+ })();
151
+ </script>
152
+ </body></html>`;
153
+ }
154
+
83
155
  // Cookie names. The web flow writes the new `gds_session` (+ `gds_oauth_state`)
84
156
  // names; both flows accept either the new or the legacy `pms_session` /
85
157
  // `pms_oauth_state` cookies on read so a builder mid-flow doesn't have to
@@ -93,6 +165,13 @@ const DISCORD_STATE_COOKIE = 'gds_discord_state';
93
165
  // flow can land a builder back on the subdomain they started from (#726). Set
94
166
  // at /auth/web/start, consumed + cleared at the callback. Defaults to /builders.
95
167
  const RETURN_COOKIE = 'gds_return';
168
+ // Short-lived cookie marking that THIS sign-in opted into repo access (the
169
+ // onboarding wizard's `?repos=1`). Set at /auth/web/start, read at the callback so
170
+ // it captures + persists the returned GitHub token — and cleared there. Scoping
171
+ // capture to an explicit intent cookie (not just "the granted scope happens to
172
+ // include repo") means an ordinary hall sign-in never stores a token, even for a
173
+ // user who previously granted the broader scope (task 1002099 / ADR 0143).
174
+ const REPOS_COOKIE = 'gds_oauth_repos';
96
175
 
97
176
  // The apex domain the instance is served under, read from the instance branding
98
177
  // pack's cookieDomain (".amazonprimea.com" → "amazonprimea.com",
@@ -182,6 +261,27 @@ function configuredOAuthOrigin() {
182
261
  }
183
262
  }
184
263
 
264
+ // The post-sign-in landing used when no explicit ?return= / same-apex Referer
265
+ // was stashed for this sign-in. Configurable per instance via
266
+ // branding.landing.postLogin (env override <PREFIX>_POST_LOGIN_LANDING) so a
267
+ // platform-home instance can land visitors on its OWN front door —
268
+ // cloudbongos.com sets it to /gate/ (the orbs) so a visitor who signs in from
269
+ // the orbs returns to the orbs, never auto-redirected into the builders' hall
270
+ // (task 1002240). Every other instance keeps the portable default '/builders':
271
+ // the hall always exists, a /gate/ orbs site does not. The configured value is
272
+ // run through the SAME open-redirect allowlist as ?return= (safeReturnTo), so a
273
+ // misconfigured landing can never become an open redirect; anything unsafe or
274
+ // absent falls back to '/builders'. Takes an optional resolved pack so the pure
275
+ // path is unit-testable without disk I/O (mirrors clientBranding()).
276
+ function postLoginLanding(brand) {
277
+ try {
278
+ const b = brand || branding();
279
+ return safeReturnTo(b.landing?.postLogin) || '/builders';
280
+ } catch {
281
+ return '/builders';
282
+ }
283
+ }
284
+
185
285
  // The OAuth redirect_uri must be a FIXED, registered origin — never derived from
186
286
  // the request's x-forwarded-host (spoofable; F2 Hacker finding). Configured via
187
287
  // env, falling back to the configured canonical public origin, so an injected host
@@ -245,10 +345,47 @@ module.exports = function buildAuthRouter() {
245
345
  }
246
346
  });
247
347
 
348
+ // POST /auth/device/exchange — CLI federation (ADR 0141 §1). The CLI ran the
349
+ // device flow against the HUB and got a hub-signed assertion; it presents it here.
350
+ // The instance verifies the Ed25519 signature OFFLINE against the provisioned hub
351
+ // public key (aud must equal this instance's client_id, unexpired), then runs the
352
+ // instance's OWN admission gate + upsertBuilder + createSession(source='cli') and
353
+ // returns the existing { token, builder }. Only reachable when auth.idp is set.
354
+ //
355
+ // rank: public — an auth-establishment endpoint; the hub-signed assertion IS the
356
+ // bearer and is cryptographically verified before any session is minted.
357
+ router.post('/auth/device/exchange', async (req, res) => {
358
+ if (!auth.idpConfigured()) {
359
+ return res.fail('idp_not_configured', 503);
360
+ }
361
+ if (validateOrRespond(req, res, {
362
+ assertion: { required: true, type: 'string', maxLength: 4096 },
363
+ })) return;
364
+ try {
365
+ const { token, builder } = await auth.completeDeviceExchange({
366
+ assertion: req.body.assertion,
367
+ userAgent: req.headers['user-agent'],
368
+ });
369
+ res.json({ token, builder });
370
+ } catch (err) {
371
+ // Mirror the device/poll admission surface so the CLI prints the same
372
+ // "ask an Archon to invite you" path.
373
+ if (err && err.code === 'NOT_APPROVED') {
374
+ return res.status(403).json({ state: 'not_approved', error: 'not_approved' });
375
+ }
376
+ // A bad / expired / forged / wrong-audience assertion is an auth failure (401),
377
+ // not a server error — never mint a session for it.
378
+ console.error('[gds] /auth/device/exchange', err);
379
+ res.fail('invalid_assertion', { status: 401, message: String(err.message || 'invalid assertion').slice(0, 160) });
380
+ }
381
+ });
382
+
248
383
  // /auth/web — browser flow (used by the /builders page).
249
384
  // rank: public — entry into the GitHub OAuth handshake.
250
385
  router.get('/auth/web/start', (req, res) => {
251
- if (!auth.authConfigured()) {
386
+ // Either direct-GitHub creds OR platform-IdP federation must be configured
387
+ // (ADR 0141 §1) — a federated instance has no GitHub app of its own.
388
+ if (!auth.authConfigured() && !auth.idpConfigured()) {
252
389
  return res.status(503).type('text/plain').send('auth not configured');
253
390
  }
254
391
  const state = crypto.randomBytes(16).toString('hex');
@@ -281,12 +418,75 @@ module.exports = function buildAuthRouter() {
281
418
  ...sessionCookieDomainOpts(req),
282
419
  });
283
420
  }
284
- res.redirect(auth.buildAuthorizeUrl(state, redirectUri));
421
+ // Opt-in repo access (task 1002099 / ADR 0143): the onboarding wizard passes
422
+ // ?repos=1 so it can list/create the user's repos. Remember the intent in a
423
+ // short-lived cookie (same recipe as the state cookie) and request the broader
424
+ // scope on the direct-GitHub leg. Absent the flag this is a no-op — hall + CLI
425
+ // sign-ins stay identity-only.
426
+ const wantsRepos = req.query.repos === '1' || req.query.repos === 'true';
427
+ if (wantsRepos) {
428
+ res.cookie(REPOS_COOKIE, '1', {
429
+ httpOnly: true,
430
+ sameSite: 'lax',
431
+ secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
432
+ maxAge: 10 * 60 * 1000,
433
+ ...sessionCookieDomainOpts(req),
434
+ });
435
+ }
436
+ // Federate through the hub when configured (ADR 0141 §1); otherwise the direct
437
+ // GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
438
+ // way — the callback validates it identically, so the hub leg is CSRF-protected too.
439
+ res.redirect(auth.idpConfigured()
440
+ ? auth.buildIdpAuthorizeUrl(state, redirectUri)
441
+ : auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos }));
442
+ });
443
+
444
+ // GET /auth/web/admission-status?login=X — the probe renderAccessPendingPage
445
+ // polls (task 1002097). Answers ONE question: would this login be admitted
446
+ // right now? It mirrors the real gate via auth.webAdmissionStatus so the page
447
+ // can never invite someone in the gate would refuse.
448
+ //
449
+ // rank: public — it must be reachable by a visitor with no session (that is
450
+ // the entire situation it exists for). It grants nothing: admission still
451
+ // runs through the normal sign-in, and this only reads state.
452
+ //
453
+ // WHAT IT LEAKS, stated plainly (task 1002430 — an earlier version of this
454
+ // comment claimed the opposite and was wrong). Returning only a boolean does
455
+ // NOT make it non-enumerable: with open enrollment off, and for any login
456
+ // that is not the configured firstAdmin, `admitted:true` means that login is
457
+ // a member or has been invited. That reveals account existence to an
458
+ // unauthenticated caller. It is also NOT throttled — rateLimitMiddleware
459
+ // returns early for non-write methods, so GETs bypass it entirely; the page's
460
+ // 3s poll bounds the honest client, not an attacker.
461
+ //
462
+ // Why it is nonetheless not a new exposure: the onboarding module's
463
+ // GET /access-requests/status?github_login=X is ALREADY public and returns
464
+ // the strictly richer member|invited|pending|dismissed|none plus its own
465
+ // `admitted`. Anything derivable here is already derivable there, so this
466
+ // adds no net-new surface — and hardening only this route would accomplish
467
+ // nothing while that one stays open. Whether unauthenticated login-existence
468
+ // probing is acceptable at all is a real question, but it belongs to both
469
+ // routes together, not to a comment here.
470
+ //
471
+ // That endpoint is also why this one exists: it models neither the firstAdmin
472
+ // bypass nor open enrollment, so it reports the OWNER as 'none' forever —
473
+ // the exact case this page has to resolve.
474
+ router.get('/auth/web/admission-status', async (req, res) => {
475
+ const login = typeof req.query.login === 'string' ? req.query.login.slice(0, 100) : '';
476
+ if (!login) return res.json({ admitted: false });
477
+ try {
478
+ const { admitted } = await auth.webAdmissionStatus(login);
479
+ return res.json({ admitted: !!admitted });
480
+ } catch (err) {
481
+ // Fail closed and quietly — the page just keeps polling.
482
+ console.error('[gds] auth/web/admission-status', err);
483
+ return res.json({ admitted: false });
484
+ }
285
485
  });
286
486
 
287
487
  // rank: public — OAuth callback; the state cookie + code are the bearer.
288
488
  router.get('/auth/web/callback', async (req, res) => {
289
- if (!auth.authConfigured()) {
489
+ if (!auth.authConfigured() && !auth.idpConfigured()) {
290
490
  return res.status(503).type('text/plain').send('auth not configured');
291
491
  }
292
492
  const expectedState = parseCookie(req, STATE_COOKIE) || parseCookie(req, STATE_COOKIE_LEGACY);
@@ -304,14 +504,19 @@ module.exports = function buildAuthRouter() {
304
504
  ));
305
505
  }
306
506
  try {
307
- // Must match the PINNED redirect_uri used at /auth/web/start (#734) — the
308
- // GitHub token exchange requires an exact string match.
507
+ // Must match the PINNED redirect_uri used at /auth/web/start (#734) — both
508
+ // the GitHub token exchange and the hub /sso/token redemption require an
509
+ // exact string match against the value sent at authorize time.
309
510
  const redirectUri = `${WEB_REDIRECT_ORIGIN}/api/gds/auth/web/callback`;
310
- const { token, expiresAt } = await auth.completeWebFlow({
311
- code,
312
- redirectUri,
313
- userAgent: req.headers['user-agent'],
314
- });
511
+ // Did THIS sign-in opt into repo access? (the wizard's ?repos=1 intent cookie)
512
+ const wantsRepos = parseCookie(req, REPOS_COOKIE) === '1';
513
+ // Federated (hub) vs direct-GitHub — same downstream: admission gate,
514
+ // upsertBuilder, createSession, gds_session cookie (ADR 0141 §1). The direct
515
+ // leg additionally captures the opt-in repo token (task 1002099 / ADR 0143);
516
+ // the hub leg has no direct GitHub token, so the wizard degrades to manual entry.
517
+ const { token, expiresAt, builder, githubToken, githubScope } = auth.idpConfigured()
518
+ ? await auth.completeIdpFlow({ code, redirectUri, userAgent: req.headers['user-agent'] })
519
+ : await auth.completeWebFlow({ code, redirectUri, userAgent: req.headers['user-agent'], captureRepoToken: wantsRepos });
315
520
  res.cookie(SESSION_COOKIE, token, {
316
521
  httpOnly: true,
317
522
  sameSite: 'lax',
@@ -322,26 +527,61 @@ module.exports = function buildAuthRouter() {
322
527
  // amazonprimea.com + builders. + status. (#726); host-only elsewhere.
323
528
  ...sessionCookieDomainOpts(req),
324
529
  });
530
+ // Persist the wizard's repo token (task 1002099 / ADR 0143): encrypted at rest,
531
+ // short-TTL, keyed to the builder — the repo picker/creator reads it back over
532
+ // the next few minutes. Gated on secret-box being configured (upsertGithubToken
533
+ // no-ops when it isn't). Best-effort: a capture failure must NEVER break sign-in.
534
+ if (wantsRepos && githubToken) {
535
+ try {
536
+ await db.upsertGithubToken({ builderId: builder.id, token: githubToken, scope: githubScope });
537
+ } catch (e) {
538
+ console.error('[gds] web/callback: repo-token capture failed (non-blocking):', e.message);
539
+ }
540
+ }
325
541
  // The state cookie is apex-scoped now (#734) — clear it with the same
326
542
  // domain or the browser keeps it. Clear the host-only form too for any
327
- // cookie set before this change.
543
+ // cookie set before this change. Clear the repos-intent cookie the same way.
328
544
  const domainOpts = sessionCookieDomainOpts(req);
329
545
  res.clearCookie(STATE_COOKIE);
330
546
  res.clearCookie(STATE_COOKIE_LEGACY);
547
+ res.clearCookie(REPOS_COOKIE);
331
548
  if (domainOpts.domain) {
332
549
  res.clearCookie(STATE_COOKIE, domainOpts);
333
550
  res.clearCookie(STATE_COOKIE_LEGACY, domainOpts);
551
+ res.clearCookie(REPOS_COOKIE, domainOpts);
334
552
  }
335
553
  // Land back where sign-in began if a validated return target was stashed
336
- // (e.g. the builders subdomain bounced through here); else the hall.
554
+ // (e.g. the builders subdomain bounced through here); else the instance's
555
+ // configured post-login landing (default '/builders'; cloudbongos.com →
556
+ // /gate/, the orbs — task 1002240).
337
557
  const returnTo = safeReturnTo(parseCookie(req, RETURN_COOKIE));
338
558
  res.clearCookie(RETURN_COOKIE, domainOpts);
339
- res.redirect(returnTo || '/builders');
559
+ res.redirect(returnTo || postLoginLanding());
340
560
  } catch (err) {
341
561
  // Approval gate (2026-05-30): a first-time GitHub account that hasn't been
342
562
  // approved by an Archon can't enter. Send them to the hall to request
343
563
  // access rather than dead-ending on a generic error.
344
564
  if (err && err.code === 'NOT_APPROVED') {
565
+ // task 1002097: a blocked sign-in is not always a rejection. On a fresh
566
+ // or just-restarted instance the configured firstAdmin lands here for a
567
+ // few seconds — before FIRST_ADMIN is loaded / the bootstrap finishes —
568
+ // and so does anyone whose access_request an Archon is about to mark
569
+ // invited. The red dead-end looked broken, and could not be retried:
570
+ // the OAuth code is single-use, so a reload throws "State mismatch"
571
+ // instead. For those two, show a calm waiting page that polls and walks
572
+ // them in by itself. Genuine strangers still get the request-access
573
+ // page below.
574
+ let expectedSoon = false;
575
+ try {
576
+ ({ expectedSoon } = await auth.webAdmissionStatus(err.login));
577
+ } catch (probeErr) {
578
+ // Never let the classifier turn a clean 403 into a 500 — fall through
579
+ // to the existing page, which is the safe answer for a stranger.
580
+ console.error('[gds] web/callback admission classify', probeErr);
581
+ }
582
+ if (expectedSoon) {
583
+ return res.status(202).type('text/html').send(renderAccessPendingPage(err.login));
584
+ }
345
585
  return res.status(403).type('text/html').send(renderAuthErrorPage(
346
586
  'This account is not approved yet',
347
587
  'Signing in with GitHub is only for builders an Archon has already approved. If you are new, request access at the builders’ hall — an Archon will approve you, then this sign-in will let you in. If you were just approved, the Archon may still need to mark you invited; give it a moment and try again.',
@@ -535,9 +775,67 @@ module.exports = function buildAuthRouter() {
535
775
  res.clearCookie(SESSION_COOKIE, domainOpts);
536
776
  res.clearCookie(SESSION_COOKIE_LEGACY, domainOpts);
537
777
  }
778
+ // Federated single-logout (ADR 0144, task 1002238): announce the logout so the
779
+ // hub (platform-identity, enabled only on cloudbongos.com) can fan a signed
780
+ // back-channel logout out to every federated project this builder entered. A
781
+ // fire-and-forget seam EMIT — zero listeners on a non-hub instance, so it's a
782
+ // no-op there; the fan-out is best-effort server-side and never blocks or fails
783
+ // this logout (the hub session is already revoked above).
784
+ seams.emit('builder.logout', {
785
+ builderId: req.builder.id,
786
+ githubId: req.builder.github_id,
787
+ githubLogin: req.builder.github_login,
788
+ });
538
789
  res.json({ ok: true });
539
790
  });
540
791
 
792
+ // POST /auth/backchannel-logout — the INSTANCE side of federated single-logout
793
+ // (ADR 0144, task 1002238). The hub POSTs a hub-signed logout token here when a
794
+ // builder signs out of the hub; we verify it and revoke ALL of that builder's
795
+ // local sessions, so one hub logout ends the session on every federated project.
796
+ //
797
+ // rank: public — the hub-signed EdDSA logout token IS the credential (like the
798
+ // OAuth callback), NOT a builder session. Inert (404) on an instance that does
799
+ // not federate through a hub (idp unset), so a self-hosted instance exposes no
800
+ // new surface. Idempotent: an unknown/replayed subject just revokes nothing.
801
+ router.post('/auth/backchannel-logout', async (req, res) => {
802
+ const idp = auth.loadIdpConfig();
803
+ if (!idp) {
804
+ // Not a federated instance — indistinguishable from "no such route".
805
+ return res.status(404).type('text/plain').send('not found');
806
+ }
807
+ if (validateOrRespond(req, res, {
808
+ logout_token: { required: true, type: 'string', maxLength: 4096 },
809
+ })) return;
810
+ const publicKey = auth.loadHubPublicKey();
811
+ if (!publicKey) return res.fail('idp_unconfigured', { status: 503, message: 'hub public key not configured' });
812
+ let payload;
813
+ try {
814
+ payload = auth.verifyHubLogoutToken(req.body.logout_token, { publicKey, clientId: idp.clientId });
815
+ } catch (err) {
816
+ // A bad / expired / forged / wrong-audience token is an auth failure (401),
817
+ // never a server error — and never revokes anything.
818
+ return res.fail('invalid_logout_token', { status: 401, message: String(err.message || 'invalid logout token').slice(0, 160) });
819
+ }
820
+ // Anti-replay (defense-in-depth, symmetric with the CLI assertion path): honor
821
+ // a logout token's jti ONCE. A replay within the ~120s TTL is an idempotent
822
+ // no-op, so a captured token can't keep killing sessions the builder creates
823
+ // AFTER it was minted.
824
+ if (auth.logoutTokenJtiSeenAndRecord(payload.jti, payload.exp)) {
825
+ return res.json({ ok: true });
826
+ }
827
+ try {
828
+ const builder = await db.getBuilderByGithubId(payload.sub);
829
+ // Revoke ALL of this builder's local sessions (web + CLI) — the hub said sign
830
+ // them out. No-op + 200 if we don't know this builder (never leak existence).
831
+ if (builder) await db.revokeAllSessionsForBuilder(builder.id);
832
+ res.json({ ok: true });
833
+ } catch (err) {
834
+ console.error('[gds] /auth/backchannel-logout revoke', err);
835
+ res.fail('server_error', { status: 500, message: 'logout failed' });
836
+ }
837
+ });
838
+
541
839
  // POST /auth/cli-token/issue — V3.R91 / #401. Issue a fresh CLI bearer for
542
840
  // the requesting builder, gated on a WEB COOKIE session (not a bearer). The
543
841
  // browser already proves identity via the gds_session cookie; this endpoint
@@ -753,6 +1051,7 @@ module.exports = function buildAuthRouter() {
753
1051
  // open-redirect guard on the post-sign-in return target and the cross-subdomain
754
1052
  // cookie-domain decision — worth locking down independently of a running server.
755
1053
  module.exports.safeReturnTo = safeReturnTo;
1054
+ module.exports.postLoginLanding = postLoginLanding;
756
1055
  module.exports.sessionCookieDomainOpts = sessionCookieDomainOpts;
757
1056
  module.exports.reqHost = reqHost;
758
1057
  // The auth-error page renderer (task 1439): pure over the branding pack, so a
@@ -37,7 +37,39 @@ const BACKUP_SCRIPT = process.env.GDS_BACKUP_SCRIPT
37
37
  // In-memory guard: true while a trigger-initiated dump is running.
38
38
  let triggerRunning = false;
39
39
 
40
- function latestDump() {
40
+ // A dump older than this counts as STALE. The nightly timer fires daily, so 36h
41
+ // allows one missed run plus slack before we call it broken.
42
+ const STALE_AFTER_HOURS = Number(process.env.GDS_BACKUP_STALE_HOURS) || 36;
43
+
44
+ // parseDumpTimestamp — the dump filename stamp `YYYY-MM-DDTHHMMSSZ` is ALMOST
45
+ // ISO-8601 but omits the time separators, so Date.parse rejects it. Re-insert
46
+ // the colons and parse. Returns epoch ms, or null if it doesn't match.
47
+ // Pure + exported so tests can pin it without touching the filesystem.
48
+ function parseDumpTimestamp(stamp) {
49
+ const m = /^(\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})Z$/.exec(String(stamp || ''));
50
+ if (!m) return null;
51
+ const ms = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}Z`);
52
+ return Number.isFinite(ms) ? ms : null;
53
+ }
54
+
55
+ // dumpAgeHours — hours between a dump stamp and now. null when unparseable, so
56
+ // callers can tell "cannot tell" apart from "fresh". Pure (now is injectable).
57
+ function dumpAgeHours(stamp, nowMs = Date.now()) {
58
+ const ms = parseDumpTimestamp(stamp);
59
+ if (ms === null) return null;
60
+ return Math.max(0, (nowMs - ms) / 3_600_000);
61
+ }
62
+
63
+ // isStaleDump — fail LOUD on an unreadable/absent stamp. A dump we cannot date
64
+ // must not read as healthy: that silence is exactly how the cloudbongos nightly
65
+ // sat failed for 13 days (2026-07-15 → 07-28) with nothing noticing (1002479).
66
+ function isStaleDump(stamp, nowMs = Date.now(), staleAfterHours = STALE_AFTER_HOURS) {
67
+ const age = dumpAgeHours(stamp, nowMs);
68
+ if (age === null) return true;
69
+ return age > staleAfterHours;
70
+ }
71
+
72
+ function latestDump(nowMs = Date.now()) {
41
73
  let files;
42
74
  try {
43
75
  files = fs.readdirSync(BACKUP_DIR);
@@ -54,7 +86,18 @@ function latestDump() {
54
86
  try { size_bytes = fs.statSync(full).size; } catch { /* ok */ }
55
87
  // Filename: <db>-<YYYY-MM-DDTHHMMSSZ>.sql.gz — extract timestamp from it.
56
88
  const ts = name.replace(`${DB_NAME}-`, '').replace('.sql.gz', '');
57
- return { filename: name, timestamp: ts, size_bytes, count: dumps.length };
89
+ const age = dumpAgeHours(ts, nowMs);
90
+ return {
91
+ filename: name,
92
+ timestamp: ts,
93
+ size_bytes,
94
+ count: dumps.length,
95
+ // Staleness is reported, not just implied by the timestamp, so a consumer
96
+ // (CLI, dashboard, alert) can act on one boolean instead of re-deriving it.
97
+ age_hours: age === null ? null : Math.round(age * 10) / 10,
98
+ stale: isStaleDump(ts, nowMs),
99
+ stale_after_hours: STALE_AFTER_HOURS,
100
+ };
58
101
  }
59
102
 
60
103
  module.exports = function buildBackupRouter() {
@@ -68,6 +111,11 @@ module.exports = function buildBackupRouter() {
68
111
  backup_dir: BACKUP_DIR,
69
112
  db_name: DB_NAME,
70
113
  latest_dump: dump,
114
+ // Top-level too, so a consumer gets a health signal WITHOUT having to
115
+ // special-case latest_dump===null. No dump at all is the worst state,
116
+ // not a neutral one, so it reports stale.
117
+ stale: dump ? dump.stale : true,
118
+ age_hours: dump ? dump.age_hours : null,
71
119
  trigger_running: triggerRunning,
72
120
  });
73
121
  }));
@@ -102,3 +150,11 @@ module.exports = function buildBackupRouter() {
102
150
 
103
151
  return router;
104
152
  };
153
+
154
+ // Pure helpers exported for tests (tests/backup_cli.mjs) — importing the real
155
+ // implementations beats re-implementing them in the test, which is how a test
156
+ // can pass while the shipped code is wrong.
157
+ module.exports.parseDumpTimestamp = parseDumpTimestamp;
158
+ module.exports.dumpAgeHours = dumpAgeHours;
159
+ module.exports.isStaleDump = isStaleDump;
160
+ module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
@@ -0,0 +1,90 @@
1
+ // src/bongos/routes/github.js — GitHub repo list/create for the signed-in user, the
2
+ // server half of the onboarding wizard's repo picker (adopt) + auto-create (fresh).
3
+ // Task 1002099 / ADR 0143.
4
+ //
5
+ // These act on the user's OWN GitHub identity, so they live in core auth alongside
6
+ // the sign-in flow that captured the token — not the provisioning module (whose
7
+ // charter holds no tokens / calls no cloud API, ADR 0111 §2). They read the
8
+ // short-lived, secret-box-encrypted `public_repo` token the web callback stored
9
+ // (db.js github_oauth_tokens). Two "please reconnect / fall back" signals the wizard
10
+ // branches on:
11
+ // • no stored token → repo_scope_required (403) → prompt a repos sign-in
12
+ // • secret-box unconfigured → github_repos_unavailable (503) → manual repo entry
13
+ // Both are behind requireBuilder + the router-wide rate-limit middleware.
14
+
15
+ const express = require('express');
16
+ const auth = require('../auth');
17
+ const db = require('../db');
18
+ const secretBox = require('../secret-box');
19
+ const { listUserRepos, createUserRepo } = require('../github-repos');
20
+ const { validateOrRespond } = require('./_helpers');
21
+
22
+ // GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
23
+ // 400; anything GitHub still rejects surfaces as the 422→409 name_taken path.
24
+ const REPO_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
25
+
26
+ // Resolve the caller's stored repo token, or send the right failure and return null.
27
+ // Centralizes the 503-vs-403 decision both endpoints share.
28
+ async function resolveRepoToken(req, res) {
29
+ if (!secretBox.isConfigured()) {
30
+ res.fail('github_repos_unavailable', { status: 503, message: 'Repo access is not configured on this server. Enter the repository manually.' });
31
+ return null;
32
+ }
33
+ const row = await db.getGithubToken({ builderId: req.builder.id });
34
+ if (!row) {
35
+ res.fail('repo_scope_required', { status: 403, message: 'Grant repository access to list or create repos.' });
36
+ return null;
37
+ }
38
+ return row.token;
39
+ }
40
+
41
+ module.exports = function buildGithubRouter() {
42
+ const router = express.Router();
43
+
44
+ // rank: any-builder — acts only on the caller's OWN GitHub identity (the token
45
+ // they granted). No rank gate beyond being a signed-in builder.
46
+ router.get('/github/repos', auth.requireBuilder, async (req, res) => {
47
+ const token = await resolveRepoToken(req, res);
48
+ if (!token) return;
49
+ try {
50
+ const q = typeof req.query.q === 'string' ? req.query.q.slice(0, 100) : '';
51
+ const repos = await listUserRepos(token, { q });
52
+ res.json({ repos });
53
+ } catch (err) {
54
+ if (err && err.status === 401) {
55
+ // The stored token was revoked/expired GitHub-side — drop it and re-prompt.
56
+ await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
57
+ return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to list your repos.' });
58
+ }
59
+ console.error('[gds] GET /github/repos', err && err.message);
60
+ res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again, or enter the repository manually.' });
61
+ }
62
+ });
63
+
64
+ // rank: any-builder — creates a repo in the caller's OWN account.
65
+ router.post('/github/repos', auth.requireBuilder, async (req, res) => {
66
+ if (validateOrRespond(req, res, { name: { required: true, type: 'string', maxLength: 100 } })) return;
67
+ const name = String((req.body && req.body.name) || '').trim();
68
+ if (!REPO_NAME_RE.test(name)) {
69
+ return res.fail('bad_repo_name', { status: 400, message: 'Repository names use letters, numbers, and . _ - (1–100 chars).' });
70
+ }
71
+ const token = await resolveRepoToken(req, res);
72
+ if (!token) return;
73
+ try {
74
+ const repo = await createUserRepo(token, { name });
75
+ res.status(201).json({ repo });
76
+ } catch (err) {
77
+ if (err && err.code === 'name_taken') {
78
+ return res.fail('name_taken', { status: 409, message: 'You already have a repository with that name. Pick another, or adopt the existing one.' });
79
+ }
80
+ if (err && err.status === 401) {
81
+ await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
82
+ return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to create the repo.' });
83
+ }
84
+ console.error('[gds] POST /github/repos', err && err.message);
85
+ res.fail('github_error', { status: 502, message: 'GitHub could not create the repo. Try again, or create it manually.' });
86
+ }
87
+ });
88
+
89
+ return router;
90
+ };
@@ -32,7 +32,7 @@ const { corsPublicGet, asyncHandler } = require('./_helpers');
32
32
  const { clientBranding } = require('../../branding');
33
33
  const { clientModules, isModuleEnabled } = require('../../modules');
34
34
  const { loadRepoInfo } = require('../repo-info');
35
- const { authConfigured, openEnrollmentEnabled } = require('../auth');
35
+ const { authConfigured, openEnrollmentEnabled, loadIdpConfig } = require('../auth');
36
36
  const { RANK_TIER } = require('../permission-path-check');
37
37
 
38
38
  // The manifest format id — bumped when the SHAPE changes so a CLI can detect a
@@ -59,6 +59,7 @@ function buildInstanceManifest({
59
59
  authConfigured: authOk,
60
60
  openEnrollment,
61
61
  devBoxEnabled,
62
+ idp = null,
62
63
  } = {}) {
63
64
  const b = branding || {};
64
65
  const rankLabels = b.rankLabels || {};
@@ -89,8 +90,15 @@ function buildInstanceManifest({
89
90
  ranks,
90
91
  repo: repoOut,
91
92
  auth: {
92
- methods: authOk ? AUTH_METHODS.slice() : [],
93
- configured: !!authOk,
93
+ // Sign-in works when the instance has its OWN GitHub app (authOk) OR it
94
+ // federates through the hub (idp) — a hosted instance has no app of its own.
95
+ methods: (authOk || idp) ? AUTH_METHODS.slice() : [],
96
+ configured: !!authOk || !!idp,
97
+ // Federation hint (ADR 0141 §1): present only when this instance federates.
98
+ // origin + client_id are PUBLIC (like an OAuth client_id); the client SECRET
99
+ // is never projected here. The CLI reads this to route device-flow + web
100
+ // sign-in through the hub instead of expecting a local GitHub app.
101
+ idp: idp || null,
94
102
  },
95
103
  admission: { policy: openEnrollment ? 'open' : 'invite-only' },
96
104
  devBox: { available: !!devBoxEnabled },
@@ -109,6 +117,7 @@ module.exports = function buildInstanceRouter() {
109
117
  // err.message if branding/modules fail to resolve (misconfigured instance) —
110
118
  // never leaks internals.
111
119
  router.get('/instance', asyncHandler('GET /instance', (_req, res) => {
120
+ const idpCfg = loadIdpConfig();
112
121
  res.json(buildInstanceManifest({
113
122
  branding: clientBranding(),
114
123
  modules: clientModules(),
@@ -117,6 +126,9 @@ module.exports = function buildInstanceRouter() {
117
126
  authConfigured: authConfigured(),
118
127
  openEnrollment: openEnrollmentEnabled(),
119
128
  devBoxEnabled: isModuleEnabled('dev-box'),
129
+ // Project OUT the client secret — the manifest is public; only origin +
130
+ // client_id (both public) advertise federation.
131
+ idp: idpCfg ? { origin: idpCfg.origin, client_id: idpCfg.clientId } : null,
120
132
  }));
121
133
  }, { message: false, errorCode: 'instance_manifest_failed' }));
122
134