@bongos/core 1.23.22 → 1.24.1
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 +902 -407
- 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 +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -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/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 +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +5 -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 +55 -6
- package/docs/page-readings.json +1185 -1011
- 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/catalog.js +6 -0
- package/modules/government/db.js +34 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -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 +156 -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/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 +24 -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 +220 -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/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 +3 -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/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_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- 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,326 @@
|
|
|
1
|
+
// tests/account_logins.mjs — a platform account is a person, and GitHub is one
|
|
2
|
+
// way in (task 1004992, ADR 0393).
|
|
3
|
+
//
|
|
4
|
+
// What this proves, DB-free (the real SQL is tests/account_logins_db.mjs):
|
|
5
|
+
//
|
|
6
|
+
// 1. A provider identity is only ever the provider's stable user id — never an
|
|
7
|
+
// email or a login — and GitHub's is a positive integer.
|
|
8
|
+
// 2. A person who arrives without GitHub gets a MINTED key that is negative,
|
|
9
|
+
// so it can never be mistaken for a GitHub id, and no GitHub login is
|
|
10
|
+
// invented for them.
|
|
11
|
+
// 3. A second sign-in by the same Google identity — or a racing double-click —
|
|
12
|
+
// reaches the SAME account; it never makes a second person.
|
|
13
|
+
// 4. GitHub accounts are never born here (one birth path per kind).
|
|
14
|
+
// 5. The person's own settings read lists which ways in they have, never the
|
|
15
|
+
// provider ids, and is keyed on the signed-in person only.
|
|
16
|
+
// 6. The hub's numeric-key guard now admits a minted key, and still refuses a
|
|
17
|
+
// system:* principal.
|
|
18
|
+
//
|
|
19
|
+
// Run: node --test tests/account_logins.mjs
|
|
20
|
+
|
|
21
|
+
import assert from 'node:assert/strict';
|
|
22
|
+
import { test, before, after } from 'node:test';
|
|
23
|
+
import http from 'node:http';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const express = require('express');
|
|
28
|
+
|
|
29
|
+
// A recording pool. `script` answers each statement by the first matching
|
|
30
|
+
// pattern; connect() hands out a client that records into the same log, so a
|
|
31
|
+
// transaction's statements are visible in order.
|
|
32
|
+
function fakePool(script) {
|
|
33
|
+
const p = { log: [], script };
|
|
34
|
+
const run = async (sql, params = []) => {
|
|
35
|
+
p.log.push({ sql, params });
|
|
36
|
+
for (const [re, answer] of p.script) {
|
|
37
|
+
if (re.test(sql)) return typeof answer === 'function' ? answer(sql, params, p.log) : answer;
|
|
38
|
+
}
|
|
39
|
+
return { rows: [], rowCount: 0 };
|
|
40
|
+
};
|
|
41
|
+
p.query = run;
|
|
42
|
+
p.connect = async () => ({ query: run, release() {} });
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Swap what the route's pool answers. The route captured the pool object at
|
|
47
|
+
// require time, so the object stays and its script and log are replaced.
|
|
48
|
+
function scriptRoute(script) {
|
|
49
|
+
routePool.script = script;
|
|
50
|
+
routePool.log.length = 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let ME = '4242';
|
|
54
|
+
const apiPath = require.resolve('../src/module-api.js');
|
|
55
|
+
const realApi = require(apiPath);
|
|
56
|
+
const routePool = fakePool([]);
|
|
57
|
+
require.cache[apiPath].exports = {
|
|
58
|
+
...realApi,
|
|
59
|
+
pool: routePool,
|
|
60
|
+
requireBuilder: (req, _res, next) => {
|
|
61
|
+
req.builder = { id: 1, github_id: ME, github_login: 'octo' };
|
|
62
|
+
next();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const logins = require('../modules/platform-identity/account-logins.js');
|
|
67
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
68
|
+
const profileRoutes = require('../modules/platform-identity/routes/profile.js');
|
|
69
|
+
const myProjectsRoutes = require('../modules/platform-identity/routes/my-projects.js');
|
|
70
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
71
|
+
|
|
72
|
+
test('a provider identity is the provider\'s stable user id, nothing else', () => {
|
|
73
|
+
assert.deepEqual(logins.normalizeLogin({ provider: 'github', subject: 583231 }), { provider: 'github', subject: '583231' });
|
|
74
|
+
assert.deepEqual(logins.normalizeLogin({ provider: 'google', subject: '110169484474386276334' }), { provider: 'google', subject: '110169484474386276334' });
|
|
75
|
+
for (const bad of [
|
|
76
|
+
{ provider: 'github', subject: 'octocat' }, // a login, not an id
|
|
77
|
+
{ provider: 'github', subject: '-5' }, // a minted key is not a GitHub id
|
|
78
|
+
{ provider: 'github', subject: '0' },
|
|
79
|
+
{ provider: 'google', subject: '' },
|
|
80
|
+
{ provider: 'google', subject: 'has space' },
|
|
81
|
+
{ provider: 'google', subject: 'x'.repeat(256) },
|
|
82
|
+
{ provider: 'email', subject: 'a@b.c' }, // not a provider (ADR 0393)
|
|
83
|
+
{ provider: 'apple', subject: '123' },
|
|
84
|
+
{},
|
|
85
|
+
]) assert.equal(logins.normalizeLogin(bad), null, JSON.stringify(bad));
|
|
86
|
+
assert.deepEqual([...logins.LOGIN_PROVIDERS], ['github', 'google']);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a minted person key is negative, and only a minted one is', () => {
|
|
90
|
+
assert.equal(logins.isMintedPersonKey('-17'), true);
|
|
91
|
+
assert.equal(logins.isMintedPersonKey(-17), true);
|
|
92
|
+
assert.equal(logins.isMintedPersonKey('583231'), false);
|
|
93
|
+
assert.equal(logins.isMintedPersonKey('system:bfg'), false);
|
|
94
|
+
assert.equal(logins.isMintedPersonKey(null), false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('the hub key guard admits a minted key and still refuses a system principal', () => {
|
|
98
|
+
assert.equal(pi.isNumericGithubId('-17'), true);
|
|
99
|
+
assert.equal(pi.isNumericGithubId('583231'), true);
|
|
100
|
+
assert.equal(pi.isNumericGithubId('system:bfg'), false);
|
|
101
|
+
assert.equal(pi.isNumericGithubId('--1'), false);
|
|
102
|
+
assert.equal(pi.isNumericGithubId(''), false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('a person arriving through Google gets a new account with a minted key and no GitHub login', async () => {
|
|
106
|
+
const pool = fakePool([
|
|
107
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [] }],
|
|
108
|
+
[/INSERT INTO platform_identity_accounts/, { rows: [{ id: 77, person_key: '-3' }] }],
|
|
109
|
+
[/INSERT INTO platform_identity_account_logins/, { rows: [], rowCount: 1 }],
|
|
110
|
+
[/UPDATE platform_identity_accounts\s+SET handle/, { rows: [{ handle: 'janedoe' }], rowCount: 1 }],
|
|
111
|
+
]);
|
|
112
|
+
const out = await logins.createAccountForLogin({
|
|
113
|
+
provider: 'google', subject: 'g-sub-1', email: ' jane@example.com ', displayName: 'Jane Doe',
|
|
114
|
+
avatarUrl: 'https://lh3.googleusercontent.com/a/x',
|
|
115
|
+
}, { pool });
|
|
116
|
+
assert.deepEqual(out, { accountId: 77, personKey: '-3', created: true });
|
|
117
|
+
|
|
118
|
+
const sqls = pool.log.map((q) => q.sql);
|
|
119
|
+
const insAcct = pool.log.find((q) => /INSERT INTO platform_identity_accounts/.test(q.sql));
|
|
120
|
+
assert.match(insAcct.sql, /-nextval\('platform_identity_person_key_seq'\)/, 'the key is minted negative from the sequence');
|
|
121
|
+
assert.match(insAcct.sql, /VALUES \(-nextval\('platform_identity_person_key_seq'\), NULL,/, 'no GitHub login is invented');
|
|
122
|
+
assert.match(insAcct.sql, /'provisional'/, 'not activated here — the sign-in route decides, as for GitHub');
|
|
123
|
+
assert.deepEqual(insAcct.params, ['Jane Doe', 'https://lh3.googleusercontent.com/a/x']);
|
|
124
|
+
|
|
125
|
+
const insLogin = pool.log.find((q) => /INSERT INTO platform_identity_account_logins/.test(q.sql));
|
|
126
|
+
assert.deepEqual(insLogin.params, [77, 'google', 'g-sub-1', 'jane@example.com']);
|
|
127
|
+
|
|
128
|
+
// Both inserts sit inside ONE transaction: an account never exists without its way in.
|
|
129
|
+
const b = sqls.indexOf('BEGIN');
|
|
130
|
+
const c = sqls.indexOf('COMMIT');
|
|
131
|
+
assert.ok(b >= 0 && c > b, 'a transaction ran');
|
|
132
|
+
assert.ok(sqls.indexOf(insAcct.sql) > b && sqls.indexOf(insLogin.sql) > sqls.indexOf(insAcct.sql) && c > sqls.indexOf(insLogin.sql),
|
|
133
|
+
'account then login, both between BEGIN and COMMIT');
|
|
134
|
+
|
|
135
|
+
const handle = pool.log.find((q) => /SET handle/.test(q.sql));
|
|
136
|
+
assert.deepEqual(handle.params, ['-3', 'janedoe'], 'the handle is seeded from the name, keyed on the minted key');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('the same Google identity signing in again reaches the same person', async () => {
|
|
140
|
+
const pool = fakePool([
|
|
141
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [{ account_id: 77, person_key: '-3' }] }],
|
|
142
|
+
]);
|
|
143
|
+
const out = await logins.createAccountForLogin({ provider: 'google', subject: 'g-sub-1' }, { pool });
|
|
144
|
+
assert.deepEqual(out, { accountId: 77, personKey: '-3', created: false });
|
|
145
|
+
assert.ok(!pool.log.some((q) => /INSERT/.test(q.sql)), 'nothing written');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('a racing double sign-up rolls back and returns the account the other request made', async () => {
|
|
149
|
+
let lookups = 0;
|
|
150
|
+
const pool = fakePool([
|
|
151
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, () => (++lookups === 1 ? { rows: [] } : { rows: [{ account_id: 78, person_key: '-4' }] })],
|
|
152
|
+
[/INSERT INTO platform_identity_accounts/, { rows: [{ id: 79, person_key: '-5' }] }],
|
|
153
|
+
[/INSERT INTO platform_identity_account_logins/, () => { const e = new Error('duplicate key'); e.code = '23505'; throw e; }],
|
|
154
|
+
]);
|
|
155
|
+
const out = await logins.createAccountForLogin({ provider: 'google', subject: 'g-sub-2' }, { pool });
|
|
156
|
+
assert.deepEqual(out, { accountId: 78, personKey: '-4', created: false });
|
|
157
|
+
const sqls = pool.log.map((q) => q.sql);
|
|
158
|
+
assert.ok(sqls.includes('ROLLBACK'), 'our half-made account rolled back with its failed login');
|
|
159
|
+
assert.ok(!sqls.includes('COMMIT'));
|
|
160
|
+
assert.ok(!pool.log.some((q) => /SET handle/.test(q.sql)), 'no handle seeded for a rolled-back account');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('any other failure is not swallowed', async () => {
|
|
164
|
+
const pool = fakePool([
|
|
165
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [] }],
|
|
166
|
+
[/INSERT INTO platform_identity_accounts/, () => { const e = new Error('boom'); e.code = '57014'; throw e; }],
|
|
167
|
+
]);
|
|
168
|
+
await assert.rejects(logins.createAccountForLogin({ provider: 'google', subject: 'g-sub-3' }, { pool }), /boom/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('GitHub accounts and malformed identities are never born here', async () => {
|
|
172
|
+
const pool = fakePool([]);
|
|
173
|
+
await assert.rejects(logins.createAccountForLogin({ provider: 'github', subject: '583231' }, { pool }), /GitHub account is born/);
|
|
174
|
+
await assert.rejects(logins.createAccountForLogin({ provider: 'google', subject: '' }, { pool }), /not a valid provider identity/);
|
|
175
|
+
await assert.rejects(logins.createAccountForLogin({ provider: 'email', subject: 'a@b.c' }, { pool }), /not a valid provider identity/);
|
|
176
|
+
assert.equal(pool.log.length, 0, 'nothing touched the database');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('a lookup never runs for an identity that cannot be one', async () => {
|
|
180
|
+
const pool = fakePool([]);
|
|
181
|
+
assert.equal(await logins.findPersonByLogin({ provider: 'github', subject: 'octocat' }, { pool }), null);
|
|
182
|
+
assert.equal(pool.log.length, 0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('the lookup keys on provider and the provider\'s id only — never an email', async () => {
|
|
186
|
+
const pool = fakePool([]);
|
|
187
|
+
await logins.findPersonByLogin({ provider: 'google', subject: 'g-sub-9' }, { pool });
|
|
188
|
+
assert.equal(pool.log.length, 1);
|
|
189
|
+
assert.match(pool.log[0].sql, /WHERE l\.provider = \$1 AND l\.subject = \$2/);
|
|
190
|
+
assert.doesNotMatch(pool.log[0].sql, /email/);
|
|
191
|
+
assert.deepEqual(pool.log[0].params, ['google', 'g-sub-9']);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// --- signing in with Google (task 1004993, ADR 0396) -------------------------
|
|
195
|
+
|
|
196
|
+
test('a returning Google sign-in finds the person, stamps the way in and answers their handle', async () => {
|
|
197
|
+
const pool = fakePool([
|
|
198
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [{ account_id: 77, person_key: '-3' }] }],
|
|
199
|
+
[/UPDATE platform_identity_account_logins SET last_used_at/, { rows: [], rowCount: 1 }],
|
|
200
|
+
[/SELECT handle, github_login FROM platform_identity_accounts/, { rows: [{ handle: 'janedoe', github_login: null }] }],
|
|
201
|
+
]);
|
|
202
|
+
const out = await logins.signInWithLogin({ provider: 'google', subject: 'g-sub-1', email: 'new@example.com' }, { pool });
|
|
203
|
+
assert.deepEqual(out, { personKey: '-3', handle: 'janedoe', githubLogin: null, created: false });
|
|
204
|
+
const stamp = pool.log.find((q) => /SET last_used_at/.test(q.sql));
|
|
205
|
+
assert.deepEqual(stamp.params, ['google', 'g-sub-1', 'new@example.com'], 'keyed on Google\'s id; the email is only stored');
|
|
206
|
+
assert.ok(!pool.log.some((q) => /INSERT/.test(q.sql)), 'no second person');
|
|
207
|
+
assert.ok(!pool.log.some((q) => /WHERE[^;]*email/.test(q.sql)), 'no statement looks a person up by email');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('a first Google sign-in creates the person and says so', async () => {
|
|
211
|
+
const pool = fakePool([
|
|
212
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [] }],
|
|
213
|
+
[/INSERT INTO platform_identity_accounts/, { rows: [{ id: 80, person_key: '-6' }] }],
|
|
214
|
+
[/INSERT INTO platform_identity_account_logins/, { rows: [], rowCount: 1 }],
|
|
215
|
+
[/UPDATE platform_identity_accounts\s+SET handle/, { rows: [{ handle: 'ana' }], rowCount: 1 }],
|
|
216
|
+
[/SELECT handle, github_login FROM platform_identity_accounts/, { rows: [{ handle: 'ana', github_login: null }] }],
|
|
217
|
+
]);
|
|
218
|
+
const out = await logins.signInWithLogin({ provider: 'google', subject: 'g-new', displayName: 'Ana' }, { pool });
|
|
219
|
+
assert.deepEqual(out, { personKey: '-6', handle: 'ana', githubLogin: null, created: true });
|
|
220
|
+
assert.ok(!pool.log.some((q) => /SET last_used_at/.test(q.sql)), 'a new way in was written with last_used_at already');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('a person who links GitHub later answers their real GitHub login', async () => {
|
|
224
|
+
const pool = fakePool([
|
|
225
|
+
[/FROM platform_identity_account_logins l\s+JOIN/, { rows: [{ account_id: 9, person_key: '583231' }] }],
|
|
226
|
+
[/SELECT handle, github_login FROM platform_identity_accounts/, { rows: [{ handle: 'octo', github_login: 'octocat' }] }],
|
|
227
|
+
]);
|
|
228
|
+
const out = await logins.signInWithLogin({ provider: 'google', subject: 'g-linked' }, { pool });
|
|
229
|
+
assert.equal(out.githubLogin, 'octocat');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('a minted key never takes a GitHub login from a caller, on insert or update', async () => {
|
|
233
|
+
const pool = fakePool([[/INSERT INTO platform_identity_accounts/, { rows: [{ github_id: '-3', handle: 'ana' }] }]]);
|
|
234
|
+
await pi.upsertAccount({ githubId: '-3', githubLogin: '~ana', displayName: 'Ana', activate: true }, { pool });
|
|
235
|
+
const q = pool.log.find((x) => /INSERT INTO platform_identity_accounts/.test(x.sql));
|
|
236
|
+
assert.match(q.sql, /VALUES \(\$1, CASE WHEN \$1::bigint < 0 THEN NULL ELSE \$2 END,/, 'a minted key is inserted with no login');
|
|
237
|
+
assert.match(q.sql, /github_login = CASE WHEN platform_identity_accounts\.github_id < 0 THEN platform_identity_accounts\.github_login ELSE EXCLUDED\.github_login END/,
|
|
238
|
+
'a minted key keeps the login only linking sets');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// --- the route ---------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
const app = express();
|
|
244
|
+
app.use(express.json());
|
|
245
|
+
app.use(attachFail);
|
|
246
|
+
app.use('/api/bongos', profileRoutes());
|
|
247
|
+
app.use('/api/bongos', myProjectsRoutes());
|
|
248
|
+
let server, port;
|
|
249
|
+
before(async () => {
|
|
250
|
+
server = http.createServer(app);
|
|
251
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
252
|
+
port = server.address().port;
|
|
253
|
+
});
|
|
254
|
+
after(async () => { await new Promise((r) => server.close(r)); });
|
|
255
|
+
|
|
256
|
+
function get(pathname) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
http.get({ host: '127.0.0.1', port, path: pathname }, (res) => {
|
|
259
|
+
let data = '';
|
|
260
|
+
res.on('data', (c) => { data += c; });
|
|
261
|
+
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data ? JSON.parse(data) : null }));
|
|
262
|
+
}).on('error', reject);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
test('GET /me/sign-in-methods lists the signed-in person\'s own ways in, and no provider ids', async () => {
|
|
267
|
+
ME = '-3';
|
|
268
|
+
scriptRoute([
|
|
269
|
+
[/FROM platform_identity_account_logins l\s+JOIN platform_identity_accounts a/, (_s, params) => ({
|
|
270
|
+
rows: params[0] === '-3'
|
|
271
|
+
? [{ provider: 'github', email: null, created_at: '2026-10-29T00:00:00Z', last_used_at: null },
|
|
272
|
+
{ provider: 'google', email: 'jane@example.com', created_at: '2026-10-28T00:00:00Z', last_used_at: '2026-10-29T00:00:00Z' }]
|
|
273
|
+
: [],
|
|
274
|
+
})],
|
|
275
|
+
]);
|
|
276
|
+
const res = await get('/api/bongos/me/sign-in-methods');
|
|
277
|
+
assert.equal(res.status, 200);
|
|
278
|
+
assert.equal(res.headers['cache-control'], 'no-store');
|
|
279
|
+
assert.deepEqual(res.body.providers, ['github', 'google']);
|
|
280
|
+
assert.deepEqual(res.body.methods.map((m) => m.provider), ['github', 'google']);
|
|
281
|
+
assert.ok(!JSON.stringify(res.body).includes('subject'), 'provider ids never cross');
|
|
282
|
+
const q = routePool.log[0];
|
|
283
|
+
assert.deepEqual(q.params, ['-3', ['github', 'google']], 'keyed on the signed-in person, never a caller-named one');
|
|
284
|
+
assert.doesNotMatch(q.sql, /l\.subject/, 'the provider id is not even selected');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('GET /me/sign-in-methods answers [] for a system principal without touching the database', async () => {
|
|
288
|
+
ME = 'system:bfg';
|
|
289
|
+
scriptRoute([]);
|
|
290
|
+
const res = await get('/api/bongos/me/sign-in-methods');
|
|
291
|
+
assert.equal(res.status, 200);
|
|
292
|
+
assert.deepEqual(res.body.methods, []);
|
|
293
|
+
assert.equal(routePool.log.length, 0);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('GET /me/sign-in-methods fails closed on a read error', async () => {
|
|
297
|
+
ME = '4242';
|
|
298
|
+
scriptRoute([[/./, () => { throw new Error('db down'); }]]);
|
|
299
|
+
const origError = console.error;
|
|
300
|
+
console.error = () => {};
|
|
301
|
+
try {
|
|
302
|
+
const res = await get('/api/bongos/me/sign-in-methods');
|
|
303
|
+
assert.equal(res.status, 500);
|
|
304
|
+
assert.ok(!('methods' in (res.body || {})));
|
|
305
|
+
} finally {
|
|
306
|
+
console.error = origError;
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('a Google-only person is not relayed to a project until halls can take one (task 1004994)', async () => {
|
|
311
|
+
ME = '-3';
|
|
312
|
+
scriptRoute([]);
|
|
313
|
+
try {
|
|
314
|
+
const res = await new Promise((resolve, reject) => {
|
|
315
|
+
const body = JSON.stringify({ origin: 'https://proj.example.com' });
|
|
316
|
+
const r = http.request({ host: '127.0.0.1', port, path: '/api/bongos/my-projects/join', method: 'POST',
|
|
317
|
+
headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) } }, (resp) => {
|
|
318
|
+
let data = ''; resp.on('data', (c) => { data += c; }); resp.on('end', () => resolve({ status: resp.statusCode, body: JSON.parse(data) }));
|
|
319
|
+
});
|
|
320
|
+
r.on('error', reject); r.end(body);
|
|
321
|
+
});
|
|
322
|
+
assert.equal(res.status, 409);
|
|
323
|
+
assert.equal(res.body.error.code, 'google_join_not_yet');
|
|
324
|
+
assert.equal(routePool.log.length, 0, 'nothing was read or relayed');
|
|
325
|
+
} finally { ME = '4242'; }
|
|
326
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// tests/account_logins_db.mjs — the ways a person signs in, against real SQL
|
|
2
|
+
// (task 1004992, ADR 0393). The DB-free sibling is tests/account_logins.mjs; this
|
|
3
|
+
// file proves what a fake pool cannot execute: the UNIQUE constraints, the
|
|
4
|
+
// sequence, the trigger, the backfill and a real race.
|
|
5
|
+
//
|
|
6
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the INTEGRATION set
|
|
7
|
+
// of scripts/bongos/run-unit-tests.js.
|
|
8
|
+
//
|
|
9
|
+
// Run: DATABASE_URL=... node tests/account_logins_db.mjs
|
|
10
|
+
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { dirname, join } from 'node:path';
|
|
15
|
+
import { strict as assert } from 'node:assert';
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const { Pool } = require('pg');
|
|
18
|
+
const logins = require('../modules/platform-identity/account-logins.js');
|
|
19
|
+
|
|
20
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
22
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
23
|
+
|
|
24
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
25
|
+
try {
|
|
26
|
+
await pool.query('SELECT 1');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.log(`account_logins_db.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
29
|
+
await pool.end().catch(() => {});
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const GH = 90499201; // a GitHub-born account
|
|
34
|
+
const GH_OLD = 90499202; // an account from before migration 034
|
|
35
|
+
const SUB = (s) => `t1004992-${s}`;
|
|
36
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
37
|
+
|
|
38
|
+
async function cleanup() {
|
|
39
|
+
await q(
|
|
40
|
+
`DELETE FROM platform_identity_accounts
|
|
41
|
+
WHERE github_id = ANY($1::bigint[])
|
|
42
|
+
OR id IN (SELECT account_id FROM platform_identity_account_logins WHERE subject LIKE 't1004992-%')`,
|
|
43
|
+
[[GH, GH_OLD]],
|
|
44
|
+
).catch(() => {});
|
|
45
|
+
}
|
|
46
|
+
const loginRows = async (accountId) => (await q(
|
|
47
|
+
`SELECT provider, subject, email FROM platform_identity_account_logins WHERE account_id = $1 ORDER BY provider`,
|
|
48
|
+
[accountId],
|
|
49
|
+
)).rows;
|
|
50
|
+
const accountRow = async (key) => (await q(
|
|
51
|
+
`SELECT id, github_id::text AS key, github_login, handle, profile_state FROM platform_identity_accounts WHERE github_id = $1`,
|
|
52
|
+
[key],
|
|
53
|
+
)).rows[0];
|
|
54
|
+
const code = async (p) => { try { await p; return null; } catch (e) { return e.code; } };
|
|
55
|
+
|
|
56
|
+
let pass = 0;
|
|
57
|
+
const ok = (cond, msg) => { assert.ok(cond, msg); pass++; console.log(` ok - ${msg}`); };
|
|
58
|
+
const same = (actual, expected, msg) => { assert.deepEqual(actual, expected, msg); pass++; console.log(` ok - ${msg}`); };
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
for (const f of [
|
|
62
|
+
'platform_identity_001_tables.sql', 'platform_identity_007_profile_consent.sql',
|
|
63
|
+
'platform_identity_008_handle.sql', 'platform_identity_009_profile_fields.sql',
|
|
64
|
+
'platform_identity_012_account_privacy.sql', 'platform_identity_019_terms_acceptance.sql',
|
|
65
|
+
'platform_identity_024_terms_grandfathered.sql',
|
|
66
|
+
]) await q(migrationSql(f));
|
|
67
|
+
await cleanup();
|
|
68
|
+
|
|
69
|
+
// An account from before this change: created, then its login row removed, as
|
|
70
|
+
// if 034 had not run yet. Re-running 034 must backfill it — and re-running is safe.
|
|
71
|
+
await q(`INSERT INTO platform_identity_accounts (github_id, github_login) VALUES ($1, 'old-octo')`, [GH_OLD]).catch(() => {});
|
|
72
|
+
await q(migrationSql('platform_identity_034_account_logins.sql'));
|
|
73
|
+
const old = await accountRow(GH_OLD);
|
|
74
|
+
await q(`DELETE FROM platform_identity_account_logins WHERE account_id = $1`, [old.id]);
|
|
75
|
+
await q(migrationSql('platform_identity_034_account_logins.sql'));
|
|
76
|
+
same(await loginRows(old.id), [{ provider: 'github', subject: String(GH_OLD), email: null }],
|
|
77
|
+
'every existing account gets its GitHub way in, and the migration re-runs safely');
|
|
78
|
+
|
|
79
|
+
// 1 — a GitHub-born account gets its login row from the trigger, with no writer change.
|
|
80
|
+
await q(`INSERT INTO platform_identity_accounts (github_id, github_login) VALUES ($1, 'octo')`, [GH]);
|
|
81
|
+
const gh = await accountRow(GH);
|
|
82
|
+
same(await loginRows(gh.id), [{ provider: 'github', subject: String(GH), email: null }],
|
|
83
|
+
'an account born through GitHub has its GitHub way in, written by the trigger');
|
|
84
|
+
|
|
85
|
+
// 2 — a person arriving through Google: a minted negative key, no GitHub login.
|
|
86
|
+
const jane = await logins.createAccountForLogin(
|
|
87
|
+
{ provider: 'google', subject: SUB('jane'), email: 'jane@example.com', displayName: 'Jane Doe' }, { pool });
|
|
88
|
+
ok(jane.created && logins.isMintedPersonKey(jane.personKey), `a Google arrival gets a minted negative key (${jane.personKey})`);
|
|
89
|
+
const janeRow = await accountRow(jane.personKey);
|
|
90
|
+
same([janeRow.github_login, janeRow.profile_state], [null, 'provisional'], 'no GitHub login is invented, and the account is provisional');
|
|
91
|
+
ok(/^janedoe(-\d+)?$/.test(janeRow.handle), `the handle grows from the name (${janeRow.handle})`);
|
|
92
|
+
same(await loginRows(janeRow.id), [{ provider: 'google', subject: SUB('jane'), email: 'jane@example.com' }],
|
|
93
|
+
'only the Google way in — the trigger writes no GitHub row for a minted key');
|
|
94
|
+
|
|
95
|
+
// 3 — the same Google identity again reaches the same person.
|
|
96
|
+
const again = await logins.createAccountForLogin({ provider: 'google', subject: SUB('jane') }, { pool });
|
|
97
|
+
same([again.personKey, again.created], [jane.personKey, false], 'signing up twice with one Google account is one person');
|
|
98
|
+
same(await logins.findPersonByLogin({ provider: 'google', subject: SUB('jane') }, { pool }),
|
|
99
|
+
{ accountId: janeRow.id, personKey: jane.personKey }, 'the sign-in lookup finds them');
|
|
100
|
+
|
|
101
|
+
// 4 — a real race: two sign-ups for one new identity at once make one person.
|
|
102
|
+
const [r1, r2] = await Promise.all([
|
|
103
|
+
logins.createAccountForLogin({ provider: 'google', subject: SUB('race') }, { pool }),
|
|
104
|
+
logins.createAccountForLogin({ provider: 'google', subject: SUB('race') }, { pool }),
|
|
105
|
+
]);
|
|
106
|
+
same(r1.personKey, r2.personKey, 'racing sign-ups land on one key');
|
|
107
|
+
const raced = (await q(
|
|
108
|
+
`SELECT count(*)::int AS n FROM platform_identity_account_logins WHERE provider = 'google' AND subject = $1`, [SUB('race')],
|
|
109
|
+
)).rows[0].n;
|
|
110
|
+
same(raced, 1, 'and one way in');
|
|
111
|
+
const orphans = (await q(
|
|
112
|
+
`SELECT count(*)::int AS n FROM platform_identity_accounts a
|
|
113
|
+
WHERE a.github_id < 0 AND NOT EXISTS (SELECT 1 FROM platform_identity_account_logins l WHERE l.account_id = a.id)`,
|
|
114
|
+
)).rows[0].n;
|
|
115
|
+
same(orphans, 0, 'the losing request left no account without a way in');
|
|
116
|
+
|
|
117
|
+
// 5 — the database refuses a second owner, and a second way in of one kind.
|
|
118
|
+
same(await code(q(
|
|
119
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'google', $2)`, [gh.id, SUB('jane')],
|
|
120
|
+
)), '23505', 'a Google identity can belong to only one person');
|
|
121
|
+
await q(`INSERT INTO platform_identity_account_logins (account_id, provider, subject, email) VALUES ($1, 'google', $2, 'octo@example.com')`,
|
|
122
|
+
[gh.id, SUB('octo')]);
|
|
123
|
+
same(await code(q(
|
|
124
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'google', $2)`, [gh.id, SUB('octo2')],
|
|
125
|
+
)), '23505', 'a person holds at most one Google way in');
|
|
126
|
+
same(await code(q(
|
|
127
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject) VALUES ($1, 'email', 'a@b.c')`, [gh.id],
|
|
128
|
+
)), '23514', 'an unknown provider is refused by the database');
|
|
129
|
+
|
|
130
|
+
// 6 — the own-scope read lists the person's ways in, GitHub first, and no ids.
|
|
131
|
+
const listed = await logins.listLoginsForPerson(String(GH), { pool });
|
|
132
|
+
same(listed.map((m) => [m.provider, m.email]), [['github', null], ['google', 'octo@example.com']],
|
|
133
|
+
'a person with both ways in sees both, GitHub first');
|
|
134
|
+
ok(listed.every((m) => !('subject' in m)), 'provider ids never leave the read');
|
|
135
|
+
same((await logins.listLoginsForPerson(jane.personKey, { pool })).map((m) => m.provider), ['google'],
|
|
136
|
+
'a minted key reads its own ways in');
|
|
137
|
+
|
|
138
|
+
// 7 — a minted key stays the key: the sequence never hands out a GitHub-shaped id.
|
|
139
|
+
const keys = (await q(`SELECT github_id FROM platform_identity_accounts WHERE id IN (
|
|
140
|
+
SELECT account_id FROM platform_identity_account_logins WHERE subject LIKE 't1004992-%' AND provider = 'google')
|
|
141
|
+
AND github_id < 0`)).rows;
|
|
142
|
+
ok(keys.length >= 2, 'every Google-born fixture holds a negative key');
|
|
143
|
+
|
|
144
|
+
// 8 — signing in with Google (task 1004993, ADR 0396): the same person every time,
|
|
145
|
+
// found by Google's id; and the hub's own sign-in never writes the core stand-in
|
|
146
|
+
// (or any login) onto a minted key.
|
|
147
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
148
|
+
const first = await logins.signInWithLogin({ provider: 'google', subject: SUB('signin'), email: 'one@example.com', displayName: 'Sign In' }, { pool });
|
|
149
|
+
ok(first.created && /^-\d+$/.test(first.personKey), 'a first Google sign-in creates a person with a minted key');
|
|
150
|
+
const second = await logins.signInWithLogin({ provider: 'google', subject: SUB('signin'), email: 'two@example.com' }, { pool });
|
|
151
|
+
same([second.personKey, second.created], [first.personKey, false], 'signing in again reaches the same person');
|
|
152
|
+
same((await logins.listLoginsForPerson(first.personKey, { pool }))[0].email, 'two@example.com', 'the shown email follows the latest sign-in');
|
|
153
|
+
await pi.upsertAccount({ githubId: first.personKey, githubLogin: '~signin', displayName: 'Sign In', activate: true }, { pool });
|
|
154
|
+
const after = await accountRow(first.personKey);
|
|
155
|
+
same([after.github_login, after.profile_state], [null, 'active'], 'the hub sign-in activates the person and writes no login');
|
|
156
|
+
|
|
157
|
+
console.log(`\naccount_logins_db.mjs: ${pass} passed`);
|
|
158
|
+
} finally {
|
|
159
|
+
await cleanup();
|
|
160
|
+
await pool.end();
|
|
161
|
+
}
|