@bongos/core 1.23.22 → 1.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +933 -413
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +96 -0
- package/clients/bongos-client/index.cjs +96 -0
- package/clients/bongos-client/index.d.ts +155 -4
- package/clients/bongos-client/index.mjs +96 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +12 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +6 -2
- package/docs/api/openapi.json +8651 -5833
- package/docs/api-reference.md +66 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +427 -339
- package/docs/copy-registry.json +1376 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +7 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +57 -7
- package/docs/page-readings.json +1210 -1027
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/access-routes.js +70 -0
- package/modules/government/access.js +88 -0
- package/modules/government/asks.js +5 -2
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +72 -0
- package/modules/government/docket.js +48 -2
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/migrations/government_036_access_holders.sql +33 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +31 -0
- package/modules/government/routing.js +49 -7
- package/modules/government/waiting.js +78 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +158 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/government-access.js +108 -0
- package/modules/hall-ui/public/government.html +21 -0
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +38 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +230 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/government-walk.js +3 -0
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +7 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/access_holders.mjs +214 -0
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_board_membership.mjs +4 -1
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/government_seed.mjs +1 -0
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// modules/platform-identity/account-logins.js — the ways a person signs in to
|
|
2
|
+
// their platform account: GitHub, Google, or both (task 1004992, ADR 0393).
|
|
3
|
+
//
|
|
4
|
+
// WHAT CHANGED. A platform account used to BE a GitHub account: keyed on GitHub's
|
|
5
|
+
// user id, so a person without GitHub could not exist on the hub. Now an account
|
|
6
|
+
// is a person, and `platform_identity_account_logins` (migration 034) lists the
|
|
7
|
+
// provider identities that reach it. The column every table and every federated
|
|
8
|
+
// instance joins on keeps its name, `github_id`, and becomes the PERSON KEY:
|
|
9
|
+
//
|
|
10
|
+
// positive — the GitHub user id of a person who arrived through GitHub. Every
|
|
11
|
+
// account before this change, unchanged; their 'github' row is
|
|
12
|
+
// backfilled, and the migration's trigger writes it for every
|
|
13
|
+
// GitHub-born account after, with no change to any writer.
|
|
14
|
+
// negative — minted from platform_identity_person_key_seq for a person who
|
|
15
|
+
// arrived WITHOUT GitHub. Never a GitHub id, and never rewritten
|
|
16
|
+
// when GitHub is linked later.
|
|
17
|
+
//
|
|
18
|
+
// So a key says who the person is, never which provider they used. Code that
|
|
19
|
+
// needs to talk to GitHub asks for the linked GitHub login; it never assumes the
|
|
20
|
+
// key is a GitHub id.
|
|
21
|
+
//
|
|
22
|
+
// THE ONE RULE THE TABLE ENFORCES. A provider identity belongs to exactly one
|
|
23
|
+
// account, and an account holds at most one per provider — both UNIQUE in the
|
|
24
|
+
// database, so a race or a buggy route cannot give one Google account two
|
|
25
|
+
// owners. An email is shown, never looked up: owning a matching address must not
|
|
26
|
+
// reach anyone's account (owner, 2026-10-29: suggest a link, never join one).
|
|
27
|
+
//
|
|
28
|
+
// NOT AUTHORITY (ADR 0016 / ADR 0141). Nothing here reads or writes a rank, a
|
|
29
|
+
// credit or an admission. Linking ways in changes how a person signs in, never
|
|
30
|
+
// what they may do.
|
|
31
|
+
//
|
|
32
|
+
// WHY ITS OWN FILE: platform-identity.js sits at its 1500-line size ratchet (the
|
|
33
|
+
// account-visibility.js precedent, ADR 0202 D4).
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const { pool: defaultPool, withTx } = require('../../src/module-api');
|
|
37
|
+
const pi = require('./platform-identity');
|
|
38
|
+
|
|
39
|
+
// The providers a person may sign in with. Mirrors migration 034's CHECK; a new
|
|
40
|
+
// one is a decision (ADR 0393), not a string someone passes in.
|
|
41
|
+
const LOGIN_PROVIDERS = Object.freeze(['github', 'google']);
|
|
42
|
+
|
|
43
|
+
// A provider identity as this file stores it, or null when it can't be one.
|
|
44
|
+
// GitHub's id is a positive integer; Google's `sub` is an opaque string of at
|
|
45
|
+
// most 255 characters. Never an email or a login: both can change hands.
|
|
46
|
+
function normalizeLogin({ provider, subject } = {}) {
|
|
47
|
+
if (!LOGIN_PROVIDERS.includes(provider)) return null;
|
|
48
|
+
const s = typeof subject === 'number' ? String(subject) : subject;
|
|
49
|
+
if (typeof s !== 'string' || s.length < 1 || s.length > 255) return null;
|
|
50
|
+
if (provider === 'github' && !/^[1-9]\d*$/.test(s)) return null;
|
|
51
|
+
if (provider !== 'github' && !/^[\x21-\x7e]+$/.test(s)) return null;
|
|
52
|
+
return { provider, subject: s };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Was this person key minted for someone who arrived without GitHub? (It is
|
|
56
|
+
// still their key after they link GitHub — a key never changes.)
|
|
57
|
+
function isMintedPersonKey(key) {
|
|
58
|
+
return /^-\d+$/.test(String(key ?? ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// An email as a provider vouched for it, trimmed and capped, or null. Shown to
|
|
62
|
+
// the person in their own settings and nothing else.
|
|
63
|
+
function cleanEmail(email) {
|
|
64
|
+
if (typeof email !== 'string') return null;
|
|
65
|
+
const e = email.trim();
|
|
66
|
+
return e && e.length <= 320 && e.includes('@') ? e : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The seed a new account's handle grows from when there is no GitHub login: the
|
|
70
|
+
// name the provider gave, else the part of the email before the @. ensureHandle
|
|
71
|
+
// squeezes it into the handle charset and falls back to builder-<key>.
|
|
72
|
+
function handleSeedFor({ displayName, email }) {
|
|
73
|
+
return displayName || (cleanEmail(email) || '').split('@')[0] || '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Who signs in with this provider identity? → { accountId, personKey } or null.
|
|
77
|
+
// The lookup every sign-in through a non-GitHub provider starts from.
|
|
78
|
+
async function findPersonByLogin(login, { pool = defaultPool } = {}) {
|
|
79
|
+
const l = normalizeLogin(login);
|
|
80
|
+
if (!l) return null;
|
|
81
|
+
const { rows } = await pool.query(
|
|
82
|
+
`SELECT a.id AS account_id, a.github_id::text AS person_key
|
|
83
|
+
FROM platform_identity_account_logins l
|
|
84
|
+
JOIN platform_identity_accounts a ON a.id = l.account_id
|
|
85
|
+
WHERE l.provider = $1 AND l.subject = $2`,
|
|
86
|
+
[l.provider, l.subject],
|
|
87
|
+
);
|
|
88
|
+
return rows[0] ? { accountId: rows[0].account_id, personKey: rows[0].person_key } : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create the account for a person arriving through a provider other than
|
|
92
|
+
// GitHub, with a minted (negative) person key → { accountId, personKey, created }.
|
|
93
|
+
// If that provider identity already has an account — including one a racing
|
|
94
|
+
// request created a moment ago — that account is returned with created: false,
|
|
95
|
+
// so a double-click never makes two people.
|
|
96
|
+
//
|
|
97
|
+
// GitHub is refused here on purpose: a GitHub-born account is keyed on its
|
|
98
|
+
// GitHub id and is born by upsertAccount, so there is one birth path per kind.
|
|
99
|
+
//
|
|
100
|
+
// The account starts 'provisional', like every account no hub browser sign-in
|
|
101
|
+
// has activated yet (ADR 0171 D3); the sign-in route that calls this decides
|
|
102
|
+
// activation, exactly as it does for GitHub.
|
|
103
|
+
async function createAccountForLogin({ provider, subject, email, displayName, avatarUrl } = {}, { pool = defaultPool } = {}) {
|
|
104
|
+
const l = normalizeLogin({ provider, subject });
|
|
105
|
+
if (!l) throw new Error('createAccountForLogin: not a valid provider identity');
|
|
106
|
+
if (l.provider === 'github') throw new Error('createAccountForLogin: a GitHub account is born keyed on its GitHub id (upsertAccount)');
|
|
107
|
+
|
|
108
|
+
const existing = await findPersonByLogin(l, { pool });
|
|
109
|
+
if (existing) return { ...existing, created: false };
|
|
110
|
+
|
|
111
|
+
const name = typeof displayName === 'string' && displayName.trim() ? displayName.trim().slice(0, 100) : null;
|
|
112
|
+
let born;
|
|
113
|
+
try {
|
|
114
|
+
born = await withTx(async (client) => {
|
|
115
|
+
const { rows } = await client.query(
|
|
116
|
+
`INSERT INTO platform_identity_accounts (github_id, github_login, display_name, avatar_url, profile_state)
|
|
117
|
+
VALUES (-nextval('platform_identity_person_key_seq'), NULL, $1, $2, 'provisional')
|
|
118
|
+
RETURNING id, github_id::text AS person_key`,
|
|
119
|
+
[name, pi.safeAvatarUrl(avatarUrl)],
|
|
120
|
+
);
|
|
121
|
+
await client.query(
|
|
122
|
+
`INSERT INTO platform_identity_account_logins (account_id, provider, subject, email, last_used_at)
|
|
123
|
+
VALUES ($1, $2, $3, $4, now())`,
|
|
124
|
+
[rows[0].id, l.provider, l.subject, cleanEmail(email)],
|
|
125
|
+
);
|
|
126
|
+
return { accountId: rows[0].id, personKey: rows[0].person_key };
|
|
127
|
+
}, { pool });
|
|
128
|
+
} catch (err) {
|
|
129
|
+
// Another request created this person between our lookup and our insert:
|
|
130
|
+
// our transaction rolled back whole, so theirs is the one account.
|
|
131
|
+
if (err && err.code === '23505') {
|
|
132
|
+
const raced = await findPersonByLogin(l, { pool });
|
|
133
|
+
if (raced) return { ...raced, created: false };
|
|
134
|
+
}
|
|
135
|
+
throw err;
|
|
136
|
+
}
|
|
137
|
+
await pi.ensureHandle({ githubId: born.personKey, githubLogin: handleSeedFor({ displayName: name, email }) }, { pool });
|
|
138
|
+
return { ...born, created: true };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A sign-in through a provider other than GitHub (task 1004993: Google) →
|
|
142
|
+
// { personKey, handle, githubLogin, created }. The person is found by the
|
|
143
|
+
// provider's own id, or born now; never by an email. A returning person's way in
|
|
144
|
+
// is stamped as used, and its email refreshed to the one the provider vouched for
|
|
145
|
+
// this time. `githubLogin` is the person's LINKED GitHub login or null: the hub's
|
|
146
|
+
// builder row shows that when there is one, and a stand-in otherwise (ADR 0396).
|
|
147
|
+
async function signInWithLogin({ provider, subject, email, displayName, avatarUrl } = {}, { pool = defaultPool } = {}) {
|
|
148
|
+
const person = await createAccountForLogin({ provider, subject, email, displayName, avatarUrl }, { pool });
|
|
149
|
+
if (!person.created) {
|
|
150
|
+
await pool.query(
|
|
151
|
+
`UPDATE platform_identity_account_logins SET last_used_at = now(), email = COALESCE($3, email)
|
|
152
|
+
WHERE provider = $1 AND subject = $2`,
|
|
153
|
+
[provider, String(subject), cleanEmail(email)],
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
const { rows } = await pool.query(
|
|
157
|
+
`SELECT handle, github_login FROM platform_identity_accounts WHERE id = $1`,
|
|
158
|
+
[person.accountId],
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
personKey: person.personKey,
|
|
162
|
+
handle: rows[0]?.handle || null,
|
|
163
|
+
githubLogin: rows[0]?.github_login || null,
|
|
164
|
+
created: person.created,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// The ways in a person has, for their OWN settings → [{ provider, email,
|
|
169
|
+
// created_at, last_used_at }], GitHub first. The provider's user id never leaves
|
|
170
|
+
// this file: the person needs to know WHICH ways they have, not the ids.
|
|
171
|
+
async function listLoginsForPerson(personKey, { pool = defaultPool } = {}) {
|
|
172
|
+
if (!pi.isNumericGithubId(personKey)) return [];
|
|
173
|
+
const { rows } = await pool.query(
|
|
174
|
+
`SELECT l.provider, l.email, l.created_at, l.last_used_at
|
|
175
|
+
FROM platform_identity_account_logins l
|
|
176
|
+
JOIN platform_identity_accounts a ON a.id = l.account_id
|
|
177
|
+
WHERE a.github_id = $1
|
|
178
|
+
ORDER BY array_position($2::text[], l.provider)`,
|
|
179
|
+
[personKey, LOGIN_PROVIDERS],
|
|
180
|
+
);
|
|
181
|
+
return rows;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = {
|
|
185
|
+
LOGIN_PROVIDERS,
|
|
186
|
+
normalizeLogin,
|
|
187
|
+
isMintedPersonKey,
|
|
188
|
+
findPersonByLogin,
|
|
189
|
+
createAccountForLogin,
|
|
190
|
+
signInWithLogin,
|
|
191
|
+
listLoginsForPerson,
|
|
192
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
-- platform_identity_034_account_logins.sql — a platform account is a PERSON, and
|
|
2
|
+
-- GitHub is one way in, no longer the only one (task 1004992, ADR 0393).
|
|
3
|
+
--
|
|
4
|
+
-- Before this, platform_identity_accounts was keyed on GitHub's user id, so a
|
|
5
|
+
-- person without a GitHub account could not exist on the hub at all. Three
|
|
6
|
+
-- changes, all additive or relaxing:
|
|
7
|
+
--
|
|
8
|
+
-- 1. platform_identity_account_logins — the ways a person signs in. One row per
|
|
9
|
+
-- (provider, provider's own user id). A person holds at most one of each
|
|
10
|
+
-- provider, and a provider identity belongs to exactly one person; both are
|
|
11
|
+
-- UNIQUE constraints, so the database, not a route, refuses a second owner.
|
|
12
|
+
--
|
|
13
|
+
-- 2. github_id keeps its name and its job as the federation key every other
|
|
14
|
+
-- table and every federated instance already joins on — but it now means
|
|
15
|
+
-- "the person key". A person who arrived through GitHub keeps their GitHub
|
|
16
|
+
-- user id as the key (every existing row, unchanged). A person who arrived
|
|
17
|
+
-- WITHOUT GitHub gets a NEGATIVE key from platform_identity_person_key_seq:
|
|
18
|
+
-- GitHub ids are positive, so a minted key can never collide with one, and
|
|
19
|
+
-- it never changes when GitHub is linked later.
|
|
20
|
+
--
|
|
21
|
+
-- 3. github_login is now NULL for a person with no GitHub linked. A login is a
|
|
22
|
+
-- GitHub fact; inventing one would let a real GitHub user of that name be
|
|
23
|
+
-- mistaken for this person by the invite-by-login path.
|
|
24
|
+
--
|
|
25
|
+
-- The trigger keeps every GitHub-born account's login row in step without a
|
|
26
|
+
-- change to any writer: an account inserted with a positive key is, by
|
|
27
|
+
-- construction, a GitHub sign-in, so its 'github' row is written beside it.
|
|
28
|
+
--
|
|
29
|
+
-- Identity only, never authority (ADR 0016 / ADR 0141): nothing here carries a
|
|
30
|
+
-- rank, a credit or an admission.
|
|
31
|
+
--
|
|
32
|
+
-- Idempotent: safe to re-run.
|
|
33
|
+
|
|
34
|
+
BEGIN;
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS platform_identity_account_logins (
|
|
37
|
+
id bigserial PRIMARY KEY,
|
|
38
|
+
account_id bigint NOT NULL REFERENCES platform_identity_accounts (id) ON DELETE CASCADE,
|
|
39
|
+
|
|
40
|
+
-- 'github' | 'google'. A new provider is a decision (ADR 0393), so it is a
|
|
41
|
+
-- CHECK and not a free string.
|
|
42
|
+
provider text NOT NULL CHECK (provider IN ('github', 'google')),
|
|
43
|
+
|
|
44
|
+
-- The provider's own stable user id: GitHub's numeric id, Google's `sub`.
|
|
45
|
+
-- Never an email or a login — both can change hands.
|
|
46
|
+
subject text NOT NULL CHECK (length(subject) BETWEEN 1 AND 255),
|
|
47
|
+
|
|
48
|
+
-- The address the provider vouched for at the last sign-in, shown to the
|
|
49
|
+
-- person in their own settings. Descriptive only: no lookup keys on it, so
|
|
50
|
+
-- owning a matching address never reaches an account.
|
|
51
|
+
email text CHECK (email IS NULL OR length(email) <= 320),
|
|
52
|
+
|
|
53
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
54
|
+
last_used_at timestamptz,
|
|
55
|
+
|
|
56
|
+
UNIQUE (provider, subject),
|
|
57
|
+
UNIQUE (account_id, provider)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
COMMENT ON TABLE platform_identity_account_logins IS
|
|
61
|
+
'The ways a platform account signs in (task 1004992, ADR 0393): one row per provider identity. A provider identity belongs to exactly one account and an account holds at most one per provider. Identity only, never authority.';
|
|
62
|
+
|
|
63
|
+
-- Keys minted for people who arrive without GitHub: the account key is
|
|
64
|
+
-- -nextval(), so it is always negative and never a GitHub id.
|
|
65
|
+
CREATE SEQUENCE IF NOT EXISTS platform_identity_person_key_seq;
|
|
66
|
+
|
|
67
|
+
ALTER TABLE platform_identity_accounts ALTER COLUMN github_login DROP NOT NULL;
|
|
68
|
+
|
|
69
|
+
COMMENT ON COLUMN platform_identity_accounts.github_id IS
|
|
70
|
+
'The PERSON key the federation joins on (ADR 0393). Positive = the GitHub user id of a person who arrived through GitHub; negative = minted for a person who arrived without it. It never changes when a way in is linked or removed.';
|
|
71
|
+
COMMENT ON COLUMN platform_identity_accounts.github_login IS
|
|
72
|
+
'The linked GitHub login, or NULL when this person has no GitHub linked (ADR 0393).';
|
|
73
|
+
|
|
74
|
+
-- Every account that exists today arrived through GitHub.
|
|
75
|
+
INSERT INTO platform_identity_account_logins (account_id, provider, subject, created_at)
|
|
76
|
+
SELECT id, 'github', github_id::text, created_at
|
|
77
|
+
FROM platform_identity_accounts
|
|
78
|
+
WHERE github_id > 0
|
|
79
|
+
ON CONFLICT DO NOTHING;
|
|
80
|
+
|
|
81
|
+
CREATE OR REPLACE FUNCTION platform_identity_github_born_login() RETURNS trigger AS $$
|
|
82
|
+
BEGIN
|
|
83
|
+
INSERT INTO platform_identity_account_logins (account_id, provider, subject)
|
|
84
|
+
VALUES (NEW.id, 'github', NEW.github_id::text)
|
|
85
|
+
ON CONFLICT DO NOTHING;
|
|
86
|
+
RETURN NEW;
|
|
87
|
+
END;
|
|
88
|
+
$$ LANGUAGE plpgsql;
|
|
89
|
+
|
|
90
|
+
DROP TRIGGER IF EXISTS platform_identity_github_born_login ON platform_identity_accounts;
|
|
91
|
+
CREATE TRIGGER platform_identity_github_born_login
|
|
92
|
+
AFTER INSERT ON platform_identity_accounts
|
|
93
|
+
FOR EACH ROW WHEN (NEW.github_id > 0)
|
|
94
|
+
EXECUTE FUNCTION platform_identity_github_born_login();
|
|
95
|
+
|
|
96
|
+
COMMIT;
|
|
@@ -173,9 +173,10 @@ async function ensureAccount({ githubId, githubLogin, displayName, avatarUrl },
|
|
|
173
173
|
async function upsertAccount({ githubId, githubLogin, displayName, avatarUrl, activate = false }, { pool = defaultPool } = {}) {
|
|
174
174
|
const { rows } = await pool.query(
|
|
175
175
|
`INSERT INTO platform_identity_accounts (github_id, github_login, display_name, avatar_url, profile_state${terms.acceptanceInsertColumns()})
|
|
176
|
-
VALUES ($1, $2, $3, $4, CASE WHEN $5::boolean THEN 'active' ELSE 'provisional' END${terms.acceptanceInsertValues('$5', '$6')})
|
|
176
|
+
VALUES ($1, CASE WHEN $1::bigint < 0 THEN NULL ELSE $2 END, $3, $4, CASE WHEN $5::boolean THEN 'active' ELSE 'provisional' END${terms.acceptanceInsertValues('$5', '$6')})
|
|
177
177
|
ON CONFLICT (github_id) DO UPDATE SET
|
|
178
|
-
|
|
178
|
+
-- A minted (negative) key's login is GitHub's, set only by linking (ADR 0393/0396).
|
|
179
|
+
github_login = CASE WHEN platform_identity_accounts.github_id < 0 THEN platform_identity_accounts.github_login ELSE EXCLUDED.github_login END,
|
|
179
180
|
display_name = CASE WHEN platform_identity_accounts.display_name_source = 'user'
|
|
180
181
|
THEN platform_identity_accounts.display_name
|
|
181
182
|
ELSE EXCLUDED.display_name END,
|
|
@@ -764,9 +765,9 @@ async function listScoutingRollup({ limit = 100, pool = defaultPool } = {}) {
|
|
|
764
765
|
// Is this a value we can safely bind against the bigint github_id column?
|
|
765
766
|
// Core builders include 'system:bfg'-style TEXT principals; binding one would
|
|
766
767
|
// throw a bigint cast error mid-request, so every profile read guards here and
|
|
767
|
-
// returns null instead (ADR 0171 D2).
|
|
768
|
+
// returns null instead (ADR 0171 D2). Negative = a person with no GitHub (ADR 0393).
|
|
768
769
|
function isNumericGithubId(githubId) {
|
|
769
|
-
return
|
|
770
|
+
return /^-?\d+$/.test(String(githubId ?? ''));
|
|
770
771
|
}
|
|
771
772
|
|
|
772
773
|
// One builder's cross-project record (ADR 0171 D2/D6/D7, task 1002278) — the
|
|
@@ -28,6 +28,7 @@ const pi = require('../platform-identity');
|
|
|
28
28
|
const projectStats = require('../project-stats');
|
|
29
29
|
const applicationEcho = require('../application-echo');
|
|
30
30
|
const guilds = require('../guilds');
|
|
31
|
+
const { isMintedPersonKey } = require('../account-logins');
|
|
31
32
|
const guildEngagements = require('../guild-engagements');
|
|
32
33
|
const inviteNotices = require('../invite-notices');
|
|
33
34
|
const hubKeys = require('../hub-keys');
|
|
@@ -226,6 +227,11 @@ module.exports = function myProjectsRoutes() {
|
|
|
226
227
|
note: { type: 'string', maxLength: APPLICANT_NOTE_MAX },
|
|
227
228
|
guild: { type: 'string', maxLength: 39 },
|
|
228
229
|
})) return;
|
|
230
|
+
// A Google-only person (a minted key) cannot be relayed to a project yet:
|
|
231
|
+
// halls learn to take one in task 1004994 (ADR 0393). Refuse before relaying.
|
|
232
|
+
if (isMintedPersonKey(req.builder.github_id)) {
|
|
233
|
+
return res.fail('google_join_not_yet', { status: 409, message: 'Projects can’t be joined with a Google sign-in yet. That is coming soon.' });
|
|
234
|
+
}
|
|
229
235
|
try {
|
|
230
236
|
const client = await pi.getClientByOrigin(req.body.origin, { pool });
|
|
231
237
|
if (!client || client.status !== 'active' || !client.origin) {
|
|
@@ -30,6 +30,8 @@
|
|
|
30
30
|
// plain-text bio); setting display_name stamps
|
|
31
31
|
// display_name_source='user' so federated re-syncs
|
|
32
32
|
// stop overwriting it (the ADR 0156 mirror).
|
|
33
|
+
// GET /me/sign-in-methods — the ways the account signs in (GitHub, Google;
|
|
34
|
+
// task 1004992, ADR 0393), for account settings.
|
|
33
35
|
//
|
|
34
36
|
// Reaches core only through the doorway (ADR 0083).
|
|
35
37
|
'use strict';
|
|
@@ -37,6 +39,7 @@
|
|
|
37
39
|
const express = require('express');
|
|
38
40
|
const api = require('../../../src/module-api');
|
|
39
41
|
const pi = require('../platform-identity');
|
|
42
|
+
const logins = require('../account-logins');
|
|
40
43
|
|
|
41
44
|
const { pool, requireBuilder, validateOrRespond } = api;
|
|
42
45
|
|
|
@@ -232,5 +235,20 @@ module.exports = function profileRoutes() {
|
|
|
232
235
|
}
|
|
233
236
|
});
|
|
234
237
|
|
|
238
|
+
// rank: any-builder — the ways the caller's OWN account signs in (task 1004992,
|
|
239
|
+
// ADR 0393): GitHub, Google, or both, with the address each provider vouched
|
|
240
|
+
// for. Keyed on the authenticated github_id (the person key); the provider's
|
|
241
|
+
// user id never crosses. A system:* principal has no platform account: [].
|
|
242
|
+
router.get('/me/sign-in-methods', requireBuilder, async (req, res) => {
|
|
243
|
+
try {
|
|
244
|
+
const methods = await logins.listLoginsForPerson(req.builder.github_id, { pool });
|
|
245
|
+
res.set('Cache-Control', 'no-store');
|
|
246
|
+
res.json({ methods, providers: logins.LOGIN_PROVIDERS });
|
|
247
|
+
} catch (err) {
|
|
248
|
+
console.error('[platform-identity] GET /me/sign-in-methods', err);
|
|
249
|
+
res.fail('sign_in_methods_read_failed', { status: 500, message: 'internal error' });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
235
253
|
return router;
|
|
236
254
|
};
|
|
@@ -28,6 +28,9 @@ const { pool, corsPublicGet } = api;
|
|
|
28
28
|
const READ_RATE_WINDOW_MS = 60 * 1000;
|
|
29
29
|
const READ_RATE_LIMIT = 120;
|
|
30
30
|
const buckets = new Map(); // ip -> timestamp[]
|
|
31
|
+
// Hard size cap (task 1004964): the sweep below bounds the map by time only, every
|
|
32
|
+
// five minutes. At the cap the oldest-inserted IP goes first; the newest survive.
|
|
33
|
+
const MAX_TRACKED_IPS = 50000;
|
|
31
34
|
|
|
32
35
|
const sweepTimer = setInterval(() => {
|
|
33
36
|
const cutoff = Date.now() - READ_RATE_WINDOW_MS;
|
|
@@ -42,7 +45,10 @@ function profileReadRateLimit(req, res, next) {
|
|
|
42
45
|
const now = Date.now();
|
|
43
46
|
const key = req.ip || req.socket?.remoteAddress || 'unknown';
|
|
44
47
|
let bucket = buckets.get(key);
|
|
45
|
-
if (!bucket) {
|
|
48
|
+
if (!bucket) {
|
|
49
|
+
while (buckets.size >= MAX_TRACKED_IPS) buckets.delete(buckets.keys().next().value);
|
|
50
|
+
bucket = []; buckets.set(key, bucket);
|
|
51
|
+
}
|
|
46
52
|
const cutoff = now - READ_RATE_WINDOW_MS;
|
|
47
53
|
while (bucket.length && bucket[0] < cutoff) bucket.shift();
|
|
48
54
|
if (bucket.length >= READ_RATE_LIMIT) {
|
|
@@ -90,3 +96,6 @@ module.exports = function publicProfileRoutes() {
|
|
|
90
96
|
|
|
91
97
|
return router;
|
|
92
98
|
};
|
|
99
|
+
|
|
100
|
+
// Test seam (task 1004964): the limiter and its map, so a test can show the cap holds.
|
|
101
|
+
module.exports._internals = { profileReadRateLimit, buckets, MAX_TRACKED_IPS };
|
|
@@ -31,6 +31,8 @@ const applicationEcho = require('../application-echo');
|
|
|
31
31
|
const guildEngagements = require('../guild-engagements');
|
|
32
32
|
const instanceFeedback = require('../instance-feedback');
|
|
33
33
|
const projectStats = require('../project-stats');
|
|
34
|
+
// Ways in other than GitHub (task 1004993, ADR 0393/0396).
|
|
35
|
+
const { signInWithLogin, isMintedPersonKey } = require('../account-logins');
|
|
34
36
|
|
|
35
37
|
// One limiter per hub process for POST /sso/feedback (task 1004462).
|
|
36
38
|
const feedbackLimiter = instanceFeedback.makeFeedbackLimiter();
|
|
@@ -74,6 +76,9 @@ function hubHost() {
|
|
|
74
76
|
const APPLICANT_READ_WINDOW_MS = 60 * 1000;
|
|
75
77
|
const APPLICANT_READ_LIMIT = 300;
|
|
76
78
|
const applicantReadBuckets = new Map(); // ip -> timestamp[]
|
|
79
|
+
// Hard size cap (task 1004964): the sweep below bounds the map by time only, every
|
|
80
|
+
// five minutes. At the cap the oldest-inserted IP goes first; the newest survive.
|
|
81
|
+
const APPLICANT_MAX_TRACKED_IPS = 50000;
|
|
77
82
|
|
|
78
83
|
const applicantSweep = setInterval(() => {
|
|
79
84
|
const cutoff = Date.now() - APPLICANT_READ_WINDOW_MS;
|
|
@@ -88,7 +93,12 @@ function applicantReadRateLimit(req, res, next) {
|
|
|
88
93
|
const now = Date.now();
|
|
89
94
|
const key = req.ip || req.socket?.remoteAddress || 'unknown';
|
|
90
95
|
let bucket = applicantReadBuckets.get(key);
|
|
91
|
-
if (!bucket) {
|
|
96
|
+
if (!bucket) {
|
|
97
|
+
while (applicantReadBuckets.size >= APPLICANT_MAX_TRACKED_IPS) {
|
|
98
|
+
applicantReadBuckets.delete(applicantReadBuckets.keys().next().value);
|
|
99
|
+
}
|
|
100
|
+
bucket = []; applicantReadBuckets.set(key, bucket);
|
|
101
|
+
}
|
|
92
102
|
const cutoff = now - APPLICANT_READ_WINDOW_MS;
|
|
93
103
|
while (bucket.length && bucket[0] < cutoff) bucket.shift();
|
|
94
104
|
if (bucket.length >= APPLICANT_READ_LIMIT) {
|
|
@@ -232,6 +242,12 @@ function registerSeams() {
|
|
|
232
242
|
recordHubSignin: ({ githubId, githubLogin, displayName, avatarUrl }) => pi.upsertAccount({
|
|
233
243
|
githubId, githubLogin, displayName, avatarUrl, activate: true,
|
|
234
244
|
}, { pool }),
|
|
245
|
+
// task 1004993 (ADR 0396): the hub's own Google sign-in. Finds the person by
|
|
246
|
+
// Google's own id or creates them; never by an email. Core passes only what it
|
|
247
|
+
// read from Google's token endpoint for the code it exchanged.
|
|
248
|
+
signInWithGoogle: ({ subject, email, displayName, avatarUrl }) => signInWithLogin({
|
|
249
|
+
provider: 'google', subject, email, displayName, avatarUrl,
|
|
250
|
+
}, { pool }),
|
|
235
251
|
});
|
|
236
252
|
}
|
|
237
253
|
}
|
|
@@ -311,6 +327,12 @@ module.exports = function ssoRoutes() {
|
|
|
311
327
|
: 'This session type cannot be used for cross-project sign-in.';
|
|
312
328
|
return res.status(403).type('text/html').send(errorPage(msg));
|
|
313
329
|
}
|
|
330
|
+
// A person who signed up with Google has a minted key, which a project's hall
|
|
331
|
+
// cannot take yet (ADR 0393: an older core must not be sent one). Task 1004994
|
|
332
|
+
// opens this door; until then say so plainly instead of sending the key.
|
|
333
|
+
if (isMintedPersonKey(session.github_id)) {
|
|
334
|
+
return res.status(403).type('text/html').send(errorPage('Projects can’t be joined with a Google sign-in yet. That is coming soon; for now, sign in to the project with GitHub.'));
|
|
335
|
+
}
|
|
314
336
|
|
|
315
337
|
// A live hub session — record/refresh the platform account and mint a
|
|
316
338
|
// single-use code bound to (client_id, github_id, redirect_uri).
|
|
@@ -860,3 +882,6 @@ module.exports = function ssoRoutes() {
|
|
|
860
882
|
|
|
861
883
|
return router;
|
|
862
884
|
};
|
|
885
|
+
|
|
886
|
+
// Test seam (task 1004964): the applicant limiter and its map, so a test can show the cap holds.
|
|
887
|
+
module.exports._internals = { applicantReadRateLimit, applicantReadBuckets, APPLICANT_MAX_TRACKED_IPS };
|
|
@@ -1127,7 +1127,8 @@ await ta('upsertAccount SQL carries the ADR 0156 guard: a user-set name is never
|
|
|
1127
1127
|
const sql = pool.calls[0].text;
|
|
1128
1128
|
assert.match(sql, /display_name\s*=\s*CASE WHEN platform_identity_accounts\.display_name_source = 'user'/,
|
|
1129
1129
|
'the conflict branch guards display_name on display_name_source');
|
|
1130
|
-
assert.match(sql, /github_login
|
|
1130
|
+
assert.match(sql, /github_login = CASE WHEN platform_identity_accounts\.github_id < 0 THEN platform_identity_accounts\.github_login ELSE EXCLUDED\.github_login END/,
|
|
1131
|
+
'github_login re-syncs for a GitHub-born key; a minted key keeps the login only linking sets (ADR 0396)');
|
|
1131
1132
|
assert.match(sql, /avatar_url\s*=\s*EXCLUDED\.avatar_url/, 'avatar_url still re-syncs unconditionally');
|
|
1132
1133
|
});
|
|
1133
1134
|
|
|
@@ -99,8 +99,8 @@ async function fetchRepoVisibility(token, { owner, name, fetchImpl, userAgent })
|
|
|
99
99
|
// send: { code, status, message }. Returning a value rather than writing to `res`
|
|
100
100
|
// keeps this directly testable and leaves the HTTP shape with the route that owns it.
|
|
101
101
|
//
|
|
102
|
-
// deps: { pool, secretBox, grantedScopeCoversRepos, userAgent,
|
|
103
|
-
// readToken?, fetchRepo? } — the last two are test seams.
|
|
102
|
+
// deps: { pool, secretBox, grantedScopeCoversRepos, userAgent, prelaunchActive?,
|
|
103
|
+
// fetchImpl?, readToken?, fetchRepo? } — the last two are test seams.
|
|
104
104
|
async function adoptCredentialFault({ builderId, onboardMode, targetRef }, deps) {
|
|
105
105
|
if (onboardMode !== 'adopt') return null; // greenfield: nothing to check
|
|
106
106
|
const { grantedScopeCoversRepos, secretBox, fetchImpl, userAgent } = deps;
|
|
@@ -159,9 +159,19 @@ async function adoptCredentialFault({ builderId, onboardMode, targetRef }, deps)
|
|
|
159
159
|
// (ADR 0176). Read-only collaborator access passes the read above and then fails at
|
|
160
160
|
// key registration — the exact "looked like it worked" shape this exists to end.
|
|
161
161
|
// The sentence matches the runner's own so the two can never disagree.
|
|
162
|
+
// PRE-LAUNCH (stealth) keeps every project's code private (task 1004998): while the
|
|
163
|
+
// instance's password gate is on, a PUBLIC repo is refused — and the admin refusal
|
|
164
|
+
// below stops offering "make it public" as a way out. Asked only on a positive read
|
|
165
|
+
// that the repo is public, so a GitHub hiccup still admits above.
|
|
166
|
+
const prelaunch = !!(deps.prelaunchActive && deps.prelaunchActive());
|
|
162
167
|
if (info.private && !info.admin) {
|
|
163
168
|
return { code: 'repo_admin_required', status: 403,
|
|
164
|
-
message: `Your GitHub account can read ${repo.owner}/${repo.name}, but adopting a private repository needs repository ADMIN so its deploy key can be registered. Ask a repository admin to raise your access, or make the repository public
|
|
169
|
+
message: `Your GitHub account can read ${repo.owner}/${repo.name}, but adopting a private repository needs repository ADMIN so its deploy key can be registered. Ask a repository admin to raise your access${prelaunch ? '.' : ', or make the repository public.'}` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!info.private && prelaunch) {
|
|
173
|
+
return { code: 'public_repo_prelaunch', status: 409,
|
|
174
|
+
message: `${repo.owner}/${repo.name} is public on GitHub, and while we are in pre-launch every project's code stays private. Make the repository private on GitHub first, then try again — or start a fresh project instead.` };
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
return null;
|
|
@@ -30,7 +30,13 @@ const { RENDER_URL_RE } = require('../app-status');
|
|
|
30
30
|
|
|
31
31
|
const logger = api.logger('provisioning');
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// CADENCE (task 1004960): every 5 minutes, not every minute. The only thing that
|
|
34
|
+
// reads what this writes is the project page's app card (app-status.js), which
|
|
35
|
+
// moves from "building" to "live" — and a Render first build itself takes
|
|
36
|
+
// several minutes, so a card that turns live up to 5 minutes after the app does
|
|
37
|
+
// costs the owner nothing, while a per-minute DB read on every hosted project
|
|
38
|
+
// costs every instance ~1,440 queries a day. Nothing user-facing needs faster.
|
|
39
|
+
const INTERVAL_MS = Number(process.env.PROVISIONING_APP_LIVENESS_MS) || 5 * 60_000;
|
|
34
40
|
const FIRST_DELAY_MS = 20_000;
|
|
35
41
|
const PROBE_TIMEOUT_MS = 10_000;
|
|
36
42
|
const SWEEP_LIMIT = 50;
|
|
@@ -95,16 +101,30 @@ function start(deps = {}) {
|
|
|
95
101
|
|
|
96
102
|
function arm(deps, delay) {
|
|
97
103
|
timer = setTimeout(() => {
|
|
98
|
-
|
|
99
|
-
.catch((e) => logger.error(`[app-liveness] sweep failed: ${e && e.message ? e.message : e}`))
|
|
100
|
-
.then(() => { if (started) arm(deps, INTERVAL_MS); });
|
|
104
|
+
tick(deps).then(() => { if (started) arm(deps, INTERVAL_MS); });
|
|
101
105
|
}, delay);
|
|
102
106
|
if (timer.unref) timer.unref();
|
|
103
107
|
}
|
|
104
108
|
|
|
109
|
+
// One run. A run that starts while the previous sweep is still in flight (ten
|
|
110
|
+
// slow knocks can outlast a short interval) is skipped, never stacked.
|
|
111
|
+
let running = false;
|
|
112
|
+
async function tick(deps = {}) {
|
|
113
|
+
if (running) return { skipped: true };
|
|
114
|
+
running = true;
|
|
115
|
+
try {
|
|
116
|
+
return await sweepOnce(deps);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
logger.error(`[app-liveness] sweep failed: ${e && e.message ? e.message : e}`);
|
|
119
|
+
return { failed: true };
|
|
120
|
+
} finally {
|
|
121
|
+
running = false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
105
125
|
function stop() {
|
|
106
126
|
started = false;
|
|
107
127
|
if (timer) { clearTimeout(timer); timer = null; }
|
|
108
128
|
}
|
|
109
129
|
|
|
110
|
-
module.exports = { start, stop, sweepOnce, appAnswers, INTERVAL_MS };
|
|
130
|
+
module.exports = { start, stop, sweepOnce, tick, appAnswers, INTERVAL_MS };
|
|
@@ -404,7 +404,7 @@ module.exports = function provisioningRoutes() {
|
|
|
404
404
|
// definition of a usable credential; ../credential-preflight.js says why.
|
|
405
405
|
const credFault = await adoptCredentialFault(
|
|
406
406
|
{ builderId: req.builder.id, onboardMode, targetRef: body.target_ref },
|
|
407
|
-
{ pool, secretBox: api.secretBox, grantedScopeCoversRepos: api.grantedScopeCoversRepos, userAgent: api.userAgent() },
|
|
407
|
+
{ pool, secretBox: api.secretBox, grantedScopeCoversRepos: api.grantedScopeCoversRepos, prelaunchActive: api.prelaunchActive, userAgent: api.userAgent() },
|
|
408
408
|
);
|
|
409
409
|
if (credFault) return res.fail(credFault.code, { status: credFault.status, message: credFault.message });
|
|
410
410
|
// A re-request must not re-litigate the address: createInstance never updates
|
|
@@ -80,8 +80,12 @@ const ALWAYS_ON_CORE = Object.freeze([
|
|
|
80
80
|
// investor page: its copy is Cloud Bongos's raise, so offering it to a new project would hand
|
|
81
81
|
// that project someone else's fundraise (task 1004852). `ai-studio` (task 1004916, ADR 0392)
|
|
82
82
|
// bills AI generation through Cloud Bongos's own payment processor account, so it stays on
|
|
83
|
-
// the hub until a project can bring its own billing.
|
|
84
|
-
|
|
83
|
+
// the hub until a project can bring its own billing. `market-watch` (task 1004935) is not
|
|
84
|
+
// hub-only in purpose: it is here because it is still EMPTY (no routes), and a picker entry
|
|
85
|
+
// for a switch that does nothing is worse than none. This list is the one that keeps a
|
|
86
|
+
// default:false module out of the opt-in catalog; the goal 1000137 task that ships the
|
|
87
|
+
// owner's on/off switch moves it to OPTIONAL_MODULES with owner-facing copy.
|
|
88
|
+
const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning', 'npm-release', 'render-deploy', 'raise', 'ai-studio', 'market-watch']);
|
|
85
89
|
|
|
86
90
|
// The OPT-IN catalog as the creation picker renders it (BV1.R20, task 1002339):
|
|
87
91
|
// every module an owner may actually choose, in the order the picker lists them,
|
|
@@ -156,11 +156,11 @@
|
|
|
156
156
|
<!-- signed-out door: revealed on a 401, the /projects gate's shape -->
|
|
157
157
|
<section class="aDoor" id="door" hidden aria-labelledby="door-h">
|
|
158
158
|
<h2 class="vh" id="door-h">Sign in</h2>
|
|
159
|
-
<p>Sign in
|
|
159
|
+
<p>Sign in to change your handle, your bio and links, and who can find you.</p>
|
|
160
160
|
<a class="btn" id="signin" href="/api/bongos/auth/web/start?return=/account&repos=1">
|
|
161
161
|
<!-- keep the spaces inside this path's numbers: same shape, and they are load-bearing (task 1004401) -->
|
|
162
162
|
<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23 .82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2 .82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12 .51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29 .25.54.73 .54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15 .46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>
|
|
163
|
-
Sign in
|
|
163
|
+
Sign in
|
|
164
164
|
</a>
|
|
165
165
|
</section>
|
|
166
166
|
|
|
@@ -204,6 +204,7 @@
|
|
|
204
204
|
<section class="aSec" id="aHandle" aria-labelledby="aHandle-h">
|
|
205
205
|
<h2 id="aHandle-h">your handle</h2>
|
|
206
206
|
<p class="aSide">The short name people find you by, the same in every project.</p>
|
|
207
|
+
<p class="projMsg ok" id="welcomeMsg" role="status" hidden>Welcome! Choose the handle people will know you by. We started you with one from your Google name, and you can change it now.</p>
|
|
207
208
|
<form class="aBody" id="handleForm" novalidate>
|
|
208
209
|
<label class="aField"><span class="aLabel">Handle</span>
|
|
209
210
|
<input type="text" id="fHandle" maxlength="39" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="your-handle"></label>
|
|
@@ -368,6 +369,13 @@
|
|
|
368
369
|
var hash = window.location && window.location.hash;
|
|
369
370
|
var target = hash && document.getElementById(hash.slice(1));
|
|
370
371
|
if (target && typeof target.scrollIntoView === 'function') target.scrollIntoView();
|
|
372
|
+
// A first Google sign-in lands here to choose a handle (task 1004993).
|
|
373
|
+
if (/[?&]welcome=google(&|$)/.test(window.location.search || '')) {
|
|
374
|
+
$('welcomeMsg').hidden = false;
|
|
375
|
+
var sec = $('aHandle');
|
|
376
|
+
if (sec && typeof sec.scrollIntoView === 'function') sec.scrollIntoView();
|
|
377
|
+
$('fHandle').focus();
|
|
378
|
+
}
|
|
371
379
|
loadBuilding();
|
|
372
380
|
loadGuilds();
|
|
373
381
|
}, function () { showAlert('Couldn’t reach the server — check your connection and refresh.'); });
|
|
Binary file
|
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
"round": 2,
|
|
8
8
|
"prompt": "Product render on a pure black background: a pair of bongo drums, one larger and one smaller joined by a short bridge, their shells liquid chrome with iridescent bronze, blue and violet reflections, crisp clean silhouette, seen straight on from slightly below so the drumheads are thin ellipses, faintly translucent drumheads, no hands, no text, no ground plane, no cast shadow or glow on the background. Pure black everywhere the drums are not.",
|
|
9
9
|
"source": ".impeccable/mocks/world-assets/r2-bongos-chrome-b.jpg",
|
|
10
|
-
"encoded": "verbatim copy of the plate (task 1003298)",
|
|
10
|
+
"encoded": "verbatim copy of the plate (task 1003298); re-encoded 2026-10-28 for page weight with sharp 0.33.5 (mozjpeg) quality 86, same 2048x2048 and format, 46.2dB PSNR against the previous file, 264391 -> 195294 bytes (task 1004930)",
|
|
11
11
|
"use": "the verbatim plate the .webp is encoded from, kept so a catalog art_url naming it still resolves; the page requests the .webp (task 1003649)"
|
|
12
12
|
}
|
|
Binary file
|