@bongos/core 1.18.3 → 1.18.19
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 +1576 -286
- 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 +4 -2
- 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 +6 -1
- 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/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 +10 -0
- package/docs/api/openapi.json +843 -17
- package/docs/api-reference.md +23 -10
- 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 +8 -0
- 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/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/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/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/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/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/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 +51 -58
- 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 -27
- 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/login.js +16 -1
- package/scripts/gds/paste-token.js +7 -1
- package/scripts/gds/preflight.js +126 -0
- package/scripts/gds/provision.js +123 -12
- package/scripts/gds/publish-manifest.js +7 -1
- package/scripts/gds/reauth.js +7 -3
- package/scripts/gds/regen-instance-docs.js +47 -0
- package/scripts/gds/run-unit-tests.js +4 -2
- 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 +31 -5
- package/scripts/gds/shell.js +5 -3
- package/scripts/gds/ship.js +319 -29
- 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 +124 -2
- package/scripts/migrate.sh +99 -14
- package/scripts/status-mirror-sync.js +114 -84
- package/src/bongos/api-errors.js +1 -0
- package/src/bongos/auth.js +447 -102
- package/src/bongos/db-kernel.js +27 -0
- package/src/bongos/db.js +39 -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 +219 -2
- package/src/bongos/routes/backup.js +58 -2
- package/src/bongos/routes/me.js +35 -1
- package/src/bongos/serve-internal.js +84 -15
- package/src/branding.js +32 -0
- package/src/instance-config.js +30 -0
- package/src/module-api.js +124 -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 +12 -0
- 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/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 +93 -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/migrate_root_resolution.mjs +124 -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/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 +132 -7
- 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 +80 -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 +139 -0
- package/tests/web_admission_status.mjs +162 -0
- package/tests/write_validation_gaps.mjs +110 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// tests/hall_signout.mjs — the hall sign-out contract (task 1002245).
|
|
2
|
+
//
|
|
3
|
+
// Regression guard for an owner-reported bug: signing out on any hall page
|
|
4
|
+
// OTHER than the index left the builder signed in, and never reached the hub.
|
|
5
|
+
// Root cause — the real sign-out (POST /auth/logout: revoke session + clear
|
|
6
|
+
// cookies + emit the federated single-logout, ADR 0144) was wired only in the
|
|
7
|
+
// index script (builders.js); every other page rendered the sign-out link via
|
|
8
|
+
// shell.js as a bare <a href="/api/bongos/auth/logout">, which is a GET to a
|
|
9
|
+
// POST-only endpoint — a dead click that never revoked anything.
|
|
10
|
+
//
|
|
11
|
+
// The fix centralised the handler in shell.js (which draws the sign-out control
|
|
12
|
+
// on EVERY page). This test EXECUTES the real shell.js in a minimal DOM stub and
|
|
13
|
+
// drives a click, proving: a sign-out anchor fires exactly one POST /auth/logout,
|
|
14
|
+
// expires gds_session (host-only + apex) and legacy pms_session, prevents the
|
|
15
|
+
// default GET navigation, and bounces home — and that ordinary links are left
|
|
16
|
+
// alone. Pure vm + file-read (no DB/network) — runs in the DB-free unit gate.
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { test } from 'node:test';
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import vm from 'node:vm';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/shell.js'), 'utf8');
|
|
26
|
+
|
|
27
|
+
// Boot shell.js under a permissive DOM stub for a given non-index page, then
|
|
28
|
+
// dispatch a single click carrying `href` through the delegated listener wire()
|
|
29
|
+
// registered. Returns the recorded side effects.
|
|
30
|
+
function driveClick({ page = 'settings', href } = {}) {
|
|
31
|
+
const calls = { requests: [], cookies: [], assign: [], prevented: 0 };
|
|
32
|
+
const docListeners = {};
|
|
33
|
+
|
|
34
|
+
const makeEl = () => ({
|
|
35
|
+
dataset: {}, style: {}, title: '', textContent: '', hidden: false,
|
|
36
|
+
classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } },
|
|
37
|
+
setAttribute() {}, getAttribute() { return null; }, removeAttribute() {},
|
|
38
|
+
addEventListener() {}, querySelectorAll() { return []; },
|
|
39
|
+
appendChild() {}, focus() {}, click() {},
|
|
40
|
+
set innerHTML(_v) {}, get innerHTML() { return ''; },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const location = {
|
|
44
|
+
hostname: 'builders.cloudbongos.com',
|
|
45
|
+
origin: 'https://builders.cloudbongos.com',
|
|
46
|
+
pathname: '/' + page,
|
|
47
|
+
assign(u) { calls.assign.push(u); },
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const api = {
|
|
51
|
+
request(method, p) {
|
|
52
|
+
calls.requests.push([method, p]);
|
|
53
|
+
return Promise.resolve({ status: 200, ok: true, data: { builder: { rank: 'metic' }, onboarding: { stages: [] } } });
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const documentObj = {
|
|
58
|
+
body: { dataset: { page } },
|
|
59
|
+
documentElement: { dataset: {}, style: {}, classList: { add() {}, remove() {}, toggle() { return false; }, contains() { return false; } } },
|
|
60
|
+
getElementById() { return makeEl(); },
|
|
61
|
+
querySelectorAll() { return []; },
|
|
62
|
+
createElement() { return makeEl(); },
|
|
63
|
+
addEventListener(type, fn) { (docListeners[type] || (docListeners[type] = [])).push(fn); },
|
|
64
|
+
get cookie() { return ''; },
|
|
65
|
+
set cookie(v) { calls.cookies.push(v); },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const windowObj = {
|
|
69
|
+
__BRANDING__: {}, __MODULES__: { enabled: {} },
|
|
70
|
+
BongosClient: { createClient: () => api },
|
|
71
|
+
matchMedia: () => ({ matches: false, addEventListener() {} }),
|
|
72
|
+
localStorage: { getItem() { return null; }, setItem() {} },
|
|
73
|
+
location,
|
|
74
|
+
addEventListener() {},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const sandbox = { window: windowObj, document: documentObj, location, console, setTimeout, clearTimeout };
|
|
78
|
+
sandbox.globalThis = sandbox;
|
|
79
|
+
vm.createContext(sandbox);
|
|
80
|
+
vm.runInContext(SRC, sandbox, { filename: 'shell.js' });
|
|
81
|
+
|
|
82
|
+
const clickListeners = docListeners.click || [];
|
|
83
|
+
assert.ok(clickListeners.length > 0, 'wire() must register a document click listener');
|
|
84
|
+
|
|
85
|
+
const anchor = { getAttribute(name) { return name === 'href' ? href : null; } };
|
|
86
|
+
const ev = { target: { closest(sel) { return sel === 'a' ? anchor : null; } }, preventDefault() { calls.prevented++; } };
|
|
87
|
+
for (const fn of clickListeners) fn(ev);
|
|
88
|
+
return calls;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Let the .then(finish, finish) in doLogout settle.
|
|
92
|
+
const flush = () => new Promise((r) => setTimeout(r, 0));
|
|
93
|
+
|
|
94
|
+
test('non-index sign-out click fires the real POST /auth/logout (not a dead GET)', async () => {
|
|
95
|
+
const calls = driveClick({ page: 'settings', href: '/api/bongos/auth/logout' });
|
|
96
|
+
await flush();
|
|
97
|
+
assert.ok(calls.prevented >= 1, 'the default GET navigation is prevented');
|
|
98
|
+
const logoutPosts = calls.requests.filter(([m, p]) => m === 'POST' && /\/auth\/logout$/.test(p));
|
|
99
|
+
assert.equal(logoutPosts.length, 1, `exactly one POST /auth/logout — got ${JSON.stringify(calls.requests)}`);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('sign-out clears both session cookies (host-only + apex) and bounces home', async () => {
|
|
103
|
+
const calls = driveClick({ page: 'settings', href: '/api/bongos/auth/logout' });
|
|
104
|
+
await flush();
|
|
105
|
+
assert.ok(calls.cookies.some((c) => /gds_session=;/.test(c) && /Max-Age=0/.test(c)), 'gds_session expired (host-only)');
|
|
106
|
+
assert.ok(calls.cookies.some((c) => /gds_session=;/.test(c) && /Domain=\.cloudbongos\.com/.test(c)), 'gds_session expired (apex-scoped)');
|
|
107
|
+
assert.ok(calls.cookies.some((c) => /pms_session=;/.test(c)), 'legacy pms_session expired');
|
|
108
|
+
assert.deepEqual(calls.assign, ['/'], 'redirected home after logout');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('the footer sign-out link (no id, bare href) is intercepted too', async () => {
|
|
112
|
+
// Same delegated match — the footer anchor renders as href="/api/bongos/auth/logout".
|
|
113
|
+
const calls = driveClick({ page: 'ranks', href: '/api/bongos/auth/logout' });
|
|
114
|
+
await flush();
|
|
115
|
+
assert.equal(calls.requests.filter(([m, p]) => m === 'POST' && /\/auth\/logout$/.test(p)).length, 1, 'footer sign-out also POSTs');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('ordinary hall links are left untouched (no logout, no redirect)', async () => {
|
|
119
|
+
const calls = driveClick({ page: 'settings', href: '/builders/ranks' });
|
|
120
|
+
await flush();
|
|
121
|
+
assert.equal(calls.prevented, 0, 'a normal link is not intercepted');
|
|
122
|
+
assert.equal(calls.requests.filter(([, p]) => /\/auth\/logout$/.test(p)).length, 0, 'no logout POST for a normal link');
|
|
123
|
+
assert.deepEqual(calls.assign, [], 'no redirect for a normal link');
|
|
124
|
+
});
|
|
@@ -36,6 +36,16 @@ const hookFiles = fs.existsSync(HOOKS_DIR)
|
|
|
36
36
|
? fs.readdirSync(HOOKS_DIR).filter((f) => f.endsWith('.js')).sort()
|
|
37
37
|
: [];
|
|
38
38
|
|
|
39
|
+
// The neutral core does NOT carry .claude/hooks/ — it's instance-materialized and
|
|
40
|
+
// package-excluded (PROTECTED_TOPOLOGY_EXCLUDE in scripts/gds/publish-manifest.js),
|
|
41
|
+
// so a clean core checkout has no hooks to scan. Skip cleanly rather than fail the
|
|
42
|
+
// assertions below (task 1002234, same class as tests/stop_refcheck.mjs's absent-hook
|
|
43
|
+
// crash); this UA invariant is enforced on an instance whose hooks are materialized.
|
|
44
|
+
if (hookFiles.length === 0) {
|
|
45
|
+
console.log('hook_user_agents: SKIP — no .claude/hooks/*.js in this checkout (instance-materialized; not in the neutral core).');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
t('.claude/hooks exists and has hook files to check', () => {
|
|
40
50
|
assert.ok(hookFiles.length > 0, `no hook .js files found under ${HOOKS_DIR}`);
|
|
41
51
|
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// tests/idea_triage.mjs
|
|
2
|
+
//
|
|
3
|
+
// task 1002447 — the guards `updateIdea` puts in front of a MERGE verdict, and
|
|
4
|
+
// the SQL a triage verdict actually writes.
|
|
5
|
+
//
|
|
6
|
+
// Merge is the verb that gained storage in migration core_195: before it,
|
|
7
|
+
// `status='merged'` recorded that a merge happened but never what it merged
|
|
8
|
+
// into (`promoted_to_task_id` could not stand in — it is REFERENCES tasks(id)).
|
|
9
|
+
// These guards are what stop the new column from collecting nonsense: an idea
|
|
10
|
+
// merged into itself, into an idea that does not exist, or into one that was
|
|
11
|
+
// itself already merged (a chain that reads as "folded into X" while X points
|
|
12
|
+
// somewhere else again).
|
|
13
|
+
//
|
|
14
|
+
// DB-free: `modules/ideas/inbox.js` destructures `pool` off the doorway at
|
|
15
|
+
// require time, so patching `pool.query` on that same object — BEFORE requiring
|
|
16
|
+
// inbox.js — intercepts every query. No live Postgres needed.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/idea_triage.mjs
|
|
19
|
+
|
|
20
|
+
import { strict as assert } from 'node:assert';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
|
|
25
|
+
const api = require('../src/module-api.js');
|
|
26
|
+
|
|
27
|
+
// The fake table every query below reads from, plus a log of what was written.
|
|
28
|
+
let rows = new Map();
|
|
29
|
+
let queries = [];
|
|
30
|
+
|
|
31
|
+
api.pool.query = async (sql, params = []) => {
|
|
32
|
+
queries.push({ sql, params });
|
|
33
|
+
if (/^\s*SELECT/i.test(sql)) {
|
|
34
|
+
const row = rows.get(String(params[0]));
|
|
35
|
+
return { rows: row ? [row] : [] };
|
|
36
|
+
}
|
|
37
|
+
// UPDATE … RETURNING — apply nothing, just echo a plausible row back so the
|
|
38
|
+
// caller's return value is shaped right.
|
|
39
|
+
const row = rows.get(String(params[0]));
|
|
40
|
+
return { rows: row ? [{ ...row }] : [] };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const inbox = require('../modules/ideas/inbox.js');
|
|
44
|
+
|
|
45
|
+
let passed = 0;
|
|
46
|
+
let failed = 0;
|
|
47
|
+
|
|
48
|
+
async function test(name, fn) {
|
|
49
|
+
queries = [];
|
|
50
|
+
rows = new Map([
|
|
51
|
+
['1', { id: '1', title: 'the duplicate', status: 'open', merged_into_idea_id: null }],
|
|
52
|
+
['2', { id: '2', title: 'the canonical one', status: 'open', merged_into_idea_id: null }],
|
|
53
|
+
['3', { id: '3', title: 'already folded away', status: 'merged', merged_into_idea_id: '2' }],
|
|
54
|
+
]);
|
|
55
|
+
try {
|
|
56
|
+
await fn();
|
|
57
|
+
passed += 1;
|
|
58
|
+
console.log(` ok ${name}`);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
failed += 1;
|
|
61
|
+
console.log(` FAIL ${name}\n ${err.message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function rejectsWith(code, fn) {
|
|
66
|
+
try {
|
|
67
|
+
await fn();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
assert.equal(err.code, code, `expected ${code}, got ${err.code || err.message}`);
|
|
70
|
+
return err;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`expected ${code}, but the call resolved`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// -- merge guards ----------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
await test('merging an idea into itself is refused', () =>
|
|
78
|
+
rejectsWith('MERGE_SELF', () => inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: 1 })));
|
|
79
|
+
|
|
80
|
+
await test('merging into an idea that does not exist is refused', () =>
|
|
81
|
+
rejectsWith('MERGE_TARGET_NOT_FOUND', () => inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: 99 })));
|
|
82
|
+
|
|
83
|
+
await test('merging into an already-merged idea is refused (no chains)', async () => {
|
|
84
|
+
const err = await rejectsWith('MERGE_TARGET_MERGED', () =>
|
|
85
|
+
inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: 3 }));
|
|
86
|
+
// The error names where the chain actually ends, so the caller can retarget.
|
|
87
|
+
assert.equal(err.merged_into, '2');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await test('merging into a live idea writes both the status and the target', async () => {
|
|
91
|
+
await inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: 2, triage_note: 'same ask' });
|
|
92
|
+
const update = queries.find((q) => /^\s*UPDATE/i.test(q.sql));
|
|
93
|
+
assert.ok(update, 'no UPDATE ran');
|
|
94
|
+
assert.match(update.sql, /merged_into_idea_id = \$\d+/);
|
|
95
|
+
assert.match(update.sql, /triage_note = \$\d+/);
|
|
96
|
+
assert.match(update.sql, /status = \$\d+/);
|
|
97
|
+
assert.ok(update.params.includes(2), 'the merge target never reached the query');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// A null clears the link — the un-merge path — and must skip the guards rather
|
|
101
|
+
// than trip MERGE_TARGET_NOT_FOUND on a lookup of `null`.
|
|
102
|
+
await test('clearing the merge link with null skips the target guards', async () => {
|
|
103
|
+
await inbox.updateIdea('1', { status: 'open', merged_into_idea_id: null });
|
|
104
|
+
assert.equal(queries.filter((q) => /^\s*SELECT/i.test(q.sql)).length, 0, 'looked up a null target');
|
|
105
|
+
const update = queries.find((q) => /^\s*UPDATE/i.test(q.sql));
|
|
106
|
+
assert.ok(update.params.includes(null));
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// -- the status/target invariant (a target exists IFF the idea is merged) ---
|
|
110
|
+
//
|
|
111
|
+
// Without it, two ordinary PATCHes build the chain the no-chains guard exists to
|
|
112
|
+
// stop: point A at B while leaving A open, then merge C into A — whose status
|
|
113
|
+
// still reads 'open'. The DB CHECK in core_195 is the backstop; these are the
|
|
114
|
+
// clean 400s in front of it.
|
|
115
|
+
|
|
116
|
+
await test('setting a merge target without status=merged is refused', () =>
|
|
117
|
+
rejectsWith('MERGE_STATUS_REQUIRED', () => inbox.updateIdea('1', { merged_into_idea_id: 2 })));
|
|
118
|
+
|
|
119
|
+
await test('un-merging while still claiming merged status is refused', () =>
|
|
120
|
+
rejectsWith('MERGE_STATUS_REQUIRED', () => inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: null })));
|
|
121
|
+
|
|
122
|
+
await test('the no-chains guard reads the target COLUMN, not the target status', async () => {
|
|
123
|
+
// idea 4: pointed at 2 but left 'open' — the exact row the old status-based
|
|
124
|
+
// guard waved through.
|
|
125
|
+
rows.set('4', { id: '4', title: 'mis-set row', status: 'open', merged_into_idea_id: '2' });
|
|
126
|
+
await rejectsWith('MERGE_TARGET_MERGED', () =>
|
|
127
|
+
inbox.updateIdea('1', { status: 'merged', merged_into_idea_id: 4 }));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// -- the discard reason ----------------------------------------------------
|
|
131
|
+
|
|
132
|
+
await test('a discard reason is written as the triage note', async () => {
|
|
133
|
+
await inbox.updateIdea('1', { status: 'discarded', triage_note: 'superseded by the new gate' });
|
|
134
|
+
const update = queries.find((q) => /^\s*UPDATE/i.test(q.sql));
|
|
135
|
+
assert.ok(update.params.includes('superseded by the new gate'));
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
await test('an oversize triage note is refused before it reaches the DB', async () => {
|
|
139
|
+
await rejectsWith('TRIAGE_NOTE_TOO_LONG', () =>
|
|
140
|
+
inbox.updateIdea('1', { status: 'discarded', triage_note: 'x'.repeat(1001) }));
|
|
141
|
+
assert.equal(queries.length, 0, 'a rejected note still hit the database');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// -- the reads carry the new columns ---------------------------------------
|
|
145
|
+
|
|
146
|
+
await test('getIdea selects the triage-provenance columns', async () => {
|
|
147
|
+
await inbox.getIdea('1');
|
|
148
|
+
const select = queries.find((q) => /^\s*SELECT/i.test(q.sql));
|
|
149
|
+
assert.match(select.sql, /merged_into_idea_id/);
|
|
150
|
+
assert.match(select.sql, /triage_note/);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await test('listOpenIdeas selects the triage-provenance columns', async () => {
|
|
154
|
+
api.pool.query = async (sql, params = []) => { queries.push({ sql, params }); return { rows: [] }; };
|
|
155
|
+
await inbox.listOpenIdeas({ limit: 5 });
|
|
156
|
+
const select = queries.find((q) => /^\s*SELECT/i.test(q.sql));
|
|
157
|
+
assert.match(select.sql, /merged_into_idea_id/);
|
|
158
|
+
assert.match(select.sql, /triage_note/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
console.log(`\nIdea triage tests: ${passed} passed, ${failed} failed`);
|
|
162
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -101,5 +101,98 @@ t('generateHubKeypair yields distinct keypairs', () => {
|
|
|
101
101
|
assert.notEqual(a.publicKeyB64, b.publicKeyB64);
|
|
102
102
|
});
|
|
103
103
|
|
|
104
|
+
// --- Federated single-logout token (ADR 0144, task 1002238) ----------------
|
|
105
|
+
// The hub mints a back-channel LOGOUT token (hub-keys.js mintHubLogoutToken) and
|
|
106
|
+
// the instance verifies it OFFLINE (auth.js verifyHubLogoutToken) before revoking
|
|
107
|
+
// the builder's local sessions. Same Ed25519 envelope as the login assertion, so
|
|
108
|
+
// it must survive the SAME adversarial battery — PLUS the two token types must be
|
|
109
|
+
// mutually non-interchangeable (a login assertion can't trigger a logout, and a
|
|
110
|
+
// logout token can't mint a session).
|
|
111
|
+
|
|
112
|
+
function mintLogout(overrides = {}, opts = {}) {
|
|
113
|
+
return hubKeys.mintHubLogoutToken(
|
|
114
|
+
{ clientId: 'client-x', githubId: 42, ...overrides },
|
|
115
|
+
{ signingKey, ...opts },
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('\nhub logout token mint → instance verify (roundtrip):');
|
|
120
|
+
|
|
121
|
+
t('a valid logout token verifies, carries the subject + purpose, NO identity/rank', () => {
|
|
122
|
+
const p = auth.verifyHubLogoutToken(mintLogout(), { publicKey, clientId: 'client-x' });
|
|
123
|
+
assert.equal(p.sub, '42');
|
|
124
|
+
assert.equal(p.purpose, 'backchannel-logout');
|
|
125
|
+
assert.equal(p.aud, 'client-x');
|
|
126
|
+
assert.equal(p.iss, 'cloudbongos-hub');
|
|
127
|
+
assert.equal(p.github_login, undefined, 'a logout token must carry no identity');
|
|
128
|
+
assert.equal(p.rank, undefined);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
console.log('\nadversarial (logout) — every one of these MUST be rejected:');
|
|
132
|
+
|
|
133
|
+
t('wrong audience (a logout for instance A cannot be replayed against instance B)', () => {
|
|
134
|
+
assert.throws(() => auth.verifyHubLogoutToken(mintLogout(), { publicKey, clientId: 'other-client' }), /audience/);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
t('expired logout token (minted 10 min ago; TTL is ~120s)', () => {
|
|
138
|
+
const stale = mintLogout({}, { nowMs: Date.now() - 10 * 60 * 1000 });
|
|
139
|
+
assert.throws(() => auth.verifyHubLogoutToken(stale, { publicKey, clientId: 'client-x' }), /expired/);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
t('tampered payload (swap sub to another github_id) fails the signature', () => {
|
|
143
|
+
const [h, p, s] = mintLogout().split('.');
|
|
144
|
+
const payload = JSON.parse(Buffer.from(p, 'base64url').toString('utf8'));
|
|
145
|
+
payload.sub = '999';
|
|
146
|
+
const forged = `${h}.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.${s}`;
|
|
147
|
+
assert.throws(() => auth.verifyHubLogoutToken(forged, { publicKey, clientId: 'client-x' }), /signature/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
t('a different signing key is rejected', () => {
|
|
151
|
+
const otherPub = crypto.generateKeyPairSync('ed25519').publicKey;
|
|
152
|
+
assert.throws(() => auth.verifyHubLogoutToken(mintLogout(), { publicKey: otherPub, clientId: 'client-x' }), /signature/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
t('algorithm-confusion (alg → "none") is rejected', () => {
|
|
156
|
+
const [h, p, s] = mintLogout().split('.');
|
|
157
|
+
const hdr = JSON.parse(Buffer.from(h, 'base64url').toString('utf8'));
|
|
158
|
+
hdr.alg = 'none';
|
|
159
|
+
const forged = `${Buffer.from(JSON.stringify(hdr)).toString('base64url')}.${p}.${s}`;
|
|
160
|
+
assert.throws(() => auth.verifyHubLogoutToken(forged, { publicKey, clientId: 'client-x' }), /alg|signature/);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
t('malformed + missing-pubkey logout tokens are rejected (fail closed)', () => {
|
|
164
|
+
assert.throws(() => auth.verifyHubLogoutToken('not-a-jwt', { publicKey, clientId: 'client-x' }), /malformed/);
|
|
165
|
+
assert.throws(() => auth.verifyHubLogoutToken(mintLogout(), { publicKey: null, clientId: 'client-x' }), /public key/);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
t('mintHubLogoutToken refuses without a signing key', () => {
|
|
169
|
+
assert.throws(() => hubKeys.mintHubLogoutToken({ clientId: 'c', githubId: 1 }, {}), /signing key/);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
console.log('\nlogin/logout tokens are mutually NON-interchangeable:');
|
|
173
|
+
|
|
174
|
+
t('a LOGIN assertion is NOT accepted as a logout token (no purpose)', () => {
|
|
175
|
+
assert.throws(() => auth.verifyHubLogoutToken(mint(), { publicKey, clientId: 'client-x' }), /purpose|logout/);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
t('a LOGOUT token is NOT accepted as a login assertion (no identity)', () => {
|
|
179
|
+
assert.throws(() => auth.verifyHubAssertion(mintLogout(), { publicKey, clientId: 'client-x' }), /identity/);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
console.log('\nlogout-token anti-replay (jti honored once):');
|
|
183
|
+
|
|
184
|
+
t('a logout jti is fresh on first use, seen on replay, and evicted after expiry', () => {
|
|
185
|
+
const now = 1_000_000;
|
|
186
|
+
const exp = Math.floor(now / 1000) + 120; // seconds, ~120s out
|
|
187
|
+
assert.equal(auth.logoutTokenJtiSeenAndRecord('jti-abc', exp, now), false, 'first use is fresh (→ revoke)');
|
|
188
|
+
assert.equal(auth.logoutTokenJtiSeenAndRecord('jti-abc', exp, now), true, 'replay within TTL is seen (→ no-op)');
|
|
189
|
+
// A different jti is independent.
|
|
190
|
+
assert.equal(auth.logoutTokenJtiSeenAndRecord('jti-xyz', exp, now), false, 'a distinct jti is fresh');
|
|
191
|
+
// After the TTL passes, the entry is swept and the jti is fresh again (bounded memory).
|
|
192
|
+
assert.equal(auth.logoutTokenJtiSeenAndRecord('jti-abc', exp, now + 200_000), false, 'expired jti is evicted → fresh');
|
|
193
|
+
// No jti → cannot dedupe (aud + TTL remain the guards), never reported as seen.
|
|
194
|
+
assert.equal(auth.logoutTokenJtiSeenAndRecord(undefined, exp, now), false, 'missing jti is never "seen"');
|
|
195
|
+
});
|
|
196
|
+
|
|
104
197
|
console.log(`\nidp_device_exchange: ${passed} passed, ${failed} failed`);
|
|
105
198
|
if (failed > 0) process.exit(1);
|
package/tests/init.mjs
CHANGED
|
@@ -445,6 +445,57 @@ test('writeConfigs (greenfield, dry-run): writes nothing to disk', () => {
|
|
|
445
445
|
}
|
|
446
446
|
});
|
|
447
447
|
|
|
448
|
+
// ---- README START-HERE (task 1002404 / goal 1000052) ----
|
|
449
|
+
test('buildReadme: names the first-run sequence in order (npm install → bongos dev → start)', () => {
|
|
450
|
+
const md = init.buildReadme(init.buildBrandingConfig(fullSpec()));
|
|
451
|
+
assert.ok(md.startsWith('# Cloud Bongos'), 'leads with the product-name heading');
|
|
452
|
+
const iNpm = md.indexOf('npm install');
|
|
453
|
+
const iDev = md.indexOf('bongos dev');
|
|
454
|
+
const iStart = md.indexOf('bongos start');
|
|
455
|
+
assert.ok(iNpm > 0 && iDev > iNpm && iStart > iDev, 'steps in order: npm install → bongos dev → bongos start');
|
|
456
|
+
assert.ok(/@bongos\/core/.test(md), 'explains WHY npm install is first (CLI ships inside @bongos/core)');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('buildReadme: login step names the instance origin when known, else a placeholder', () => {
|
|
460
|
+
const withOrigin = init.buildReadme({ identity: { productName: 'X' }, domains: { publicOrigin: 'http://localhost:3007' } });
|
|
461
|
+
assert.ok(withOrigin.includes('bongos login http://localhost:3007'), 'uses the configured origin');
|
|
462
|
+
const noOrigin = init.buildReadme({ identity: { productName: 'X' } });
|
|
463
|
+
assert.ok(noOrigin.includes('bongos login <your-instance-url>'), 'placeholder when origin unknown');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test('buildReadme: no bare #N (linkify rule) and no unfilled holes', () => {
|
|
467
|
+
const md = init.buildReadme(init.buildBrandingConfig(fullSpec()));
|
|
468
|
+
assert.ok(!/#\d/.test(md), 'no bare #N that would mis-autolink to a GitHub issue');
|
|
469
|
+
assert.ok(!/undefined|\[object Object\]/.test(md), 'no undefined / stringified-object holes');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test('writeConfigs (greenfield): writes a root README START-HERE, never clobbering an existing one', () => {
|
|
473
|
+
const dir = mkdtempSync(join(tmpdir(), 'greenfield-'));
|
|
474
|
+
try {
|
|
475
|
+
const res = init.writeConfigs(fullSpec(), { dir, force: false, dryRun: false, log: () => {} });
|
|
476
|
+
assert.ok(existsSync(res.readmePath), 'README.md written');
|
|
477
|
+
const md = readFileSync(res.readmePath, 'utf8');
|
|
478
|
+
assert.ok(/## Start here/.test(md) && md.includes('npm install'), 'is the START-HERE guide');
|
|
479
|
+
|
|
480
|
+
// A --force re-init must NOT clobber a README the owner has since edited.
|
|
481
|
+
writeFileSync(res.readmePath, '# my own readme\n');
|
|
482
|
+
init.writeConfigs(fullSpec(), { dir, force: true, dryRun: false, log: () => {} });
|
|
483
|
+
assert.equal(readFileSync(res.readmePath, 'utf8'), '# my own readme\n', 'existing README left untouched on --force');
|
|
484
|
+
} finally {
|
|
485
|
+
rmSync(dir, { recursive: true, force: true });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('writeConfigs (greenfield, dry-run): mints no README', () => {
|
|
490
|
+
const dir = mkdtempSync(join(tmpdir(), 'greenfield-'));
|
|
491
|
+
try {
|
|
492
|
+
init.writeConfigs(fullSpec(), { dir, force: false, dryRun: true, log: () => {} });
|
|
493
|
+
assert.equal(existsSync(join(dir, 'README.md')), false, 'dry-run writes no README');
|
|
494
|
+
} finally {
|
|
495
|
+
rmSync(dir, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
448
499
|
// ---- seedViaApi provisioning arm (live path, injected fake api) ----
|
|
449
500
|
// A fake apiCall that records calls and returns a configured response per path
|
|
450
501
|
// substring (default: 200 ok). Lets us exercise the live seedViaApi branching
|
|
@@ -74,3 +74,68 @@ test('resolveDocsRoot: the explicit instance root when set, else coreRoot (no-op
|
|
|
74
74
|
assert.equal(resolveDocsRoot({ env: { CLOUDBONGOS_INSTANCE_ROOT: '/srv/consumer' } }), path.resolve('/srv/consumer'), 'consumer writes into its own repo');
|
|
75
75
|
assert.equal(resolveDocsRoot({ env: {} }), resolveCoreRoot(), 'single checkout: coincides with coreRoot');
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// resolveApiBase — the ONE session-aware CLI base resolution (task 1002427).
|
|
80
|
+
//
|
|
81
|
+
// Regression fixture for a bug that locked a builder out of re-authenticating.
|
|
82
|
+
// Three call sites resolved the API base three different ways and none reached
|
|
83
|
+
// the instance the builder was actually signed into:
|
|
84
|
+
// * paste-token.js hardcoded 'https://amazonprimea.com' (a straggler the task
|
|
85
|
+
// 1002201 sweep missed), so a token minted by cloudbongos.com was verified
|
|
86
|
+
// against the founder's host and rejected 401 — a VALID token, wrong host.
|
|
87
|
+
// * reauth.js used the module-level branding default, so it printed a
|
|
88
|
+
// localhost:3000 click-path to a builder signed into cloudbongos.com.
|
|
89
|
+
// * cliClient() alone had it right (env -> session.api_base -> default).
|
|
90
|
+
// The session file knew the answer the whole time. This locks the order in.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const BRAND_B = { domains: { publicOrigin: 'https://instance-b.example' } };
|
|
94
|
+
|
|
95
|
+
test('resolveApiBase: the persisted session beats the branding default', () => {
|
|
96
|
+
// THE regression. A builder authenticated against instance A must keep hitting
|
|
97
|
+
// A even when this checkout's own branding names B.
|
|
98
|
+
assert.equal(
|
|
99
|
+
ic.resolveApiBase({
|
|
100
|
+
session: { api_base: 'https://instance-a.example' },
|
|
101
|
+
env: {},
|
|
102
|
+
b: BRAND_B,
|
|
103
|
+
}),
|
|
104
|
+
'https://instance-a.example'
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('resolveApiBase: an explicit env override beats the session', () => {
|
|
109
|
+
assert.equal(
|
|
110
|
+
ic.resolveApiBase({
|
|
111
|
+
session: { api_base: 'https://instance-a.example' },
|
|
112
|
+
env: { CLOUDBONGOS_API_BASE: 'http://localhost:3000' },
|
|
113
|
+
b: BRAND_B,
|
|
114
|
+
}),
|
|
115
|
+
'http://localhost:3000'
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('resolveApiBase: falls back to the branding default with no env and no session', () => {
|
|
120
|
+
assert.equal(
|
|
121
|
+
ic.resolveApiBase({ session: null, env: {}, b: BRAND_B }),
|
|
122
|
+
'https://instance-b.example'
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('resolveApiBase: a session without a usable api_base falls back, never returns empty', () => {
|
|
127
|
+
for (const session of [{}, { api_base: '' }, { api_base: ' ' }, { api_base: 42 }]) {
|
|
128
|
+
assert.equal(
|
|
129
|
+
ic.resolveApiBase({ session, env: {}, b: BRAND_B }),
|
|
130
|
+
'https://instance-b.example',
|
|
131
|
+
`session ${JSON.stringify(session)} must fall back to the branding default`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('resolveApiBase: session:null does not read the session file from disk', () => {
|
|
137
|
+
// Passing null explicitly means "no session" — the resolver must honour that
|
|
138
|
+
// rather than falling back to a disk read, so this test stays pure/hermetic
|
|
139
|
+
// and callers that already know there is no session get a deterministic answer.
|
|
140
|
+
assert.equal(ic.resolveApiBase({ session: null, env: {}, b: BRAND_B }), 'https://instance-b.example');
|
|
141
|
+
});
|
|
@@ -27,6 +27,8 @@ function test(name, fn) {
|
|
|
27
27
|
const DEMO = { domains: { publicOrigin: 'https://demo.cloudbongos.com' } };
|
|
28
28
|
const OTB = { domains: { publicOrigin: 'https://amazonprimea.com' } };
|
|
29
29
|
const noEnv = {}; // no <PREFIX>_API_BASE override in scope
|
|
30
|
+
const branded = () => true; // simulate a checkout that HAS config/branding.json (a declared instance)
|
|
31
|
+
const neutral = () => false; // simulate a NEUTRAL checkout (portable core — no config/branding.json)
|
|
30
32
|
|
|
31
33
|
test('instanceOriginHost: extracts host, tolerates trailing slash', () => {
|
|
32
34
|
assert.equal(cli.instanceOriginHost('https://amazonprimea.com'), 'amazonprimea.com');
|
|
@@ -41,24 +43,24 @@ test('instanceOriginHost: unparseable input → null (edge)', () => {
|
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
test('match: same-host session + branding → no mismatch (the OTB/normal case)', () => {
|
|
44
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: OTB, env: noEnv });
|
|
46
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: OTB, env: noEnv, hasInstanceConfig: branded });
|
|
45
47
|
assert.equal(r.mismatch, false);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('match: scheme/trailing-slash differences still match on host', () => {
|
|
49
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com/' }, brand: OTB, env: noEnv });
|
|
51
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com/' }, brand: OTB, env: noEnv, hasInstanceConfig: branded });
|
|
50
52
|
assert.equal(r.mismatch, false);
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
test('mismatch: demo repo + founder amazonprimea session → REFUSE (the reported failure)', () => {
|
|
54
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: noEnv });
|
|
56
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: noEnv, hasInstanceConfig: branded });
|
|
55
57
|
assert.equal(r.mismatch, true);
|
|
56
58
|
assert.equal(r.expected, 'https://demo.cloudbongos.com');
|
|
57
59
|
assert.equal(r.actual, 'https://amazonprimea.com');
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
test('mismatch: demo repo + cloudbongos.com session (sibling instance) → REFUSE', () => {
|
|
61
|
-
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: DEMO, env: noEnv });
|
|
63
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: DEMO, env: noEnv, hasInstanceConfig: branded });
|
|
62
64
|
assert.equal(r.mismatch, true);
|
|
63
65
|
});
|
|
64
66
|
|
|
@@ -71,8 +73,23 @@ test('env-override: an explicit *_API_BASE override wins (dev/staging) → no mi
|
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
test('no-branding: repo declares no publicOrigin → cannot assert a mismatch → no mismatch', () => {
|
|
74
|
-
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: {}, env: noEnv }).mismatch, false);
|
|
75
|
-
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: { domains: {} }, env: noEnv }).mismatch, false);
|
|
76
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: {}, env: noEnv, hasInstanceConfig: branded }).mismatch, false);
|
|
77
|
+
assert.equal(cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: { domains: {} }, env: noEnv, hasInstanceConfig: branded }).mismatch, false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('NEUTRAL checkout (no config/branding.json) → trust the session, NO mismatch (task 1002399)', () => {
|
|
81
|
+
// A neutral core checkout signed into a real instance: even though the resolved
|
|
82
|
+
// brand host would differ from the session host, there is no declared instance to
|
|
83
|
+
// enforce, so the guard must NOT fire. This is exactly what lets a neutral checkout
|
|
84
|
+
// run /builder-* with no <PREFIX>_API_BASE prefix.
|
|
85
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://cloudbongos.com' }, brand: OTB, env: noEnv, hasInstanceConfig: neutral });
|
|
86
|
+
assert.equal(r.mismatch, false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('neutral guard is precedence-safe: env override still wins even when neutral', () => {
|
|
90
|
+
// env override short-circuits before the neutral check — order-independent.
|
|
91
|
+
const r = cli.detectInstanceMismatch({ session: { api_base: 'https://amazonprimea.com' }, brand: DEMO, env: { GDS_API_BASE: 'http://localhost:3000' }, hasInstanceConfig: neutral });
|
|
92
|
+
assert.equal(r.mismatch, false);
|
|
76
93
|
});
|
|
77
94
|
|
|
78
95
|
test('no-session: nothing to borrow → no mismatch (the no-session/401 path handles it)', () => {
|