cartethyia 2.7.2 → 2.7.4
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/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +2 -0
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
- package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +118 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
- package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
- package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
- package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
- package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
- package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
- package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
- package/package.json +2 -1
- package/src/boot/app.ts +9 -8
- package/src/config/network.ts +7 -3
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +475 -410
- package/src/console/api-keys/routes.ts +211 -23
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/account-reset.ts +4 -4
- package/src/console/auth/session.ts +3 -3
- package/src/console/backup/contracts.ts +2 -0
- package/src/console/backup/export-endpoint.ts +14 -17
- package/src/console/backup/routes.ts +43 -11
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/routes.ts +3 -3
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +4 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +4 -2
- package/src/console/observability/contracts.ts +4 -0
- package/src/console/observability/live.ts +3 -3
- package/src/console/observability/logs.ts +3 -3
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/store.ts +16 -16
- package/src/console/providers/catalog/contracts.ts +43 -22
- package/src/console/providers/catalog/provider-operations.ts +17 -2
- package/src/console/providers/catalog/routes.ts +3 -3
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1523 -1292
- package/src/console/providers/detail/routes.ts +3 -3
- package/src/console/providers/oauth/routes.ts +3 -3
- package/src/console/quota/account-quota-tenant.ts +34 -2
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/account-test-egress.ts +4 -4
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +11 -8
- package/src/console/routing/pools/operations.ts +14 -7
- package/src/console/routing/pools/probe-verdict.ts +8 -3
- package/src/console/routing/pools/relay-deploy.ts +9 -9
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/routes.ts +3 -3
- package/src/console/share/share-router.ts +51 -18
- package/src/console/share/share-stats.ts +158 -49
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/shared/read-through-cache.ts +38 -2
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/console/updates/routes.ts +7 -4
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +75 -4
- package/src/network/pool/resolver.ts +39 -39
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +21 -1
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/ssrf.ts +4 -4
- package/src/observability/event-query.ts +437 -22
- package/src/observability/telemetry-status.ts +1 -1
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/db-sqlite.ts +7 -2
- package/src/persistence/readiness.ts +18 -6
- package/src/persistence/schema.ts +162 -1
- package/src/persistence/stores/api-key-store.ts +849 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +58 -2
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/codecs.ts +6 -6
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-generation.ts +2 -2
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/protocol/response/chat.ts +2 -2
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +5 -5
- package/src/protocol/response/responses.ts +2 -2
- package/src/protocol/sse-decoder.ts +14 -3
- package/src/protocol/translation/capabilities.ts +2 -2
- package/src/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +2 -2
- package/src/providers/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/discovery/probe-phases.ts +2 -2
- package/src/providers/discovery/probing-service.ts +12 -12
- package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity.ts +33 -47
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
- package/src/providers/integrations/buddy/buddy-media.ts +21 -17
- package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-credentials.ts +1 -1
- package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
- package/src/providers/integrations/claude/claude.ts +3 -10
- package/src/providers/integrations/cline/cline.ts +8 -1
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +4 -4
- package/src/providers/integrations/commandcode/quota.ts +5 -1
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
- package/src/providers/integrations/cursor/cursor.ts +15 -15
- package/src/providers/integrations/factory/factory.ts +4 -4
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github.ts +26 -7
- package/src/providers/integrations/grok/grok.ts +2 -6
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- package/src/providers/integrations/kiro/kiro.ts +10 -10
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
- package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
- package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
- package/src/providers/integrations/qoder/qoder.ts +192 -24
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae.ts +6 -9
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-error-classification.ts +6 -8
- package/src/providers/operations/account-health-service.ts +14 -16
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +4 -4
- package/src/providers/operations/provider-credential-service.ts +16 -4
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +55 -3
- package/src/providers/provider-credential-resolver.ts +1 -1
- package/src/providers/provider-registry.ts +4 -4
- package/src/providers/quota/quota-contracts.ts +24 -10
- package/src/providers/quota/refresh.ts +2 -2
- package/src/providers/search/search-provider.ts +5 -5
- package/src/security/abuse.ts +4 -4
- package/src/security/access-control.ts +19 -0
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/api-key-auth.ts +18 -5
- package/src/shared/coerce.ts +19 -0
- package/src/transport/dispatch/attempt-finalize.ts +4 -3
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/empty-stream.ts +6 -6
- package/src/transport/dispatch/generation.ts +9 -9
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/proxy-request.ts +38 -9
- package/src/transport/dispatch/responses-compact.ts +11 -11
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/finalize.ts +9 -8
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/systemone.ts +8 -8
- package/src/transport/dispatch/token-count.ts +9 -9
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/dispatch/websearch.ts +6 -6
- package/src/transport/drain-endpoint.ts +2 -2
- package/src/transport/failure-policy.ts +13 -6
- package/src/transport/guards/auth-guard.ts +6 -5
- package/src/transport/guards/body-policy.ts +4 -4
- package/src/transport/guards/error-lifecycle.ts +18 -3
- package/src/transport/guards/guard-errors.ts +7 -3
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +6 -6
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +3 -3
- package/src/transport/request/preparer.ts +37 -21
- package/src/transport/request/state.ts +4 -7
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +104 -2
- package/src/transport/routing/route-model.ts +60 -7
- package/src/transport/routing/router.ts +306 -256
- package/src/transport/shutdown-notice.ts +19 -7
- package/src/transport/surface/completion.ts +1 -1
- package/src/transport/surface/messages/encode.ts +1 -1
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
- package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
- package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
- package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
- package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
- package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
|
@@ -69,11 +69,11 @@ export function createPerplexityAdapter(fetchImpl?: typeof fetch): ProviderAdapt
|
|
|
69
69
|
void _candidate;
|
|
70
70
|
const query = extractSearchQuery(request);
|
|
71
71
|
if (!query) {
|
|
72
|
-
throw new GatewayError("invalid_request", 400, "Search query is required.");
|
|
72
|
+
throw new GatewayError("invalid_request", 400, "Search query is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
|
|
73
73
|
}
|
|
74
74
|
const token = tokenFromContext(context);
|
|
75
75
|
if (!token) {
|
|
76
|
-
throw new GatewayError("authentication_failed", 401, "Missing Perplexity API key");
|
|
76
|
+
throw new GatewayError("authentication_failed", 401, "Missing Perplexity API key (request {requestId}). Wrong key, right door — check the key 🔑");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const fetchFn = (context.outbound_fetch as unknown as typeof fetch | undefined) ?? fetchImpl ?? globalThis.fetch;
|
|
@@ -133,13 +133,15 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
|
|
|
133
133
|
qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: true, is_reasoning: true },
|
|
134
134
|
qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
135
135
|
qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
|
|
136
|
-
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|
|
136
|
+
kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3.1-Pro", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
|
|
137
137
|
kmodel: { id: "kmodel", display_name: "Kimi-K2.8-Preview", max_input_tokens: 1000000, max_output_tokens: 65_536, is_vl: true, is_free: false, is_reasoning: false },
|
|
138
138
|
gmodel: { id: "gmodel", display_name: "GLM-5.3", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: true },
|
|
139
139
|
gfmodel: { id: "gfmodel", display_name: "GLM-5.3-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
|
|
140
140
|
dmodel: { id: "dmodel", display_name: "DeepSeek-V4-Pro", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// Live roster (2026-11-01) reports dfmodel as billed (is_free false); it was
|
|
142
|
+
// previously marked free. Display names are corrected from the same capture.
|
|
143
|
+
dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
|
|
144
|
+
mmodel: { id: "mmodel", display_name: "MiniMax-M3.1-Flash", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: false },
|
|
143
145
|
};
|
|
144
146
|
|
|
145
147
|
/**
|
|
@@ -353,6 +355,14 @@ interface CachedRoster {
|
|
|
353
355
|
/** Per-realm live-list cache; keyed by realm, not by account. */
|
|
354
356
|
const discoveryCache = new Map<QoderRealm, CachedRoster>();
|
|
355
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Per-realm max-context window per roster key, taken from the live list's
|
|
360
|
+
* `context_config` (the widest selectable window). This is what dispatch reads
|
|
361
|
+
* to send `model_config.max_input_tokens`; without it the request would use the
|
|
362
|
+
* static roster's *currently active* window (often 180k) instead of the max.
|
|
363
|
+
*/
|
|
364
|
+
const discoveryMaxContextByKey = new Map<QoderRealm, Map<string, number>>();
|
|
365
|
+
|
|
356
366
|
/**
|
|
357
367
|
* Drops one realm's cached list.
|
|
358
368
|
*
|
|
@@ -361,8 +371,21 @@ const discoveryCache = new Map<QoderRealm, CachedRoster>();
|
|
|
361
371
|
* previous assertion left behind.
|
|
362
372
|
*/
|
|
363
373
|
export function resetQoderDiscoveryCache(realm?: QoderRealm): void {
|
|
364
|
-
if (realm === undefined)
|
|
365
|
-
|
|
374
|
+
if (realm === undefined) {
|
|
375
|
+
discoveryCache.clear();
|
|
376
|
+
discoveryMaxContextByKey.clear();
|
|
377
|
+
} else {
|
|
378
|
+
discoveryCache.delete(realm);
|
|
379
|
+
discoveryMaxContextByKey.delete(realm);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* The widest selectable context window per roster key for one realm, from the
|
|
385
|
+
* live model list. Empty when discovery has not succeeded for that realm yet.
|
|
386
|
+
*/
|
|
387
|
+
export function qoderDiscoveredMaxContexts(realm: QoderRealm): ReadonlyMap<string, number> {
|
|
388
|
+
return discoveryMaxContextByKey.get(realm) ?? new Map();
|
|
366
389
|
}
|
|
367
390
|
|
|
368
391
|
export async function discoverQoderModels(input: {
|
|
@@ -389,7 +412,7 @@ export async function discoverQoderModels(input: {
|
|
|
389
412
|
throw new GatewayError(
|
|
390
413
|
"authentication_failed",
|
|
391
414
|
401,
|
|
392
|
-
"Qoder model discovery requires an account credential.",
|
|
415
|
+
"Qoder model discovery requires an account credential (request {requestId}). Wrong key, right door — check the credential 🔑",
|
|
393
416
|
{ providerId, realm: input.realm },
|
|
394
417
|
);
|
|
395
418
|
}
|
|
@@ -398,7 +421,7 @@ export async function discoverQoderModels(input: {
|
|
|
398
421
|
throw new GatewayError(
|
|
399
422
|
"invalid_request",
|
|
400
423
|
400,
|
|
401
|
-
"Qoder model discovery requires a validated outbound fetch",
|
|
424
|
+
"Qoder model discovery requires a validated outbound fetch (request {requestId}). No bridge to cross — configure a pool first 🏗️",
|
|
402
425
|
{ providerId, realm: input.realm },
|
|
403
426
|
);
|
|
404
427
|
}
|
|
@@ -409,6 +432,11 @@ export async function discoverQoderModels(input: {
|
|
|
409
432
|
const url = `${host}${QODER_MODELS_PATH}`;
|
|
410
433
|
try {
|
|
411
434
|
const headers = await input.headers(url, signedBody);
|
|
435
|
+
// GET, not POST: upstream refuses POST on the model-list surface (400
|
|
436
|
+
// "Request method 'POST' not supported"). The signature covers the body,
|
|
437
|
+
// so the body must accompany the GET. The gateway's validated egress
|
|
438
|
+
// dials GET+body through the Node http(s) client (the runtime fetch
|
|
439
|
+
// rejects GET bodies), which is exactly the shape the donor client sends.
|
|
412
440
|
const response = await fetcher(url, {
|
|
413
441
|
method: "GET",
|
|
414
442
|
headers: headers as unknown as HeadersInit,
|
|
@@ -430,9 +458,19 @@ export async function discoverQoderModels(input: {
|
|
|
430
458
|
continue;
|
|
431
459
|
}
|
|
432
460
|
const models = enabled.map((entry) => qoderCatalogModel(entry, providerId));
|
|
461
|
+
// Index the widest selectable window per key (from `context_config`,
|
|
462
|
+
// already computed into `maxInputTokens` by `mapQoderCatalogResponse`) so
|
|
463
|
+
// dispatch can send the maximum rather than the currently active one.
|
|
464
|
+
const maxCtx = new Map<string, number>();
|
|
465
|
+
for (const entry of enabled) {
|
|
466
|
+
if (entry.maxInputTokens !== undefined && entry.key.length > 0) {
|
|
467
|
+
maxCtx.set(entry.key, entry.maxInputTokens);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
433
470
|
// Cached only after a host actually answered: a failure must stay
|
|
434
471
|
// retryable on the next call rather than being frozen for five minutes.
|
|
435
472
|
discoveryCache.set(input.realm, { at: Date.now(), models });
|
|
473
|
+
discoveryMaxContextByKey.set(input.realm, maxCtx);
|
|
436
474
|
return models;
|
|
437
475
|
} catch (error) {
|
|
438
476
|
lastError = error;
|
|
@@ -441,9 +479,9 @@ export async function discoverQoderModels(input: {
|
|
|
441
479
|
throw new GatewayError(
|
|
442
480
|
"transport_unavailable",
|
|
443
481
|
502,
|
|
444
|
-
`Qoder model discovery failed for ${input.realm}: ${
|
|
482
|
+
`Qoder model discovery failed for ${input.realm} (request {requestId}): ${
|
|
445
483
|
lastError instanceof Error ? lastError.message : "unknown error"
|
|
446
|
-
}
|
|
484
|
+
}. The connection ghosted us — come back in a moment 👻`,
|
|
447
485
|
{ providerId, realm: input.realm },
|
|
448
486
|
"upstream",
|
|
449
487
|
);
|
|
@@ -17,6 +17,18 @@
|
|
|
17
17
|
* the console surface treat Qoder like any other daily-grant provider.
|
|
18
18
|
*/
|
|
19
19
|
import { QODER_REALMS, qoderRealmForProvider } from "./qoder-realm";
|
|
20
|
+
import { getQoderVersion } from "../../operations/client-versions";
|
|
21
|
+
import {
|
|
22
|
+
qoderMachineHeaders,
|
|
23
|
+
type QoderNativeIdentity,
|
|
24
|
+
} from "./qoder-umid";
|
|
25
|
+
import {
|
|
26
|
+
allocateQoderNativeIdentity,
|
|
27
|
+
ensureQoderNativeIdentity,
|
|
28
|
+
type QoderNativeIdentityStore,
|
|
29
|
+
} from "./qoder-native-identity-store";
|
|
30
|
+
import type { NativeIdentityRecord } from "../../../persistence/schema";
|
|
31
|
+
import { log } from "../../../observability/logger";
|
|
20
32
|
|
|
21
33
|
/** Structural fetch bound; Bun's own fetch type carries extra host props. */
|
|
22
34
|
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -37,6 +49,36 @@ const CHECKIN_CLAIM_PATH = "/sash/api/v1/me/daily-check-in/claim";
|
|
|
37
49
|
const CAMPAIGNS_PATH = "/sash/api/v1/me/campaigns";
|
|
38
50
|
const CAMPAIGN_CLAIM_PATH = "/sash/api/v1/me/campaigns/%s/claim";
|
|
39
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Headers the official desktop client sends to the `/sash` campaign surface.
|
|
54
|
+
* Without these — notably `cosy-clienttype`, `cosy-version`, and the `Qoder`
|
|
55
|
+
* user agent — the upstream hides the campaign list entirely
|
|
56
|
+
* (`showCampaign:false`), so the daily grant never appears. Verified live:
|
|
57
|
+
* the bare Authorization+Accept request returns an empty list, the desktop
|
|
58
|
+
* header set returns `showCampaign:true` with the campaigns populated.
|
|
59
|
+
*
|
|
60
|
+
* `native` optionally carries the UMID machine headers (see `qoder-umid`);
|
|
61
|
+
* when present they unlock the daily 100-credit campaign, which the server
|
|
62
|
+
* otherwise silently filters out.
|
|
63
|
+
*/
|
|
64
|
+
export function buildCampaignHeaders(
|
|
65
|
+
credential: string,
|
|
66
|
+
native?: Record<string, string>,
|
|
67
|
+
): Record<string, string> {
|
|
68
|
+
return {
|
|
69
|
+
Authorization: `Bearer ${credential}`,
|
|
70
|
+
Accept: "application/json",
|
|
71
|
+
"User-Agent": "Qoder",
|
|
72
|
+
"Cosy-ClientType": "10",
|
|
73
|
+
"Cosy-Version": getQoderVersion(),
|
|
74
|
+
...(native ?? {}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function campaignHeaders(credential: string): Record<string, string> {
|
|
79
|
+
return buildCampaignHeaders(credential);
|
|
80
|
+
}
|
|
81
|
+
|
|
40
82
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
41
83
|
if (typeof value !== "object" || value === null) return null;
|
|
42
84
|
return value as Record<string, unknown>;
|
|
@@ -80,19 +122,32 @@ function asState(state: string, today: string, lastClaimedAt: unknown): { active
|
|
|
80
122
|
return { active, todayCheckedIn: claimedToday };
|
|
81
123
|
}
|
|
82
124
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Normalizes one upstream campaign row to the snake_case shape the claim loop
|
|
127
|
+
* consumes. The API returns camelCase keys (`campaignId`, `actionType`,
|
|
128
|
+
* `claimStatus`); the fixtures and the donor hub use snake_case — accept both.
|
|
129
|
+
*/
|
|
130
|
+
function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
|
|
131
|
+
const benefit = asRecord(raw["benefit"]);
|
|
132
|
+
return {
|
|
133
|
+
campaign_id: String(raw["campaignId"] ?? raw["campaign_id"] ?? ""),
|
|
134
|
+
campaign_key: String(raw["campaignKey"] ?? raw["campaign_key"] ?? ""),
|
|
135
|
+
action_type: String(raw["actionType"] ?? raw["action_type"] ?? ""),
|
|
136
|
+
claim_status: String(raw["claimStatus"] ?? raw["claim_status"] ?? ""),
|
|
137
|
+
...(benefit !== null ? { benefit } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** GET campaign list once with given headers; `null` when unreachable/unauthorized. */
|
|
142
|
+
async function getCampaigns(
|
|
85
143
|
base: string,
|
|
86
|
-
|
|
144
|
+
headers: Record<string, string>,
|
|
87
145
|
fetcher: Fetcher,
|
|
88
146
|
): Promise<Record<string, unknown>[] | null> {
|
|
89
147
|
try {
|
|
90
148
|
const response = await fetcher(`${base}${CAMPAIGNS_PATH}`, {
|
|
91
149
|
method: "GET",
|
|
92
|
-
headers
|
|
93
|
-
Authorization: `Bearer ${credential}`,
|
|
94
|
-
Accept: "application/json",
|
|
95
|
-
},
|
|
150
|
+
headers,
|
|
96
151
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
97
152
|
});
|
|
98
153
|
if (response.status === 401 || response.status === 403) return null;
|
|
@@ -100,12 +155,112 @@ async function fetchCampaigns(
|
|
|
100
155
|
const payload: unknown = await response.json().catch(() => null);
|
|
101
156
|
const rec = asRecord(payload);
|
|
102
157
|
const list = Array.isArray(rec?.["campaigns"]) ? rec!["campaigns"] : Array.isArray(payload) ? payload : null;
|
|
103
|
-
|
|
158
|
+
if (!Array.isArray(list)) return null;
|
|
159
|
+
return (list as Record<string, unknown>[]).map(normalizeCampaign);
|
|
104
160
|
} catch {
|
|
105
161
|
return null;
|
|
106
162
|
}
|
|
107
163
|
}
|
|
108
164
|
|
|
165
|
+
/**
|
|
166
|
+
* One attempted claim across a header set, returning the same result shape as
|
|
167
|
+
* `claimCampaign` (already-claimed grants resolve to `already_claimed`).
|
|
168
|
+
*/
|
|
169
|
+
async function tryClaimDaily(
|
|
170
|
+
base: string,
|
|
171
|
+
headers: Record<string, string>,
|
|
172
|
+
fetcher: Fetcher,
|
|
173
|
+
): Promise<{ state: DailyCheckinState; amount: number; error: string | null }> {
|
|
174
|
+
const campaigns = await getCampaigns(base, headers, fetcher);
|
|
175
|
+
if (campaigns === null) {
|
|
176
|
+
return { state: "error", amount: 0, error: "Campaign list unavailable." };
|
|
177
|
+
}
|
|
178
|
+
// Claim a `CLAIMABLE` CREDITS-kind campaign. Promo rows (`VIEW_DETAILS`,
|
|
179
|
+
// no benefit) are not credit grants and must not be claimed — claiming one
|
|
180
|
+
// spends the account's daily slot for nothing and makes the sweep report a
|
|
181
|
+
// misleading `claimed` with 0 credits.
|
|
182
|
+
const claimable = campaigns.filter((c) => {
|
|
183
|
+
if (String(c["claim_status"] ?? "").toUpperCase() !== "CLAIMABLE") return false;
|
|
184
|
+
const kind = String(asRecord(c["benefit"])?.["kind"] ?? "").toUpperCase();
|
|
185
|
+
const action = String(c["action_type"] ?? "").toUpperCase();
|
|
186
|
+
return kind === "CREDITS" || action === "CLAIM_BENEFIT";
|
|
187
|
+
});
|
|
188
|
+
for (const campaign of claimable) {
|
|
189
|
+
const benefit = asRecord(campaign["benefit"]);
|
|
190
|
+
const campaignId = String(campaign["campaign_id"] ?? "");
|
|
191
|
+
if (!campaignId) continue;
|
|
192
|
+
const claim = await claimWithHeaders(base, campaignId, headers, fetcher);
|
|
193
|
+
if (claim.state === "claimed") {
|
|
194
|
+
const amount = asNumber(benefit?.["amount"]) ?? claim.amount;
|
|
195
|
+
return { state: "claimed", amount, error: null };
|
|
196
|
+
}
|
|
197
|
+
if (claim.state === "already_claimed") return { state: "already_claimed", amount: 0, error: null };
|
|
198
|
+
if (claim.state === "not_eligible") return { state: "not_eligible", amount: 0, error: null };
|
|
199
|
+
return { state: "error", amount: 0, error: "Campaign claim failed." };
|
|
200
|
+
}
|
|
201
|
+
// Nothing claimable. Only a CREDITS-kind campaign that is `CLAIMED` proves
|
|
202
|
+
// today's credit was already taken; a claimed promo (`VIEW_DETAILS`, no
|
|
203
|
+
// benefit) says nothing about the daily grant, so it must not be reported
|
|
204
|
+
// as already_claimed. When the CREDITS campaign is absent entirely, the
|
|
205
|
+
// daily grant is simply not present (or hidden) for this account.
|
|
206
|
+
const anyCreditsClaimed = campaigns.some((c) => {
|
|
207
|
+
if (String(c["claim_status"] ?? "").toUpperCase() !== "CLAIMED") return false;
|
|
208
|
+
const kind = String(asRecord(c["benefit"])?.["kind"] ?? "").toUpperCase();
|
|
209
|
+
const action = String(c["action_type"] ?? "").toUpperCase();
|
|
210
|
+
return kind === "CREDITS" || action === "CLAIM_BENEFIT";
|
|
211
|
+
});
|
|
212
|
+
if (anyCreditsClaimed) return { state: "already_claimed", amount: 0, error: null };
|
|
213
|
+
return { state: "unavailable", amount: 0, error: "No daily CREDITS campaign claimable." };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Probes the daily CREDITS grant under a native device identity.
|
|
218
|
+
*
|
|
219
|
+
* The server binds the grant to the device identity and hides it once a
|
|
220
|
+
* device claimed today, so an account claims under its own persisted device
|
|
221
|
+
* (minted seed). A bound identity that resolves to `claimed`/`already_claimed`
|
|
222
|
+
* is terminal; one that resolves to `unavailable`/`not_eligible` is honest
|
|
223
|
+
* (the device is spent for the day or the person is blocked) and is also
|
|
224
|
+
* terminal — the account must not move to a different device, because that is
|
|
225
|
+
* how devices are re-spun. Only an *error* (stored identity stopped working)
|
|
226
|
+
* falls back to a freshly minted identity.
|
|
227
|
+
*
|
|
228
|
+
* Returns `{ result, identity }` where `identity` is the device that produced
|
|
229
|
+
* a terminal `claimed`/`already_claimed` outcome (or `null`).
|
|
230
|
+
*/
|
|
231
|
+
async function probeDailyClaim(
|
|
232
|
+
base: string,
|
|
233
|
+
credential: string,
|
|
234
|
+
fetcher: Fetcher,
|
|
235
|
+
options: {
|
|
236
|
+
readonly bound?: QoderNativeIdentity | null;
|
|
237
|
+
readonly alt?: QoderNativeIdentity | null;
|
|
238
|
+
} = {},
|
|
239
|
+
): Promise<{ result: { state: DailyCheckinState; amount: number; error: string | null }; identity: QoderNativeIdentity | null }> {
|
|
240
|
+
const basic = await tryClaimDaily(base, campaignHeaders(credential), fetcher);
|
|
241
|
+
if (basic.state === "claimed" || basic.state === "already_claimed") {
|
|
242
|
+
// `tryClaimDaily` only claims CREDITS-kind campaigns, so a `claimed` or
|
|
243
|
+
// `already_claimed` basic result is already about the daily credit grant —
|
|
244
|
+
// terminal, no identity probe needed.
|
|
245
|
+
return { result: basic, identity: null };
|
|
246
|
+
}
|
|
247
|
+
if (basic.state === "unavailable") {
|
|
248
|
+
const identities = [options.bound, options.alt].filter((i): i is QoderNativeIdentity => i !== null && i !== undefined);
|
|
249
|
+
for (const identity of identities) {
|
|
250
|
+
const headers = { ...campaignHeaders(credential), ...qoderMachineHeaders(identity) };
|
|
251
|
+
const result = await tryClaimDaily(base, headers, fetcher);
|
|
252
|
+
if (result.state === "claimed" || result.state === "already_claimed") {
|
|
253
|
+
return { result, identity };
|
|
254
|
+
}
|
|
255
|
+
// Only an error falls through to the next identity (e.g. the bound one
|
|
256
|
+
// stopped working). Any honest outcome (unavailable/not_eligible) is
|
|
257
|
+
// terminal.
|
|
258
|
+
if (result.state !== "error") return { result, identity };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { result: basic, identity: null };
|
|
262
|
+
}
|
|
263
|
+
|
|
109
264
|
/**
|
|
110
265
|
* Claims one campaign and returns the resulting state. Mirrors the donor's
|
|
111
266
|
* `claim_campaign` idempotence handling: a 409 or `ALREADY_CLAIMED` body is
|
|
@@ -113,10 +268,10 @@ async function fetchCampaigns(
|
|
|
113
268
|
* same-person block (not a failure of this account's day), and anything else
|
|
114
269
|
* is an error.
|
|
115
270
|
*/
|
|
116
|
-
async function
|
|
271
|
+
async function claimWithHeaders(
|
|
117
272
|
base: string,
|
|
118
273
|
campaignId: string,
|
|
119
|
-
|
|
274
|
+
headers: Record<string, string>,
|
|
120
275
|
fetcher: Fetcher,
|
|
121
276
|
): Promise<{ state: DailyCheckinState; amount: number }> {
|
|
122
277
|
const url = base + CAMPAIGN_CLAIM_PATH.replace("%s", encodeURIComponent(campaignId));
|
|
@@ -124,11 +279,7 @@ async function claimCampaign(
|
|
|
124
279
|
try {
|
|
125
280
|
response = await fetcher(url, {
|
|
126
281
|
method: "POST",
|
|
127
|
-
headers: {
|
|
128
|
-
Authorization: `Bearer ${credential}`,
|
|
129
|
-
"Content-Type": "application/json",
|
|
130
|
-
Accept: "application/json",
|
|
131
|
-
},
|
|
282
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
132
283
|
body: "{}",
|
|
133
284
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
134
285
|
});
|
|
@@ -233,6 +384,16 @@ export async function fetchQoderDailyCheckin(args: {
|
|
|
233
384
|
readonly credential: string;
|
|
234
385
|
readonly fetcher: Fetcher;
|
|
235
386
|
readonly now?: () => Date;
|
|
387
|
+
/**
|
|
388
|
+
* The account's persisted native device identity. When absent and
|
|
389
|
+
* `nativeIdentityStore` is provided, the first check-in mints one and
|
|
390
|
+
* persists it (see `allocateQoderNativeIdentity`). When the store is absent
|
|
391
|
+
* too (tests), `identities` supplies the identities to probe.
|
|
392
|
+
*/
|
|
393
|
+
readonly nativeIdentity?: NativeIdentityRecord | null;
|
|
394
|
+
readonly nativeIdentityStore?: QoderNativeIdentityStore;
|
|
395
|
+
/** Test-only: override the identity list (defaults to bridge-minted). */
|
|
396
|
+
readonly identities?: readonly QoderNativeIdentity[];
|
|
236
397
|
}): Promise<DailyCheckinResult> {
|
|
237
398
|
const { providerId, accountId, credential, fetcher } = args;
|
|
238
399
|
const baseResult = baseOutcome(providerId, accountId);
|
|
@@ -258,46 +419,76 @@ export async function fetchQoderDailyCheckin(args: {
|
|
|
258
419
|
};
|
|
259
420
|
}
|
|
260
421
|
|
|
261
|
-
// Legacy surface absent: campaign fallback.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
422
|
+
// Legacy surface absent: campaign fallback. Prefer the account's persisted
|
|
423
|
+
// device (loaded from the store); on first check-in with no stored device,
|
|
424
|
+
// mint one before probing so the account claims under the same identity
|
|
425
|
+
// every day.
|
|
426
|
+
const store = args.nativeIdentityStore;
|
|
427
|
+
let bound: QoderNativeIdentity | null = null;
|
|
428
|
+
let boundRecord: NativeIdentityRecord | null = args.nativeIdentity ?? null;
|
|
429
|
+
if (store) {
|
|
430
|
+
boundRecord = (await store.get(providerId, accountId)) ?? boundRecord;
|
|
265
431
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
alreadySeen = true;
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
280
|
-
if (claimStatus !== "CLAIMABLE") continue;
|
|
281
|
-
const campaignId = String(rec["campaign_id"] ?? "");
|
|
282
|
-
if (!campaignId) continue;
|
|
283
|
-
const result = await claimCampaign(base, campaignId, credential, fetcher);
|
|
284
|
-
if (result.state === "claimed") {
|
|
285
|
-
const amount = asNumber(benefit?.["amount"]) ?? result.amount;
|
|
286
|
-
return { ...baseResult, state: "claimed", credit: amount, streakDays: null };
|
|
432
|
+
if (boundRecord) {
|
|
433
|
+
// Ensure the stored device is active on this host (replay its seed).
|
|
434
|
+
const active = store ? await ensureQoderNativeIdentity(store, providerId, accountId) : true;
|
|
435
|
+
if (active) {
|
|
436
|
+
bound = {
|
|
437
|
+
machineToken: boundRecord.machineToken,
|
|
438
|
+
machineType: boundRecord.machineType,
|
|
439
|
+
machineCode: boundRecord.machineCode,
|
|
440
|
+
isVm: false,
|
|
441
|
+
source: "runtime-info",
|
|
442
|
+
};
|
|
287
443
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
444
|
+
}
|
|
445
|
+
if (!bound && store) {
|
|
446
|
+
const minted = await allocateQoderNativeIdentity(store, providerId, accountId);
|
|
447
|
+
if (minted) {
|
|
448
|
+
boundRecord = minted;
|
|
449
|
+
bound = {
|
|
450
|
+
machineToken: minted.machineToken,
|
|
451
|
+
machineType: minted.machineType,
|
|
452
|
+
machineCode: minted.machineCode,
|
|
453
|
+
isVm: false,
|
|
454
|
+
source: "runtime-info",
|
|
455
|
+
};
|
|
291
456
|
}
|
|
292
|
-
|
|
293
|
-
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const alt = args.identities?.[0] ?? null;
|
|
460
|
+
const probe = await probeDailyClaim(base, credential, fetcher, {
|
|
461
|
+
bound,
|
|
462
|
+
alt,
|
|
463
|
+
});
|
|
464
|
+
const probed = probe.result;
|
|
465
|
+
|
|
466
|
+
// If a non-bound identity won (the bound one errored), persist it as the
|
|
467
|
+
// account's device — but only when its token is actually free, so the
|
|
468
|
+
// one-device-per-account invariant holds.
|
|
469
|
+
if (store && probe.identity !== null && probe.identity.machineToken !== bound?.machineToken) {
|
|
470
|
+
const occupied = await store.occupied(accountId);
|
|
471
|
+
if (!occupied.has(probe.identity.machineToken)) {
|
|
472
|
+
try {
|
|
473
|
+
await store.bind(providerId, accountId, {
|
|
474
|
+
machineToken: probe.identity.machineToken,
|
|
475
|
+
machineType: probe.identity.machineType,
|
|
476
|
+
machineCode: probe.identity.machineCode,
|
|
477
|
+
});
|
|
478
|
+
} catch (error) {
|
|
479
|
+
log.warn(`[qoder] could not bind native identity for account=${accountId}`, error as Error);
|
|
480
|
+
}
|
|
294
481
|
}
|
|
295
|
-
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (probed.state === "claimed") {
|
|
485
|
+
return { ...baseResult, state: "claimed", credit: probed.amount, streakDays: null };
|
|
486
|
+
}
|
|
487
|
+
if (probed.state === "already_claimed") {
|
|
488
|
+
return { ...baseResult, state: "already_claimed", streakDays: null };
|
|
489
|
+
}
|
|
490
|
+
if (probed.state === "not_eligible") {
|
|
296
491
|
return { ...baseResult, state: "not_eligible", error: "Same-person claim already used today." };
|
|
297
492
|
}
|
|
298
|
-
return {
|
|
299
|
-
...baseResult,
|
|
300
|
-
state: alreadySeen ? "already_claimed" : "unavailable",
|
|
301
|
-
error: alreadySeen ? null : "No daily CREDITS campaign claimable.",
|
|
302
|
-
};
|
|
493
|
+
return { ...baseResult, state: probed.state, error: probed.error };
|
|
303
494
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for the Qoder native machine identity (UMID) bound to an
|
|
3
|
+
* account.
|
|
4
|
+
*
|
|
5
|
+
* Same-account probes found that daily 100-credit grant visibility does not
|
|
6
|
+
* depend on machine headers. This store preserves native identities and seeds
|
|
7
|
+
* for deterministic replay and other surfaces that may use device identity.
|
|
8
|
+
* A native identity is a deterministic function of a seed file
|
|
9
|
+
* (`~/.config/.locale_cfg`). The bound record lives in
|
|
10
|
+
* `provider_accounts.native_identity` (plain JSON, not a secret).
|
|
11
|
+
*
|
|
12
|
+
* Uniqueness of the device is enforced by the partial unique index on the
|
|
13
|
+
* extracted `machineToken`; this store keeps the JS side of that internal
|
|
14
|
+
* identity-allocation invariant and surfaces the DB constraint as the backstop.
|
|
15
|
+
*/
|
|
16
|
+
import { and, eq, isNotNull, ne } from "drizzle-orm";
|
|
17
|
+
import type { CartethyiaDatabase } from "../../../persistence/db-handle";
|
|
18
|
+
import { providerAccounts, type NativeIdentityRecord } from "../../../persistence/schema";
|
|
19
|
+
import {
|
|
20
|
+
mintQoderNativeIdentity,
|
|
21
|
+
replayQoderNativeIdentity,
|
|
22
|
+
type QoderNativeIdentity,
|
|
23
|
+
} from "./qoder-umid";
|
|
24
|
+
|
|
25
|
+
/** Stateful read/write boundary for bound native identities. */
|
|
26
|
+
export interface QoderNativeIdentityStore {
|
|
27
|
+
/**
|
|
28
|
+
* The identity bound to one account, or `undefined` when the account has
|
|
29
|
+
* no identity yet.
|
|
30
|
+
*/
|
|
31
|
+
get(providerId: string, accountId: string): Promise<NativeIdentityRecord | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Machine tokens already bound to **other** accounts, so a caller can mint
|
|
34
|
+
* a fresh identity without colliding. A bound token represents a device identity, and
|
|
35
|
+
* the unique index that backs this spans all `provider_accounts` rows. The
|
|
36
|
+
* account's own bound token is not "occupied" — reusing one's own device is
|
|
37
|
+
* the point.
|
|
38
|
+
*/
|
|
39
|
+
occupied(accountId: string): Promise<ReadonlySet<string>>;
|
|
40
|
+
/** Binds an identity to an account (upsert). Throws on a device collision. */
|
|
41
|
+
bind(providerId: string, accountId: string, record: NativeIdentityRecord): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class DrizzleQoderNativeIdentityStore implements QoderNativeIdentityStore {
|
|
45
|
+
constructor(private readonly db: CartethyiaDatabase) {}
|
|
46
|
+
|
|
47
|
+
async get(providerId: string, accountId: string): Promise<NativeIdentityRecord | undefined> {
|
|
48
|
+
const rows = await this.db
|
|
49
|
+
.select({ nativeIdentity: providerAccounts.nativeIdentity })
|
|
50
|
+
.from(providerAccounts)
|
|
51
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.providerId, providerId)))
|
|
52
|
+
.limit(1);
|
|
53
|
+
const row = rows[0];
|
|
54
|
+
return row?.nativeIdentity ?? undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async occupied(accountId: string): Promise<ReadonlySet<string>> {
|
|
58
|
+
const rows = await this.db
|
|
59
|
+
.select({ nativeIdentity: providerAccounts.nativeIdentity })
|
|
60
|
+
.from(providerAccounts)
|
|
61
|
+
.where(
|
|
62
|
+
accountId
|
|
63
|
+
? and(isNotNull(providerAccounts.nativeIdentity), ne(providerAccounts.id, accountId))
|
|
64
|
+
: isNotNull(providerAccounts.nativeIdentity),
|
|
65
|
+
);
|
|
66
|
+
const occupied = new Set<string>();
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const token = row.nativeIdentity?.machineToken;
|
|
69
|
+
if (token !== undefined && token !== "") occupied.add(token);
|
|
70
|
+
}
|
|
71
|
+
return occupied;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async bind(providerId: string, accountId: string, record: NativeIdentityRecord): Promise<void> {
|
|
75
|
+
await this.db
|
|
76
|
+
.update(providerAccounts)
|
|
77
|
+
.set({ nativeIdentity: record })
|
|
78
|
+
.where(and(eq(providerAccounts.id, accountId), eq(providerAccounts.providerId, providerId)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Serializes identity minting across accounts in this process, so two
|
|
84
|
+
* concurrent allocations cannot interleave seed swaps.
|
|
85
|
+
*/
|
|
86
|
+
let allocationQueue: Promise<unknown> = Promise.resolve();
|
|
87
|
+
function withAllocationLock<T>(work: () => Promise<T>): Promise<T> {
|
|
88
|
+
const next = allocationQueue.then(work, work);
|
|
89
|
+
allocationQueue = next.catch(() => undefined);
|
|
90
|
+
return next;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Identity minting injected for tests; defaults to the real bridge. */
|
|
94
|
+
export type NativeIdentityMinter = (accountId: string) => { identity: QoderNativeIdentity; seed: string } | null;
|
|
95
|
+
export type NativeIdentityReplayer = (seed: string, accountId: string) => QoderNativeIdentity | null;
|
|
96
|
+
|
|
97
|
+
const defaultMinter: NativeIdentityMinter = (accountId) => mintQoderNativeIdentity(accountId);
|
|
98
|
+
const defaultReplayer: NativeIdentityReplayer = (seed, accountId) => replayQoderNativeIdentity(seed, accountId);
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Mints and binds a fresh device identity for an account: runs the bridge
|
|
102
|
+
* with a cleared seed, captures the new seed, and persists both. Returns the
|
|
103
|
+
* bound record, or `null` when the bridge fails.
|
|
104
|
+
*
|
|
105
|
+
* `avoidToken` optionally excludes the account's own current token so a
|
|
106
|
+
* regenerate moves to a *different* device.
|
|
107
|
+
*/
|
|
108
|
+
export function allocateQoderNativeIdentity(
|
|
109
|
+
store: QoderNativeIdentityStore,
|
|
110
|
+
providerId: string,
|
|
111
|
+
accountId: string,
|
|
112
|
+
options: {
|
|
113
|
+
readonly avoidToken?: string;
|
|
114
|
+
readonly mint?: NativeIdentityMinter;
|
|
115
|
+
readonly replayer?: NativeIdentityReplayer;
|
|
116
|
+
} = {},
|
|
117
|
+
): Promise<NativeIdentityRecord | null> {
|
|
118
|
+
const mint = options.mint ?? defaultMinter;
|
|
119
|
+
return withAllocationLock(async () => {
|
|
120
|
+
const occupied = await store.occupied(accountId);
|
|
121
|
+
for (let i = 0; i < 6; i += 1) {
|
|
122
|
+
const minted = mint(accountId);
|
|
123
|
+
if (!minted) return null;
|
|
124
|
+
const token = minted.identity.machineToken;
|
|
125
|
+
if (occupied.has(token)) continue; // collision (rare) → mint again
|
|
126
|
+
if (token === options.avoidToken) continue;
|
|
127
|
+
const record: NativeIdentityRecord = {
|
|
128
|
+
machineToken: token,
|
|
129
|
+
machineType: minted.identity.machineType,
|
|
130
|
+
machineCode: minted.identity.machineCode,
|
|
131
|
+
...(minted.seed ? { seed: minted.seed } : {}),
|
|
132
|
+
source: minted.identity.source,
|
|
133
|
+
};
|
|
134
|
+
await store.bind(providerId, accountId, record);
|
|
135
|
+
return record;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ensures the bound identity is currently active on this host: replays the
|
|
143
|
+
* stored seed so the bridge yields the same token. Returns `false` when the
|
|
144
|
+
* account has no bound identity or replay failed.
|
|
145
|
+
*/
|
|
146
|
+
export async function ensureQoderNativeIdentity(
|
|
147
|
+
store: QoderNativeIdentityStore,
|
|
148
|
+
providerId: string,
|
|
149
|
+
accountId: string,
|
|
150
|
+
replayer: NativeIdentityReplayer = defaultReplayer,
|
|
151
|
+
): Promise<boolean> {
|
|
152
|
+
const record = await store.get(providerId, accountId);
|
|
153
|
+
if (!record) return false;
|
|
154
|
+
if (!record.seed) return true; // no seed captured → assume already active
|
|
155
|
+
const identity = replayer(record.seed, accountId);
|
|
156
|
+
return identity !== null && identity.machineToken === record.machineToken;
|
|
157
|
+
}
|