@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,114 +0,0 @@
1
- // tests/box_auth_check.mjs
2
- //
3
- // Unit tests for the dev-box session-start auth gate (task 1712). The pure core
4
- // `decideBoxAuth` is the high-risk surface: it decides whether a box session is
5
- // told to STOP and re-auth before it can safely claim/ship. The truth table
6
- // must be exactly right — box-scoped (source='box', ADR 0053) is the incident
7
- // case and must be caught from the local file alone (no network), an expired
8
- // real token must still be caught via the bounded /me probe, and any inability
9
- // to verify must FAIL OPEN (silent) rather than cry wolf.
10
- //
11
- // Importing the helper must NOT run its main() (which reads the session file and
12
- // hits the network) — box-auth-check.js guards main() behind
13
- // `require.main === module`, and only require()s cli-lib inside main(), so this
14
- // import is side-effect free (no I/O, no fetch). This test proves that holds.
15
- //
16
- // Run: node tests/box_auth_check.mjs
17
-
18
- import { strict as assert } from 'node:assert';
19
- import { createRequire } from 'node:module';
20
-
21
- const require = createRequire(import.meta.url);
22
- const { decideBoxAuth } = require('../scripts/gds/box-auth-check.js');
23
-
24
- let passed = 0;
25
- let failed = 0;
26
- function test(name, fn) {
27
- try {
28
- fn();
29
- passed++;
30
- console.log(` ok ${name}`);
31
- } catch (err) {
32
- failed++;
33
- console.log(` FAIL ${name}`);
34
- console.log(` ${err.message}`);
35
- }
36
- }
37
-
38
- // A real, validated session on a box — the common clear-to-work case.
39
- const OK = { onBox: true, hasToken: true, isBoxScoped: false, meStatus: 200 };
40
-
41
- // ---------- skip: off a box, this check does nothing ----------
42
-
43
- test('not a box → skip (laptops are untouched, whatever the token looks like)', () => {
44
- assert.equal(decideBoxAuth({ onBox: false, hasToken: false, isBoxScoped: false, meStatus: null }).action, 'skip');
45
- assert.equal(decideBoxAuth({ onBox: false, hasToken: true, isBoxScoped: true, meStatus: 403 }).action, 'skip');
46
- });
47
-
48
- // ---------- setup: on a box with no session ----------
49
-
50
- test('box, no session → setup', () => {
51
- const d = decideBoxAuth({ onBox: true, hasToken: false, isBoxScoped: false, meStatus: null });
52
- assert.equal(d.action, 'setup');
53
- assert.equal(d.reason, 'no-session');
54
- });
55
-
56
- // ---------- reauth: the box-scoped incident case (decided locally, no network) ----------
57
-
58
- test('box, box-scoped token → reauth (box-scoped), regardless of meStatus', () => {
59
- const d = decideBoxAuth({ onBox: true, hasToken: true, isBoxScoped: true, meStatus: null });
60
- assert.equal(d.action, 'reauth');
61
- assert.equal(d.reason, 'box-scoped');
62
- });
63
-
64
- test('box-scoped is decided BEFORE meStatus — even a 200 stays reauth', () => {
65
- // A box token can never legitimately 200 on /me, but the ordering guarantee is
66
- // what keeps the incident case off the network path.
67
- const d = decideBoxAuth({ onBox: true, hasToken: true, isBoxScoped: true, meStatus: 200 });
68
- assert.equal(d.action, 'reauth');
69
- assert.equal(d.reason, 'box-scoped');
70
- });
71
-
72
- // ---------- reauth: an expired/revoked real token (caught via the bounded /me) ----------
73
-
74
- test('box, real token, /me 401 → reauth (expired-or-revoked)', () => {
75
- const d = decideBoxAuth({ ...OK, meStatus: 401 });
76
- assert.equal(d.action, 'reauth');
77
- assert.equal(d.reason, 'expired-or-revoked');
78
- });
79
-
80
- test('box, real token, /me 403 → reauth (expired-or-revoked)', () => {
81
- const d = decideBoxAuth({ ...OK, meStatus: 403 });
82
- assert.equal(d.action, 'reauth');
83
- assert.equal(d.reason, 'expired-or-revoked');
84
- });
85
-
86
- // ---------- ok: a real, validated session ----------
87
-
88
- test('box, real token, /me 200 → ok (clear to work)', () => {
89
- const d = decideBoxAuth(OK);
90
- assert.equal(d.action, 'ok');
91
- assert.equal(d.reason, 'validated');
92
- });
93
-
94
- // ---------- fail-open: couldn't verify → stay silent, never warn falsely ----------
95
-
96
- test('box, real token, /me not attempted (null) → skip (fail-open, no false alarm)', () => {
97
- const d = decideBoxAuth({ ...OK, meStatus: null });
98
- assert.equal(d.action, 'skip');
99
- assert.equal(d.reason, 'unverified');
100
- });
101
-
102
- test('box, real token, /me 500 (server error) → skip (not a definitive auth failure)', () => {
103
- const d = decideBoxAuth({ ...OK, meStatus: 500 });
104
- assert.equal(d.action, 'skip');
105
- assert.equal(d.reason, 'unverified');
106
- });
107
-
108
- test('box, real token, /me 502 (gateway) → skip (fail-open)', () => {
109
- const d = decideBoxAuth({ ...OK, meStatus: 502 });
110
- assert.equal(d.action, 'skip');
111
- });
112
-
113
- console.log(`\n${passed} passed, ${failed} failed`);
114
- process.exit(failed === 0 ? 0 : 1);
@@ -1,129 +0,0 @@
1
- // tests/box_code_staleness.mjs
2
- //
3
- // Unit smoke for boxes.computeCodeStaleness (idea 332 / task 1316, ADR 0072) —
4
- // the pure "is this dev box running stale code?" decision behind the hall's
5
- // "rebuild to update" flag. No DB, no network.
6
- //
7
- // Run: node tests/box_code_staleness.mjs
8
-
9
- import { strict as assert } from 'node:assert';
10
- import { createRequire } from 'node:module';
11
-
12
- const require = createRequire(import.meta.url);
13
- const boxes = require('../modules/dev-box/boxes.js');
14
-
15
- let passed = 0;
16
- let failed = 0;
17
- function t(name, fn) {
18
- try { fn(); passed++; console.log(` PASS ${name}`); }
19
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
20
- }
21
-
22
- const NOW = Date.UTC(2026, 5, 20, 12, 0, 0); // fixed "now" so cases are deterministic
23
- const iso = (ms) => new Date(ms).toISOString();
24
- const HOUR = 3600 * 1000;
25
- const DAY = 24 * HOUR;
26
-
27
- console.log('computeCodeStaleness:');
28
-
29
- t('a parked/destroyed box is never judged (not_active)', () => {
30
- for (const state of ['parked', 'destroyed', 'provisioning', 'none']) {
31
- const r = boxes.computeCodeStaleness({ state, now: NOW });
32
- assert.equal(r.stale, false, state);
33
- assert.equal(r.reason, 'not_active', state);
34
- }
35
- });
36
-
37
- t('active + never reported + just provisioned → not stale (awaiting first report)', () => {
38
- const r = boxes.computeCodeStaleness({
39
- state: 'active', provisionedAt: iso(NOW - 5 * 60 * 1000), codeReportedAt: null, now: NOW,
40
- });
41
- assert.equal(r.stale, false);
42
- assert.equal(r.reason, 'awaiting_first_report');
43
- });
44
-
45
- t('active + never reported + past the provision grace → NOT stale (never_reported is an unknown, task 1002698)', () => {
46
- const r = boxes.computeCodeStaleness({
47
- state: 'active', provisionedAt: iso(NOW - 13 * DAY), codeReportedAt: null, now: NOW,
48
- });
49
- assert.equal(r.stale, false);
50
- assert.equal(r.reason, 'never_reported');
51
- });
52
-
53
- // Regression (task 1002698): the whole fleet reported never_reported because the
54
- // reporter ADR 0072 specified was never written (task 1002699). Silence must never
55
- // be enough on its own to tell a builder to destroy a working box.
56
- t('silence alone never flags a box — a report is required to call it stale', () => {
57
- for (const age of [31 * 60 * 1000, 13 * DAY, 400 * DAY]) {
58
- const r = boxes.computeCodeStaleness({
59
- state: 'active', provisionedAt: iso(NOW - age), codeReportedAt: null,
60
- serverCommittedAt: iso(NOW - HOUR), now: NOW,
61
- });
62
- assert.equal(r.stale, false, `age=${age}`);
63
- }
64
- });
65
-
66
- t('active + reported a commit within the lag window → current', () => {
67
- const r = boxes.computeCodeStaleness({
68
- state: 'active',
69
- provisionedAt: iso(NOW - 30 * DAY),
70
- codeReportedAt: iso(NOW - 5 * 60 * 1000),
71
- codeCommittedAt: iso(NOW - 3 * HOUR), // box code is 3h behind deployed
72
- serverCommittedAt: iso(NOW - 1 * HOUR), // deployed commit is 1h old
73
- now: NOW,
74
- });
75
- assert.equal(r.stale, false);
76
- assert.equal(r.reason, 'current');
77
- });
78
-
79
- t('active + reported a commit > 2 days behind deployed → STALE (behind)', () => {
80
- const r = boxes.computeCodeStaleness({
81
- state: 'active',
82
- provisionedAt: iso(NOW - 30 * DAY),
83
- codeReportedAt: iso(NOW - 5 * 60 * 1000),
84
- codeCommittedAt: iso(NOW - 9 * DAY), // box stuck 9 days behind (pull not ff)
85
- serverCommittedAt: iso(NOW - 1 * HOUR),
86
- now: NOW,
87
- });
88
- assert.equal(r.stale, true);
89
- assert.equal(r.reason, 'behind');
90
- assert.ok(r.lag_ms > 2 * DAY);
91
- });
92
-
93
- t('reported but no commit dates to compare → not stale (unknown, never false-alarm)', () => {
94
- const r = boxes.computeCodeStaleness({
95
- state: 'active', provisionedAt: iso(NOW - 30 * DAY),
96
- codeReportedAt: iso(NOW - 60 * 1000), codeCommittedAt: null, serverCommittedAt: null, now: NOW,
97
- });
98
- assert.equal(r.stale, false);
99
- assert.equal(r.reason, 'unknown');
100
- });
101
-
102
- t('accepts Date objects as well as ISO strings', () => {
103
- const r = boxes.computeCodeStaleness({
104
- state: 'active',
105
- provisionedAt: new Date(NOW - 30 * DAY),
106
- codeReportedAt: new Date(NOW - 60 * 1000),
107
- codeCommittedAt: new Date(NOW - 10 * DAY),
108
- serverCommittedAt: new Date(NOW - HOUR),
109
- now: NOW,
110
- });
111
- assert.equal(r.stale, true);
112
- assert.equal(r.reason, 'behind');
113
- });
114
-
115
- t('exactly at the 2-day lag boundary is NOT stale (strictly greater)', () => {
116
- const r = boxes.computeCodeStaleness({
117
- state: 'active',
118
- provisionedAt: iso(NOW - 30 * DAY),
119
- codeReportedAt: iso(NOW - 60 * 1000),
120
- codeCommittedAt: iso(NOW - (2 * DAY + HOUR)),
121
- serverCommittedAt: iso(NOW - HOUR), // lag exactly 2 days
122
- now: NOW,
123
- });
124
- assert.equal(r.stale, false);
125
- assert.equal(r.reason, 'current');
126
- });
127
-
128
- console.log(`\n${passed} passed, ${failed} failed`);
129
- process.exit(failed ? 1 : 0);
@@ -1,196 +0,0 @@
1
- // tests/box_connect_e2e.mjs
2
- //
3
- // END-TO-END smoke for the builder bootstrap (#685, ADR 0031 §1): runs the REAL
4
- // infra/box-connect.sh against a local stub server and asserts the whole flow.
5
- // NO production, NO Postgres, NO GitHub, NO real box — but the genuine shell
6
- // script, the genuine box router handlers (POST /box/ssh-key, GET
7
- // /box/managed-settings), and a stubbed device-flow.
8
- //
9
- // What it proves a real builder's `curl … | bash` would do:
10
- // 1. generates an SSH key at OTB_SSH_KEY (private half stays local),
11
- // 2. device-flow signs in and registers the PUBLIC key — and the fingerprint
12
- // the server computes matches the key on disk,
13
- // 3. downloads + installs managed-settings.json at the install dir,
14
- // 4. exits 0.
15
- //
16
- // Hermetic + deterministic (the device-flow poll returns 'ok' immediately; a
17
- // PATH shim no-ops `open`/`xdg-open` so no browser pops). Requires bash, curl,
18
- // ssh-keygen — present on macOS and the CI ubuntu runner.
19
- //
20
- // Run: node tests/box_connect_e2e.mjs
21
-
22
- import { strict as assert } from 'node:assert';
23
- import { createRequire } from 'node:module';
24
- import http from 'node:http';
25
- import fs from 'node:fs';
26
- import os from 'node:os';
27
- import path from 'node:path';
28
- import { spawn } from 'node:child_process';
29
-
30
- const require = createRequire(import.meta.url);
31
- const express = require('express');
32
- const auth = require('../src/bongos/auth.js');
33
- const o = require('../modules/dev-box/box-onboard.js');
34
- const gdsDb = require('../src/bongos/db.js');
35
- const boxes = require('../modules/dev-box/boxes.js');
36
- const boxAccess = require('../modules/dev-box/box-access.js');
37
-
38
- // Async spawn helper: run a real shell script against the in-process stub server
39
- // without blocking the event loop (spawnSync would deadlock the HTTP server).
40
- function runScript(scriptArgs, extraEnv) {
41
- return new Promise((resolve) => {
42
- const child = spawn('bash', scriptArgs, { env: { ...process.env, ...extraEnv } });
43
- let stdout = '';
44
- let stderr = '';
45
- child.stdout.on('data', (d) => { stdout += d; });
46
- child.stderr.on('data', (d) => { stderr += d; });
47
- const killer = setTimeout(() => child.kill('SIGKILL'), 60000);
48
- child.on('close', (status) => { clearTimeout(killer); resolve({ status, stdout, stderr }); });
49
- });
50
- }
51
-
52
- let passed = 0;
53
- let failed = 0;
54
- function t(name, fn) {
55
- try { fn(); passed++; console.log(` PASS ${name}`); }
56
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
57
- }
58
-
59
- // ── stub the trust boundary + the DB write the connect flow hits ────────────
60
- let registered = null;
61
- let CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' }; // flipped to test the clawback
62
- auth.requireBuilder = (req, _res, next) => { req.builder = { id: 1, github_login: 'e2etester', rank: 'metic' }; next(); };
63
- auth.requireRank = () => (_req, _res, next) => next();
64
- o.registerSshKey = async (_db, _id, k) => { registered = k; return { id: 1, key_type: k.keyType, fingerprint: k.fingerprint, label: k.label }; };
65
- // Stubs for the GET /box/authorized-keys path (box-side sync test below).
66
- gdsDb.getBuilderById = async () => CURRENT_DB_BUILDER;
67
- boxes.getBoxByBuilderId = async () => ({ id: 1, builder_id: 1, state: 'active', scope: 'full' });
68
- boxAccess.recordSourceEvent = async () => {};
69
- o.listSshKeys = async () => ([{ public_key: 'ssh-ed25519 BUILDERKEYAAA', label: 'laptop' }]);
70
- o.touchSshKeysUsed = async () => {};
71
-
72
- const buildBoxRouter = require('../modules/dev-box/routes/box.js');
73
- const app = express();
74
- app.use(express.json());
75
- // Stub device-flow: start hands back a code, poll immediately approves.
76
- app.post('/api/gds/auth/device/start', (_req, res) =>
77
- res.json({ device_code: 'dc-e2e', user_code: 'WXYZ-1234', verification_uri: 'http://127.0.0.1:9/device', interval: 1, expires_in: 30 }));
78
- app.post('/api/gds/auth/device/poll', (_req, res) => res.json({ state: 'ok', token: 'e2e-token' }));
79
- app.use('/api/gds', buildBoxRouter());
80
-
81
- const server = http.createServer(app);
82
- await new Promise((r) => server.listen(0, r));
83
- const port = server.address().port;
84
-
85
- // ── run the REAL bootstrap with test overrides ──────────────────────────────
86
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-e2e-'));
87
- const keyPath = path.join(tmp, 'otb_builder');
88
- const settingsDir = path.join(tmp, 'claude');
89
- // PATH shim so the script's best-effort `open <url>` doesn't launch a browser.
90
- const shimDir = path.join(tmp, 'bin');
91
- fs.mkdirSync(shimDir);
92
- for (const cmd of ['open', 'xdg-open']) {
93
- fs.writeFileSync(path.join(shimDir, cmd), '#!/bin/sh\nexit 0\n', { mode: 0o755 });
94
- }
95
-
96
- // Async spawn (NOT spawnSync): the stub server runs in THIS process's event
97
- // loop, so a synchronous spawn would block it and the child's HTTP calls would
98
- // hang. spawn + await-on-close keeps the loop free to answer the bootstrap.
99
- const run = await new Promise((resolve) => {
100
- const child = spawn('bash', ['infra/box-connect.sh'], {
101
- env: {
102
- ...process.env,
103
- PATH: `${shimDir}:${process.env.PATH}`,
104
- GDS_API_BASE: `http://127.0.0.1:${port}`,
105
- OTB_SSH_KEY: keyPath,
106
- OTB_MANAGED_SETTINGS_DIR: settingsDir,
107
- },
108
- });
109
- let stdout = '';
110
- let stderr = '';
111
- child.stdout.on('data', (d) => { stdout += d; });
112
- child.stderr.on('data', (d) => { stderr += d; });
113
- const killer = setTimeout(() => child.kill('SIGKILL'), 60000);
114
- child.on('close', (status) => { clearTimeout(killer); resolve({ status, stdout, stderr }); });
115
- });
116
-
117
- console.log('box-connect.sh end-to-end (real script, stub server):');
118
-
119
- t('exits 0', () => {
120
- assert.equal(run.status, 0, `non-zero exit. stdout:\n${run.stdout}\nstderr:\n${run.stderr}`);
121
- });
122
-
123
- t('generated an SSH keypair at OTB_SSH_KEY (private + public)', () => {
124
- assert.ok(fs.existsSync(keyPath), 'private key missing');
125
- assert.ok(fs.existsSync(`${keyPath}.pub`), 'public key missing');
126
- });
127
-
128
- t('registered the public key, and the server-computed fingerprint matches the key on disk', () => {
129
- assert.ok(registered, 'registerSshKey was never called');
130
- const pub = fs.readFileSync(`${keyPath}.pub`, 'utf8');
131
- const expected = o.validateSshPublicKey(pub);
132
- assert.equal(registered.fingerprint, expected.fingerprint, 'fingerprint mismatch');
133
- // The script trims the comment before sending; stored normalized = "<type> <base64>".
134
- assert.equal(registered.publicKey, expected.normalized);
135
- });
136
-
137
- t('installed managed-settings.json at the override dir, keyed off the session login', () => {
138
- const f = path.join(settingsDir, 'managed-settings.json');
139
- assert.ok(fs.existsSync(f), 'managed-settings.json not written');
140
- const s = JSON.parse(fs.readFileSync(f, 'utf8'));
141
- assert.equal(s.sshConfigs[0].sshHost, 'root@box-e2etester.dev.example.com');
142
- assert.deepEqual(s.sshHostAllowlist, ['*.dev.example.com']);
143
- });
144
-
145
- t('did NOT leak the GDS token into stdout/stderr', () => {
146
- assert.ok(!`${run.stdout}${run.stderr}`.includes('e2e-token'), 'token leaked to output');
147
- });
148
-
149
- // ── box-side: box-authorized-keys-fetch.sh against the real route ───────────
150
- console.log('\nbox-authorized-keys-fetch.sh end-to-end (real script, stub server):');
151
-
152
- const sessionFile = path.join(tmp, 'gds-session.json');
153
- fs.writeFileSync(sessionFile, JSON.stringify({ token: 'e2e-token' }));
154
- const akFile = path.join(tmp, 'authorized_keys');
155
- const OPERATOR_LINE = 'ssh-ed25519 OPERATORKEYZZZ operator@control-plane';
156
- fs.writeFileSync(akFile, OPERATOR_LINE + '\n'); // pre-seed operator key (must survive)
157
-
158
- const akEnv = {
159
- GDS_BASE: `http://127.0.0.1:${port}/api/gds`,
160
- GDS_SESSION: sessionFile,
161
- AUTHORIZED_KEYS: akFile,
162
- };
163
-
164
- // Scenario A — active builder: managed block synced, operator key preserved.
165
- CURRENT_DB_BUILDER = { rank: 'metic', status: 'active' };
166
- const sync = await runScript(['infra/box-authorized-keys-fetch.sh'], akEnv);
167
-
168
- t('active builder: fetch exits 0', () => {
169
- assert.equal(sync.status, 0, `stdout:\n${sync.stdout}\nstderr:\n${sync.stderr}`);
170
- });
171
- t('active builder: managed block holds the builder key AND the operator key survives', () => {
172
- const ak = fs.readFileSync(akFile, 'utf8');
173
- assert.ok(ak.includes('BUILDERKEYAAA'), 'builder key not synced');
174
- assert.ok(ak.includes(OPERATOR_LINE), 'operator key clobbered');
175
- assert.ok(ak.includes(o.MANAGED_BLOCK_BEGIN) && ak.includes(o.MANAGED_BLOCK_END), 'markers missing');
176
- });
177
-
178
- // Scenario B — deactivated builder: 403 clawback empties the block, operator stays.
179
- CURRENT_DB_BUILDER = { rank: 'metic', status: 'inactive' };
180
- const claw = await runScript(['infra/box-authorized-keys-fetch.sh'], akEnv);
181
-
182
- t('deactivated builder: fetch exits 3 (revoked)', () => {
183
- assert.equal(claw.status, 3, `stdout:\n${claw.stdout}\nstderr:\n${claw.stderr}`);
184
- });
185
- t('deactivated builder: managed block emptied (login cut), operator key STILL present', () => {
186
- const ak = fs.readFileSync(akFile, 'utf8');
187
- assert.ok(!ak.includes('BUILDERKEYAAA'), 'builder key NOT revoked — still in authorized_keys!');
188
- assert.ok(ak.includes(OPERATOR_LINE), 'operator key clobbered by the clawback');
189
- assert.ok(ak.includes(o.MANAGED_BLOCK_BEGIN), 'managed block markers should remain (empty)');
190
- });
191
-
192
- await new Promise((r) => server.close(r));
193
- fs.rmSync(tmp, { recursive: true, force: true });
194
-
195
- console.log(`\nbox_connect_e2e.mjs: ${passed} passed, ${failed} failed`);
196
- process.exit(failed === 0 ? 0 : 1);
@@ -1,70 +0,0 @@
1
- // tests/box_cost.mjs — pure-function coverage for the container cost ledger
2
- // (#602): storageCostUsd (parked-interval cost) + summarizeBoxCost (bucket
3
- // cost_log rows into spinup/compute/storage/total). The DB writers
4
- // (recordSpinupCost/recordStorageCost) and the per-builder summary queries take a
5
- // `db` param and are exercised by the integration smoke, not here. No DB, no clock.
6
- //
7
- // Run: node tests/box_cost.mjs (also runs in the unit gate)
8
-
9
- import { strict as assert } from 'node:assert';
10
- import { createRequire } from 'node:module';
11
-
12
- const require = createRequire(import.meta.url);
13
- const boxes = require('../modules/dev-box/boxes.js');
14
-
15
- let passed = 0, failed = 0;
16
- const t = (n, f) => { try { f(); passed++; console.log(` PASS ${n}`); } catch (e) { failed++; console.log(` FAIL ${n}\n ${e.message}`); } };
17
-
18
- t('storageCostUsd: hours parked × snapshot hourly rate (monthly/730)', () => {
19
- const hourly = boxes.SNAPSHOT_MONTHLY_USD / 730;
20
- // 730h parked ≈ one month
21
- const month = boxes.storageCostUsd({ parkedSinceMs: 0, untilMs: 730 * 3_600_000 });
22
- assert.ok(Math.abs(month - boxes.SNAPSHOT_MONTHLY_USD) < 0.001, `~monthly: got ${month}`);
23
- // 10h parked
24
- const ten = boxes.storageCostUsd({ parkedSinceMs: 0, untilMs: 10 * 3_600_000 });
25
- assert.ok(Math.abs(ten - hourly * 10) < 0.0001, `10h: got ${ten}`);
26
- });
27
-
28
- t('storageCostUsd: zero/negative/invalid interval → 0', () => {
29
- assert.equal(boxes.storageCostUsd({ parkedSinceMs: 100, untilMs: 100 }), 0);
30
- assert.equal(boxes.storageCostUsd({ parkedSinceMs: 200, untilMs: 100 }), 0);
31
- assert.equal(boxes.storageCostUsd({}), 0);
32
- assert.equal(boxes.storageCostUsd({ parkedSinceMs: NaN, untilMs: 1 }), 0);
33
- });
34
-
35
- t('summarizeBoxCost: buckets by source into spinup/compute/storage + total', () => {
36
- const rows = [
37
- { source: 'box-spinup', amount_usd: 0 },
38
- { source: 'box-spinup', amount_usd: 0.02 },
39
- { source: 'box-lifecycle', amount_usd: 1.50 },
40
- { source: 'box-lifecycle', amount_usd: 0.25 },
41
- { source: 'box-storage', amount_usd: 0.30 },
42
- ];
43
- const s = boxes.summarizeBoxCost(rows);
44
- assert.equal(s.spinup_usd, 0.02);
45
- assert.equal(s.compute_usd, 1.75);
46
- assert.equal(s.storage_usd, 0.30);
47
- assert.equal(s.total_usd, 2.07);
48
- });
49
-
50
- t('summarizeBoxCost: empty / null → all zeros', () => {
51
- assert.deepEqual(boxes.summarizeBoxCost([]), { spinup_usd: 0, compute_usd: 0, storage_usd: 0, total_usd: 0 });
52
- assert.deepEqual(boxes.summarizeBoxCost(null), { spinup_usd: 0, compute_usd: 0, storage_usd: 0, total_usd: 0 });
53
- });
54
-
55
- t('summarizeBoxCost: an unknown source still counts toward total (no silent drop)', () => {
56
- const s = boxes.summarizeBoxCost([{ source: 'box-mystery', amount_usd: 0.5 }]);
57
- assert.equal(s.total_usd, 0.5);
58
- assert.equal(s.compute_usd, 0);
59
- });
60
-
61
- t('BOX_COST_SOURCES covers exactly the three box ledger sources', () => {
62
- assert.deepEqual([...boxes.BOX_COST_SOURCES].sort(), ['box-lifecycle', 'box-spinup', 'box-storage']);
63
- });
64
-
65
- t('SPINUP_COST_USD defaults to 0 (DO charges no provision fee; the row is an event marker)', () => {
66
- assert.equal(boxes.SPINUP_COST_USD, 0);
67
- });
68
-
69
- console.log(`\nbox_cost: ${passed} passed, ${failed} failed`);
70
- process.exit(failed ? 1 : 0);
@@ -1,125 +0,0 @@
1
- // tests/box_credential.mjs
2
- //
3
- // R102 (goal 1000051, ADR 0148 C4) — VERIFY the server-side-token posture holds
4
- // once source scope is CLAIM-based (R92–R94). The security property task-scoping
5
- // must not weaken: the GitHub credential is server-issued per fetch from the prod
6
- // ENV (never the repo — ADR 0022), the box never receives a fabricated token, and
7
- // the claim-scoped ACCESS DECISION carries NO secret — the token is minted
8
- // SEPARATELY, so changing what the box materializes (sparse paths) never touches
9
- // the token model.
10
- //
11
- // Pure + hermetic: makeEnvCredentialProvider takes an injected env; decideSourceAccess
12
- // is pure. NO env, NO network, NO box. The DEFERRED hardening (short-lived per-claim
13
- // credential + hard "can't over-pull" isolation) is documented in ADR 0148 C4 /
14
- // Consequences — this test pins the posture that ships TODAY.
15
- //
16
- // Run: node tests/box_credential.mjs
17
-
18
- import { strict as assert } from 'node:assert';
19
- import { createRequire } from 'node:module';
20
-
21
- const require = createRequire(import.meta.url);
22
- const cred = require('../modules/dev-box/box-credential.js');
23
- const access = require('../modules/dev-box/box-access.js');
24
- const scopeMap = require('../src/bongos/module-scope-map.js');
25
-
26
- let passed = 0;
27
- let failed = 0;
28
- function t(name, fn) {
29
- try { fn(); passed++; console.log(` PASS ${name}`); }
30
- catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
31
- }
32
-
33
- const REPO = 'https://github.com/acme/portable-core.git';
34
- const STARTER_REPO = 'https://github.com/acme/starter-surface.git';
35
-
36
- console.log('box-credential — the token is SERVER-ENV sourced, never fabricated (ADR 0022):');
37
-
38
- t('no env configured → configured:false + token:null (the box gets guidance, not a token)', () => {
39
- const p = cred.makeEnvCredentialProvider({});
40
- for (const scope of ['full', 'starter']) {
41
- const c = p.issue(scope, { nowMs: 0 });
42
- assert.equal(c.configured, false, `${scope}: unconfigured`);
43
- assert.equal(c.token, null, `${scope}: NO token is ever fabricated`);
44
- assert.equal(c.repoUrl, null, `${scope}: no repo without env`);
45
- assert.match(c.guidance || '', /env/i, `${scope}: guidance points at the prod env`);
46
- }
47
- });
48
-
49
- t('repo set but token unset → still configured:false (no partial/empty-token leak)', () => {
50
- const c = cred.makeEnvCredentialProvider({ BOX_SOURCE_REPO_URL: REPO }).issue('full', { nowMs: 0 });
51
- assert.equal(c.configured, false);
52
- assert.equal(c.token, null);
53
- });
54
-
55
- t('token comes STRAIGHT from the env var — and lands ONLY in .token, no other field', () => {
56
- const p = cred.makeEnvCredentialProvider({
57
- BOX_SOURCE_REPO_URL: REPO,
58
- BOX_SOURCE_FULL_TOKEN: 'FULL-SECRET-abc',
59
- BOX_SOURCE_STARTER_TOKEN: 'STARTER-SECRET-xyz',
60
- });
61
- const full = p.issue('full', { nowMs: 0 });
62
- assert.equal(full.token, 'FULL-SECRET-abc', 'full scope → the FULL env token, verbatim');
63
- const starter = p.issue('starter', { nowMs: 0 });
64
- assert.equal(starter.token, 'STARTER-SECRET-xyz', 'starter scope → the STARTER env token, verbatim');
65
- // The secret must appear in NO field other than `token` (scope/repoUrl/guidance/
66
- // expiresAt/ttl are safe to log + audit — see the source_grant audit).
67
- for (const c of [full, starter]) {
68
- for (const [k, v] of Object.entries(c)) {
69
- if (k === 'token') continue;
70
- assert.ok(!String(v).includes(c.token), `secret must not leak into .${k}`);
71
- }
72
- }
73
- });
74
-
75
- console.log('\nbox-credential — the credential SCOPE is rank-based; scope can never over-reach the repo:');
76
-
77
- t('a separate starter repo is a HARD boundary: only "full" reaches the full repo', () => {
78
- const p = cred.makeEnvCredentialProvider({
79
- BOX_SOURCE_REPO_URL: REPO,
80
- BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
81
- BOX_SOURCE_FULL_TOKEN: 'F',
82
- BOX_SOURCE_STARTER_TOKEN: 'S',
83
- });
84
- assert.equal(p.issue('full', { nowMs: 0 }).repoUrl, REPO, 'full → the full repo');
85
- const starter = p.issue('starter', { nowMs: 0 });
86
- assert.equal(starter.repoUrl, STARTER_REPO, 'starter → the SEPARATE starter repo, cannot reach the full one');
87
- assert.equal(starter.separateStarterRepo, true);
88
- });
89
-
90
- t('fail-closed: an UNEXPECTED scope label ("task"/"base") never escalates to the full repo', () => {
91
- // R94 passes only rank scopes ('starter'|'full') to issue(); this pins the safety
92
- // margin if a task/base label ever reached it — it takes the NON-full (starter)
93
- // branch, so on a separate-starter-repo instance it can't reach the crown jewels.
94
- const p = cred.makeEnvCredentialProvider({
95
- BOX_SOURCE_REPO_URL: REPO,
96
- BOX_SOURCE_STARTER_REPO_URL: STARTER_REPO,
97
- BOX_SOURCE_FULL_TOKEN: 'F',
98
- BOX_SOURCE_STARTER_TOKEN: 'S',
99
- });
100
- for (const label of ['task', 'base', 'wizard', undefined]) {
101
- const c = p.issue(label, { nowMs: 0 });
102
- assert.equal(c.repoUrl, STARTER_REPO, `scope='${label}' must NOT reach the full repo`);
103
- assert.notEqual(c.token, 'F', `scope='${label}' must NOT get the full-only token`);
104
- }
105
- });
106
-
107
- console.log('\ntask-scoping decoupling — the claim-scoped ACCESS DECISION carries NO secret:');
108
-
109
- t('decideSourceAccess returns scope/mode/sparsePaths only — the token is minted separately', () => {
110
- const d = access.decideSourceAccess({ rank: 'metic', status: 'active', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
111
- assert.equal(d.allowed, true);
112
- assert.deepEqual(Object.keys(d.spec).sort(), ['mode', 'scope', 'sparsePaths'], 'the spec is scope/mode/sparsePaths — NO credential field');
113
- const blob = JSON.stringify(d);
114
- assert.ok(!/token|credential|secret|ghp_|github_pat/i.test(blob), 'the decision blob holds no secret-shaped field');
115
- });
116
-
117
- t('revocation scrubs at the credential layer: an inactive/below-floor builder is denied BEFORE any credential', () => {
118
- const inactive = access.decideSourceAccess({ rank: 'metic', status: 'inactive', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
119
- assert.equal(inactive.allowed, false, 'inactive → denied (the route never reaches issue())');
120
- const belowFloor = access.decideSourceAccess({ rank: 'xenos', status: 'active', floor: 'metic', scopeKeys: ['dev-box'], moduleScopeMap: scopeMap });
121
- assert.equal(belowFloor.allowed, false, 'below floor → denied (no credential path)');
122
- });
123
-
124
- console.log(`\nbox_credential.mjs: ${passed} passed, ${failed} failed`);
125
- process.exit(failed === 0 ? 0 : 1);