cartethyia 2.7.3 → 2.7.5
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/README.md +17 -5
- package/dist/dashboard/assets/{BannedPage-2TJD37Hb.js → BannedPage-Cj15GsMp.js} +1 -1
- package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
- package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-xefJNRmJ.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-Rj9iHQ3F.js → CliToolsPage-BIccZmeL.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
- package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
- package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-C4w7vea-.js → LoginPage-DkXb6dx-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BtPcAfot.js → ModelPicker-DnQPSIA8.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
- package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
- package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js +9 -0
- package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
- package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BQS0Rx_u.js → SetupPage-WTa0sktC.js} +1 -1
- package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-TP7voAQ0.js → StudioPage-BfAb5-o_.js} +3 -3
- package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
- package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
- package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
- package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
- package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C0YYuZJ4.js → assert-C0K-c4eo.js} +1 -1
- package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
- package/dist/dashboard/assets/{badge-B_DMpMJN.js → badge-DfbhQU4k.js} +1 -1
- package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DMrMPmth.js → banner-BllT4LcF.js} +1 -1
- package/dist/dashboard/assets/banner-BllT4LcF.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-Be5Uzamx.js → card-CtkX66Wd.js} +1 -1
- package/dist/dashboard/assets/card-CtkX66Wd.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/{cli-tools-muFRphDu.js → cli-tools-CQXVMn4P.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DOfrVmsi.js → clipboard-button-BQqBQd2n.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-C9omdjQt.js → dialog-BEK7KP0H.js} +1 -1
- package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bm8WKaGY.js → drawer-BrTEZ8zu.js} +1 -1
- package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
- package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
- package/dist/dashboard/assets/{entry-q5HwZ8nN.js → entry-EBHJ3DrE.js} +1 -1
- package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
- package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-BTScrQpe.js → formatters-BAACA727.js} +1 -1
- package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-Orjuzemr.js → github-badge-YSCE4UC2.js} +1 -1
- package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
- package/dist/dashboard/assets/{index-cPrX5vn6.js → index-FQlj0HUT.js} +2 -2
- package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
- package/dist/dashboard/assets/{inline-Dvux8w-0.js → inline-C4gU5iOL.js} +1 -1
- package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
- package/dist/dashboard/assets/{input-CU1Rgf3p.js → input-BHHX_Tmm.js} +1 -1
- package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
- package/dist/dashboard/assets/{layout-C_7A6H55.js → layout-CajrYUnD.js} +1 -1
- package/dist/dashboard/assets/{layout-C_7A6H55.js.gz → layout-CajrYUnD.js.gz} +0 -0
- package/dist/dashboard/assets/{list-row-CzFvogbj.js → list-row-Bx2I7XUe.js} +1 -1
- package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
- package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
- package/dist/dashboard/assets/live-DNNqRbj2.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-JJGQm8TD.js → mutation-Rr0TIvcj.js} +1 -1
- package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
- package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
- package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
- package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CLaNz8Xz.js → query-keys-BloKtaXJ.js} +1 -1
- package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
- package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BEH32QOC.js → routing-in0f_IfK.js} +1 -1
- package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
- package/dist/dashboard/assets/{select-B3iTetHN.js → select-BLULNqTp.js} +2 -2
- package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
- package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
- package/dist/dashboard/assets/settings-CcGczj6I.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-KrOrm7JZ.js → stack-D2lIR0tg.js} +1 -1
- package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
- package/dist/dashboard/assets/{state-DLS7IDMy.js → state-DTARjfiW.js} +1 -1
- package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
- package/dist/dashboard/assets/{switch-42ItvbAs.js → switch-c0oPSnqW.js} +1 -1
- package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dmnfh1Ue.js → useMutation-m8WGNPrz.js} +1 -1
- package/dist/dashboard/assets/useMutation-m8WGNPrz.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 +59 -57
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +9 -10
- 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/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/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +9 -10
- 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/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/package.json +4 -5
- package/scripts/cli/update.ts +15 -41
- package/src/boot/app.ts +67 -10
- package/src/boot/dependencies.ts +786 -732
- package/src/boot/lifecycle.ts +11 -12
- package/src/config/cooldowns.ts +34 -55
- package/src/config/network.ts +8 -4
- package/src/config/package-paths.ts +1 -1
- package/src/config/providers.ts +13 -14
- package/src/config/resources.ts +0 -5
- package/src/config/runtime.ts +2 -5
- package/src/config/spec.ts +26 -6
- package/src/console/api-keys/ban-constants.ts +38 -0
- package/src/console/api-keys/contracts.ts +92 -31
- package/src/console/api-keys/routes.ts +131 -6
- package/src/console/audit/routes.ts +3 -3
- package/src/console/auth/access.ts +4 -4
- package/src/console/auth/account-reset.ts +5 -5
- 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 +40 -34
- 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 +11 -9
- package/src/console/backup/export-endpoint.ts +11 -12
- package/src/console/backup/nine-router.ts +1 -7
- package/src/console/backup/routes.ts +32 -36
- 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 +7 -7
- package/src/console/cli-tools/injectors/cline.ts +110 -110
- package/src/console/cli-tools/injectors/codex.ts +181 -181
- package/src/console/cli-tools/injectors/copilot.ts +108 -108
- package/src/console/cli-tools/injectors/driver.ts +247 -247
- package/src/console/cli-tools/injectors/jcode.ts +115 -115
- package/src/console/cli-tools/injectors/openclaw.ts +161 -161
- package/src/console/cli-tools/injectors/opencode.ts +153 -153
- package/src/console/cli-tools/routes.ts +4 -4
- package/src/console/cli-tools/service.ts +1 -1
- package/src/console/cli-tools/store.ts +2 -2
- package/src/console/console-router-guards.ts +2 -2
- package/src/console/console-router.ts +36 -4
- package/src/console/dashboard-assets.ts +9 -9
- package/src/console/data-management/routes.ts +3 -3
- package/src/console/domain-registration.ts +18 -4
- 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 +5 -1
- package/src/console/observability/live.ts +34 -21
- package/src/console/observability/logs.ts +4 -4
- package/src/console/observability/performance.ts +3 -3
- package/src/console/observability/routes.ts +3 -3
- package/src/console/observability/sse.ts +62 -8
- package/src/console/observability/store.ts +36 -20
- package/src/console/providers/catalog/catalog-projections.ts +1 -1
- package/src/console/providers/catalog/contracts.ts +82 -22
- package/src/console/providers/catalog/model-operations.ts +1 -1
- package/src/console/providers/catalog/provider-operations.ts +110 -17
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/routes.ts +30 -6
- package/src/console/providers/catalog/store-helpers.ts +349 -326
- package/src/console/providers/catalog/store.ts +1662 -1292
- package/src/console/providers/detail/routes.ts +4 -4
- package/src/console/providers/oauth/callback-listener.ts +100 -18
- package/src/console/providers/oauth/routes.ts +95 -16
- 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 +285 -67
- package/src/console/quota/account-test-egress.ts +4 -4
- 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/model/routes.ts +3 -3
- package/src/console/routing/pools/contracts.ts +13 -10
- package/src/console/routing/pools/import-parse.ts +3 -3
- 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 +12 -12
- package/src/console/routing/pools/routes.ts +3 -3
- package/src/console/routing/pools/store.ts +21 -1
- package/src/console/settings/contracts.ts +29 -2
- package/src/console/settings/routes.ts +3 -3
- package/src/console/settings/store.ts +20 -0
- package/src/console/share/share-router.ts +112 -44
- package/src/console/share/share-stats.ts +109 -39
- 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 +42 -6
- 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 +256 -98
- 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 +76 -5
- package/src/network/pool/agent.ts +7 -6
- package/src/network/pool/byte-accounting.ts +1 -1
- package/src/network/pool/resolver.ts +55 -51
- package/src/network/pool/selector.ts +150 -136
- package/src/network/pool-cooldown-store.ts +71 -16
- package/src/network/pool-health-machine.ts +22 -2
- package/src/network/rate-limit-verdict.ts +68 -0
- package/src/network/retry.ts +1 -1
- package/src/network/ssrf.ts +24 -75
- package/src/observability/event-log.ts +2 -2
- package/src/observability/event-query.ts +286 -214
- 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 +12 -3
- package/src/observability/telemetry-reconciliation.ts +3 -3
- package/src/observability/telemetry-status.ts +1 -1
- 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 +10 -5
- package/src/persistence/dialect.ts +5 -1
- package/src/persistence/readiness.ts +19 -7
- package/src/persistence/redis.ts +1 -1
- package/src/persistence/schema.ts +177 -45
- package/src/persistence/shared-cache.ts +2 -2
- package/src/persistence/sort-order.ts +1 -1
- package/src/persistence/stores/api-key-store.ts +109 -4
- package/src/persistence/stores/share-store.ts +20 -8
- package/src/persistence/stores/telemetry-store.ts +1 -1
- 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 +12 -16
- package/src/protocol/dsml.ts +3 -49
- package/src/protocol/gateway-error.ts +6 -3
- package/src/protocol/image-clamp.ts +2 -2
- package/src/protocol/image-generation.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 +27 -12
- package/src/protocol/response/codex.ts +2 -2
- package/src/protocol/response/gemini.ts +1 -1
- package/src/protocol/response/messages.ts +6 -6
- package/src/protocol/response/omp-tool-call-markup.ts +1 -1
- package/src/protocol/response/responses.ts +3 -3
- package/src/protocol/sse-decoder.ts +64 -4
- 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 +8 -8
- 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/protocol/upstream/send-claude-messages.ts +3 -3
- package/src/providers/adapters/base-adapter.ts +1 -1
- package/src/providers/adapters/compatible-adapter.ts +44 -5
- 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/authentication/oauth-refresh-service.ts +2 -2
- package/src/providers/bundled-discovery-providers.ts +6 -30
- package/src/providers/configured-openai-providers.ts +82 -97
- package/src/providers/configured-provider.ts +1 -1
- package/src/providers/default-registry.ts +23 -35
- 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 +50 -15
- package/src/providers/discovery/probing-service.ts +18 -20
- package/src/providers/integrations/agentrouter/agentrouter.ts +14 -5
- package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
- package/src/providers/integrations/anthropic.ts +4 -11
- package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
- package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
- package/src/providers/integrations/antigravity/antigravity.ts +15 -26
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +2 -2
- 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-discovery-shared.ts +37 -6
- package/src/providers/integrations/buddy/buddy-identity.ts +108 -1
- package/src/providers/integrations/buddy/buddy-media.ts +16 -16
- 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 +5 -16
- package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
- package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -10
- package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
- package/src/providers/integrations/claude/claude-betas.ts +1 -1
- package/src/providers/integrations/claude/claude-cch.ts +148 -148
- 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/claude/fallback-credit.ts +3 -7
- package/src/providers/integrations/cline/cline.ts +2 -2
- package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
- package/src/providers/integrations/codex/codex-device-code.ts +101 -101
- package/src/providers/integrations/codex/codex-errors.ts +1 -1
- package/src/providers/integrations/codex/codex-oauth.ts +0 -1
- package/src/providers/integrations/codex/codex.ts +14 -15
- package/src/providers/integrations/commandcode/commandcode.ts +76 -116
- package/src/providers/integrations/commandcode/quota.ts +132 -48
- package/src/providers/integrations/cursor/cursor-http2.ts +11 -14
- 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 +53 -20
- 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 +13 -13
- package/src/providers/integrations/gemini.ts +11 -11
- package/src/providers/integrations/github/github-discovery.ts +55 -10
- package/src/providers/integrations/github/github-oauth.ts +6 -5
- package/src/providers/integrations/github/github.ts +47 -20
- 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 +88 -42
- package/src/providers/integrations/inferhub.ts +4 -11
- package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
- package/src/providers/integrations/kimi/kimi.ts +4 -11
- 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 +91 -78
- package/src/providers/integrations/muse/muse-oauth.ts +0 -1
- package/src/providers/integrations/muse/muse-quota.ts +0 -4
- package/src/providers/integrations/muse/muse.ts +6 -9
- package/src/providers/integrations/opencode/fingerprint.ts +7 -5
- package/src/providers/integrations/opencode/provider.ts +17 -24
- package/src/providers/integrations/perplexity.ts +2 -2
- package/src/providers/integrations/qoder/qoder-catalog.ts +132 -17
- package/src/providers/integrations/qoder/qoder-checkin.ts +26 -8
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +9 -8
- 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 +22 -28
- package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +33 -38
- package/src/providers/integrations/qoder/qoder-virtual.ts +14 -2
- package/src/providers/integrations/qoder/qoder.ts +521 -63
- 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/tokenharbor.ts +2 -2
- 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 +59 -19
- package/src/providers/integrations/xai/xai-oauth.ts +5 -5
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- 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 +131 -116
- package/src/providers/operations/account-health-service.ts +31 -39
- package/src/providers/operations/account-reset-service.ts +58 -1
- 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 +22 -6
- package/src/providers/operations/provider-credential-service.ts +35 -6
- package/src/providers/operations/upstream-deadline.ts +3 -3
- package/src/providers/provider-contracts.ts +65 -10
- package/src/providers/provider-credential-resolver.ts +1 -1
- 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-registry.ts +4 -4
- package/src/providers/provider-usage.ts +81 -16
- package/src/providers/quota/cache.ts +29 -0
- package/src/providers/quota/quota-contracts.ts +28 -10
- package/src/providers/quota/refresh.ts +29 -6
- package/src/providers/quota/snapshot.ts +94 -0
- package/src/providers/search/search-provider.ts +6 -8
- package/src/providers/search/search-providers.ts +2 -1
- package/src/security/abuse.ts +8 -8
- package/src/security/admission/reasons.ts +17 -17
- package/src/security/admission/service.ts +2 -2
- package/src/security/admission/ttl.ts +61 -61
- package/src/security/api-key-auth.ts +20 -8
- 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/coerce.ts +19 -0
- package/src/shared/invoke-guard.ts +47 -0
- package/src/transport/contracts.ts +13 -4
- package/src/transport/dispatch/attempt-finalize.ts +5 -4
- package/src/transport/dispatch/attempt-loop.ts +10 -7
- package/src/transport/dispatch/capture-policy.ts +34 -37
- package/src/transport/dispatch/empty-stream.ts +28 -25
- package/src/transport/dispatch/generation.ts +33 -53
- package/src/transport/dispatch/leases.ts +8 -8
- package/src/transport/dispatch/native-shared.ts +127 -0
- package/src/transport/dispatch/proxy-request.ts +10 -8
- package/src/transport/dispatch/responses-compact.ts +35 -56
- package/src/transport/dispatch/retry-policy.ts +4 -67
- package/src/transport/dispatch/stream-bridge.ts +1 -1
- package/src/transport/dispatch/streaming/encoder.ts +18 -72
- package/src/transport/dispatch/streaming/finalize.ts +11 -10
- 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 +27 -55
- package/src/transport/dispatch/token-count.ts +49 -46
- package/src/transport/dispatch/websearch.ts +24 -50
- package/src/transport/drain-endpoint.ts +3 -3
- package/src/transport/failure-policy.ts +19 -6
- package/src/transport/guards/auth-guard.ts +6 -11
- package/src/transport/guards/body-policy.ts +7 -7
- 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 +2 -2
- package/src/transport/guards/pipeline.ts +7 -7
- package/src/transport/guards/readiness-guard.ts +16 -2
- package/src/transport/guards/request-context.ts +15 -20
- package/src/transport/request/preparer.ts +306 -27
- 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 +5 -20
- 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 +113 -19
- package/src/transport/routing/route-model.ts +131 -38
- package/src/transport/routing/router.ts +154 -39
- package/src/transport/shutdown-notice.ts +21 -9
- 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 +3 -3
- package/src/transport/surface/content-parts.ts +1 -1
- package/src/transport/surface/dialects.ts +2 -2
- package/src/transport/surface/messages/encode.ts +1 -1
- package/src/transport/surface/messages/parse.ts +3 -3
- 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 +376 -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-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js +0 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.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-0OsXRGXe.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +0 -9
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js +0 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-BAL5npBC.js +0 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js +0 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +0 -2
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/entry-DS90gUks.js +0 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/entry-j40LJotO.js +0 -4
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/live-KaG15HLQ.js +0 -1
- package/dist/dashboard/assets/live-KaG15HLQ.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-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BNDZnsUE.js +0 -2
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.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-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js +0 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/state-DLS7IDMy.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-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- 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/{app-wvwCi4Eh.js → app-Bgd3zANQ.js} +0 -0
- /package/dist/dashboard/assets/{app-wvwCi4Eh.js.gz → app-Bgd3zANQ.js.gz} +0 -0
- /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;
|
|
@@ -101,6 +113,11 @@ export interface ApiKeyResponse {
|
|
|
101
113
|
readonly sharePopupBody?: string;
|
|
102
114
|
readonly createdAt: string;
|
|
103
115
|
readonly revokedAt?: string;
|
|
116
|
+
/** Ban state; see the schema columns for the contract. */
|
|
117
|
+
readonly bannedAt?: string;
|
|
118
|
+
readonly banReason?: string;
|
|
119
|
+
/** When the ban lifts on its own; absent means permanent. */
|
|
120
|
+
readonly bannedUntil?: string;
|
|
104
121
|
readonly tokensConsumed: number;
|
|
105
122
|
}
|
|
106
123
|
/** Creation result; plaintext is returned only when a personal key is minted. */
|
|
@@ -113,6 +130,31 @@ export interface UpdateApiKeyResponse extends ApiKeyResponse {
|
|
|
113
130
|
readonly secret?: string;
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
/**
|
|
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`).
|
|
139
|
+
*/
|
|
140
|
+
export {
|
|
141
|
+
API_KEY_BAN_REASONS,
|
|
142
|
+
BAN_DURATION_CHOICES,
|
|
143
|
+
} from "./ban-constants";
|
|
144
|
+
|
|
145
|
+
/** What a bare "ban this key" click uses: the shortest, most reversible choice. */
|
|
146
|
+
export const DEFAULT_BAN_MINUTES: number = 60;
|
|
147
|
+
|
|
148
|
+
/** Body accepted by the ban endpoint. */
|
|
149
|
+
export interface BanApiKeyRequest {
|
|
150
|
+
readonly reason: string;
|
|
151
|
+
/**
|
|
152
|
+
* Ban duration in whole minutes from {@link BAN_DURATION_CHOICES}; `null`
|
|
153
|
+
* means permanent. Absent = {@link DEFAULT_BAN_MINUTES}.
|
|
154
|
+
*/
|
|
155
|
+
readonly minutes?: number | null;
|
|
156
|
+
}
|
|
157
|
+
|
|
116
158
|
/** API-key route dependency boundary. */
|
|
117
159
|
export interface ApiKeyConfig {
|
|
118
160
|
readonly store: ApiKeyStore;
|
|
@@ -206,16 +248,20 @@ export function generateApiKeySecret(prefix = DEFAULT_API_KEY_PREFIX): {
|
|
|
206
248
|
return { secret, hash: hashSecret(secret), prefix };
|
|
207
249
|
}
|
|
208
250
|
|
|
209
|
-
/** Normalizes a caller-supplied prefix; falls back to
|
|
210
|
-
|
|
251
|
+
/** Normalizes a caller-supplied prefix; falls back to `fallback`. */
|
|
252
|
+
function resolvePrefix(raw: string | undefined, fallback: string): string {
|
|
211
253
|
const trimmed = raw?.trim();
|
|
212
|
-
return trimmed && trimmed.length > 0 ? trimmed :
|
|
254
|
+
return trimmed && trimmed.length > 0 ? trimmed : fallback;
|
|
255
|
+
}
|
|
256
|
+
|
|
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);
|
|
213
260
|
}
|
|
214
261
|
|
|
215
|
-
/** Resolves the share-key prefix; falls back to the default. */
|
|
262
|
+
/** Resolves the share-key prefix; falls back to the share default. */
|
|
216
263
|
export function resolveShareKeyPrefix(raw: string | undefined): string {
|
|
217
|
-
|
|
218
|
-
return !trimmed ? DEFAULT_SHARE_API_KEY_PREFIX : trimmed;
|
|
264
|
+
return resolvePrefix(raw, DEFAULT_SHARE_API_KEY_PREFIX);
|
|
219
265
|
}
|
|
220
266
|
|
|
221
267
|
/** Maximum hint the enroll page accepts; the dashboard caps its input there. */
|
|
@@ -300,6 +346,19 @@ export function validateApiKeyRequest(request: CreateApiKeyRequest): readonly Ac
|
|
|
300
346
|
finitePositive(request.monthlyTokenLimit, "monthlyTokenLimit");
|
|
301
347
|
finitePositive(request.lifetimeTokenBudget, "lifetimeTokenBudget");
|
|
302
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
|
+
}
|
|
303
362
|
if (request.maxChildren !== undefined && request.maxChildren !== null) {
|
|
304
363
|
finitePositive(request.maxChildren, "maxChildren");
|
|
305
364
|
if (request.maxChildren > 10_000) {
|
|
@@ -375,31 +434,33 @@ export function sanitizeApiKeyResponse(record: ApiKeyRecord): ApiKeyResponse {
|
|
|
375
434
|
* renaming the key cannot change which key is the default.
|
|
376
435
|
*/
|
|
377
436
|
isDefault: isDefaultGatewayApiKey(record),
|
|
378
|
-
...(
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
:
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
:
|
|
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
|
+
}),
|
|
401
463
|
createdAt: record.createdAt.toISOString(),
|
|
402
|
-
...(record.revokedAt === undefined ? {} : { revokedAt: record.revokedAt.toISOString() }),
|
|
403
464
|
tokensConsumed: record.tokensConsumed,
|
|
404
465
|
};
|
|
405
466
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Owns validation, CRUD operations, and route wiring through the ApiKeyStore.
|
|
4
4
|
|
|
5
|
-
import { Elysia, t } from "elysia";
|
|
5
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
6
6
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
7
7
|
import { decryptCredentialToString, encryptCredential } from "../../security/crypto";
|
|
8
8
|
import { invalidateApiKeyCache } from "../../security/api-key-auth";
|
|
@@ -11,8 +11,8 @@ 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";
|
|
15
|
-
import type { ApiKeyConfig, ApiKeyResponse, UpdateApiKeyResponse } from "./contracts";
|
|
14
|
+
import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, API_KEY_CONCURRENCY_SCOPES, type ShareLinkKind } from "../../persistence/schema";
|
|
15
|
+
import type { ApiKeyConfig, ApiKeyResponse, BanApiKeyRequest, UpdateApiKeyResponse } from "./contracts";
|
|
16
16
|
import type {
|
|
17
17
|
ApiKeyPatch,
|
|
18
18
|
ApiKeyRecord,
|
|
@@ -20,6 +20,9 @@ import type {
|
|
|
20
20
|
SharedChildrenPageOptions,
|
|
21
21
|
} from "../../persistence/stores/api-key-store";
|
|
22
22
|
import {
|
|
23
|
+
API_KEY_BAN_REASONS,
|
|
24
|
+
BAN_DURATION_CHOICES,
|
|
25
|
+
DEFAULT_BAN_MINUTES,
|
|
23
26
|
generateApiKeySecret,
|
|
24
27
|
isDefaultGatewayApiKey,
|
|
25
28
|
parseSharePopupImage,
|
|
@@ -38,6 +41,7 @@ import {
|
|
|
38
41
|
} from "./contracts";
|
|
39
42
|
import type { SharedKeyActivityDetail, SharedKeySummary } from "../share/share-usage";
|
|
40
43
|
import { popupImageBytes } from "./share-popup-image";
|
|
44
|
+
import { log } from "../../observability/logger";
|
|
41
45
|
|
|
42
46
|
const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
|
|
43
47
|
"createdAt",
|
|
@@ -48,6 +52,9 @@ const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
|
|
|
48
52
|
"enabled",
|
|
49
53
|
]);
|
|
50
54
|
|
|
55
|
+
/** The offered ban durations as a set of minute values, hoisted once. */
|
|
56
|
+
const BAN_DURATION_MINUTES = new Set(BAN_DURATION_CHOICES.map((choice) => choice.minutes));
|
|
57
|
+
|
|
51
58
|
/** Parses and bounds the paginated share-child list query. */
|
|
52
59
|
function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOptions {
|
|
53
60
|
const limitRaw = query.get("limit");
|
|
@@ -93,7 +100,16 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
93
100
|
if (record.keyMode !== "share") return response;
|
|
94
101
|
const family = await config.store
|
|
95
102
|
.sumFamilyTelemetryConsumed(authorized.tenantId, record.id)
|
|
96
|
-
.catch(() =>
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
// A failed sum must not fail the whole list, but it must not
|
|
105
|
+
// be silent either: reporting the share template's spend as 0
|
|
106
|
+
// under-reports consumed tokens in the console list.
|
|
107
|
+
log.warn(
|
|
108
|
+
`[api-keys] sumFamilyTelemetryConsumed failed for key ${record.id}; reporting 0`,
|
|
109
|
+
error as Error,
|
|
110
|
+
);
|
|
111
|
+
return 0;
|
|
112
|
+
});
|
|
97
113
|
return { ...response, tokensConsumed: response.tokensConsumed + family };
|
|
98
114
|
}),
|
|
99
115
|
);
|
|
@@ -185,6 +201,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
185
201
|
...(request.monthlyTokenLimit == null ? {} : { monthlyTokenLimit: request.monthlyTokenLimit }),
|
|
186
202
|
...(request.lifetimeTokenBudget == null ? {} : { lifetimeTokenBudget: request.lifetimeTokenBudget }),
|
|
187
203
|
...(request.maxConcurrentRequests == null ? {} : { maxConcurrentRequests: request.maxConcurrentRequests }),
|
|
204
|
+
...(request.concurrencyScope !== undefined ? { concurrencyScope: request.concurrencyScope } : {}),
|
|
188
205
|
...(request.maxChildren == null ? {} : { maxChildren: request.maxChildren }),
|
|
189
206
|
...(request.modelPrefix === undefined ? {} : { modelPrefix: request.modelPrefix }),
|
|
190
207
|
modelAccessMode: request.modelAccessMode ?? "whitelist",
|
|
@@ -320,6 +337,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
320
337
|
...(patchRequest.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: patchRequest.monthlyTokenLimit }),
|
|
321
338
|
...(patchRequest.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: patchRequest.lifetimeTokenBudget }),
|
|
322
339
|
...(patchRequest.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: patchRequest.maxConcurrentRequests }),
|
|
340
|
+
...(patchRequest.concurrencyScope !== undefined ? { concurrencyScope: patchRequest.concurrencyScope } : {}),
|
|
323
341
|
...(patchRequest.maxChildren === undefined ? {} : { maxChildren: patchRequest.maxChildren }),
|
|
324
342
|
...(patchRequest.modelPrefix === undefined ? {} : { modelPrefix: patchRequest.modelPrefix }),
|
|
325
343
|
...(patchRequest.modelAccessMode === undefined ? {} : { modelAccessMode: patchRequest.modelAccessMode }),
|
|
@@ -775,6 +793,86 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
775
793
|
});
|
|
776
794
|
return { success: true };
|
|
777
795
|
},
|
|
796
|
+
async banKey(
|
|
797
|
+
access: AccessDecision | undefined,
|
|
798
|
+
keyId: string,
|
|
799
|
+
request: BanApiKeyRequest,
|
|
800
|
+
): Promise<ApiKeyResponse> {
|
|
801
|
+
const authorized = requireTenantScope(access, "dashboard:write");
|
|
802
|
+
const reason = request.reason.trim();
|
|
803
|
+
if (reason.length === 0)
|
|
804
|
+
throw new ConsoleDomainError("invalid_request", 400, "reason is required");
|
|
805
|
+
// Parse the duration choice. `null` is permanent; absent defaults to one
|
|
806
|
+
// hour. Anything not on the offered list is rejected rather than clamped —
|
|
807
|
+
// clamping would answer a request for 90 days with a week and report
|
|
808
|
+
// success.
|
|
809
|
+
const chosenMinutes = request.minutes === undefined ? DEFAULT_BAN_MINUTES : request.minutes;
|
|
810
|
+
if (!BAN_DURATION_MINUTES.has(chosenMinutes)) {
|
|
811
|
+
throw new ConsoleDomainError(
|
|
812
|
+
"invalid_request",
|
|
813
|
+
400,
|
|
814
|
+
"Unsupported ban duration. Choose one of the offered durations.",
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const bannedUntil = chosenMinutes === null ? null : new Date(Date.now() + chosenMinutes * 60_000);
|
|
818
|
+
const current = await config.store.get(authorized.tenantId, keyId);
|
|
819
|
+
if (!current || current.revokedAt !== undefined)
|
|
820
|
+
throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
821
|
+
const banned = await config.store.ban(authorized.tenantId, keyId, reason, bannedUntil);
|
|
822
|
+
if (!banned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
823
|
+
// A banned child key does not affect its share template or sibling keys:
|
|
824
|
+
// `store.ban` only turns off the one row named by `keyId`, and the auth
|
|
825
|
+
// path rejects per key hash. The template keeps issuing working children.
|
|
826
|
+
// A banned key must leave the auth cache for every process at once.
|
|
827
|
+
invalidateApiKeyCache();
|
|
828
|
+
await config.auditSink?.record({
|
|
829
|
+
access: authorized,
|
|
830
|
+
action: "api_key.banned",
|
|
831
|
+
target: keyId,
|
|
832
|
+
detail: {
|
|
833
|
+
reason,
|
|
834
|
+
// The human-readable label is derived server-side from the machine
|
|
835
|
+
// `reason`, so the audit trail cannot be skewed by a client-authored
|
|
836
|
+
// label. Unknown reasons (custom text) simply carry no label; the
|
|
837
|
+
// free-text `reason` above is still recorded verbatim.
|
|
838
|
+
...(API_KEY_BAN_REASONS.find((r) => r.value === reason)?.label === undefined
|
|
839
|
+
? {}
|
|
840
|
+
: { reasonLabel: API_KEY_BAN_REASONS.find((r) => r.value === reason)!.label }),
|
|
841
|
+
...(bannedUntil === null ? { permanent: true } : { bannedUntil: bannedUntil.toISOString() }),
|
|
842
|
+
},
|
|
843
|
+
});
|
|
844
|
+
// The ban mutates the row already fetched; derive the response from that
|
|
845
|
+
// record instead of re-fetching after the write. Match the store's own
|
|
846
|
+
// mapping: a `null` (permanent) `bannedUntil` is omitted, not carried.
|
|
847
|
+
return sanitizeApiKeyResponse({
|
|
848
|
+
...current,
|
|
849
|
+
bannedAt: new Date(),
|
|
850
|
+
banReason: reason,
|
|
851
|
+
...(bannedUntil ? { bannedUntil } : {}),
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
async unbanKey(
|
|
855
|
+
access: AccessDecision | undefined,
|
|
856
|
+
keyId: string,
|
|
857
|
+
): Promise<ApiKeyResponse> {
|
|
858
|
+
const authorized = requireTenantScope(access, "dashboard:write");
|
|
859
|
+
const current = await config.store.get(authorized.tenantId, keyId);
|
|
860
|
+
if (!current || current.revokedAt !== undefined)
|
|
861
|
+
throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
862
|
+
const unbanned = await config.store.unban(authorized.tenantId, keyId);
|
|
863
|
+
if (!unbanned) throw new ConsoleDomainError("key_not_found", 404, "Key not found");
|
|
864
|
+
invalidateApiKeyCache();
|
|
865
|
+
await config.auditSink?.record({
|
|
866
|
+
access: authorized,
|
|
867
|
+
action: "api_key.unbanned",
|
|
868
|
+
target: keyId,
|
|
869
|
+
});
|
|
870
|
+
// Unban clears all three ban columns and re-enables the key; derive the
|
|
871
|
+
// response from the pre-fetch instead of re-fetching after the write.
|
|
872
|
+
const { bannedAt: _bannedAt, banReason: _banReason, bannedUntil: _bannedUntil, ...currentRest } = current;
|
|
873
|
+
const unbannedRecord: ApiKeyRecord = { ...currentRest, enabled: true };
|
|
874
|
+
return sanitizeApiKeyResponse(unbannedRecord);
|
|
875
|
+
},
|
|
778
876
|
};
|
|
779
877
|
return operations;
|
|
780
878
|
}
|
|
@@ -793,6 +891,7 @@ const apiKeyBody = t.Object({
|
|
|
793
891
|
monthlyTokenLimit: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
794
892
|
lifetimeTokenBudget: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
795
893
|
maxConcurrentRequests: t.Optional(t.Union([t.Number(), t.Null()])),
|
|
894
|
+
concurrencyScope: t.Optional(literalUnion(API_KEY_CONCURRENCY_SCOPES)),
|
|
796
895
|
modelPrefix: t.Optional(t.String()),
|
|
797
896
|
modelAccessMode: t.Optional(literalUnion(API_KEY_MODEL_ACCESS_MODES)),
|
|
798
897
|
modelList: t.Optional(t.Array(t.String())),
|
|
@@ -830,7 +929,7 @@ function jsonNull(): Response {
|
|
|
830
929
|
});
|
|
831
930
|
}
|
|
832
931
|
/** Creates tenant-bound API-key routes with request-local authorization. */
|
|
833
|
-
export function createApiKeyRoutes(config: ApiKeyConfig):
|
|
932
|
+
export function createApiKeyRoutes(config: ApiKeyConfig): AnyElysia {
|
|
834
933
|
const factory = createApiKeyOperations(config);
|
|
835
934
|
return new Elysia({ prefix: "/api-keys" })
|
|
836
935
|
.get("/", async ({ request, set }) => {
|
|
@@ -950,6 +1049,32 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
950
1049
|
return errorResponse(error, set, "API-key operation failed");
|
|
951
1050
|
}
|
|
952
1051
|
})
|
|
1052
|
+
.post(
|
|
1053
|
+
"/:keyId/ban",
|
|
1054
|
+
{
|
|
1055
|
+
body: t.Object({
|
|
1056
|
+
reason: t.String(),
|
|
1057
|
+
// `reasonLabel` is deliberately not accepted: the audit label is
|
|
1058
|
+
// derived server-side from `reason` (see `banKey`), so a client
|
|
1059
|
+
// cannot author the human-readable audit entry.
|
|
1060
|
+
minutes: t.Optional(t.Union([t.Number({ minimum: 1 }), t.Null()])),
|
|
1061
|
+
}),
|
|
1062
|
+
},
|
|
1063
|
+
async ({ request, params, body, set }) => {
|
|
1064
|
+
try {
|
|
1065
|
+
return await factory.banKey(config.accessResolver(request), params.keyId, body);
|
|
1066
|
+
} catch (error) {
|
|
1067
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
1068
|
+
}
|
|
1069
|
+
},
|
|
1070
|
+
)
|
|
1071
|
+
.post("/:keyId/unban", async ({ request, params, set }) => {
|
|
1072
|
+
try {
|
|
1073
|
+
return await factory.unbanKey(config.accessResolver(request), params.keyId);
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
1076
|
+
}
|
|
1077
|
+
})
|
|
953
1078
|
.get("/:keyId/share", async ({ request, params, set }) => {
|
|
954
1079
|
try {
|
|
955
1080
|
const share = await factory.getShare(config.accessResolver(request), params.keyId, {
|
|
@@ -988,5 +1113,5 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
988
1113
|
} catch (error) {
|
|
989
1114
|
return errorResponse(error, set, "API-key operation failed");
|
|
990
1115
|
}
|
|
991
|
-
})
|
|
1116
|
+
});
|
|
992
1117
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Elysia, t } from "elysia";
|
|
1
|
+
import { Elysia, t, type AnyElysia } from "elysia";
|
|
2
2
|
import { errorResponse, requireGlobalAdmin, requireTenantScope } from "../shared/errors";
|
|
3
3
|
import { parseQueryLimit } from "../shared/query";
|
|
4
4
|
import type { AuditRoutesConfig } from "./contracts";
|
|
@@ -10,7 +10,7 @@ const listQuery = t.Object({
|
|
|
10
10
|
actor: t.Optional(t.String()),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
export function createAuditRoutes(config: AuditRoutesConfig):
|
|
13
|
+
export function createAuditRoutes(config: AuditRoutesConfig): AnyElysia {
|
|
14
14
|
return new Elysia().get("/audit", { query: listQuery }, async ({ request, query, set }) => {
|
|
15
15
|
try {
|
|
16
16
|
const resolved = config.accessResolver(request);
|
|
@@ -28,5 +28,5 @@ export function createAuditRoutes(config: AuditRoutesConfig): Elysia {
|
|
|
28
28
|
} catch (e) {
|
|
29
29
|
return errorResponse(e, set, "Audit read failed");
|
|
30
30
|
}
|
|
31
|
-
})
|
|
31
|
+
});
|
|
32
32
|
}
|
|
@@ -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. */
|
|
@@ -42,12 +42,12 @@ export function createConsoleAccountResetHandler(
|
|
|
42
42
|
return async ({ request, body }) => {
|
|
43
43
|
if (!isLoopbackPeer(deps.resolvePeerAddress(request)))
|
|
44
44
|
throw new GatewayError(
|
|
45
|
-
"
|
|
45
|
+
"policy_rejected",
|
|
46
46
|
403,
|
|
47
|
-
"console account reset is only available from loopback",
|
|
47
|
+
"console account reset is only available from loopback (request {requestId}). This operation is only allowed from a trusted loopback connection — no hard feelings 🫂",
|
|
48
48
|
);
|
|
49
49
|
if (!tokenMatches(request.headers.get("x-drain-token"), deps.token))
|
|
50
|
-
throw new GatewayError("
|
|
50
|
+
throw new GatewayError("authentication_failed", 401, "invalid reset token (request {requestId}). Wrong key, right door — check the token 🔑");
|
|
51
51
|
const confirm =
|
|
52
52
|
body !== null && typeof body === "object" && "confirm" in body
|
|
53
53
|
? body.confirm
|
|
@@ -56,7 +56,7 @@ export function createConsoleAccountResetHandler(
|
|
|
56
56
|
throw new GatewayError(
|
|
57
57
|
"invalid_request",
|
|
58
58
|
400,
|
|
59
|
-
`send {"confirm":"${CONFIRMATION}"} to confirm; this deletes every console account
|
|
59
|
+
`send {"confirm":"${CONFIRMATION}"} to confirm (request {requestId}); this deletes every console account. Make sure you mean it — there's no undo 🫂`,
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
const removed = await deps.resetConsoleAccount();
|
|
@@ -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> {
|