@bongos/core 1.18.0 → 1.18.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +1740 -640
- package/.claude/skills/builder-box/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +97 -0
- package/.claude/skills/builder-ship/SKILL.md +8 -2
- package/.claude/skills/builder-start/SKILL.md +11 -0
- package/.claude/skills/builder-sync/SKILL.md +41 -0
- package/.claude/skills/fix-task/SKILL.md +64 -0
- package/.claude/skills/goal-create/SKILL.md +158 -0
- package/.claude/skills/goal-review/SKILL.md +3 -1
- package/.claude/skills/idea-triage/SKILL.md +14 -8
- package/.claude/skills/merge-mode/SKILL.md +2 -2
- package/.claude/skills/new-project/SKILL.md +1 -0
- package/.claude/skills/planning-session/SKILL.md +45 -18
- package/.env.local.example +4 -3
- package/README.md +5 -3
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +2 -2
- package/clients/bongos-client/bongos-client.global.js +82 -18
- package/clients/bongos-client/index.cjs +82 -18
- package/clients/bongos-client/index.d.ts +131 -29
- package/clients/bongos-client/index.mjs +82 -18
- package/config/branding.neutral.json +11 -2
- package/config/governance.neutral.json +21 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +24 -0
- package/docs/adr/0090-metic-task-authoring.md +1 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/0144-devbox-rehome-onto-cloudbongos-plane.md +41 -0
- package/docs/adr/0144-federated-single-logout-backchannel.md +42 -0
- package/docs/adr/0145-devbox-app-branding-driven-module.md +44 -0
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +62 -0
- package/docs/adr/0146-configurable-reward-mode-cost-plus-only.md +53 -0
- package/docs/adr/0147-standalone-live-docs-regen.md +39 -0
- package/docs/adr/0148-task-scoped-box-source-access.md +48 -0
- package/docs/adr/0149-bongos-dev-local-launcher.md +57 -0
- package/docs/adr/0150-box-first-boot-bringup-vendored-instances.md +68 -0
- package/docs/adr/0151-governance-permissions-as-atom-ranks-as-roles.md +229 -0
- package/docs/adr/0152-landing-gate-served-by-the-core-app.md +92 -0
- package/docs/adr/0152-metic-task-abandonment.md +67 -0
- package/docs/adr/0153-sequential-chain-runner.md +51 -0
- package/docs/adr/0154-goal-owner-criterion-authoring.md +86 -0
- package/docs/adr/README.md +15 -2
- package/docs/api/openapi.json +2648 -483
- package/docs/api-reference.md +73 -13
- package/docs/architecture.md +4 -2
- package/docs/branding-contract.md +3 -1
- package/docs/canonical-permissions.md +21 -8
- package/docs/file-map.md +15 -7
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +2 -2
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +3 -1
- package/docs/onboarding/diagrams/05-versioning.mmd +1 -3
- package/docs/onboarding/diagrams/assertions.json +4 -7
- package/docs/onboarding/drachmae-and-karma.md +3 -3
- package/docs/onboarding/primer.md +17 -16
- package/docs/onboarding/slash-commands.md +124 -0
- package/docs/recipes/builder-box-lifecycle.md +23 -0
- package/docs/recipes/devbox-app-release.md +14 -4
- package/docs/recipes/instance-first-run.md +16 -0
- package/docs/recipes/local-dev.md +81 -0
- package/docs/recipes/ops-gotchas.md +19 -1
- package/docs/recipes/overnight-builder.md +1 -1
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/parallel-goal-waves.md +174 -0
- package/docs/recipes/ship-and-regrade.md +2 -2
- package/docs/recipes/standalone-live-docs.md +48 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/docs/recipes/work-on-a-task-from-your-dev-box.md +108 -0
- package/migrations/003_pms.sql +6 -7
- package/migrations/006_pms_v2.sql +3 -31
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/migrations/core_193_task_needs_rebase.sql +28 -0
- package/migrations/core_194_confirmed_backlog_clawback.sql +63 -0
- package/migrations/core_195_idea_triage_provenance.sql +57 -0
- package/migrations/core_196_session_reward_topup.sql +69 -0
- package/modules/autonomy/pollers/routine-timers.js +9 -8
- package/modules/builder-settings/wandering-prefs.js +61 -18
- package/modules/dev-box/app/README.md +73 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-alertTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-busyTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-offTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate.png +0 -0
- package/modules/dev-box/app/assets/tray/mac-onTemplate@2x.png +0 -0
- package/modules/dev-box/app/assets/tray/win-alert.png +0 -0
- package/modules/dev-box/app/assets/tray/win-busy.png +0 -0
- package/modules/dev-box/app/assets/tray/win-off.png +0 -0
- package/modules/dev-box/app/assets/tray/win-on.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background.png +0 -0
- package/modules/dev-box/app/build-resources/dmg-background@2x.png +0 -0
- package/modules/dev-box/app/build-resources/icon.png +0 -0
- package/modules/dev-box/app/electron-builder.tmpl.yml +95 -0
- package/modules/dev-box/app/electron-builder.yml +95 -0
- package/modules/dev-box/app/package-lock.json +5291 -0
- package/modules/dev-box/app/package.json +30 -0
- package/modules/dev-box/app/renderer/app.js +319 -0
- package/modules/dev-box/app/renderer/index.html +45 -0
- package/modules/dev-box/app/renderer/styles.css +246 -0
- package/modules/dev-box/app/renderer/theme.generated.css +16 -0
- package/modules/dev-box/app/scripts/apply-dmg-backdrop.sh +82 -0
- package/modules/dev-box/app/scripts/dmgbuild-settings.py +39 -0
- package/modules/dev-box/app/scripts/gen-brand.js +138 -0
- package/modules/dev-box/app/scripts/gen-dmg-backdrop.py +323 -0
- package/modules/dev-box/app/scripts/gen-icons.js +286 -0
- package/modules/dev-box/app/scripts/vendor-client.js +23 -0
- package/modules/dev-box/app/src/box-controller.js +287 -0
- package/modules/dev-box/app/src/brand.generated.json +13 -0
- package/modules/dev-box/app/src/brand.js +16 -0
- package/modules/dev-box/app/src/config.js +19 -0
- package/modules/dev-box/app/src/gds-client.js +90 -0
- package/modules/dev-box/app/src/main.js +610 -0
- package/modules/dev-box/app/src/pairing.js +57 -0
- package/modules/dev-box/app/src/preload.js +29 -0
- package/modules/dev-box/app/src/session-store.js +58 -0
- package/modules/dev-box/app/src/ssh-setup.js +569 -0
- package/modules/dev-box/app/src/task-picker.js +70 -0
- package/modules/dev-box/app/src/tray.js +99 -0
- package/modules/dev-box/app/src/updater.js +55 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +698 -0
- package/modules/dev-box/app/test/unit.mjs +526 -0
- package/modules/dev-box/box-access.js +121 -4
- package/modules/dev-box/boxes.js +20 -0
- package/modules/dev-box/module.json +1 -1
- package/modules/dev-box/routes/box.js +87 -3
- package/modules/economy/credits.js +116 -25
- package/modules/economy/reward.js +8 -0
- package/modules/economy/streak.js +2 -1
- package/modules/governance/CLAUDE.md +15 -0
- package/modules/governance/catalog.js +419 -0
- package/modules/governance/config.js +174 -0
- package/modules/governance/db.js +125 -0
- package/modules/governance/migrations/governance_001_tables.sql +81 -0
- package/modules/governance/migrations/governance_002_seed_and_backfill.sql +103 -0
- package/modules/governance/migrations/governance_003_task_abandon_metic.sql +28 -0
- package/modules/governance/migrations/governance_004_criterion_create.sql +39 -0
- package/modules/governance/migrations/governance_005_resync_builder_rank_roles.sql +56 -0
- package/modules/governance/module.json +10 -0
- package/modules/governance/protected-surfaces.js +88 -0
- package/modules/governance/protected-surfaces.json +80 -0
- package/modules/governance/rank-roles.js +83 -0
- package/modules/governance/resolver.js +221 -0
- package/modules/governance/routes/governance.js +89 -0
- package/modules/governance/routes/role-sync.js +49 -0
- package/modules/governance/session-scopes.js +127 -0
- package/modules/governance/session-scopes.json +38 -0
- package/modules/hall-ui/public/atlas.css +117 -0
- package/modules/hall-ui/public/atlas.html +3 -114
- package/modules/hall-ui/public/brand-apply.js +34 -6
- package/modules/hall-ui/public/builders.js +187 -219
- package/modules/hall-ui/public/diagrams.html +2 -0
- package/modules/hall-ui/public/dom-utils.js +27 -0
- package/modules/hall-ui/public/drachmae.html +2 -0
- package/modules/hall-ui/public/drachmae.js +54 -1
- package/modules/hall-ui/public/gate.css +98 -0
- package/modules/hall-ui/public/gate.html +3 -95
- package/modules/hall-ui/public/goal-inbox.js +4 -1
- package/modules/hall-ui/public/goals-page.js +626 -114
- package/modules/hall-ui/public/goals.css +79 -1
- package/modules/hall-ui/public/goals.html +38 -11
- package/modules/hall-ui/public/hall-widgets.js +62 -52
- package/modules/hall-ui/public/harbor.css +123 -0
- package/modules/hall-ui/public/harbor.html +3 -120
- package/modules/hall-ui/public/index.html +11 -4
- package/modules/hall-ui/public/instances-panel.js +4 -1
- package/modules/hall-ui/public/modules.css +63 -0
- package/modules/hall-ui/public/modules.html +3 -60
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/new-project-wizard.js +7 -3
- package/modules/hall-ui/public/not-ready.html +2 -0
- package/modules/hall-ui/public/pair.css +73 -0
- package/modules/hall-ui/public/pair.html +3 -70
- package/modules/hall-ui/public/palette.js +302 -0
- package/modules/hall-ui/public/primer.css +13 -0
- package/modules/hall-ui/public/primer.html +2 -0
- package/modules/hall-ui/public/profile.html +2 -0
- package/modules/hall-ui/public/ranks.html +2 -0
- package/modules/hall-ui/public/roadmap.css +179 -0
- package/modules/hall-ui/public/roadmap.html +9 -172
- package/modules/hall-ui/public/roadmap.js +329 -316
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/hall-ui/public/sessions.css +16 -0
- package/modules/hall-ui/public/sessions.html +3 -13
- package/modules/hall-ui/public/settings.html +2 -0
- package/modules/hall-ui/public/settings.js +4 -4
- package/modules/hall-ui/public/shell.js +143 -18
- package/modules/hall-ui/public/style.css +231 -39
- package/modules/hall-ui/public/task-detail.js +347 -26
- package/modules/hall-ui/public/task.html +6 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/work.html +8 -6
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/ideas/inbox.js +61 -11
- package/modules/ideas/routes/inbox.js +23 -0
- package/modules/lifecycle/claim-eligibility.js +72 -0
- package/modules/lifecycle/db.js +519 -139
- package/modules/lifecycle/done-when.js +18 -3
- package/modules/lifecycle/github-push.js +19 -2
- package/modules/lifecycle/goal-advisory.js +114 -0
- package/modules/lifecycle/goal-authz.js +62 -0
- package/modules/lifecycle/lifecycle.js +9 -0
- package/modules/lifecycle/publish-reconciler.js +109 -6
- package/modules/lifecycle/routes/claims.js +68 -23
- package/modules/lifecycle/routes/done-when.js +110 -15
- package/modules/lifecycle/routes/goals.js +8 -38
- package/modules/lifecycle/routes/tasks.js +38 -6
- package/modules/lifecycle/ship-card.js +6 -0
- package/modules/onboarding/onboarding-state.js +1 -1
- package/modules/platform-identity/CLAUDE.md +15 -0
- package/modules/platform-identity/hub-device.js +147 -0
- package/modules/platform-identity/hub-keys.js +151 -0
- package/modules/platform-identity/migrations/platform_identity_001_tables.sql +114 -0
- package/modules/platform-identity/migrations/platform_identity_002_client_origin_index.sql +18 -0
- package/modules/platform-identity/migrations/platform_identity_003_project_memberships.sql +42 -0
- package/modules/platform-identity/migrations/platform_identity_004_projects.sql +44 -0
- package/modules/platform-identity/migrations/platform_identity_005_builder_activity.sql +42 -0
- package/modules/platform-identity/module.json +18 -0
- package/modules/platform-identity/platform-identity.js +531 -0
- package/modules/platform-identity/pollers/code-sweep.js +43 -0
- package/modules/platform-identity/routes/my-projects.js +140 -0
- package/modules/platform-identity/routes/projects.js +107 -0
- package/modules/platform-identity/routes/scouting.js +35 -0
- package/modules/platform-identity/routes/sso.js +402 -0
- package/modules/platform-identity/tests/platform-identity.mjs +388 -0
- package/modules/platform-identity/ui/.gitkeep +0 -0
- package/modules/security/routes/reports.js +10 -0
- package/modules/sessions/db.js +166 -21
- package/modules/sessions/module.json +1 -1
- package/modules/sessions/pollers/session-reward-reconcile.js +78 -0
- package/modules/sessions/routes/sessions.js +33 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/anim/idle/frame_00.webp +0 -0
- package/public-landing/assets/anim/idle/frame_01.webp +0 -0
- package/public-landing/assets/anim/idle/frame_02.webp +0 -0
- package/public-landing/assets/anim/idle/frame_03.webp +0 -0
- package/public-landing/assets/anim/idle/frame_04.webp +0 -0
- package/public-landing/assets/anim/idle/frame_05.webp +0 -0
- package/public-landing/assets/anim/idle/frame_06.webp +0 -0
- package/public-landing/assets/anim/idle/frame_07.webp +0 -0
- package/public-landing/assets/anim/idle/frame_08.webp +0 -0
- package/public-landing/assets/anim/idle/frame_09.webp +0 -0
- package/public-landing/assets/anim/idle/frame_10.webp +0 -0
- package/public-landing/assets/anim/idle/frame_11.webp +0 -0
- package/public-landing/assets/anim/idle/frame_12.webp +0 -0
- package/public-landing/assets/anim/idle/frame_13.webp +0 -0
- package/public-landing/assets/anim/idle/frame_14.webp +0 -0
- package/public-landing/assets/anim/idle/frame_15.webp +0 -0
- package/public-landing/assets/anim/idle/frame_16.webp +0 -0
- package/public-landing/assets/anim/idle/frame_17.webp +0 -0
- package/public-landing/assets/anim/idle/frame_18.webp +0 -0
- package/public-landing/assets/anim/idle/frame_19.webp +0 -0
- package/public-landing/assets/anim/idle/frame_20.webp +0 -0
- package/public-landing/assets/anim/idle/frame_21.webp +0 -0
- package/public-landing/assets/anim/idle/frame_22.webp +0 -0
- package/public-landing/assets/anim/idle/frame_23.webp +0 -0
- package/public-landing/assets/anim/idle/frame_24.webp +0 -0
- package/public-landing/assets/anim/idle/frame_25.webp +0 -0
- package/public-landing/assets/anim/idle/frame_26.webp +0 -0
- package/public-landing/assets/anim/idle/frame_27.webp +0 -0
- package/public-landing/assets/anim/idle/frame_28.webp +0 -0
- package/public-landing/assets/anim/idle/frame_29.webp +0 -0
- package/public-landing/assets/anim/idle/frame_30.webp +0 -0
- package/public-landing/assets/anim/idle/frame_31.webp +0 -0
- package/public-landing/assets/anim/idle/frame_32.webp +0 -0
- package/public-landing/assets/anim/idle/frame_33.webp +0 -0
- package/public-landing/assets/anim/idle/frame_34.webp +0 -0
- package/public-landing/assets/anim/idle/frame_35.webp +0 -0
- package/public-landing/assets/anim/idle/frame_36.webp +0 -0
- package/public-landing/assets/anim/idle/frame_37.webp +0 -0
- package/public-landing/assets/anim/idle/frame_38.webp +0 -0
- package/public-landing/assets/anim/idle/frame_39.webp +0 -0
- package/public-landing/assets/anim/idle/frame_40.webp +0 -0
- package/public-landing/assets/anim/idle/frame_41.webp +0 -0
- package/public-landing/assets/anim/idle/frame_42.webp +0 -0
- package/public-landing/assets/anim/idle/frame_43.webp +0 -0
- package/public-landing/create.html +252 -62
- package/public-landing/gate/README.md +137 -0
- package/public-landing/gate/dispmaps/bar.png +0 -0
- package/public-landing/gate/dispmaps/flat.png +0 -0
- package/public-landing/gate/dispmaps/key.png +0 -0
- package/public-landing/gate/index.html +587 -0
- package/public-landing/gate/previews/cloud-bongos.jpg +0 -0
- package/public-landing/gate/previews/off-the-boats.jpg +0 -0
- package/public-landing/gate/space.css +799 -0
- package/public-landing/gate/space.js +1826 -0
- package/public-landing/gate/sprites/enter.png +0 -0
- package/public-landing/gate/sprites/search.png +0 -0
- package/public-landing/gate/sprites/terminal-blank.png +0 -0
- package/public-landing/gate/sprites/tile.png +0 -0
- package/public-landing/world/assets/clouds/cloud-island-large.png +0 -0
- package/public-landing/world/assets/clouds/cloud-puff.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-cargo.png +0 -0
- package/public-landing/world/assets/creatures/creature-whale-glide.png +0 -0
- package/public-landing/world/assets/plates/plate-clouds-near.png +0 -0
- package/public-landing/world/assets/plates/scene-establishing.png +0 -0
- package/scripts/gds/abandon.js +6 -4
- package/scripts/gds/api.js +10 -5
- package/scripts/gds/api.ps1 +26 -0
- package/scripts/gds/api.sh +6 -0
- package/scripts/gds/audit-ships.js +7 -1
- package/scripts/gds/backfill-sessions.js +78 -18
- package/scripts/gds/backup.js +23 -1
- package/scripts/gds/box-connect-lib.js +124 -31
- package/scripts/gds/box-sync.js +66 -0
- package/scripts/gds/box.js +225 -53
- package/scripts/gds/cf-tunnel.js +145 -6
- package/scripts/gds/claim.js +34 -3
- package/scripts/gds/cli-lib.js +161 -21
- package/scripts/gds/code.js +6 -2
- package/scripts/gds/deploy-divergence.js +209 -0
- package/scripts/gds/dev-lib.js +309 -0
- package/scripts/gds/dev.js +322 -0
- package/scripts/gds/doctor.js +90 -61
- package/scripts/gds/fetch-art-key.js +1 -1
- package/scripts/gds/fetch-sound-prefs.js +2 -2
- package/scripts/gds/fitness.js +228 -12
- package/scripts/gds/gds-db-backup.service +16 -1
- package/scripts/gds/gds-db-offsite.service +9 -1
- package/scripts/gds/gen-api-client.js +1 -1
- package/scripts/gds/gen-api-docs.js +36 -14
- package/scripts/gds/gen-diagrams.js +45 -28
- package/scripts/gds/gen-file-map.js +2 -1
- package/scripts/gds/gen-freshness.js +32 -0
- package/scripts/gds/gen-repo-map.js +3 -2
- package/scripts/gds/gen-session-index.js +14 -5
- package/scripts/gds/hooks/pre-push.js +2 -2
- package/scripts/gds/init.js +60 -3
- package/scripts/gds/install-git-hooks.js +1 -1
- package/scripts/gds/key-card.js +15 -6
- package/scripts/gds/key-status.js +1 -1
- package/scripts/gds/linkify-refs.js +35 -9
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +58 -11
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +268 -19
- package/scripts/gds/publish-manifest.js +22 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +42 -16
- package/scripts/gds/seed-bongos-v1-seo-tasks.js +104 -0
- package/scripts/gds/seed-bv1-profiles-tasks.js +126 -0
- package/scripts/gds/sequence.js +425 -0
- package/scripts/gds/session-cost.js +10 -1
- package/scripts/gds/setup.js +72 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +433 -59
- package/scripts/gds/smoke-gds.js +139 -8
- package/scripts/gds/start.js +171 -22
- package/scripts/gds/status.js +1 -1
- package/scripts/gds/triage.js +10 -9
- package/scripts/gds/upgrade.js +271 -7
- package/scripts/migrate.sh +10 -0
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +809 -107
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +105 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/module-scope-map.js +27 -1
- package/src/bongos/permission-path-check.js +136 -102
- package/src/bongos/repo-info.js +47 -11
- package/src/bongos/route-rank-check.js +429 -46
- package/src/bongos/routes/_helpers.js +12 -1
- package/src/bongos/routes/auth.js +312 -13
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/routes.js +2 -0
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +49 -2
- package/src/instance-config.js +46 -0
- package/src/module-api.js +140 -1
- package/src/platform-server.js +86 -9
- package/tests/abandon_task.mjs +59 -3
- package/tests/api_cli_flags.mjs +53 -0
- package/tests/auth_page_gate.mjs +13 -0
- package/tests/backfill_sessions.mjs +127 -1
- package/tests/backup_cli.mjs +54 -0
- package/tests/bongos_box_connect.mjs +77 -0
- package/tests/box_access.mjs +130 -1
- package/tests/box_credential.mjs +125 -0
- package/tests/box_host_keys.mjs +24 -0
- package/tests/box_infra_resolve.mjs +149 -0
- package/tests/box_scope_session.mjs +6 -2
- package/tests/box_sync.mjs +55 -0
- package/tests/box_task_scope.mjs +99 -0
- package/tests/branding.mjs +41 -23
- package/tests/cf_ruleset_preflight.mjs +142 -0
- package/tests/cf_tunnel.mjs +37 -0
- package/tests/claim_gate_rebase.mjs +98 -0
- package/tests/clawback_confirmed_backlog.mjs +103 -0
- package/tests/cli_lib_formatters.mjs +20 -0
- package/tests/confirm_attribution.mjs +30 -46
- package/tests/credit_grant.mjs +198 -10
- package/tests/criterion_authoring_gate.mjs +272 -0
- package/tests/criterion_progress.mjs +33 -1
- package/tests/dev_lib.mjs +253 -0
- package/tests/devbox_artifact_gate.mjs +75 -0
- package/tests/devbox_dmg_backdrop.mjs +137 -0
- package/tests/devbox_icons.mjs +304 -0
- package/tests/devbox_installer_brand.mjs +232 -0
- package/tests/devbox_key_register.mjs +234 -0
- package/tests/docs_openapi_route.mjs +110 -0
- package/tests/doctor_hooks.mjs +141 -0
- package/tests/extract_json.mjs +140 -0
- package/tests/fitness.mjs +89 -0
- package/tests/gen_freshness.mjs +96 -0
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/goal_advisory.mjs +251 -0
- package/tests/goal_criterion_confirm_ui.mjs +269 -0
- package/tests/goal_routes.mjs +59 -4
- package/tests/goal_tier.mjs +12 -3
- package/tests/governance_catalog.mjs +129 -0
- package/tests/governance_claim_eligibility.mjs +262 -0
- package/tests/governance_config.mjs +187 -0
- package/tests/governance_ownership_scope.mjs +410 -0
- package/tests/governance_page_gates.mjs +324 -0
- package/tests/governance_parity.mjs +336 -0
- package/tests/governance_principals.mjs +232 -0
- package/tests/governance_protected_surfaces.mjs +208 -0
- package/tests/governance_require_permission.mjs +223 -0
- package/tests/governance_resolver.mjs +84 -0
- package/tests/governance_role_sync.mjs +231 -0
- package/tests/governance_seed.mjs +127 -0
- package/tests/governance_session_scope.mjs +434 -0
- package/tests/governance_vanilla_boot.mjs +203 -0
- package/tests/grade_attribution.mjs +7 -7
- package/tests/hall_audit.mjs +223 -0
- package/tests/hall_nav.mjs +191 -0
- package/tests/hall_palette.mjs +206 -0
- package/tests/hall_signout.mjs +124 -0
- package/tests/hook_user_agents.mjs +10 -0
- package/tests/idea_triage.mjs +162 -0
- package/tests/idp_device_exchange.mjs +198 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/init.mjs +51 -0
- package/tests/instance_config.mjs +65 -0
- package/tests/instance_mismatch_guard.mjs +23 -6
- package/tests/instance_release_config.mjs +104 -0
- package/tests/linkify_refs.mjs +15 -2
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/module_contributions.mjs +55 -0
- package/tests/module_loader.mjs +10 -5
- package/tests/module_scope_active_claims.mjs +94 -0
- package/tests/needs_rebase.mjs +92 -0
- package/tests/one_active_claim_per_session.mjs +76 -0
- package/tests/override_attribution.mjs +10 -8
- package/tests/override_request_ownership.mjs +8 -4
- package/tests/package_core.mjs +246 -1
- package/tests/parse_cookie_decode.mjs +42 -0
- package/tests/pay_on_land.mjs +129 -0
- package/tests/platform_boot.mjs +100 -1
- package/tests/post_login_landing.mjs +42 -0
- package/tests/preflight_checks.mjs +72 -0
- package/tests/provision.mjs +289 -7
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/publish_branch_route.mjs +53 -0
- package/tests/publish_reconciler.mjs +187 -2
- package/tests/repo_info_binding.mjs +248 -0
- package/tests/reward_gate.mjs +11 -2
- package/tests/reward_mode_cost_plus_only.mjs +116 -0
- package/tests/reward_mode_surfacing.mjs +168 -0
- package/tests/route_permission_check.mjs +301 -0
- package/tests/route_rank_check.mjs +254 -12
- package/tests/route_validation.mjs +30 -0
- package/tests/sequence.mjs +267 -0
- package/tests/session_digest.mjs +44 -0
- package/tests/session_index.mjs +18 -6
- package/tests/session_records.mjs +39 -1
- package/tests/session_reward_on_land.mjs +381 -0
- package/tests/session_reward_reconcile.mjs +229 -0
- package/tests/ship_api_client_pathspec.mjs +100 -0
- package/tests/ship_base_flag.mjs +93 -0
- package/tests/ship_ci_deploy.mjs +140 -3
- package/tests/ship_upstream_deletion.mjs +57 -0
- package/tests/smoke_target.mjs +233 -0
- package/tests/start_env_guard.mjs +87 -0
- package/tests/start_rebase_warning.mjs +103 -0
- package/tests/status_mirror_sync.mjs +122 -0
- package/tests/stop_refcheck.mjs +19 -1
- package/tests/task_goal_link.mjs +15 -6
- package/tests/upgrade.mjs +393 -5
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
package/tests/platform_boot.mjs
CHANGED
|
@@ -10,9 +10,16 @@
|
|
|
10
10
|
import assert from 'node:assert/strict';
|
|
11
11
|
import { test, before, after } from 'node:test';
|
|
12
12
|
import http from 'node:http';
|
|
13
|
+
import net from 'node:net';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { spawn } from 'node:child_process';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
13
17
|
import platformMod from '../src/platform-server.js';
|
|
18
|
+
import loader from '../src/module-loader/loader.js';
|
|
14
19
|
|
|
15
|
-
const { buildPlatformApp } = platformMod;
|
|
20
|
+
const { buildPlatformApp, startBackgroundServices } = platformMod;
|
|
21
|
+
|
|
22
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
23
|
|
|
17
24
|
let server;
|
|
18
25
|
let port;
|
|
@@ -168,3 +175,95 @@ test('R60: the served hall injects window.__MODULES__ (feature-module flags) syn
|
|
|
168
175
|
assert.equal(typeof parsed.enabled['dev-box'], 'boolean');
|
|
169
176
|
assert.equal(typeof parsed.enabled.discord, 'boolean');
|
|
170
177
|
});
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// task 1002419 — the platform entrypoint must actually START the module pollers.
|
|
181
|
+
//
|
|
182
|
+
// Regression fixture for a silent, high-blast-radius outage: startModulePollers
|
|
183
|
+
// had ZERO callers after the game entrypoint (server.js) was retired, so on a
|
|
184
|
+
// standalone instance EVERY module poller was dead — most visibly the lifecycle
|
|
185
|
+
// publish-reconciler, which is what drives 'confirmed' tasks to 'shipped'. Eleven
|
|
186
|
+
// tasks stranded at 'confirmed' (oldest 6.2 days) with nothing in any log to say
|
|
187
|
+
// why: a poller that is never started is indistinguishable from one with nothing
|
|
188
|
+
// to do. Both tests below are DB-free.
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
test('task 1002419: startBackgroundServices starts the enabled modules’ pollers', async () => {
|
|
192
|
+
// The wiring resolves a real, ENABLED module poller. lifecycle is default:true
|
|
193
|
+
// and declares contributes.pollers:["publish-reconciler"], so it must appear.
|
|
194
|
+
loader._reset();
|
|
195
|
+
let keys = [];
|
|
196
|
+
try {
|
|
197
|
+
const started = startBackgroundServices();
|
|
198
|
+
keys = started.map((p) => `${p.key}/${p.poller}`);
|
|
199
|
+
assert.ok(
|
|
200
|
+
keys.includes('lifecycle/publish-reconciler'),
|
|
201
|
+
`the lifecycle publish-reconciler poller must start (got: ${keys.join(', ') || 'none'})`
|
|
202
|
+
);
|
|
203
|
+
} finally {
|
|
204
|
+
// Clear the real (unref'd) timers this just started so the suite leaves none.
|
|
205
|
+
// The stopped count is also the loader's own record of what it started, so it
|
|
206
|
+
// must match what startBackgroundServices reported.
|
|
207
|
+
const stopped = await loader.stopModulePollers();
|
|
208
|
+
assert.equal(stopped, keys.length, 'stopModulePollers stops exactly what was started');
|
|
209
|
+
loader._reset();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function freePort() {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const s = net.createServer();
|
|
216
|
+
s.on('error', reject);
|
|
217
|
+
s.listen(0, '127.0.0.1', () => {
|
|
218
|
+
const { port } = s.address();
|
|
219
|
+
s.close(() => resolve(port));
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
test('task 1002419: a real `node src/platform-server.js` boot announces the reconciler', async () => {
|
|
225
|
+
// The mutation-proof half. The test above would still pass if the entry block
|
|
226
|
+
// stopped calling startBackgroundServices(); this one spawns the entrypoint the
|
|
227
|
+
// way docker-entrypoint.sh does and asserts the poller announces itself in the
|
|
228
|
+
// boot log — so deleting the call site turns it red.
|
|
229
|
+
const port = await freePort();
|
|
230
|
+
const child = spawn(process.execPath, [path.join(REPO_ROOT, 'src', 'platform-server.js')], {
|
|
231
|
+
cwd: REPO_ROOT,
|
|
232
|
+
env: {
|
|
233
|
+
...process.env,
|
|
234
|
+
PORT: String(port),
|
|
235
|
+
HOST: '127.0.0.1',
|
|
236
|
+
// Push every first-fire far past the life of this test: we assert the
|
|
237
|
+
// START announcement, never a sweep, so no poller ever touches Postgres.
|
|
238
|
+
PUBLISH_RECONCILE_FIRST_MS: '3600000',
|
|
239
|
+
ROUTINE_PEER_VOTE_FIRST_MS: '3600000',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const log = await new Promise((resolve, reject) => {
|
|
245
|
+
let out = '';
|
|
246
|
+
const timer = setTimeout(
|
|
247
|
+
() => reject(new Error(`platform-server did not announce the reconciler within 20s. Log:\n${out}`)),
|
|
248
|
+
20_000
|
|
249
|
+
);
|
|
250
|
+
const onData = (d) => {
|
|
251
|
+
out += d;
|
|
252
|
+
if (/\[publish-reconciler\] started/.test(out)) {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
resolve(out);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
child.stdout.on('data', onData);
|
|
258
|
+
child.stderr.on('data', onData);
|
|
259
|
+
child.on('error', (e) => { clearTimeout(timer); reject(e); });
|
|
260
|
+
child.on('exit', (code) => {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
reject(new Error(`platform-server exited early (code ${code}). Log:\n${out}`));
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
assert.match(log, /\[publish-reconciler\] started — sweeping confirmed→shipped every \d+s/);
|
|
266
|
+
} finally {
|
|
267
|
+
child.kill('SIGKILL');
|
|
268
|
+
}
|
|
269
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// tests/post_login_landing.mjs — the configurable post-sign-in landing (task 1002240).
|
|
2
|
+
//
|
|
3
|
+
// postLoginLanding(brand) resolves the redirect target used after a successful
|
|
4
|
+
// web sign-in when NO explicit ?return= / same-apex Referer was carried. It is
|
|
5
|
+
// the ONE seam that lets a platform-home instance (cloudbongos.com) land a
|
|
6
|
+
// visitor back on its orbs (/gate/) instead of the builders' hall, while every
|
|
7
|
+
// other instance keeps the portable '/builders' default (the hall always
|
|
8
|
+
// exists; a /gate/ orbs site does not). Pure over an injected brand pack, and
|
|
9
|
+
// hardened through the SAME open-redirect allowlist as ?return= (safeReturnTo),
|
|
10
|
+
// so a misconfigured landing can never become an open redirect.
|
|
11
|
+
//
|
|
12
|
+
// Pure function, no DB — requiring the route module does not open a pool.
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
const require = createRequire(import.meta.url);
|
|
16
|
+
const { postLoginLanding } = require('../src/bongos/routes/auth.js');
|
|
17
|
+
|
|
18
|
+
let pass = 0, fail = 0;
|
|
19
|
+
function ok(cond, msg) {
|
|
20
|
+
if (cond) { pass++; console.log(` ok ${msg}`); }
|
|
21
|
+
else { fail++; console.log(` FAIL ${msg}`); }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log('\npostLoginLanding — portable default is the hall:');
|
|
25
|
+
ok(postLoginLanding({}) === '/builders', 'no landing config → /builders');
|
|
26
|
+
ok(postLoginLanding({ landing: {} }) === '/builders', 'empty landing block → /builders');
|
|
27
|
+
// No-arg path reads the real committed neutral pack, which ships postLogin:'/builders'.
|
|
28
|
+
ok(postLoginLanding() === '/builders', 'committed neutral pack → /builders');
|
|
29
|
+
|
|
30
|
+
console.log('\npostLoginLanding — an instance lands visitors on its own front door:');
|
|
31
|
+
ok(postLoginLanding({ landing: { postLogin: '/gate/' } }) === '/gate/', 'cloudbongos orbs → /gate/');
|
|
32
|
+
ok(postLoginLanding({ landing: { postLogin: '/gate/?entered=1' } }) === '/gate/?entered=1', 'relative path with query preserved');
|
|
33
|
+
|
|
34
|
+
console.log('\npostLoginLanding — a misconfigured landing can never become an open redirect:');
|
|
35
|
+
ok(postLoginLanding({ landing: { postLogin: 'https://evil.com/' } }) === '/builders', 'foreign https origin → fallback');
|
|
36
|
+
ok(postLoginLanding({ landing: { postLogin: '//evil.com' } }) === '/builders', 'protocol-relative → fallback');
|
|
37
|
+
ok(postLoginLanding({ landing: { postLogin: 'javascript:alert(1)' } }) === '/builders', 'javascript: scheme → fallback');
|
|
38
|
+
ok(postLoginLanding({ landing: { postLogin: '/a\\b' } }) === '/builders', 'backslash path → fallback');
|
|
39
|
+
ok(postLoginLanding({ landing: { postLogin: 42 } }) === '/builders', 'non-string value → fallback');
|
|
40
|
+
|
|
41
|
+
console.log(`\nPost-login-landing tests: ${pass} passed, ${fail} failed`);
|
|
42
|
+
if (fail) process.exit(1);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// tests/preflight_checks.mjs
|
|
2
|
+
//
|
|
3
|
+
// Unit test for scripts/gds/preflight.js `preflightChecks()` (task 1002401,
|
|
4
|
+
// onboarding-audit area 4) — the pure onboarding environment preflight shared by
|
|
5
|
+
// /builder-setup and `bongos doctor`. DB-free and does zero real I/O: node
|
|
6
|
+
// version, fs, and child_process are all injected, so we assert the real
|
|
7
|
+
// decision logic (levels + fixes) without touching the machine.
|
|
8
|
+
|
|
9
|
+
import { test } from 'node:test';
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { preflightChecks, formatPreflight } = require('../scripts/gds/preflight.js');
|
|
15
|
+
|
|
16
|
+
const okSpawn = () => ({ status: 0, stdout: '1.2.3 (Claude Code)\n' });
|
|
17
|
+
const badSpawn = () => ({ error: Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' }) });
|
|
18
|
+
const fsWith = (present) => ({ existsSync: () => present });
|
|
19
|
+
const find = (results, title) => results.find((r) => r.title === title);
|
|
20
|
+
|
|
21
|
+
test('all green: node>=22, deps present, claude launches', () => {
|
|
22
|
+
const r = preflightChecks({ nodeVersion: 'v22.5.0', fs: fsWith(true), spawn: okSpawn, env: {} });
|
|
23
|
+
assert.equal(find(r, 'Node').level, 'ok');
|
|
24
|
+
assert.equal(find(r, 'Dependencies').level, 'ok');
|
|
25
|
+
assert.equal(find(r, 'claude CLI').level, 'ok');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('node below v22 → fail with an upgrade fix', () => {
|
|
29
|
+
const r = preflightChecks({ nodeVersion: 'v18.19.0', fs: fsWith(true), spawn: okSpawn, env: {} });
|
|
30
|
+
const node = find(r, 'Node');
|
|
31
|
+
assert.equal(node.level, 'fail');
|
|
32
|
+
assert.match(node.fix, /22|nvm|nodejs/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('missing node_modules → warn (non-fatal) with `npm ci` fix', () => {
|
|
36
|
+
const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(false), spawn: okSpawn, env: {} });
|
|
37
|
+
const deps = find(r, 'Dependencies');
|
|
38
|
+
assert.equal(deps.level, 'warn');
|
|
39
|
+
assert.match(deps.fix, /npm ci/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('claude not launchable → fail naming SUBAGENT_SPAWN_FAILED + install fix', () => {
|
|
43
|
+
const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(true), spawn: badSpawn, env: {} });
|
|
44
|
+
const claude = find(r, 'claude CLI');
|
|
45
|
+
assert.equal(claude.level, 'fail');
|
|
46
|
+
assert.match(claude.detail, /SUBAGENT_SPAWN_FAILED/);
|
|
47
|
+
assert.match(claude.fix, /@anthropic-ai\/claude-code|OTB_CLAUDE_BIN/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('OTB_CLAUDE_BIN is used as the probe binary and labelled', () => {
|
|
51
|
+
let seenBin = null;
|
|
52
|
+
const spy = (bin) => { seenBin = bin; return { status: 0, stdout: 'x\n' }; };
|
|
53
|
+
const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsWith(true), spawn: spy, env: { OTB_CLAUDE_BIN: '/opt/claude' } });
|
|
54
|
+
assert.equal(seenBin, '/opt/claude');
|
|
55
|
+
assert.match(find(r, 'claude CLI').detail, /via OTB_CLAUDE_BIN/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('a thrown existsSync is treated as deps-missing, never crashes', () => {
|
|
59
|
+
const fsThrows = { existsSync: () => { throw new Error('boom'); } };
|
|
60
|
+
const r = preflightChecks({ nodeVersion: 'v22.0.0', fs: fsThrows, spawn: okSpawn, env: {} });
|
|
61
|
+
assert.equal(find(r, 'Dependencies').level, 'warn');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('formatPreflight renders glyphs + fixes and stays plain (no ANSI) by default', () => {
|
|
65
|
+
const r = preflightChecks({ nodeVersion: 'v18.0.0', fs: fsWith(false), spawn: badSpawn, env: {} });
|
|
66
|
+
const s = formatPreflight(r, { heading: true });
|
|
67
|
+
assert.match(s, /Checking your environment/);
|
|
68
|
+
assert.match(s, /✖ Node/);
|
|
69
|
+
assert.match(s, /! Dependencies/);
|
|
70
|
+
assert.match(s, /→ .*npm ci/);
|
|
71
|
+
assert.doesNotMatch(s, /\x1b\[/); // color:false ⇒ no ANSI escapes
|
|
72
|
+
});
|
package/tests/provision.mjs
CHANGED
|
@@ -282,6 +282,10 @@ function recorder({ apply = false } = {}) {
|
|
|
282
282
|
// Stub the DNS token-scope preflight (task 1980) so the composition stays hermetic
|
|
283
283
|
// (no real CF call). Default: reachable; a test overrides it to exercise the block.
|
|
284
284
|
checkDnsTokenScope: async () => ({ reachable: true, zone: 'example.com' }),
|
|
285
|
+
// Owner OAuth token lookup for the scaffold push (task 1002317). Default: none →
|
|
286
|
+
// the scaffold takes the SSH `origin` fallback (so existing push assertions hold
|
|
287
|
+
// without touching the real db module). A test overrides it to exercise the token path.
|
|
288
|
+
getGithubToken: async () => null,
|
|
285
289
|
};
|
|
286
290
|
return { deps, cmds, writes, statuses };
|
|
287
291
|
}
|
|
@@ -337,8 +341,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
337
341
|
const r = await P.provisionInstance({ ...coTenant, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
|
|
338
342
|
assert.equal(r.ok, true);
|
|
339
343
|
const joined = cmds.join('\n');
|
|
340
|
-
// scaffold leg (task 2055):
|
|
341
|
-
assert.match(joined, /git
|
|
344
|
+
// scaffold leg (task 2055): git init → package-core → init --vendor-core → push
|
|
345
|
+
assert.match(joined, /git -C \S+ init -q/, 'inits the instance working tree (empty repo — nothing to clone)');
|
|
346
|
+
assert.match(joined, /git -C \S+ remote add origin git@github-deploy:lars589\/mercury\.git/, 'sets the instance repo as origin');
|
|
342
347
|
assert.match(joined, /node scripts\/gds\/package-core\.js/, 'builds the core tarball');
|
|
343
348
|
assert.match(joined, /init\.js --from .* --dir .* --no-seed --force --vendor-core /, 'materializes + file:-pins the core');
|
|
344
349
|
assert.match(joined, /git -C .* push -u origin main/, 'pushes the scaffold');
|
|
@@ -349,9 +354,9 @@ await ta('provisionInstance(standalone) SCAFFOLDS its repo, then syncs (pull + n
|
|
|
349
354
|
assert.match(joined, /node_modules\/@bongos\/core\/scripts\/migrate\.sh/, 'migrates from the INSTANCE root\'s core');
|
|
350
355
|
assert.ok(!cmds.some((c) => /\.\/scripts\/migrate\.sh/.test(c)), 'standalone does NOT use the control-plane checkout migrate');
|
|
351
356
|
assert.match(joined, /INSERT INTO versions/, 'standalone seeds its first version too (task 2119)');
|
|
352
|
-
// ordering:
|
|
357
|
+
// ordering: git init → package-core → init → push → pull → npm ci → migrate → service
|
|
353
358
|
const idx = (re) => cmds.findIndex((c) => re.test(c));
|
|
354
|
-
assert.ok(idx(/git
|
|
359
|
+
assert.ok(idx(/git -C \S+ init -q/) < idx(/package-core/), 'git init before tarball build');
|
|
355
360
|
assert.ok(idx(/package-core/) < idx(/init\.js/), 'tarball before init');
|
|
356
361
|
assert.ok(idx(/init\.js/) < idx(/push -u origin main/), 'init before push');
|
|
357
362
|
assert.ok(idx(/push -u origin main/) < idx(/git pull/), 'scaffold push before the deploy pull');
|
|
@@ -415,7 +420,7 @@ await ta('scaffoldStandaloneRepo: idempotent — a repo that already carries pac
|
|
|
415
420
|
const { deps, cmds } = recorder({ apply: true });
|
|
416
421
|
const res = await P.scaffoldStandaloneRepo({ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' }, deps);
|
|
417
422
|
assert.equal(res.already, true);
|
|
418
|
-
assert.equal(cmds.length, 0, 'no
|
|
423
|
+
assert.equal(cmds.length, 0, 'no init/build/push when already scaffolded');
|
|
419
424
|
} finally {
|
|
420
425
|
P.CONFIG.standaloneBase = origBase;
|
|
421
426
|
rmSync(tmp, { recursive: true, force: true });
|
|
@@ -431,9 +436,13 @@ async function scaffoldWithEnv(env) {
|
|
|
431
436
|
P.CONFIG.standaloneBase = tmp;
|
|
432
437
|
try {
|
|
433
438
|
const { deps, cmds } = recorder({ apply: true });
|
|
439
|
+
// instanceRoot = the fresh base (no package.json → no `file:` core pin), so these
|
|
440
|
+
// vendored-path assertions stay deterministic: installedCoreTarball returns null and
|
|
441
|
+
// the scaffold rebuilds via package-core.js (task 1002275). A consumer-instance root
|
|
442
|
+
// (a real pin) takes the reuse path — covered by its own test below.
|
|
434
443
|
await P.scaffoldStandaloneRepo(
|
|
435
444
|
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
|
|
436
|
-
{ ...deps, env },
|
|
445
|
+
{ ...deps, env, instanceRoot: tmp },
|
|
437
446
|
);
|
|
438
447
|
return cmds.join('\n');
|
|
439
448
|
} finally {
|
|
@@ -463,6 +472,122 @@ await ta('scaffoldStandaloneRepo: default (no registry env) stays on the vendore
|
|
|
463
472
|
assert.ok(!/--registry-core/.test(joined));
|
|
464
473
|
});
|
|
465
474
|
|
|
475
|
+
// ── consumer control-plane core reuse (the empty-repo fix, task 1002275) ──────────
|
|
476
|
+
// When the runner is ITSELF a consumer instance (its @bongos/core is a vendored npm dep,
|
|
477
|
+
// not a source checkout), package-core.js can't rebuild the core from git (REPO_ROOT is
|
|
478
|
+
// inside gitignored node_modules → an EMPTY core set → abort → no push → EMPTY repo, seen
|
|
479
|
+
// live on cloudbongos.com for trial/test). Reuse the pinned tarball the instance runs.
|
|
480
|
+
t('installedCoreTarball: reuses a consumer instance file: core pin; null on a source checkout', () => {
|
|
481
|
+
// consumer instance: package.json file:-pins the core AND the tarball is on disk → reuse it
|
|
482
|
+
const inst = mkdtempSync(join(tmpdir(), 'cptgz-consumer-'));
|
|
483
|
+
mkdirSync(join(inst, 'vendor'));
|
|
484
|
+
writeFileSync(join(inst, 'vendor', 'bongos-core-1.17.4.tgz'), 'x');
|
|
485
|
+
writeFileSync(join(inst, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.17.4.tgz' } }));
|
|
486
|
+
assert.equal(P.installedCoreTarball({ instanceRoot: inst }), join(inst, 'vendor', 'bongos-core-1.17.4.tgz'), 'reuses the pinned tarball');
|
|
487
|
+
|
|
488
|
+
// dangling pin (tarball pruned) → null, so the caller never points init at a missing file
|
|
489
|
+
rmSync(join(inst, 'vendor', 'bongos-core-1.17.4.tgz'));
|
|
490
|
+
assert.equal(P.installedCoreTarball({ instanceRoot: inst }), null, 'a dangling file: pin resolves to null');
|
|
491
|
+
|
|
492
|
+
// source checkout: a caret range (no file: pin) → null → the caller rebuilds via package-core.js
|
|
493
|
+
const src = mkdtempSync(join(tmpdir(), 'cptgz-source-'));
|
|
494
|
+
writeFileSync(join(src, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': '^1.17.4' } }));
|
|
495
|
+
assert.equal(P.installedCoreTarball({ instanceRoot: src }), null, 'a source checkout has no file: pin');
|
|
496
|
+
|
|
497
|
+
// no package.json at all → null
|
|
498
|
+
const bare = mkdtempSync(join(tmpdir(), 'cptgz-bare-'));
|
|
499
|
+
assert.equal(P.installedCoreTarball({ instanceRoot: bare }), null, 'no package.json → null');
|
|
500
|
+
|
|
501
|
+
// the @cloudbongos/core scope alias is honored too (infra/core-node.sh accepts both)
|
|
502
|
+
const alt = mkdtempSync(join(tmpdir(), 'cptgz-alt-'));
|
|
503
|
+
mkdirSync(join(alt, 'vendor'));
|
|
504
|
+
writeFileSync(join(alt, 'vendor', 'bongos-core-2.0.0.tgz'), 'x');
|
|
505
|
+
writeFileSync(join(alt, 'package.json'), JSON.stringify({ dependencies: { '@cloudbongos/core': 'file:vendor/bongos-core-2.0.0.tgz' } }));
|
|
506
|
+
assert.equal(P.installedCoreTarball({ instanceRoot: alt }), join(alt, 'vendor', 'bongos-core-2.0.0.tgz'), 'honors the @cloudbongos/core scope alias');
|
|
507
|
+
|
|
508
|
+
[inst, src, bare, alt].forEach((d) => rmSync(d, { recursive: true, force: true }));
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
await ta('scaffoldStandaloneRepo: on a consumer control plane it REUSES the pinned tarball — no package-core rebuild (task 1002275)', async () => {
|
|
512
|
+
// the control plane is itself a consumer instance: its package.json file:-pins the core
|
|
513
|
+
const cp = mkdtempSync(join(tmpdir(), 'cptgz-cp-'));
|
|
514
|
+
mkdirSync(join(cp, 'vendor'));
|
|
515
|
+
const pinned = join(cp, 'vendor', 'bongos-core-1.17.4.tgz');
|
|
516
|
+
writeFileSync(pinned, 'x');
|
|
517
|
+
writeFileSync(join(cp, 'package.json'), JSON.stringify({ dependencies: { '@bongos/core': 'file:vendor/bongos-core-1.17.4.tgz' } }));
|
|
518
|
+
const base = mkdtempSync(join(tmpdir(), 'cptgz-standalone-')); // FRESH base → the scaffold proceeds
|
|
519
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
520
|
+
P.CONFIG.standaloneBase = base;
|
|
521
|
+
try {
|
|
522
|
+
const { deps, cmds } = recorder({ apply: true });
|
|
523
|
+
await P.scaffoldStandaloneRepo(
|
|
524
|
+
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo' },
|
|
525
|
+
{ ...deps, instanceRoot: cp },
|
|
526
|
+
);
|
|
527
|
+
const joined = cmds.join('\n');
|
|
528
|
+
assert.ok(!/package-core\.js/.test(joined), 'does NOT rebuild the core from source (the empty-set abort path)');
|
|
529
|
+
assert.match(joined, new RegExp('init\\.js --from .* --no-seed --force --vendor-core ' + reEsc(pinned)), 'feeds the PINNED tarball to init --vendor-core');
|
|
530
|
+
assert.match(joined, /git -C \S+ remote add origin git@github-deploy:lars589\/demo\.git/, 'still targets the repo as origin');
|
|
531
|
+
assert.match(joined, /push /, 'still pushes the scaffold');
|
|
532
|
+
} finally {
|
|
533
|
+
P.CONFIG.standaloneBase = origBase;
|
|
534
|
+
[cp, base].forEach((d) => rmSync(d, { recursive: true, force: true }));
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// ── scaffold push via the owner OAuth token (task 1002317) ────────────────────────
|
|
539
|
+
// Deploy keys are per-repo; a shared key can't write to a NEW project repo. Push with the
|
|
540
|
+
// owner's token (which created the repo) instead — WITHOUT leaking it into a logged command.
|
|
541
|
+
await ta('scaffoldStandaloneRepo: pushes via the owner OAuth token (not SSH), token never in a logged command (task 1002317)', async () => {
|
|
542
|
+
const base = mkdtempSync(join(tmpdir(), 'push-tok-'));
|
|
543
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
544
|
+
P.CONFIG.standaloneBase = base;
|
|
545
|
+
try {
|
|
546
|
+
const { deps, cmds } = recorder({ apply: true });
|
|
547
|
+
const writes = [];
|
|
548
|
+
deps.writeFile = (p, body, opts) => { writes.push({ p, body, opts }); return { ok: true }; };
|
|
549
|
+
deps.getGithubToken = async () => ({ token: 'gho_SECRET_TOKEN_XYZ', scope: 'public_repo' });
|
|
550
|
+
await P.scaffoldStandaloneRepo(
|
|
551
|
+
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo', owner_builder_id: 7 },
|
|
552
|
+
{ ...deps, instanceRoot: base },
|
|
553
|
+
);
|
|
554
|
+
const joined = cmds.join('\n');
|
|
555
|
+
// SECURITY: the token must NEVER appear in a command string (makeExec logs commands)
|
|
556
|
+
assert.ok(!joined.includes('gho_SECRET_TOKEN_XYZ'), 'token never appears in any command string');
|
|
557
|
+
// pushes over HTTPS via the credential-store helper to HEAD:main, NOT the SSH origin
|
|
558
|
+
assert.match(joined, /credential\.helper='store --file=[^']*bongos-gitcred-demo[^']*' push https:\/\/x-access-token@github\.com\/lars589\/demo\.git HEAD:main/, 'pushes via the token credential-store helper');
|
|
559
|
+
assert.ok(!/push -u origin main/.test(joined), 'does NOT use the SSH origin push when a token is available');
|
|
560
|
+
// the credential file is written chmod 600, carries the token (content, NOT logged), then removed
|
|
561
|
+
const cred = writes.find((w) => /bongos-gitcred-demo/.test(w.p));
|
|
562
|
+
assert.ok(cred, 'wrote a credential-store file');
|
|
563
|
+
assert.equal(cred.opts && cred.opts.mode, 0o600, 'credential file is chmod 600');
|
|
564
|
+
assert.ok(String(cred.body).includes('gho_SECRET_TOKEN_XYZ'), 'credential file carries the token in its CONTENT (makeWriteFile logs only the path + byte count)');
|
|
565
|
+
assert.match(joined, /rm -f [^\n]*bongos-gitcred-demo/, 'removes the credential file after the push');
|
|
566
|
+
} finally {
|
|
567
|
+
P.CONFIG.standaloneBase = origBase;
|
|
568
|
+
rmSync(base, { recursive: true, force: true });
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
await ta('scaffoldStandaloneRepo: no owner token → falls back to the SSH origin push (unchanged; task 1002317)', async () => {
|
|
573
|
+
const base = mkdtempSync(join(tmpdir(), 'push-ssh-'));
|
|
574
|
+
const origBase = P.CONFIG.standaloneBase;
|
|
575
|
+
P.CONFIG.standaloneBase = base;
|
|
576
|
+
try {
|
|
577
|
+
const { deps, cmds } = recorder({ apply: true }); // default getGithubToken → null
|
|
578
|
+
await P.scaffoldStandaloneRepo(
|
|
579
|
+
{ slug: 'demo', hosting_shape: 'standalone', target_ref: 'lars589/demo', owner_builder_id: 7 },
|
|
580
|
+
{ ...deps, instanceRoot: base },
|
|
581
|
+
);
|
|
582
|
+
const joined = cmds.join('\n');
|
|
583
|
+
assert.match(joined, /git -C [^\n]* push -u origin main/, 'falls back to the SSH origin push');
|
|
584
|
+
assert.ok(!/credential\.helper=/.test(joined), 'no token credential helper when there is no token');
|
|
585
|
+
} finally {
|
|
586
|
+
P.CONFIG.standaloneBase = origBase;
|
|
587
|
+
rmSync(base, { recursive: true, force: true });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
466
591
|
await ta('provisionInstance is idempotent: an already-active instance is a no-op', async () => {
|
|
467
592
|
const { deps, cmds } = recorder();
|
|
468
593
|
const r = await P.provisionInstance({ ...coTenant, status: 'active' }, deps);
|
|
@@ -914,7 +1039,7 @@ await ta('provisionInstance(standalone, remote): EVERY box step runs control-pla
|
|
|
914
1039
|
assert.ok(!box.some((c) => re.test(c)), `${re} does NOT cross to the co-hosting box`);
|
|
915
1040
|
}
|
|
916
1041
|
// The scaffold leg was already control-local; confirm it stayed there.
|
|
917
|
-
assert.ok(control.some((c) => /git
|
|
1042
|
+
assert.ok(control.some((c) => /git -C \S+ init -q/.test(c)) && control.some((c) => /push -u origin main/.test(c)), 'scaffold init+push stay control-local');
|
|
918
1043
|
// NOTHING crosses to the remote box for a standalone — the whole point of the fix.
|
|
919
1044
|
assert.equal(box.length, 0, 'zero remote (co-hosting box) commands for a standalone');
|
|
920
1045
|
assert.equal(boxWrites.length, 0, 'zero remote writes for a standalone');
|
|
@@ -969,5 +1094,162 @@ await ta('teardownInstance(standalone, remote): tears down on the CONTROL PLANE
|
|
|
969
1094
|
assert.equal(box.length, 0, 'nothing crosses to the co-hosting box for a standalone teardown');
|
|
970
1095
|
});
|
|
971
1096
|
|
|
1097
|
+
console.log('\ncross-site identity federation (ADR 0141 §4, task #1002109):');
|
|
1098
|
+
|
|
1099
|
+
// Fakes for the hub-side module handles provisioning is injected. hubKeysOn = a
|
|
1100
|
+
// hub-configured control plane; hubKeysOff = signing key unset. fakePI records every
|
|
1101
|
+
// registerClient call so we can assert the (security-critical) origin binding.
|
|
1102
|
+
const hubKeysOn = { hubSigningConfigured: () => true, hubPublicKeyB64: () => 'HUBPUBKEYB64==' };
|
|
1103
|
+
const hubKeysOff = { hubSigningConfigured: () => false, hubPublicKeyB64: () => null };
|
|
1104
|
+
function fakePI() { const calls = []; return { calls, registerClient: async (a) => { calls.push(a); return { client_id: a.clientId }; } }; }
|
|
1105
|
+
function withEnv(vars, fn) {
|
|
1106
|
+
const saved = {};
|
|
1107
|
+
for (const k of Object.keys(vars)) { saved[k] = process.env[k]; if (vars[k] === undefined) delete process.env[k]; else process.env[k] = vars[k]; }
|
|
1108
|
+
const restore = () => { for (const k of Object.keys(saved)) { if (saved[k] === undefined) delete process.env[k]; else process.env[k] = saved[k]; } };
|
|
1109
|
+
// Promise-aware: for an async fn, restore only AFTER it settles (a plain try/finally
|
|
1110
|
+
// would restore synchronously, before the awaited body ever reads the env).
|
|
1111
|
+
let result;
|
|
1112
|
+
try { result = fn(); } catch (e) { restore(); throw e; }
|
|
1113
|
+
if (result && typeof result.then === 'function') return result.then((v) => { restore(); return v; }, (e) => { restore(); throw e; });
|
|
1114
|
+
restore();
|
|
1115
|
+
return result;
|
|
1116
|
+
}
|
|
1117
|
+
const HUB_ENV = { PROVISION_HUB_ORIGIN: 'https://cloudbongos.com', [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined };
|
|
1118
|
+
const coTenantWithDomain = { ...coTenant, domain: 'mercury.example.com' };
|
|
1119
|
+
|
|
1120
|
+
t('federationHubOrigin: explicit PROVISION_HUB_ORIGIN wins; falls back to <PREFIX>_PUBLIC_ORIGIN; strips trailing slash; null when neither', () => {
|
|
1121
|
+
withEnv({ PROVISION_HUB_ORIGIN: 'https://hub.example/', [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: 'https://own.example' }, () => {
|
|
1122
|
+
assert.equal(P.federationHubOrigin(), 'https://hub.example', 'explicit wins + trailing slash stripped');
|
|
1123
|
+
});
|
|
1124
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: 'https://own.example/' }, () => {
|
|
1125
|
+
assert.equal(P.federationHubOrigin(), 'https://own.example', 'falls back to the control plane\'s own public origin');
|
|
1126
|
+
});
|
|
1127
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined }, () => {
|
|
1128
|
+
assert.equal(P.federationHubOrigin(), null, 'null when nothing is configured (→ not hub-configured)');
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
t('shouldFederate: dormant unless hub-configured AND co-tenant AND a domain is set', () => {
|
|
1133
|
+
withEnv(HUB_ENV, () => {
|
|
1134
|
+
// the happy path
|
|
1135
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, true, 'co-tenant + hub-configured + domain → federate');
|
|
1136
|
+
// each guard, in turn
|
|
1137
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: null, hubKeys: hubKeysOn }).federate, false, 'module absent (not the hub) → skip');
|
|
1138
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOff }).federate, false, 'no signing key → skip');
|
|
1139
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, domain: null }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'no domain → skip');
|
|
1140
|
+
// a standalone (owner-hosted) keeps its own GitHub app; a dedicated droplet has no local web.env bake yet
|
|
1141
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'standalone' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'standalone = self-hosted → skip');
|
|
1142
|
+
assert.equal(P.shouldFederate({ ...coTenantWithDomain, hosting_shape: 'dedicated' }, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'dedicated → skip (no local web.env yet)');
|
|
1143
|
+
});
|
|
1144
|
+
// no hub origin resolvable (even with a signing key) → skip
|
|
1145
|
+
withEnv({ PROVISION_HUB_ORIGIN: undefined, [`${NEUTRAL_PREFIX}_PUBLIC_ORIGIN`]: undefined }, () => {
|
|
1146
|
+
assert.equal(P.shouldFederate(coTenantWithDomain, { platformIdentity: fakePI(), hubKeys: hubKeysOn }).federate, false, 'no hub origin → skip');
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
t('webEnvBody(federation): flips <PREFIX>_AUTH_IDP + writes the HUB_* block; GitHub creds stay blank; NO block without federation', () => {
|
|
1151
|
+
const federation = { hubOrigin: 'https://cloudbongos.com', clientId: 'mercury', clientSecret: 'S'.repeat(43), hubPublicKey: 'PUB==' };
|
|
1152
|
+
const b = P.webEnvBody(coTenantWithDomain, { federation });
|
|
1153
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_AUTH_IDP=true`), 'flips the federation switch auth.js idpEnabled() reads');
|
|
1154
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_ORIGIN=https://cloudbongos\\.com`));
|
|
1155
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_ID=mercury`));
|
|
1156
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_SECRET=S{43}`));
|
|
1157
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_PUBLIC_KEY=PUB==`));
|
|
1158
|
+
// a federated instance needs no GitHub app of its own → those creds stay blank
|
|
1159
|
+
assert.match(b, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_GITHUB_CLIENT_ID=\\n`), 'GitHub id blank (federated)');
|
|
1160
|
+
// regression: a non-federated write carries NO hub block + NO AUTH_IDP flip
|
|
1161
|
+
const plain = P.webEnvBody(coTenantWithDomain);
|
|
1162
|
+
assert.doesNotMatch(plain, /_AUTH_IDP=/, 'default (unset) never flips federation on');
|
|
1163
|
+
assert.doesNotMatch(plain, /_HUB_ORIGIN=/, 'default write carries no hub block');
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
t('branding: <PREFIX>_AUTH_IDP env override flips auth.idp to a real boolean (idpEnabled reads === true)', () => {
|
|
1167
|
+
const requireB = createRequire(import.meta.url);
|
|
1168
|
+
const branding = requireB('../src/branding.js');
|
|
1169
|
+
const neutral = { envPrefix: NEUTRAL_PREFIX, llmProjectName: 'x', identity: { productName: 'x', worldName: 'w' }, domains: { publicOrigin: 'https://x', buildersOrigin: 'https://x', statusOrigin: 'https://x', oauthOrigin: 'https://x' }, auth: { idp: false } };
|
|
1170
|
+
const off = branding.resolveBranding({ neutral, instance: {}, env: {} });
|
|
1171
|
+
assert.equal(off.auth.idp, false, 'unset → stays false (dormant default)');
|
|
1172
|
+
const on = branding.resolveBranding({ neutral, instance: {}, env: { [`${NEUTRAL_PREFIX}_AUTH_IDP`]: 'true' } });
|
|
1173
|
+
assert.equal(on.auth.idp, true, 'the string "true" coerces to boolean true (not the string, which would fail === true)');
|
|
1174
|
+
assert.strictEqual(typeof on.auth.idp, 'boolean', 'coerced to a real boolean');
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
await ta('federateInstance(--apply): registers the client on the hub bound to the EXACT origin (the #1002108 takeover fix), returns the env block', async () => {
|
|
1178
|
+
await withEnv(HUB_ENV, async () => {
|
|
1179
|
+
const pi = fakePI();
|
|
1180
|
+
const deps = { apply: true, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [{ github_id: 'gh-owner-1' }] }) } };
|
|
1181
|
+
const block = await P.federateInstance(coTenantWithDomain, deps);
|
|
1182
|
+
assert.equal(pi.calls.length, 1, 'registered exactly once');
|
|
1183
|
+
const reg = pi.calls[0];
|
|
1184
|
+
assert.equal(reg.origin, 'https://mercury.example.com', 'origin is the instance EXACT public origin (hub binds the mint audience to it)');
|
|
1185
|
+
assert.equal(reg.clientId, 'mercury', 'client_id is the stable slug (re-provision rotates the same row)');
|
|
1186
|
+
assert.ok(reg.redirectUris.includes('https://mercury.example.com/api/gds/auth/web/callback'), 'registers the exact browser callback the instance sends');
|
|
1187
|
+
assert.equal(reg.ownerGithubId, 'gh-owner-1', 'threads the owner github_id for the catalog');
|
|
1188
|
+
assert.ok(typeof reg.secret === 'string' && reg.secret.length >= 32, 'a strong per-client secret was generated');
|
|
1189
|
+
// the block webEnvBody bakes in
|
|
1190
|
+
assert.equal(block.hubOrigin, 'https://cloudbongos.com');
|
|
1191
|
+
assert.equal(block.clientId, 'mercury');
|
|
1192
|
+
assert.equal(block.clientSecret, reg.secret, 'the secret written to the instance is the one registered on the hub');
|
|
1193
|
+
assert.equal(block.hubPublicKey, 'HUBPUBKEYB64==', 'the hub PUBLIC key (for offline assertion verify) rides along');
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
await ta('federateInstance(dry-run): registers NOTHING, returns a placeholder block (so webEnvBody can still compose)', async () => {
|
|
1198
|
+
await withEnv(HUB_ENV, async () => {
|
|
1199
|
+
const pi = fakePI();
|
|
1200
|
+
const block = await P.federateInstance(coTenantWithDomain, { apply: false, log: () => {}, platformIdentity: pi, hubKeys: hubKeysOn, db: { query: async () => ({ rows: [] }) } });
|
|
1201
|
+
assert.equal(pi.calls.length, 0, 'dry-run performs no hub write');
|
|
1202
|
+
assert.equal(block.clientId, 'mercury');
|
|
1203
|
+
assert.equal(block.hubPublicKey, 'HUBPUBKEYB64==');
|
|
1204
|
+
});
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
await ta('provisionInstance(co-tenant, hub-configured): bakes the HUB block into web.env, records a "federated" event, registers the client', async () => {
|
|
1208
|
+
await withEnv(HUB_ENV, async () => {
|
|
1209
|
+
const { deps } = recorder({ apply: true });
|
|
1210
|
+
const bodies = {}; const events = []; const pi = fakePI();
|
|
1211
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1212
|
+
deps.provisioning.recordEvent = async (_db, e) => { events.push(e.event); };
|
|
1213
|
+
deps.platformIdentity = pi; deps.hubKeys = hubKeysOn;
|
|
1214
|
+
await P.provisionInstance(coTenantWithDomain, deps);
|
|
1215
|
+
const env = bodies[P.webEnvPath(coTenantWithDomain)];
|
|
1216
|
+
assert.ok(env, 'wrote the per-instance web.env');
|
|
1217
|
+
assert.match(env, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_AUTH_IDP=true`), 'the co-tenant comes up already federated');
|
|
1218
|
+
assert.match(env, new RegExp(`${reEsc(NEUTRAL_PREFIX)}_HUB_CLIENT_SECRET=`), 'hub creds baked into web.env');
|
|
1219
|
+
assert.ok(events.includes('federated'), 'records the federation event for the audit trail');
|
|
1220
|
+
assert.equal(pi.calls.length, 1, 'registered the client on the hub');
|
|
1221
|
+
assert.equal(pi.calls[0].origin, 'https://mercury.example.com');
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
await ta('provisionInstance: a NON-hub control plane (no module/key) does NOT federate — the instance owns its auth (unchanged)', async () => {
|
|
1226
|
+
await withEnv(HUB_ENV, async () => {
|
|
1227
|
+
const { deps } = recorder({ apply: true });
|
|
1228
|
+
const bodies = {};
|
|
1229
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1230
|
+
// default recorder has no platformIdentity/hubKeys → shouldFederate skips
|
|
1231
|
+
await P.provisionInstance(coTenantWithDomain, deps);
|
|
1232
|
+
const env = bodies[P.webEnvPath(coTenantWithDomain)];
|
|
1233
|
+
assert.ok(env, 'wrote web.env');
|
|
1234
|
+
assert.doesNotMatch(env, /_AUTH_IDP=/, 'no federation flip on a non-hub control plane');
|
|
1235
|
+
assert.doesNotMatch(env, /_HUB_ORIGIN=/, 'no hub block — the direct-GitHub path, byte-identical to today');
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
await ta('provisionInstance(standalone, hub-configured): still NOT federated — owner-hosted keeps its own GitHub app', async () => {
|
|
1240
|
+
await withEnv(HUB_ENV, async () => {
|
|
1241
|
+
const { deps } = recorder({ apply: true });
|
|
1242
|
+
const bodies = {}; const pi = fakePI();
|
|
1243
|
+
deps.controlWriteFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1244
|
+
deps.writeFile = (p, body) => { bodies[p] = body; return { ok: true }; };
|
|
1245
|
+
deps.platformIdentity = pi; deps.hubKeys = hubKeysOn;
|
|
1246
|
+
await P.provisionInstance({ ...coTenantWithDomain, hosting_shape: 'standalone', target_ref: 'lars589/mercury' }, deps);
|
|
1247
|
+
const env = bodies[P.webEnvPath({ ...coTenantWithDomain, hosting_shape: 'standalone' })];
|
|
1248
|
+
assert.ok(env, 'wrote web.env');
|
|
1249
|
+
assert.doesNotMatch(env, /_AUTH_IDP=/, 'standalone is the self-hosted path — no federation');
|
|
1250
|
+
assert.equal(pi.calls.length, 0, 'no hub registration for a standalone');
|
|
1251
|
+
});
|
|
1252
|
+
});
|
|
1253
|
+
|
|
972
1254
|
console.log(`\nprovision: ${passed} passed, ${failed} failed`);
|
|
973
1255
|
process.exit(failed ? 1 : 0);
|