@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
@@ -194,6 +194,115 @@ function regenerateApiArtifacts({ instanceDir, coreRoot }, run = spawnSync, fsIm
194
194
  return { ok: true, ran };
195
195
  }
196
196
 
197
+ // The instance's GITIGNORED WHOLE-FILE navigation docs (ADR 0110). On the monorepo these
198
+ // regenerate at deploy (deploy-prod.sh) + box-fetch and are gitignored so no branch ever diffs
199
+ // them. STANDALONE instances (ADR 0108) never got that step: their bespoke deploy regenerated
200
+ // nothing and init/upgrade only did the API docs, so a fresh instance shipped with an empty docs/
201
+ // and nothing kept the index live (task 1002314, the owner's "docs didn't transfer from primea").
202
+ // This is the standalone sibling of deploy-prod.sh's regen: run the generator(s) against the
203
+ // INSTANCE root, best-effort + LOUD — a broken generator must surface (::ERROR::) but must NEVER
204
+ // fail the deploy/bump (a stale nav doc is tolerable; a failed deploy is not — ADR 0110 §2
205
+ // "accepted staleness").
206
+ //
207
+ // SCOPE — deliberately just session-index (the one gitignored whole-file doc that is INSTANCE
208
+ // content). Excluded on purpose:
209
+ // • repo-map.md — CORE-scoped: gen-repo-map writes to resolveCoreRoot() (the platform code map),
210
+ // not the instance. A pure standalone instance runs core from node_modules and has ~no code of
211
+ // its own to map, so an instance-level repo-map is empty/meaningless; the core ships its own.
212
+ // (An instance-scoped map for BROWNFIELD/adopted repos is a separate enhancement — task 1001655.)
213
+ // • file-map.md + API docs — TRACKED/committed (seeded at init, refreshed at ship/upgrade);
214
+ // regenerating them on the box would perpetually dirty the tree that deploy.sh `git reset
215
+ // --hard`es. file-map is also HYBRID (hand-written prose + generated sections, ADR 0110) so it
216
+ // cannot leave git; seeding it at standup is a follow-up (needs a scaffolded file-map.md).
217
+ // • diagrams + atlas.json — NOT instance content: they are CORE-scoped generated assets that
218
+ // live under resolveCoreRoot(). They are handled separately by healDocAssets() below, because
219
+ // an upgrade is precisely what destroys them (task 1002441).
220
+ const NAV_WHOLE_FILE_GENERATORS = ['gen-session-index.js'];
221
+ function regenerateNavDocs({ instanceDir, coreRoot, generators = NAV_WHOLE_FILE_GENERATORS }, run = spawnSync, fsImpl = fs) {
222
+ const env = { ...process.env, CLOUDBONGOS_INSTANCE_ROOT: instanceDir };
223
+ const ran = [];
224
+ const failed = [];
225
+ for (const gen of generators) {
226
+ const script = path.join(coreRoot, 'scripts', 'gds', gen);
227
+ if (!fsImpl.existsSync(script)) { failed.push({ gen, reason: `not found in core (${coreRoot})` }); continue; }
228
+ const r = run(process.execPath, [script], { cwd: instanceDir, env, stdio: 'inherit' });
229
+ if (!r || r.error || r.status !== 0) {
230
+ // LOUD per ADR 0110 §2 — journald greps ::ERROR:: so a broken generator alarms.
231
+ failed.push({ gen, reason: r && r.error ? r.error.message : `exited ${r && r.status}` });
232
+ continue;
233
+ }
234
+ ran.push(gen);
235
+ }
236
+ return { ok: failed.length === 0, ran, failed };
237
+ }
238
+
239
+ // The CORE-scoped generated doc assets that back two builders'-hall surfaces:
240
+ //
241
+ // modules/hall-ui/public/atlas.json → the Repo Atlas (/builders/atlas)
242
+ // docs/onboarding/diagrams/<n>.svg+.png → the system maps (/builders/diagrams)
243
+ //
244
+ // Both are GITIGNORED (.gitignore: `modules/hall-ui/public/atlas.json`,
245
+ // `docs/onboarding/diagrams/*.svg|*.png` — ADR 0122 / task 1002027) and both are generated INTO
246
+ // the vendored core root. So an upgrade DESTROYS them: npmInstall replaces node_modules wholesale
247
+ // and the new tarball carries only the .mmd sources. Nothing else puts them back —
248
+ // • packaging does not render (package-core.js has no render step, and the renders are
249
+ // gitignored so a git checkout has none to copy), and
250
+ // • a standalone instance's pull-deploy typically short-circuits on an unchanged git SHA, which
251
+ // an upgrade does not move — so its regen hooks never fire.
252
+ // Result before this healer: /builders/atlas 404'd and the diagrams page showed broken images
253
+ // after every `bongos upgrade`, until some unrelated commit happened to land (task 1002441).
254
+ //
255
+ // Both generators resolve their own output dir from __dirname, so running the CORE's copy always
256
+ // writes into coreRoot regardless of cwd. Idempotent by design — each asset is regenerated only
257
+ // when actually absent, so this is also safe to call from a deploy loop.
258
+ //
259
+ // Best-effort + LOUD, per ADR 0110 §2 "accepted staleness": a stale diagram is tolerable, a failed
260
+ // upgrade is not. render-diagrams.sh additionally needs a system Chromium (ADR 0122); where none is
261
+ // installed it fails and we say so via ::ERROR:: rather than aborting the bump.
262
+ const DOC_ASSET_GENERATORS = {
263
+ atlas: { rel: path.join('scripts', 'gds', 'gen-atlas.js'), kind: 'node' },
264
+ diagrams: { rel: path.join('scripts', 'render-diagrams.sh'), kind: 'bash' },
265
+ };
266
+
267
+ // Which of the two asset groups are missing under coreRoot. Exported for testing.
268
+ function missingDocAssets(coreRoot, fsImpl = fs) {
269
+ const missing = [];
270
+ if (!fsImpl.existsSync(path.join(coreRoot, 'modules', 'hall-ui', 'public', 'atlas.json'))) {
271
+ missing.push('atlas');
272
+ }
273
+ const diagramDir = path.join(coreRoot, 'docs', 'onboarding', 'diagrams');
274
+ let sources = [];
275
+ try {
276
+ sources = fsImpl.readdirSync(diagramDir).filter((f) => f.endsWith('.mmd'));
277
+ } catch { sources = []; } // no diagram dir in this core → nothing to render
278
+ const unrendered = sources.some((src) => {
279
+ const base = path.join(diagramDir, src.replace(/\.mmd$/, ''));
280
+ return !fsImpl.existsSync(`${base}.svg`) || !fsImpl.existsSync(`${base}.png`);
281
+ });
282
+ if (unrendered) missing.push('diagrams');
283
+ return missing;
284
+ }
285
+
286
+ function healDocAssets({ coreRoot }, run = spawnSync, fsImpl = fs) {
287
+ const missing = missingDocAssets(coreRoot, fsImpl);
288
+ if (missing.length === 0) return { ok: true, ran: [], failed: [], missing: [] };
289
+ const ran = [];
290
+ const failed = [];
291
+ for (const group of missing) {
292
+ const gen = DOC_ASSET_GENERATORS[group];
293
+ const script = path.join(coreRoot, gen.rel);
294
+ if (!fsImpl.existsSync(script)) { failed.push({ group, reason: `not found in core (${gen.rel})` }); continue; }
295
+ const argv = gen.kind === 'bash' ? ['bash', [script]] : [process.execPath, [script]];
296
+ const r = run(argv[0], argv[1], { cwd: coreRoot, stdio: 'inherit' });
297
+ if (!r || r.error || r.status !== 0) {
298
+ failed.push({ group, reason: r && r.error ? r.error.message : `exited ${r && r.status}` });
299
+ continue;
300
+ }
301
+ ran.push(group);
302
+ }
303
+ return { ok: failed.length === 0, ran, failed, missing };
304
+ }
305
+
197
306
  function restartService({ service, restartCmd }, run = spawnSync) {
198
307
  if (restartCmd) {
199
308
  const parts = restartCmd.split(/\s+/);
@@ -295,6 +404,91 @@ async function pollHealth(healthUrl, deps = {}) {
295
404
  }
296
405
  }
297
406
 
407
+ // ---- integrity-pin verification (task 1002216 / audit H8, finding F1) --------
408
+ // The packager (package-core.js) computes tree_sha256 = canonicalTreeHash over the
409
+ // redacted core files (+ the two synthesized files) and records it, the per-file
410
+ // hashes, AND the exact tarball bytes' sha256 in a side-car manifest — stating a
411
+ // consumer "verifies an install by recomputing it from the unpacked tree." Until now
412
+ // nothing on the CONSUME side did: `bongos upgrade` compared only the version STRING,
413
+ // so a corrupted / truncated / swapped tarball whose package.json still read the target
414
+ // version installed silently — the R88-class failure this goal exists to prevent.
415
+ // The three functions below close that gap: recompute the pin over the INSTALLED tree
416
+ // and assert it equals the TRUSTED reference manifest.
417
+
418
+ // Read + parse a manifest JSON; null on any failure (missing / unparseable).
419
+ function readManifest(file, fsImpl = fs) {
420
+ try { return JSON.parse(fsImpl.readFileSync(file, 'utf8')); } catch { return null; }
421
+ }
422
+
423
+ // Resolve the trusted reference pin for this bump:
424
+ // • VENDORED — the committed side-car `vendor/<artifact>-<v>.manifest.json` (carries
425
+ // tree_sha256 + files[] + the exact tarball.sha256). It lives OUTSIDE the tarball
426
+ // (version-controlled in the consumer repo), so it detects a swapped/corrupted
427
+ // tarball. This is the strong reference.
428
+ // • REGISTRY / no side-car — the inner manifest `<core>/.bongos-core.json` that ships
429
+ // INSIDE the package (tree_sha256 + files[], no tarball.*). Self-referential (a full
430
+ // swap regenerates it too), so it is a self-consistency check backing npm's own
431
+ // registry integrity rather than an independent one — flagged `strong:false`.
432
+ function resolveReferenceManifest({ instanceDir, coreDir, targetVersion }, fsImpl = fs) {
433
+ const sidecarPath = path.join(instanceDir, 'vendor', `${ARTIFACT}-${targetVersion}.manifest.json`);
434
+ const sidecar = readManifest(sidecarPath, fsImpl);
435
+ if (sidecar && sidecar.tree_sha256) {
436
+ const tgz = path.join(instanceDir, 'vendor', `${ARTIFACT}-${targetVersion}.tgz`);
437
+ return { manifest: sidecar, tgzPath: fsImpl.existsSync(tgz) ? tgz : null, source: 'vendored side-car manifest', strong: true };
438
+ }
439
+ const inner = readManifest(path.join(coreDir, '.bongos-core.json'), fsImpl);
440
+ if (inner && inner.tree_sha256) return { manifest: inner, tgzPath: null, source: 'installed inner manifest', strong: false };
441
+ return { manifest: null, tgzPath: null, source: null, strong: false };
442
+ }
443
+
444
+ // Recompute the integrity pin over the installed core tree and compare to `referenceManifest`.
445
+ // Reuses the PACKAGER's exact hash functions (canonicalTreeHash / sha256hex) so the consume-side
446
+ // recompute can never drift from the produce-side pin. Returns { ran, ok, checks, problems };
447
+ // ran=false ⇒ the manifest carried no pin to check against.
448
+ function verifyInstalledPin({ coreDir, referenceManifest, tgzPath }, deps = {}) {
449
+ const fsImpl = deps.fs || fs;
450
+ const pc = deps.packageCore || require('./package-core');
451
+ const checks = [];
452
+ const problems = [];
453
+ if (!referenceManifest || !referenceManifest.tree_sha256) return { ran: false, ok: false, checks, problems };
454
+
455
+ // (a) exact tarball bytes (vendored path only) — the strongest single check: the bytes
456
+ // npm actually installs, hashed and compared to the externally-held pin.
457
+ const tb = referenceManifest.tarball;
458
+ if (tgzPath && tb && tb.sha256 && fsImpl.existsSync(tgzPath)) {
459
+ const got = pc.sha256hex(fsImpl.readFileSync(tgzPath));
460
+ const ok = got === tb.sha256;
461
+ checks.push({ name: 'tarball.sha256', ok });
462
+ if (!ok) problems.push(`vendored tarball bytes sha256 ${got.slice(0, 12)}… ≠ pinned ${String(tb.sha256).slice(0, 12)}…`);
463
+ }
464
+
465
+ // (b) installed tree — recompute each pinned file's sha256 from the unpacked tree, rebuild
466
+ // the canonical tree hash (using the manifest's already-canonical modes), assert equality.
467
+ // Catches partial extraction, post-install corruption, or a registry package that diverged.
468
+ const files = Array.isArray(referenceManifest.files) ? referenceManifest.files : [];
469
+ if (files.length) {
470
+ const recomputed = [];
471
+ let missing = 0, changed = 0;
472
+ for (const f of files) {
473
+ const abs = path.join(coreDir, ...String(f.path).split('/')); // POSIX manifest path → native
474
+ let buf;
475
+ try { buf = fsImpl.readFileSync(abs); } catch { missing++; problems.push(`missing installed file: ${f.path}`); continue; }
476
+ const sha = pc.sha256hex(buf);
477
+ if (sha !== f.sha256) { changed++; problems.push(`content mismatch: ${f.path}`); }
478
+ recomputed.push({ path: f.path, mode: f.mode, sha256: sha });
479
+ }
480
+ const treeHash = pc.canonicalTreeHash(recomputed);
481
+ const ok = missing === 0 && changed === 0 && treeHash === referenceManifest.tree_sha256;
482
+ checks.push({ name: 'tree_sha256', ok, missing, changed, file_count: files.length });
483
+ if (!ok && missing === 0 && changed === 0) {
484
+ problems.push(`recomputed tree_sha256 ${treeHash.slice(0, 12)}… ≠ pinned ${String(referenceManifest.tree_sha256).slice(0, 12)}…`);
485
+ }
486
+ }
487
+
488
+ const ran = checks.length > 0;
489
+ return { ran, ok: ran && problems.length === 0, checks, problems };
490
+ }
491
+
298
492
  // Auto-rollback (task 2149): a bump failed AFTER the pin was moved — an install or migrate
299
493
  // error, or a failed post-restart health check. Revert the code pin to the pre-upgrade
300
494
  // snapshot, reinstall the previous core, restart, re-verify health, and record the reversal
@@ -390,6 +584,18 @@ async function runUpgrade(opts, deps = {}) {
390
584
  log(` ✓ pre-flight: ${mods.note}`);
391
585
  if (!mods.ok) return { ok: false, error: `module pre-check failed: ${mods.blocking.map((b) => b.key).join(', ')} incompatible with core ${targetVersion}` };
392
586
 
587
+ // 1b. mandatory post-restart health check (audit H9 / task 1002222, finding F5). A bump that
588
+ // brings the new core live via a managed restart (--service / --restart-cmd) MUST confirm it
589
+ // actually serves — an upgrade that can't be health-checked can't be reported as a success.
590
+ // Enforced here in PRE-FLIGHT, before anything is mutated, so there is nothing to roll back:
591
+ // if this bump will restart the service and the check isn't explicitly opted out, a
592
+ // --health-url is required. (No managed restart ⇒ nothing is brought live here ⇒ the check
593
+ // does not apply; the operator restarts + verifies separately.)
594
+ const willRestart = !!(opts.service || opts.restartCmd);
595
+ if (!dryRun && opts.healthCheck !== false && willRestart && !opts.healthUrl) {
596
+ return { ok: false, error: 'post-restart health check is mandatory for a bump that restarts the service, but no --health-url was given.\n Pass --health-url <url> to confirm the new core serves after restart, or --no-health-check to explicitly skip the confirmation.' };
597
+ }
598
+
393
599
  // 2. bump the pin. Two mutually exclusive core sources (ADR 0134, task 2090):
394
600
  // REGISTRY (--registry): pin @bongos/core@<version> + ensure the env-fed .npmrc, so
395
601
  // the `npm install` below pulls the PRIVATE package from the registry (needs NPM_TOKEN).
@@ -429,6 +635,38 @@ async function runUpgrade(opts, deps = {}) {
429
635
  }
430
636
  log(' ✓ npm install');
431
637
 
638
+ // 3b. verify the integrity pin (audit H8 / task 1002216, finding F1). Recompute tree_sha256 over
639
+ // the freshly-installed core (+ the exact vendored tarball bytes) and assert it matches the
640
+ // TRUSTED reference manifest — the pin the packager produced but the consume side never
641
+ // re-checked. Runs BEFORE migrate so a corrupted core can never touch the DB. A MISMATCH is
642
+ // fatal → rollback (like an install/migrate failure). A MISSING reference (no side-car AND no
643
+ // inner manifest) can't be verified: a warning by default, fatal only with --require-pin.
644
+ if (!opts.skipPinVerify) {
645
+ const coreDir = path.join(instanceDir, 'node_modules', CORE_PKG);
646
+ const ref = (deps.resolveReferenceManifest || resolveReferenceManifest)({ instanceDir, coreDir, targetVersion }, fsImpl);
647
+ if (!ref.manifest) {
648
+ const msg = `integrity pin: no reference manifest (looked for vendor/${ARTIFACT}-${targetVersion}.manifest.json and ${CORE_PKG}/.bongos-core.json) — cannot verify this install`;
649
+ if (opts.requirePin) {
650
+ const error = `${msg} (--require-pin)`;
651
+ if (rollbackOnFailure && snapshot) { const rb = await doRollback('integrity pin unverifiable (--require-pin)'); return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb }; }
652
+ return { ok: false, error, fromVersion };
653
+ }
654
+ err(` ! ${msg} — pass --require-pin to treat this as fatal.`);
655
+ } else {
656
+ const pin = (deps.verifyInstalledPin || verifyInstalledPin)({ coreDir, referenceManifest: ref.manifest, tgzPath: ref.tgzPath }, deps);
657
+ if (!pin.ok) {
658
+ const detail = pin.problems.slice(0, 6).join('; ') + (pin.problems.length > 6 ? `; +${pin.problems.length - 6} more` : '');
659
+ const error = `integrity pin MISMATCH (${ref.source}) — installed core does not match its pin: ${detail}`;
660
+ if (rollbackOnFailure && snapshot) {
661
+ const rb = await doRollback('integrity pin mismatch');
662
+ return { ok: false, error, fromVersion, toVersion: targetVersion, rolledBack: true, rollback: rb };
663
+ }
664
+ return { ok: false, error, fromVersion };
665
+ }
666
+ log(` ✓ integrity pin verified (${pin.checks.map((c) => c.name).join(' + ')}) against the ${ref.source}${ref.strong ? '' : ' — self-consistency check; npm registry integrity is the primary guard'}`);
667
+ }
668
+ } else log(' • integrity-pin verify skipped (--skip-pin-verify)');
669
+
432
670
  // 4. migrate (additive core_* migrations).
433
671
  if (!opts.skipMigrate) {
434
672
  const mig = runMigrate(instanceDir, run);
@@ -458,7 +696,20 @@ async function runUpgrade(opts, deps = {}) {
458
696
  const regen = (deps.regenerateApiArtifacts || regenerateApiArtifacts)({ instanceDir, coreRoot }, run);
459
697
  if (regen.ok) log(` ✓ regenerated API docs + typed client from core ${targetVersion}`);
460
698
  else err(` ! API-artifact regen incomplete (${regen.reason}) — ran ${regen.ran.join(', ') || 'nothing'}; the next ship's fitness gate refreshes them.`);
461
- } else log(' • API-artifact regen skipped (--skip-regen-docs)');
699
+ // 5c. regenerate the whole-file navigation docs (repo-map + session-index) from the new
700
+ // core against the instance root (ADR 0110 / task 1002314). Best-effort + LOUD: a broken
701
+ // generator alarms via ::ERROR:: but never fails the bump.
702
+ const nav = (deps.regenerateNavDocs || regenerateNavDocs)({ instanceDir, coreRoot }, run);
703
+ if (nav.ok) log(` ✓ regenerated navigation docs (${nav.ran.join(', ')}) from core ${targetVersion}`);
704
+ else err(` ! ::ERROR:: navigation-doc regen incomplete — ran ${nav.ran.join(', ') || 'nothing'}; failed ${nav.failed.map((f) => `${f.gen} (${f.reason})`).join(', ')}. The next deploy retries.`);
705
+ // 5d. re-generate the CORE-scoped doc assets this very upgrade just wiped — the Repo Atlas
706
+ // data file + the onboarding diagram renders (task 1002441). See healDocAssets() for why
707
+ // nothing else puts them back. Best-effort + LOUD, same contract as 5c.
708
+ const heal = (deps.healDocAssets || healDocAssets)({ coreRoot }, run);
709
+ if (heal.missing.length === 0) log(' • doc assets intact (atlas + diagram renders present)');
710
+ else if (heal.ok) log(` ✓ regenerated doc assets wiped by the core swap (${heal.ran.join(', ')})`);
711
+ else err(` ! ::ERROR:: doc-asset regen incomplete — ran ${heal.ran.join(', ') || 'nothing'}; failed ${heal.failed.map((f) => `${f.group} (${f.reason})`).join(', ')}. /builders/atlas or /builders/diagrams may be broken until the next deploy retries (diagram renders need a system Chromium — ADR 0122).`);
712
+ } else log(' • doc regen skipped (--skip-regen-docs)');
462
713
 
463
714
  // 6. restart.
464
715
  const rs = restartService({ service: opts.service, restartCmd: opts.restartCmd }, run);
@@ -472,11 +723,17 @@ async function runUpgrade(opts, deps = {}) {
472
723
  log(` ${versionOk ? '✓' : '!'} installed core version: ${installed || '(unknown)'}${versionOk ? '' : ` (expected ${targetVersion})`}`);
473
724
 
474
725
  let healthOk = null;
475
- if (opts.healthUrl) {
726
+ if (opts.healthCheck === false) {
727
+ log(' • post-restart health check skipped (--no-health-check) — success is NOT confirmed against a live endpoint.');
728
+ } else if (opts.healthUrl) {
476
729
  const h = await pollHealth(opts.healthUrl, deps);
477
730
  healthOk = h.ok;
478
731
  if (h.ok) log(` ✓ health ${opts.healthUrl} → ${h.status}${h.attempts > 1 ? ` (ready after ${h.attempts} tries)` : ''}`);
479
732
  else err(` ! health ${opts.healthUrl} not ready after ${h.attempts} tr${h.attempts === 1 ? 'y' : 'ies'}: ${h.error || 'status ' + h.status}`);
733
+ } else {
734
+ // Reachable only when NO managed restart happened (pre-flight requires a URL when one will):
735
+ // the new core is installed on disk, but nothing was brought live here to health-check.
736
+ log(' • no managed restart + no --health-url — new core installed but not confirmed serving; restart the instance + health-check it to complete the bump.');
480
737
  }
481
738
 
482
739
  // A failed post-restart health check means the new core is serving broken — auto-revert (task
@@ -514,6 +771,9 @@ function parseArgs(argv) {
514
771
  skipMigrate: hasFlag('--skip-migrate', argv),
515
772
  skipMaterialize: hasFlag('--skip-materialize', argv),
516
773
  skipRegenDocs: hasFlag('--skip-regen-docs', argv),
774
+ skipPinVerify: hasFlag('--skip-pin-verify', argv), // task 1002216: opt out of the post-install integrity-pin check
775
+ requirePin: hasFlag('--require-pin', argv), // task 1002216: a MISSING reference manifest becomes fatal (not just a warning)
776
+ healthCheck: !hasFlag('--no-health-check', argv), // task 1002222: post-restart health check is ON by default (finding F5/H9)
517
777
  rollbackOnFailure: !hasFlag('--no-rollback-on-failure', argv), // ADR 0100: auto-revert ON by default (task 2149)
518
778
  };
519
779
  }
@@ -533,16 +793,19 @@ async function main(argv = process.argv.slice(2)) {
533
793
  ' --instance <dir> consumer instance root (default: cwd)',
534
794
  ' --service <unit> systemd unit to restart after applying',
535
795
  ' --restart-cmd <cmd> explicit restart command (overrides --service)',
536
- ' --health-url <url> poll after restart (~30s) until 2xx; still-failing is a warning',
796
+ ' --health-url <url> poll after restart (~30s) until 2xx; a still-failing check fails the bump (auto-rollback). REQUIRED for a bump that restarts the service, unless --no-health-check.',
797
+ ' --no-health-check explicitly skip the post-restart health confirmation (default: the check is ON — a bump that restarts the service must confirm it serves)',
537
798
  ' --skip-migrate do not run `npm run migrate`',
538
799
  ' --skip-materialize do not refresh .claude/',
539
800
  ' --skip-regen-docs do not regenerate the OpenAPI spec + typed client from the new core',
540
- ' --no-rollback-on-failure do NOT auto-revert on a failed install/migrate/health check (default: auto-rollback ON)',
801
+ ' --skip-pin-verify do not verify the integrity pin (tree_sha256 + tarball.sha256) after install',
802
+ ' --require-pin treat a MISSING reference manifest as fatal (default: warn — a MISMATCH is always fatal)',
803
+ ' --no-rollback-on-failure do NOT auto-revert on a failed install/pin-verify/migrate/health check (default: auto-rollback ON)',
541
804
  ' --dry-run show the plan; change nothing',
542
805
  ' --force skip the clean-tree + same-version guards',
543
806
  '',
544
- 'Full bump: pre-flight → bump pin → npm install → migrate → materialize .claude → regen API docs+client → restart → verify.',
545
- 'On a failure after the pin moves (install/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
807
+ 'Full bump: pre-flight → bump pin → npm install → verify integrity pin → migrate → materialize .claude → regen API docs+client → restart → verify.',
808
+ 'On a failure after the pin moves (install/pin-verify/migrate/health), auto-rollback reverts the pin, reinstalls the previous core, restarts, and records a rolled_back ledger row (the DB is not rolled back — core migrations are additive).',
546
809
  ].join('\n') + '\n');
547
810
  return 0;
548
811
  }
@@ -559,7 +822,8 @@ async function main(argv = process.argv.slice(2)) {
559
822
  module.exports = {
560
823
  runUpgrade, parseArgs, main, rollback,
561
824
  versionFromTgzPath, versionFromDep, readInstalledCoreVersion, readPinnedCoreVersion, writePin, writeRegistryPin, restorePin,
562
- vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, restartService, recordLedger, pollHealth,
825
+ vendorTarball, gitTreeClean, preflightModules, npmInstall, runMigrate, regenerateApiArtifacts, regenerateNavDocs, NAV_WHOLE_FILE_GENERATORS, missingDocAssets, healDocAssets, DOC_ASSET_GENERATORS, restartService, recordLedger, pollHealth,
826
+ readManifest, resolveReferenceManifest, verifyInstalledPin, // task 1002216 (audit H8) — integrity-pin verification
563
827
  CORE_PKG, ARTIFACT,
564
828
  };
565
829
 
@@ -110,6 +110,16 @@ apply_migration() {
110
110
  >/dev/null
111
111
  }
112
112
 
113
+ # --- Forward-safety notice (task 1002223 / audit H10) ---------------------
114
+ # `bongos upgrade` auto-rollback reverts the code pin but NOT the DB — safe only if core migrations
115
+ # are forward-safe (the PREVIOUS code still runs on the new schema). Surface — never block — any
116
+ # UNACKNOWLEDGED forward-unsafe core migration so an operator knows a rollback past it would strand
117
+ # old code on new schema. Fail-closed enforcement is the unit gate (tests/migrations_forward_safe.mjs);
118
+ # here it is advisory (`|| true`) so a lint hiccup can NEVER break a deploy.
119
+ if [ -f "$CORE_ROOT/scripts/gds/lint-migrations.js" ]; then
120
+ node "$CORE_ROOT/scripts/gds/lint-migrations.js" --warn --dir "$CORE_ROOT/migrations" || true
121
+ fi
122
+
113
123
  # Build the ordered migration list and apply it as ONE numeric sequence by
114
124
  # filename stem, regardless of directory:
115
125
  # $CORE_ROOT/migrations/*.sql — core: schema + platform-universal seeds (EVERY instance).
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/status-mirror-sync.js — publish the status surface to the off-box status
3
- // mirror (lars589/amazonprimea-status → GitHub Pages → status.amazonprimea.com).
3
+ // mirror (a separate repo → a static Pages host → the instance's status domain).
4
4
  //
5
5
  // SOURCE PATH (BV1.R84 / task 1635): the status dashboard moved from public-status/
6
6
  // to modules/status-ui/public/ when the status surface became a module (a web
@@ -14,19 +14,28 @@
14
14
  // (that's how #726's reorg + #731's theming never showed up). This makes the
15
15
  // publish deterministic and CI-driven.
16
16
  //
17
+ // CONFIG (task 1002510 — no host identity is baked in here; ADR 0062 §3):
18
+ // domains.publicOrigin — the origin the mirror calls cross-origin
19
+ // (window.STATUS_API_BASE), and the site label the
20
+ // availability banner names as up/down.
21
+ // domains.statusDataUrl — the off-box prober's status.json URL the availability
22
+ // banner reads. EMPTY ⇒ this instance runs no off-box
23
+ // mirror, and the publish REFUSES rather than shipping a
24
+ // page whose banner points nowhere.
25
+ //
17
26
  // DESIGN: status.js + style.css + cursors/ are byte-identical between the
18
27
  // droplet copy and the mirror (#737 parameterized the one differing constant via
19
28
  // window.STATUS_API_BASE), so they are PURE COPIES. Only index.html carries the
20
29
  // three off-box additions, injected here at stable anchors:
21
- // 1. <script>window.STATUS_API_BASE='https://amazonprimea.com'</script> (head)
22
- // 2. <div id="offbox-status">…</div> (after <body>)
23
- // 3. the availability-banner driver <script> (reads the status-data branch) (before status.js)
30
+ // 1. <script>window.STATUS_API_BASE = <domains.publicOrigin></script> (head)
31
+ // 2. <div id="offbox-status">…</div> (after <body>)
32
+ // 3. the availability-banner driver <script> (reads domains.statusDataUrl) (before status.js)
24
33
  // If any anchor is missing the script THROWS — a silent mis-sync is worse than
25
34
  // a failed run.
26
35
  //
27
36
  // USAGE:
28
37
  // node scripts/status-mirror-sync.js --repo-dir <path-to-mirror-checkout> [--push]
29
- // --repo-dir : a checkout of lars589/amazonprimea-status (its `main` branch).
38
+ // --repo-dir : a checkout of the status-mirror repo (its `main` branch).
30
39
  // --push : commit + push the result. Without it the files are written
31
40
  // and `git status` is printed for inspection (dry-ish run).
32
41
  //
@@ -42,52 +51,36 @@ const SRC = path.join(REPO_ROOT, 'modules', 'status-ui', 'public');
42
51
  const { branding, clientBranding } = require('../src/branding');
43
52
 
44
53
  // Stamp instance-identity tokens into the mirror HTML. The off-box mirror is
45
- // served by GitHub Pages, NOT the branding-injecting Node server — so this script
46
- // must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else the
47
- // mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
48
- // with src/bongos/serve-internal.js applyBrandTokens.
49
- function brandTokens(html) {
50
- const b = clientBranding();
51
- const world = (b.identity && b.identity.worldName) || '';
52
- const product = (b.identity && b.identity.productName) || '';
53
- const cur = (b.currency && b.currency.label) || 'credits';
54
+ // served by a static Pages host, NOT the branding-injecting Node server — so this
55
+ // script must reproduce serve-internal.applyBrandTokens (task 1705, ADR 0062 §3), else
56
+ // the mirror would render literal {{worldName}}/{{currency}} placeholders. KEEP IN SYNC
57
+ // with src/bongos/serve-internal.js applyBrandTokens. `brand` is the client-safe pack
58
+ // and `publicOrigin` the resolved origin — both injected, never read ambiently, so the
59
+ // transform stays pure and testable.
60
+ function brandTokens(html, brand = {}, publicOrigin = '') {
61
+ const world = (brand.identity && brand.identity.worldName) || '';
62
+ const product = (brand.identity && brand.identity.productName) || '';
63
+ const cur = (brand.currency && brand.currency.label) || 'credits';
54
64
  const Cur = cur ? cur.charAt(0).toUpperCase() + cur.slice(1) : cur;
55
- let origin = '';
56
- try { origin = branding().domains.publicOrigin || ''; } catch (_) { /* neutral */ }
57
65
  return html
58
66
  .split('{{worldName}}').join(world)
59
67
  .split('{{productName}}').join(product)
60
68
  .split('{{Currency}}').join(Cur)
61
69
  .split('{{currency}}').join(cur)
62
- .split('{{publicOrigin}}').join(origin);
63
- }
64
-
65
- function arg(name) {
66
- const i = process.argv.indexOf(name);
67
- return i >= 0 ? process.argv[i + 1] : null;
68
- }
69
- const repoDir = arg('--repo-dir');
70
- const doPush = process.argv.includes('--push');
71
- if (!repoDir) {
72
- console.error('usage: node scripts/status-mirror-sync.js --repo-dir <mirror-checkout> [--push]');
73
- process.exit(2);
74
- }
75
- if (!fs.existsSync(path.join(repoDir, 'CNAME'))) {
76
- console.error(`refusing: ${repoDir} does not look like the status mirror (no CNAME). Clone lars589/amazonprimea-status first.`);
77
- process.exit(2);
70
+ .split('{{publicOrigin}}').join(publicOrigin || '');
78
71
  }
79
72
 
80
73
  // ---- the three off-box index.html injections -------------------------------
81
74
 
82
- const HEAD_INJECT =
75
+ const headInject = (apiBase) =>
83
76
  ` <!-- #737: off-box mirror runs cross-origin against the droplet. -->\n` +
84
- ` <script>window.STATUS_API_BASE = 'https://amazonprimea.com';</script>\n`;
77
+ ` <script>window.STATUS_API_BASE = ${JSON.stringify(apiBase)};</script>\n`;
85
78
 
86
79
  // The client-safe branding pack, so status.js reads the instance currency label
87
80
  // on the mirror exactly as it does on the droplet (serve-internal injects the same
88
81
  // global there). Without this, status.js would fall back to the neutral 'credits'.
89
- const BRANDING_INJECT =
90
- ` <script>window.__BRANDING__ = ${JSON.stringify(clientBranding())};</script>\n`;
82
+ const brandingInject = (brand) =>
83
+ ` <script>window.__BRANDING__ = ${JSON.stringify(brand)};</script>\n`;
91
84
 
92
85
  const BANNER_DIV =
93
86
  `\n<!-- Off-box availability banner (ADR 0029) — injected by scripts/status-mirror-sync.js.\n` +
@@ -95,11 +88,17 @@ const BANNER_DIV =
95
88
  ` prober's status.json from this repo's status-data branch, no droplet dependency. -->\n` +
96
89
  `<div id="offbox-status" role="status" aria-live="polite"></div>\n`;
97
90
 
98
- const BANNER_DRIVER = `<!-- Off-box availability banner driver (ADR 0029) — loaded before status.js so
91
+ // The banner names the site it is reporting on. Derived from the configured origin
92
+ // so no hostname is baked in; a non-URL origin falls back to the raw string.
93
+ function siteLabel(origin) {
94
+ try { return new URL(origin).host; } catch { return String(origin || ''); }
95
+ }
96
+
97
+ const bannerDriver = (statusDataUrl, label) => `<!-- Off-box availability banner driver (ADR 0029) — loaded before status.js so
99
98
  it lights up even if the dashboard script or its cross-origin fetches fail. -->
100
99
  <script>
101
100
  (function () {
102
- var OFFBOX_URL = 'https://raw.githubusercontent.com/lars589/amazonprimea-status/status-data/status.json';
101
+ var OFFBOX_URL = ${JSON.stringify(statusDataUrl)};
103
102
  var el = document.getElementById('offbox-status');
104
103
  if (!el) return;
105
104
  function fmtUtc(iso) {
@@ -113,8 +112,8 @@ const BANNER_DRIVER = `<!-- Off-box availability banner driver (ADR 0029) — lo
113
112
  .then(function (res) { if (!res.ok) throw new Error('HTTP ' + res.status); return res.json(); })
114
113
  .then(function (data) {
115
114
  var cur = data && data.current, status = cur && cur.status, when = fmtUtc(cur && cur.t);
116
- if (status === 'up') setState('up', '\\uD83D\\uDFE2 amazonprimea.com is operational \\u2014 checked ' + when);
117
- else if (status === 'down') setState('down', '\\uD83D\\uDD34 amazonprimea.com is DOWN \\u2014 checked ' + when);
115
+ if (status === 'up') setState('up', '\\uD83D\\uDFE2 ${label} is operational \\u2014 checked ' + when);
116
+ else if (status === 'down') setState('down', '\\uD83D\\uDD34 ${label} is DOWN \\u2014 checked ' + when);
118
117
  else setState('unknown', 'status data is momentarily unavailable');
119
118
  })
120
119
  .catch(function () { setState('unknown', 'status data is momentarily unavailable'); });
@@ -134,59 +133,90 @@ function injectOnce(haystack, anchor, insertion, where) {
134
133
  return haystack.slice(0, idx + anchor.length) + insertion + haystack.slice(idx + anchor.length);
135
134
  }
136
135
 
137
- function buildIndexHtml() {
138
- let html = fs.readFileSync(path.join(SRC, 'index.html'), 'utf8');
136
+ // Pure: droplet index.html + resolved config → the mirror's index.html. No disk, no
137
+ // network, no ambient branding read — everything the output depends on is a parameter.
138
+ function buildMirrorIndex({ srcHtml, apiBase, statusDataUrl, brand }) {
139
+ if (!statusDataUrl) {
140
+ throw new Error('domains.statusDataUrl is empty — the off-box availability banner would point nowhere. Set domains.statusDataUrl (the prober\'s status.json URL) in the branding config, or do not publish a mirror for this instance.');
141
+ }
142
+ let html = String(srcHtml == null ? '' : srcHtml);
139
143
  if (html.includes('offbox-status')) {
140
144
  throw new Error('modules/status-ui/public/index.html already contains offbox markup — the source should be the droplet (no-banner) copy. Aborting.');
141
145
  }
142
- html = injectOnce(html, '</head>', HEAD_INJECT + BRANDING_INJECT, 'before');
146
+ html = injectOnce(html, '</head>', headInject(apiBase) + brandingInject(brand), 'before');
143
147
  html = injectOnce(html, '<body>', BANNER_DIV, 'after');
144
148
  // Insert the driver right before the main status.js script tag.
145
149
  const scriptAnchor = '<script src="/status.js';
146
- html = injectOnce(html, scriptAnchor, BANNER_DRIVER + '', 'before');
147
- // Stamp the instance branding tokens LAST — GitHub Pages won't run the server's
150
+ html = injectOnce(html, scriptAnchor, bannerDriver(statusDataUrl, siteLabel(apiBase)), 'before');
151
+ // Stamp the instance branding tokens LAST — the Pages host won't run the server's
148
152
  // injection, so the mirror must resolve {{worldName}}/{{currency}}/… itself.
149
- html = brandTokens(html);
150
- return html;
153
+ return brandTokens(html, brand, apiBase);
151
154
  }
152
155
 
153
- // ---- copy the verbatim assets + the transformed index ----------------------
156
+ // ---- CLI -------------------------------------------------------------------
157
+
158
+ if (require.main === module) {
159
+ const arg = (name) => {
160
+ const i = process.argv.indexOf(name);
161
+ return i >= 0 ? process.argv[i + 1] : null;
162
+ };
163
+ const repoDir = arg('--repo-dir');
164
+ const doPush = process.argv.includes('--push');
165
+ if (!repoDir) {
166
+ console.error('usage: node scripts/status-mirror-sync.js --repo-dir <mirror-checkout> [--push]');
167
+ process.exit(2);
168
+ }
169
+ if (!fs.existsSync(path.join(repoDir, 'CNAME'))) {
170
+ console.error(`refusing: ${repoDir} does not look like the status mirror (no CNAME). Clone the status-mirror repo first.`);
171
+ process.exit(2);
172
+ }
154
173
 
155
- function copyFile(rel) {
156
- fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
157
- console.error(` copied ${rel}`);
158
- }
159
- function copyDir(rel) {
160
- const from = path.join(SRC, rel), to = path.join(repoDir, rel);
161
- if (!fs.existsSync(from)) return;
162
- fs.rmSync(to, { recursive: true, force: true });
163
- fs.cpSync(from, to, { recursive: true });
164
- console.error(` copied ${rel}/`);
174
+ const b = branding();
175
+
176
+ // ---- copy the verbatim assets + the transformed index ----------------------
177
+
178
+ const copyFile = (rel) => {
179
+ fs.copyFileSync(path.join(SRC, rel), path.join(repoDir, rel));
180
+ console.error(` copied ${rel}`);
181
+ };
182
+ const copyDir = (rel) => {
183
+ const from = path.join(SRC, rel), to = path.join(repoDir, rel);
184
+ if (!fs.existsSync(from)) return;
185
+ fs.rmSync(to, { recursive: true, force: true });
186
+ fs.cpSync(from, to, { recursive: true });
187
+ console.error(` copied ${rel}/`);
188
+ };
189
+
190
+ console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
191
+ const indexHtml = buildMirrorIndex({
192
+ srcHtml: fs.readFileSync(path.join(SRC, 'index.html'), 'utf8'),
193
+ apiBase: b.domains.publicOrigin,
194
+ statusDataUrl: b.domains.statusDataUrl,
195
+ brand: clientBranding(),
196
+ });
197
+ fs.writeFileSync(path.join(repoDir, 'index.html'), indexHtml);
198
+ console.error(' wrote index.html (with off-box injections)');
199
+ copyFile('status.js');
200
+ copyFile('style.css');
201
+ copyDir('cursors');
202
+
203
+ // ---- commit + (optionally) push -------------------------------------------
204
+
205
+ const git = (args) => execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
206
+ git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
207
+ const status = git(['status', '--porcelain']);
208
+ if (!status.trim()) {
209
+ console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
210
+ process.exit(0);
211
+ }
212
+ console.error('[status-mirror-sync] changes staged:\n' + status);
213
+ if (!doPush) {
214
+ console.error('[status-mirror-sync] dry run (no --push). Review the checkout, then re-run with --push.');
215
+ process.exit(0);
216
+ }
217
+ git(['commit', '-m', 'chore(status): sync off-box mirror from modules/status-ui/public/ (#737)']);
218
+ git(['push', 'origin', 'HEAD:main']);
219
+ console.error(`[status-mirror-sync] published. The Pages host rebuilds ${b.domains.statusOrigin || 'the mirror'} in ~1 min.`);
165
220
  }
166
221
 
167
- console.error('[status-mirror-sync] building mirror from modules/status-ui/public/ …');
168
- fs.writeFileSync(path.join(repoDir, 'index.html'), buildIndexHtml());
169
- console.error(' wrote index.html (with off-box injections)');
170
- copyFile('status.js');
171
- copyFile('style.css');
172
- copyDir('cursors');
173
-
174
- // ---- commit + (optionally) push -------------------------------------------
175
-
176
- function git(args) {
177
- return execFileSync('git', ['-C', repoDir, ...args], { encoding: 'utf8' });
178
- }
179
- git(['add', 'index.html', 'status.js', 'style.css', 'cursors']);
180
- const status = git(['status', '--porcelain']);
181
- if (!status.trim()) {
182
- console.error('[status-mirror-sync] mirror already up to date — nothing to publish.');
183
- process.exit(0);
184
- }
185
- console.error('[status-mirror-sync] changes staged:\n' + status);
186
- if (!doPush) {
187
- console.error('[status-mirror-sync] dry run (no --push). Review the checkout, then re-run with --push.');
188
- process.exit(0);
189
- }
190
- git(['commit', '-m', 'chore(status): sync off-box mirror from modules/status-ui/public/ (#737)']);
191
- git(['push', 'origin', 'HEAD:main']);
192
- console.error('[status-mirror-sync] published. GitHub Pages rebuilds status.amazonprimea.com in ~1 min.');
222
+ module.exports = { buildMirrorIndex, injectOnce, brandTokens };