@bongos/core 1.23.22 → 1.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +902 -407
- package/.claude/skills/blocker-review/SKILL.md +9 -1
- package/.claude/skills/blocker-solve/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +1 -1
- package/.claude/skills/builder-reauth/SKILL.md +1 -1
- package/.claude/skills/builder-redteam/SKILL.md +1 -1
- package/.claude/skills/builder-sequence/SKILL.md +1 -1
- package/.claude/skills/builder-setup/SKILL.md +1 -1
- package/.claude/skills/builder-ship/SKILL.md +1 -1
- package/.claude/skills/collab-review/SKILL.md +5 -2
- package/.claude/skills/design/SKILL.md +1 -1
- package/.claude/skills/goal-create/SKILL.md +2 -2
- package/.claude/skills/grade-recover/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/owner-review/SKILL.md +1 -1
- package/.claude/skills/planning-session/SKILL.md +11 -11
- package/.claude/skills/read-session-export/SKILL.md +1 -1
- package/.claude/skills/recall/SKILL.md +1 -1
- package/.claude/skills/scan-before-install/SKILL.md +1 -1
- package/.claude/skills/session-handoff/SKILL.md +1 -1
- package/.claude/skills/strand-fix/SKILL.md +13 -0
- package/.claude/skills/worktree-clean/SKILL.md +1 -1
- package/AGENTS.md +15 -0
- package/GEMINI.md +5 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +90 -0
- package/clients/bongos-client/index.cjs +90 -0
- package/clients/bongos-client/index.d.ts +149 -4
- package/clients/bongos-client/index.mjs +90 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +3 -1
- package/docs/adr/0055-server-mediated-branch-publish.md +1 -1
- package/docs/adr/0393-a-platform-account-is-a-person-and-github-is-one-way-in.md +37 -0
- package/docs/adr/0394-market-watch-searches-through-the-messages-api-on-the-projects-key-and-runs-from-its-own-poller.md +102 -0
- package/docs/adr/0395-a-task-says-what-it-needs-and-lists-show-only-what-you-can-do.md +36 -0
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +41 -0
- package/docs/adr/README.md +5 -1
- package/docs/api/openapi.json +8555 -5876
- package/docs/api-reference.md +63 -10
- package/docs/architecture.md +37 -1
- package/docs/canonical-permissions.md +1 -1
- package/docs/copy-inventory.md +420 -337
- package/docs/copy-registry.json +1321 -500
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +5 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +3 -3
- package/docs/onboarding/diagrams/README.md +1 -1
- package/docs/onboarding/drachmae-and-karma.md +15 -15
- package/docs/onboarding/primer.md +2 -2
- package/docs/page-inventory.json +55 -6
- package/docs/page-readings.json +1185 -1011
- package/docs/recipes/google-sign-in.md +60 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/migrations/core_282_builders_lower_login_idx.sql +33 -0
- package/migrations/core_283_claimable_ignores_page_markers.sql +53 -0
- package/migrations/core_283_payout_approvals.sql +183 -0
- package/migrations/core_284_payout_gates.sql +179 -0
- package/migrations/core_285_task_needs.sql +74 -0
- package/modules/ai-studio/billing.js +67 -0
- package/modules/ai-studio/context.js +29 -0
- package/modules/ai-studio/db.js +172 -0
- package/modules/ai-studio/gemini.js +75 -0
- package/modules/ai-studio/generate.js +119 -0
- package/modules/ai-studio/keys.js +73 -0
- package/modules/ai-studio/migrations/ai_studio_001_keys.sql +22 -0
- package/modules/ai-studio/migrations/ai_studio_002_ledger.sql +48 -0
- package/modules/ai-studio/migrations/ai_studio_003_requests.sql +55 -0
- package/modules/ai-studio/openrouter.js +90 -0
- package/modules/ai-studio/payments.js +105 -0
- package/modules/ai-studio/pricing.js +92 -0
- package/modules/ai-studio/routes/ai-studio.js +235 -5
- package/modules/ai-studio/studio.js +121 -0
- package/modules/autonomy/fence.js +64 -1
- package/modules/builder-settings/builder-needs.js +27 -1
- package/modules/copy-desk/routes/copy-desk.js +5 -0
- package/modules/economy/achievements.js +22 -10
- package/modules/economy/cost-classify.js +9 -0
- package/modules/economy/cost-write-guard.js +52 -0
- package/modules/economy/cost.js +2 -1
- package/modules/economy/credits.js +18 -15
- package/modules/economy/goal-reward.js +161 -0
- package/modules/economy/reward.js +4 -0
- package/modules/economy/routes/cost.js +33 -7
- package/modules/government/catalog.js +6 -0
- package/modules/government/db.js +34 -0
- package/modules/government/docket.js +36 -0
- package/modules/government/migrations/government_035_payout_approve.sql +29 -0
- package/modules/government/pollers/board-expiry.js +108 -20
- package/modules/government/protected-surfaces.json +1 -0
- package/modules/government/routes/government.js +22 -0
- package/modules/government/routing.js +39 -5
- package/modules/government/waiting.js +74 -0
- package/modules/hall-ui/public/ai-studio-lib.js +268 -0
- package/modules/hall-ui/public/ai-studio.css +193 -0
- package/modules/hall-ui/public/ai-studio.html +98 -0
- package/modules/hall-ui/public/ai-studio.js +556 -0
- package/modules/hall-ui/public/ai-studio.states.json +94 -0
- package/modules/hall-ui/public/blockers-by-person.js +156 -0
- package/modules/hall-ui/public/blockers-lib.js +15 -1
- package/modules/hall-ui/public/blockers.html +8 -0
- package/modules/hall-ui/public/blockers.js +1 -1
- package/modules/hall-ui/public/claim-action.js +3 -0
- package/modules/hall-ui/public/collab.html +12 -0
- package/modules/hall-ui/public/craft-mode.js +4 -1
- package/modules/hall-ui/public/criterion-uat-ui.js +4 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/genesis/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-2-first-cells.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-3-genome.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-4-membrane.jpg +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/hall-ui/public/genesis/r5-life-5-nervous-system.jpg +0 -0
- package/modules/hall-ui/public/goal-inbox.js +1 -1
- package/modules/hall-ui/public/goals-render.js +4 -0
- package/modules/hall-ui/public/goals.html +8 -0
- package/modules/hall-ui/public/gov-living.js +6 -2
- package/modules/hall-ui/public/hall-ask.css +5 -0
- package/modules/hall-ui/public/hall-ask.js +39 -3
- package/modules/hall-ui/public/notes-panel.css +20 -0
- package/modules/hall-ui/public/notes-panel.js +146 -0
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/project-settings.js +7 -5
- package/modules/hall-ui/public/settings-ai-keys.js +124 -0
- package/modules/hall-ui/public/settings.html +15 -0
- package/modules/hall-ui/public/street-deck.js +12 -3
- package/modules/hall-ui/public/studio.css +90 -8
- package/modules/hall-ui/public/studio.html +39 -10
- package/modules/hall-ui/public/studio.js +148 -18
- package/modules/hall-ui/public/task.html +2 -0
- package/modules/hall-ui/public/task.js +4 -0
- package/modules/hall-ui/public/tweak-editor.js +12 -3
- package/modules/hall-ui/public/welcome-boot.css +42 -6
- package/modules/hall-ui/public/welcome-boot.js +20 -6
- package/modules/hall-ui/public/work.js +20 -0
- package/modules/hall-ui/records/ai-studio.md +36 -0
- package/modules/hall-ui/records/blockers-people.md +24 -0
- package/modules/lifecycle/db-claims.js +32 -4
- package/modules/lifecycle/db-goals.js +18 -5
- package/modules/lifecycle/db-tasks.js +15 -6
- package/modules/lifecycle/done-when.js +23 -1
- package/modules/lifecycle/lifecycle.js +5 -0
- package/modules/lifecycle/migrations/lifecycle_017_notes.sql +46 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/notes.js +219 -0
- package/modules/lifecycle/page-notice.js +124 -0
- package/modules/lifecycle/page-tweak-artist.js +18 -1
- package/modules/lifecycle/publish-reconciler.js +67 -20
- package/modules/lifecycle/routes/claims.js +5 -2
- package/modules/lifecycle/routes/goals.js +3 -1
- package/modules/lifecycle/routes/notes.js +91 -0
- package/modules/lifecycle/routes/project-settings.js +43 -35
- package/modules/lifecycle/routes/task-route-helpers.js +21 -0
- package/modules/lifecycle/routes/task-write-routes.js +21 -3
- package/modules/lifecycle/routes/tasks.js +12 -12
- package/modules/lifecycle/task-needs-gate.js +70 -0
- package/modules/market-watch/CLAUDE.md +15 -0
- package/modules/market-watch/db.js +340 -0
- package/modules/market-watch/key.js +58 -0
- package/modules/market-watch/migrations/market_watch_001_tables.sql +249 -0
- package/modules/market-watch/migrations/market_watch_002_run.sql +46 -0
- package/modules/market-watch/module.json +26 -0
- package/modules/market-watch/pollers/hub-seed.js +54 -0
- package/modules/market-watch/pollers/weekly-run.js +100 -0
- package/modules/market-watch/research.js +350 -0
- package/modules/market-watch/run.js +245 -0
- package/modules/market-watch/seed/market-data.json +38 -0
- package/modules/market-watch/seed/partners-data.json +497 -0
- package/modules/market-watch/seed.js +252 -0
- package/modules/market-watch/store.js +230 -0
- package/modules/memory/routes/memory.js +14 -2
- package/modules/memory/routes/search.js +8 -2
- package/modules/platform-identity/account-logins.js +192 -0
- package/modules/platform-identity/migrations/platform_identity_034_account_logins.sql +96 -0
- package/modules/platform-identity/platform-identity.js +5 -4
- package/modules/platform-identity/routes/my-projects.js +6 -0
- package/modules/platform-identity/routes/profile.js +18 -0
- package/modules/platform-identity/routes/public-profile.js +10 -1
- package/modules/platform-identity/routes/sso.js +26 -1
- package/modules/platform-identity/tests/platform-identity.mjs +2 -1
- package/modules/provisioning/credential-preflight.js +13 -3
- package/modules/provisioning/pollers/app-liveness.js +25 -5
- package/modules/provisioning/routes/provisioning.js +1 -1
- package/modules/provisioning/starter-bundles.js +6 -2
- package/modules/public-landing/public/account.html +10 -2
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
- package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-bead-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-frost-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-gold-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-blob-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r3-obsidian-pair-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r3-space-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-chrome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg +0 -0
- package/modules/public-landing/public/assets/world/r4-card-nebula.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-1-primordial-sea.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-2-first-cells.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-3-genome.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-4-membrane.jpg.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system-cut.webp.json +1 -1
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg +0 -0
- package/modules/public-landing/public/assets/world/r5-life-5-nervous-system.jpg.json +1 -1
- package/modules/public-landing/public/community.html +1 -1
- package/modules/public-landing/public/connections.html +2 -2
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/invitations.html +2 -2
- package/modules/public-landing/public/projects.html +5 -5
- package/modules/raise/views/nda.html +2 -0
- package/modules/specialities/db.js +32 -1
- package/modules/specialities/routes/specialities.js +10 -6
- package/modules/status-ui/public/index.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +220 -0
- package/scripts/bongos/autobongos-tweak-lane.js +46 -1
- package/scripts/bongos/backfill-subagent-rewards.js +2 -2
- package/scripts/bongos/claim.js +22 -1
- package/scripts/bongos/db-backup-nightly.sh +29 -0
- package/scripts/bongos/deploy-divergence.js +7 -0
- package/scripts/bongos/doc-cli-guard.js +6 -0
- package/scripts/bongos/fitness.js +3 -2
- package/scripts/bongos/gen-diagrams.js +3 -3
- package/scripts/bongos/lib.sh +4 -5
- package/scripts/bongos/migration-namespace.js +13 -1
- package/scripts/bongos/plain-cards.js +4 -0
- package/scripts/bongos/provision-github-setup.js +3 -1
- package/scripts/bongos/provision-units.js +6 -1
- package/scripts/bongos/publish-manifest.js +3 -0
- package/scripts/bongos/release.js +2 -0
- package/scripts/bongos/run-unit-tests.js +268 -82
- package/scripts/bongos/seed-tasks.example.json +56 -0
- package/scripts/bongos/seed-tasks.js +222 -0
- package/scripts/bongos/seed-verify-criteria.js +1 -2
- package/scripts/bongos/ship-finish.js +2 -0
- package/scripts/bongos/ship-resume.js +2 -0
- package/scripts/bongos/strand-lease.js +250 -0
- package/scripts/bongos/strand-watch.js +26 -5
- package/scripts/bongos/version-literal-guard.js +2 -2
- package/scripts/discord/post.js +4 -3
- package/scripts/gds/seed-tasks.js +7 -0
- package/scripts/gds/{build-bongos.js → strand-lease.js} +3 -3
- package/scripts/hall-preview/server.js +21 -0
- package/src/bongos/auth-config.js +29 -1
- package/src/bongos/auth-discord.js +3 -3
- package/src/bongos/auth-github.js +3 -6
- package/src/bongos/auth-google.js +217 -0
- package/src/bongos/device-approval.js +140 -0
- package/src/bongos/llm-pricing.js +17 -1
- package/src/bongos/metrics.js +48 -2
- package/src/bongos/middleware/rate-limit.js +30 -11
- package/src/bongos/module-scope-map.js +12 -1
- package/src/bongos/optimizer.js +15 -1
- package/src/bongos/payout-approvals.js +342 -0
- package/src/bongos/payout-gates.js +364 -0
- package/src/bongos/prelaunch-gate.js +7 -9
- package/src/bongos/prelaunch-switch.js +20 -0
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth-google.js +246 -0
- package/src/bongos/routes/auth.js +38 -1
- package/src/bongos/routes/backup.js +33 -9
- package/src/bongos/routes/builders.js +57 -30
- package/src/bongos/routes/github.js +9 -0
- package/src/bongos/routes/me.js +18 -0
- package/src/bongos/routes/my-payouts.js +133 -0
- package/src/bongos/routes/payout-approvals.js +312 -0
- package/src/bongos/routes/payout-gates.js +179 -0
- package/src/bongos/routes.js +15 -1
- package/src/bongos/secret-box.js +18 -3
- package/src/bongos/serve-internal.js +47 -19
- package/src/bongos/sign-in-pages.js +138 -0
- package/src/bongos/task-needs.js +146 -0
- package/src/bongos-downloads.js +18 -1
- package/src/instance-config.js +28 -0
- package/src/module-api.js +14 -1
- package/tests/account_logins.mjs +326 -0
- package/tests/account_logins_db.mjs +161 -0
- package/tests/ai_studio_billing.mjs +253 -0
- package/tests/ai_studio_generate.mjs +282 -0
- package/tests/ai_studio_keys.mjs +159 -0
- package/tests/ai_studio_requests.mjs +319 -0
- package/tests/auth_backchannel_logout_route.mjs +201 -0
- package/tests/auto_satisfy_criteria.mjs +10 -1
- package/tests/autobongos_tweak_lane.mjs +144 -0
- package/tests/autobongos_tweak_pin_identity.mjs +137 -0
- package/tests/backfill_subagent_rewards.mjs +17 -1
- package/tests/backup_trigger_lock.mjs +119 -0
- package/tests/blockers_people.mjs +198 -0
- package/tests/bounded_rate_maps.mjs +184 -0
- package/tests/claimable_blocker_status.mjs +1 -1
- package/tests/claims_batch_routes.mjs +421 -0
- package/tests/config_read_cache.mjs +171 -0
- package/tests/copy_desk_page_editor.mjs +12 -0
- package/tests/cost_write_guard.mjs +200 -0
- package/tests/craft_achievements.mjs +33 -10
- package/tests/criterion_goal_attach.mjs +1 -1
- package/tests/db_backup_nightly.mjs +73 -1
- package/tests/deploy_divergence_line.mjs +7 -0
- package/tests/effective_visibility_predicate.mjs +12 -0
- package/tests/fitness.mjs +19 -0
- package/tests/fixtures/seed-tasks/bongos-v1-idea-routing.json +256 -0
- package/tests/fixtures/seed-tasks/bv1-profiles.json +592 -0
- package/tests/fixtures/seed-tasks/bv2-ideator-surface.json +373 -0
- package/tests/fixtures/seed-tasks/fake-api.mjs +80 -0
- package/tests/github_one_trip.mjs +34 -0
- package/tests/github_repo_visibility.mjs +20 -0
- package/tests/goal_achievement.mjs +30 -1
- package/tests/goal_achievement_reward.mjs +205 -0
- package/tests/goal_sort_staleness.mjs +2 -1
- package/tests/goal_tier.mjs +11 -1
- package/tests/google_fonts_nonblocking.mjs +84 -0
- package/tests/google_sign_in.mjs +401 -0
- package/tests/government_board_expiry.mjs +94 -0
- package/tests/government_protected_surfaces.mjs +1 -1
- package/tests/government_routes.mjs +1 -1
- package/tests/hall_ai_studio.mjs +347 -0
- package/tests/hall_page_gate_map.mjs +3 -0
- package/tests/hall_studio_home.mjs +61 -2
- package/tests/home_attention_cards.mjs +1 -1
- package/tests/human_signal.mjs +141 -0
- package/tests/install_script_reader.mjs +51 -0
- package/tests/integration_fixture_ids.mjs +82 -0
- package/tests/invite_expiry_db.mjs +13 -1
- package/tests/lib_sh_resolution.mjs +0 -4
- package/tests/market_watch_data.mjs +244 -0
- package/tests/market_watch_data_db.mjs +308 -0
- package/tests/market_watch_module.mjs +114 -0
- package/tests/market_watch_run.mjs +503 -0
- package/tests/metrics.mjs +124 -0
- package/tests/migration_namespace.mjs +14 -0
- package/tests/module_api.mjs +2 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/notes.mjs +182 -0
- package/tests/own_scope_guard.mjs +5 -0
- package/tests/page_tweak_marker_sql.mjs +76 -0
- package/tests/page_tweak_no_block.mjs +135 -0
- package/tests/page_tweak_notice.mjs +103 -0
- package/tests/payout_approvals.mjs +253 -0
- package/tests/payout_approvals_db.mjs +106 -0
- package/tests/payout_gates.mjs +322 -0
- package/tests/payout_gates_db.mjs +113 -0
- package/tests/planning_session_skill.mjs +13 -3
- package/tests/project_settings_page.mjs +29 -5
- package/tests/project_settings_spend_cap_route.mjs +235 -0
- package/tests/promotion_sweep.mjs +29 -1
- package/tests/provision.mjs +3 -2
- package/tests/provisioning_app_status.mjs +18 -0
- package/tests/provisioning_credential_preflight.mjs +53 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +223 -0
- package/tests/publish_reconciler_overlap.mjs +60 -0
- package/tests/rank_change_broadcast_port.mjs +151 -0
- package/tests/seed_tasks.mjs +176 -0
- package/tests/seed_verify_criteria.mjs +2 -6
- package/tests/site_classified_once.mjs +142 -0
- package/tests/speciality_db_shape.mjs +23 -0
- package/tests/speciality_session_skills.mjs +48 -2
- package/tests/strand_lease.mjs +249 -0
- package/tests/strand_watch.mjs +10 -0
- package/tests/task_criteria_routes.mjs +275 -0
- package/tests/task_criteria_unlink_db.mjs +123 -0
- package/tests/task_needs.mjs +233 -0
- package/tests/task_needs_db.mjs +110 -0
- package/tests/unit_runner_serial.mjs +266 -43
- package/tests/version_literals.mjs +5 -7
- package/tests/welcome_boot.mjs +16 -5
- package/tests/welcome_boot_browser.mjs +36 -4
- package/docs/recipes/bongos-cli-release.md +0 -66
- package/scripts/bongos/backfill-disciplines.js +0 -144
- package/scripts/bongos/backfill-session-earnings.js +0 -123
- package/scripts/bongos/backfill-session-types.js +0 -87
- package/scripts/bongos/build-bongos.js +0 -94
- package/scripts/bongos/cleanup-orphans.js +0 -74
- package/scripts/bongos/seed-bongos-v1-idea-routing-tasks.js +0 -152
- package/scripts/bongos/seed-bv1-profiles-tasks.js +0 -135
- package/scripts/bongos/seed-bv2-ideator-surface-tasks.js +0 -390
- package/scripts/bongos/smoke-batch-claim.sh +0 -238
- package/scripts/bongos/smoke-batch-release.sh +0 -316
- package/scripts/bongos/smoke-claim-consistency.sh +0 -222
- package/scripts/bongos/smoke-dependencies.sh +0 -143
- package/scripts/gds/backfill-disciplines.js +0 -7
- package/scripts/gds/backfill-session-earnings.js +0 -7
- package/scripts/gds/backfill-session-types.js +0 -7
- package/scripts/gds/cleanup-orphans.js +0 -7
- package/scripts/gds/seed-bongos-v1-idea-routing-tasks.js +0 -6
- package/scripts/gds/seed-bv1-profiles-tasks.js +0 -6
- package/scripts/gds/seed-bv2-ideator-surface-tasks.js +0 -6
- package/scripts/gds/smoke-batch-claim.sh +0 -8
- package/scripts/gds/smoke-batch-release.sh +0 -8
- package/scripts/gds/smoke-claim-consistency.sh +0 -8
- package/scripts/gds/smoke-dependencies.sh +0 -8
- package/tests/smoke_dependencies_witness.mjs +0 -46
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// modules/ai-studio/billing.js — paying for one AI generation on our key
|
|
2
|
+
// (task 1004920, goal 1000136, ADR 0392 decision 5).
|
|
3
|
+
//
|
|
4
|
+
// The order is the guarantee the owner asked for:
|
|
5
|
+
//
|
|
6
|
+
// 1. HOLD the most the call can cost (its cost ceiling x the multiplier) on the
|
|
7
|
+
// processor-held balance. Short balance -> refuse; nothing runs.
|
|
8
|
+
// 2. RUN the provider call, which reports what it really cost.
|
|
9
|
+
// 3. CAPTURE the real price (cost x multiplier, never more than the hold); the
|
|
10
|
+
// rest of the hold goes back to the balance.
|
|
11
|
+
// On any failure in step 2 the whole hold is RELEASED.
|
|
12
|
+
//
|
|
13
|
+
// Every step writes a ledger row (amount, provider cost, markup, processor fee
|
|
14
|
+
// apart). A ledger write that fails after money moved is logged, never thrown
|
|
15
|
+
// past the money step, so a builder is never charged twice by a retry.
|
|
16
|
+
//
|
|
17
|
+
// The payments backend and the ledger writer are handed in, so this is tested
|
|
18
|
+
// with no processor and no DB.
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const P = require('./pricing');
|
|
23
|
+
|
|
24
|
+
class BillingRefusal extends Error {
|
|
25
|
+
constructor(code, message, extra) { super(message); this.code = code; Object.assign(this, extra); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// runPaid({ builderId, requestId, maxCostMicro, run, payments, ledger, log })
|
|
29
|
+
// run() -> Promise<{ costMicro, result }> the provider call
|
|
30
|
+
// Resolves { result, charge } where charge = priceFor(cost) plus the hold id.
|
|
31
|
+
async function runPaid({ builderId, requestId, maxCostMicro, run, payments, ledger, log }) {
|
|
32
|
+
const ceiling = P.priceFor(maxCostMicro).price_micro;
|
|
33
|
+
const hold = await payments.hold(builderId, ceiling);
|
|
34
|
+
if (!hold.ok) {
|
|
35
|
+
const msg = hold.reason === 'insufficient_funds'
|
|
36
|
+
? 'Your balance is too low for this generation. Top up to continue.'
|
|
37
|
+
: 'Paid generations are not set up here yet. Add your own key in Settings to use yours.';
|
|
38
|
+
throw new BillingRefusal(hold.reason, msg, { needed_micro: ceiling, available_micro: hold.available_micro });
|
|
39
|
+
}
|
|
40
|
+
await safeLedger(ledger, log, { builderId, requestId, kind: 'hold', amountMicro: ceiling, ref: hold.holdId });
|
|
41
|
+
|
|
42
|
+
let outcome;
|
|
43
|
+
try {
|
|
44
|
+
outcome = await run();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const released = await payments.release(hold.holdId);
|
|
47
|
+
await safeLedger(ledger, log, { builderId, requestId, kind: 'release', amountMicro: released.released_micro, ref: hold.holdId, note: 'provider failed' });
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const cost = Math.min(Math.max(0, Math.round(outcome.costMicro || 0)), maxCostMicro);
|
|
52
|
+
const charge = P.priceFor(cost);
|
|
53
|
+
const captured = await payments.capture(hold.holdId, Math.min(charge.price_micro, ceiling));
|
|
54
|
+
await safeLedger(ledger, log, {
|
|
55
|
+
builderId, requestId, kind: 'capture', amountMicro: captured.captured_micro,
|
|
56
|
+
providerCostMicro: charge.provider_cost_micro, markupMicro: charge.markup_micro, ref: hold.holdId,
|
|
57
|
+
});
|
|
58
|
+
return { result: outcome.result, charge: { ...charge, hold_id: hold.holdId } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function safeLedger(ledger, log, row) {
|
|
62
|
+
try { await ledger(row); } catch (err) {
|
|
63
|
+
if (log) log.error({ err: err.message, kind: row.kind, request_id: row.requestId }, 'ai-studio ledger write failed after money moved');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { runPaid, BillingRefusal };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// modules/ai-studio/context.js — reading the project context a generation is
|
|
2
|
+
// given (task 1004917, goal 1000136, ADR 0392 decision 9): the branding pack,
|
|
3
|
+
// the project context doc and DESIGN.md, all read on the SERVER from the
|
|
4
|
+
// instance's own files. The browser never sends context, so an artist cannot
|
|
5
|
+
// steer the AI away from the project by editing a request.
|
|
6
|
+
//
|
|
7
|
+
// The project doc is the instance's filled-in docs/project-context.md; a
|
|
8
|
+
// project that has not written one yet falls back to the template beside it.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const api = require('../../src/module-api');
|
|
15
|
+
const S = require('./studio');
|
|
16
|
+
|
|
17
|
+
async function readOptional(file) {
|
|
18
|
+
try { return await fs.promises.readFile(file, 'utf8'); } catch (_) { return ''; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Returns the system prompt string. `root` and `branding` are for tests.
|
|
22
|
+
async function projectContext({ root = api.resolveInstanceRoot(), branding = api.clientBranding() } = {}) {
|
|
23
|
+
const projectDoc = (await readOptional(path.join(root, 'docs', 'project-context.md')))
|
|
24
|
+
|| (await readOptional(path.join(root, 'docs', 'project-context.template.md')));
|
|
25
|
+
const designDoc = await readOptional(path.join(root, 'DESIGN.md'));
|
|
26
|
+
return S.buildContext({ branding, projectDoc, designDoc });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = { projectContext };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// modules/ai-studio/db.js — the thin SQL behind the AI studio (goal 1000136).
|
|
2
|
+
// Every function takes an optional executor so a test can hand in a fake query
|
|
3
|
+
// function; the default is the shared pool.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { pool } = require('../../src/module-api');
|
|
8
|
+
|
|
9
|
+
const exec = (executor) => executor || ((text, params) => pool.query(text, params));
|
|
10
|
+
|
|
11
|
+
// ---- provider keys (task 1004919) -------------------------------------------
|
|
12
|
+
|
|
13
|
+
// The builder's stored keys WITHOUT the ciphertext: what Settings lists.
|
|
14
|
+
async function listKeyMeta(builderId, executor) {
|
|
15
|
+
const { rows } = await exec(executor)(
|
|
16
|
+
`SELECT provider, last4, set_at, true AS key_enc
|
|
17
|
+
FROM ai_studio_provider_keys WHERE builder_id = $1`, [builderId]);
|
|
18
|
+
return rows;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// The ciphertext for one provider, for the server's own provider call only.
|
|
22
|
+
async function getKeyEnc(builderId, provider, executor) {
|
|
23
|
+
const { rows } = await exec(executor)(
|
|
24
|
+
'SELECT key_enc FROM ai_studio_provider_keys WHERE builder_id = $1 AND provider = $2', [builderId, provider]);
|
|
25
|
+
return rows[0] ? rows[0].key_enc : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function setKey(builderId, provider, keyEnc, last4, executor) {
|
|
29
|
+
const { rows } = await exec(executor)(
|
|
30
|
+
`INSERT INTO ai_studio_provider_keys (builder_id, provider, key_enc, last4)
|
|
31
|
+
VALUES ($1, $2, $3, $4)
|
|
32
|
+
ON CONFLICT (builder_id, provider)
|
|
33
|
+
DO UPDATE SET key_enc = EXCLUDED.key_enc, last4 = EXCLUDED.last4, set_at = now()
|
|
34
|
+
RETURNING provider, last4, set_at, true AS key_enc`, [builderId, provider, keyEnc, last4]);
|
|
35
|
+
return rows[0];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function clearKey(builderId, provider, executor) {
|
|
39
|
+
const { rowCount } = await exec(executor)(
|
|
40
|
+
'DELETE FROM ai_studio_provider_keys WHERE builder_id = $1 AND provider = $2', [builderId, provider]);
|
|
41
|
+
return rowCount > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- the ledger (task 1004920) ---------------------------------------------
|
|
45
|
+
// Append-only: there is no update or delete here, and the table's trigger
|
|
46
|
+
// refuses both. No balance is stored; the processor holds it (ADR 0385).
|
|
47
|
+
|
|
48
|
+
async function appendLedger(row, executor) {
|
|
49
|
+
const { rows } = await exec(executor)(
|
|
50
|
+
`INSERT INTO ai_studio_ledger
|
|
51
|
+
(builder_id, kind, request_id, amount_micro, provider_cost_micro, markup_micro, processor_fee_micro, backend, ref, note)
|
|
52
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
53
|
+
RETURNING id, created_at`,
|
|
54
|
+
[row.builderId, row.kind, row.requestId || null, row.amountMicro || 0, row.providerCostMicro || 0,
|
|
55
|
+
row.markupMicro || 0, row.processorFeeMicro || 0, row.backend, row.ref || null, (row.note || '').slice(0, 300)]);
|
|
56
|
+
return rows[0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function recentLedger(builderId, { limit = 50 } = {}, executor) {
|
|
60
|
+
const { rows } = await exec(executor)(
|
|
61
|
+
`SELECT id, kind, request_id, amount_micro, provider_cost_micro, markup_micro, processor_fee_micro, backend, note, created_at
|
|
62
|
+
FROM ai_studio_ledger WHERE builder_id = $1 ORDER BY created_at DESC, id DESC LIMIT $2`,
|
|
63
|
+
[builderId, Math.min(Math.max(1, limit | 0), 200)]);
|
|
64
|
+
return rows;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---- requests and their files (task 1004917) -------------------------------
|
|
68
|
+
// A file row's bytes are read only by getFile; every other query leaves them out.
|
|
69
|
+
|
|
70
|
+
const FILE_META = 'id, request_id, role, name, content_type, size_bytes, created_at';
|
|
71
|
+
const REQUEST_COLS = `id, builder_id, prompt, key_source, status, reply_md, provider_cost_micro, markup_micro,
|
|
72
|
+
price_micro, error, created_at, finished_at`;
|
|
73
|
+
|
|
74
|
+
async function addUpload(builderId, file, executor) {
|
|
75
|
+
const { rows } = await exec(executor)(
|
|
76
|
+
`INSERT INTO ai_studio_files (builder_id, role, name, content_type, size_bytes, bytes)
|
|
77
|
+
VALUES ($1, 'upload', $2, $3, $4, $5) RETURNING ${FILE_META}`,
|
|
78
|
+
[builderId, file.name, file.content_type, file.bytes.length, file.bytes]);
|
|
79
|
+
return rows[0];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Creates the request and attaches the builder's own, not-yet-used uploads to
|
|
83
|
+
// it in one statement. Returns null (and creates nothing) unless every id is
|
|
84
|
+
// such an upload, so a request can never borrow another builder's file.
|
|
85
|
+
async function createRequest(builderId, prompt, fileIds, executor) {
|
|
86
|
+
const { rows } = await exec(executor)(
|
|
87
|
+
`WITH ok AS (
|
|
88
|
+
SELECT count(*) AS n FROM ai_studio_files
|
|
89
|
+
WHERE id = ANY($3::bigint[]) AND builder_id = $1 AND role = 'upload' AND request_id IS NULL
|
|
90
|
+
), req AS (
|
|
91
|
+
INSERT INTO ai_studio_requests (builder_id, prompt)
|
|
92
|
+
SELECT $1, $2 FROM ok WHERE ok.n = cardinality($3::bigint[])
|
|
93
|
+
RETURNING ${REQUEST_COLS}
|
|
94
|
+
), att AS (
|
|
95
|
+
UPDATE ai_studio_files f SET request_id = req.id FROM req
|
|
96
|
+
WHERE f.id = ANY($3::bigint[]) AND f.builder_id = $1 AND f.role = 'upload' AND f.request_id IS NULL
|
|
97
|
+
RETURNING f.id
|
|
98
|
+
)
|
|
99
|
+
SELECT req.*, (SELECT count(*) FROM att) AS attached FROM req`,
|
|
100
|
+
[builderId, prompt, fileIds]);
|
|
101
|
+
return rows[0] || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function markRunning(requestId, keySource, executor) {
|
|
105
|
+
await exec(executor)(
|
|
106
|
+
`UPDATE ai_studio_requests SET status = 'running', key_source = $2 WHERE id = $1`, [requestId, keySource]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function finishRequest(requestId, f, executor) {
|
|
110
|
+
await exec(executor)(
|
|
111
|
+
`UPDATE ai_studio_requests
|
|
112
|
+
SET status = $2, reply_md = $3, provider_cost_micro = $4, markup_micro = $5, price_micro = $6,
|
|
113
|
+
error = $7, finished_at = now()
|
|
114
|
+
WHERE id = $1`,
|
|
115
|
+
[requestId, f.status, f.reply_md || null, f.provider_cost_micro ?? null, f.markup_micro ?? null,
|
|
116
|
+
f.price_micro ?? null, f.error ? String(f.error).slice(0, 500) : null]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function addOutput(requestId, builderId, img, executor) {
|
|
120
|
+
const bytes = Buffer.from(img.base64, 'base64');
|
|
121
|
+
const ext = img.content_type.split('/')[1];
|
|
122
|
+
const { rows } = await exec(executor)(
|
|
123
|
+
`INSERT INTO ai_studio_files (builder_id, request_id, role, name, content_type, size_bytes, bytes)
|
|
124
|
+
VALUES ($1, $2, 'output', $3, $4, $5, $6) RETURNING ${FILE_META}`,
|
|
125
|
+
[builderId, requestId, `generated.${ext}`, img.content_type, bytes.length, bytes]);
|
|
126
|
+
return rows[0];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The request with its files' metadata, only when it is the builder's own.
|
|
130
|
+
async function getRequest(builderId, requestId, executor) {
|
|
131
|
+
const q = exec(executor);
|
|
132
|
+
const { rows } = await q(`SELECT ${REQUEST_COLS} FROM ai_studio_requests WHERE id = $1 AND builder_id = $2`, [requestId, builderId]);
|
|
133
|
+
if (!rows[0]) return null;
|
|
134
|
+
const files = await q(`SELECT ${FILE_META} FROM ai_studio_files WHERE request_id = $1 ORDER BY id`, [requestId]);
|
|
135
|
+
return { ...rows[0], files: files.rows };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function listRequests(builderId, { limit = 30 } = {}, executor) {
|
|
139
|
+
const { rows } = await exec(executor)(
|
|
140
|
+
`SELECT ${REQUEST_COLS} FROM ai_studio_requests WHERE builder_id = $1 ORDER BY created_at DESC, id DESC LIMIT $2`,
|
|
141
|
+
[builderId, Math.min(Math.max(1, limit | 0), 100)]);
|
|
142
|
+
return rows;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// How many of the builder's requests are still queued or running (task 1004918).
|
|
146
|
+
// A request stuck past 15 minutes no longer counts, so a crashed run can never
|
|
147
|
+
// lock a builder out.
|
|
148
|
+
async function countActiveRequests(builderId, executor) {
|
|
149
|
+
const { rows } = await exec(executor)(
|
|
150
|
+
`SELECT count(*)::int AS n FROM ai_studio_requests
|
|
151
|
+
WHERE builder_id = $1 AND status IN ('queued', 'running') AND created_at > now() - interval '15 minutes'`, [builderId]);
|
|
152
|
+
return rows[0] ? rows[0].n : 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// One file's bytes, only when it is the builder's own.
|
|
156
|
+
async function getFile(builderId, fileId, executor) {
|
|
157
|
+
const { rows } = await exec(executor)(
|
|
158
|
+
'SELECT id, name, content_type, bytes FROM ai_studio_files WHERE id = $1 AND builder_id = $2', [fileId, builderId]);
|
|
159
|
+
return rows[0] || null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The uploads on a request, bytes included, for the provider calls.
|
|
163
|
+
async function requestUploads(requestId, executor) {
|
|
164
|
+
const { rows } = await exec(executor)(
|
|
165
|
+
`SELECT name, content_type, bytes FROM ai_studio_files WHERE request_id = $1 AND role = 'upload' ORDER BY id`, [requestId]);
|
|
166
|
+
return rows;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
listKeyMeta, getKeyEnc, setKey, clearKey, appendLedger, recentLedger,
|
|
171
|
+
addUpload, createRequest, markRunning, finishRequest, addOutput, getRequest, listRequests, getFile, requestUploads, countActiveRequests,
|
|
172
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// modules/ai-studio/gemini.js — the same two calls as openrouter.js, made
|
|
2
|
+
// straight to Google's Gemini API for a builder who brought a Google AI Studio
|
|
3
|
+
// key (task 1004918, goal 1000136, ADR 0392 decision 8).
|
|
4
|
+
//
|
|
5
|
+
// Only ever used on the builder's OWN key: Google's response does not report a
|
|
6
|
+
// price, so it could not be billed at cost x the multiplier, and nothing here
|
|
7
|
+
// pretends to. The cost comes back as null and the request records it as "not
|
|
8
|
+
// reported by the provider".
|
|
9
|
+
//
|
|
10
|
+
// Same interface as openrouter.js (reply / images, { text | images, costMicro }),
|
|
11
|
+
// so generate.js does not care which one it was handed. The key goes only in
|
|
12
|
+
// the x-goog-api-key header, never in the URL, where proxies and logs keep it.
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const { ProviderError } = require('./openrouter');
|
|
17
|
+
|
|
18
|
+
const BASE = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
19
|
+
const MODELS = { reply: 'gemini-2.5-pro', images: 'gemini-2.5-flash-image' };
|
|
20
|
+
const TIMEOUT_MS = 120000;
|
|
21
|
+
|
|
22
|
+
function parts(text, files) {
|
|
23
|
+
return [{ text }, ...(files || []).map((f) => ({ inline_data: { mime_type: f.content_type, data: f.base64 } }))];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function post(fetchImpl, key, model, body) {
|
|
27
|
+
const ctl = new AbortController();
|
|
28
|
+
const timer = setTimeout(() => ctl.abort(), TIMEOUT_MS);
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await fetchImpl(`${BASE}/${model}:generateContent`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'x-goog-api-key': key, 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify(body),
|
|
35
|
+
signal: ctl.signal,
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new ProviderError(err.name === 'AbortError' ? 'provider_timeout' : 'provider_unreachable', 'Google could not be reached.');
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
}
|
|
42
|
+
let json = null;
|
|
43
|
+
try { json = await res.json(); } catch (_) { /* fall through */ }
|
|
44
|
+
if (!res.ok || !json || json.error) {
|
|
45
|
+
throw new ProviderError('provider_error', `Google refused the request (HTTP ${res.status}). Check the key in Settings.`);
|
|
46
|
+
}
|
|
47
|
+
const cand = json.candidates && json.candidates[0];
|
|
48
|
+
return (cand && cand.content && Array.isArray(cand.content.parts)) ? cand.content.parts : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reply({ fetchImpl, key, system, prompt, files }) {
|
|
52
|
+
const out = await post(fetchImpl, key, MODELS.reply, {
|
|
53
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
54
|
+
contents: [{ role: 'user', parts: parts(prompt, files) }],
|
|
55
|
+
});
|
|
56
|
+
const text = out.filter((p) => typeof p.text === 'string').map((p) => p.text).join('').trim();
|
|
57
|
+
if (!text) throw new ProviderError('provider_empty', 'Google returned no reply.');
|
|
58
|
+
return { text, costMicro: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function images({ fetchImpl, key, system, brief, files }) {
|
|
62
|
+
const out = await post(fetchImpl, key, MODELS.images, {
|
|
63
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
64
|
+
contents: [{ role: 'user', parts: parts(brief, (files || []).filter((f) => f.content_type !== 'application/pdf')) }],
|
|
65
|
+
generationConfig: { responseModalities: ['TEXT', 'IMAGE'] },
|
|
66
|
+
});
|
|
67
|
+
const list = out
|
|
68
|
+
.map((p) => p.inlineData || p.inline_data)
|
|
69
|
+
.filter((d) => d && /^image\/(png|jpeg|webp)$/.test(d.mimeType || d.mime_type) && d.data)
|
|
70
|
+
.map((d) => ({ content_type: d.mimeType || d.mime_type, base64: d.data }));
|
|
71
|
+
if (!list.length) throw new ProviderError('provider_no_images', 'Google returned no images.');
|
|
72
|
+
return { images: list, costMicro: null };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = { BASE, reply, images };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// modules/ai-studio/generate.js — running one submitted generation end to end
|
|
2
|
+
// (task 1004917, goal 1000136, ADR 0392): the Gemini reply, then the images,
|
|
3
|
+
// paid for on our key through billing.runPaid, with the result stored on the
|
|
4
|
+
// request row.
|
|
5
|
+
//
|
|
6
|
+
// Everything with a side effect is handed in (`deps`): the provider calls, the
|
|
7
|
+
// payments backend, the db, the logger, our key. So the whole path runs in a
|
|
8
|
+
// test with fakes, and the route only wires the real ones.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const S = require('./studio');
|
|
13
|
+
const { runPaid, BillingRefusal } = require('./billing');
|
|
14
|
+
|
|
15
|
+
// deps: { db, payments, providers: { reply, images }, fetchImpl, ourKey, context, log }
|
|
16
|
+
// request: { id, builder_id, prompt }, files: [{ name, content_type, base64 }] (studio.forProvider)
|
|
17
|
+
async function runOnOurKey(request, files, deps) {
|
|
18
|
+
const { db, payments, providers, fetchImpl, ourKey, context, log } = deps;
|
|
19
|
+
if (!ourKey) {
|
|
20
|
+
await db.finishRequest(request.id, { status: 'refused', error: 'Generations on our key are not set up here yet. Add your own key in Settings.' });
|
|
21
|
+
return { status: 'refused', code: 'our_key_not_configured' };
|
|
22
|
+
}
|
|
23
|
+
await db.markRunning(request.id, 'ours');
|
|
24
|
+
const ledger = (row) => db.appendLedger({ ...row, backend: payments.name });
|
|
25
|
+
|
|
26
|
+
let paid;
|
|
27
|
+
try {
|
|
28
|
+
paid = await runPaid({
|
|
29
|
+
builderId: request.builder_id,
|
|
30
|
+
requestId: String(request.id),
|
|
31
|
+
maxCostMicro: S.MAX_COST_MICRO,
|
|
32
|
+
payments,
|
|
33
|
+
ledger,
|
|
34
|
+
log,
|
|
35
|
+
run: async () => {
|
|
36
|
+
const r = await providers.reply({ fetchImpl, key: ourKey, system: context, prompt: request.prompt, files });
|
|
37
|
+
const im = await providers.images({ fetchImpl, key: ourKey, system: context, brief: S.imageBrief(request.prompt, r.text), files });
|
|
38
|
+
return { costMicro: r.costMicro + im.costMicro, result: { reply: r.text, images: im.images } };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const refused = err instanceof BillingRefusal;
|
|
43
|
+
await db.finishRequest(request.id, {
|
|
44
|
+
status: refused ? 'refused' : 'failed',
|
|
45
|
+
error: refused ? err.message : `${err.message || 'The generation failed.'} You were not charged.`,
|
|
46
|
+
});
|
|
47
|
+
if (!refused && log) log.warn({ request_id: request.id, code: err.code }, 'ai generation failed; hold released');
|
|
48
|
+
return { status: refused ? 'refused' : 'failed', code: err.code };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Paid from here on: a storage failure must not be reported as "not charged".
|
|
52
|
+
const { result, charge } = paid;
|
|
53
|
+
const receipt = { provider_cost_micro: charge.provider_cost_micro, markup_micro: charge.markup_micro, price_micro: charge.price_micro };
|
|
54
|
+
try {
|
|
55
|
+
for (const img of result.images) await db.addOutput(request.id, request.builder_id, img);
|
|
56
|
+
await db.finishRequest(request.id, { status: 'done', reply_md: result.reply, ...receipt });
|
|
57
|
+
return { status: 'done', charge };
|
|
58
|
+
} catch (err) {
|
|
59
|
+
if (log) log.error({ request_id: request.id, err: err.message }, 'ai generation paid but its result could not be stored');
|
|
60
|
+
await db.finishRequest(request.id, {
|
|
61
|
+
status: 'failed', reply_md: result.reply, ...receipt,
|
|
62
|
+
error: 'The images were made and paid for, but could not be saved. Ask the project owner to look into it.',
|
|
63
|
+
}).catch(() => {});
|
|
64
|
+
return { status: 'failed', code: 'store_failed', charge };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// On the builder's OWN key (task 1004918, ADR 0392 decision 8): the same two
|
|
69
|
+
// calls, but no money moves. Nothing is held, captured or released, the
|
|
70
|
+
// payments backend is never even handed in, and the ledger gets one own_key row
|
|
71
|
+
// noting what the provider reported it cost them (null when it reports nothing,
|
|
72
|
+
// as Google does).
|
|
73
|
+
//
|
|
74
|
+
// deps: { db, providers, fetchImpl, key, provider, context, log }
|
|
75
|
+
async function runOnOwnKey(request, files, deps) {
|
|
76
|
+
const { db, providers, fetchImpl, key, provider, context, log } = deps;
|
|
77
|
+
await db.markRunning(request.id, 'own');
|
|
78
|
+
let result;
|
|
79
|
+
let cost = null;
|
|
80
|
+
try {
|
|
81
|
+
const r = await providers.reply({ fetchImpl, key, system: context, prompt: request.prompt, files });
|
|
82
|
+
const im = await providers.images({ fetchImpl, key, system: context, brief: S.imageBrief(request.prompt, r.text), files });
|
|
83
|
+
cost = Number.isSafeInteger(r.costMicro) && Number.isSafeInteger(im.costMicro) ? r.costMicro + im.costMicro : null;
|
|
84
|
+
result = { reply: r.text, images: im.images };
|
|
85
|
+
} catch (err) {
|
|
86
|
+
await db.finishRequest(request.id, {
|
|
87
|
+
status: 'failed',
|
|
88
|
+
error: `${err.message || 'The generation failed.'} Nothing was charged by us; your provider may bill you for any part that ran.`,
|
|
89
|
+
});
|
|
90
|
+
if (log) log.warn({ request_id: request.id, provider, code: err.code }, 'ai generation on own key failed');
|
|
91
|
+
return { status: 'failed', code: err.code };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
await db.appendLedger({
|
|
96
|
+
builderId: request.builder_id, requestId: String(request.id), kind: 'own_key', amountMicro: 0,
|
|
97
|
+
providerCostMicro: cost || 0, backend: 'own_key',
|
|
98
|
+
note: cost === null ? `on your own ${provider} key; the provider did not report a cost` : `on your own ${provider} key`,
|
|
99
|
+
});
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (log) log.error({ request_id: request.id, err: err.message }, 'ai-studio own-key ledger write failed');
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
for (const img of result.images) await db.addOutput(request.id, request.builder_id, img);
|
|
105
|
+
await db.finishRequest(request.id, {
|
|
106
|
+
status: 'done', reply_md: result.reply, provider_cost_micro: cost, markup_micro: 0, price_micro: 0,
|
|
107
|
+
});
|
|
108
|
+
return { status: 'done', costMicro: cost };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (log) log.error({ request_id: request.id, err: err.message }, 'ai generation on own key ran but its result could not be stored');
|
|
111
|
+
await db.finishRequest(request.id, {
|
|
112
|
+
status: 'failed', reply_md: result.reply, provider_cost_micro: cost, markup_micro: 0, price_micro: 0,
|
|
113
|
+
error: 'The images were made on your key, but could not be saved. Ask the project owner to look into it.',
|
|
114
|
+
}).catch(() => {});
|
|
115
|
+
return { status: 'failed', code: 'store_failed' };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { runOnOurKey, runOnOwnKey };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// modules/ai-studio/keys.js — a builder's own AI provider keys (task 1004919,
|
|
2
|
+
// goal 1000136, ADR 0392 decision 8).
|
|
3
|
+
//
|
|
4
|
+
// A key is entered ONLY on the Settings page, stored encrypted with the kernel's
|
|
5
|
+
// secret box, and shown back only as its last four characters. Nothing in this
|
|
6
|
+
// module returns a plaintext key to a route, and `publicKey` is the one shape a
|
|
7
|
+
// route may send: it has no field that could carry the key or its ciphertext.
|
|
8
|
+
//
|
|
9
|
+
// Pure: the encrypt/decrypt functions are handed in, so a test runs it with no
|
|
10
|
+
// master key and no DB.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
// The providers a builder may bring a key for. A new one is a new entry here
|
|
15
|
+
// plus whatever calls it; the Settings card renders this list.
|
|
16
|
+
const PROVIDERS = Object.freeze({
|
|
17
|
+
openrouter: { label: 'OpenRouter', learn_more_url: 'https://openrouter.ai/settings/keys' },
|
|
18
|
+
gemini: { label: 'Google AI Studio (Gemini)', learn_more_url: 'https://aistudio.google.com/app/apikey' },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const MIN_KEY = 16;
|
|
22
|
+
const MAX_KEY = 512;
|
|
23
|
+
|
|
24
|
+
function isProvider(p) { return typeof p === 'string' && Object.prototype.hasOwnProperty.call(PROVIDERS, p); }
|
|
25
|
+
|
|
26
|
+
// Trims and sanity-checks a pasted key. Returns { key } or { error }. It never
|
|
27
|
+
// echoes the key back inside the error.
|
|
28
|
+
function cleanKey(raw) {
|
|
29
|
+
if (typeof raw !== 'string') return { error: 'key_required' };
|
|
30
|
+
const key = raw.trim();
|
|
31
|
+
if (!key) return { error: 'key_required' };
|
|
32
|
+
if (key.length < MIN_KEY || key.length > MAX_KEY) return { error: 'key_wrong_length' };
|
|
33
|
+
if (/\s/.test(key)) return { error: 'key_has_spaces' };
|
|
34
|
+
return { key };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function last4(key) { return key.slice(-4); }
|
|
38
|
+
|
|
39
|
+
// The only shape a route sends for a stored key: which provider, whether one is
|
|
40
|
+
// set, its last four characters and when. Built field by field from the row so
|
|
41
|
+
// a new column can never leak through a spread.
|
|
42
|
+
function publicKey(provider, row) {
|
|
43
|
+
return {
|
|
44
|
+
provider,
|
|
45
|
+
label: PROVIDERS[provider].label,
|
|
46
|
+
learn_more_url: PROVIDERS[provider].learn_more_url,
|
|
47
|
+
set: !!(row && row.key_enc),
|
|
48
|
+
last4: row && row.key_enc ? row.last4 : null,
|
|
49
|
+
set_at: row && row.key_enc ? row.set_at : null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Every provider in display order, merged with the builder's stored rows.
|
|
54
|
+
function listPublic(rows) {
|
|
55
|
+
const byProvider = new Map((rows || []).map((r) => [r.provider, r]));
|
|
56
|
+
return Object.keys(PROVIDERS).map((p) => publicKey(p, byProvider.get(p)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Which key a generation runs on (task 1004918). `stored` is the providers the
|
|
60
|
+
// builder has a key for; `payWith` is what they asked for: 'own', 'ours' or
|
|
61
|
+
// nothing. With nothing asked, their own key wins whenever they have one, so a
|
|
62
|
+
// builder who linked a key is never charged by surprise. OpenRouter is
|
|
63
|
+
// preferred over Gemini when both are set: it reports its cost.
|
|
64
|
+
// Returns { source: 'own', provider } | { source: 'ours' } | { error }.
|
|
65
|
+
function chooseKey(stored, payWith) {
|
|
66
|
+
const own = ['openrouter', 'gemini'].find((p) => (stored || []).includes(p));
|
|
67
|
+
if (payWith === 'ours') return { source: 'ours' };
|
|
68
|
+
if (payWith === 'own' && !own) return { error: 'no_own_key' };
|
|
69
|
+
if (payWith !== undefined && payWith !== null && payWith !== 'own') return { error: 'bad_pay_with' };
|
|
70
|
+
return own ? { source: 'own', provider: own } : { source: 'ours' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { PROVIDERS, MIN_KEY, MAX_KEY, isProvider, cleanKey, last4, publicKey, listPublic, chooseKey };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- ai_studio_001_keys.sql — a builder's own AI provider keys, encrypted at rest
|
|
2
|
+
-- (task 1004919, goal 1000136, ADR 0388 decision 8).
|
|
3
|
+
--
|
|
4
|
+
-- One row per builder and provider. key_enc is the kernel secret box's blob
|
|
5
|
+
-- ("v1.<iv>.<tag>.<ct>"); the master key lives only in server env, so a database
|
|
6
|
+
-- dump alone decrypts nothing. last4 is shown in Settings ("…ABCD") and is not a
|
|
7
|
+
-- secret. Removing a key deletes the row.
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS ai_studio_provider_keys (
|
|
10
|
+
builder_id BIGINT NOT NULL REFERENCES builders(id),
|
|
11
|
+
provider TEXT NOT NULL,
|
|
12
|
+
key_enc TEXT NOT NULL,
|
|
13
|
+
last4 TEXT NOT NULL,
|
|
14
|
+
set_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
15
|
+
PRIMARY KEY (builder_id, provider),
|
|
16
|
+
CONSTRAINT ai_studio_provider_keys_provider_chk CHECK (provider IN ('openrouter', 'gemini')),
|
|
17
|
+
CONSTRAINT ai_studio_provider_keys_last4_chk CHECK (length(last4) = 4),
|
|
18
|
+
CONSTRAINT ai_studio_provider_keys_enc_chk CHECK (key_enc LIKE 'v1.%')
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
INSERT INTO schema_migrations (version) VALUES ('ai_studio_001_keys')
|
|
22
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- ai_studio_002_ledger.sql — the append-only record of every AI-studio money
|
|
2
|
+
-- movement (task 1004920, goal 1000136, ADR 0392 decision 6).
|
|
3
|
+
--
|
|
4
|
+
-- Bongos holds no user money (ADR 0385): the balance lives with the payment
|
|
5
|
+
-- processor, so there is deliberately NO balance column anywhere here. What we
|
|
6
|
+
-- keep is the record of each movement and its cause, which that ADR requires:
|
|
7
|
+
--
|
|
8
|
+
-- topup a card payment credited to the processor-held balance
|
|
9
|
+
-- hold the full price reserved before a generation runs
|
|
10
|
+
-- capture the real price taken after it ran (cost and markup apart)
|
|
11
|
+
-- release a hold returned, e.g. because the provider failed
|
|
12
|
+
-- own_key a generation on the builder's own key: no money moved, cost noted
|
|
13
|
+
--
|
|
14
|
+
-- Amounts are integer micro-dollars. Rows are never updated or deleted (the
|
|
15
|
+
-- trigger below refuses both), so the record cannot be rewritten.
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS ai_studio_ledger (
|
|
18
|
+
id BIGSERIAL PRIMARY KEY,
|
|
19
|
+
builder_id BIGINT NOT NULL REFERENCES builders(id),
|
|
20
|
+
kind TEXT NOT NULL,
|
|
21
|
+
request_id TEXT,
|
|
22
|
+
amount_micro BIGINT NOT NULL DEFAULT 0,
|
|
23
|
+
provider_cost_micro BIGINT NOT NULL DEFAULT 0,
|
|
24
|
+
markup_micro BIGINT NOT NULL DEFAULT 0,
|
|
25
|
+
processor_fee_micro BIGINT NOT NULL DEFAULT 0,
|
|
26
|
+
backend TEXT NOT NULL,
|
|
27
|
+
ref TEXT,
|
|
28
|
+
note TEXT NOT NULL DEFAULT '',
|
|
29
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
30
|
+
CONSTRAINT ai_studio_ledger_kind_chk CHECK (kind IN ('topup', 'hold', 'capture', 'release', 'own_key')),
|
|
31
|
+
CONSTRAINT ai_studio_ledger_amounts_chk CHECK (amount_micro >= 0 AND provider_cost_micro >= 0 AND markup_micro >= 0 AND processor_fee_micro >= 0),
|
|
32
|
+
CONSTRAINT ai_studio_ledger_note_chk CHECK (length(note) <= 300)
|
|
33
|
+
);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS ai_studio_ledger_by_builder ON ai_studio_ledger (builder_id, created_at DESC);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS ai_studio_ledger_by_request ON ai_studio_ledger (request_id) WHERE request_id IS NOT NULL;
|
|
36
|
+
|
|
37
|
+
CREATE OR REPLACE FUNCTION ai_studio_ledger_append_only() RETURNS trigger AS $$
|
|
38
|
+
BEGIN
|
|
39
|
+
RAISE EXCEPTION 'ai_studio_ledger is append-only';
|
|
40
|
+
END;
|
|
41
|
+
$$ LANGUAGE plpgsql;
|
|
42
|
+
|
|
43
|
+
DROP TRIGGER IF EXISTS ai_studio_ledger_no_rewrite ON ai_studio_ledger;
|
|
44
|
+
CREATE TRIGGER ai_studio_ledger_no_rewrite BEFORE UPDATE OR DELETE ON ai_studio_ledger
|
|
45
|
+
FOR EACH ROW EXECUTE FUNCTION ai_studio_ledger_append_only();
|
|
46
|
+
|
|
47
|
+
INSERT INTO schema_migrations (version) VALUES ('ai_studio_002_ledger')
|
|
48
|
+
ON CONFLICT DO NOTHING;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
-- ai_studio_003_requests.sql — an artist's generation requests and the files on
|
|
2
|
+
-- them (task 1004917, goal 1000136, ADR 0392).
|
|
3
|
+
--
|
|
4
|
+
-- ai_studio_requests one submission: the prompt, which key paid (ours or the
|
|
5
|
+
-- builder's own), where it stands, the reply, and the
|
|
6
|
+
-- price split into provider cost and markup.
|
|
7
|
+
-- ai_studio_files the sketches and documents the artist uploaded (role
|
|
8
|
+
-- 'upload') and the images that came back ('output').
|
|
9
|
+
-- An upload is stored first with no request, then
|
|
10
|
+
-- attached when the request is sent.
|
|
11
|
+
--
|
|
12
|
+
-- Amounts are integer micro-dollars. The money record itself is the ledger
|
|
13
|
+
-- (ai_studio_002); the price here is the request's own receipt.
|
|
14
|
+
|
|
15
|
+
CREATE TABLE IF NOT EXISTS ai_studio_requests (
|
|
16
|
+
id BIGSERIAL PRIMARY KEY,
|
|
17
|
+
builder_id BIGINT NOT NULL REFERENCES builders(id),
|
|
18
|
+
prompt TEXT NOT NULL,
|
|
19
|
+
key_source TEXT,
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'queued',
|
|
21
|
+
reply_md TEXT,
|
|
22
|
+
provider_cost_micro BIGINT,
|
|
23
|
+
markup_micro BIGINT,
|
|
24
|
+
price_micro BIGINT,
|
|
25
|
+
error TEXT,
|
|
26
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
27
|
+
finished_at TIMESTAMPTZ,
|
|
28
|
+
CONSTRAINT ai_studio_requests_status_chk CHECK (status IN ('queued', 'running', 'done', 'failed', 'refused')),
|
|
29
|
+
CONSTRAINT ai_studio_requests_key_chk CHECK (key_source IS NULL OR key_source IN ('ours', 'own')),
|
|
30
|
+
CONSTRAINT ai_studio_requests_money_chk CHECK (
|
|
31
|
+
COALESCE(provider_cost_micro, 0) >= 0 AND COALESCE(markup_micro, 0) >= 0 AND COALESCE(price_micro, 0) >= 0)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
CREATE INDEX IF NOT EXISTS ai_studio_requests_builder_idx ON ai_studio_requests (builder_id, created_at DESC);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE IF NOT EXISTS ai_studio_files (
|
|
37
|
+
id BIGSERIAL PRIMARY KEY,
|
|
38
|
+
builder_id BIGINT NOT NULL REFERENCES builders(id),
|
|
39
|
+
request_id BIGINT REFERENCES ai_studio_requests(id) ON DELETE CASCADE,
|
|
40
|
+
role TEXT NOT NULL,
|
|
41
|
+
name TEXT NOT NULL,
|
|
42
|
+
content_type TEXT NOT NULL,
|
|
43
|
+
size_bytes INTEGER NOT NULL,
|
|
44
|
+
bytes BYTEA NOT NULL,
|
|
45
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
46
|
+
CONSTRAINT ai_studio_files_role_chk CHECK (role IN ('upload', 'output')),
|
|
47
|
+
CONSTRAINT ai_studio_files_type_chk CHECK (content_type IN ('image/png', 'image/jpeg', 'image/gif', 'image/webp', 'application/pdf')),
|
|
48
|
+
CONSTRAINT ai_studio_files_size_chk CHECK (size_bytes > 0 AND size_bytes <= 8388608)
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
CREATE INDEX IF NOT EXISTS ai_studio_files_request_idx ON ai_studio_files (request_id);
|
|
52
|
+
CREATE INDEX IF NOT EXISTS ai_studio_files_builder_idx ON ai_studio_files (builder_id, created_at DESC);
|
|
53
|
+
|
|
54
|
+
INSERT INTO schema_migrations (version) VALUES ('ai_studio_003_requests')
|
|
55
|
+
ON CONFLICT DO NOTHING;
|