@bongos/core 1.19.1073 → 1.19.1075

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 (377) hide show
  1. package/.bongos-core.json +404 -549
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/bin/bongos.js +1 -3
  4. package/clients/bongos-client/README.md +1 -1
  5. package/clients/bongos-client/bongos-client.global.js +4 -60
  6. package/clients/bongos-client/index.cjs +4 -60
  7. package/clients/bongos-client/index.d.ts +5 -90
  8. package/clients/bongos-client/index.mjs +4 -60
  9. package/config/branding.neutral.json +1 -5
  10. package/config/modules.neutral.json +2 -3
  11. package/config/scheduled-routines.json +0 -2
  12. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  13. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  14. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  15. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  16. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +2 -0
  17. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  18. package/docs/adr/README.md +1 -0
  19. package/docs/api/openapi.json +175 -1665
  20. package/docs/api-reference.md +9 -41
  21. package/docs/architecture.md +16 -1
  22. package/docs/branding-contract.md +0 -3
  23. package/docs/copy-inventory.md +454 -479
  24. package/docs/copy-registry.json +746 -978
  25. package/docs/file-map.md +5 -34
  26. package/docs/module-api-changelog.md +4 -0
  27. package/docs/modules-contract.md +13 -1
  28. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  29. package/docs/onboarding/diagrams/README.md +3 -3
  30. package/docs/onboarding/diagrams/assertions.json +2 -22
  31. package/docs/onboarding/primer.md +9 -28
  32. package/docs/page-inventory.json +4 -1
  33. package/docs/page-readings.json +1032 -1077
  34. package/docs/recipes/render-pilot.md +29 -10
  35. package/migrations/core_257_module_store_registry.sql +87 -0
  36. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  37. package/modules/agents/module.json +1 -0
  38. package/modules/autonomy/module.json +1 -0
  39. package/modules/builder-settings/module.json +1 -0
  40. package/modules/builder-settings/render-prefs.js +4 -5
  41. package/modules/copy-desk/docx.js +78 -5
  42. package/modules/copy-desk/module.json +1 -0
  43. package/modules/copy-desk/routes/copy-desk.js +10 -10
  44. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  45. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  46. package/modules/discord/module.json +1 -0
  47. package/modules/discord/ship-broadcast.js +3 -3
  48. package/modules/economy/credits.js +2 -2
  49. package/modules/economy/module.json +1 -0
  50. package/modules/economy/routes/credits.js +1 -2
  51. package/modules/government/catalog.js +13 -17
  52. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  53. package/modules/government/module.json +1 -0
  54. package/modules/government/protected-surfaces.json +3 -11
  55. package/modules/government/resolver.js +3 -46
  56. package/modules/government/routes/government.js +0 -2
  57. package/modules/government/session-scopes.js +43 -83
  58. package/modules/government/session-scopes.json +8 -18
  59. package/modules/grading/module.json +1 -0
  60. package/modules/hall-ui/module.json +1 -0
  61. package/modules/hall-ui/public/board-lib.js +2 -2
  62. package/modules/hall-ui/public/builders.js +29 -40
  63. package/modules/hall-ui/public/collab.html +1 -1
  64. package/modules/hall-ui/public/collab.js +1 -1
  65. package/modules/hall-ui/public/diagrams.html +3 -3
  66. package/modules/hall-ui/public/dom-utils.js +1 -1
  67. package/modules/hall-ui/public/gate.js +2 -2
  68. package/modules/hall-ui/public/gate.states.json +1 -1
  69. package/modules/hall-ui/public/hall-render.js +43 -79
  70. package/modules/hall-ui/public/index.html +5 -1
  71. package/modules/hall-ui/public/modules.js +1 -0
  72. package/modules/hall-ui/public/oversight.css +5 -5
  73. package/modules/hall-ui/public/palette.js +0 -11
  74. package/modules/hall-ui/public/primer.js +1 -1
  75. package/modules/hall-ui/public/sessions.html +1 -1
  76. package/modules/hall-ui/public/settings-sessions.js +1 -1
  77. package/modules/hall-ui/public/settings.css +0 -1
  78. package/modules/hall-ui/public/settings.html +4 -33
  79. package/modules/hall-ui/public/settings.js +3 -33
  80. package/modules/hall-ui/public/settings.states.json +2 -2
  81. package/modules/hall-ui/public/studio.html +1 -1
  82. package/modules/hall-ui/public/style.css +10 -13
  83. package/modules/hall-ui/public/task.html +4 -1
  84. package/modules/hall-ui/public/task.js +36 -1
  85. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  86. package/modules/hall-ui/public/tweak-editor.css +16 -0
  87. package/modules/hall-ui/public/tweak-editor.html +26 -3
  88. package/modules/hall-ui/public/tweak-editor.js +32 -1
  89. package/modules/hall-ui/public/work.js +1 -1
  90. package/modules/hall-ui/records/tweak-editor.md +1 -0
  91. package/modules/ideas/module.json +1 -0
  92. package/modules/ideas/projection.js +1 -1
  93. package/modules/lifecycle/db-claim-reads.js +1 -58
  94. package/modules/lifecycle/db-ship.js +4 -9
  95. package/modules/lifecycle/db.js +0 -4
  96. package/modules/lifecycle/github-push.js +12 -14
  97. package/modules/lifecycle/kickoff-checklist.js +0 -4
  98. package/modules/lifecycle/lifecycle.js +0 -8
  99. package/modules/lifecycle/module.json +1 -0
  100. package/modules/lifecycle/publish-reconciler.js +10 -10
  101. package/modules/lifecycle/routes/tasks.js +13 -15
  102. package/modules/lifecycle/ship-card.js +1 -1
  103. package/modules/lifecycle/task-visuals.js +2 -2
  104. package/modules/memory/module.json +1 -0
  105. package/modules/memory/routes/memory.js +2 -2
  106. package/modules/npm-release/module.json +1 -0
  107. package/modules/onboarding/module.json +1 -0
  108. package/modules/onboarding/onboarding-state.js +17 -36
  109. package/modules/onboarding/routes/access-requests.js +3 -3
  110. package/modules/platform-identity/module.json +1 -0
  111. package/modules/platform-identity/platform-identity.js +13 -5
  112. package/modules/platform-identity/routes/sso.js +5 -4
  113. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  114. package/modules/provisioning/capacity.js +1 -1
  115. package/modules/provisioning/module.json +1 -0
  116. package/modules/provisioning/provisioning.js +6 -5
  117. package/modules/provisioning/routes/provisioning.js +4 -4
  118. package/modules/provisioning/starter-bundles.js +5 -25
  119. package/modules/public-landing/module.json +1 -0
  120. package/modules/public-landing/public/assets/cosmos.css +1 -1
  121. package/modules/public-landing/public/contact.html +1 -1
  122. package/modules/public-landing/public/privacy.html +1 -1
  123. package/modules/public-landing/public/projects.html +101 -32
  124. package/modules/public-landing/public/projects.probes.json +2 -2
  125. package/modules/public-landing/public/projects.states.json +4 -3
  126. package/modules/public-landing/public/terms.html +1 -1
  127. package/modules/security/module.json +1 -0
  128. package/modules/sessions/db.js +1 -1
  129. package/modules/sessions/module.json +1 -0
  130. package/modules/specialities/module.json +1 -0
  131. package/modules/status-ui/module.json +1 -0
  132. package/modules/ui-design/module.json +1 -0
  133. package/package-lock.json +2 -2
  134. package/package.json +1 -1
  135. package/release-notes.json +84 -0
  136. package/scripts/gds/artifact-format.js +203 -0
  137. package/scripts/gds/artifact-staleness.js +2 -2
  138. package/scripts/gds/audit-authorship.sh +1 -13
  139. package/scripts/gds/backfill-task-rewards.js +5 -5
  140. package/scripts/gds/claim.js +0 -15
  141. package/scripts/gds/claude-materialize.js +2 -2
  142. package/scripts/gds/cli-lib.js +15 -15
  143. package/scripts/gds/codemod-rename-src-gds.js +1 -1
  144. package/scripts/gds/context-pack.js +5 -7
  145. package/scripts/gds/diagram-facts.js +12 -23
  146. package/scripts/gds/do-api.js +13 -80
  147. package/scripts/gds/doc-cli-guard.js +2 -0
  148. package/scripts/gds/doctor.js +4 -4
  149. package/scripts/gds/feedback-latest.js +139 -9
  150. package/scripts/gds/fitness-checks-identity.js +2 -7
  151. package/scripts/gds/fitness-checks-packaging.js +4 -4
  152. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  153. package/scripts/gds/fitness-ratchets.js +5 -5
  154. package/scripts/gds/fitness.js +10 -10
  155. package/scripts/gds/gen-api-client.js +4 -4
  156. package/scripts/gds/gen-api-docs.js +1 -1
  157. package/scripts/gds/gen-diagrams.js +49 -71
  158. package/scripts/gds/gen-repo-map.js +13 -14
  159. package/scripts/gds/gen-session-index.js +8 -8
  160. package/scripts/gds/http-api-client.js +12 -19
  161. package/scripts/gds/init.js +1 -1
  162. package/scripts/gds/leak-scan-allowlist.js +1 -1
  163. package/scripts/gds/lint-infra-exec.js +1 -1
  164. package/scripts/gds/local-preview-lib.js +8 -39
  165. package/scripts/gds/local-preview.js +2 -15
  166. package/scripts/gds/migration-namespace.js +3 -3
  167. package/scripts/gds/module-artifact.js +212 -0
  168. package/scripts/gds/module.js +95 -6
  169. package/scripts/gds/package-core.js +5 -116
  170. package/scripts/gds/plain-cards.js +1 -1
  171. package/scripts/gds/provision-config.js +2 -2
  172. package/scripts/gds/provision-net.js +2 -3
  173. package/scripts/gds/provision.js +9 -9
  174. package/scripts/gds/publish-manifest.js +1 -2
  175. package/scripts/gds/push-path-brief.js +10 -10
  176. package/scripts/gds/regen-instance-docs.js +1 -1
  177. package/scripts/gds/rename-history-check.js +1 -1
  178. package/scripts/gds/run-unit-tests.js +2 -16
  179. package/scripts/gds/sandbox-stage.js +39 -341
  180. package/scripts/gds/session-digest-build.js +2 -2
  181. package/scripts/gds/ship-deploy-target.js +13 -13
  182. package/scripts/gds/ship-flow.js +7 -7
  183. package/scripts/gds/ship-land.js +9 -10
  184. package/scripts/gds/ship-merge.js +4 -4
  185. package/scripts/gds/ship-preflight-steps.js +4 -4
  186. package/scripts/gds/ship-regen.js +15 -49
  187. package/scripts/gds/ship.js +13 -33
  188. package/scripts/gds/skill-preflight.js +9 -8
  189. package/scripts/gds/start.js +0 -15
  190. package/scripts/hall-preview/README.md +5 -5
  191. package/scripts/hall-preview/server.js +6 -6
  192. package/scripts/render-diagrams.sh +1 -1
  193. package/src/bongos/api-errors.js +1 -1
  194. package/src/bongos/api-prefix.js +4 -4
  195. package/src/bongos/auth-github.js +2 -3
  196. package/src/bongos/auth.js +51 -126
  197. package/src/bongos/db-kernel.js +2 -3
  198. package/src/bongos/db.js +5 -28
  199. package/src/bongos/module-scope-map.js +14 -29
  200. package/src/bongos/module-store.js +141 -0
  201. package/src/bongos/platform-visibility-gate.js +1 -1
  202. package/src/bongos/route-rank-check.js +3 -16
  203. package/src/bongos/routes/auth.js +0 -116
  204. package/src/bongos/routes/builders.js +5 -15
  205. package/src/bongos/routes/instance.js +8 -14
  206. package/src/bongos/routes/me.js +7 -23
  207. package/src/bongos/routes/modules.js +83 -0
  208. package/src/bongos/routes/security.js +3 -3
  209. package/src/bongos/routes.js +7 -2
  210. package/src/bongos/serve-internal.js +5 -6
  211. package/src/bongos-downloads.js +1 -6
  212. package/src/branding.js +2 -2
  213. package/src/build-info.js +4 -6
  214. package/src/instance-config.js +2 -2
  215. package/src/module-api.js +13 -6
  216. package/src/module-loader/catalog.js +3 -0
  217. package/src/module-loader/loader.js +4 -4
  218. package/src/module-loader/manifest-schema.js +16 -8
  219. package/src/module-seams.js +2 -3
  220. package/src/modules.js +42 -14
  221. package/tests/agents_spend_guard.mjs +1 -1
  222. package/tests/api_alias_caller_ratchet.mjs +1 -1
  223. package/tests/api_alias_exceptions.mjs +1 -1
  224. package/tests/api_client.mjs +1 -16
  225. package/tests/api_path_404.mjs +1 -1
  226. package/tests/auth_page_gate.mjs +10 -10
  227. package/tests/backfill_task_rewards.mjs +1 -1
  228. package/tests/canonical_profile_url.mjs +1 -3
  229. package/tests/claim_action.mjs +4 -1
  230. package/tests/claim_from_task_and_home.mjs +127 -0
  231. package/tests/{box_ship_permissions.mjs → cli_allowlist_permissions.mjs} +5 -5
  232. package/tests/cli_sessions.mjs +1 -1
  233. package/tests/consumer_layout_boot.mjs +2 -0
  234. package/tests/context_pack.mjs +13 -13
  235. package/tests/copy_desk_page_docx.mjs +33 -2
  236. package/tests/copy_inventory.mjs +7 -7
  237. package/tests/core_upgrade_runner.mjs +6 -6
  238. package/tests/credit_grant.mjs +2 -2
  239. package/tests/criterion_suggest.mjs +1 -1
  240. package/tests/deploy_divergence_line.mjs +11 -11
  241. package/tests/design_tokens_sync.mjs +3 -2
  242. package/tests/diagram_facts_offset.mjs +1 -1
  243. package/tests/do_api.mjs +106 -0
  244. package/tests/feedback_latest.mjs +122 -0
  245. package/tests/fitness.mjs +44 -42
  246. package/tests/fitness_ratchets.mjs +2 -2
  247. package/tests/gate_approvals.mjs +0 -1
  248. package/tests/github_push_land_proof.mjs +1 -1
  249. package/tests/go_live.mjs +3 -3
  250. package/tests/goal_advisory.mjs +3 -3
  251. package/tests/goal_suggest.mjs +3 -3
  252. package/tests/government_abuse_matrix.mjs +21 -19
  253. package/tests/government_ownership_scope.mjs +33 -35
  254. package/tests/government_parity.mjs +2 -2
  255. package/tests/government_protected_surfaces.mjs +56 -37
  256. package/tests/government_require_permission.mjs +3 -3
  257. package/tests/government_seed.mjs +37 -4
  258. package/tests/government_session_scope.mjs +92 -366
  259. package/tests/hall_audit.mjs +1 -1
  260. package/tests/hall_error_envelope.mjs +4 -0
  261. package/tests/hall_landing_boot.mjs +4 -1
  262. package/tests/hall_palette.mjs +1 -16
  263. package/tests/hall_record_world.mjs +6 -0
  264. package/tests/hall_tweak_editor.mjs +52 -1
  265. package/tests/helpers.mjs +1 -1
  266. package/tests/host_topology_skips.mjs +3 -4
  267. package/tests/html_comment_nesting.mjs +137 -0
  268. package/tests/infra_exec_paths.mjs +9 -10
  269. package/tests/init.mjs +4 -5
  270. package/tests/instance_manifest.mjs +18 -25
  271. package/tests/kickoff_checklist.mjs +1 -1
  272. package/tests/lib_sh_resolution.mjs +0 -221
  273. package/tests/local_preview.mjs +0 -12
  274. package/tests/migration_namespace.mjs +6 -6
  275. package/tests/module-scope-map.mjs +9 -9
  276. package/tests/module_api.mjs +2 -2
  277. package/tests/module_catalog.mjs +1 -1
  278. package/tests/module_cli.mjs +20 -20
  279. package/tests/module_contributions.mjs +6 -6
  280. package/tests/module_loader.mjs +6 -6
  281. package/tests/module_manifest.mjs +26 -17
  282. package/tests/module_route_rank.mjs +2 -2
  283. package/tests/module_store_publish.mjs +307 -0
  284. package/tests/module_store_publish_route.mjs +121 -0
  285. package/tests/module_store_registry_migration.mjs +70 -0
  286. package/tests/modules.mjs +56 -26
  287. package/tests/modules_route_wiring.mjs +1 -1
  288. package/tests/onboarding_route_signals.mjs +21 -31
  289. package/tests/onboarding_state.mjs +35 -49
  290. package/tests/paste_token_session_store.mjs +1 -1
  291. package/tests/permission_path.mjs +0 -3
  292. package/tests/platform_boot.mjs +1 -2
  293. package/tests/platform_visibility_gate.mjs +1 -1
  294. package/tests/profile_route.mjs +1 -6
  295. package/tests/project_modules_ui.mjs +17 -17
  296. package/tests/projects_hub.mjs +5 -5
  297. package/tests/projects_hub_app_step.mjs +138 -0
  298. package/tests/projects_hub_module_picker.mjs +77 -76
  299. package/tests/projects_hub_pre_uat.mjs +4 -5
  300. package/tests/provision.mjs +7 -7
  301. package/tests/provisioning_capacity.mjs +1 -1
  302. package/tests/provisioning_recommendations.mjs +2 -1
  303. package/tests/provisioning_starter_bundles.mjs +0 -22
  304. package/tests/publish_branch_route.mjs +11 -19
  305. package/tests/publish_manifest.mjs +2 -3
  306. package/tests/publish_reconciler.mjs +8 -8
  307. package/tests/push_path_brief.mjs +10 -9
  308. package/tests/rank_tier_single_source.mjs +1 -13
  309. package/tests/regrade_eligibility.mjs +1 -1
  310. package/tests/rename_history_restraint.mjs +1 -1
  311. package/tests/repo_map.mjs +12 -13
  312. package/tests/runner_drift.mjs +3 -21
  313. package/tests/sandbox_stage.mjs +57 -390
  314. package/tests/seam_wiring_guard.mjs +11 -11
  315. package/tests/search_isolation.mjs +3 -3
  316. package/tests/session_records.mjs +1 -1
  317. package/tests/session_rename_fallback.mjs +1 -22
  318. package/tests/session_start_freshness.mjs +3 -3
  319. package/tests/session_token_hash_db.mjs +1 -1
  320. package/tests/ship_card.mjs +1 -1
  321. package/tests/ship_ci_deploy.mjs +17 -17
  322. package/tests/ship_error_shape.mjs +1 -1
  323. package/tests/ship_premerge.mjs +1 -1
  324. package/tests/ship_resume.mjs +1 -1
  325. package/tests/skill_preflight.mjs +2 -2
  326. package/tests/skip_is_not_pass.mjs +1 -1
  327. package/tests/task_visual_slots.mjs +31 -0
  328. package/tests/terms_acceptance.mjs +2 -2
  329. package/tests/upgrade.mjs +4 -4
  330. package/tests/watch_sealed_floor.mjs +0 -1
  331. package/tests/wizard_draft_resume.mjs +2 -2
  332. package/tests/wizard_intent_resume.mjs +21 -10
  333. package/tests/wizard_preselect_why.mjs +18 -18
  334. package/docs/onboarding/browser-terminal-guide.md +0 -73
  335. package/docs/recipes/managed-settings-remote-control.md +0 -416
  336. package/modules/dev-box/CLAUDE.md +0 -15
  337. package/modules/dev-box/box-access.js +0 -623
  338. package/modules/dev-box/box-credential.js +0 -94
  339. package/modules/dev-box/box-onboard.js +0 -486
  340. package/modules/dev-box/boxes.js +0 -1038
  341. package/modules/dev-box/db.js +0 -39
  342. package/modules/dev-box/module.json +0 -19
  343. package/modules/dev-box/routes/box.js +0 -1223
  344. package/scripts/gds/box-auth-check.js +0 -160
  345. package/scripts/gds/box-infra.js +0 -321
  346. package/scripts/gds/box-sync.js +0 -216
  347. package/scripts/gds/box.js +0 -1332
  348. package/scripts/gds/cf-tunnel.js +0 -558
  349. package/scripts/gds/smoke-box.sh +0 -150
  350. package/scripts/gds/tree-preflight.js +0 -241
  351. package/src/bongos/app-pair.js +0 -172
  352. package/tests/app_pair.mjs +0 -168
  353. package/tests/box_access.mjs +0 -578
  354. package/tests/box_auth_check.mjs +0 -114
  355. package/tests/box_code_staleness.mjs +0 -129
  356. package/tests/box_connect_e2e.mjs +0 -196
  357. package/tests/box_cost.mjs +0 -70
  358. package/tests/box_credential.mjs +0 -125
  359. package/tests/box_dns_repoint.mjs +0 -153
  360. package/tests/box_env.mjs +0 -64
  361. package/tests/box_host_keys.mjs +0 -126
  362. package/tests/box_infra_resolve.mjs +0 -246
  363. package/tests/box_onboard.mjs +0 -317
  364. package/tests/box_scope_session.mjs +0 -119
  365. package/tests/box_sweep_docs.mjs +0 -119
  366. package/tests/box_sync.mjs +0 -55
  367. package/tests/box_sync_scope_report.mjs +0 -126
  368. package/tests/box_task_scope.mjs +0 -169
  369. package/tests/box_task_scope_pause.mjs +0 -66
  370. package/tests/box_terminal_ssrf.mjs +0 -96
  371. package/tests/boxes.mjs +0 -1585
  372. package/tests/cf_ruleset_preflight.mjs +0 -142
  373. package/tests/cf_tunnel.mjs +0 -496
  374. package/tests/docs_scrubber_damage.mjs +0 -111
  375. package/tests/module_scope_active_claims.mjs +0 -94
  376. package/tests/ship_api_client_pathspec.mjs +0 -100
  377. package/tests/tree_preflight.mjs +0 -243
@@ -0,0 +1,212 @@
1
+ // scripts/gds/module-artifact.js — pack and verify ONE module version for the
2
+ // Bongos-hosted module store (task 1004271, ADR 0338 D1).
3
+ //
4
+ // ADR 0338 settled "no second packaging format": a module version is a tarball plus a
5
+ // hash manifest in exactly the shape package-core.js emits for the core — per-file
6
+ // sha256, a compression-independent tree_sha256, a deterministic ustar+gzip — so the
7
+ // tar writer, reader and tree pin all come from artifact-format.js, the file the core
8
+ // release builds with. What differs is only WHAT is packed and what the manifest names:
9
+ //
10
+ // tarball package/<path inside modules/<key>/> (module.json at package/module.json)
11
+ // package/.bongos-module.json the inner manifest (below)
12
+ // manifest { artifact: 'bongos-module', manifest_schema, generator, module_key,
13
+ // module_version, core_version, built_at, file_count, tree_sha256,
14
+ // files: [{ path, mode, sha256 }] }
15
+ // side-car the same manifest + { tarball: { name, sha256, bytes } } (withTarballInfo)
16
+ //
17
+ // THE GATE. The core's isPublishable() is default-deny: a path ships only if the
18
+ // allowlist names it and the denylist does not. The allowlist names the CORE's own
19
+ // modules, so for a store module the allow side is "inside this module's own
20
+ // modules/<key>/ directory" and the deny side is the SAME repo-wide denylist
21
+ // (matchesDeny — game content, pixel art, copyrighted references, instance topology,
22
+ // secrets), plus credential-named files anywhere in the module (SECRET_FILE_RE). Anything the denylist catches refuses the whole publish rather than being
23
+ // silently dropped: a module that ships without a file it expected is a broken module.
24
+ //
25
+ // Both halves live here so the CLI (packModule) and the store route
26
+ // (verifyModuleArtifact) cannot disagree about the format. Pure apart from reading the
27
+ // module directory in packModule; never touches the network or the database.
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+ const { sha256hex, canonicalTreeHash, normalizeMode, buildTar, readTarAsync } = require('./artifact-format');
32
+ const { matchesDeny } = require('./publish-manifest');
33
+ const { validateManifest } = require('../../src/module-loader/manifest-schema');
34
+
35
+ const ARTIFACT_NAME = 'bongos-module';
36
+ const MANIFEST_SCHEMA = 1;
37
+ const TAR_ROOT = 'package/';
38
+ const INNER_MANIFEST_PATH = '.bongos-module.json';
39
+ const INNER_MANIFEST = TAR_ROOT + INNER_MANIFEST_PATH;
40
+ // Local-only files a module dir may hold that are never part of the module itself.
41
+ const NEVER_PACKED = new Set(['.upstream-signoff.json', INNER_MANIFEST_PATH]);
42
+ // The store's caps. A real feature module is tens of KB (the biggest bundled one packs
43
+ // to ~90 KB), so these are generous headroom — and deliberately small, because the
44
+ // store verifies an upload inside the web process: every byte is inflated and hashed
45
+ // there. 5 MB on the wire matches the upstreaming queue's SUBMISSION_MAX_BYTES; the
46
+ // unpacked cap bounds a gzip bomb.
47
+ const MAX_TARBALL_BYTES = 5 * 1024 * 1024;
48
+ const READ_LIMITS = { maxUnpackedBytes: 16 * 1024 * 1024, maxFiles: 2000 };
49
+
50
+ function tarballName(key, version) { return `${ARTIFACT_NAME}-${key}-${version}.tgz`; }
51
+
52
+ // Every file under dir, as module-relative forward-slash paths, sorted. Skips
53
+ // VCS/dependency noise the way listModuleFiles (module.js) does.
54
+ function listFiles(dir, rel = '') {
55
+ const out = [];
56
+ for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
57
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
58
+ const r = rel ? `${rel}/${entry.name}` : entry.name;
59
+ if (entry.isDirectory()) out.push(...listFiles(dir, r));
60
+ else if (entry.isFile()) out.push(r);
61
+ }
62
+ return out.sort();
63
+ }
64
+
65
+ // A module-relative path is safe to unpack only if it stays inside the module.
66
+ function isSafeRelPath(p) {
67
+ return typeof p === 'string' && p.length > 0 && !p.startsWith('/') && !p.includes('\\')
68
+ && !/^[A-Za-z]:/.test(p) && !p.split('/').some((seg) => seg === '' || seg === '.' || seg === '..');
69
+ }
70
+
71
+ // The repo denylist anchors its secret entries at the root (`.env`, not
72
+ // `modules/x/.env`), which is right for the core tree and wrong for a module that
73
+ // travels on its own. So a store module is also refused any file whose NAME marks it
74
+ // as a credential, wherever it sits in the module.
75
+ const SECRET_FILE_RE = /^(?:\.env(?:\..*)?|\.npmrc|\.netrc|\.pgpass|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|.*\.(?:pem|key|p12|pfx|jks|keystore))$/i;
76
+
77
+ // The files the gate refuses, named at their canonical modules/<key>/ path.
78
+ function deniedFiles(key, relPaths) {
79
+ return relPaths
80
+ .filter((p) => matchesDeny(`modules/${key}/${p}`) || SECRET_FILE_RE.test(p.split('/').pop()))
81
+ .map((p) => `modules/${key}/${p}`);
82
+ }
83
+
84
+ function buildModuleManifest({ key, version, coreVersion, builtAtIso, files, treeHash }) {
85
+ return {
86
+ artifact: ARTIFACT_NAME,
87
+ manifest_schema: MANIFEST_SCHEMA,
88
+ generator: 'scripts/gds/module-artifact.js',
89
+ module_key: key,
90
+ module_version: version,
91
+ core_version: coreVersion,
92
+ built_at: builtAtIso,
93
+ file_count: files.length,
94
+ tree_sha256: treeHash,
95
+ files: files.map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: f.sha256 })),
96
+ };
97
+ }
98
+
99
+ // Pack modules/<key>/ into { tgz, manifest, sidecar } or throw with a plain reason.
100
+ // `modeOf` is injectable because Windows reports no executable bit.
101
+ function packModule(key, { modulesDir, now = () => new Date(), modeOf } = {}) {
102
+ const dir = path.join(modulesDir, key);
103
+ if (!fs.existsSync(dir)) throw new Error(`modules/${key} does not exist`);
104
+ let moduleJson;
105
+ try { moduleJson = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8')); }
106
+ catch { throw new Error(`modules/${key}/module.json is missing or is not valid JSON`); }
107
+ const { valid, errors } = validateManifest(moduleJson);
108
+ if (!valid) throw new Error(`modules/${key}/module.json is invalid: ${errors.join('; ')}`);
109
+ if (moduleJson.key !== key) throw new Error(`modules/${key}/module.json names key "${moduleJson.key}"`);
110
+
111
+ const rel = listFiles(dir).filter((p) => !NEVER_PACKED.has(p));
112
+ const denied = deniedFiles(key, rel);
113
+ if (denied.length) {
114
+ throw new Error(`the publish denylist refuses: ${denied.join(', ')} — remove them from the module (ADR 0098)`);
115
+ }
116
+ const unsafe = rel.filter((p) => !isSafeRelPath(p));
117
+ if (unsafe.length) throw new Error(`unpackable file name(s): ${unsafe.join(', ')}`);
118
+
119
+ const files = [];
120
+ const entries = [];
121
+ for (const p of rel) {
122
+ const abs = path.join(dir, ...p.split('/'));
123
+ const buf = fs.readFileSync(abs);
124
+ const mode = normalizeMode(modeOf ? modeOf(abs) : (fs.statSync(abs).mode & 0o777).toString(8));
125
+ files.push({ path: p, mode, sha256: sha256hex(buf) });
126
+ entries.push({ name: TAR_ROOT + p, mode: parseInt(mode, 8), buf });
127
+ }
128
+ const manifest = buildModuleManifest({
129
+ key, version: moduleJson.version, coreVersion: moduleJson.coreVersion,
130
+ builtAtIso: now().toISOString(), files, treeHash: canonicalTreeHash(files),
131
+ });
132
+ entries.push({ name: INNER_MANIFEST, mode: 0o644, buf: Buffer.from(JSON.stringify(manifest, null, 2) + '\n') });
133
+ const tgz = buildTar(entries);
134
+ const sidecar = { ...manifest, tarball: { name: tarballName(key, manifest.module_version), sha256: sha256hex(tgz), bytes: tgz.length } };
135
+ return { tgz, manifest, sidecar, moduleJson };
136
+ }
137
+
138
+ // The store's check on an upload. Trusts NOTHING the client says beyond the bytes:
139
+ // every hash is recomputed, the inner manifest must describe exactly the files the
140
+ // tarball holds, module.json must validate and name the route's key, and the
141
+ // denylist runs again. Resolves { ok: true, ... } or { ok: false, code, message }.
142
+ // Async so the inflate runs off the event loop (readTarAsync); the rest is one pass
143
+ // over at most READ_LIMITS.maxUnpackedBytes.
144
+ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES } = {}) {
145
+ const fail = (code, message) => ({ ok: false, code, message });
146
+ if (!Buffer.isBuffer(tgz) || tgz.length === 0) return fail('empty_artifact', 'the upload is empty');
147
+ if (tgz.length > maxBytes) return fail('artifact_too_large', `the tarball is over ${maxBytes} bytes`);
148
+
149
+ let entries;
150
+ try { entries = await readTarAsync(tgz, READ_LIMITS); }
151
+ catch (e) { return fail('bad_tarball', e.message); }
152
+
153
+ const byName = new Map();
154
+ for (const e of entries) {
155
+ if (!e.name.startsWith(TAR_ROOT)) return fail('bad_tarball', `"${e.name}" is outside ${TAR_ROOT}`);
156
+ const rel = e.name.slice(TAR_ROOT.length);
157
+ if (!isSafeRelPath(rel)) return fail('bad_tarball', `"${e.name}" is not a safe path`);
158
+ if (byName.has(rel)) return fail('bad_tarball', `"${e.name}" appears twice`);
159
+ byName.set(rel, e);
160
+ }
161
+ const inner = byName.get(INNER_MANIFEST_PATH);
162
+ if (!inner) return fail('no_manifest', `the tarball carries no ${INNER_MANIFEST}`);
163
+ let manifest;
164
+ try { manifest = JSON.parse(inner.buf.toString('utf8')); }
165
+ catch { return fail('no_manifest', `${INNER_MANIFEST} is not valid JSON`); }
166
+ if (manifest.artifact !== ARTIFACT_NAME || manifest.manifest_schema !== MANIFEST_SCHEMA || !Array.isArray(manifest.files)) {
167
+ return fail('bad_manifest', `${INNER_MANIFEST} is not a ${ARTIFACT_NAME} schema ${MANIFEST_SCHEMA} manifest`);
168
+ }
169
+
170
+ // Recompute the pin from the bytes and hold the manifest to it, file by file.
171
+ byName.delete(INNER_MANIFEST_PATH);
172
+ const actual = [...byName.entries()]
173
+ .map(([p, e]) => ({ path: p, mode: normalizeMode(e.mode.toString(8)), sha256: sha256hex(e.buf) }))
174
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
175
+ const declared = [...manifest.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
176
+ if (declared.length !== actual.length || manifest.file_count !== actual.length) {
177
+ return fail('hash_mismatch', `the manifest lists ${declared.length} file(s) but the tarball holds ${actual.length}`);
178
+ }
179
+ for (let i = 0; i < actual.length; i++) {
180
+ const a = actual[i]; const d = declared[i];
181
+ if (d.path !== a.path || normalizeMode(d.mode) !== a.mode || d.sha256 !== a.sha256) {
182
+ return fail('hash_mismatch', `"${a.path}" does not match its manifest entry`);
183
+ }
184
+ }
185
+ const treeSha = canonicalTreeHash(actual);
186
+ if (manifest.tree_sha256 !== treeSha) return fail('hash_mismatch', 'tree_sha256 does not match the files');
187
+
188
+ const denied = deniedFiles(key, actual.map((f) => f.path));
189
+ if (denied.length) return fail('denied_content', `the publish denylist refuses: ${denied.join(', ')}`);
190
+
191
+ const mj = byName.get('module.json');
192
+ if (!mj) return fail('bad_module', 'the tarball has no module.json');
193
+ let moduleJson;
194
+ try { moduleJson = JSON.parse(mj.buf.toString('utf8')); }
195
+ catch { return fail('bad_module', 'module.json is not valid JSON'); }
196
+ const v = validateManifest(moduleJson);
197
+ if (!v.valid) return fail('bad_module', `module.json is invalid: ${v.errors.join('; ')}`);
198
+ if (moduleJson.key !== key || manifest.module_key !== key) {
199
+ return fail('key_mismatch', `the tarball is module "${moduleJson.key}", not "${key}"`);
200
+ }
201
+ if (manifest.module_version !== moduleJson.version || manifest.core_version !== moduleJson.coreVersion) {
202
+ return fail('bad_manifest', 'the manifest version does not match module.json');
203
+ }
204
+
205
+ return {
206
+ ok: true, manifest, moduleJson,
207
+ version: moduleJson.version, coreVersion: moduleJson.coreVersion,
208
+ treeSha256: treeSha, tarballSha256: sha256hex(tgz), bytes: tgz.length,
209
+ };
210
+ }
211
+
212
+ module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact };
@@ -102,12 +102,12 @@ function envPrefix() {
102
102
  // Pure builders (exported for tests — no I/O, no network)
103
103
  // ---------------------------------------------------------------------------
104
104
 
105
- // dev-box -> "Dev Box"; economy -> "Economy". A readable default title.
105
+ // art-pipeline -> "Art Pipeline"; economy -> "Economy". A readable default title.
106
106
  function titleize(key) {
107
107
  return String(key).split(/[-_]+/).filter(Boolean)
108
108
  .map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
109
109
  }
110
- // dev-box -> devBox (for the generated route-factory function name).
110
+ // art-pipeline -> artPipeline (for the generated route-factory function name).
111
111
  function camel(key) {
112
112
  return String(key).replace(/[-_]+(.)/g, (_, c) => c.toUpperCase());
113
113
  }
@@ -125,6 +125,8 @@ function buildManifest(key, { core = coreVersion() } = {}) {
125
125
  key,
126
126
  title: titleize(key),
127
127
  description: `TODO: one sentence describing the ${key} module.`,
128
+ // The module's own version (task 1003781). A fresh scaffold is pre-release.
129
+ version: '0.1.0',
128
130
  coreVersion: `^${core}`,
129
131
  default: false,
130
132
  author: 'TODO: your name or builder handle',
@@ -265,7 +267,7 @@ function evaluateUpgrade({ rows = [], errors = [], core } = {}) {
265
267
 
266
268
  function scaffoldModule({ key, modulesDir = MODULES_DIR, core = coreVersion(), force = false } = {}) {
267
269
  if (!key || !KEY_RE.test(key)) {
268
- throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "dev-box")`);
270
+ throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "art-pipeline")`);
269
271
  }
270
272
  const dir = path.join(modulesDir, key);
271
273
  if (fs.existsSync(dir) && !force) {
@@ -411,14 +413,14 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
411
413
  return out.join('\n');
412
414
  }
413
415
 
414
- out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
416
+ out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
415
417
  for (const r of shown) {
416
418
  const mark = r.enabled ? '[x]' : '[ ]';
417
419
  // An inferred credit is marked so it never reads as something the author
418
420
  // typed — the catalog's whole job is honest attribution (ADR 0107 §2).
419
421
  const suffix = r.credit.inferred ? '*' : '';
420
422
  const upkeep = (r.maintenance && r.maintenance.status) || 'undeclared';
421
- out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
423
+ out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
422
424
  }
423
425
  out.push('');
424
426
  out.push(' [x] enabled here · [ ] available, not enabled');
@@ -776,6 +778,90 @@ async function cmdSubmit(args, {
776
778
  return 0;
777
779
  }
778
780
 
781
+ // ---------------------------------------------------------------------------
782
+ // bongos module publish <key> — put a module version in the store (ADR 0338 D1)
783
+ // ---------------------------------------------------------------------------
784
+
785
+ // `bongos module publish <key>` — task 1004271. Packs modules/<key>/ into the
786
+ // store's tarball + hash manifest (scripts/gds/module-artifact.js — the core
787
+ // release's own format, through the publish denylist), then uploads it to
788
+ // POST /store/modules/:key/versions, which re-verifies every hash itself and
789
+ // records the version. `--dry-run` packs and reports without uploading;
790
+ // `--out <dir>` also writes the tarball and its side-car manifest there.
791
+ // Distinct from `submit`, which proposes a module INTO core (ADR 0135).
792
+ async function cmdPublish(args, {
793
+ log = console.log, errlog = console.error,
794
+ modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
795
+ } = {}) {
796
+ const key = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--out');
797
+ if (!key) {
798
+ errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
799
+ return 2;
800
+ }
801
+ const dryRun = args.includes('--dry-run');
802
+ const outIdx = args.indexOf('--out');
803
+ const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
804
+ if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
805
+ errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
806
+ return 2;
807
+ }
808
+
809
+ const artifact = require('./module-artifact');
810
+ let packed;
811
+ try { packed = (pack || ((k) => artifact.packModule(k, { modulesDir })))(key); }
812
+ catch (e) { errlog(`REFUSING: ${e.message}`); return 1; }
813
+ const { tgz, sidecar } = packed;
814
+ log(`bongos module publish — "${key}" ${sidecar.module_version} (needs core ${sidecar.core_version})`);
815
+ log(` ✓ packed ${sidecar.file_count} file(s), ${tgz.length} bytes — tree ${sidecar.tree_sha256.slice(0, 12)}…`);
816
+ if (tgz.length > artifact.MAX_TARBALL_BYTES) {
817
+ errlog(`REFUSING: the tarball is ${tgz.length} bytes, over the store's ${artifact.MAX_TARBALL_BYTES}-byte cap.`);
818
+ return 1;
819
+ }
820
+
821
+ if (outDir) {
822
+ fs.mkdirSync(outDir, { recursive: true });
823
+ const tgzPath = path.join(outDir, sidecar.tarball.name);
824
+ writeFile(tgzPath, tgz);
825
+ writeFile(tgzPath.replace(/\.tgz$/, '.manifest.json'), JSON.stringify(sidecar, null, 2) + '\n');
826
+ log(` ✓ wrote ${tgzPath} (+ .manifest.json)`);
827
+ }
828
+ if (dryRun) {
829
+ log('\n--dry-run: nothing uploaded.');
830
+ return 0;
831
+ }
832
+
833
+ const doUpload = upload || (async (k, body) => {
834
+ // Lazy require — the same reason cmdSubmit gives: this file loads in the server
835
+ // process too, and cli-lib's session/fetch plumbing must never come with it.
836
+ const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
837
+ const session = readSessionToken();
838
+ if (!session) throw new Error('no Bongos session — run `bongos setup` first');
839
+ const res = await dispatchedFetch(`${session.apiBase}/api/bongos/store/modules/${encodeURIComponent(k)}/versions`, {
840
+ method: 'POST',
841
+ headers: { 'Content-Type': 'application/gzip', Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT },
842
+ body,
843
+ dispatcher: noKeepAliveDispatcher(),
844
+ });
845
+ const text = await res.text();
846
+ let data;
847
+ try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
848
+ return { status: res.status, ok: res.ok, data };
849
+ });
850
+
851
+ let res;
852
+ try { res = await doUpload(key, tgz); }
853
+ catch (e) { errlog(`bongos module publish: upload failed — ${e.message}`); return 1; }
854
+ if (!res.ok) {
855
+ const err = res.data && (res.data.error || res.data);
856
+ const detail = (err && (err.message || err.code)) || JSON.stringify(res.data);
857
+ errlog(`\nREFUSING: the store rejected "${key}" ${sidecar.module_version} (HTTP ${res.status}): ${detail}`);
858
+ return 1;
859
+ }
860
+ const row = res.data && res.data.version;
861
+ log(`\n✓ Published "${key}" ${row ? row.version : sidecar.module_version} to the store${res.data && res.data.created_module ? ' — its first version, so you are its author' : ''}.`);
862
+ return 0;
863
+ }
864
+
779
865
  function printHelp(out = console.log) {
780
866
  out('bongos module — scaffold + manage Cloud Bongos modules\n');
781
867
  out('Usage:');
@@ -787,6 +873,8 @@ function printHelp(out = console.log) {
787
873
  out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
788
874
  out(' bongos module submit <key> [--sign-off "Name <email>"]');
789
875
  out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
876
+ out(' bongos module publish <key> [--dry-run] [--out <dir>]');
877
+ out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
790
878
  }
791
879
 
792
880
  async function main(argv) {
@@ -797,6 +885,7 @@ async function main(argv) {
797
885
  if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
798
886
  if (sub === 'check') return cmdCheck(args.slice(1));
799
887
  if (sub === 'submit') return cmdSubmit(args.slice(1));
888
+ if (sub === 'publish') return cmdPublish(args.slice(1));
800
889
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
801
890
  printHelp();
802
891
  return sub ? 0 : 2;
@@ -807,7 +896,7 @@ async function main(argv) {
807
896
  }
808
897
 
809
898
  module.exports = {
810
- main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, printHelp,
899
+ main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, printHelp,
811
900
  gatherCatalog, formatCatalog, ownProjectName,
812
901
  scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
813
902
  formatMaintenanceHint, isPastRemoveAfter,
@@ -103,6 +103,7 @@ const REPO_ROOT = path.resolve(__dirname, '..', '..');
103
103
  // Reuse — never re-type. The core definition, the redaction, and the tree parser
104
104
  // all already exist and are unit-tested at their homes.
105
105
  const { isPublishable, PUBLISH_ALLOWLIST } = require(path.join(REPO_ROOT, 'scripts/gds/publish-manifest.js'));
106
+ const { sha256hex, canonicalTreeHash, normalizeMode, octal, tarChecksum, ustarHeader, splitUstarName, buildTar } = require('./artifact-format');
106
107
  const {
107
108
  buildRedactionRules, collectSensitiveLiterals, findLeaks,
108
109
  } = require(path.join(REPO_ROOT, 'scripts/gds/mirror-redact.js'));
@@ -144,9 +145,6 @@ function coreVersion() {
144
145
  // Pure helpers (exported for tests — no I/O, no git, no network)
145
146
  // ---------------------------------------------------------------------------
146
147
 
147
- function sha256hex(buf) {
148
- return crypto.createHash('sha256').update(buf).digest('hex');
149
- }
150
148
 
151
149
  // ---------------------------------------------------------------------------
152
150
  // CONSUMER-CORE extraction policy (task 1824) — the file-classification layer.
@@ -418,31 +416,8 @@ function prunePackageLock(sourceLock, { name, version, rootRanges, rootDepNames
418
416
  };
419
417
  }
420
418
 
421
- // The PIN. A canonical, compression-independent digest of the core tree: one
422
- // "path\0mode\0filesha256\n" line per file, byte-sorted by path. A consumer
423
- // recomputes this from the unpacked tree to prove an install matches the pin.
424
- // `files` = [{ path, mode, sha256 }]. Modes are normalized so the pin is stable
425
- // regardless of the checkout's umask.
426
- function canonicalTreeHash(files) {
427
- const lines = (files || [])
428
- .map((f) => `${f.path}\u0000${normalizeMode(f.mode)}\u0000${f.sha256}`)
429
- .sort();
430
- return sha256hex(Buffer.from(lines.join('\n') + '\n', 'utf8'));
431
- }
432
-
433
- // Collapse ANY mode form — git's 100644/100755, our canonical 0000644/0000755, or a
434
- // raw filesystem octal string like "755" — to two canonical tar modes, keyed only on
435
- // the executable bit. IDEMPOTENT (normalizeMode(normalizeMode(x)) === normalizeMode(x))
436
- // AND exec-aware, so the tree pin reproduces byte-identically whether a consumer
437
- // recomputes it from the manifest's files[] (already-canonical modes) or from the
438
- // unpacked tree's stat() modes. Expects a STRING (git mode / canonical mode / octal).
439
- function normalizeMode(mode) {
440
- const s = String(mode);
441
- if (s === '100755' || s === '0000755' || s === '755') return '0000755';
442
- const n = parseInt(s, 8);
443
- if (Number.isFinite(n) && (n & 0o111)) return '0000755';
444
- return '0000644';
445
- }
419
+ // The PIN (canonicalTreeHash) and normalizeMode live in scripts/gds/artifact-format.js
420
+ // (task 1004271) — the module store verifies with the same functions.
446
421
 
447
422
  // The pin manifest (WITHOUT tarball.* — added by withTarballInfo for the side-car).
448
423
  function buildCoreManifest({
@@ -478,94 +453,8 @@ function tarballName(version) { return `${ARTIFACT_NAME}-${version}.tgz`; }
478
453
  function manifestName(version) { return `${ARTIFACT_NAME}-${version}.manifest.json`; }
479
454
  function stageDirName(version) { return `${ARTIFACT_NAME}-${version}`; }
480
455
 
481
- // ---- a minimal, correct, DETERMINISTIC ustar+gzip writer (node-only, #686) ----
482
- // Deterministic (fixed mtime/uid/gid, normalized mode, sorted entries) so the same
483
- // source yields byte-identical bytes — a bonus over the tree_sha256 pin. Long paths
484
- // use the ustar name/prefix split, falling back to a GNU 'L' longname entry (read
485
- // by GNU tar, bsdtar/macOS, and node-tar) for the rare path that won't split.
486
-
487
- function octal(n, width) {
488
- // width-1 octal digits, zero-padded, then a NUL — the classic numeric field.
489
- return Buffer.from(n.toString(8).padStart(width - 1, '0').slice(-(width - 1)) + '\0', 'ascii');
490
- }
491
-
492
- function tarChecksum(block) {
493
- let sum = 0;
494
- for (let i = 0; i < 512; i++) sum += (i >= 148 && i < 156) ? 0x20 : block[i];
495
- return sum;
496
- }
497
-
498
- // Build one 512-byte ustar header. `name`/`prefix` must already fit (<=100 / <=155).
499
- function ustarHeader({ name, prefix = '', mode, size, typeflag }) {
500
- const h = Buffer.alloc(512, 0);
501
- h.write(name, 0, 100, 'utf8');
502
- octal(mode, 8).copy(h, 100);
503
- octal(0, 8).copy(h, 108); // uid
504
- octal(0, 8).copy(h, 116); // gid
505
- octal(size, 12).copy(h, 124);
506
- octal(0, 12).copy(h, 136); // mtime = 0 (determinism)
507
- h.write(typeflag, 156, 1, 'ascii');
508
- h.write('ustar\0', 257, 6, 'ascii');
509
- h.write('00', 263, 2, 'ascii');
510
- if (prefix) h.write(prefix, 345, 155, 'utf8');
511
- // checksum field: 6 octal digits + NUL + space, computed with the field = spaces.
512
- const cksum = tarChecksum(h);
513
- h.write(cksum.toString(8).padStart(6, '0').slice(-6) + '\0 ', 148, 8, 'ascii');
514
- return h;
515
- }
516
-
517
- // Split a >100-byte path into { name<=100, prefix<=155 } at a '/' boundary, or
518
- // return null if no split fits (→ caller uses the GNU longname fallback).
519
- function splitUstarName(fullPath) {
520
- const bytes = Buffer.byteLength(fullPath, 'utf8');
521
- if (bytes <= 100) return { name: fullPath, prefix: '' };
522
- // Find the LAST '/' such that the tail (after it) is <=100 and the head <=155.
523
- for (let i = fullPath.length - 1; i > 0; i--) {
524
- if (fullPath[i] !== '/') continue;
525
- const prefix = fullPath.slice(0, i);
526
- const name = fullPath.slice(i + 1);
527
- if (Buffer.byteLength(name, 'utf8') <= 100 && Buffer.byteLength(prefix, 'utf8') <= 155) {
528
- return { name, prefix };
529
- }
530
- }
531
- return null;
532
- }
533
-
534
- function padTo512(buf) {
535
- const rem = buf.length % 512;
536
- return rem === 0 ? buf : Buffer.concat([buf, Buffer.alloc(512 - rem, 0)]);
537
- }
538
-
539
- // GNU longname pseudo-entry: a header (typeflag 'L', name '././@LongLink') whose
540
- // BODY is the full path (incl. trailing NUL), immediately followed by the real
541
- // file entry. Read by GNU tar, bsdtar/macOS, and node-tar.
542
- function gnuLongName(fullPath) {
543
- const payload = Buffer.from(fullPath + '\0', 'utf8');
544
- const hdr = ustarHeader({ name: '././@LongLink', mode: 0o644, size: payload.length, typeflag: 'L' });
545
- return Buffer.concat([hdr, padTo512(payload)]);
546
- }
547
-
548
- // entries: [{ name, mode, buf }] — `name` includes the TAR_ROOT prefix. Returns a
549
- // gzip'd tar Buffer. Pure + deterministic given sorted entries.
550
- function buildTar(entries) {
551
- const sorted = [...entries].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
552
- const chunks = [];
553
- for (const e of sorted) {
554
- const mode = e.mode & 0o7777;
555
- const split = splitUstarName(e.name);
556
- if (split) {
557
- chunks.push(ustarHeader({ name: split.name, prefix: split.prefix, mode, size: e.buf.length, typeflag: '0' }));
558
- } else {
559
- chunks.push(gnuLongName(e.name));
560
- chunks.push(ustarHeader({ name: e.name.slice(0, 100), mode, size: e.buf.length, typeflag: '0' }));
561
- }
562
- chunks.push(padTo512(e.buf));
563
- }
564
- // two zero blocks terminate the archive
565
- chunks.push(Buffer.alloc(1024, 0));
566
- const tar = Buffer.concat(chunks);
567
- return zlib.gzipSync(tar, { level: 9 });
568
- }
456
+ // The ustar+gzip writer (buildTar and its header helpers) lives in
457
+ // scripts/gds/artifact-format.js (task 1004271), shared with the module store.
569
458
 
570
459
  // ---- the git-LESS source: enumerate the working tree (task 1003995) --------
571
460
  //
@@ -8,7 +8,7 @@
8
8
  // what happened, what the agent will do next, whether a PERSON is needed (and who), and
9
9
  // whether this is a rule rather than a fault. That last one is the expensive mistake:
10
10
  // an agent that reads a deliberate restriction as breakage goes looking for a way round
11
- // it (the box session that asked the owner for a personal access token).
11
+ // it (the agent that asked the owner for a personal access token).
12
12
  //
13
13
  // STATIC TEXT ONLY. A card interpolates nothing — no task title, no server message — so
14
14
  // printing it adds no prompt-injection surface (the strand-watch / push-path-brief rule).
@@ -83,7 +83,7 @@ const CONFIG = {
83
83
  tag: process.env.PROVISION_TAG || 'cloudbongos-instance',
84
84
  doToken: process.env.DO_API_TOKEN || null,
85
85
  sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
86
- // The Cloudflare DNS hook (the SAME infra/box-dns-cloudflare.sh box.js uses).
86
+ // The Cloudflare DNS hook (the instance's infra/box-dns-cloudflare.sh).
87
87
  // P4 (#1945) formalizes the UPSERT: point the instance's domain at the target
88
88
  // public IP (below), in the domain's OWN Cloudflare zone, proxied=false (see
89
89
  // dnsUpsertEnv). No hook configured → DNS is skipped (a co-tenant instance is
@@ -179,7 +179,7 @@ const MANIFEST_VERIFY_TRIES = numEnv('PROVISION_MANIFEST_VERIFY_TRIES', 90);
179
179
  const MANIFEST_VERIFY_INTERVAL_MS = numEnv('PROVISION_MANIFEST_VERIFY_INTERVAL_MS', 2000);
180
180
 
181
181
  // ---------------------------------------------------------------------------
182
- // Lazy dep load (so --help / arg errors need no Postgres) — the box.js pattern.
182
+ // Lazy dep load (so --help / arg errors need no Postgres).
183
183
  // ---------------------------------------------------------------------------
184
184
  let _deps;
185
185
  function loadDeps() {
@@ -130,8 +130,7 @@ function dnsUpsertEnv(inst, { publicIp, zone, proxied = false } = {}) {
130
130
  }
131
131
 
132
132
  // The RFC 5737 TEST-NET-1 sentinel a released hostname is parked at (task
133
- // 1003237) — the shape scripts/gds/box-infra.js proved for a torn-down box
134
- // (idea 309 / task 1234). 192.0.2.1 is reserved for documentation and is
133
+ // 1003237, first proven in task 1234). 192.0.2.1 is reserved for documentation and is
135
134
  // GUARANTEED never routable, so a name parked here can never dial platform
136
135
  // infra or a recycled address.
137
136
  const DNS_BLACKHOLE_IP = '192.0.2.1';
@@ -141,7 +140,7 @@ const DNS_BLACKHOLE_IP = '192.0.2.1';
141
140
  // address UPSERTed at the platform's IP for a project that no longer answers
142
141
  // there — the ADR 0181 §2 leak — and this is how the runner lets it go.
143
142
  //
144
- // REPOINT, not delete, for the reason box-infra.js records: deleting a name
143
+ // REPOINT, not delete (task 1234): deleting a name
145
144
  // opens an NXDOMAIN window that resolvers negative-cache for the zone's SOA
146
145
  // neg-TTL (~30 min, not editable), so a client that queried mid-gap stays
147
146
  // broken long after. A name that always resolves — to a guaranteed-dead host —
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  // scripts/gds/provision.js — the INSTANCE-provisioning control-plane runner
4
- // (ADR 0111 §2/§3, BONGOS-V1 goal 32, task #1944). A sibling of box.js.
4
+ // (ADR 0111 §2/§3, BONGOS-V1 goal 32, task #1944).
5
5
  //
6
6
  // WHAT IT IS. The trust-boundary-safe executor for the `provisioning` module.
7
7
  // The internet-facing web tier can only ENQUEUE a provision/teardown intent into
@@ -20,7 +20,7 @@
20
20
  // → Caddy site block + reload
21
21
  // → /healthz
22
22
  //
23
- // DESIGN — inherited from box.js / do-api.js:
23
+ // DESIGN — shared with do-api.js:
24
24
  // • DRY-RUN by DEFAULT. Without --apply, every step only PRINTS the command it
25
25
  // WOULD run and mutates nothing (no DB writes, no shell, no cloud call). The
26
26
  // LIVE leg is an explicit operator/timer `--apply`.
@@ -37,8 +37,8 @@
37
37
  // • The step-command generators are PURE functions (exported) → the SAME command
38
38
  // strings drive local exec (co-tenant) AND the dedicated droplet's cloud-init.
39
39
  //
40
- // This is control-plane ops tooling (scripts/), so — like box.js — it may require
41
- // the core + a module's data slice directly (a module may not, but a CLI may). It
40
+ // This is control-plane ops tooling (scripts/), so it may require the core + a
41
+ // module's data slice directly (a module may not, but a CLI may). It
42
42
  // reuses modules/provisioning/provisioning.js (the intent queue + state) and
43
43
  // scripts/gds/do-api.js (the shared DO client extracted in task #1943).
44
44
 
@@ -336,7 +336,7 @@ async function provisionInstance(inst, deps) {
336
336
  if (inst.hosting_shape === 'dedicated') {
337
337
  // Dedicated droplet: create it + settle its public IP via the shared DO client
338
338
  // (do-api, dry-run-default). The on-droplet app standup rides cloud-init
339
- // user-data built from the SAME step commands below (mirrors box.js). For the
339
+ // user-data built from the SAME step commands below. For the
340
340
  // dry-run plan we surface the intent; --apply creates the droplet.
341
341
  const doApi = deps.doApi;
342
342
  log(' [server] dedicated droplet');
@@ -1249,8 +1249,8 @@ async function drainOAuthManifests(deps) {
1249
1249
  }
1250
1250
 
1251
1251
  // ---------------------------------------------------------------------------
1252
- // run-intents — drain provisioning_intents (the systemd-timer entry point). The
1253
- // box.js cmdRunIntents shape: dry-run lists the pending queue; --apply claims +
1252
+ // run-intents — drain provisioning_intents (the systemd-timer entry point).
1253
+ // Dry-run lists the pending queue; --apply claims +
1254
1254
  // executes each, with a poison-attempt cap. FOR UPDATE SKIP LOCKED (in
1255
1255
  // provisioning.claimNextIntent) makes overlapping ticks safe. It ALSO drains the
1256
1256
  // GitHub-App-Manifest conversion queue on the same tick (task 2080).
@@ -1324,8 +1324,8 @@ async function cmdRunIntents(deps) {
1324
1324
 
1325
1325
  // A light drift reconcile: surface instances stuck mid-flight (provisioning /
1326
1326
  // tearing_down) with no open intent — a runner died mid-op. Reports; --apply
1327
- // re-enqueues a fresh intent so the next tick retries. (Full droplet-vs-DB drift,
1328
- // the box.js reconcile-drift analogue, is a later hardening.)
1327
+ // re-enqueues a fresh intent so the next tick retries. (Full droplet-vs-DB drift
1328
+ // is a later hardening.)
1329
1329
  async function cmdReconcile(deps) {
1330
1330
  const { db, provisioning, log, apply } = deps;
1331
1331
  const { rows } = await db.query(
@@ -68,9 +68,8 @@ const PUBLISH_ALLOWLIST = [
68
68
  'modules/hall-ui/',
69
69
  'modules/status-ui/',
70
70
  'modules/public-landing/', // the front door (task 1003322, ADR 0218) — the repo-root public-landing/ entry this replaces was always here, and for the same reason: default:true, and it is the only thing an instance serves at its apex root. Omit it and a released core answers the game-less 404 tail on its own home page.
71
- 'modules/dev-box/',
72
71
  'modules/discord/',
73
- 'modules/provisioning/', // instance-hosting control plane — CORE like its sibling dev-box (same do-api/cf-tunnel toolchain, all core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
72
+ 'modules/provisioning/', // instance-hosting control plane — CORE (its do-api toolchain is core). Reclassified IN so the core CLI scripts/gds/provision.js resolves it from the core package post-split, not a MODULE_NOT_FOUND (task 2135). default:false — shipped ≠ enabled.
74
73
  'modules/agents/', // the declarative agent registry + spawn seam (goal 1000038) — CORE: its own manifest declares maintenance.status 'core-maintained', and it consumes the core `grade` port, so a released core without it cannot run the MAS machinery the methodology describes. default:false — shipped is not enabled (ADR 0083), exactly like provisioning + platform-identity below. Omitted at module-creation time (tasks 1002485/1002486): the omission was invisible until tests/agents_spawn.mjs became the first PUBLISHABLE file to import it and the R89 boundary guard caught it (task 1002489).
75
74
  'modules/npm-release/', // the deploy page's "where is the work" + version history (task 1002622). default:false — on only in the hall that publishes the core. MUST be here: modules/hall-ui/ ships as one subtree, so a released core without this carries deploy.js's loader and the #nr-work mounts with no routes behind them — the copy-desk asymmetry above.
76
75
  'modules/platform-identity/', // hub identity provider (ADR 0141) — ships in core, enabled ONLY on the hub (cloudbongos.com). MUST be here or the hub's enabledModules() throws "unknown module" and provision.js's federation load MODULE_NOT_FOUNDs (task 1002235, same rationale as provisioning above). default:false — shipped ≠ enabled; a vanilla/self-hosted instance never turns it on.