@bongos/core 1.23.22 → 1.24.2
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 +933 -413
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +57 -7
- package/docs/page-readings.json +1210 -1027
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +158 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +38 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +230 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/government-walk.js +3 -0
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/access_holders.mjs +214 -0
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// tests/google_sign_in.mjs — signing up and in with Google, and approving a
|
|
2
|
+
// command-line sign-in in the browser (task 1004993, ADR 0396).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Google is the one new way into the platform. What must hold:
|
|
5
|
+
// - a Google identity is trusted only when Google issued it for OUR app, for
|
|
6
|
+
// THIS sign-in (nonce), and it has not expired;
|
|
7
|
+
// - the person is found by Google's own id through the hub, never an email, and
|
|
8
|
+
// a first sign-in creates them and sends them to choose their name;
|
|
9
|
+
// - their builder row gets a stand-in login that can never be a GitHub login;
|
|
10
|
+
// - the site's sign-in links show "Continue with GitHub / Continue with Google";
|
|
11
|
+
// - the CLI's device sign-in works for them: the terminal is sent to an approval
|
|
12
|
+
// page, the signed-in person approves from their browser cookie, and the
|
|
13
|
+
// terminal's poll receives a CLI session exactly once.
|
|
14
|
+
// No server, no database, no network: the db-kernel functions and the hub port are
|
|
15
|
+
// stubbed before the files that destructure them load, and Google's token
|
|
16
|
+
// endpoint is a fake fetch.
|
|
17
|
+
//
|
|
18
|
+
// Run: node tests/google_sign_in.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
|
+
process.env.BONGOS_GITHUB_CLIENT_ID = process.env.BONGOS_GITHUB_CLIENT_ID || 'gh-test-client';
|
|
26
|
+
process.env.BONGOS_GITHUB_CLIENT_SECRET = process.env.BONGOS_GITHUB_CLIENT_SECRET || 'gh-test-secret';
|
|
27
|
+
process.env.BONGOS_GOOGLE_CLIENT_ID = 'google-test-client';
|
|
28
|
+
process.env.BONGOS_GOOGLE_CLIENT_SECRET = 'google-test-secret';
|
|
29
|
+
// The hub runs open enrollment; one case below closes it.
|
|
30
|
+
process.env.BONGOS_OPEN_XENOS_ENROLLMENT = '1';
|
|
31
|
+
|
|
32
|
+
// ── stubs, installed before anything destructures them ──
|
|
33
|
+
const kernel = require('../src/bongos/db-kernel.js');
|
|
34
|
+
const calls = { upsertBuilder: [], createSession: [], signIn: [], recordHubSignin: [] };
|
|
35
|
+
const builders = new Map(); // github_id → row
|
|
36
|
+
kernel.getBuilderByGithubId = async (id) => builders.get(String(id)) || null;
|
|
37
|
+
kernel.upsertBuilder = async (b) => {
|
|
38
|
+
calls.upsertBuilder.push(b);
|
|
39
|
+
const row = { id: 77, github_id: String(b.githubId), github_login: b.githubLogin, display_name: b.displayName, rank: 'xenos', status: 'active' };
|
|
40
|
+
builders.set(String(b.githubId), row);
|
|
41
|
+
return row;
|
|
42
|
+
};
|
|
43
|
+
kernel.createSession = async (s) => { calls.createSession.push(s); return { id: 1, expires_at: s.expiresAt }; };
|
|
44
|
+
const dbMod = require('../src/bongos/db.js');
|
|
45
|
+
dbMod.createSession = kernel.createSession;
|
|
46
|
+
const admission = require('../src/bongos/auth-admission.js');
|
|
47
|
+
admission.safeSyncRankRole = async () => {};
|
|
48
|
+
|
|
49
|
+
const seams = require('../src/module-seams.js');
|
|
50
|
+
let nextPerson = { personKey: '-5', handle: 'ana', githubLogin: null, created: true };
|
|
51
|
+
if (!seams.hasProvider('platform-identity.hub')) {
|
|
52
|
+
seams.registerProvider('platform-identity.hub', {
|
|
53
|
+
signInWithGoogle: async (identity) => { calls.signIn.push(identity); return { ...nextPerson }; },
|
|
54
|
+
recordHubSignin: async (r) => { calls.recordHubSignin.push(r); },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const { googleConfigured, buildGoogleAuthorizeUrl, verifyGoogleClaims, exchangeGoogleCode, standInLogin } = require('../src/bongos/auth-google.js');
|
|
59
|
+
const { FLOW_TTL_MS, normalizeUserCode, startBrowserDeviceFlow, pendingFlow, pollBrowserDeviceFlow } = require('../src/bongos/device-approval.js');
|
|
60
|
+
const pages = require('../src/bongos/sign-in-pages.js');
|
|
61
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
62
|
+
const buildAuthGoogleRouter = require('../src/bongos/routes/auth-google.js');
|
|
63
|
+
const buildAuthRouter = require('../src/bongos/routes/auth.js');
|
|
64
|
+
|
|
65
|
+
let pass = 0, fail = 0;
|
|
66
|
+
const t = async (name, fn) => {
|
|
67
|
+
try { await fn(); pass++; console.log(`PASS ${name}`); }
|
|
68
|
+
catch (e) { fail++; console.log(`FAIL ${name}\n ${e.stack || e.message}`); }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function idToken(claims) {
|
|
72
|
+
const b = (o) => Buffer.from(JSON.stringify(o)).toString('base64url');
|
|
73
|
+
return `${b({ alg: 'RS256' })}.${b(claims)}.sig`;
|
|
74
|
+
}
|
|
75
|
+
const NOW = Date.UTC(2026, 9, 29, 12);
|
|
76
|
+
const good = (over = {}) => ({
|
|
77
|
+
iss: 'https://accounts.google.com', aud: 'google-test-client', sub: '1098765',
|
|
78
|
+
exp: NOW / 1000 + 600, nonce: 'n1', email: 'ana@example.com', email_verified: true,
|
|
79
|
+
name: 'Ana Example', picture: 'https://lh3.googleusercontent.com/a/x', ...over,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function fakeRes() {
|
|
83
|
+
const res = {
|
|
84
|
+
_status: 200, _body: null, _redirect: null, _type: null, _cookies: {}, _cleared: [], _headers: {},
|
|
85
|
+
status(c) { this._status = c; return this; },
|
|
86
|
+
type(x) { this._type = x; return this; },
|
|
87
|
+
send(b) { this._body = b; return this; },
|
|
88
|
+
json(b) { this._body = b; return this; },
|
|
89
|
+
set(k, v) { this._headers[k.toLowerCase()] = v; return this; },
|
|
90
|
+
redirect(a, b) { this._redirect = b === undefined ? a : b; return this; },
|
|
91
|
+
cookie(n, v, o) { this._cookies[n] = { v, o }; return this; },
|
|
92
|
+
clearCookie(n) { this._cleared.push(n); return this; },
|
|
93
|
+
};
|
|
94
|
+
attachFail({}, res, () => {});
|
|
95
|
+
return res;
|
|
96
|
+
}
|
|
97
|
+
function handler(router, method, path) {
|
|
98
|
+
const layer = router.stack.find((l) => l.route?.path === path && l.route.methods[method]);
|
|
99
|
+
assert.ok(layer, `route ${method.toUpperCase()} ${path} exists`);
|
|
100
|
+
return layer.route.stack.at(-1).handle;
|
|
101
|
+
}
|
|
102
|
+
const req = (over = {}) => ({ query: {}, headers: { host: 'localhost' }, baseUrl: '/api/bongos', body: {}, ...over });
|
|
103
|
+
|
|
104
|
+
// ── the ID token check ──
|
|
105
|
+
await t('a Google identity issued for this app and this sign-in is accepted', () => {
|
|
106
|
+
const id = verifyGoogleClaims(good(), { clientId: 'google-test-client', nonce: 'n1', nowMs: NOW });
|
|
107
|
+
assert.equal(id.subject, '1098765');
|
|
108
|
+
assert.equal(id.email, 'ana@example.com');
|
|
109
|
+
assert.equal(id.emailVerified, true);
|
|
110
|
+
assert.equal(id.displayName, 'Ana Example');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await t('a wrong issuer, audience, expiry, nonce or id is refused', () => {
|
|
114
|
+
const opts = { clientId: 'google-test-client', nonce: 'n1', nowMs: NOW };
|
|
115
|
+
for (const bad of [
|
|
116
|
+
good({ iss: 'https://evil.example' }),
|
|
117
|
+
good({ aud: 'someone-elses-app' }),
|
|
118
|
+
good({ aud: ['google-test-client', 'other'], azp: 'other' }),
|
|
119
|
+
good({ exp: NOW / 1000 - 3600 }),
|
|
120
|
+
good({ nonce: 'stolen' }),
|
|
121
|
+
good({ nonce: undefined }),
|
|
122
|
+
good({ sub: '' }),
|
|
123
|
+
good({ sub: 'has space' }),
|
|
124
|
+
]) {
|
|
125
|
+
assert.throws(() => verifyGoogleClaims(bad, opts), undefined, JSON.stringify(bad));
|
|
126
|
+
}
|
|
127
|
+
assert.throws(() => verifyGoogleClaims(null, opts));
|
|
128
|
+
assert.throws(() => verifyGoogleClaims(good(), { ...opts, nonce: '' }), undefined, 'no nonce on our side is never a match');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await t('the authorize URL asks only for identity, carries state + nonce and lets the person pick an account', () => {
|
|
132
|
+
const u = new URL(buildGoogleAuthorizeUrl({ state: 's1', nonce: 'n1', redirectUri: 'https://hub.example/api/bongos/auth/google/callback' }));
|
|
133
|
+
assert.equal(u.origin + u.pathname, 'https://accounts.google.com/o/oauth2/v2/auth');
|
|
134
|
+
assert.equal(u.searchParams.get('scope'), 'openid email profile');
|
|
135
|
+
assert.equal(u.searchParams.get('client_id'), 'google-test-client');
|
|
136
|
+
assert.equal(u.searchParams.get('state'), 's1');
|
|
137
|
+
assert.equal(u.searchParams.get('nonce'), 'n1');
|
|
138
|
+
assert.equal(u.searchParams.get('response_type'), 'code');
|
|
139
|
+
assert.equal(u.searchParams.get('prompt'), 'select_account');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await t('the code exchange posts our secret to Google and checks the token it returns', async () => {
|
|
143
|
+
let sent = null;
|
|
144
|
+
const fetchImpl = async (url, init) => { sent = { url, body: new URLSearchParams(init.body) }; return { ok: true, status: 200, json: async () => ({ id_token: idToken(good()) }) }; };
|
|
145
|
+
const id = await exchangeGoogleCode({ code: 'c1', redirectUri: 'https://hub/cb', nonce: 'n1', fetchImpl, nowMs: NOW });
|
|
146
|
+
assert.equal(sent.url, 'https://oauth2.googleapis.com/token');
|
|
147
|
+
assert.equal(sent.body.get('client_secret'), 'google-test-secret');
|
|
148
|
+
assert.equal(sent.body.get('grant_type'), 'authorization_code');
|
|
149
|
+
assert.equal(id.subject, '1098765');
|
|
150
|
+
const refused = async () => ({ ok: false, status: 400, json: async () => ({ error: 'invalid_grant' }) });
|
|
151
|
+
await assert.rejects(exchangeGoogleCode({ code: 'c1', redirectUri: 'x', nonce: 'n1', fetchImpl: refused }), /invalid_grant/);
|
|
152
|
+
const forged = async () => ({ ok: true, status: 200, json: async () => ({ id_token: idToken(good({ nonce: 'other' })) }) });
|
|
153
|
+
await assert.rejects(exchangeGoogleCode({ code: 'c1', redirectUri: 'x', nonce: 'n1', fetchImpl: forged, nowMs: NOW }), /this sign-in/);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ── the stand-in login ──
|
|
157
|
+
await t('a stand-in login can never be a GitHub login', () => {
|
|
158
|
+
const GITHUB_LOGIN = /^[A-Za-z0-9-]+$/;
|
|
159
|
+
for (const [handle, key] of [['ana', '-5'], ['Ana Smith!', '-6'], ['', '-7'], [null, '-8'], ['x'.repeat(80), '-9']]) {
|
|
160
|
+
const s = standInLogin(handle, key);
|
|
161
|
+
assert.ok(s.startsWith('~'), s);
|
|
162
|
+
assert.ok(!GITHUB_LOGIN.test(s), `${s} must not look like a GitHub login`);
|
|
163
|
+
assert.ok(s.length <= 40, s);
|
|
164
|
+
}
|
|
165
|
+
assert.equal(standInLogin('ana', '-5'), '~ana');
|
|
166
|
+
assert.equal(standInLogin('', '-7'), '~builder7');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── the whole browser sign-in through the routes ──
|
|
170
|
+
const gRouter = buildAuthGoogleRouter();
|
|
171
|
+
|
|
172
|
+
await t('Google sign-in is offered only where the hub and the owner’s Google app both are', () => {
|
|
173
|
+
assert.equal(googleConfigured(), true);
|
|
174
|
+
const saved = process.env.BONGOS_GOOGLE_CLIENT_SECRET;
|
|
175
|
+
delete process.env.BONGOS_GOOGLE_CLIENT_SECRET;
|
|
176
|
+
try {
|
|
177
|
+
assert.equal(googleConfigured(), false);
|
|
178
|
+
const res = fakeRes();
|
|
179
|
+
handler(gRouter, 'get', '/auth/google/start')(req(), res);
|
|
180
|
+
assert.equal(res._status, 404, 'no app, no Google door');
|
|
181
|
+
} finally { process.env.BONGOS_GOOGLE_CLIENT_SECRET = saved; }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
let stateCookie;
|
|
185
|
+
await t('start sets a state+nonce cookie and sends the person to Google', () => {
|
|
186
|
+
const res = fakeRes();
|
|
187
|
+
handler(gRouter, 'get', '/auth/google/start')(req({ query: { return: '/community' } }), res);
|
|
188
|
+
stateCookie = res._cookies.bongos_google_state.v;
|
|
189
|
+
assert.match(stateCookie, /^[0-9a-f]{32}\.[0-9a-f]{32}$/);
|
|
190
|
+
assert.equal(res._cookies.bongos_google_state.o.httpOnly, true);
|
|
191
|
+
assert.equal(res._cookies.gds_return.v, '/community');
|
|
192
|
+
const u = new URL(res._redirect);
|
|
193
|
+
assert.equal(u.hostname, 'accounts.google.com');
|
|
194
|
+
assert.equal(u.searchParams.get('state'), stateCookie.split('.')[0]);
|
|
195
|
+
assert.equal(u.searchParams.get('nonce'), stateCookie.split('.')[1]);
|
|
196
|
+
assert.match(u.searchParams.get('redirect_uri'), /\/api\/bongos\/auth\/google\/callback$/);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await t('a callback whose state does not match this browser’s is refused and signs nobody in', async () => {
|
|
200
|
+
const res = fakeRes();
|
|
201
|
+
await handler(gRouter, 'get', '/auth/google/callback')(req({
|
|
202
|
+
query: { state: 'forged', code: 'c1' }, headers: { host: 'localhost', cookie: `bongos_google_state=${stateCookie}` },
|
|
203
|
+
}), res);
|
|
204
|
+
assert.equal(res._status, 400);
|
|
205
|
+
assert.equal(calls.createSession.length, 0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const realFetch = globalThis.fetch;
|
|
209
|
+
async function callback({ person, cookieExtra = '' }) {
|
|
210
|
+
nextPerson = person;
|
|
211
|
+
const [state, nonce] = stateCookie.split('.');
|
|
212
|
+
globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ id_token: idToken(good({ nonce, exp: Date.now() / 1000 + 600 })) }) });
|
|
213
|
+
const res = fakeRes();
|
|
214
|
+
try {
|
|
215
|
+
await handler(gRouter, 'get', '/auth/google/callback')(req({
|
|
216
|
+
query: { state, code: 'c1' }, headers: { host: 'localhost', cookie: `bongos_google_state=${stateCookie}${cookieExtra}` },
|
|
217
|
+
}), res);
|
|
218
|
+
} finally { globalThis.fetch = realFetch; }
|
|
219
|
+
return res;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await t('a first Google sign-in creates the person, signs them in and sends them to choose their name', async () => {
|
|
223
|
+
const res = await callback({ person: { personKey: '-5', handle: 'ana', githubLogin: null, created: true } });
|
|
224
|
+
assert.equal(res._status, 200, res._body);
|
|
225
|
+
assert.deepEqual(calls.signIn.at(-1), {
|
|
226
|
+
subject: '1098765', email: 'ana@example.com', emailVerified: true, displayName: 'Ana Example',
|
|
227
|
+
avatarUrl: 'https://lh3.googleusercontent.com/a/x',
|
|
228
|
+
}, 'the hub is asked by Google’s own id; the email rides along for display only');
|
|
229
|
+
const b = calls.upsertBuilder.at(-1);
|
|
230
|
+
assert.equal(b.githubId, '-5', 'the builder row is keyed on the minted person key');
|
|
231
|
+
assert.equal(b.githubLogin, '~ana', 'with a stand-in login, never a GitHub-shaped one');
|
|
232
|
+
assert.equal(calls.createSession.at(-1).source, 'web');
|
|
233
|
+
assert.equal(calls.createSession.at(-1).builderId, 77);
|
|
234
|
+
assert.ok(Object.values(res._cookies).some((c) => c.v === calls.createSession.at(-1).token), 'the session cookie is set');
|
|
235
|
+
assert.equal(res._redirect, '/account?welcome=google');
|
|
236
|
+
assert.equal(calls.recordHubSignin.at(-1).githubLogin, null, 'the hub is never handed the stand-in as a GitHub login');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await t('a returning Google sign-in goes back where it began', async () => {
|
|
240
|
+
const res = await callback({ person: { personKey: '-5', handle: 'ana', githubLogin: null, created: false }, cookieExtra: '; gds_return=%2Fcommunity' });
|
|
241
|
+
assert.equal(res._redirect, '/community');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
await t('a person whose GitHub is linked keeps their real GitHub login on the builder row', async () => {
|
|
245
|
+
await callback({ person: { personKey: '424242', handle: 'octo', githubLogin: 'octocat', created: false } });
|
|
246
|
+
assert.equal(calls.upsertBuilder.at(-1).githubLogin, 'octocat');
|
|
247
|
+
assert.equal(calls.recordHubSignin.at(-1).githubLogin, 'octocat');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await t('an invite-only site turns a first-time Google person away without a session', async () => {
|
|
251
|
+
const before = calls.createSession.length;
|
|
252
|
+
const saved = process.env.BONGOS_OPEN_XENOS_ENROLLMENT;
|
|
253
|
+
process.env.BONGOS_OPEN_XENOS_ENROLLMENT = '0';
|
|
254
|
+
try {
|
|
255
|
+
assert.equal(admission.openEnrollmentEnabled(), false, 'the case runs with enrollment closed');
|
|
256
|
+
const res = await callback({ person: { personKey: '-99', handle: 'newbie', githubLogin: null, created: true } });
|
|
257
|
+
assert.equal(res._status, 403);
|
|
258
|
+
assert.equal(calls.createSession.length, before, 'no session for a refused person');
|
|
259
|
+
// …while a person who already has a seat here still gets in.
|
|
260
|
+
const back = await callback({ person: { personKey: '-5', handle: 'ana', githubLogin: null, created: false } });
|
|
261
|
+
assert.equal(back._status, 200);
|
|
262
|
+
} finally {
|
|
263
|
+
if (saved === undefined) delete process.env.BONGOS_OPEN_XENOS_ENROLLMENT; else process.env.BONGOS_OPEN_XENOS_ENROLLMENT = saved;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// ── the sign-in chooser on every sign-in link ──
|
|
268
|
+
await t('every sign-in link lands on Continue with GitHub / Continue with Google', async () => {
|
|
269
|
+
const start = handler(buildAuthRouter(), 'get', '/auth/web/start');
|
|
270
|
+
const res = fakeRes();
|
|
271
|
+
await start(req({ query: { return: '/connections', repos: '1' } }), res);
|
|
272
|
+
assert.equal(res._redirect, null, 'no straight jump to GitHub');
|
|
273
|
+
assert.match(res._body, /Continue with GitHub/);
|
|
274
|
+
assert.match(res._body, /Continue with Google/);
|
|
275
|
+
assert.match(res._body, /auth\/web\/start\?via=github&return=%2Fconnections&repos=1/);
|
|
276
|
+
assert.match(res._body, /auth\/google\/start\?return=%2Fconnections/);
|
|
277
|
+
assert.doesNotMatch(res._body, /<script/i, 'the chooser runs no script');
|
|
278
|
+
|
|
279
|
+
const viaGithub = fakeRes();
|
|
280
|
+
await start(req({ query: { via: 'github', return: '/connections' } }), viaGithub);
|
|
281
|
+
assert.match(viaGithub._redirect, /^https:\/\/github\.com\/login\/oauth\/authorize/, 'the GitHub button goes to GitHub');
|
|
282
|
+
|
|
283
|
+
const sw = fakeRes();
|
|
284
|
+
await start(req({ query: { switch: '1' } }), sw);
|
|
285
|
+
assert.match(sw._redirect, /github\.com/, '"New account" names a GitHub account and skips the chooser');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ── the command-line sign-in, approved in the browser ──
|
|
289
|
+
await t('typed codes are read the way people type them', () => {
|
|
290
|
+
assert.equal(normalizeUserCode('bcdf-ghjk'), 'BCDF-GHJK');
|
|
291
|
+
assert.equal(normalizeUserCode(' bcdf ghjk '), 'BCDF-GHJK');
|
|
292
|
+
assert.equal(normalizeUserCode('BCDFGHJ'), null);
|
|
293
|
+
assert.equal(normalizeUserCode('ABCD-EFGH'), null, 'vowels are never in a code');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
await t('with Google offered, the CLI’s device start points the terminal at this site’s approval page', async () => {
|
|
297
|
+
const start = handler(buildAuthRouter(), 'post', '/auth/device/start');
|
|
298
|
+
const res = fakeRes();
|
|
299
|
+
await start(req(), res);
|
|
300
|
+
const out = res._body;
|
|
301
|
+
assert.match(out.user_code, /^[B-Z]{4}-[B-Z]{4}$/);
|
|
302
|
+
assert.match(out.verification_uri, /\/api\/bongos\/auth\/device\?code=[B-Z]{4}-[B-Z]{4}$/);
|
|
303
|
+
assert.equal(typeof out.device_code, 'string');
|
|
304
|
+
assert.equal(out.interval, 5);
|
|
305
|
+
assert.ok(out.expires_in > 0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await t('the approval page asks a signed-out person to sign in, with both ways in, and comes back', async () => {
|
|
309
|
+
const flow = startBrowserDeviceFlow({ pageUrl: 'https://hub/api/bongos/auth/device' });
|
|
310
|
+
const res = fakeRes();
|
|
311
|
+
await handler(gRouter, 'get', '/auth/device')(req({ query: { code: flow.user_code.toLowerCase() } }), res);
|
|
312
|
+
assert.match(res._body, /Continue with GitHub/);
|
|
313
|
+
assert.match(res._body, /Continue with Google/);
|
|
314
|
+
assert.ok(res._body.includes(encodeURIComponent(`/api/bongos/auth/device?code=${flow.user_code}`)), 'both doors return to this page');
|
|
315
|
+
const gone = fakeRes();
|
|
316
|
+
await handler(gRouter, 'get', '/auth/device')(req({ query: { code: 'BBBB-BBBB' } }), gone);
|
|
317
|
+
assert.equal(gone._status, 404);
|
|
318
|
+
assert.match(gone._body, /Enter your code/);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const approver = { id: 77, github_id: '-5', github_login: '~ana', display_name: 'Ana Example', rank: 'xenos', status: 'active' };
|
|
322
|
+
const approveReq = (code, over = {}) => req({
|
|
323
|
+
method: 'POST',
|
|
324
|
+
body: { user_code: code, decision: 'approve' },
|
|
325
|
+
builder: approver,
|
|
326
|
+
headers: { host: 'localhost', cookie: 'bongos_session=tok', origin: 'http://localhost', 'sec-fetch-site': 'same-origin' },
|
|
327
|
+
...over,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await t('approving needs the browser cookie and a same-site form, never a bearer or another site', async () => {
|
|
331
|
+
const approve = handler(gRouter, 'post', '/auth/device/approve');
|
|
332
|
+
const flow = startBrowserDeviceFlow({ pageUrl: 'https://hub/x' });
|
|
333
|
+
const before = calls.createSession.length;
|
|
334
|
+
for (const headers of [
|
|
335
|
+
{ host: 'localhost', authorization: 'Bearer tok' },
|
|
336
|
+
{ host: 'localhost', cookie: 'bongos_session=tok', 'sec-fetch-site': 'cross-site' },
|
|
337
|
+
{ host: 'localhost', cookie: 'bongos_session=tok', origin: 'https://evil.example' },
|
|
338
|
+
]) {
|
|
339
|
+
const res = fakeRes();
|
|
340
|
+
await approve(approveReq(flow.user_code, { headers }), res);
|
|
341
|
+
assert.equal(res._status, 403, JSON.stringify(headers));
|
|
342
|
+
}
|
|
343
|
+
assert.equal(calls.createSession.length, before, 'nothing minted');
|
|
344
|
+
assert.ok(pendingFlow(flow.user_code), 'the code is still waiting');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await t('an approved code hands the terminal a CLI session for the approver, exactly once', async () => {
|
|
348
|
+
const flow = startBrowserDeviceFlow({ pageUrl: 'https://hub/x' });
|
|
349
|
+
const poll = handler(buildAuthRouter(), 'post', '/auth/device/poll');
|
|
350
|
+
const pollOnce = async (now) => {
|
|
351
|
+
const res = fakeRes();
|
|
352
|
+
await poll(req({ method: 'POST', body: { device_code: flow.device_code } }), res);
|
|
353
|
+
return res._body;
|
|
354
|
+
};
|
|
355
|
+
assert.equal((await pollOnce()).state, 'pending');
|
|
356
|
+
|
|
357
|
+
const res = fakeRes();
|
|
358
|
+
await handler(gRouter, 'post', '/auth/device/approve')(approveReq(flow.user_code), res);
|
|
359
|
+
assert.equal(res._status, 200, res._body);
|
|
360
|
+
assert.match(res._body, /You’re signed in/);
|
|
361
|
+
const minted = calls.createSession.at(-1);
|
|
362
|
+
assert.equal(minted.source, 'cli');
|
|
363
|
+
assert.equal(minted.builderId, 77, 'the session belongs to whoever approved');
|
|
364
|
+
assert.equal(minted.expiresAt, null);
|
|
365
|
+
|
|
366
|
+
const out = pollBrowserDeviceFlow(flow.device_code, Date.now() + 60_000);
|
|
367
|
+
assert.equal(out.state, 'ok');
|
|
368
|
+
assert.equal(out.token, minted.token);
|
|
369
|
+
assert.equal(out.builder.github_login, '~ana');
|
|
370
|
+
assert.equal(pollBrowserDeviceFlow(flow.device_code, Date.now() + 120_000).state, 'expired', 'the token leaves once');
|
|
371
|
+
|
|
372
|
+
const again = fakeRes();
|
|
373
|
+
await handler(gRouter, 'post', '/auth/device/approve')(approveReq(flow.user_code), again);
|
|
374
|
+
assert.equal(again._status, 404, 'a used code cannot be approved twice');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await t('a cancelled code tells the terminal it was denied and mints nothing', async () => {
|
|
378
|
+
const flow = startBrowserDeviceFlow({ pageUrl: 'https://hub/x' });
|
|
379
|
+
const before = calls.createSession.length;
|
|
380
|
+
const res = fakeRes();
|
|
381
|
+
await handler(gRouter, 'post', '/auth/device/approve')(approveReq(flow.user_code, { body: { user_code: flow.user_code, decision: 'deny' } }), res);
|
|
382
|
+
assert.match(res._body, /Sign-in cancelled/);
|
|
383
|
+
assert.equal(calls.createSession.length, before);
|
|
384
|
+
assert.equal(pollBrowserDeviceFlow(flow.device_code, Date.now() + 60_000).state, 'denied');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
await t('a code expires after ten minutes', () => {
|
|
388
|
+
const now = Date.now();
|
|
389
|
+
const flow = startBrowserDeviceFlow({ pageUrl: 'https://hub/x', now });
|
|
390
|
+
assert.ok(pendingFlow(flow.user_code, now + 60_000));
|
|
391
|
+
assert.equal(pendingFlow(flow.user_code, now + FLOW_TTL_MS + 1), null);
|
|
392
|
+
assert.equal(pollBrowserDeviceFlow(flow.device_code, now + FLOW_TTL_MS + 1).state, 'expired');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
await t('the pages escape what they show', () => {
|
|
396
|
+
const html = pages.renderDeviceApprovalPage({ state: 'confirm', userCode: 'BCDF-GHJK', name: '<img src=x onerror=alert(1)>', action: '/a' });
|
|
397
|
+
assert.ok(!html.includes('<img src=x'), 'a display name is never markup');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
console.log(`\n${pass} passed, ${fail} failed`);
|
|
401
|
+
if (fail) process.exit(1);
|
|
@@ -107,3 +107,97 @@ test('the manifest declares the poller, and start/stop are idempotent + unref-sa
|
|
|
107
107
|
// the test runner hanging IS the assertion.
|
|
108
108
|
assert.ok(true);
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
// --- task 1004960: sleep until the soonest deadline, never overlap ------------
|
|
112
|
+
|
|
113
|
+
test('cadence: sleeps until just after the soonest deadline, at most 5 minutes; overdue retries at 1 minute', () => {
|
|
114
|
+
assert.equal(poller.MAX_WAIT_MS, 5 * 60_000, 'the idle wait is 5 minutes, not the old 1');
|
|
115
|
+
assert.equal(poller.nextDelayMs(null), 5 * 60_000, 'no open clock: one read per 5 minutes');
|
|
116
|
+
assert.equal(poller.nextDelayMs(30_000), 30_000 + poller.GRACE_MS, 'a window closing in 30s is swept ~1s after it closes');
|
|
117
|
+
assert.equal(poller.nextDelayMs(2 * 3600_000), 5 * 60_000, 'a far deadline is capped at 5 minutes');
|
|
118
|
+
assert.equal(poller.nextDelayMs(-5_000), 60_000, 'still overdue after a sweep (close rolled back): retry in a minute, never spin');
|
|
119
|
+
assert.equal(poller.nextDelayMs(0), 60_000);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('a run arms the next one from ONE soonest-deadline read', async () => {
|
|
123
|
+
let reads = 0;
|
|
124
|
+
const deps = { listFn: async () => [], log: () => {}, error: () => {}, soonestFn: async () => { reads += 1; return 40_000; } };
|
|
125
|
+
poller.start(deps);
|
|
126
|
+
try {
|
|
127
|
+
await poller.tick(deps);
|
|
128
|
+
assert.equal(reads, 1, 'one MIN(closes_at) read per run');
|
|
129
|
+
const ms = poller._state().msUntilNextRun;
|
|
130
|
+
assert.ok(ms > 39_000 && ms <= 41_000, `armed for just after the deadline (got ${ms})`);
|
|
131
|
+
await poller.tick({ ...deps, soonestFn: async () => null });
|
|
132
|
+
const idle = poller._state().msUntilNextRun;
|
|
133
|
+
assert.ok(idle > 299_000 && idle <= 300_000, `no clock open: 5-minute sleep (got ${idle})`);
|
|
134
|
+
} finally { poller.stop(); }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('a window opened mid-sleep wakes the poller sooner, so a 1-minute window still closes on time', async () => {
|
|
138
|
+
const deps = { listFn: async () => [], log: () => {}, error: () => {}, soonestFn: async () => null };
|
|
139
|
+
poller.start(deps);
|
|
140
|
+
try {
|
|
141
|
+
await poller.tick(deps);
|
|
142
|
+
assert.ok(poller._state().msUntilNextRun > 299_000);
|
|
143
|
+
poller.noteDeadline(60_000);
|
|
144
|
+
const ms = poller._state().msUntilNextRun;
|
|
145
|
+
assert.ok(ms > 60_000 && ms <= 61_000, `woken for the new deadline (got ${ms})`);
|
|
146
|
+
poller.noteDeadline(4 * 60_000);
|
|
147
|
+
assert.ok(poller._state().msUntilNextRun <= 61_000, 'a LATER deadline never pushes the alarm back');
|
|
148
|
+
} finally { poller.stop(); }
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('a deadline noted while a sweep is in flight is kept for the re-arm', async () => {
|
|
152
|
+
let release;
|
|
153
|
+
const gate = new Promise((r) => { release = r; });
|
|
154
|
+
const deps = { listFn: async () => { await gate; return []; }, log: () => {}, error: () => {}, soonestFn: async () => null };
|
|
155
|
+
poller.start(deps);
|
|
156
|
+
try {
|
|
157
|
+
const run = poller.tick(deps);
|
|
158
|
+
poller.noteDeadline(90_000);
|
|
159
|
+
release();
|
|
160
|
+
await run;
|
|
161
|
+
const ms = poller._state().msUntilNextRun;
|
|
162
|
+
assert.ok(ms > 89_000 && ms <= 91_000, `the mid-sweep hint set the alarm (got ${ms})`);
|
|
163
|
+
} finally { poller.stop(); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('a run that starts while the previous one is in flight is skipped', async () => {
|
|
167
|
+
let reads = 0;
|
|
168
|
+
let release;
|
|
169
|
+
const gate = new Promise((r) => { release = r; });
|
|
170
|
+
const deps = { listFn: async () => { reads += 1; await gate; return []; }, log: () => {}, error: () => {}, soonestFn: async () => null };
|
|
171
|
+
const first = poller.tick(deps);
|
|
172
|
+
const secondRun = poller.tick(deps); // fires while the first is still reading
|
|
173
|
+
release();
|
|
174
|
+
const [, second] = await Promise.all([first, secondRun]);
|
|
175
|
+
assert.deepEqual(second, { skipped: true });
|
|
176
|
+
assert.equal(reads, 1, 'the overlapping run never touched the database');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('opening a timed window through db.openBoardItem wakes the started poller', async () => {
|
|
180
|
+
const gdb = require(path.join(GOV, 'db.js'));
|
|
181
|
+
const { pool } = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
182
|
+
const realQuery = pool.query;
|
|
183
|
+
pool.query = async () => ({ rows: [{ id: '9', closes_at: new Date() }] }); // the INSERT ... RETURNING
|
|
184
|
+
const deps = { listFn: async () => [], log: () => {}, error: () => {}, soonestFn: async () => null };
|
|
185
|
+
poller.start(deps);
|
|
186
|
+
try {
|
|
187
|
+
await poller.tick(deps);
|
|
188
|
+
assert.ok(poller._state().msUntilNextRun > 299_000, 'idle: asleep for 5 minutes');
|
|
189
|
+
await gdb.openBoardItem({ subjectType: 'full_idea', subjectId: '1', windowMinutes: 2 });
|
|
190
|
+
const ms = poller._state().msUntilNextRun;
|
|
191
|
+
assert.ok(ms > 119_000 && ms <= 121_000, `a 2-minute window woke it for its deadline (got ${ms})`);
|
|
192
|
+
poller.stop();
|
|
193
|
+
await gdb.openBoardItem({ subjectType: 'full_idea', subjectId: '2', windowMinutes: 1 });
|
|
194
|
+
assert.equal(poller._state().msUntilNextRun, null, 'a stopped poller is not re-armed');
|
|
195
|
+
} finally { poller.stop(); pool.query = realQuery; }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('the soonest-deadline read is on the database clock (SQL pin)', () => {
|
|
199
|
+
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
200
|
+
const read = src.slice(src.indexOf('async function msUntilSoonestOpenDeadline'), src.indexOf('// The summons read'));
|
|
201
|
+
assert.match(read, /MIN\(closes_at\) - now\(\)/, 'measured on the DATABASE clock');
|
|
202
|
+
assert.match(read, /closed_at IS NULL AND closes_at IS NOT NULL/, 'open items with a clock only');
|
|
203
|
+
});
|
|
@@ -256,7 +256,10 @@ test('the membership queries exclude machines and the deactivated — structural
|
|
|
256
256
|
const src = fs.readFileSync(path.join(GOV, 'db.js'), 'utf8');
|
|
257
257
|
const start = src.indexOf('getActiveBuildersWithRank');
|
|
258
258
|
assert.ok(start > -1);
|
|
259
|
-
|
|
259
|
+
// bounded to the two membership reads: a later query filtering status for its own
|
|
260
|
+
// reasons (task 1005000's access holders) is not one of them
|
|
261
|
+
const end = src.indexOf('\nasync function ', src.indexOf('async function builderHoldsRankActive') + 1);
|
|
262
|
+
const block = src.slice(start, end > start ? end : undefined);
|
|
260
263
|
assert.equal((block.match(/b\.status = 'active'/g) || []).length, 2, 'both reads filter status');
|
|
261
264
|
assert.equal((block.match(/b\.system_role IS NULL/g) || []).length, 2, 'both reads exclude principals');
|
|
262
265
|
});
|
|
@@ -279,7 +279,7 @@ const ADDED_SINCE_R101 = [
|
|
|
279
279
|
// had been carved into `<name>-*.js` siblings and only the facade stayed registered. The
|
|
280
280
|
// carve tripwire further down is what found the four beyond the one the task named.
|
|
281
281
|
// Listed BY NAME, not as `<name>-*.js`: path-match (the goal scope wall) reads no '*'.
|
|
282
|
-
...carve('src/bongos/auth.js', ['auth-admission', 'auth-config', 'auth-discord', 'auth-github', 'auth-idp'],
|
|
282
|
+
...carve('src/bongos/auth.js', ['auth-admission', 'auth-config', 'auth-discord', 'auth-github', 'auth-google', 'auth-idp'],
|
|
283
283
|
'the auth.js carve: session-token minting and the OAuth exchange, admission and '
|
|
284
284
|
+ 'first-admin seating, IdP assertion checks.'),
|
|
285
285
|
...carve('src/bongos/db.js', ['db-kernel'],
|
|
@@ -104,7 +104,7 @@ test('every /governance route gates via REAL middleware on exactly its PINNED at
|
|
|
104
104
|
// task 1004712: the treasury is read, and its pots and borrows decided, where the room is (the holder and the seat are the walls).
|
|
105
105
|
// task 1004715: the strictness presets are read where the law book is.
|
|
106
106
|
// task 1004716: the counts are read where the docket they count is.
|
|
107
|
-
const boardFamily = (key) => key.includes('/board/') || key.includes('/government/asks') || key.endsWith('/government/constitution') || key.endsWith('/government/docket') || key.endsWith('/government/counts') || key.endsWith('/government/laws') || key.endsWith('/government/leads') || key.endsWith('/government/buildouts') || key.includes('/government/treasury') || key.endsWith('/government/presets');
|
|
107
|
+
const boardFamily = (key) => key.includes('/board/') || key.includes('/government/asks') || key.endsWith('/government/constitution') || key.endsWith('/government/docket') || key.includes('/government/blockers/') || key.endsWith('/government/access') || key.endsWith('/government/counts') || key.endsWith('/government/laws') || key.endsWith('/government/leads') || key.endsWith('/government/buildouts') || key.includes('/government/treasury') || key.endsWith('/government/presets');
|
|
108
108
|
for (const r of classified.filter((x) => !boardFamily(x.key))) {
|
|
109
109
|
assert.deepEqual(r.perms, ['government.manage'], `${r.key}: the admin surface gates on the manage atom`);
|
|
110
110
|
}
|
|
@@ -52,6 +52,7 @@ const NON_GRANT_MIGRATIONS = new Set([
|
|
|
52
52
|
'government_030_buildouts.sql', // a Lead's build-outs, on the record at once or by a passed vote (task 1004711)
|
|
53
53
|
'government_031_treasury.sql', // the treasury: a pot per goal and the loans a dry pot asks the reserve for (task 1004712)
|
|
54
54
|
'government_032_ask_flags.sql', // the reword list: matters a reader flagged as unclear (owner 2026-10-23)
|
|
55
|
+
'government_036_access_holders.sql', // who holds the keys outside Bongos + the access:<key> ask seat (task 1005000)
|
|
55
56
|
]);
|
|
56
57
|
|
|
57
58
|
// Permission keys RENAMED after the grant migrations that first granted them.
|