cartethyia 2.4.5 → 2.5.0
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 +4 -1
- package/README.md +63 -13
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
- package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
- package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
- package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
- package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
- package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
- package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
- package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
- package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
- package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
- package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
- package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
- package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
- package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
- package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
- package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
- package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
- package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
- package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
- package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
- package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
- package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
- package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
- package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
- package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
- package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
- package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
- package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
- package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
- package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
- package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
- package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
- package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
- package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
- package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
- package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
- package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
- package/dist/dashboard/assets/providers/cursor.webp +0 -0
- package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
- package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
- package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
- package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
- package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
- package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
- package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
- package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
- package/dist/dashboard/assets/providers/trae.webp +0 -0
- package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
- package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
- package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
- package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
- package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
- package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
- package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
- package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
- package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
- package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
- package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
- package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
- package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
- package/dist/dashboard/index.html +1 -1
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +122 -0
- package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
- package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
- package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
- package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
- package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
- package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
- package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
- package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
- package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
- package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
- package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
- package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
- package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
- package/package.json +26 -30
- package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
- package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
- package/scripts/cli/command/admin.ts +65 -0
- package/scripts/cli/command/console-menu.ts +246 -0
- package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
- package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
- package/scripts/cli/command/start.ts +77 -0
- package/scripts/cli/command/tray.ts +165 -0
- package/scripts/cli/command/update.ts +23 -0
- package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
- package/scripts/cli/gateway/instance-lock.ts +105 -0
- package/scripts/cli/gateway/spawn.ts +173 -0
- package/scripts/cli/gateway/stop.ts +51 -0
- package/scripts/cli/main.ts +94 -0
- package/scripts/cli/support/bun.ts +27 -0
- package/scripts/cli/support/env.ts +99 -0
- package/scripts/cli/support/platform.ts +64 -0
- package/scripts/cli/support/services.ts +88 -0
- package/scripts/cli/support/startup-signal.ts +23 -0
- package/scripts/cli/{version.ts → support/version.ts} +1 -1
- package/scripts/cli/tray/hidden-window.vbs +28 -0
- package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
- package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
- package/scripts/cli/ui/background.ts +110 -0
- package/src/boot/app.ts +23 -7
- package/src/boot/dependencies.ts +55 -13
- package/src/boot/elysia-typebox-bridge.ts +37 -0
- package/src/boot/lifecycle.ts +2 -0
- package/src/config/cooldowns.ts +138 -138
- package/src/config/providers.ts +31 -5
- package/src/config/runtime.ts +19 -3
- package/src/config/spec.ts +20 -0
- package/src/console/api-keys/contracts.ts +414 -391
- package/src/console/api-keys/routes.ts +880 -870
- package/src/console/auth/account-reset-store.ts +14 -0
- package/src/console/auth/account-reset.ts +3 -2
- package/src/console/auth/setup-service.ts +6 -3
- package/src/console/backup/routes.ts +8 -0
- package/src/console/cli-tools/contracts.ts +6 -6
- package/src/console/console-router.ts +5 -3
- package/src/console/dashboard-assets.ts +81 -11
- package/src/console/domain-registration.ts +9 -2
- package/src/console/observability/contracts.ts +475 -423
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +1015 -912
- package/src/console/providers/catalog/contracts.ts +31 -0
- package/src/console/providers/catalog/provider-operations.ts +8 -0
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +1532 -1521
- package/src/console/providers/oauth/callback-listener.ts +18 -3
- package/src/console/providers/oauth/routes.ts +38 -15
- package/src/console/quota/account-quota-tenant.ts +9 -6
- package/src/console/quota/refresh.ts +19 -0
- package/src/console/response-compression.ts +69 -2
- package/src/console/routing/pools/probe-verdict.ts +72 -0
- package/src/console/share/share-router.ts +7 -4
- package/src/console/share/share-stats.ts +12 -5
- package/src/console/shared/read-through-cache.ts +133 -7
- package/src/console/updates/repository-updates.ts +39 -34
- package/src/console/updates/routes.ts +162 -4
- package/src/main.ts +3 -0
- package/src/network/http2-bidi.ts +85 -0
- package/src/network/outbound-fetch.ts +87 -24
- package/src/network/pool/agent.ts +478 -460
- package/src/network/pool/resolver.ts +1104 -544
- package/src/network/pool-health-machine.ts +12 -5
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/payload-capture.ts +344 -324
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/db-pg.ts +9 -3
- package/src/persistence/db-sqlite.ts +497 -495
- package/src/persistence/health-event-flush.ts +137 -0
- package/src/persistence/migrate.ts +181 -129
- package/src/persistence/schema.ts +1360 -1257
- package/src/persistence/settings-revision.ts +25 -0
- package/src/persistence/shared-cache.ts +217 -0
- package/src/persistence/stores/share-store.ts +556 -528
- package/src/persistence/usage-flush.ts +61 -1
- package/src/protocol/canonical-model.ts +23 -1
- package/src/protocol/response/chat.ts +143 -10
- package/src/protocol/response/codex.ts +9 -75
- package/src/protocol/response/omp-tool-call-markup.ts +221 -0
- package/src/protocol/tool-identity.ts +43 -9
- package/src/providers/authentication/jwt-validator.ts +130 -187
- package/src/providers/authentication/oauth-flow-store.ts +16 -9
- package/src/providers/default-registry.ts +177 -1
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/openai-model-discovery.ts +39 -3
- package/src/providers/discovery/openrouter-free-roster.ts +98 -0
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/discovery/probing-service.ts +1022 -1001
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
- package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
- package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
- package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/claude/claude.ts +606 -582
- package/src/providers/integrations/cline/cline-quota.ts +18 -16
- package/src/providers/integrations/cline/cline.ts +539 -468
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/commandcode/commandcode.ts +675 -541
- package/src/providers/integrations/commandcode/discovery.ts +116 -0
- package/src/providers/integrations/commandcode/quota.ts +160 -0
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
- package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
- package/src/providers/integrations/cursor/cursor-request.ts +471 -0
- package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
- package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
- package/src/providers/integrations/cursor/cursor.ts +960 -0
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
- package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/github/github.ts +4 -4
- package/src/providers/integrations/hermes.ts +79 -8
- package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
- package/src/providers/integrations/kilo/kilo.ts +153 -146
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +532 -499
- package/src/providers/integrations/openrouter/provider.ts +139 -281
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
- package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
- package/src/providers/integrations/qoder/qoder.ts +865 -730
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/integrations/trae/trae-catalog.ts +190 -0
- package/src/providers/integrations/trae/trae-headers.ts +149 -0
- package/src/providers/integrations/trae/trae-oauth.ts +773 -0
- package/src/providers/integrations/trae/trae-protocol.ts +400 -0
- package/src/providers/integrations/trae/trae-realm.ts +220 -0
- package/src/providers/integrations/trae/trae.ts +458 -0
- package/src/providers/operations/account-health-service.ts +107 -10
- package/src/providers/operations/client-versions.ts +143 -6
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-seeder.ts +95 -86
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/providers/operations/provider-credential-service.ts +46 -7
- package/src/providers/provider-contracts.ts +76 -0
- package/src/providers/provider-metadata.ts +78 -6
- package/src/providers/provider-model-definition.ts +373 -349
- package/src/security/api-key-auth.ts +126 -8
- package/src/transport/contracts.ts +3 -0
- package/src/transport/dispatch/attempt-finalize.ts +39 -2
- package/src/transport/dispatch/attempt-loop.ts +530 -513
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/dispatch/generation.ts +88 -21
- package/src/transport/dispatch/leases.ts +99 -2
- package/src/transport/dispatch/proxy-request.ts +60 -3
- package/src/transport/dispatch/streaming/finalize.ts +16 -1
- package/src/transport/dispatch/streaming/stream.ts +22 -2
- package/src/transport/dispatch/upstream.ts +19 -1
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/guards/auth-guard.ts +26 -1
- package/src/transport/guards/body-policy.ts +5 -4
- package/src/transport/request/preparer.ts +103 -0
- package/src/transport/request/state.ts +14 -0
- package/src/transport/routing/plan-gate.ts +45 -0
- package/src/transport/routing/route-catalog.ts +799 -738
- package/src/transport/routing/route-model.ts +52 -1
- package/src/transport/routing/router.ts +51 -0
- package/src/transport/surface/chat/encode.ts +18 -31
- package/src/transport/surface/messages/stream.ts +3 -2
- package/src/transport/translation/tool-repair.ts +37 -16
- package/src/workers/account-identity-sweep.ts +14 -12
- package/src/workers/daily-checkin.ts +62 -0
- package/src/workers/quota-refresh-sweep.ts +27 -2
- package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
- package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
- package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
- package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
- package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
- package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
- package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
- package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
- package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
- package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
- package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
- package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
- package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
- package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
- package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
- package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
- package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
- package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
- package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
- package/scripts/cli/cli.ts +0 -771
- package/scripts/operations/probe-egress.ts +0 -335
- package/scripts/operations/restart.ts +0 -120
- package/scripts/shared/env.ts +0 -269
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
- /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
- /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
- /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
|
@@ -13,47 +13,46 @@
|
|
|
13
13
|
* Only asymmetric algorithms are accepted. `none` and HMAC (`HS*`) are
|
|
14
14
|
* rejected so a token cannot downgrade to a shared-secret or unsigned form.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
|
|
16
|
+
import {
|
|
17
|
+
compactVerify,
|
|
18
|
+
createRemoteJWKSet,
|
|
19
|
+
customFetch,
|
|
20
|
+
decodeJwt,
|
|
21
|
+
decodeProtectedHeader,
|
|
22
|
+
} from "jose";
|
|
23
|
+
import type { JSONWebKeySet, RemoteJWKSet } from "jose";
|
|
18
24
|
import { TtlCache } from "../../persistence/ttl-cache";
|
|
19
25
|
import { resolveRedisClient } from "../../persistence/redis";
|
|
20
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Algorithms this module will verify.
|
|
29
|
+
*
|
|
30
|
+
* `jose` performs the cryptography, so the entry is now only the key type —
|
|
31
|
+
* the digest, RSA-PSS padding and salt length were implementation details of
|
|
32
|
+
* the hand-rolled `cryptoVerify` call this replaced. The allowlist itself is
|
|
33
|
+
* unchanged: asymmetric only, so `none` and the HMAC (`HS*`) family can never
|
|
34
|
+
* be negotiated down to.
|
|
35
|
+
*/
|
|
21
36
|
interface AlgorithmSpec {
|
|
22
|
-
readonly hash: string;
|
|
23
37
|
readonly kty: "RSA" | "EC";
|
|
24
|
-
/** RSA-PSS padding, when the algorithm is PS*; RSA-PKCS1 is the default. */
|
|
25
|
-
readonly padding?: number;
|
|
26
|
-
readonly saltLength?: number;
|
|
27
38
|
}
|
|
28
39
|
|
|
29
|
-
/** Algorithms this module will verify, mapped to their digest and key type. */
|
|
30
40
|
const SUPPORTED_ALGORITHMS: Readonly<Record<string, AlgorithmSpec>> = Object.freeze({
|
|
31
|
-
RS256: {
|
|
32
|
-
RS384: {
|
|
33
|
-
RS512: {
|
|
34
|
-
PS256: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
PS384: {
|
|
41
|
-
hash: "sha384",
|
|
42
|
-
kty: "RSA",
|
|
43
|
-
padding: constants.RSA_PKCS1_PSS_PADDING,
|
|
44
|
-
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
|
|
45
|
-
},
|
|
46
|
-
PS512: {
|
|
47
|
-
hash: "sha512",
|
|
48
|
-
kty: "RSA",
|
|
49
|
-
padding: constants.RSA_PKCS1_PSS_PADDING,
|
|
50
|
-
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
|
|
51
|
-
},
|
|
52
|
-
ES256: { hash: "sha256", kty: "EC" },
|
|
53
|
-
ES384: { hash: "sha384", kty: "EC" },
|
|
54
|
-
ES512: { hash: "sha512", kty: "EC" },
|
|
41
|
+
RS256: { kty: "RSA" },
|
|
42
|
+
RS384: { kty: "RSA" },
|
|
43
|
+
RS512: { kty: "RSA" },
|
|
44
|
+
PS256: { kty: "RSA" },
|
|
45
|
+
PS384: { kty: "RSA" },
|
|
46
|
+
PS512: { kty: "RSA" },
|
|
47
|
+
ES256: { kty: "EC" },
|
|
48
|
+
ES384: { kty: "EC" },
|
|
49
|
+
ES512: { kty: "EC" },
|
|
55
50
|
});
|
|
56
51
|
|
|
52
|
+
/** Fetch timeout for a JWKS document, matching jose's own default. */
|
|
53
|
+
const JWKS_FETCH_TIMEOUT_MS = 5_000;
|
|
54
|
+
|
|
55
|
+
|
|
57
56
|
interface ParsedJwt {
|
|
58
57
|
readonly header: Record<string, unknown>;
|
|
59
58
|
readonly payload: Record<string, unknown>;
|
|
@@ -61,23 +60,17 @@ interface ParsedJwt {
|
|
|
61
60
|
readonly signedInput: string;
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function decodeJsonObject(segment: string): Record<string, unknown> | undefined {
|
|
65
|
-
if (segment.length === 0 || !/^[A-Za-z0-9_-]+$/.test(segment)) return undefined;
|
|
66
|
-
try {
|
|
67
|
-
const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8")) as unknown;
|
|
68
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
69
|
-
? (value as Record<string, unknown>)
|
|
70
|
-
: undefined;
|
|
71
|
-
} catch {
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
63
|
/**
|
|
77
64
|
* Strictly parses a compact JWS. Returns `undefined` for anything that is not a
|
|
78
65
|
* three-part token with JSON header/payload objects, a non-empty `alg`, and a
|
|
79
66
|
* non-empty signature — so `alg: none` and structurally broken tokens are never
|
|
80
67
|
* treated as JWTs.
|
|
68
|
+
*
|
|
69
|
+
* Header and payload decoding delegates to `jose`, which enforces the same
|
|
70
|
+
* strictness the hand-rolled decoder did (compact three-part shape, base64url
|
|
71
|
+
* decoding to a JSON *object* — never an array or a scalar) and is the same
|
|
72
|
+
* decoder the verification path uses, so what is inspected is exactly what is
|
|
73
|
+
* later verified.
|
|
81
74
|
*/
|
|
82
75
|
export function parseJwt(token: string): ParsedJwt | undefined {
|
|
83
76
|
if (typeof token !== "string") return undefined;
|
|
@@ -86,9 +79,14 @@ export function parseJwt(token: string): ParsedJwt | undefined {
|
|
|
86
79
|
const headerSegment = parts[0] ?? "";
|
|
87
80
|
const payloadSegment = parts[1] ?? "";
|
|
88
81
|
const signature = parts[2] ?? "";
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
82
|
+
let header: Record<string, unknown>;
|
|
83
|
+
let payload: Record<string, unknown>;
|
|
84
|
+
try {
|
|
85
|
+
header = decodeProtectedHeader(token) as Record<string, unknown>;
|
|
86
|
+
payload = decodeJwt(token) as Record<string, unknown>;
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
92
90
|
if (typeof header.alg !== "string" || header.alg.length === 0) return undefined;
|
|
93
91
|
if (header.alg === "none" || signature.length === 0) return undefined;
|
|
94
92
|
return { header, payload, signature, signedInput: `${headerSegment}.${payloadSegment}` };
|
|
@@ -167,14 +165,6 @@ export function validateJwtClaims(
|
|
|
167
165
|
return undefined;
|
|
168
166
|
}
|
|
169
167
|
|
|
170
|
-
interface Jwk {
|
|
171
|
-
readonly kty?: unknown;
|
|
172
|
-
readonly kid?: unknown;
|
|
173
|
-
readonly use?: unknown;
|
|
174
|
-
readonly alg?: unknown;
|
|
175
|
-
readonly [key: string]: unknown;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
168
|
interface JwksVerifierOptions {
|
|
179
169
|
readonly url: string;
|
|
180
170
|
readonly fetch?: typeof fetch;
|
|
@@ -186,191 +176,144 @@ interface JwksVerifierOptions {
|
|
|
186
176
|
|
|
187
177
|
const DEFAULT_JWKS_CACHE_TTL_MS = 5 * 60 * 1_000;
|
|
188
178
|
const DEFAULT_MIN_REFRESH_INTERVAL_MS = 30_000;
|
|
189
|
-
const MAX_JWKS_KEYS = 64;
|
|
190
179
|
|
|
191
180
|
/** Shared JWKS window, matching the local verifier cache so peers reuse fetched keys. */
|
|
192
181
|
const JWKS_SHARED_TTL_SECONDS = 300;
|
|
193
182
|
const JWKS_SHARED_KEY_PREFIX = "jwks:v1:";
|
|
194
183
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return undefined;
|
|
204
|
-
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* A JWKS document shared with peer instances, with the timestamp it was
|
|
187
|
+
* fetched at. Redis-backed so a fleet does not each re-fetch the issuer.
|
|
188
|
+
*/
|
|
189
|
+
interface SharedJwks {
|
|
190
|
+
readonly jwks: JSONWebKeySet;
|
|
191
|
+
readonly uat: number;
|
|
205
192
|
}
|
|
206
193
|
|
|
207
|
-
|
|
208
|
-
async function readSharedJwks(url: string): Promise<readonly Jwk[] | undefined> {
|
|
194
|
+
async function readSharedJwksDocument(url: string): Promise<SharedJwks | undefined> {
|
|
209
195
|
const redis = resolveRedisClient();
|
|
210
196
|
if (!redis) return undefined;
|
|
211
197
|
try {
|
|
212
198
|
const hit = await redis.get(`${JWKS_SHARED_KEY_PREFIX}${url}`);
|
|
213
|
-
|
|
199
|
+
if (hit === null) return undefined;
|
|
200
|
+
const parsed = JSON.parse(hit) as unknown;
|
|
201
|
+
if (parsed === null || typeof parsed !== "object") return undefined;
|
|
202
|
+
const { uat, jwks } = parsed as { uat?: unknown; jwks?: unknown };
|
|
203
|
+
if (typeof uat !== "number" || !Number.isFinite(uat)) return undefined;
|
|
204
|
+
if (jwks === null || typeof jwks !== "object") return undefined;
|
|
205
|
+
return { jwks: jwks as JSONWebKeySet, uat };
|
|
214
206
|
} catch {
|
|
215
207
|
return undefined;
|
|
216
208
|
}
|
|
217
209
|
}
|
|
218
210
|
|
|
219
|
-
/** Publishes fetched
|
|
220
|
-
async function
|
|
211
|
+
/** Publishes a fetched document for peer instances. Best-effort: never throws. */
|
|
212
|
+
async function writeSharedJwksDocument(url: string, jwks: JSONWebKeySet): Promise<void> {
|
|
221
213
|
const redis = resolveRedisClient();
|
|
222
214
|
if (!redis) return;
|
|
223
215
|
try {
|
|
224
216
|
await redis.set(
|
|
225
217
|
`${JWKS_SHARED_KEY_PREFIX}${url}`,
|
|
226
|
-
JSON.stringify(
|
|
218
|
+
JSON.stringify({ uat: Date.now(), jwks } satisfies SharedJwks),
|
|
227
219
|
"EX",
|
|
228
220
|
JWKS_SHARED_TTL_SECONDS,
|
|
229
221
|
);
|
|
230
222
|
} catch {
|
|
231
|
-
// Shared visibility only;
|
|
223
|
+
// Shared visibility only; each instance's own cache stays authoritative.
|
|
232
224
|
}
|
|
233
225
|
}
|
|
234
226
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function publicKeyFromJwk(jwk: Jwk): KeyObject | undefined {
|
|
241
|
-
try {
|
|
242
|
-
return createPublicKey({ key: jwk as unknown as JsonWebKey, format: "jwk" });
|
|
243
|
-
} catch {
|
|
244
|
-
return undefined;
|
|
245
|
-
}
|
|
227
|
+
function jsonResponse(body: unknown): Response {
|
|
228
|
+
return new Response(JSON.stringify(body), {
|
|
229
|
+
status: 200,
|
|
230
|
+
headers: { "content-type": "application/json" },
|
|
231
|
+
});
|
|
246
232
|
}
|
|
247
233
|
|
|
248
234
|
/**
|
|
249
|
-
* Verifies compact-JWS signatures against a provider's JWKS endpoint.
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
235
|
+
* Verifies compact-JWS signatures against a provider's JWKS endpoint.
|
|
236
|
+
*
|
|
237
|
+
* Key fetching, caching, in-flight deduplication, the minimum refresh interval
|
|
238
|
+
* on an unknown `kid`, and `kid`-rotation pickup are all delegated to
|
|
239
|
+
* `jose`'s `createRemoteJWKSet` — the same algorithm this class used to
|
|
240
|
+
* reimplement by hand. `jose` additionally honours each JWK's `use` and
|
|
241
|
+
* `key_ops`, so an encryption key published alongside the signing keys cannot
|
|
242
|
+
* be selected for verification.
|
|
243
|
+
*
|
|
244
|
+
* The algorithm is checked against `SUPPORTED_ALGORITHMS` *before* any key is
|
|
245
|
+
* resolved, so the asymmetric-only allowlist (no `none`, no HMAC) is
|
|
246
|
+
* unchanged by the migration.
|
|
253
247
|
*/
|
|
254
248
|
export class JwksVerifier {
|
|
255
|
-
readonly #
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
readonly #cacheTtlMs: number;
|
|
259
|
-
readonly #minRefreshIntervalMs: number;
|
|
260
|
-
#cache: CachedJwks | undefined;
|
|
261
|
-
#inflight: Promise<readonly Jwk[]> | undefined;
|
|
262
|
-
#lastRefreshAt = 0;
|
|
249
|
+
readonly #keys: RemoteJWKSet;
|
|
250
|
+
/** Whether this instance has fetched from the issuer yet. */
|
|
251
|
+
#fetched = false;
|
|
263
252
|
|
|
264
253
|
constructor(options: JwksVerifierOptions) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.#
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
254
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
255
|
+
const url = options.url;
|
|
256
|
+
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_JWKS_CACHE_TTL_MS;
|
|
257
|
+
this.#keys = createRemoteJWKSet(new URL(url), {
|
|
258
|
+
timeoutDuration: JWKS_FETCH_TIMEOUT_MS,
|
|
259
|
+
cacheMaxAge: cacheTtlMs,
|
|
260
|
+
cooldownDuration: options.minRefreshIntervalMs ?? DEFAULT_MIN_REFRESH_INTERVAL_MS,
|
|
261
|
+
[customFetch]: async (target, init) => {
|
|
262
|
+
// First resolution only: a peer's copy saves an issuer round-trip.
|
|
263
|
+
// A later fetch is a rotation reload, which must reach the issuer
|
|
264
|
+
// rather than re-read the set that just failed to match the `kid`.
|
|
265
|
+
if (!this.#fetched) {
|
|
266
|
+
const shared = await readSharedJwksDocument(url);
|
|
267
|
+
if (shared !== undefined && Date.now() - shared.uat < cacheTtlMs) {
|
|
268
|
+
this.#fetched = true;
|
|
269
|
+
return jsonResponse(shared.jwks);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const response = await fetchFn(target, init as RequestInit);
|
|
273
|
+
if (response.ok) {
|
|
274
|
+
try {
|
|
275
|
+
const body = (await response.clone().json()) as unknown;
|
|
276
|
+
if (body !== null && typeof body === "object" && Array.isArray((body as { keys?: unknown }).keys)) {
|
|
277
|
+
void writeSharedJwksDocument(url, body as JSONWebKeySet);
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// Publishing is best-effort.
|
|
281
|
+
}
|
|
282
|
+
return response;
|
|
283
|
+
}
|
|
284
|
+
return response;
|
|
285
|
+
},
|
|
276
286
|
});
|
|
277
|
-
if (!response.ok) throw new Error(`JWKS request failed (${response.status})`);
|
|
278
|
-
const body = (await response.json()) as unknown;
|
|
279
|
-
const keys = body !== null && typeof body === "object" ? (body as { keys?: unknown }).keys : undefined;
|
|
280
|
-
if (!Array.isArray(keys)) throw new Error("JWKS response missing keys array");
|
|
281
|
-
return keys.slice(0, MAX_JWKS_KEYS).filter(
|
|
282
|
-
(key): key is Jwk => key !== null && typeof key === "object",
|
|
283
|
-
);
|
|
284
287
|
}
|
|
285
288
|
|
|
286
289
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
290
|
+
* Verifies `token`'s signature and returns the parsed token on success.
|
|
291
|
+
*
|
|
292
|
+
* Returns `undefined` for anything unverifiable — a non-JWT, an algorithm
|
|
293
|
+
* outside the allowlist, a JWKS fetch failure, or a bad signature. A JWKS
|
|
294
|
+
* outage therefore fails closed rather than trusting an unverified token.
|
|
291
295
|
*/
|
|
292
|
-
async #fetchKeys(useSharedCache: boolean): Promise<readonly Jwk[]> {
|
|
293
|
-
if (useSharedCache) {
|
|
294
|
-
const shared = await readSharedJwks(this.#url);
|
|
295
|
-
if (shared !== undefined) return shared;
|
|
296
|
-
}
|
|
297
|
-
const keys = await this.#fetchKeysRemote();
|
|
298
|
-
await writeSharedJwks(this.#url, keys);
|
|
299
|
-
return keys;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async #getKeys(forceRefresh: boolean): Promise<readonly Jwk[]> {
|
|
303
|
-
const now = this.#clock();
|
|
304
|
-
if (!forceRefresh && this.#cache && now - this.#cache.fetchedAt < this.#cacheTtlMs) {
|
|
305
|
-
return this.#cache.keys;
|
|
306
|
-
}
|
|
307
|
-
if (this.#inflight) return this.#inflight;
|
|
308
|
-
const request = this.#fetchKeys(!forceRefresh)
|
|
309
|
-
.then((keys) => {
|
|
310
|
-
this.#cache = { keys, fetchedAt: this.#clock() };
|
|
311
|
-
this.#lastRefreshAt = this.#clock();
|
|
312
|
-
return keys;
|
|
313
|
-
})
|
|
314
|
-
.finally(() => {
|
|
315
|
-
this.#inflight = undefined;
|
|
316
|
-
});
|
|
317
|
-
this.#inflight = request;
|
|
318
|
-
return request;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/** Verifies `token`'s signature and returns the parsed token on success. */
|
|
322
296
|
async verify(token: string): Promise<ParsedJwt | undefined> {
|
|
323
297
|
const parsed = parseJwt(token);
|
|
324
298
|
if (!parsed) return undefined;
|
|
325
299
|
const algorithm = SUPPORTED_ALGORITHMS[parsed.header.alg as string];
|
|
326
300
|
if (!algorithm) return undefined;
|
|
327
|
-
|
|
328
|
-
const kid = typeof parsed.header.kid === "string" ? parsed.header.kid : undefined;
|
|
329
|
-
let keys: readonly Jwk[];
|
|
330
301
|
try {
|
|
331
|
-
|
|
302
|
+
// `compactVerify` checks the signature only. Registered claims stay the
|
|
303
|
+
// responsibility of `validateJwtClaims`, whose failure vocabulary
|
|
304
|
+
// ("expired", "issuer_mismatch", …) the console surfaces verbatim;
|
|
305
|
+
// `jwtVerify` would replace it with its own, and requires `iss`/`aud`
|
|
306
|
+
// that an OAuth access token need not carry.
|
|
307
|
+
await compactVerify(token, this.#keys, {
|
|
308
|
+
algorithms: [parsed.header.alg as string],
|
|
309
|
+
});
|
|
310
|
+
return parsed;
|
|
332
311
|
} catch {
|
|
333
312
|
return undefined;
|
|
334
313
|
}
|
|
335
|
-
let candidates = selectKeys(keys, kid, algorithm.kty);
|
|
336
|
-
if (candidates.length === 0 && this.#clock() - this.#lastRefreshAt >= this.#minRefreshIntervalMs) {
|
|
337
|
-
try {
|
|
338
|
-
candidates = selectKeys(await this.#getKeys(true), kid, algorithm.kty);
|
|
339
|
-
} catch {
|
|
340
|
-
return undefined;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (candidates.length === 0) return undefined;
|
|
344
|
-
|
|
345
|
-
const data = Buffer.from(parsed.signedInput, "utf8");
|
|
346
|
-
const signature = Buffer.from(parsed.signature, "base64url");
|
|
347
|
-
for (const jwk of candidates) {
|
|
348
|
-
const key = publicKeyFromJwk(jwk);
|
|
349
|
-
if (!key) continue;
|
|
350
|
-
const verifyOptions =
|
|
351
|
-
algorithm.kty === "EC"
|
|
352
|
-
? { key, dsaEncoding: "ieee-p1363" as const }
|
|
353
|
-
: algorithm.padding === undefined || algorithm.saltLength === undefined
|
|
354
|
-
? { key }
|
|
355
|
-
: { key, padding: algorithm.padding, saltLength: algorithm.saltLength };
|
|
356
|
-
try {
|
|
357
|
-
if (cryptoVerify(algorithm.hash, data, verifyOptions, signature)) return parsed;
|
|
358
|
-
} catch {
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return undefined;
|
|
363
314
|
}
|
|
364
315
|
}
|
|
365
316
|
|
|
366
|
-
function selectKeys(keys: readonly Jwk[], kid: string | undefined, kty: "RSA" | "EC"): readonly Jwk[] {
|
|
367
|
-
return keys.filter((jwk) => {
|
|
368
|
-
if (jwk.kty !== kty) return false;
|
|
369
|
-
if (kid !== undefined && jwk.kid !== kid) return false;
|
|
370
|
-
return true;
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
|
|
374
317
|
/**
|
|
375
318
|
* One verifier per URL, bounded and time-limited.
|
|
376
319
|
*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// OAuth client contracts, registries, PKCE state, and ephemeral flow storage.
|
|
2
2
|
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { base64url, decodeJwt } from "jose";
|
|
4
5
|
import type { RedisClient } from "../../persistence/redis";
|
|
5
6
|
import { TtlCache } from "../../persistence/ttl-cache";
|
|
6
7
|
|
|
@@ -286,8 +287,14 @@ interface PkcePair {
|
|
|
286
287
|
readonly codeChallenge: string;
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Base64url without padding, via `jose`.
|
|
292
|
+
*
|
|
293
|
+
* Shared so PKCE verifiers/challenges and provider-specific encoders cannot
|
|
294
|
+
* drift apart on padding or alphabet handling.
|
|
295
|
+
*/
|
|
289
296
|
export function base64UrlEncode(input: Buffer): string {
|
|
290
|
-
return
|
|
297
|
+
return base64url.encode(input);
|
|
291
298
|
}
|
|
292
299
|
|
|
293
300
|
/** Generates a fresh PKCE verifier/challenge pair using the S256 transform. */
|
|
@@ -398,17 +405,17 @@ export function expiryFromSeconds(value: unknown, fallbackSec = 3600): Date {
|
|
|
398
405
|
|
|
399
406
|
/**
|
|
400
407
|
* Decodes a JWT payload without verifying (OAuth metadata only — the trust
|
|
401
|
-
* boundary stays at TLS + issuer).
|
|
402
|
-
*
|
|
408
|
+
* boundary stays at TLS + issuer).
|
|
409
|
+
*
|
|
410
|
+
* Delegates to `jose`'s `decodeJwt`, which enforces exactly the contract
|
|
411
|
+
* callers rely on: compact three-part shape, a base64url payload decoding to
|
|
412
|
+
* UTF-8 JSON that is an object (not an array, not a scalar). Anything else
|
|
413
|
+
* yields `undefined` rather than throwing — a credential that is simply not a
|
|
414
|
+
* JWT (an opaque token) is a normal outcome here, not an error.
|
|
403
415
|
*/
|
|
404
416
|
export function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
405
|
-
const parts = token.split(".");
|
|
406
|
-
if (parts.length !== 3) return undefined;
|
|
407
417
|
try {
|
|
408
|
-
|
|
409
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
410
|
-
? (value as Record<string, unknown>)
|
|
411
|
-
: undefined;
|
|
418
|
+
return decodeJwt(token) as Record<string, unknown>;
|
|
412
419
|
} catch {
|
|
413
420
|
return undefined;
|
|
414
421
|
}
|