@bongos/core 1.19.1072 → 1.19.1074

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 (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -1,150 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Smoke test for the per-builder dev box lifecycle (ADR 0031 §2/§5, #598).
3
- #
4
- # Unlike the other GDS smokes, this one is HERMETIC — no live server, no DB, no
5
- # DigitalOcean account, no network. It verifies the decision core + the dry-run
6
- # contract that make the lifecycle safe to ship before a single live box exists:
7
- #
8
- # 1. tests/boxes.mjs — pure logic (rank gate, cost, idle/dormant selection,
9
- # transition legality) + the DO client's dry-run/live contract.
10
- # 2. route-rank invariant — the three /box* routes classify to their TRUE gate
11
- # (archon roster, any-builder own-read + heartbeat), no 'unknown' findings.
12
- # 3. modules load — boxes.js, do-api.js, routes/box.js, and routes.js (mount).
13
- # 4. box.js --help + a dry-run plan run without a DB (control-plane safety:
14
- # `--apply` is required to touch anything; dry-run is inert).
15
- #
16
- # Run: bash scripts/gds/smoke-box.sh
17
- set -euo pipefail
18
-
19
- cd "$(dirname "$0")/../.."
20
-
21
- step() { printf '\n\033[1;36m==> %s\033[0m\n' "$*"; }
22
- fail() { printf '\033[0;31m✖ %s\033[0m\n' "$*" >&2; exit 1; }
23
-
24
- step "1. pure-logic + DO-client unit test (tests/boxes.mjs)"
25
- node tests/boxes.mjs || fail "boxes.mjs unit test failed"
26
-
27
- step "1b. box-access decision core (#600 rank-gated source access)"
28
- # tests/box_access.mjs (23 assertions) — re-added under #674 (written+passing in
29
- # #600 but deferred by parallel-claim conflicts on tests/). Covers the PURE
30
- # source-access decisions (decideSourceAccess / reconcileSourceAccess /
31
- # cloneSpecForScope / boxCanHoldSource / STARTER_SPARSE_PATHS) the
32
- # GET /box/source-access route and `box.js reconcile` both consume.
33
- node tests/box_access.mjs || fail "box_access.mjs unit test failed"
34
-
35
- step "1b2. box-credential provider (#600 scoped credential issuance)"
36
- # Kept inline: box-credential.js has no dedicated unit file yet. These pin the
37
- # env-provider's configured/unconfigured, scope→token/repo, and separate-starter-
38
- # repo hard-boundary behaviour so credential issuance never ships unverified.
39
- node -e "
40
- const c = require('./modules/dev-box/box-credential.js');
41
- const assert = require('node:assert').strict;
42
- let n = 0; const ok = (name, fn) => { fn(); n++; };
43
- ok('unconfigured→configured:false', () => assert.equal(c.makeEnvCredentialProvider({}).issue('full',{nowMs:0}).configured,false));
44
- ok('configured full→token+url', () => { const r=c.makeEnvCredentialProvider({BOX_SOURCE_REPO_URL:'u',BOX_SOURCE_FULL_TOKEN:'t'}).issue('full',{nowMs:0}); assert.equal(r.configured,true); assert.equal(r.token,'t'); });
45
- ok('starter falls back to full token', () => assert.equal(c.makeEnvCredentialProvider({BOX_SOURCE_REPO_URL:'u',BOX_SOURCE_FULL_TOKEN:'t'}).issue('starter',{nowMs:0}).token,'t'));
46
- ok('separate starter repo→hard-boundary flag', () => { const r=c.makeEnvCredentialProvider({BOX_SOURCE_REPO_URL:'u',BOX_SOURCE_FULL_TOKEN:'t',BOX_SOURCE_STARTER_REPO_URL:'s',BOX_SOURCE_STARTER_TOKEN:'st'}).issue('starter',{nowMs:0}); assert.equal(r.repoUrl,'s'); assert.equal(r.separateStarterRepo,true); });
47
- console.log(' ' + n + ' box-credential provider assertions passed');
48
- " || fail "box-credential provider failed"
49
-
50
- step "1c. onboarding module — SSH-key validation + renderers (#685, tests/box_onboard.mjs)"
51
- node tests/box_onboard.mjs || fail "box_onboard.mjs unit test failed"
52
-
53
- step "1c2. named-tunnel CF client — dry-run/live contract + ensure/teardown (#771, tests/cf_tunnel.mjs)"
54
- node tests/cf_tunnel.mjs || fail "cf_tunnel.mjs unit test failed"
55
-
56
- step "1d. end-to-end — real box-connect.sh + box-authorized-keys-fetch.sh vs a stub server (#685)"
57
- # Runs the GENUINE shell scripts against the real route handlers (stubbed DB):
58
- # key-gen -> register -> install settings, and the box-side authkeys sync + the
59
- # 403 clawback. Needs bash + curl + ssh-keygen (present on macOS + CI ubuntu).
60
- if command -v ssh-keygen >/dev/null 2>&1; then
61
- node tests/box_connect_e2e.mjs || fail "box_connect_e2e.mjs end-to-end failed"
62
- else
63
- echo " (skipped — ssh-keygen not available)"
64
- fi
65
-
66
- step "1e. PowerShell bootstrap parses (#685) — best-effort, skipped where pwsh is absent"
67
- # box-connect.ps1 has no behavioral e2e (the harness is bash-only; the empty-
68
- # passphrase + BOM-less-write fixes rest on review). This at least catches gross
69
- # syntax errors on a pwsh-equipped machine (CI / a Windows runner); it no-ops on
70
- # a stock macOS/Linux dev box without PowerShell.
71
- if command -v pwsh >/dev/null 2>&1; then
72
- pwsh -NoProfile -Command '$ErrorActionPreference="Stop"; $null=[System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path infra/box-connect.ps1), [ref]$null, [ref]$null); Write-Output " box-connect.ps1 parses"' \
73
- || fail "box-connect.ps1 failed to parse under pwsh"
74
- else
75
- echo " (skipped — pwsh not installed; box-connect.ps1 behavior rests on review)"
76
- fi
77
-
78
- step "2. route-rank invariant — /box* routes classify to their true gate"
79
- node -e "
80
- const rc = require('./src/bongos/route-rank-check.js');
81
- const res = rc.checkRouteRanks();
82
- if (res.hardFail) { console.error('route-rank HARD FAIL'); process.exit(1); }
83
- const mine = res.routes.filter(r => r.file.endsWith('routes/box.js'));
84
- const want = {
85
- 'GET /boxes':'archon', 'GET /box/me':'any-builder', 'GET /box/source-access':'any-builder',
86
- 'POST /box/heartbeat':'any-builder',
87
- // #685 onboarding surface
88
- 'POST /box/ssh-key':'any-builder', 'GET /box/ssh-key':'any-builder', 'DELETE /box/ssh-key/:id':'any-builder',
89
- 'GET /box/authorized-keys':'any-builder', 'GET /box/managed-settings':'any-builder',
90
- // #701 onboarding middleware (auto-provision + wake-on-connect) + #760 terminal (ADR 0038)
91
- 'POST /box/ensure':'any-builder', 'GET /box/terminal':'any-builder', 'POST /box/terminal':'any-builder',
92
- 'GET /box/connect':'public', 'GET /box/connect.sh':'public', 'GET /box/connect.ps1':'public',
93
- };
94
- let bad = 0;
95
- for (const [key, rank] of Object.entries(want)) {
96
- const got = (mine.find(r => r.key === key) || {}).rank;
97
- if (got !== rank) { console.error(' MISMATCH ' + key + ': want ' + rank + ' got ' + got); bad++; }
98
- else console.log(' ok ' + key + ' -> ' + rank);
99
- }
100
- process.exit(bad === 0 ? 0 : 1);
101
- " || fail "route-rank classification wrong"
102
-
103
- step "3. modules load cleanly (incl. the routes.js mount edit)"
104
- node -e "
105
- require('./modules/dev-box/boxes.js');
106
- require('./modules/dev-box/box-access.js');
107
- require('./modules/dev-box/box-credential.js');
108
- require('./modules/dev-box/box-onboard.js');
109
- require('./scripts/gds/do-api.js');
110
- require('./scripts/gds/cf-tunnel.js');
111
- require('./modules/dev-box/routes/box.js');
112
- require('./src/bongos/db.js');
113
- require('./src/bongos/routes.js');
114
- console.log(' all modules loaded');
115
- " || fail "module load failed"
116
-
117
- step "4. CLI is DB-free for --help (control-plane safety); reconcile + onboard registered"
118
- node scripts/gds/box.js --help | grep -q 'reconcile' || fail "box.js reconcile subcommand missing from help"
119
- node scripts/gds/box.js --help | grep -q 'onboard' || fail "box.js onboard subcommand missing from help (#685)"
120
- node scripts/gds/box.js --help | grep -q 'run-intents' || fail "box.js run-intents subcommand missing from help (#701)"
121
- echo " box.js --help ok (reconcile + onboard + run-intents present)"
122
-
123
- step "5. cloud-init carries the box scripts as valid base64 (#600 source + #685 authkeys)"
124
- node -e "
125
- const fs = require('fs');
126
- const o = require('./modules/dev-box/box-onboard.js');
127
- const raw = fs.readFileSync('infra/builder-box-cloud-init.yaml','utf8');
128
- const inject = (name) => {
129
- const script = fs.readFileSync('infra/' + name,'utf8');
130
- const b64 = Buffer.from(script,'utf8').toString('base64');
131
- if (Buffer.from(b64,'base64').toString('utf8') !== script) { console.error(' ' + name + ' b64 does not round-trip'); process.exit(1); }
132
- return b64;
133
- };
134
- const block = o.wrapManagedBlock([{ public_key: 'ssh-ed25519 AAAA', label: 'laptop' }]);
135
- const rendered = raw
136
- .replace(/__REPO_URL__/g,'')
137
- .replace(/__BOX_SOURCE_FETCH_B64__/g, inject('box-source-fetch.sh'))
138
- .replace(/__BOX_AUTHKEYS_FETCH_B64__/g, inject('box-authorized-keys-fetch.sh'))
139
- .replace(/__BUILDER_AUTHORIZED_KEYS_B64__/g, Buffer.from(block,'utf8').toString('base64'))
140
- .replace(/__BOX_ENV_B64__/g, Buffer.from('','utf8').toString('base64'));
141
- const leftover = rendered.match(/__[A-Z_]+__/g);
142
- if (leftover) { console.error(' placeholders not substituted: ' + leftover.join(', ')); process.exit(1); }
143
- if (!raw.includes('encoding: b64')) { console.error(' write_files missing encoding: b64'); process.exit(1); }
144
- if (!raw.includes('/root/.ssh/otb-authorized-keys.block')) { console.error(' baked authorized_keys block not wired'); process.exit(1); }
145
- if (!raw.includes('box-authorized-keys-fetch.sh')) { console.error(' authkeys-fetch cron not wired'); process.exit(1); }
146
- if (!raw.includes('/etc/otb/box.env')) { console.error(' named-tunnel box.env not wired (#771)'); process.exit(1); }
147
- console.log(' source-fetch + authkeys-fetch scripts inject + round-trip; baked-keys block + box.env + crons wired');
148
- " || fail "cloud-init injection broken"
149
-
150
- printf '\n\033[1;32m✓ box-lifecycle smoke passed\033[0m\n'
@@ -1,241 +0,0 @@
1
- 'use strict';
2
-
3
- // scripts/gds/tree-preflight.js — does this checkout contain the code the command
4
- // is about to require? (task 1003098)
5
- //
6
- // THE FAILURE THIS REPLACES. On a TASK-SCOPED dev box — a supported, server-driven
7
- // configuration — `GET /box/source-access` returns a sparse path list that carries
8
- // `scripts`, `src/bongos`, `tests`, `clients`, `config` and NOT `modules/`, except
9
- // the one module the claimed task is scoped to. So the platform itself produces a
10
- // checkout in which its own entry-point commands cannot run, and they said so like
11
- // this:
12
- //
13
- // $ node scripts/gds/ship.js
14
- // Error: Cannot find module '../../modules/lifecycle/ship-preflight'
15
- // Require stack: scripts/gds/ship-state.js
16
- //
17
- // A raw stack trace, naming ONE missing thing. Fix that one and the next run names
18
- // the next — the filer lost a whole session to guess-read-fix-repeat, and their
19
- // workaround (`git sparse-checkout disable` plus commenting out the re-scope cron)
20
- // is not something a builder should have to discover.
21
- //
22
- // ENUMERATION IS THE WHOLE POINT. Node's loader is lazy by construction: it fails
23
- // at the first unresolvable specifier and cannot tell you about the second, because
24
- // it never got there. That is correct behaviour for a module loader and useless as
25
- // a diagnosis. This runs BEFORE the first such require and reports every missing
26
- // requirement it can see in ONE message.
27
- //
28
- // THE REQUIRED LIST IS COMPUTED, NEVER DECLARED. A hand-kept list of "what ship.js
29
- // needs" is a second copy of the require graph, and it would be wrong within a
30
- // month — ship.js alone fans out across sixteen ship-*.js phase files. So the
31
- // preflight WALKS the graph the same way the loader would: read the entry file,
32
- // find its `require('./x')` siblings, follow them, and collect every
33
- // `require('../../modules/...')` it passes. Pure string work over the file system;
34
- // nothing is executed, so a missing module cannot throw during the check that
35
- // exists to report it.
36
- //
37
- // WHY IT DOES NOT SIMPLY TRY/CATCH EACH REQUIRE. Individual guards are what the
38
- // codebase already does where a module is genuinely OPTIONAL (context-pack.js's
39
- // classifier, cli-lib.js's reward policy) and they are right for that: the command
40
- // still works, slightly diminished. These requires are not optional — ship.js
41
- // cannot ship without `modules/lifecycle/ship-preflight`. Wrapping them would turn
42
- // a loud failure into a confusing one.
43
- //
44
- // PURE + INJECTED (`readFile`, `exists`) so the whole thing is testable with a fake
45
- // file system and no box — which is the layer task 1003098 asks for the test at.
46
-
47
- const fsMod = require('node:fs');
48
- const pathMod = require('node:path');
49
-
50
- const REPO_ROOT = pathMod.resolve(__dirname, '..', '..');
51
-
52
- // A require of this repo's OWN module code, which is what a scoped checkout drops.
53
- // Matches the two spellings the CLI uses; the quote style is captured so a require
54
- // written with double quotes is not silently invisible to the walk.
55
- const MODULE_REQUIRE_RE = /require\(\s*['"](\.\.\/\.\.\/modules\/[^'"]+)['"]\s*\)/g;
56
- // A sibling require inside scripts/gds/ — the edges the walk follows.
57
- const LOCAL_REQUIRE_RE = /require\(\s*['"](\.\/[^'"]+)['"]\s*\)/g;
58
-
59
- // Comments are stripped before scanning. A header that QUOTES a require to explain
60
- // why it was removed would otherwise be walked as a live edge — the defect filed as
61
- // task 1003832 against a test that scanned raw source, and one this file would hit
62
- // immediately, because its own header above quotes the exact failing require.
63
- function stripComments(src) {
64
- return String(src)
65
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
66
- .replace(/^\s*\/\/.*$/gm, ' ');
67
- }
68
-
69
- // Is the character at `index` inside a try{} block?
70
- //
71
- // THE DISTINCTION THIS DRAWS IS THE WHOLE CORRECTNESS OF THE PREFLIGHT. Several of
72
- // the CLI's module requires are deliberately guarded — context-pack.js's classifier
73
- // and cli-lib.js's reward policy both sit in a try and fall back honestly, so those
74
- // commands RUN on a scoped box. Reporting them as missing requirements would refuse
75
- // a command that works, which is a worse failure than the one being fixed: a builder
76
- // who could have shipped is told they cannot.
77
- //
78
- // A brace walk, not a parser — this repo ships none (the route-rank-check /
79
- // exec-path-guard precedent). It tracks a stack of block kinds and answers whether
80
- // any enclosing block was opened by `try`. Comments are already stripped by the
81
- // caller; string and regex literals containing braces are the known blind spot, and
82
- // the failure direction is benign: a miscount can only mis-scope a block, and the
83
- // classification is proved against the real files by tests/tree_preflight.mjs, which
84
- // pins that start.js's requires read optional and ship.js's read required.
85
- function insideTry(src, index) {
86
- const stack = [];
87
- for (let i = 0; i < index && i < src.length; i++) {
88
- const c = src[i];
89
- if (c === '{') {
90
- // Look back past whitespace for the keyword that opened this block.
91
- const before = src.slice(Math.max(0, i - 200), i);
92
- stack.push(/\btry\s*$/.test(before) ? 'try' : 'block');
93
- } else if (c === '}') {
94
- stack.pop();
95
- }
96
- }
97
- return stack.includes('try');
98
- }
99
-
100
- function matchAll(src, re) {
101
- const out = [];
102
- let m;
103
- re.lastIndex = 0;
104
- while ((m = re.exec(src)) !== null) out.push({ spec: m[1], index: m.index });
105
- return out;
106
- }
107
-
108
- // Resolve a relative specifier the way require would, to a repo-relative posix path.
109
- function resolveRel(fromFile, spec, { root = REPO_ROOT } = {}) {
110
- const abs = pathMod.resolve(pathMod.dirname(fromFile), spec);
111
- const rel = pathMod.relative(root, abs).split(pathMod.sep).join('/');
112
- return rel;
113
- }
114
-
115
- // Add the `.js` a bare specifier omits. Only ever appended — a spec that already
116
- // names an extension is left exactly as written, so a `.json` require still points
117
- // at the file it names.
118
- function withJsExt(rel) {
119
- return /\.[a-z]+$/i.test(rel) ? rel : `${rel}.js`;
120
- }
121
-
122
- /**
123
- * Every module-code path the entry point's require graph reaches. PURE.
124
- *
125
- * Follows `./sibling` edges inside scripts/gds/ transitively and collects the
126
- * `../../modules/...` leaves. A file it cannot read is skipped rather than thrown
127
- * on: this runs on a checkout that is by definition incomplete, and a preflight
128
- * that crashes while diagnosing a partial tree is worse than the trace it replaces.
129
- *
130
- * @returns {string[]} repo-relative posix paths, deduped, stable order
131
- */
132
- function requiredModulePaths(entryFile, { readFile, root = REPO_ROOT } = {}) {
133
- const read = readFile || ((f) => fsMod.readFileSync(f, 'utf8'));
134
- const seen = new Set();
135
- const found = new Set();
136
- const queue = [pathMod.resolve(entryFile)];
137
-
138
- while (queue.length) {
139
- const file = queue.shift();
140
- if (seen.has(file)) continue;
141
- seen.add(file);
142
- let src;
143
- try { src = stripComments(read(file)); } catch (_) { continue; }
144
-
145
- for (const { spec, index } of matchAll(src, MODULE_REQUIRE_RE)) {
146
- // A guarded require is not a requirement — see insideTry.
147
- if (insideTry(src, index)) continue;
148
- found.add(withJsExt(resolveRel(file, spec, { root })));
149
- }
150
- for (const { spec } of matchAll(src, LOCAL_REQUIRE_RE)) {
151
- const abs = pathMod.resolve(pathMod.dirname(file), spec);
152
- queue.push(/\.[a-z]+$/i.test(abs) ? abs : `${abs}.js`);
153
- }
154
- }
155
- return [...found].sort();
156
- }
157
-
158
- /**
159
- * Which of those paths are not on disk. PURE. Returns repo-relative paths.
160
- */
161
- function missingPaths(paths, { exists, root = REPO_ROOT } = {}) {
162
- const has = exists || ((rel) => fsMod.existsSync(pathMod.join(root, rel)));
163
- return paths.filter((rel) => !has(rel));
164
- }
165
-
166
- // The distinct top-level module directories a set of missing paths falls under —
167
- // what the builder actually has to get back, and what box-sync speaks in.
168
- function missingModuleDirs(paths) {
169
- const dirs = new Set();
170
- for (const p of paths) {
171
- const m = String(p).match(/^modules\/([^/]+)\//);
172
- if (m) dirs.add(`modules/${m[1]}`);
173
- }
174
- return [...dirs].sort();
175
- }
176
-
177
- /**
178
- * ONE actionable message, or null when the tree is complete. PURE.
179
- *
180
- * Names the CONDITION (a scoped checkout, not a broken install), EVERY missing
181
- * requirement, and BOTH remedies — re-scope, or take the whole tree. Two are given
182
- * because they solve different problems: box-sync re-reads the server's path list
183
- * and is right when the claim moved, while sparse-checkout disable is the escape
184
- * hatch when the scope itself is what is wrong.
185
- *
186
- * It does not promise which one applies, because that is exactly what this layer
187
- * cannot know — it can see that the code is absent, not why.
188
- */
189
- function preflightMessage(missing, { command = 'this command' } = {}) {
190
- if (!missing || !missing.length) return null;
191
- const dirs = missingModuleDirs(missing);
192
- const lines = [
193
- `${command} needs code this checkout does not contain.`,
194
- '',
195
- `Missing (${missing.length}):`,
196
- ...missing.map((p) => ` - ${p}`),
197
- '',
198
- 'This is normal on a TASK-SCOPED dev box: the box carries scripts/, src/bongos/,',
199
- 'tests/ and the module your claimed task is scoped to — not every module. It is',
200
- 'not a broken install, and re-running will not fix it.',
201
- '',
202
- 'Fix it with ONE of:',
203
- ' node scripts/gds/box-sync.js re-scope this box to what you hold now',
204
- ' git sparse-checkout disable take the whole tree instead',
205
- ];
206
- if (dirs.length) {
207
- lines.splice(lines.indexOf('') + 1, 0, `Whole directories absent: ${dirs.join(', ')}`);
208
- }
209
- return lines.join('\n');
210
- }
211
-
212
- /**
213
- * The one call an entry point makes. Returns null when the tree is complete, or the
214
- * message to print before exiting non-zero.
215
- *
216
- * Every dependency is injectable so the test drives it with a fake file system —
217
- * and so a failure inside the preflight can never be the reason a command dies.
218
- */
219
- function checkTree(entryFile, { readFile, exists, root = REPO_ROOT, command } = {}) {
220
- try {
221
- const required = requiredModulePaths(entryFile, { readFile, root });
222
- const missing = missingPaths(required, { exists, root });
223
- return preflightMessage(missing, { command: command || pathMod.basename(entryFile) });
224
- } catch (_) {
225
- // A preflight that throws is a preflight that replaced a clear stack trace with
226
- // a worse one. Silence here means the command proceeds and fails the old way,
227
- // which is the outcome this file improves on — never the one it causes.
228
- return null;
229
- }
230
- }
231
-
232
- module.exports = {
233
- REPO_ROOT,
234
- requiredModulePaths,
235
- missingPaths,
236
- missingModuleDirs,
237
- preflightMessage,
238
- checkTree,
239
- stripComments,
240
- insideTry,
241
- };
@@ -1,172 +0,0 @@
1
- // src/bongos/app-pair.js — in-memory pairing store for the Dev Box desktop app
2
- // (#904, ADR 0045).
3
- //
4
- // The desktop app needs a browser-clicks-only sign-in (Lars's UI-first rule:
5
- // no terminal, no paste, no typing codes). GitHub's device flow makes the
6
- // builder hand-type an 8-char code; the web OAuth flow can't hand a token to a
7
- // local process (safeReturnTo deliberately rejects loopback targets). So the
8
- // app pairs against the GDS itself:
9
- //
10
- // 1. app → POST /auth/app-pair/start → { pair_code, poll_token }
11
- // 2. app → opens the browser at /builders/pair?code=<pair_code>
12
- // 3. human → already signed into the hall (or signs in via the normal web
13
- // flow) and clicks ONE Approve button
14
- // 4. server (at approve, cookie-gated like /auth/cli-token/issue) mints a
15
- // source='cli' session and parks it on the pairing entry
16
- // 5. app → POST /auth/app-pair/poll { poll_token } → { token } (ONCE)
17
- //
18
- // Two credentials with distinct trust:
19
- // - pair_code — short, human-visible, shown in the browser URL. It can only
20
- // APPROVE (and only from an authenticated browser cookie session); it can
21
- // never read the minted token.
22
- // - poll_token — long, secret, never leaves the app↔server channel. It is
23
- // the ONLY credential that can collect the minted session token, and the
24
- // token is handed over exactly once, then the entry is destroyed.
25
- //
26
- // Storage is an in-memory Map, same posture as the GitHub device-flow's
27
- // pending map in src/bongos/auth.js — single Node process (ADR 0006), short TTL,
28
- // a restart simply voids in-flight pairings and the app retries. No migration,
29
- // no new table, nothing durable to leak.
30
-
31
- const crypto = require('node:crypto');
32
-
33
- // Unambiguous code alphabet (Crockford-ish: no 0/O, 1/I/L, U/V confusion).
34
- const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTWXYZ23456789';
35
- const CODE_GROUPS = 2; // XXXX-XXXX
36
- const CODE_GROUP_LEN = 4;
37
-
38
- const DEFAULT_TTL_MS = 10 * 60 * 1000; // mirrors the OAuth state cookies
39
- const DEFAULT_MAX_PENDING = 500; // memory cap; start() refuses beyond
40
- const POLL_INTERVAL_S = 5; // suggested client poll cadence
41
-
42
- function randomPairCode() {
43
- const groups = [];
44
- for (let g = 0; g < CODE_GROUPS; g++) {
45
- let s = '';
46
- for (let i = 0; i < CODE_GROUP_LEN; i++) {
47
- s += CODE_ALPHABET[crypto.randomInt(CODE_ALPHABET.length)];
48
- }
49
- groups.push(s);
50
- }
51
- return groups.join('-');
52
- }
53
-
54
- // Normalize what a human/browser hands back: trim, uppercase, drop spaces,
55
- // tolerate a missing dash.
56
- function normalizeCode(raw) {
57
- if (typeof raw !== 'string') return null;
58
- const s = raw.trim().toUpperCase().replace(/[\s-]+/g, '');
59
- if (s.length !== CODE_GROUPS * CODE_GROUP_LEN) return null;
60
- if (![...s].every((c) => CODE_ALPHABET.includes(c))) return null;
61
- return `${s.slice(0, CODE_GROUP_LEN)}-${s.slice(CODE_GROUP_LEN)}`;
62
- }
63
-
64
- // createPairStore({ nowMs, ttlMs, maxPending }) — nowMs injectable for tests.
65
- function createPairStore(opts = {}) {
66
- const nowMs = opts.nowMs || (() => Date.now());
67
- const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
68
- const maxPending = opts.maxPending ?? DEFAULT_MAX_PENDING;
69
-
70
- const byCode = new Map(); // pair_code → entry
71
- const byPoll = new Map(); // poll_token → entry
72
-
73
- function destroy(entry) {
74
- byCode.delete(entry.pairCode);
75
- byPoll.delete(entry.pollToken);
76
- }
77
-
78
- function expired(entry) {
79
- return nowMs() >= entry.expiresAt;
80
- }
81
-
82
- // Lazy purge — called on every public op so the maps never accumulate.
83
- function purge() {
84
- for (const entry of byCode.values()) {
85
- if (expired(entry)) destroy(entry);
86
- }
87
- }
88
-
89
- return {
90
- // → { pairCode, pollToken, expiresIn, interval } | { error: 'busy' }
91
- start({ deviceName, platform } = {}) {
92
- purge();
93
- if (byCode.size >= maxPending) return { error: 'busy' };
94
- let pairCode = randomPairCode();
95
- while (byCode.has(pairCode)) pairCode = randomPairCode(); // vanishing odds
96
- const entry = {
97
- pairCode,
98
- pollToken: crypto.randomBytes(32).toString('hex'),
99
- deviceName: String(deviceName || 'Dev Box app').slice(0, 80),
100
- platform: String(platform || 'unknown').slice(0, 20),
101
- createdAt: nowMs(),
102
- expiresAt: nowMs() + ttlMs,
103
- status: 'pending', // pending → approved (→ delivered = gone)
104
- grant: null, // set at approve: { token, builder, expires_at }
105
- };
106
- byCode.set(entry.pairCode, entry);
107
- byPoll.set(entry.pollToken, entry);
108
- return {
109
- pairCode: entry.pairCode,
110
- pollToken: entry.pollToken,
111
- expiresIn: Math.floor(ttlMs / 1000),
112
- interval: POLL_INTERVAL_S,
113
- };
114
- },
115
-
116
- // What the approve page shows BEFORE the human clicks. null = unknown/expired
117
- // (indistinguishable on purpose — no oracle for code guessing).
118
- info(rawCode) {
119
- purge();
120
- const code = normalizeCode(rawCode);
121
- const entry = code ? byCode.get(code) : null;
122
- if (!entry || expired(entry)) return null;
123
- return {
124
- pairCode: entry.pairCode,
125
- deviceName: entry.deviceName,
126
- platform: entry.platform,
127
- createdAt: entry.createdAt,
128
- status: entry.status,
129
- };
130
- },
131
-
132
- // Park the minted grant on a pending entry. The route mints the session
133
- // (it holds req.builder); the store only records the handover.
134
- // → { ok: true } | { error: 'not_found' | 'already_approved' }
135
- approve(rawCode, grant) {
136
- purge();
137
- const code = normalizeCode(rawCode);
138
- const entry = code ? byCode.get(code) : null;
139
- if (!entry || expired(entry)) return { error: 'not_found' };
140
- if (entry.status !== 'pending') return { error: 'already_approved' };
141
- entry.status = 'approved';
142
- entry.grant = grant;
143
- return { ok: true };
144
- },
145
-
146
- // → { status: 'pending', interval }
147
- // | { status: 'approved', grant } (exactly once; entry destroyed)
148
- // | { status: 'expired' } (also for unknown poll tokens — no oracle)
149
- poll(pollToken) {
150
- purge();
151
- const entry = typeof pollToken === 'string' ? byPoll.get(pollToken) : null;
152
- if (!entry || expired(entry)) {
153
- if (entry) destroy(entry);
154
- return { status: 'expired' };
155
- }
156
- if (entry.status === 'approved') {
157
- const grant = entry.grant;
158
- destroy(entry); // one-shot delivery
159
- return { status: 'approved', grant };
160
- }
161
- return { status: 'pending', interval: POLL_INTERVAL_S };
162
- },
163
-
164
- // test/ops introspection only
165
- _size() { return byCode.size; },
166
- };
167
- }
168
-
169
- // Singleton the routes use. Tests build their own stores with injected clocks.
170
- const pairStore = createPairStore();
171
-
172
- module.exports = { createPairStore, normalizeCode, pairStore };