@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,246 @@
|
|
|
1
|
+
// src/bongos/routes/auth-google.js — the Google sign-in routes, and the browser
|
|
2
|
+
// page that approves a command-line sign-in (task 1004993, ADR 0396).
|
|
3
|
+
//
|
|
4
|
+
// WHY ITS OWN FILE. routes/auth.js is the front door for GitHub and sits near the
|
|
5
|
+
// file-size ratchet; these routes reuse its cookie and redirect helpers (all
|
|
6
|
+
// exported from there) rather than copying them, so both ways in keep one
|
|
7
|
+
// definition of "host-only session cookie", "safe return target" and "handoff to
|
|
8
|
+
// builders./status.".
|
|
9
|
+
//
|
|
10
|
+
// GET /auth/google/start → Google's account picker (state + nonce cookie)
|
|
11
|
+
// GET /auth/google/callback → checks the state, trades the code, signs in
|
|
12
|
+
// GET /auth/device → the page a terminal sends the person to
|
|
13
|
+
// POST /auth/device/approve → the signed-in person approves or cancels it
|
|
14
|
+
//
|
|
15
|
+
// Every route answers only where googleConfigured() — the owner's Google app AND
|
|
16
|
+
// the hub that keeps who signs in with it (auth-google.js).
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const express = require('express');
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
|
|
22
|
+
const auth = require('../auth');
|
|
23
|
+
const db = require('../db');
|
|
24
|
+
const seams = require('../../module-seams');
|
|
25
|
+
const { pool } = require('../pool');
|
|
26
|
+
const { parseCookie, validateOrRespond } = require('./_helpers');
|
|
27
|
+
const { googleConfigured, buildGoogleAuthorizeUrl, exchangeGoogleCode, completeGoogleFlow } = require('../auth-google');
|
|
28
|
+
const { pendingFlow, decideDeviceFlow, normalizeUserCode } = require('../device-approval');
|
|
29
|
+
const { renderDeviceApprovalPage } = require('../sign-in-pages');
|
|
30
|
+
const authRoutes = require('./auth');
|
|
31
|
+
const log = require('../logger').logger.child({ src: 'auth-google' });
|
|
32
|
+
|
|
33
|
+
// state.nonce for one Google sign-in, host-only, alive as long as GitHub's handshake.
|
|
34
|
+
const GOOGLE_STATE_COOKIE = 'bongos_google_state';
|
|
35
|
+
// The redirect URI the owner registers in Google Cloud. Pinned to the canonical
|
|
36
|
+
// origin (the same rule as GitHub's, task #734): never derived from the request host.
|
|
37
|
+
const GOOGLE_CALLBACK_PATH = '/api/bongos/auth/google/callback';
|
|
38
|
+
|
|
39
|
+
function googleRedirectUri() {
|
|
40
|
+
return `${authRoutes.webRedirectOrigin().replace(/\/+$/, '')}${GOOGLE_CALLBACK_PATH}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSecure(req) {
|
|
44
|
+
return !!(req.secure || req.headers['x-forwarded-proto'] === 'https');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handshakeCookie(req, res, name, value) {
|
|
48
|
+
res.cookie(name, value, {
|
|
49
|
+
httpOnly: true,
|
|
50
|
+
sameSite: 'lax',
|
|
51
|
+
secure: isSecure(req),
|
|
52
|
+
maxAge: authRoutes.HANDSHAKE_COOKIE_MS,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sendNotOffered(res) {
|
|
57
|
+
return res.status(404).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
58
|
+
'Google sign-in isn’t offered here',
|
|
59
|
+
'This site signs people in with GitHub only.',
|
|
60
|
+
{ subtitle: 'Sign in with GitHub instead.' },
|
|
61
|
+
));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The approval page's own address for a code (relative, so it is a safe return target).
|
|
65
|
+
function devicePagePath(req, userCode) {
|
|
66
|
+
const base = authRoutes.apiBase(req);
|
|
67
|
+
return userCode ? `${base}/auth/device?code=${encodeURIComponent(userCode)}` : `${base}/auth/device`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// A same-site form post only. The session cookie is SameSite=Lax (a cross-site
|
|
71
|
+
// POST never carries it), and this refuses a cross-site request outright too.
|
|
72
|
+
function sameSitePost(req) {
|
|
73
|
+
const site = req.headers['sec-fetch-site'];
|
|
74
|
+
if (site && site !== 'same-origin') return false;
|
|
75
|
+
const origin = req.headers.origin;
|
|
76
|
+
if (!origin) return true;
|
|
77
|
+
try { return new URL(origin).hostname.toLowerCase() === authRoutes.reqHost(req); } catch { return false; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = function buildAuthGoogleRouter() {
|
|
81
|
+
const router = express.Router();
|
|
82
|
+
|
|
83
|
+
// rank: public — entry into the Google OAuth handshake; identity establishment only.
|
|
84
|
+
router.get('/auth/google/start', (req, res) => {
|
|
85
|
+
if (!googleConfigured()) return sendNotOffered(res);
|
|
86
|
+
// Begun on builders./status.? Restart on the canonical host, where the
|
|
87
|
+
// callback lands and can read the host-only state cookie back.
|
|
88
|
+
const host = authRoutes.reqHost(req);
|
|
89
|
+
if (authRoutes.isSatelliteHubHost(host)) {
|
|
90
|
+
const raw = authRoutes.safeReturnTo(req.query.return) || authRoutes.safeReturnTo(req.headers.referer) || '/';
|
|
91
|
+
const back = raw.startsWith('/') ? `https://${host}${raw}` : raw;
|
|
92
|
+
return res.redirect(`${authRoutes.webRedirectOrigin().replace(/\/+$/, '')}${authRoutes.apiBase(req)}/auth/google/start?${new URLSearchParams({ return: back })}`);
|
|
93
|
+
}
|
|
94
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
95
|
+
const nonce = crypto.randomBytes(16).toString('hex');
|
|
96
|
+
handshakeCookie(req, res, GOOGLE_STATE_COOKIE, `${state}.${nonce}`);
|
|
97
|
+
const returnTo = authRoutes.safeReturnTo(req.query.return) || authRoutes.safeReturnTo(req.headers.referer);
|
|
98
|
+
if (returnTo) handshakeCookie(req, res, authRoutes.RETURN_COOKIE, returnTo);
|
|
99
|
+
res.redirect(buildGoogleAuthorizeUrl({ state, nonce, redirectUri: googleRedirectUri() }));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// rank: public — OAuth callback; the state cookie + code are the bearer.
|
|
103
|
+
router.get('/auth/google/callback', async (req, res) => {
|
|
104
|
+
if (!googleConfigured()) return sendNotOffered(res);
|
|
105
|
+
const startHref = `${authRoutes.apiBase(req)}/auth/google/start`;
|
|
106
|
+
const [expectedState, nonce] = String(parseCookie(req, GOOGLE_STATE_COOKIE) || '').split('.');
|
|
107
|
+
const { state, code, error } = req.query;
|
|
108
|
+
res.clearCookie(GOOGLE_STATE_COOKIE);
|
|
109
|
+
if (error) {
|
|
110
|
+
// access_denied is the person pressing Cancel at Google: not a fault.
|
|
111
|
+
return res.status(400).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
112
|
+
error === 'access_denied' ? 'You cancelled the Google sign-in' : 'Google refused the sign-in',
|
|
113
|
+
error === 'access_denied' ? 'Nothing was changed. You can try again whenever you like.' : `Google answered "${error}". Try again; if it keeps happening, ask the ${authRoutes.admitterLabel()} to check the Google sign-in app.`,
|
|
114
|
+
{ startHref, note: 'Your account is unchanged.' },
|
|
115
|
+
));
|
|
116
|
+
}
|
|
117
|
+
if (!code || !state || !expectedState || !nonce || state !== expectedState) {
|
|
118
|
+
return res.status(400).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
119
|
+
'The sign-in took too long',
|
|
120
|
+
'This sign-in started too long ago or in another browser. Start it again.',
|
|
121
|
+
{ startHref, note: 'Your account is unchanged.' },
|
|
122
|
+
));
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const identity = await exchangeGoogleCode({ code, redirectUri: googleRedirectUri(), nonce });
|
|
126
|
+
const { token, expiresAt, created } = await completeGoogleFlow({ identity, userAgent: req.headers['user-agent'] });
|
|
127
|
+
authRoutes.setSessionCookie(req, res, token, expiresAt);
|
|
128
|
+
const returnTo = authRoutes.safeReturnTo(parseCookie(req, authRoutes.RETURN_COOKIE));
|
|
129
|
+
res.clearCookie(authRoutes.RETURN_COOKIE);
|
|
130
|
+
// A brand-new person chooses the name people see first (owner, 2026-10-29) —
|
|
131
|
+
// unless they were mid-way through approving a terminal sign-in.
|
|
132
|
+
const toDevicePage = returnTo && returnTo.startsWith(`${authRoutes.apiBase(req)}/auth/device`);
|
|
133
|
+
if (created && !toDevicePage) return res.redirect('/account?welcome=google');
|
|
134
|
+
const handoff = returnTo && !returnTo.startsWith('/') && authRoutes.handoffUrlFor(req, returnTo, token, expiresAt);
|
|
135
|
+
return res.redirect(handoff || returnTo || authRoutes.postLoginLanding());
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err && err.code === 'NOT_APPROVED') {
|
|
138
|
+
return res.status(403).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
139
|
+
'This site takes invited builders only',
|
|
140
|
+
`New accounts here need an invite from the ${authRoutes.admitterLabel()}, and invites are sent to GitHub names. Ask the ${authRoutes.admitterLabel()} to let you in, then sign in again.`,
|
|
141
|
+
{ subtitle: 'Sign-in is for approved builders.', cta: { href: '/builders', label: 'Go to the hall' } },
|
|
142
|
+
));
|
|
143
|
+
}
|
|
144
|
+
if (err && err.code === 'BUILDER_INACTIVE') {
|
|
145
|
+
return res.status(403).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
146
|
+
'This account has been switched off',
|
|
147
|
+
`This Google account belongs to a builder whose account has been switched off, so it can no longer sign in. If that is a mistake, ask the ${authRoutes.admitterLabel()} to switch it back on.`,
|
|
148
|
+
{ subtitle: 'A switched-off account cannot sign in.', cta: { href: '/builders', label: 'Go to the hall' } },
|
|
149
|
+
));
|
|
150
|
+
}
|
|
151
|
+
log.error({ err: err && err.message }, '[auth] google/callback');
|
|
152
|
+
return res.status(500).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
153
|
+
'The Google sign-in didn’t finish',
|
|
154
|
+
`Something went wrong signing you in with Google. Try again; if it keeps happening, ask the ${authRoutes.admitterLabel()} to look at the server logs.`,
|
|
155
|
+
{ startHref, note: 'Your account is unchanged.' },
|
|
156
|
+
));
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// rank: public — shows a terminal's pending sign-in; approving it is the POST below.
|
|
161
|
+
router.get('/auth/device', async (req, res) => {
|
|
162
|
+
res.set('Cache-Control', 'no-store');
|
|
163
|
+
const enterAction = devicePagePath(req);
|
|
164
|
+
if (!req.query.code) return res.type('text/html').send(renderDeviceApprovalPage({ state: 'enter', action: enterAction }));
|
|
165
|
+
const pending = pendingFlow(req.query.code);
|
|
166
|
+
if (!pending) {
|
|
167
|
+
return res.status(404).type('text/html').send(renderDeviceApprovalPage({
|
|
168
|
+
state: 'enter', action: enterAction,
|
|
169
|
+
problem: 'That code has run out, was already used, or was mistyped. Check it, or start the sign-in again in your terminal.',
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
let session = null;
|
|
173
|
+
try { session = await auth.resolveSession(req); } catch (err) { log.error({ err: err && err.message }, '[auth] device page session'); }
|
|
174
|
+
const here = devicePagePath(req, pending.userCode);
|
|
175
|
+
if (!session || session.status === 'inactive') {
|
|
176
|
+
const base = authRoutes.apiBase(req);
|
|
177
|
+
return res.type('text/html').send(renderDeviceApprovalPage({
|
|
178
|
+
state: 'signin',
|
|
179
|
+
userCode: pending.userCode,
|
|
180
|
+
githubHref: `${base}/auth/web/start?via=github&return=${encodeURIComponent(here)}`,
|
|
181
|
+
googleHref: `${base}/auth/google/start?return=${encodeURIComponent(here)}`,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
return res.type('text/html').send(renderDeviceApprovalPage({
|
|
185
|
+
state: 'confirm',
|
|
186
|
+
userCode: pending.userCode,
|
|
187
|
+
name: session.display_name || session.github_login || 'you',
|
|
188
|
+
action: `${authRoutes.apiBase(req)}/auth/device/approve`,
|
|
189
|
+
}));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// rank: any-builder (own identity only) — mints a CLI session for the signed-in
|
|
193
|
+
// person themselves, from their browser cookie, never from a bearer (the
|
|
194
|
+
// /auth/cli-token/issue rule: a leaked CLI token must not mint more).
|
|
195
|
+
router.post('/auth/device/approve', express.urlencoded({ extended: false, limit: '2kb' }), auth.requireBuilder, async (req, res) => {
|
|
196
|
+
res.set('Cache-Control', 'no-store');
|
|
197
|
+
if (!authRoutes.sessionCookieToken(req) || !sameSitePost(req)) {
|
|
198
|
+
return res.status(403).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
199
|
+
'Approve from the sign-in page',
|
|
200
|
+
'A command-line sign-in can only be approved from its own page in your browser.',
|
|
201
|
+
{ cta: { href: devicePagePath(req), label: 'Open the sign-in page' }, note: 'Nothing was signed in.' },
|
|
202
|
+
));
|
|
203
|
+
}
|
|
204
|
+
// Only the page's own two fields; anything else is not this form.
|
|
205
|
+
if (validateOrRespond(req, res, {
|
|
206
|
+
user_code: { required: true, type: 'string', maxLength: 16 },
|
|
207
|
+
decision: { required: true, type: 'string', enum: ['approve', 'deny'] },
|
|
208
|
+
}, { strict: true })) return;
|
|
209
|
+
const userCode = req.body.user_code;
|
|
210
|
+
const approve = req.body.decision === 'approve';
|
|
211
|
+
const builder = req.builder;
|
|
212
|
+
try {
|
|
213
|
+
const decided = await decideDeviceFlow({
|
|
214
|
+
userCode,
|
|
215
|
+
approve,
|
|
216
|
+
mint: async () => {
|
|
217
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
218
|
+
// A CLI bearer has no expiry and is revoked from Settings (ADR 0294).
|
|
219
|
+
await db.createSession({ builderId: builder.id, token, source: 'cli', userAgent: req.headers['user-agent'] ?? null, expiresAt: null });
|
|
220
|
+
await seams.resolveOptional('onboarding')?.safeMarkStage(pool, builder.id, 'auth_complete', builder);
|
|
221
|
+
return {
|
|
222
|
+
token,
|
|
223
|
+
builder: { id: builder.id, github_id: builder.github_id, github_login: builder.github_login, display_name: builder.display_name, rank: builder.rank },
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
if (!decided) {
|
|
228
|
+
return res.status(404).type('text/html').send(renderDeviceApprovalPage({
|
|
229
|
+
state: 'enter', action: devicePagePath(req),
|
|
230
|
+
problem: 'That code has run out or was already used. Start the sign-in again in your terminal.',
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
return res.type('text/html').send(renderDeviceApprovalPage({ state: 'done', approved: approve }));
|
|
234
|
+
} catch (err) {
|
|
235
|
+
log.error({ err: err && err.message }, '[auth] device/approve');
|
|
236
|
+
return res.status(500).type('text/html').send(authRoutes.renderAuthErrorPage(
|
|
237
|
+
'The approval didn’t go through',
|
|
238
|
+
'Something went wrong. Go back to the sign-in page and press Approve again.',
|
|
239
|
+
{ cta: { href: devicePagePath(req, normalizeUserCode(userCode)), label: 'Back to the sign-in page' } },
|
|
240
|
+
));
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return router;
|
|
245
|
+
};
|
|
246
|
+
|
|
@@ -45,6 +45,11 @@ const seams = require('../../module-seams');
|
|
|
45
45
|
const log = require('../logger').logger.child({ src: 'auth' });
|
|
46
46
|
|
|
47
47
|
const { escapeHtml } = require('../../html-escape'); // the one server escaper (task 1004876)
|
|
48
|
+
// Google sign-in (task 1004993, ADR 0396): the chooser /auth/web/start shows, and
|
|
49
|
+
// the browser-approved device flow the CLI gets when Google is offered.
|
|
50
|
+
const { googleConfigured } = require('../auth-google');
|
|
51
|
+
const { startBrowserDeviceFlow, isBrowserDeviceCode, pollBrowserDeviceFlow } = require('../device-approval');
|
|
52
|
+
const { renderSignInChooser } = require('../sign-in-pages');
|
|
48
53
|
|
|
49
54
|
// Render a small standalone HTML page in the builders' hall palette. Used for
|
|
50
55
|
// OAuth error branches so the visitor sees something coherent rather than a
|
|
@@ -597,10 +602,17 @@ module.exports = function buildAuthRouter() {
|
|
|
597
602
|
|
|
598
603
|
// /auth/device — CLI flow
|
|
599
604
|
// rank: public — auth flow itself; identity establishment, not authorization.
|
|
600
|
-
router.post('/auth/device/start', async (
|
|
605
|
+
router.post('/auth/device/start', async (req, res) => {
|
|
601
606
|
if (!auth.authConfigured()) {
|
|
602
607
|
return res.fail('auth_not_configured', { status: 503, message: 'GitHub OAuth App credentials missing on the server. See blockers/blockers.md#github-oauth-app.' });
|
|
603
608
|
}
|
|
609
|
+
// Google offered here: the terminal is sent to this instance's own approval
|
|
610
|
+
// page, where the person signs in with GitHub or Google (device-approval.js).
|
|
611
|
+
if (googleConfigured()) {
|
|
612
|
+
return res.json(startBrowserDeviceFlow({
|
|
613
|
+
pageUrl: `${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/device`,
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
604
616
|
try {
|
|
605
617
|
const out = await auth.startDeviceFlow();
|
|
606
618
|
res.json(out);
|
|
@@ -629,6 +641,7 @@ module.exports = function buildAuthRouter() {
|
|
|
629
641
|
})) return;
|
|
630
642
|
const { device_code: deviceCode } = req.body || {};
|
|
631
643
|
if (!deviceCode) return res.fail('device_code_required', 400);
|
|
644
|
+
if (isBrowserDeviceCode(deviceCode)) return res.json(pollBrowserDeviceFlow(deviceCode));
|
|
632
645
|
try {
|
|
633
646
|
const out = await auth.pollDeviceFlow(deviceCode, req.headers['user-agent']);
|
|
634
647
|
res.json(out);
|
|
@@ -711,6 +724,7 @@ module.exports = function buildAuthRouter() {
|
|
|
711
724
|
const params = new URLSearchParams({ return: back });
|
|
712
725
|
if (wantsRepos) params.set('repos', '1');
|
|
713
726
|
if (wantsSwitch) params.set('switch', '1');
|
|
727
|
+
if (req.query.via === 'github') params.set('via', 'github');
|
|
714
728
|
return res.redirect(`${WEB_REDIRECT_ORIGIN.replace(/\/+$/, '')}${apiBase(req)}/auth/web/start?${params}`);
|
|
715
729
|
}
|
|
716
730
|
// Already signed in on the canonical host and headed for a satellite hub
|
|
@@ -730,6 +744,22 @@ module.exports = function buildAuthRouter() {
|
|
|
730
744
|
}
|
|
731
745
|
}
|
|
732
746
|
}
|
|
747
|
+
// Google offered here (task 1004993, ADR 0396): every sign-in link on the site
|
|
748
|
+
// lands on one chooser — Continue with GitHub or Continue with Google — so no
|
|
749
|
+
// page needs its own pair of buttons. `via=github` is the GitHub button (and
|
|
750
|
+
// any door that needs GitHub itself); "New account" names a GitHub account.
|
|
751
|
+
if (req.query.via !== 'github' && !wantsSwitch && googleConfigured()) {
|
|
752
|
+
const ret = safeReturnTo(req.query.return) || safeReturnTo(req.headers.referer);
|
|
753
|
+
const gh = new URLSearchParams({ via: 'github' });
|
|
754
|
+
if (ret) gh.set('return', ret);
|
|
755
|
+
if (wantsRepos) gh.set('repos', '1');
|
|
756
|
+
const goo = new URLSearchParams(ret ? { return: ret } : {});
|
|
757
|
+
res.set('Cache-Control', 'no-store');
|
|
758
|
+
return res.type('text/html').send(renderSignInChooser({
|
|
759
|
+
githubHref: `${apiBase(req)}/auth/web/start?${gh}`,
|
|
760
|
+
googleHref: `${apiBase(req)}/auth/google/start${goo.toString() ? `?${goo}` : ''}`,
|
|
761
|
+
}));
|
|
762
|
+
}
|
|
733
763
|
const state = crypto.randomBytes(16).toString('hex');
|
|
734
764
|
// redirect_uri is PINNED to the canonical origin (#734) — never derived from
|
|
735
765
|
// the request host. GitHub only knows the apex callback, so a sign-in begun
|
|
@@ -1406,6 +1436,13 @@ module.exports.redeemHandoffCode = redeemHandoffCode;
|
|
|
1406
1436
|
module.exports.handoffUrlFor = handoffUrlFor;
|
|
1407
1437
|
module.exports.HANDOFF_TTL_MS = HANDOFF_TTL_MS;
|
|
1408
1438
|
module.exports.reqHost = reqHost;
|
|
1439
|
+
// The helpers the Google sign-in routes reuse (routes/auth-google.js, task 1004993),
|
|
1440
|
+
// so both ways in share one cookie, host and return-target rule.
|
|
1441
|
+
module.exports.isSatelliteHubHost = isSatelliteHubHost;
|
|
1442
|
+
module.exports.sessionCookieToken = sessionCookieToken;
|
|
1443
|
+
module.exports.apiBase = apiBase;
|
|
1444
|
+
module.exports.RETURN_COOKIE = RETURN_COOKIE;
|
|
1445
|
+
module.exports.webRedirectOrigin = () => WEB_REDIRECT_ORIGIN;
|
|
1409
1446
|
// The auth-error page renderer (task 1439): pure over the branding pack, so a
|
|
1410
1447
|
// test can prove every visible string is brand-owned (no OTB lore baked in).
|
|
1411
1448
|
module.exports.renderAuthErrorPage = renderAuthErrorPage;
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
// in the background. The builder polls status to see it land.
|
|
14
14
|
// - concurrent triggers: a simple in-memory flag prevents two dumps from
|
|
15
15
|
// running simultaneously (one droplet, one disk, no need for DB state).
|
|
16
|
+
// - trigger vs the nightly timer: the timer runs db-backup-nightly.sh outside
|
|
17
|
+
// this process, so the flag cannot see it. The SCRIPT takes a per-database
|
|
18
|
+
// flock under the backup dir (task 1004966), and both paths run that one
|
|
19
|
+
// script, so they share the lock. A run that finds it held exits 0 with a
|
|
20
|
+
// `gds-db-backup: SKIPPED` line, which the trigger logs as a skip.
|
|
16
21
|
// - the backup dir and script paths default to the on-droplet locations but
|
|
17
22
|
// can be overridden via env vars for local tests.
|
|
18
23
|
|
|
@@ -42,6 +47,9 @@ const DB_NAME = resolveEnv('BACKUP_DB')
|
|
|
42
47
|
const BACKUP_SCRIPT = resolveEnv('BACKUP_SCRIPT')
|
|
43
48
|
|| path.join(REPO_ROOT, 'scripts', 'bongos', 'db-backup-nightly.sh');
|
|
44
49
|
|
|
50
|
+
// The line db-backup-nightly.sh prints when its flock is already held (task 1004966).
|
|
51
|
+
const SKIPPED_MARK = 'gds-db-backup: SKIPPED';
|
|
52
|
+
|
|
45
53
|
// In-memory guard: true while a trigger-initiated dump is running.
|
|
46
54
|
let triggerRunning = false;
|
|
47
55
|
|
|
@@ -130,7 +138,10 @@ function backupDeskRows(dump, running) {
|
|
|
130
138
|
return [{ id: 'now', name: 'Back up now', fact, facts: dump && dump.stale ? ['the last one is stale'] : [], tool: true, ref: { type: 'backup', id: 'now' }, acts: running ? [] : [{ key: 'backup' }] }];
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
|
|
141
|
+
// `deps.execFile` is the test seam (task 1004924): the single-flight lock below
|
|
142
|
+
// is pinned by tests/backup_trigger_lock.mjs without spawning a real dump.
|
|
143
|
+
module.exports = function buildBackupRouter(deps = {}) {
|
|
144
|
+
const runFile = deps.execFile || execFile;
|
|
134
145
|
seams.contribute('governor.docket', { desk: 'treasury.backup', gate: { permission: 'backup.manage' }, read: async () => backupDeskRows(await newestDumpAge(), triggerRunning) });
|
|
135
146
|
const router = express.Router();
|
|
136
147
|
|
|
@@ -165,15 +176,26 @@ module.exports = function buildBackupRouter() {
|
|
|
165
176
|
const builderId = req.builder && req.builder.id;
|
|
166
177
|
log.info(`[gds] backup triggered by builder ${builderId} at ${startedAt}`);
|
|
167
178
|
|
|
168
|
-
|
|
179
|
+
// A spawn that throws synchronously never reaches the callback, so it must
|
|
180
|
+
// release the lock itself — or every later trigger 409s until a restart
|
|
181
|
+
// (task 1004924).
|
|
182
|
+
try {
|
|
183
|
+
runFile('bash', [BACKUP_SCRIPT], { cwd: REPO_ROOT, timeout: 120_000 }, (err, stdout, stderr) => {
|
|
184
|
+
triggerRunning = false;
|
|
185
|
+
if (err) {
|
|
186
|
+
log.error(`[gds] backup trigger failed (builder ${builderId}):`, err.message);
|
|
187
|
+
if (stderr) log.error('[gds] backup stderr:', stderr.toString().trim());
|
|
188
|
+
} else if (String(stdout || '').includes(SKIPPED_MARK)) {
|
|
189
|
+
log.info(`[gds] backup trigger skipped: another backup already holds the lock (builder ${builderId})`);
|
|
190
|
+
} else {
|
|
191
|
+
log.info(`[gds] backup trigger completed (builder ${builderId})`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} catch (e) {
|
|
169
195
|
triggerRunning = false;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
} else {
|
|
174
|
-
log.info(`[gds] backup trigger completed (builder ${builderId})`);
|
|
175
|
-
}
|
|
176
|
-
});
|
|
196
|
+
log.error(`[gds] backup trigger could not start (builder ${builderId}):`, e && e.message);
|
|
197
|
+
return res.fail('backup_start_failed', { status: 500, message: 'The backup could not be started — see the server log.' });
|
|
198
|
+
}
|
|
177
199
|
|
|
178
200
|
res.status(202).json({
|
|
179
201
|
ok: true,
|
|
@@ -192,5 +214,7 @@ module.exports.parseDumpTimestamp = parseDumpTimestamp;
|
|
|
192
214
|
module.exports.dumpAgeHours = dumpAgeHours;
|
|
193
215
|
module.exports.isStaleDump = isStaleDump;
|
|
194
216
|
module.exports.STALE_AFTER_HOURS = STALE_AFTER_HOURS;
|
|
217
|
+
module.exports.SKIPPED_MARK = SKIPPED_MARK;
|
|
218
|
+
module.exports.BACKUP_SCRIPT = BACKUP_SCRIPT;
|
|
195
219
|
|
|
196
220
|
module.exports.backupDeskRows = backupDeskRows;
|
|
@@ -22,8 +22,6 @@
|
|
|
22
22
|
// lifecycle module (BV1.R86) and are resolved through the `lifecycle` kernel port
|
|
23
23
|
// (`lc()`), never a direct module require — the core↔module boundary rule.
|
|
24
24
|
|
|
25
|
-
const path = require('node:path');
|
|
26
|
-
const { execFile } = require('node:child_process');
|
|
27
25
|
const express = require('express');
|
|
28
26
|
const auth = require('../auth');
|
|
29
27
|
// WHO DECIDES AT TREASURY (task 1004657; ADR 0363 D5): the budget write refuses a
|
|
@@ -31,6 +29,7 @@ const auth = require('../auth');
|
|
|
31
29
|
// matter sets the limit through setMonthlyBudget directly (the governor.matterAct
|
|
32
30
|
// below), never through the route, so the sitting is never refused by its own gate.
|
|
33
31
|
const { requireActAlone } = require('../act-alone');
|
|
32
|
+
const { setCapabilityGrants } = require('../task-needs');
|
|
34
33
|
const db = require('../db');
|
|
35
34
|
const { validateOrRespond, LIMITS, parseId } = require('./_helpers');
|
|
36
35
|
// The onboarding state machine carved into modules/onboarding/ (BV1.R81 / ADR
|
|
@@ -109,10 +108,6 @@ const discordIsLinked = (builderId) =>
|
|
|
109
108
|
// picks up the latest credit_log row via GET /me/pending-rank-change). Audit
|
|
110
109
|
// log proper lands later (R36).
|
|
111
110
|
|
|
112
|
-
// Resolve the repo root from this file. Used to invoke scripts/discord/post.js
|
|
113
|
-
// regardless of where the node process was started from.
|
|
114
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
115
|
-
|
|
116
111
|
// Map a rank id (db enum string) to its display form used in the Discord
|
|
117
112
|
// broadcast — capitalized, no surprises. Centralized here so the wording is
|
|
118
113
|
// consistent everywhere ceremony surfaces the rank.
|
|
@@ -143,34 +138,37 @@ function buildRankChangeBroadcast({ displayName, direction, newRank }) {
|
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
// Fire the rank-change broadcast in the background. Best-effort: a post
|
|
146
|
-
// failure must never undo a successful rank change
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
141
|
+
// failure must never undo a successful rank change, so the route never awaits
|
|
142
|
+
// this and every failure is swallowed into a log line.
|
|
143
|
+
//
|
|
144
|
+
// IN-PROCESS, THROUGH THE PORT (task 1004962). This used to spawn a whole
|
|
145
|
+
// `node scripts/discord/post.js` per rank change — 50-100 MB and ~300 ms of CPU
|
|
146
|
+
// each, with nothing bounding a burst. It now calls the discord module's
|
|
147
|
+
// `discord.post` port, resolved per call (the module registers it at mount and
|
|
148
|
+
// may mount after this router). Never a require into modules/ — the core→module
|
|
149
|
+
// edge ADR 0083 forbids. No discord module, or no #ship-news webhook configured
|
|
150
|
+
// (the port answers posted:false), and nothing is posted; the rank change is
|
|
151
|
+
// untouched either way.
|
|
152
|
+
//
|
|
153
|
+
// CHAT_DRY_RUN=1 short-circuits to a "[broadcast dry-run]" log line — the smoke
|
|
154
|
+
// path's guard so a test rank flip doesn't actually hit Discord. The name is a
|
|
155
|
+
// legacy artifact; it gates all broadcast transports, not just Chat.
|
|
156
|
+
//
|
|
157
|
+
// Returns the post's promise (always resolves) so tests can await the outcome.
|
|
153
158
|
function fireDiscordBroadcast(text) {
|
|
154
|
-
if (!text) return;
|
|
159
|
+
if (!text) return Promise.resolve({ posted: false, reason: 'no_text' });
|
|
155
160
|
if (process.env.CHAT_DRY_RUN === '1') {
|
|
156
161
|
log.info(`[gds] [broadcast dry-run] would post: ${text}`);
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
execFile(
|
|
161
|
-
'node',
|
|
162
|
-
['scripts/discord/post.js', text],
|
|
163
|
-
{ cwd: REPO_ROOT, timeout: 10_000 },
|
|
164
|
-
(err, _stdout, stderr) => {
|
|
165
|
-
if (err) {
|
|
166
|
-
log.info('[gds] rank-change broadcast failed (non-blocking):', err.message);
|
|
167
|
-
if (stderr) log.info('[gds] discord stderr:', stderr.toString().trim());
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
log.info('[gds] rank-change broadcast spawn failed (non-blocking):', e && e.message);
|
|
162
|
+
return Promise.resolve({ posted: false, reason: 'dry_run' });
|
|
173
163
|
}
|
|
164
|
+
const post = seams.resolveOptional('discord.post', null);
|
|
165
|
+
if (typeof post !== 'function') return Promise.resolve({ posted: false, reason: 'no_discord' });
|
|
166
|
+
return Promise.resolve()
|
|
167
|
+
.then(() => post({ channel: 'ship_news', text }))
|
|
168
|
+
.catch((e) => {
|
|
169
|
+
log.info(`[gds] rank-change broadcast failed (non-blocking): ${e && e.message}`);
|
|
170
|
+
return { posted: false, reason: 'post_failed' };
|
|
171
|
+
});
|
|
174
172
|
}
|
|
175
173
|
|
|
176
174
|
// The People desk's builder rows (task 1004559): one row per active builder the
|
|
@@ -422,6 +420,34 @@ module.exports = function buildBuildersRouter() {
|
|
|
422
420
|
}
|
|
423
421
|
});
|
|
424
422
|
|
|
423
|
+
// PATCH /builders/:id/capabilities — the owner gives or takes a builder's server
|
|
424
|
+
// access (task 1004950, ADR 0395). Body: { server: boolean }. A task that needs
|
|
425
|
+
// the server is shown to and claimable by a builder only while this is on.
|
|
426
|
+
// Default off. Only the person in the owner seat may set it (403 not_project_owner),
|
|
427
|
+
// read fresh per request like rank — an Archon who is not the owner cannot.
|
|
428
|
+
// rank: archon — changes what another builder may claim. Gate:
|
|
429
|
+
// requirePermission('builder.status.set') AND the owner seat (in-handler).
|
|
430
|
+
router.patch('/builders/:id/capabilities', auth.requireBuilder, auth.requirePermission('builder.status.set'), async (req, res) => {
|
|
431
|
+
if (validateOrRespond(req, res, { server: { required: true, type: 'boolean' } })) return;
|
|
432
|
+
const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
|
|
433
|
+
if (targetId === null) return;
|
|
434
|
+
try {
|
|
435
|
+
// The owner-seat check runs inside setCapabilityGrants, at the write.
|
|
436
|
+
const builder = await setCapabilityGrants({
|
|
437
|
+
actorBuilderId: req.builder.id, builderId: targetId, grants: req.body.server ? ['server'] : [],
|
|
438
|
+
});
|
|
439
|
+
if (!builder) return res.fail('builder_not_found', 404);
|
|
440
|
+
res.json({ builder });
|
|
441
|
+
} catch (err) {
|
|
442
|
+
if (err && err.code === 'NOT_PROJECT_OWNER') {
|
|
443
|
+
return res.fail('not_project_owner', { status: 403, message: 'Only the project owner can give or take server access.' });
|
|
444
|
+
}
|
|
445
|
+
if (err && err.code === 'TARGET_IS_SYSTEM_PRINCIPAL') return res.fail('target_is_system_principal', 403);
|
|
446
|
+
log.error('[gds] PATCH /builders/:id/capabilities', err);
|
|
447
|
+
res.fail('capability_change_failed', { status: 500, message: 'internal error' });
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
425
451
|
// PATCH /builders/:id/budget — set or clear a builder's monthly spend ceiling
|
|
426
452
|
// (V3.R84 #302, criterion C3). Archon-only. Body: { monthly_budget_usd: <num> }
|
|
427
453
|
// sets the per-builder override — the manual emergency brake; a positive value
|
|
@@ -834,4 +860,5 @@ module.exports = function buildBuildersRouter() {
|
|
|
834
860
|
};
|
|
835
861
|
|
|
836
862
|
module.exports.builderDeskRows = builderDeskRows;
|
|
863
|
+
module.exports.fireDiscordBroadcast = fireDiscordBroadcast;
|
|
837
864
|
module.exports.budgetDeskRows = budgetDeskRows;
|
|
@@ -25,6 +25,7 @@ const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo
|
|
|
25
25
|
// pin to the real implementations.
|
|
26
26
|
const githubRepos = require('../github-repos');
|
|
27
27
|
const { validateOrRespond } = require('./_helpers');
|
|
28
|
+
const prelaunch = require('../prelaunch-gate');
|
|
28
29
|
const log = require('../logger').logger.child({ src: 'github' });
|
|
29
30
|
|
|
30
31
|
// GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
|
|
@@ -228,6 +229,14 @@ module.exports = function buildGithubRouter() {
|
|
|
228
229
|
message: 'Make a repository private from its project’s manage page — the platform has to give the project its own deploy key first, or its next deploy would break.',
|
|
229
230
|
});
|
|
230
231
|
}
|
|
232
|
+
// Pre-launch (stealth) keeps every project's code private (task 1004998): while
|
|
233
|
+
// the instance's password gate is on, nothing here flips a repo public.
|
|
234
|
+
if (prelaunch.enabled()) {
|
|
235
|
+
return res.fail('public_repo_prelaunch', {
|
|
236
|
+
status: 409,
|
|
237
|
+
message: 'While we are in pre-launch, every project’s code stays private, so a repository can’t be made public yet.',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
231
240
|
const token = await resolveRepoToken(req, res);
|
|
232
241
|
if (!token) return;
|
|
233
242
|
try {
|
package/src/bongos/routes/me.js
CHANGED
|
@@ -208,6 +208,12 @@ module.exports = function buildMeRouter() {
|
|
|
208
208
|
// task 1004777: stuck page tweaks this builder should be told about, through
|
|
209
209
|
// the copy-desk PORT (core never imports the module, ADR 0093). Started here so
|
|
210
210
|
// it runs beside the fan-out below; [] when copy-desk is off or the read fails.
|
|
211
|
+
// task 1004914 (N10): open holds on this builder's pay. Pay is never withheld
|
|
212
|
+
// silently (ADR 0385 D8), so each open hold is a need on every surface /me
|
|
213
|
+
// feeds. Fail-open to none: a money read must never break a profile.
|
|
214
|
+
const openPayoutHoldsP = (async () => {
|
|
215
|
+
try { return await require('../payout-gates').openHoldsFor(req.builder.id); } catch (_) { return null; }
|
|
216
|
+
})();
|
|
211
217
|
const stuckPageTweaksP = (async () => {
|
|
212
218
|
try {
|
|
213
219
|
const port = seams.resolveOptional('copy-desk.stuckPageTweaksFor');
|
|
@@ -421,6 +427,16 @@ module.exports = function buildMeRouter() {
|
|
|
421
427
|
const activeClaimsWithPreview = await claimsWithStreakPreview(
|
|
422
428
|
reward, req.builder.id, activeClaims, streakInfo, craftModes
|
|
423
429
|
);
|
|
430
|
+
// task 1004948: a gentle "this page is being updated" line per code claim, so the
|
|
431
|
+
// Conductor can tell the session. Information only; a failed read is no line.
|
|
432
|
+
// One read for all the claims; an artist's own page claim (no worktree) is not code work.
|
|
433
|
+
const codeClaims = activeClaimsWithPreview.filter((c) => c.worktree_name != null);
|
|
434
|
+
let notices = new Map();
|
|
435
|
+
try {
|
|
436
|
+
const l = lc();
|
|
437
|
+
if (l && l.noticesForClaims) notices = await l.noticesForClaims(codeClaims);
|
|
438
|
+
} catch (_) { /* heads-up only */ }
|
|
439
|
+
for (const c of activeClaimsWithPreview) c.page_notice = notices.get(String(c.task_id)) || [];
|
|
424
440
|
const payload = {
|
|
425
441
|
builder,
|
|
426
442
|
active_claims: activeClaimsWithPreview,
|
|
@@ -622,6 +638,8 @@ module.exports = function buildMeRouter() {
|
|
|
622
638
|
openMingleIntroductions: mingleIntroductionCount,
|
|
623
639
|
// task 1004777: one Needs-attention line per stuck page tweak.
|
|
624
640
|
stuckPageTweaks: await stuckPageTweaksP,
|
|
641
|
+
// task 1004914: { count, first_id } of open holds on this builder's pay.
|
|
642
|
+
openPayoutHolds: await openPayoutHoldsP,
|
|
625
643
|
}) : [],
|
|
626
644
|
};
|
|
627
645
|
// Module→core contribution seam (BV1.R42, ADR 0083). Let each enabled module
|