@bongos/core 1.23.21 → 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 +983 -428
- 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 +6 -3
- 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 +94 -0
- package/clients/bongos-client/index.cjs +94 -0
- package/clients/bongos-client/index.d.ts +154 -4
- package/clients/bongos-client/index.mjs +94 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +87 -0
- package/docs/adr/0386-the-funding-wall-is-a-per-project-rule-claim-time-hold-by-default.md +37 -0
- package/docs/adr/0387-two-ledgers-credits-measure-work-money-stays-with-the-processor.md +39 -0
- package/docs/adr/0388-a-module-sale-is-split-by-the-project-the-buyer-pays-the-platform-fee.md +42 -0
- package/docs/adr/0392-ai-generation-runs-on-a-processor-held-balance-and-the-builders-own-keys.md +33 -0
- 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 +10 -1
- package/docs/api/openapi.json +8619 -5885
- package/docs/api-reference.md +68 -9
- 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 +4 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +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/CLAUDE.md +15 -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/module.json +26 -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 +252 -0
- 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 +108 -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 +50 -18
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/grading/grader-subagent.js +30 -11
- 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/city.html +15 -15
- 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-analytics.js +10 -3
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +48 -18
- package/modules/lifecycle/db-shared.js +9 -0
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/db-versions.js +21 -8
- 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/ship-assurance.js +4 -1
- package/modules/lifecycle/ship-card.js +8 -2
- 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/onboarding/howto-moments.js +8 -1
- 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 +8 -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 +286 -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 +4 -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 +129 -12
- 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 +16 -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/src/platform-server.js +27 -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/batched_writes_parity.mjs +340 -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/conductor_main_e2e.mjs +4 -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/per-row-loops-legacy.cjs +245 -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_archive_disposition.mjs +5 -5
- 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 +146 -23
- package/tests/grader.mjs +31 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_city.mjs +8 -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/hook_shell_tools.mjs +8 -0
- package/tests/hooks_drain.mjs +1 -1
- package/tests/host_topology_skips.mjs +2 -0
- package/tests/howto_moments.mjs +17 -0
- 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/module_skills_hook.mjs +69 -0
- 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/public_landing_projects.mjs +37 -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/session_start_freshness.mjs +66 -1
- package/tests/shell_hooks.mjs +209 -0
- package/tests/ship_assurance.mjs +28 -0
- package/tests/ship_card.mjs +20 -5
- 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 +58 -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_close_route.mjs +1 -1
- 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
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
//
|
|
32
32
|
// Usage:
|
|
33
33
|
// node scripts/bongos/strand-watch.js # human-readable report
|
|
34
|
-
// node scripts/bongos/strand-watch.js --line # ONE line, or nothing; always exit 0
|
|
34
|
+
// node scripts/bongos/strand-watch.js --line # ONE line, or nothing; always exit 0 (cached; see linePlan)
|
|
35
|
+
// node scripts/bongos/strand-watch.js --refresh-line # refresh that cache; prints nothing (spawned detached by --line)
|
|
35
36
|
// node scripts/bongos/strand-watch.js --json # machine shape
|
|
36
37
|
// node scripts/bongos/strand-watch.js --hours N # override the `completed` threshold
|
|
37
38
|
//
|
|
@@ -240,7 +241,28 @@ function line(r, { max = 3 } = {}) {
|
|
|
240
241
|
// Name the skill, not just the diagnosis: 19 of the last 30 days' session logs mention a strand,
|
|
241
242
|
// and the next claim is refused REBASE_REQUIRED until one is landed or released (task 1003548).
|
|
242
243
|
+ `Run /strand-fix ${worst.id} to walk it. `
|
|
243
|
-
+ 'A strand produces no signal of its own; this line is the only one.'
|
|
244
|
+
+ 'A strand produces no signal of its own; this line is the only one. '
|
|
245
|
+
// Agents kept relaying this to the owner on every session (owner, 2026-10-28; task 1004952).
|
|
246
|
+
+ RELAY_RULE;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// This line is context for the AGENT. Fixed text only — it is injected every session.
|
|
250
|
+
// "Fix it yourself" sent EVERY session at the same strand (task 1004956): /strand-fix now takes a
|
|
251
|
+
// lease first, so the rule says one session fixes it and the others are told who has it.
|
|
252
|
+
const RELAY_RULE = 'This note is for you, not the builder: do not repeat it to them. ONE session fixes a strand — '
|
|
253
|
+
+ '/strand-fix takes it first and stops if another session already has it; tell the builder only if it needs a person or they ask.';
|
|
254
|
+
|
|
255
|
+
// The lease clause, appended at PRINT time, never cached: the line text is shared across
|
|
256
|
+
// sessions for up to two hours (linePlan), while who holds a strand changes by the minute and
|
|
257
|
+
// differs per worktree. Fail-open: no lease folder, no clause.
|
|
258
|
+
function withLeases(text, { now = Date.now() } = {}) {
|
|
259
|
+
if (!text) return text;
|
|
260
|
+
try {
|
|
261
|
+
const lease = require('./strand-lease.js');
|
|
262
|
+
const me = lease.currentHolder();
|
|
263
|
+
const clause = lease.othersClause(lease.liveLeases({ dir: lease.leaseDir(), now }), me && me.holder, now);
|
|
264
|
+
return clause ? `${text} ${clause}` : text;
|
|
265
|
+
} catch { return text; }
|
|
244
266
|
}
|
|
245
267
|
|
|
246
268
|
function report(r) {
|
|
@@ -273,6 +295,77 @@ function report(r) {
|
|
|
273
295
|
console.log('');
|
|
274
296
|
}
|
|
275
297
|
|
|
298
|
+
// ---- the SessionStart line, kept off the critical path (task 1004928) --------
|
|
299
|
+
//
|
|
300
|
+
// THE MEASUREMENT. The credit-burn audit timed this hook at 8.2 s — the slowest SessionStart
|
|
301
|
+
// producer, and so the floor for every session start. The shared cache it used (task 1003689)
|
|
302
|
+
// lived 60 s, which only helps sessions opened together; the next session an hour later paid
|
|
303
|
+
// the whole ledger read again, up to 8 s per call (and the held-tweak pass can add another).
|
|
304
|
+
//
|
|
305
|
+
// THE FIX. A strand is measured in hours (one hour for `confirmed`, twelve for `completed`), so
|
|
306
|
+
// an answer a few minutes old is as true as a fresh one. So: keep the answer LINE_FRESH_MS and
|
|
307
|
+
// print it at once; past that, up to LINE_STALE_MAX_MS, print it at once LABELLED WITH ITS AGE
|
|
308
|
+
// and refresh the cache in a detached child for the next session; with nothing usable, read the
|
|
309
|
+
// ledger but give up after LINE_DEADLINE_MS. An undetermined read is never cached — a stored
|
|
310
|
+
// silence would pass for a clean board for the whole fresh window.
|
|
311
|
+
const LINE_CACHE_KEY = 'strand-watch-line';
|
|
312
|
+
const LINE_FRESH_MS = 15 * 60 * 1000;
|
|
313
|
+
const LINE_STALE_MAX_MS = 2 * 60 * 60 * 1000;
|
|
314
|
+
const LINE_DEADLINE_MS = 4000;
|
|
315
|
+
|
|
316
|
+
// PURE — what to do with the cached record: 'fresh' (print it), 'stale' (print it with its age
|
|
317
|
+
// and refresh in the background) or 'miss' (read the ledger now, time-boxed). A record from the
|
|
318
|
+
// future means the clock moved, and is treated as a miss rather than trusted.
|
|
319
|
+
function linePlan(record, nowMs, { freshMs = LINE_FRESH_MS, staleMaxMs = LINE_STALE_MAX_MS } = {}) {
|
|
320
|
+
if (!record || typeof record.at !== 'number' || !Number.isFinite(nowMs)) return 'miss';
|
|
321
|
+
const age = nowMs - record.at;
|
|
322
|
+
if (age < 0) return 'miss';
|
|
323
|
+
if (age < freshMs) return 'fresh';
|
|
324
|
+
if (age < staleMaxMs) return 'stale';
|
|
325
|
+
return 'miss';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// PURE — the line as printed from a stale cache: the same words, plus how old they are, so a
|
|
329
|
+
// reader can weigh them. A clean (null) answer stays silent: there is nothing to label.
|
|
330
|
+
function labelStale(text, ageMs) {
|
|
331
|
+
if (!text) return null;
|
|
332
|
+
return `${text} (Checked ${humanAge(ageMs)} ago; refreshing for the next session.)`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The ledger read for the line, bounded as a WHOLE (check() bounds each call, not the sum).
|
|
336
|
+
// Throws on an undetermined read so probe-cache stores nothing.
|
|
337
|
+
async function produceLine(opts, deadlineMs) {
|
|
338
|
+
let timer;
|
|
339
|
+
const deadline = new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('deadline')), deadlineMs); });
|
|
340
|
+
try {
|
|
341
|
+
const r = await Promise.race([check({ ...opts, timeoutMs: Math.min(deadlineMs, DEFAULT_TIMEOUT_MS) }), deadline]);
|
|
342
|
+
if (!r || r.undetermined) throw new Error('undetermined');
|
|
343
|
+
return line(r);
|
|
344
|
+
} finally { clearTimeout(timer); }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readLineRecord() {
|
|
348
|
+
try {
|
|
349
|
+
const { cachePath } = require('./probe-cache.js');
|
|
350
|
+
return JSON.parse(require('node:fs').readFileSync(cachePath(LINE_CACHE_KEY), 'utf8'));
|
|
351
|
+
} catch { return null; }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Detached + unref'd + hidden, so the session start never waits on it (windowsHide: a detached
|
|
355
|
+
// child on Windows otherwise flashes a console window). --hours is passed through so the
|
|
356
|
+
// refreshed answer uses the same threshold.
|
|
357
|
+
function spawnRefresh() {
|
|
358
|
+
try {
|
|
359
|
+
const args = [__filename, '--refresh-line'];
|
|
360
|
+
const hours = arg('--hours');
|
|
361
|
+
if (hours) args.push('--hours', String(hours));
|
|
362
|
+
const child = require('node:child_process').spawn(process.execPath, args, {
|
|
363
|
+
detached: true, stdio: 'ignore', windowsHide: true, cwd: process.cwd(),
|
|
364
|
+
});
|
|
365
|
+
child.unref();
|
|
366
|
+
} catch { /* the next session reads the ledger itself */ }
|
|
367
|
+
}
|
|
368
|
+
|
|
276
369
|
async function main() {
|
|
277
370
|
const hours = Number(arg('--hours'));
|
|
278
371
|
const opts = Number.isFinite(hours) && hours > 0 ? { completedStaleMs: hours * 3600000 } : {};
|
|
@@ -285,16 +378,39 @@ async function main() {
|
|
|
285
378
|
// deploy-divergence's identical comment).
|
|
286
379
|
if (hasFlag('--line')) {
|
|
287
380
|
try {
|
|
288
|
-
// SHARED ACROSS
|
|
289
|
-
// within three seconds — one worktree per claim, the normal pattern here — used
|
|
290
|
-
// to make this same call four times and ALL of them timed out, which produces no
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
381
|
+
// SHARED ACROSS SESSION STARTS (task 1003689, widened by task 1004928). Four sessions
|
|
382
|
+
// opened within three seconds — one worktree per claim, the normal pattern here — used
|
|
383
|
+
// to make this same call four times and ALL of them timed out, which produces no line
|
|
384
|
+
// at all. The first session to ask answers for the rest; see linePlan for how long.
|
|
385
|
+
const rec = readLineRecord();
|
|
386
|
+
const plan = linePlan(rec, Date.now());
|
|
387
|
+
if (plan === 'fresh') {
|
|
388
|
+
if (rec.value) console.log(withLeases(rec.value));
|
|
389
|
+
} else if (plan === 'stale') {
|
|
390
|
+
const one = labelStale(rec.value, Date.now() - rec.at);
|
|
391
|
+
if (one) console.log(withLeases(one));
|
|
392
|
+
spawnRefresh();
|
|
393
|
+
} else {
|
|
394
|
+
const { cached } = require('./probe-cache.js');
|
|
395
|
+
const { value: one, from } = await cached(LINE_CACHE_KEY, () => produceLine(opts, LINE_DEADLINE_MS), { ttlMs: LINE_FRESH_MS });
|
|
396
|
+
// A sibling holds the lock and the cache handed back its last answer, however old:
|
|
397
|
+
// say how old, so a long-expired line is never passed off as current.
|
|
398
|
+
const shown = from === 'stale-while-locked' && rec && typeof rec.at === 'number'
|
|
399
|
+
? labelStale(one, Date.now() - rec.at) : one;
|
|
400
|
+
if (shown) console.log(withLeases(shown));
|
|
401
|
+
}
|
|
402
|
+
} catch { /* no session, offline, unreadable, past the deadline — all silence */ }
|
|
403
|
+
process.exitCode = 0;
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// --refresh-line: the detached child spawnRefresh starts. ttlMs 0 forces a fresh read; a
|
|
408
|
+
// sibling already refreshing holds the lock, and then this child simply stops. Prints nothing.
|
|
409
|
+
if (hasFlag('--refresh-line')) {
|
|
410
|
+
try {
|
|
294
411
|
const { cached } = require('./probe-cache.js');
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
} catch { /* no session, offline, unreadable — all silence */ }
|
|
412
|
+
await cached(LINE_CACHE_KEY, () => produceLine(opts, DEFAULT_TIMEOUT_MS * 2), { ttlMs: 0 });
|
|
413
|
+
} catch { /* the next session reads the ledger itself */ }
|
|
298
414
|
process.exitCode = 0;
|
|
299
415
|
return;
|
|
300
416
|
}
|
|
@@ -310,6 +426,7 @@ if (require.main === module) {
|
|
|
310
426
|
}
|
|
311
427
|
|
|
312
428
|
module.exports = {
|
|
313
|
-
check, line, report, classifyStrand, rankStrands, humanAge,
|
|
429
|
+
check, line, withLeases, report, classifyStrand, rankStrands, humanAge,
|
|
430
|
+
linePlan, labelStale, produceLine, LINE_FRESH_MS, LINE_STALE_MAX_MS, LINE_DEADLINE_MS,
|
|
314
431
|
CONFIRMED_STALE_MS, COMPLETED_STALE_MS, STRAND_REASON_PREFIX, WATCHED, PAGE_LIMIT,
|
|
315
432
|
};
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
//
|
|
12
12
|
// WHAT IT CHECKS, narrowly: a version-shaped literal ASSIGNED TO A VERSION FIELD
|
|
13
13
|
// or used as a version DEFAULT, in an executable script. Not any mention of a
|
|
14
|
-
// version anywhere —
|
|
15
|
-
//
|
|
14
|
+
// version anywhere — a script may legitimately say "PMS-V2 P6 not live yet" in a
|
|
15
|
+
// comment or an echo, and flagging that would be the over-broad
|
|
16
16
|
// source assertion this repo has learned to avoid (learning 1000196). Comment
|
|
17
17
|
// lines are stripped before matching for the same reason.
|
|
18
18
|
//
|
package/scripts/discord/post.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
// modules/discord/ship-broadcast.js, whose kind filter decides which ships are
|
|
17
17
|
// news. What does pass through here is the project's own news in plain words,
|
|
18
18
|
// which is what #ship-news promises: a builder graduating
|
|
19
|
-
// (modules/onboarding/onboarding-state.js),
|
|
20
|
-
// (src/bongos/routes/builders.js), an outage heads-up
|
|
19
|
+
// (modules/onboarding/onboarding-state.js), an outage heads-up
|
|
21
20
|
// (modules/provisioning/pollers/liveness-notify.js) and the style-guide audit
|
|
22
|
-
// (scripts/cron/style_guide_audit.sh).
|
|
21
|
+
// (scripts/cron/style_guide_audit.sh). A rank change posts the same #ship-news line
|
|
22
|
+
// in-process through the discord.post port instead (task 1004962,
|
|
23
|
+
// src/bongos/routes/builders.js). Builder housekeeping names its channel:
|
|
23
24
|
// scripts/bongos/pulse.js's reclaim nudge passes --channel ship_feed.
|
|
24
25
|
|
|
25
26
|
const fs = require('node:fs');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/seed-tasks.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
+
// The real script is scripts/bongos/seed-tasks.js. This forwarder keeps callers that still name
|
|
5
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
|
+
'use strict';
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'seed-tasks.js');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// scripts/gds/
|
|
2
|
+
// scripts/gds/strand-lease.js — @bongos-forwarder (task 1004848). Generated by
|
|
3
3
|
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
4
|
-
// The real script is scripts/bongos/
|
|
4
|
+
// The real script is scripts/bongos/strand-lease.js. This forwarder keeps callers that still name
|
|
5
5
|
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
6
6
|
'use strict';
|
|
7
|
-
require('../bongos/forwarder.js').forward(module, '
|
|
7
|
+
require('../bongos/forwarder.js').forward(module, 'strand-lease.js');
|
|
@@ -156,6 +156,15 @@ const FIXTURE_FALLBACKS = [
|
|
|
156
156
|
// a store version's how-to page (task 1004366): one reading serves any key and
|
|
157
157
|
// version, since the Modules tab's how-to view shows one document.
|
|
158
158
|
[/^store\/modules\/[a-z0-9-]+\/versions\/[^/]+\/howto$/, 'store__modules__howto'],
|
|
159
|
+
// the studio's AI pages (task 1004921): one request per STATE, picked by id,
|
|
160
|
+
// so the waiting page, a result on the balance, a result on a Gemini key and a
|
|
161
|
+
// failed run each render: 1002 is running, 1003 failed, 1004 ran on the
|
|
162
|
+
// builder's own Gemini key, and any other id is the finished result on the
|
|
163
|
+
// balance.
|
|
164
|
+
[/^ai-studio\/requests\/1002$/, 'ai-studio__requests__running'],
|
|
165
|
+
[/^ai-studio\/requests\/1003$/, 'ai-studio__requests__failed'],
|
|
166
|
+
[/^ai-studio\/requests\/1004$/, 'ai-studio__requests__own'],
|
|
167
|
+
[/^ai-studio\/requests\/\d+$/, 'ai-studio__requests__done'],
|
|
159
168
|
];
|
|
160
169
|
|
|
161
170
|
// CANNED WRITES (task 1004321). The tweak editor's whole flow is writes (the
|
|
@@ -186,6 +195,11 @@ const CANNED_WRITES = [
|
|
|
186
195
|
// Settings → Software update's "Ask for this upgrade" (task 1004595), so the
|
|
187
196
|
// pending line the panel draws after the press renders here too.
|
|
188
197
|
['POST', /^software-update\/request$/, 'software-update__request.post'],
|
|
198
|
+
// the studio's AI pages (task 1004921): a file uploads, a request is sent
|
|
199
|
+
// (it answers as 1002, the running one), and money is added.
|
|
200
|
+
['POST', /^ai-studio\/uploads$/, 'ai-studio__uploads.post'],
|
|
201
|
+
['POST', /^ai-studio\/requests$/, 'ai-studio__requests.post'],
|
|
202
|
+
['POST', /^ai-studio\/topup$/, 'ai-studio__topup.post'],
|
|
189
203
|
];
|
|
190
204
|
|
|
191
205
|
// A DATE THAT ROTS CANNOT DEMO AN AGE SIGNAL (task 1004041). The help-request
|
|
@@ -316,6 +330,13 @@ app.use((req, res, next) => {
|
|
|
316
330
|
// Studio's review card, where the picture IS the card — previews as a broken
|
|
317
331
|
// frame, so the one thing being reviewed is the one thing you cannot see.
|
|
318
332
|
// A generated stand-in; see scripts/hall-preview/task-visual.js.
|
|
333
|
+
// The studio's AI files (task 1004921) are images too: an upload's thumbnail
|
|
334
|
+
// and the pictures a request made. The same generated stand-in.
|
|
335
|
+
if (/^ai-studio\/files\/\d+$/.test(apiPath) && req.method === 'GET') {
|
|
336
|
+
const png = require('./task-visual').placeholderPng();
|
|
337
|
+
res.set({ 'content-type': 'image/png', 'cache-control': 'no-store' });
|
|
338
|
+
return res.end(png);
|
|
339
|
+
}
|
|
319
340
|
if (/^task-visuals\/[A-Za-z0-9._-]+$/.test(apiPath) && req.method === 'GET') {
|
|
320
341
|
// HALL_PREVIEW_VISUALS=<dir> serves a REAL image of that name from <dir>
|
|
321
342
|
// instead (task 1004323): the Approval queue's review sheet shows the renders
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
'use strict';
|
|
11
11
|
|
|
12
12
|
const ic = require('../instance-config');
|
|
13
|
+
const { readConfigFileCached } = require('../instance-config'); // destructured: knip credits only this form
|
|
13
14
|
const { branding, userAgent } = require('../branding');
|
|
14
15
|
|
|
15
16
|
const SCOPE = 'read:user';
|
|
@@ -62,6 +63,29 @@ function grantedScopeCoversRepos(granted) {
|
|
|
62
63
|
// (the user just re-runs the setup skill). Acceptable for V1 — scaling up means
|
|
63
64
|
// moving this to Redis or a tiny `device_flows` table.
|
|
64
65
|
const deviceFlows = new Map();
|
|
66
|
+
// Task 1004964: expired flows used to be reaped only when a NEW flow started, so the
|
|
67
|
+
// map held its last burst forever on a quiet instance. Now it is also pruned on an
|
|
68
|
+
// unref'd five-minute timer (rare enough not to keep an idle server awake), and
|
|
69
|
+
// hard-capped: at the cap the oldest-inserted flow is evicted, the newest survive.
|
|
70
|
+
const DEVICE_FLOW_MAX = 50000;
|
|
71
|
+
const DEVICE_FLOW_PRUNE_MS = 5 * 60 * 1000;
|
|
72
|
+
|
|
73
|
+
function pruneDeviceFlows(now = Date.now()) {
|
|
74
|
+
for (const [k, v] of deviceFlows) {
|
|
75
|
+
if (v.expires_at < now) deviceFlows.delete(k);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// trackDeviceFlow(code, flow) — record a pending flow, evicting oldest-first at the cap.
|
|
80
|
+
// O(1): expiry is the timer's job, so a burst of starts never rescans the whole map.
|
|
81
|
+
function trackDeviceFlow(code, flow) {
|
|
82
|
+
deviceFlows.delete(code);
|
|
83
|
+
while (deviceFlows.size >= DEVICE_FLOW_MAX) deviceFlows.delete(deviceFlows.keys().next().value);
|
|
84
|
+
deviceFlows.set(code, flow);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const deviceFlowPruneTimer = setInterval(pruneDeviceFlows, DEVICE_FLOW_PRUNE_MS);
|
|
88
|
+
if (typeof deviceFlowPruneTimer.unref === 'function') deviceFlowPruneTimer.unref();
|
|
65
89
|
|
|
66
90
|
function loadGithubCreds() {
|
|
67
91
|
// Resolve the configured instance prefix first (CLOUDBONGOS_ on a fresh
|
|
@@ -77,7 +101,7 @@ function loadGithubCreds() {
|
|
|
77
101
|
// across the configured config dir and the legacy ~/.config/otb dir.
|
|
78
102
|
for (const name of ['gds-github.json', 'pms-github.json']) {
|
|
79
103
|
try {
|
|
80
|
-
const raw =
|
|
104
|
+
const raw = readConfigFileCached(name); // task 1004924: per-request path, cached (60 s TTL)
|
|
81
105
|
if (raw) {
|
|
82
106
|
const creds = JSON.parse(raw);
|
|
83
107
|
if (creds.client_id && creds.client_secret) return creds;
|
|
@@ -143,6 +167,10 @@ module.exports = {
|
|
|
143
167
|
SCOPE,
|
|
144
168
|
authConfigured,
|
|
145
169
|
deviceFlows,
|
|
170
|
+
trackDeviceFlow,
|
|
171
|
+
pruneDeviceFlows,
|
|
172
|
+
DEVICE_FLOW_MAX,
|
|
173
|
+
DEVICE_FLOW_PRUNE_MS,
|
|
146
174
|
grantedScopeCoversRepos,
|
|
147
175
|
idpConfigured,
|
|
148
176
|
idpEnabled,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const { readConfigFileCached } = require('../instance-config'); // destructured: knip credits only this form
|
|
12
12
|
|
|
13
13
|
const DISCORD_SCOPE = 'identify guilds.join';
|
|
14
14
|
|
|
@@ -17,7 +17,7 @@ function loadDiscordCreds() {
|
|
|
17
17
|
const clientSecret = process.env.DISCORD_CLIENT_SECRET;
|
|
18
18
|
if (clientId && clientSecret) return { client_id: clientId, client_secret: clientSecret };
|
|
19
19
|
try {
|
|
20
|
-
const raw =
|
|
20
|
+
const raw = readConfigFileCached('discord-bot.json'); // task 1004924: per-request path, cached (60 s TTL)
|
|
21
21
|
if (raw) {
|
|
22
22
|
const creds = JSON.parse(raw);
|
|
23
23
|
if (creds.client_id && creds.client_secret) {
|
|
@@ -43,7 +43,7 @@ function loadDiscordBotToken() {
|
|
|
43
43
|
const envToken = process.env.DISCORD_BOT_TOKEN;
|
|
44
44
|
if (envToken) return envToken;
|
|
45
45
|
try {
|
|
46
|
-
const raw =
|
|
46
|
+
const raw = readConfigFileCached('discord-bot.json'); // task 1004924: per-request path, cached (60 s TTL)
|
|
47
47
|
if (raw) {
|
|
48
48
|
const creds = JSON.parse(raw);
|
|
49
49
|
if (creds.bot_token) return creds.bot_token;
|
|
@@ -25,7 +25,7 @@ const { pool } = require('./pool');
|
|
|
25
25
|
const seams = require('../module-seams');
|
|
26
26
|
const { branding, userAgent } = require('../branding');
|
|
27
27
|
const { firstTimeAdmissionBlocked, maybeSeatFirstAdmin, openEnrollmentEnabled, safeSyncRankRole } = require('./auth-admission.js');
|
|
28
|
-
const { SCOPE, deviceFlows, loadGithubCreds, scopeForWebFlow } = require('./auth-config.js');
|
|
28
|
+
const { SCOPE, deviceFlows, trackDeviceFlow, loadGithubCreds, scopeForWebFlow } = require('./auth-config.js');
|
|
29
29
|
const log = require('./logger').logger.child({ src: 'auth-github' });
|
|
30
30
|
|
|
31
31
|
function newSessionToken() {
|
|
@@ -121,15 +121,12 @@ async function startDeviceFlow() {
|
|
|
121
121
|
throw new Error('GitHub device/code returned no device_code (unexpected response shape).');
|
|
122
122
|
}
|
|
123
123
|
// data: { device_code, user_code, verification_uri, expires_in, interval }
|
|
124
|
-
|
|
124
|
+
// Evicts oldest-first at the size cap; an unref'd timer reaps expired flows (auth-config.js).
|
|
125
|
+
trackDeviceFlow(data.device_code, {
|
|
125
126
|
interval: data.interval || 5,
|
|
126
127
|
expires_at: Date.now() + data.expires_in * 1000,
|
|
127
128
|
last_polled_at: 0,
|
|
128
129
|
});
|
|
129
|
-
// Reap expired flows (cheap)
|
|
130
|
-
for (const [k, v] of deviceFlows) {
|
|
131
|
-
if (v.expires_at < Date.now()) deviceFlows.delete(k);
|
|
132
|
-
}
|
|
133
130
|
return {
|
|
134
131
|
device_code: data.device_code,
|
|
135
132
|
user_code: data.user_code,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// src/bongos/auth-google.js — signing in with Google (task 1004993, ADR 0396).
|
|
2
|
+
//
|
|
3
|
+
// WHY. A person with no GitHub account could not sign up at all. The owner chose
|
|
4
|
+
// Google as the one new way in (no email codes, no passwords, no other
|
|
5
|
+
// providers). This file holds the Google half of the browser sign-in: the
|
|
6
|
+
// authorize URL, the code exchange, checking the ID token Google returns, and
|
|
7
|
+
// turning that identity into a session on this instance.
|
|
8
|
+
//
|
|
9
|
+
// WHO THE PERSON IS. Google's own stable user id (`sub`) is the only thing that
|
|
10
|
+
// finds a person. The hub's account-logins table maps it to the person key
|
|
11
|
+
// (ADR 0393); an email is shown, never looked up, so owning an address that
|
|
12
|
+
// matches someone's account reaches nothing. The lookup lives on the hub, so this
|
|
13
|
+
// only works where the platform-identity module answers the `signInWithGoogle`
|
|
14
|
+
// port function — on every other instance Google sign-in is simply not offered.
|
|
15
|
+
//
|
|
16
|
+
// THE BUILDER ROW. Core's `builders.github_login` cannot be empty, and a person
|
|
17
|
+
// who signed up with Google has no GitHub login. They get a stand-in that can
|
|
18
|
+
// never be a GitHub login — `~` and the handle they choose on their account page
|
|
19
|
+
// (`standInLogin`) — so no admission rule, invite or GitHub call keyed on a login
|
|
20
|
+
// can ever reach a real GitHub user through them. Once GitHub is linked (task
|
|
21
|
+
// 1004995) the real login is used instead.
|
|
22
|
+
//
|
|
23
|
+
// THE ID TOKEN. It comes straight from Google's token endpoint over TLS in
|
|
24
|
+
// exchange for our client secret, so its signature is not re-checked (OpenID
|
|
25
|
+
// Connect Core's "ID Token Validation" allows TLS in its place); its issuer,
|
|
26
|
+
// audience, expiry and nonce are.
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const crypto = require('node:crypto');
|
|
30
|
+
const ic = require('../instance-config');
|
|
31
|
+
const { createSession, getBuilderByGithubId, upsertBuilder } = require('./db-kernel');
|
|
32
|
+
const { pool } = require('./pool');
|
|
33
|
+
const seams = require('../module-seams');
|
|
34
|
+
const { firstTimeAdmissionBlocked, maybeSeatFirstAdmin, safeSyncRankRole } = require('./auth-admission.js');
|
|
35
|
+
const log = require('./logger').logger.child({ src: 'auth-google' });
|
|
36
|
+
|
|
37
|
+
const GOOGLE_AUTHORIZE_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
38
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
39
|
+
const GOOGLE_ISSUERS = Object.freeze(['https://accounts.google.com', 'accounts.google.com']);
|
|
40
|
+
// Identity only: who they are, the address Google vouches for, a name and a picture.
|
|
41
|
+
const GOOGLE_SCOPE = 'openid email profile';
|
|
42
|
+
// Clock skew allowed when checking the token's expiry.
|
|
43
|
+
const EXPIRY_SKEW_MS = 60 * 1000;
|
|
44
|
+
|
|
45
|
+
// The Google sign-in app's credentials, from env (<PREFIX>_GOOGLE_CLIENT_ID /
|
|
46
|
+
// _GOOGLE_CLIENT_SECRET), or null. Env only — never a committed file.
|
|
47
|
+
function loadGoogleCreds() {
|
|
48
|
+
const clientId = ic.resolveEnv('GOOGLE_CLIENT_ID');
|
|
49
|
+
const clientSecret = ic.resolveEnv('GOOGLE_CLIENT_SECRET');
|
|
50
|
+
return clientId && clientSecret ? { client_id: clientId, client_secret: clientSecret } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The hub port that finds or creates a person by their Google id, or null.
|
|
54
|
+
function googleHub() {
|
|
55
|
+
try {
|
|
56
|
+
const hub = seams.resolveOptional('platform-identity.hub');
|
|
57
|
+
return hub && typeof hub.signInWithGoogle === 'function' ? hub : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Is "Continue with Google" offered here? Both halves must be present: the
|
|
64
|
+
// owner's Google app, and the hub that keeps who signs in with it.
|
|
65
|
+
function googleConfigured() {
|
|
66
|
+
return !!loadGoogleCreds() && !!googleHub();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The Google authorize URL for one sign-in. `nonce` is echoed back inside the ID
|
|
70
|
+
// token and checked, binding the token to this browser's sign-in.
|
|
71
|
+
function buildGoogleAuthorizeUrl({ state, nonce, redirectUri }) {
|
|
72
|
+
const creds = loadGoogleCreds();
|
|
73
|
+
if (!creds) throw new Error('Google sign-in is not configured');
|
|
74
|
+
const u = new URL(GOOGLE_AUTHORIZE_URL);
|
|
75
|
+
u.searchParams.set('client_id', creds.client_id);
|
|
76
|
+
u.searchParams.set('redirect_uri', redirectUri);
|
|
77
|
+
u.searchParams.set('response_type', 'code');
|
|
78
|
+
u.searchParams.set('scope', GOOGLE_SCOPE);
|
|
79
|
+
u.searchParams.set('state', state);
|
|
80
|
+
u.searchParams.set('nonce', nonce);
|
|
81
|
+
// Let a person with several Google accounts pick one, instead of Google
|
|
82
|
+
// silently using whichever is signed in (the GitHub "New account" lesson).
|
|
83
|
+
u.searchParams.set('prompt', 'select_account');
|
|
84
|
+
return u.toString();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The claims inside an ID token (the middle, base64url part), or null.
|
|
88
|
+
function decodeIdToken(idToken) {
|
|
89
|
+
const parts = String(idToken || '').split('.');
|
|
90
|
+
if (parts.length !== 3) return null;
|
|
91
|
+
try {
|
|
92
|
+
return JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check an ID token's claims → the identity this sign-in carries
|
|
99
|
+
// { subject, email, emailVerified, displayName, avatarUrl }. Throws on anything
|
|
100
|
+
// that makes the token not ours, not Google's, stale, or not this sign-in's.
|
|
101
|
+
function verifyGoogleClaims(claims, { clientId, nonce, nowMs = Date.now() }) {
|
|
102
|
+
if (!claims || typeof claims !== 'object') throw new Error('Google returned no identity');
|
|
103
|
+
if (!GOOGLE_ISSUERS.includes(claims.iss)) throw new Error('the identity was not issued by Google');
|
|
104
|
+
const aud = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
105
|
+
if (!clientId || !aud.includes(clientId)) throw new Error('the identity was issued for another app');
|
|
106
|
+
if (aud.length > 1 && claims.azp !== clientId) throw new Error('the identity was issued for another app');
|
|
107
|
+
if (!Number.isFinite(claims.exp) || claims.exp * 1000 + EXPIRY_SKEW_MS < nowMs) throw new Error('the identity has expired');
|
|
108
|
+
if (!nonce || typeof claims.nonce !== 'string' || !timingSafeEqualStr(claims.nonce, nonce)) {
|
|
109
|
+
throw new Error('the identity does not belong to this sign-in');
|
|
110
|
+
}
|
|
111
|
+
const subject = typeof claims.sub === 'string' ? claims.sub : '';
|
|
112
|
+
if (!/^[\x21-\x7e]{1,255}$/.test(subject)) throw new Error('Google returned no account id');
|
|
113
|
+
return {
|
|
114
|
+
subject,
|
|
115
|
+
email: typeof claims.email === 'string' ? claims.email : null,
|
|
116
|
+
emailVerified: claims.email_verified === true || claims.email_verified === 'true',
|
|
117
|
+
displayName: typeof claims.name === 'string' ? claims.name.slice(0, 100) : null,
|
|
118
|
+
avatarUrl: typeof claims.picture === 'string' ? claims.picture : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function timingSafeEqualStr(a, b) {
|
|
123
|
+
const x = Buffer.from(String(a));
|
|
124
|
+
const y = Buffer.from(String(b));
|
|
125
|
+
return x.length === y.length && crypto.timingSafeEqual(x, y);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Trade the code Google redirected back with for the ID token, and check it →
|
|
129
|
+
// the identity. `fetchImpl` is injectable so a test needs no network.
|
|
130
|
+
async function exchangeGoogleCode({ code, redirectUri, nonce, fetchImpl = fetch, nowMs = Date.now() }) {
|
|
131
|
+
const creds = loadGoogleCreds();
|
|
132
|
+
if (!creds) throw new Error('Google sign-in is not configured');
|
|
133
|
+
const res = await fetchImpl(GOOGLE_TOKEN_URL, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
136
|
+
body: new URLSearchParams({
|
|
137
|
+
code,
|
|
138
|
+
client_id: creds.client_id,
|
|
139
|
+
client_secret: creds.client_secret,
|
|
140
|
+
redirect_uri: redirectUri,
|
|
141
|
+
grant_type: 'authorization_code',
|
|
142
|
+
}).toString(),
|
|
143
|
+
});
|
|
144
|
+
const data = await res.json().catch(() => ({}));
|
|
145
|
+
if (!res.ok || data.error || !data.id_token) {
|
|
146
|
+
throw new Error(`Google token exchange: ${data.error_description || data.error || `HTTP ${res.status}`}`);
|
|
147
|
+
}
|
|
148
|
+
return verifyGoogleClaims(decodeIdToken(data.id_token), { clientId: creds.client_id, nonce, nowMs });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// The builder-row login for a person with no GitHub linked: `~` plus their
|
|
152
|
+
// handle. `~` is never in a GitHub login, so this can never name a real GitHub
|
|
153
|
+
// user (ADR 0396). The handle is the one they choose on their account page.
|
|
154
|
+
function standInLogin(handle, personKey) {
|
|
155
|
+
const h = String(handle || '').toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 39);
|
|
156
|
+
return `~${h || `builder${String(personKey || '').replace(/\D/g, '')}`}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function signInError(code, extra = {}) {
|
|
160
|
+
const err = new Error(code.toLowerCase());
|
|
161
|
+
err.code = code;
|
|
162
|
+
Object.assign(err, extra);
|
|
163
|
+
return err;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Turn a checked Google identity into a web session here → { token, builder,
|
|
167
|
+
// expiresAt, created }. The same gates as the GitHub web sign-in
|
|
168
|
+
// (completeWebFlow): a first-timer is admitted only under open enrollment (they
|
|
169
|
+
// have no GitHub login an Archon could have invited), and a switched-off
|
|
170
|
+
// builder gets no session.
|
|
171
|
+
async function completeGoogleFlow({ identity, userAgent }) {
|
|
172
|
+
const hub = googleHub();
|
|
173
|
+
if (!hub) throw signInError('GOOGLE_NOT_AVAILABLE');
|
|
174
|
+
const person = await hub.signInWithGoogle(identity);
|
|
175
|
+
const existing = await getBuilderByGithubId(person.personKey);
|
|
176
|
+
if (!existing && firstTimeAdmissionBlocked({ existing, approved: false, login: null })) {
|
|
177
|
+
throw signInError('NOT_APPROVED');
|
|
178
|
+
}
|
|
179
|
+
const login = person.githubLogin || standInLogin(person.handle, person.personKey);
|
|
180
|
+
let builder = await upsertBuilder({
|
|
181
|
+
githubId: person.personKey,
|
|
182
|
+
githubLogin: login,
|
|
183
|
+
displayName: identity.displayName || person.handle || login,
|
|
184
|
+
avatarUrl: identity.avatarUrl,
|
|
185
|
+
});
|
|
186
|
+
builder = await maybeSeatFirstAdmin(builder);
|
|
187
|
+
if (builder.status === 'inactive') throw signInError('BUILDER_INACTIVE', { login });
|
|
188
|
+
await safeSyncRankRole(builder);
|
|
189
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
190
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
191
|
+
await createSession({ builderId: builder.id, token, source: 'web', userAgent: userAgent ?? null, expiresAt });
|
|
192
|
+
await seams.resolveOptional('onboarding')?.safeMarkStage(pool, builder.id, 'auth_complete', builder);
|
|
193
|
+
// The hub's own browser sign-in accepts the platform terms (ADR 0217 D3), as
|
|
194
|
+
// for GitHub. Best-effort: a failed stamp never costs the sign-in.
|
|
195
|
+
try {
|
|
196
|
+
if (typeof hub.recordHubSignin === 'function') {
|
|
197
|
+
await hub.recordHubSignin({
|
|
198
|
+
githubId: String(person.personKey),
|
|
199
|
+
githubLogin: person.githubLogin,
|
|
200
|
+
displayName: identity.displayName,
|
|
201
|
+
avatarUrl: identity.avatarUrl,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} catch (err) {
|
|
205
|
+
log.error({ err: err && err.message }, 'hub sign-in terms acceptance not recorded (google)');
|
|
206
|
+
}
|
|
207
|
+
return { token, builder, expiresAt, created: !!person.created };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
googleConfigured,
|
|
212
|
+
buildGoogleAuthorizeUrl,
|
|
213
|
+
verifyGoogleClaims,
|
|
214
|
+
exchangeGoogleCode,
|
|
215
|
+
standInLogin,
|
|
216
|
+
completeGoogleFlow,
|
|
217
|
+
};
|