cartethyia 2.7.4 → 2.7.6
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 +5 -0
- package/README.md +18 -6
- package/dist/dashboard/assets/BannedPage-B1N603cM.js +1 -0
- package/dist/dashboard/assets/BannedPage-B1N603cM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js +3 -0
- package/dist/dashboard/assets/CliToolDetailPage-9q4_4NoK.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-DwshGtK-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js +1 -0
- package/dist/dashboard/assets/CombosPage-D762MIkp.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-KRYjHZsX.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-DXsxk5uM.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-BEjAjD4F.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js +899 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-d5v3K_Yc.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js +1 -0
- package/dist/dashboard/assets/LoginPage-Ccv4tjNS.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-C6WlpU5E.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DN9fYNWA.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-BY0ieUgz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-DNsbfea2.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-DxjvUzSD.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-DxjvUzSD.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js +1 -0
- package/dist/dashboard/assets/QuotaPage-OaHScjpX.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BW4AKj2H.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js +1 -0
- package/dist/dashboard/assets/SetupPage-44BZrxFC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-DRPavCdU.js} +4 -4
- package/dist/dashboard/assets/StudioPage-DRPavCdU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
- package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js +56 -0
- package/dist/dashboard/assets/UsagePage-B7M7LvMM.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
- package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
- package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
- package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
- package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js +1 -0
- package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-BaX2GTrn.js} +1 -1
- package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DympBNq4.js → banner-DFH4Bkte.js} +1 -1
- package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
- package/dist/dashboard/assets/brand/login_register.jpg +0 -0
- package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
- package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
- package/dist/dashboard/assets/brand/og_image.jpg +0 -0
- package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CxDw0lK6.js} +1 -1
- package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
- package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
- package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +1 -0
- package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-DfS1ntiq.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
- package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-QqPul6sC.js} +1 -1
- package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
- package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js +4 -0
- package/dist/dashboard/assets/entry-D6WvojeD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
- package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-u7JPB5BX.js} +1 -1
- package/dist/dashboard/assets/formatters-u7JPB5BX.js.gz +0 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
- package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
- package/dist/dashboard/assets/{index-DBNPOfhV.js → index-Ko4dLRnV.js} +2 -2
- package/dist/dashboard/assets/index-Ko4dLRnV.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-CHJQwxLa.js} +1 -1
- package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
- package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BlmlizVV.js} +1 -1
- package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
- package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-Bz93I-yE.js} +1 -1
- package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Ml16KCfu.js} +1 -1
- package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js +1 -0
- package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
- package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js +1 -0
- package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
- package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js +1 -0
- package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai.webp +0 -0
- package/dist/dashboard/assets/providers/zcode.webp +0 -0
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
- package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
- package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js +1 -0
- package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
- package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BHqpd-bg.js} +2 -2
- package/dist/dashboard/assets/select-BHqpd-bg.js.gz +0 -0
- package/dist/dashboard/assets/settings-BxslsErn.js +1 -0
- package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
- package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
- package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-j7cXjgYX.js} +1 -1
- package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
- package/dist/dashboard/assets/{state-CkfdYWel.js → state-DHmEnLa2.js} +1 -1
- package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
- package/dist/dashboard/assets/{switch-SYm237HG.js → switch-DmZcyTIC.js} +1 -1
- package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-CbCP4w1o.js} +1 -1
- package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
- package/dist/dashboard/console-backgrounds.jpg +0 -0
- package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +60 -59
- package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
- package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
- package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
- package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
- package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
- package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
- package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
- package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
- package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
- package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
- package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
- package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
- package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
- package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
- package/package.json +4 -6
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +58 -2
- package/src/boot/dependencies.ts +794 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +1 -1
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +19 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +33 -6
- package/src/config/usage.ts +27 -0
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +71 -62
- package/src/console/api-keys/routes.ts +56 -24
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +1 -1
- package/src/console/auth/audit-recorder.ts +1 -1
- package/src/console/auth/lockout.ts +7 -3
- package/src/console/auth/row-coercion.ts +57 -12
- package/src/console/auth/session-resolver.ts +1 -1
- package/src/console/auth/session-service.ts +8 -2
- package/src/console/auth/session.ts +37 -31
- package/src/console/auth/setup-service.ts +4 -3
- package/src/console/auth/user-store.ts +1 -1
- package/src/console/backup/9router-map.ts +0 -3
- package/src/console/backup/contracts.ts +9 -9
- package/src/console/backup/export-endpoint.ts +5 -6
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +29 -33
- package/src/console/backup/service.ts +468 -468
- package/src/console/backup/store.ts +47 -29
- package/src/console/backup/validate.ts +11 -1
- package/src/console/cli-tools/contracts.ts +43 -299
- package/src/console/cli-tools/fs-ops.ts +29 -0
- package/src/console/cli-tools/injectors/claude.ts +3 -3
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/driver.ts +150 -247
- package/src/console/cli-tools/injectors/kilo.ts +146 -21
- package/src/console/cli-tools/injectors/opencode.ts +176 -153
- package/src/console/cli-tools/routes.ts +7 -1
- package/src/console/cli-tools/service.ts +8 -7
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router.ts +32 -0
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/domain-registration.ts +16 -2
- package/src/console/inferhub/client.ts +0 -7
- package/src/console/inferhub/key-store.ts +1 -1
- package/src/console/inferhub/normalize.ts +2 -2
- package/src/console/inferhub/routes.ts +1 -1
- package/src/console/inferhub/types.ts +1 -1
- package/src/console/observability/contracts.ts +7 -1
- package/src/console/observability/live.ts +31 -18
- package/src/console/observability/logs.ts +1 -1
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +23 -4
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +39 -0
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +95 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +27 -3
- package/src/console/providers/catalog/store.ts +147 -8
- package/src/console/providers/detail/routes.ts +1 -1
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +92 -13
- package/src/console/quota/account-quota-global.ts +41 -3
- package/src/console/quota/account-quota-shared.ts +10 -3
- package/src/console/quota/account-quota-tenant.ts +259 -66
- package/src/console/quota/daily-growth-job.ts +308 -0
- package/src/console/quota/native-identity.ts +2 -2
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/routes.ts +1 -1
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/routing/model/contracts.ts +1 -1
- package/src/console/routing/pools/contracts.ts +2 -2
- package/src/console/routing/pools/import-parse.ts +3 -3
- package/src/console/routing/pools/relay-deploy.ts +3 -3
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +100 -32
- package/src/console/share/share-stats.ts +11 -13
- package/src/console/share/share-usage.ts +1 -1
- package/src/console/shared/errors.ts +2 -2
- package/src/console/shared/object.ts +18 -0
- package/src/console/shared/read-through-cache.ts +4 -4
- package/src/console/shared/transaction.ts +14 -0
- package/src/console/system-prompt/contracts.ts +89 -12
- package/src/console/system-prompt/routes.ts +98 -45
- package/src/console/updates/apply-update.ts +244 -0
- package/src/console/updates/routes.ts +252 -97
- package/src/main.ts +11 -5
- package/src/network/http2-bidi.ts +1 -1
- package/src/network/ip-utils.ts +298 -0
- package/src/network/outbound-fetch.ts +1 -1
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +16 -12
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-health-machine.ts +1 -1
- package/src/network/rate-limit-verdict.ts +1 -1
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +20 -71
- package/src/observability/event-log.ts +9 -2
- package/src/observability/event-query.ts +61 -291
- package/src/observability/log-sanitize.ts +1 -1
- package/src/observability/metrics.ts +4 -4
- package/src/observability/payload-capture.ts +6 -5
- package/src/observability/payload-store.ts +1 -1
- package/src/observability/runtime-metrics.ts +1 -1
- package/src/observability/telemetry-buffer.ts +19 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/token-speed.ts +1 -1
- package/src/persistence/connection-url.ts +1 -1
- package/src/persistence/db-handle.ts +1 -1
- package/src/persistence/db-pg.ts +2 -2
- package/src/persistence/db-sqlite.ts +3 -3
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +121 -40
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +29 -2
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/system-prompt-store.ts +63 -18
- package/src/persistence/tenant-preferences.ts +1 -1
- package/src/persistence/ttl-cache.ts +3 -3
- package/src/persistence/usage-flush.ts +7 -2
- package/src/protocol/canonical-model.ts +1 -1
- package/src/protocol/codecs.ts +6 -10
- package/src/protocol/dsml.ts +8 -125
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/reasoning-text.ts +1 -1
- package/src/protocol/reasoning.ts +7 -52
- package/src/protocol/registry.ts +50 -15
- package/src/protocol/request/chat.ts +46 -19
- package/src/protocol/request/codex.ts +1 -1
- package/src/protocol/request/messages.ts +46 -14
- package/src/protocol/request/responses.ts +19 -10
- package/src/protocol/response/chat.ts +25 -10
- package/src/protocol/response/codex.ts +1 -1
- package/src/protocol/response/messages.ts +1 -1
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +1 -1
- package/src/protocol/sse-decoder.ts +50 -1
- package/src/protocol/stop-reason.ts +2 -2
- package/src/protocol/think-split.ts +347 -0
- package/src/protocol/translation/attachment-integrity.ts +3 -3
- package/src/protocol/translation/cache-controls.ts +8 -20
- package/src/protocol/translation/capabilities.ts +6 -6
- package/src/protocol/translation/dialect/deepseek.ts +10 -0
- package/src/protocol/translation/dialect/inband.ts +39 -0
- package/src/protocol/translation/dialect/model-thinking.ts +89 -0
- package/src/protocol/translation/dialect/qwen.ts +114 -0
- package/src/protocol/translation/dialect/resolver.ts +83 -0
- package/src/protocol/translation/dialect/rules.ts +93 -0
- package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
- package/src/protocol/translation/dialect/types.ts +58 -0
- package/src/protocol/translation/quirks.ts +18 -4
- package/src/protocol/translation/thinking.ts +12 -252
- package/src/protocol/translation/tool-id.ts +3 -3
- package/src/protocol/translation/tool-repair.ts +1 -1
- package/src/protocol/upstream/post-json.ts +6 -1
- package/src/providers/adapters/compatible-adapter.ts +71 -4
- package/src/providers/authentication/jwt-validator.ts +0 -5
- package/src/providers/authentication/oauth-client.ts +254 -253
- package/src/providers/authentication/oauth-flow-store.ts +42 -7
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/default-registry.ts +35 -37
- package/src/providers/discovery/catalog/catalog-format.ts +13 -2
- package/src/providers/discovery/discovery-types.ts +0 -1
- package/src/providers/discovery/openai-model-discovery.ts +2 -2
- package/src/providers/discovery/probe-phases.ts +48 -13
- package/src/providers/discovery/probing-service.ts +6 -8
- package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +88 -28
- package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
- package/src/providers/integrations/antigravity/antigravity.ts +22 -13
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
- package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
- package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- package/src/providers/integrations/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +1 -1
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/commandcode/commandcode.ts +75 -115
- package/src/providers/integrations/commandcode/quota.ts +131 -51
- package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
- package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
- package/src/providers/integrations/cursor/cursor-request.ts +0 -87
- package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
- package/src/providers/integrations/cursor/cursor.ts +35 -3
- package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
- package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
- package/src/providers/integrations/factory/factory.ts +9 -9
- package/src/providers/integrations/github/github-discovery.ts +70 -12
- package/src/providers/integrations/github/github-oauth.ts +184 -14
- package/src/providers/integrations/github/github.ts +134 -26
- package/src/providers/integrations/grok/grok-oauth.ts +261 -261
- package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
- package/src/providers/integrations/grok/grok-quota.ts +16 -7
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
- package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
- package/src/providers/integrations/grok/grok.ts +86 -36
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
- package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
- package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
- package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
- package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
- package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
- package/src/providers/integrations/kiro/kiro-request.ts +8 -11
- package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
- package/src/providers/integrations/kiro/kiro.ts +80 -68
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/qoder/qoder-catalog.ts +94 -11
- package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
- package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
- package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
- package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
- package/src/providers/integrations/qoder/qoder.ts +422 -71
- package/src/providers/integrations/shared/connect.ts +0 -54
- package/src/providers/integrations/shared/hash.ts +15 -0
- package/src/providers/integrations/shared/session-cache.ts +136 -0
- package/src/providers/integrations/trae/trae-cache.ts +7 -87
- package/src/providers/integrations/trae/trae-catalog.ts +1 -6
- package/src/providers/integrations/trae/trae-protocol.ts +17 -8
- package/src/providers/integrations/trae/trae-realm.ts +0 -7
- package/src/providers/integrations/trae/trae.ts +51 -8
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
- package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
- package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
- package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
- package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
- package/src/providers/operations/account-error-classification.ts +198 -133
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- package/src/providers/operations/byok-wire-profile.ts +19 -15
- package/src/providers/operations/cli-platform.ts +31 -5
- package/src/providers/operations/client-version-resolver.ts +4 -6
- package/src/providers/operations/client-versions.ts +10 -7
- package/src/providers/operations/provider-catalog-seeder.ts +2 -0
- package/src/providers/operations/provider-catalog-service.ts +18 -2
- package/src/providers/operations/provider-credential-service.ts +19 -2
- package/src/providers/provider-contracts.ts +18 -7
- package/src/providers/provider-metadata.ts +17 -2
- package/src/providers/provider-model-definition.ts +22 -1
- package/src/providers/provider-probe.ts +4 -2
- package/src/providers/provider-usage.ts +50 -19
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +4 -0
- package/src/providers/quota/refresh.ts +27 -4
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +1 -3
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +4 -4
- package/src/security/admission/service.ts +13 -6
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +17 -3
- package/src/security/client-router-fingerprint.ts +4 -4
- package/src/security/credential-codec.ts +3 -3
- package/src/security/csrf.ts +2 -2
- package/src/security/ip-boundary.ts +16 -189
- package/src/security/model-access-rule.ts +1 -1
- package/src/security/outbound-headers.ts +18 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +70 -22
- package/src/transport/dispatch/attempt-loop.ts +22 -0
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +23 -20
- package/src/transport/dispatch/generation.ts +25 -45
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +3 -1
- package/src/transport/dispatch/responses-compact.ts +25 -46
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +2 -2
- package/src/transport/dispatch/streaming/stream.ts +7 -3
- package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
- package/src/transport/dispatch/systemone.ts +20 -48
- package/src/transport/dispatch/token-count.ts +41 -38
- package/src/transport/dispatch/websearch.ts +19 -45
- package/src/transport/drain-endpoint.ts +1 -1
- package/src/transport/failure-policy.ts +21 -1
- package/src/transport/guards/auth-guard.ts +0 -6
- package/src/transport/guards/body-policy.ts +3 -3
- package/src/transport/guards/error-lifecycle.ts +4 -0
- package/src/transport/guards/headroom-guard.ts +1 -1
- package/src/transport/guards/pipeline.ts +1 -1
- package/src/transport/guards/request-context.ts +12 -17
- package/src/transport/request/preparer.ts +286 -7
- package/src/transport/request/rtk/compress-request.ts +3 -3
- package/src/transport/request/rtk/rtk-constants.ts +2 -2
- package/src/transport/request/rtk/rtk-filters.ts +8 -8
- package/src/transport/request/state.ts +7 -13
- package/src/transport/request/system-prompt-injection.ts +74 -11
- package/src/transport/request/tenant-preferences.ts +22 -0
- package/src/transport/routing/candidate-index.ts +2 -2
- package/src/transport/routing/round-robin.ts +1 -1
- package/src/transport/routing/route-catalog.ts +17 -18
- package/src/transport/routing/route-model.ts +126 -33
- package/src/transport/routing/router.ts +159 -37
- package/src/transport/shutdown-notice.ts +2 -2
- package/src/transport/surface/adapters.ts +3 -3
- package/src/transport/surface/chat/encode.ts +7 -7
- package/src/transport/surface/chat/parse.ts +7 -7
- package/src/transport/surface/completion.ts +2 -2
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/parse.ts +3 -3
- package/src/transport/surface/reasoning-summary.ts +9 -14
- package/src/transport/surface/responses/adapter.ts +1 -1
- package/src/transport/surface/responses/errors.ts +0 -9
- package/src/transport/vision-handoff-cache.ts +58 -0
- package/src/transport/vision-handoff.ts +431 -0
- package/src/workers/account-identity-sweep.ts +2 -2
- package/src/workers/buddy-growth-tasks.ts +392 -346
- package/src/workers/daily-checkin.ts +102 -49
- package/src/workers/maintenance-lease.ts +2 -35
- package/src/workers/oauth-refresh-sweep.ts +8 -1
- package/src/workers/quota-refresh-sweep.ts +120 -82
- package/src/workers/scheduler.ts +67 -4
- package/src/workers/sweep-runner.ts +24 -5
- package/src/workers/zcode-gift-claim.ts +108 -0
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js +0 -1
- package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
- package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js +0 -1
- package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
- package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
- package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
- package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
- package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +0 -1
- package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
- package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
- package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
- package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
- package/dist/dashboard/assets/app-BayTTy01.js +0 -9
- package/dist/dashboard/assets/app-BayTTy01.js.gz +0 -0
- package/dist/dashboard/assets/app-a159k0ub.css +0 -2
- package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
- package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
- package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
- package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
- package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
- package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
- package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
- package/dist/dashboard/assets/dialog-8fiE6tec.js +0 -1
- package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
- package/dist/dashboard/assets/entry-BSAsYsmV.js +0 -1
- package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
- package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
- package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
- package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
- package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
- package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-CPK47xQx.js +0 -1
- package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
- package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
- package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
- package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
- package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
- package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
- package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
- package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
- package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
- package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
- package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
- package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
- package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
- package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
- package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
- package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
- package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
- package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
- package/src/console/cli-tools/injectors/cline.ts +0 -110
- package/src/console/cli-tools/injectors/copilot.ts +0 -108
- package/src/console/cli-tools/injectors/cowork.ts +0 -64
- package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
- package/src/console/cli-tools/injectors/droid.ts +0 -96
- package/src/console/cli-tools/injectors/grok-build.ts +0 -70
- package/src/console/cli-tools/injectors/hermes.ts +0 -101
- package/src/console/cli-tools/injectors/jcode.ts +0 -115
- package/src/console/cli-tools/injectors/openclaw.ts +0 -161
- package/src/network/response-headers.ts +0 -39
- package/src/protocol/response/dsml.ts +0 -38
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
- /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
- /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
|
@@ -8,11 +8,12 @@ import { randomBytes } from "node:crypto";
|
|
|
8
8
|
import type { ApiKeyRecord, ApiKeyStore } from "../../persistence/stores/api-key-store";
|
|
9
9
|
import { hashSecret } from "../../security/crypto";
|
|
10
10
|
import { type ShareLinkStore, type ShareLinkSummary } from "../../persistence/stores/share-store";
|
|
11
|
-
import { API_KEY_MODES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
|
|
11
|
+
import { API_KEY_MODES, API_KEY_CONCURRENCY_SCOPES, type ApiKeyMode, type ApiKeyModelAccessMode, type ShareLinkKind } from "../../persistence/schema";
|
|
12
12
|
import { isValidTenantKeyScope, type AccessScope } from "../../security/access-control";
|
|
13
13
|
import { normalizeClientRouterId } from "../../security/client-router-fingerprint";
|
|
14
14
|
import type { ShareActivityPort } from "../share/share-usage";
|
|
15
15
|
import { ConsoleDomainError } from "../shared/errors";
|
|
16
|
+
import { pickDefined } from "../shared/object";
|
|
16
17
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
17
18
|
import type { ApiKeyAdmissionService } from "../../security/admission/service";
|
|
18
19
|
import {
|
|
@@ -34,6 +35,9 @@ export type {
|
|
|
34
35
|
SharedKeySummary,
|
|
35
36
|
SharedKeyActivityDetail,
|
|
36
37
|
} from "../share/share-usage";
|
|
38
|
+
/** How a share template's concurrency cap applies to its recipients. */
|
|
39
|
+
export type ConcurrencyScope = (typeof API_KEY_CONCURRENCY_SCOPES)[number];
|
|
40
|
+
|
|
37
41
|
/** Input accepted when creating or editing a key. Null limits mean unlimited. */
|
|
38
42
|
export interface CreateApiKeyRequest {
|
|
39
43
|
enabled?: boolean;
|
|
@@ -49,6 +53,13 @@ export interface CreateApiKeyRequest {
|
|
|
49
53
|
monthlyTokenLimit?: number | null;
|
|
50
54
|
lifetimeTokenBudget?: number | null;
|
|
51
55
|
maxConcurrentRequests?: number | null;
|
|
56
|
+
/**
|
|
57
|
+
* How `maxConcurrentRequests` applies to a share template's family:
|
|
58
|
+
* `family` (default) = one cap across all recipient keys (a single shared
|
|
59
|
+
* admission counter); `per_key` = each recipient key gets its own cap and
|
|
60
|
+
* independent counter. Ignored/meaningless for personal keys.
|
|
61
|
+
*/
|
|
62
|
+
concurrencyScope?: ConcurrencyScope;
|
|
52
63
|
/** Share-template slot cap; only meaningful when keyMode === "share". */
|
|
53
64
|
maxChildren?: number | null;
|
|
54
65
|
modelPrefix?: string;
|
|
@@ -90,6 +101,7 @@ export interface ApiKeyResponse {
|
|
|
90
101
|
readonly modelList?: readonly string[];
|
|
91
102
|
readonly clientRouterDenylist?: readonly string[];
|
|
92
103
|
readonly maxConcurrentRequests?: number;
|
|
104
|
+
readonly concurrencyScope?: ConcurrencyScope;
|
|
93
105
|
readonly maxChildren?: number;
|
|
94
106
|
readonly notesTitle?: string;
|
|
95
107
|
readonly notesSubtitle?: string;
|
|
@@ -119,34 +131,16 @@ export interface UpdateApiKeyResponse extends ApiKeyResponse {
|
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
/**
|
|
122
|
-
* Ban reasons offered by the console dropdown
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
export const API_KEY_BAN_REASONS: readonly { readonly value: string; readonly label: string }[] = [
|
|
128
|
-
{ value: "instant_arbitrary", label: "Instant Ban — Suka suka Gw" },
|
|
129
|
-
{ value: "traffic_suspicious", label: "Your traffic looks suspicious" },
|
|
130
|
-
{ value: "abuse", label: "Abuse of the gateway" },
|
|
131
|
-
{ value: "policy_violation", label: "Policy violation" },
|
|
132
|
-
{ value: "compromised", label: "Key may be compromised" },
|
|
133
|
-
{ value: "quota_exceeded", label: "Quota abused" },
|
|
134
|
-
{ value: "other", label: "Other / custom" },
|
|
135
|
-
] as const;
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Ban durations the console offers, in whole minutes. `null` is permanent.
|
|
139
|
-
* Mirrored by the dashboard (`dashboard/src/lib/ban-durations.ts`) so the UI
|
|
140
|
-
* and the API agree on one list.
|
|
134
|
+
* Ban reasons offered by the console dropdown, and the durations the ban
|
|
135
|
+
* endpoint accepts. These are pure lists, kept in `ban-constants.ts` (no
|
|
136
|
+
* `node:*` imports) so the dashboard can import them without dragging Node
|
|
137
|
+
* crypto into the browser bundle. Re-exported here for backend consumers
|
|
138
|
+
* (`routes.ts` validates against `BAN_DURATION_CHOICES`).
|
|
141
139
|
*/
|
|
142
|
-
export
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
{ minutes: 24 * 60, label: "1 day" },
|
|
147
|
-
{ minutes: 7 * 24 * 60, label: "7 days" },
|
|
148
|
-
{ minutes: null, label: "Permanent" },
|
|
149
|
-
];
|
|
140
|
+
export {
|
|
141
|
+
API_KEY_BAN_REASONS,
|
|
142
|
+
BAN_DURATION_CHOICES,
|
|
143
|
+
} from "./ban-constants";
|
|
150
144
|
|
|
151
145
|
/** What a bare "ban this key" click uses: the shortest, most reversible choice. */
|
|
152
146
|
export const DEFAULT_BAN_MINUTES: number = 60;
|
|
@@ -154,7 +148,6 @@ export const DEFAULT_BAN_MINUTES: number = 60;
|
|
|
154
148
|
/** Body accepted by the ban endpoint. */
|
|
155
149
|
export interface BanApiKeyRequest {
|
|
156
150
|
readonly reason: string;
|
|
157
|
-
readonly reasonLabel?: string;
|
|
158
151
|
/**
|
|
159
152
|
* Ban duration in whole minutes from {@link BAN_DURATION_CHOICES}; `null`
|
|
160
153
|
* means permanent. Absent = {@link DEFAULT_BAN_MINUTES}.
|
|
@@ -255,16 +248,20 @@ export function generateApiKeySecret(prefix = DEFAULT_API_KEY_PREFIX): {
|
|
|
255
248
|
return { secret, hash: hashSecret(secret), prefix };
|
|
256
249
|
}
|
|
257
250
|
|
|
258
|
-
/** Normalizes a caller-supplied prefix; falls back to
|
|
259
|
-
|
|
251
|
+
/** Normalizes a caller-supplied prefix; falls back to `fallback`. */
|
|
252
|
+
function resolvePrefix(raw: string | undefined, fallback: string): string {
|
|
260
253
|
const trimmed = raw?.trim();
|
|
261
|
-
return trimmed && trimmed.length > 0 ? trimmed :
|
|
254
|
+
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
|
262
255
|
}
|
|
263
256
|
|
|
264
|
-
/**
|
|
257
|
+
/** Normalizes a caller-supplied prefix; falls back to the personal default. */
|
|
258
|
+
export function resolveKeyPrefix(raw: string | undefined): string {
|
|
259
|
+
return resolvePrefix(raw, DEFAULT_API_KEY_PREFIX);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Resolves the share-key prefix; falls back to the share default. */
|
|
265
263
|
export function resolveShareKeyPrefix(raw: string | undefined): string {
|
|
266
|
-
|
|
267
|
-
return !trimmed ? DEFAULT_SHARE_API_KEY_PREFIX : trimmed;
|
|
264
|
+
return resolvePrefix(raw, DEFAULT_SHARE_API_KEY_PREFIX);
|
|
268
265
|
}
|
|
269
266
|
|
|
270
267
|
/** Maximum hint the enroll page accepts; the dashboard caps its input there. */
|
|
@@ -349,6 +346,19 @@ export function validateApiKeyRequest(request: CreateApiKeyRequest): readonly Ac
|
|
|
349
346
|
finitePositive(request.monthlyTokenLimit, "monthlyTokenLimit");
|
|
350
347
|
finitePositive(request.lifetimeTokenBudget, "lifetimeTokenBudget");
|
|
351
348
|
finitePositive(request.maxConcurrentRequests, "maxConcurrentRequests");
|
|
349
|
+
if (
|
|
350
|
+
request.concurrencyScope !== undefined &&
|
|
351
|
+
!(API_KEY_CONCURRENCY_SCOPES as readonly string[]).includes(request.concurrencyScope)
|
|
352
|
+
) {
|
|
353
|
+
throw new ConsoleDomainError("invalid_limits", 400, "concurrencyScope must be family or per_key");
|
|
354
|
+
}
|
|
355
|
+
if (request.concurrencyScope !== undefined && keyMode !== "share") {
|
|
356
|
+
throw new ConsoleDomainError(
|
|
357
|
+
"invalid_limits",
|
|
358
|
+
400,
|
|
359
|
+
"concurrencyScope is only valid for share templates",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
352
362
|
if (request.maxChildren !== undefined && request.maxChildren !== null) {
|
|
353
363
|
finitePositive(request.maxChildren, "maxChildren");
|
|
354
364
|
if (request.maxChildren > 10_000) {
|
|
@@ -424,34 +434,33 @@ export function sanitizeApiKeyResponse(record: ApiKeyRecord): ApiKeyResponse {
|
|
|
424
434
|
* renaming the key cannot change which key is the default.
|
|
425
435
|
*/
|
|
426
436
|
isDefault: isDefaultGatewayApiKey(record),
|
|
427
|
-
...(
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
:
|
|
437
|
+
...pickDefined({
|
|
438
|
+
keyPrefix,
|
|
439
|
+
parentKeyId: record.parentKeyId,
|
|
440
|
+
notesTitle: record.notesTitle,
|
|
441
|
+
notesSubtitle: record.notesSubtitle,
|
|
442
|
+
notesBody: record.notesBody,
|
|
443
|
+
sharePopupEnabled: record.sharePopupEnabled,
|
|
444
|
+
sharePopupImageMime: record.sharePopupImageMime,
|
|
445
|
+
sharePopupTitle: record.sharePopupTitle,
|
|
446
|
+
sharePopupBody: record.sharePopupBody,
|
|
447
|
+
requestsPerMinute: record.requestsPerMinute,
|
|
448
|
+
dailyTokenLimit: record.dailyTokenLimit,
|
|
449
|
+
monthlyTokenLimit: record.monthlyTokenLimit,
|
|
450
|
+
lifetimeTokenBudget: record.lifetimeTokenBudget,
|
|
451
|
+
maxConcurrentRequests: record.maxConcurrentRequests,
|
|
452
|
+
concurrencyScope: record.concurrencyScope,
|
|
453
|
+
maxChildren: record.maxChildren,
|
|
454
|
+
modelPrefix: record.modelPrefix,
|
|
455
|
+
modelAccessMode: record.modelAccessMode,
|
|
456
|
+
modelList: record.modelList,
|
|
457
|
+
clientRouterDenylist: record.clientRouterDenylist,
|
|
458
|
+
revokedAt: record.revokedAt?.toISOString(),
|
|
459
|
+
bannedAt: record.bannedAt?.toISOString(),
|
|
460
|
+
banReason: record.banReason,
|
|
461
|
+
bannedUntil: record.bannedUntil?.toISOString(),
|
|
462
|
+
}),
|
|
450
463
|
createdAt: record.createdAt.toISOString(),
|
|
451
|
-
...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt.toISOString() }),
|
|
452
|
-
...(record.bannedAt === undefined ? {} : { bannedAt: record.bannedAt.toISOString() }),
|
|
453
|
-
...(record.banReason === undefined ? {} : { banReason: record.banReason }),
|
|
454
|
-
...(record.bannedUntil === undefined ? {} : { bannedUntil: record.bannedUntil.toISOString() }),
|
|
455
464
|
tokensConsumed: record.tokensConsumed,
|
|
456
465
|
};
|
|
457
466
|
}
|
|
@@ -11,7 +11,7 @@ import { generateShareSlug, hashShareToken } from "../../persistence/stores/shar
|
|
|
11
11
|
import type { AccessDecision } from "../../security/access-control";
|
|
12
12
|
import { ConsoleDomainError, errorResponse, requireTenantScope } from "../shared/errors";
|
|
13
13
|
import { literalUnion } from "../shared/elysia-schema";
|
|
14
|
-
import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, type ShareLinkKind } from "../../persistence/schema";
|
|
14
|
+
import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, API_KEY_CONCURRENCY_SCOPES, type ShareLinkKind } from "../../persistence/schema";
|
|
15
15
|
import type { ApiKeyConfig, ApiKeyResponse, BanApiKeyRequest, UpdateApiKeyResponse } from "./contracts";
|
|
16
16
|
import type {
|
|
17
17
|
ApiKeyPatch,
|
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
SharedChildrenPageOptions,
|
|
21
21
|
} from "../../persistence/stores/api-key-store";
|
|
22
22
|
import {
|
|
23
|
+
API_KEY_BAN_REASONS,
|
|
23
24
|
BAN_DURATION_CHOICES,
|
|
24
25
|
DEFAULT_BAN_MINUTES,
|
|
25
26
|
generateApiKeySecret,
|
|
@@ -50,6 +51,9 @@ const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
|
|
|
50
51
|
"enabled",
|
|
51
52
|
]);
|
|
52
53
|
|
|
54
|
+
/** The offered ban durations as a set of minute values, hoisted once. */
|
|
55
|
+
const BAN_DURATION_MINUTES = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
|
|
56
|
+
|
|
53
57
|
/** Parses and bounds the paginated share-child list query. */
|
|
54
58
|
function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOptions {
|
|
55
59
|
const limitRaw = query.get("limit");
|
|
@@ -82,24 +86,23 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
82
86
|
const topLevel = records.filter(
|
|
83
87
|
(record) => record.revokedAt === undefined && record.parentKeyId === undefined,
|
|
84
88
|
);
|
|
85
|
-
// A share template's own counter
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
// telemetry total the family ever recorded — revoked/deleted children
|
|
90
|
-
// included (their spend is already spent), so the number survives
|
|
91
|
-
// cleanup and matches the share page.
|
|
89
|
+
// A share template's own counter is the live family accumulator now:
|
|
90
|
+
// every recipient commit adds to it (see ApiKeyAdmissionService
|
|
91
|
+
// commitUsage), so reporting it raw gives the enforced family total.
|
|
92
|
+
// No telemetry add-on is needed, and adding it here would double-count.
|
|
92
93
|
return Promise.all(
|
|
93
94
|
topLevel.map(async (record) => {
|
|
94
95
|
const response = sanitizeApiKeyResponse(record);
|
|
95
96
|
if (record.keyMode !== "share") return response;
|
|
96
|
-
|
|
97
|
-
.sumFamilyTelemetryConsumed(authorized.tenantId, record.id)
|
|
98
|
-
.catch(() => 0);
|
|
99
|
-
return { ...response, tokensConsumed: response.tokensConsumed + family };
|
|
97
|
+
return response;
|
|
100
98
|
}),
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
async listActiveBannedKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
|
|
102
|
+
const authorized = requireTenantScope(access, "dashboard:read");
|
|
103
|
+
const records = await config.store.listActiveBanned(authorized.tenantId);
|
|
104
|
+
return records.map(sanitizeApiKeyResponse);
|
|
105
|
+
},
|
|
103
106
|
/** Persists a new credential order. `keyIds` must name every top-level key
|
|
104
107
|
the tenant has, so a partial list cannot silently renumber rows. */
|
|
105
108
|
async reorderKeys(access: AccessDecision | undefined, keyIds: readonly string[]): Promise<void> {
|
|
@@ -187,6 +190,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
187
190
|
...(request.monthlyTokenLimit == null ? {} : { monthlyTokenLimit: request.monthlyTokenLimit }),
|
|
188
191
|
...(request.lifetimeTokenBudget == null ? {} : { lifetimeTokenBudget: request.lifetimeTokenBudget }),
|
|
189
192
|
...(request.maxConcurrentRequests == null ? {} : { maxConcurrentRequests: request.maxConcurrentRequests }),
|
|
193
|
+
...(request.concurrencyScope !== undefined ? { concurrencyScope: request.concurrencyScope } : {}),
|
|
190
194
|
...(request.maxChildren == null ? {} : { maxChildren: request.maxChildren }),
|
|
191
195
|
...(request.modelPrefix === undefined ? {} : { modelPrefix: request.modelPrefix }),
|
|
192
196
|
modelAccessMode: request.modelAccessMode ?? "whitelist",
|
|
@@ -322,6 +326,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
322
326
|
...(patchRequest.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: patchRequest.monthlyTokenLimit }),
|
|
323
327
|
...(patchRequest.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: patchRequest.lifetimeTokenBudget }),
|
|
324
328
|
...(patchRequest.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: patchRequest.maxConcurrentRequests }),
|
|
329
|
+
...(patchRequest.concurrencyScope !== undefined ? { concurrencyScope: patchRequest.concurrencyScope } : {}),
|
|
325
330
|
...(patchRequest.maxChildren === undefined ? {} : { maxChildren: patchRequest.maxChildren }),
|
|
326
331
|
...(patchRequest.modelPrefix === undefined ? {} : { modelPrefix: patchRequest.modelPrefix }),
|
|
327
332
|
...(patchRequest.modelAccessMode === undefined ? {} : { modelAccessMode: patchRequest.modelAccessMode }),
|
|
@@ -791,8 +796,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
791
796
|
// clamping would answer a request for 90 days with a week and report
|
|
792
797
|
// success.
|
|
793
798
|
const chosenMinutes = request.minutes === undefined ? DEFAULT_BAN_MINUTES : request.minutes;
|
|
794
|
-
|
|
795
|
-
if (!allowed.has(chosenMinutes)) {
|
|
799
|
+
if (!BAN_DURATION_MINUTES.has(chosenMinutes)) {
|
|
796
800
|
throw new ConsoleDomainError(
|
|
797
801
|
"invalid_request",
|
|
798
802
|
400,
|
|
@@ -814,11 +818,27 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
814
818
|
access: authorized,
|
|
815
819
|
action: "api_key.banned",
|
|
816
820
|
target: keyId,
|
|
817
|
-
detail: {
|
|
821
|
+
detail: {
|
|
822
|
+
reason,
|
|
823
|
+
// The human-readable label is derived server-side from the machine
|
|
824
|
+
// `reason`, so the audit trail cannot be skewed by a client-authored
|
|
825
|
+
// label. Unknown reasons (custom text) simply carry no label; the
|
|
826
|
+
// free-text `reason` above is still recorded verbatim.
|
|
827
|
+
...(API_KEY_BAN_REASONS.find((r) => r.value === reason)?.label === undefined
|
|
828
|
+
? {}
|
|
829
|
+
: { reasonLabel: API_KEY_BAN_REASONS.find((r) => r.value === reason)!.label }),
|
|
830
|
+
...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }),
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
// The ban mutates the row already fetched; derive the response from that
|
|
834
|
+
// record instead of re-fetching after the write. Match the store's own
|
|
835
|
+
// mapping: a `null` (permanent) `bannedUntil` is omitted, not carried.
|
|
836
|
+
return sanitizeApiKeyResponse({
|
|
837
|
+
...current,
|
|
838
|
+
bannedAt: new Date(),
|
|
839
|
+
banReason: reason,
|
|
840
|
+
...(bannedUntil ? { bannedUntil } : {}),
|
|
818
841
|
});
|
|
819
|
-
const record = await config.store.get(authorized.tenantId, keyId);
|
|
820
|
-
if (!record) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
821
|
-
return sanitizeApiKeyResponse(record);
|
|
822
842
|
},
|
|
823
843
|
async unbanKey(
|
|
824
844
|
access: AccessDecision | undefined,
|
|
@@ -836,9 +856,11 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
836
856
|
action: "api_key.unbanned",
|
|
837
857
|
target: keyId,
|
|
838
858
|
});
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
859
|
+
// Unban clears all three ban columns and re-enables the key; derive the
|
|
860
|
+
// response from the pre-fetch instead of re-fetching after the write.
|
|
861
|
+
const { bannedAt: _bannedAt, banReason: _banReason, bannedUntil: _bannedUntil, ...currentRest } = current;
|
|
862
|
+
const unbannedRecord: ApiKeyRecord = { ...currentRest, enabled: true };
|
|
863
|
+
return sanitizeApiKeyResponse(unbannedRecord);
|
|
842
864
|
},
|
|
843
865
|
};
|
|
844
866
|
return operations;
|
|
@@ -858,6 +880,7 @@ const apiKeyBody = t.Object({
|
|
|
858
880
|
monthlyTokenLimit: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
859
881
|
lifetimeTokenBudget: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
860
882
|
maxConcurrentRequests: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
883
|
+
concurrencyScope: t.Optional(literalUnion(API_KEY_CONCURRENCY_SCOPES)),
|
|
861
884
|
modelPrefix: t.Optional(t.String()),
|
|
862
885
|
modelAccessMode: t.Optional(literalUnion(API_KEY_MODEL_ACCESS_MODES)),
|
|
863
886
|
modelList: t.Optional(t.Array(t.String())),
|
|
@@ -905,6 +928,13 @@ export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
|
|
|
905
928
|
return errorResponse(error, set, "API-key operation failed");
|
|
906
929
|
}
|
|
907
930
|
})
|
|
931
|
+
.get("/banned", async ({ request, set }) => {
|
|
932
|
+
try {
|
|
933
|
+
return await factory.listActiveBannedKeys(config.accessResolver(request));
|
|
934
|
+
} catch (error) {
|
|
935
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
936
|
+
}
|
|
937
|
+
})
|
|
908
938
|
.post("/reorder", { body: reorderKeysBody }, async ({ request, body, set }) => {
|
|
909
939
|
try {
|
|
910
940
|
await factory.reorderKeys(config.accessResolver(request), body.keyIds);
|
|
@@ -1020,7 +1050,9 @@ export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
|
|
|
1020
1050
|
{
|
|
1021
1051
|
body: t.Object({
|
|
1022
1052
|
reason: t.String(),
|
|
1023
|
-
reasonLabel:
|
|
1053
|
+
// `reasonLabel` is deliberately not accepted: the audit label is
|
|
1054
|
+
// derived server-side from `reason` (see `banKey`), so a client
|
|
1055
|
+
// cannot author the human-readable audit entry.
|
|
1024
1056
|
minutes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
|
|
1025
1057
|
}),
|
|
1026
1058
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccessDecision } from "../../security/access-control";
|
|
2
|
-
|
|
3
|
-
/** Resolves the authenticated console request to a tenant-scoped decision. */
|
|
4
|
-
export type ConsoleAccessResolver = (request: Request) => AccessDecision | undefined;
|
|
1
|
+
import type { AccessDecision } from "../../security/access-control";
|
|
2
|
+
|
|
3
|
+
/** Resolves the authenticated console request to a tenant-scoped decision. */
|
|
4
|
+
export type ConsoleAccessResolver = (request: Request) => AccessDecision | undefined;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
26
26
|
import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
interface ConsoleAccountResetDeps {
|
|
29
29
|
/** Shared secret; the route is registered only when this is set. */
|
|
30
30
|
readonly token: string;
|
|
31
31
|
/** Removes every console user and session. Resolves to the number removed. */
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
9
9
|
import { type AccessDecision } from "../../security/access-control";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface AuditRecordInput {
|
|
12
12
|
readonly access: AccessDecision;
|
|
13
13
|
readonly action: string;
|
|
14
14
|
readonly target: string;
|
|
@@ -7,8 +7,9 @@ import {
|
|
|
7
7
|
} from "../../persistence/schema";
|
|
8
8
|
|
|
9
9
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
10
|
+
import { log } from "../../observability/logger";
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
class ConsoleLockoutStore {
|
|
12
13
|
constructor(private readonly db: CartethyiaDatabase) {}
|
|
13
14
|
|
|
14
15
|
async getLockedUntil(ip: string): Promise<Date | null | undefined> {
|
|
@@ -143,8 +144,11 @@ export class ConsoleLockoutService {
|
|
|
143
144
|
enforcement: "full_network_lockout",
|
|
144
145
|
},
|
|
145
146
|
});
|
|
146
|
-
} catch {
|
|
147
|
-
// Best-effort audit logging; the ban itself remains enforced via the DB
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// Best-effort audit logging; the ban itself remains enforced via the DB
|
|
149
|
+
// row. A failed write is logged — for a security-relevant event
|
|
150
|
+
// (`security.ip_banned`) total silence would hide a broken audit trail.
|
|
151
|
+
log.error(`[auth] failed to record ip_banned audit for ${ip}`, error as Error, reason);
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -3,7 +3,7 @@ import type { ConsoleSession, ConsoleUser } from "../../persistence/schema";
|
|
|
3
3
|
|
|
4
4
|
import { isRecord } from "../../protocol/codecs";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
function asDate(value: unknown): Date | undefined {
|
|
7
7
|
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
|
|
8
8
|
if (typeof value === "string" || typeof value === "number") {
|
|
9
9
|
const date = new Date(value);
|
|
@@ -14,33 +14,78 @@ export function asDate(value: unknown): Date | undefined {
|
|
|
14
14
|
|
|
15
15
|
export function sessionRecord(value: unknown): ConsoleSession | undefined {
|
|
16
16
|
if (!isRecord(value)) return undefined;
|
|
17
|
+
const id = value.id;
|
|
18
|
+
const userId = value.userId;
|
|
19
|
+
const sessionToken = value.sessionToken;
|
|
20
|
+
const ipAddress = value.ipAddress;
|
|
21
|
+
const userAgent = value.userAgent;
|
|
17
22
|
if (
|
|
18
|
-
typeof
|
|
19
|
-
typeof
|
|
20
|
-
typeof
|
|
23
|
+
typeof id !== "string" ||
|
|
24
|
+
typeof userId !== "string" ||
|
|
25
|
+
typeof sessionToken !== "string"
|
|
21
26
|
) {
|
|
22
27
|
return undefined;
|
|
23
28
|
}
|
|
24
29
|
const expiresAt = asDate(value.expiresAt);
|
|
25
30
|
const createdAt = asDate(value.createdAt);
|
|
26
31
|
if (!expiresAt || !createdAt) return undefined;
|
|
27
|
-
|
|
32
|
+
// Construct the result explicitly field-by-field so the output shape is
|
|
33
|
+
// exactly what was validated — arbitrary extra keys on a driver row must
|
|
34
|
+
// not survive into the typed session.
|
|
35
|
+
return {
|
|
36
|
+
id,
|
|
37
|
+
userId,
|
|
38
|
+
sessionToken,
|
|
39
|
+
...(ipAddress === null || typeof ipAddress === "string" ? { ipAddress } : { ipAddress: null }),
|
|
40
|
+
...(userAgent === null || typeof userAgent === "string" ? { userAgent } : { userAgent: null }),
|
|
41
|
+
expiresAt,
|
|
42
|
+
createdAt,
|
|
43
|
+
};
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export function userRecord(value: unknown): ConsoleUser | undefined {
|
|
31
47
|
if (!isRecord(value)) return undefined;
|
|
48
|
+
const id = value.id;
|
|
49
|
+
const username = value.username;
|
|
50
|
+
const passwordHash = value.passwordHash;
|
|
51
|
+
const isActive = value.isActive;
|
|
32
52
|
if (
|
|
33
|
-
typeof
|
|
34
|
-
typeof
|
|
35
|
-
typeof
|
|
36
|
-
typeof
|
|
53
|
+
typeof id !== "string" ||
|
|
54
|
+
typeof username !== "string" ||
|
|
55
|
+
typeof passwordHash !== "string" ||
|
|
56
|
+
typeof isActive !== "boolean"
|
|
37
57
|
) {
|
|
38
58
|
return undefined;
|
|
39
59
|
}
|
|
40
|
-
|
|
60
|
+
// Timestamps are coerced through `asDate` because SQLite hands them back as
|
|
61
|
+
// text; a login path that rejects non-Date values must still work there.
|
|
62
|
+
const createdAt = asDate(value.createdAt);
|
|
63
|
+
const updatedAt = asDate(value.updatedAt);
|
|
64
|
+
if (!createdAt || !updatedAt) return undefined;
|
|
65
|
+
const tenantId = value.tenantId;
|
|
66
|
+
const email = value.email;
|
|
67
|
+
const displayName = value.displayName;
|
|
68
|
+
const isFirstBoot = value.isFirstBoot;
|
|
69
|
+
const isPlatformAdmin = value.isPlatformAdmin;
|
|
70
|
+
// Construct the result explicitly field-by-field so the output shape is
|
|
71
|
+
// exactly what was validated — arbitrary extra keys on a driver row must
|
|
72
|
+
// not survive into the typed user.
|
|
73
|
+
return {
|
|
74
|
+
id,
|
|
75
|
+
tenantId: typeof tenantId === "string" ? tenantId : "",
|
|
76
|
+
username,
|
|
77
|
+
email: typeof email === "string" ? email : null,
|
|
78
|
+
passwordHash,
|
|
79
|
+
displayName: typeof displayName === "string" ? displayName : null,
|
|
80
|
+
isActive,
|
|
81
|
+
isPlatformAdmin: isPlatformAdmin === true,
|
|
82
|
+
isFirstBoot: isFirstBoot === true,
|
|
83
|
+
createdAt,
|
|
84
|
+
updatedAt,
|
|
85
|
+
};
|
|
41
86
|
}
|
|
42
87
|
|
|
43
|
-
|
|
88
|
+
function mutationAffectedRows(value: unknown): number | undefined {
|
|
44
89
|
if (!isRecord(value)) return undefined;
|
|
45
90
|
for (const key of ["rowCount", "rowsAffected", "changes"] as const) {
|
|
46
91
|
const count = value[key];
|
|
@@ -50,7 +95,7 @@ export function mutationAffectedRows(value: unknown): number | undefined {
|
|
|
50
95
|
}
|
|
51
96
|
|
|
52
97
|
/** Awaitable Drizzle mutation builder that may expose `.returning()`. */
|
|
53
|
-
|
|
98
|
+
interface ReturningBuilder extends PromiseLike<unknown> {
|
|
54
99
|
returning?: (...columns: unknown[]) => PromiseLike<unknown[]>;
|
|
55
100
|
}
|
|
56
101
|
|
|
@@ -45,7 +45,7 @@ export async function resolveConsoleAccess(
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** The signed-in console user behind a request, or `null` when unauthenticated. */
|
|
48
|
-
|
|
48
|
+
interface ResolvedConsoleUser {
|
|
49
49
|
readonly id: string;
|
|
50
50
|
readonly sessionId: string;
|
|
51
51
|
readonly tenantId: string;
|
|
@@ -58,7 +58,7 @@ function newSessionToken(): string {
|
|
|
58
58
|
return randomBytes(32).toString("base64url");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
class ConsoleSessionStore {
|
|
62
62
|
constructor(private readonly db: CartethyiaDatabase) {}
|
|
63
63
|
|
|
64
64
|
async create(newSession: {
|
|
@@ -73,7 +73,13 @@ export class ConsoleSessionStore {
|
|
|
73
73
|
const inserted = await returningRows(this.db.insert(consoleSessions).values(newSession));
|
|
74
74
|
assertMutationApplied(inserted, "Console session");
|
|
75
75
|
const persisted = sessionRecord(inserted.rows[0]);
|
|
76
|
-
|
|
76
|
+
if (!persisted) {
|
|
77
|
+
// `assertMutationApplied` already refused a failed insert, so an
|
|
78
|
+
// unshaped row here is a driver/schema mismatch — pretending success by
|
|
79
|
+
// returning the input cast to ConsoleSession would mask it.
|
|
80
|
+
throw new Error("Console session row could not be coerced after insert");
|
|
81
|
+
}
|
|
82
|
+
return persisted;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
async findValidByTokenHash(tokenHash: string, now: Date): Promise<ConsoleSession | null> {
|