cartethyia 2.4.5 → 2.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +4 -1
- package/README.md +18 -0
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-DUrXpp-X.js → BannedPage-QhvbJB9o.js} +1 -1
- package/dist/dashboard/assets/BannedPage-QhvbJB9o.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-Bcy150Jo.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bcy150Jo.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-DyNXefph.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DyNXefph.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js +1 -0
- package/dist/dashboard/assets/CombosPage-Cak0ljnO.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-CATpAEOz.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-D6jEU7nL.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-D6jEU7nL.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-CZkpn0a7.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-BJlMVN4U.js} +1 -1
- package/dist/dashboard/assets/LoginPage-BJlMVN4U.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js +1 -0
- package/dist/dashboard/assets/ModelPicker-Br3CXEau.js.gz +0 -0
- package/dist/dashboard/assets/{OverviewPage-C3NIzsDx.js → OverviewPage-CQxXbo2g.js} +1 -1
- package/dist/dashboard/assets/OverviewPage-CQxXbo2g.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage--0RBNh99.js.gz +0 -0
- package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-1M-C3mbu.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js +7 -0
- package/dist/dashboard/assets/ProxyPage-CXiumA53.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BGDIfIYP.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-BHW4Zt06.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-BHW4Zt06.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js +1 -0
- package/dist/dashboard/assets/SetupPage-VOYC_lIa.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js +1 -0
- package/dist/dashboard/assets/SortableList-DpOTnGuH.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js +31 -0
- package/dist/dashboard/assets/StudioPage-BNIDLiU6.js.gz +0 -0
- package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-Djes-Epy.js → UsagePage-BAFaSDQV.js} +14 -14
- package/dist/dashboard/assets/UsagePage-BAFaSDQV.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js +1 -0
- package/dist/dashboard/assets/api-keys-BETfaMYZ.js.gz +0 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css +2 -0
- package/dist/dashboard/assets/app-BLCvGdzb.css.gz +0 -0
- package/dist/dashboard/assets/{app-BDJWMThu.js → app-Cjq7nrwu.js} +3 -3
- package/dist/dashboard/assets/app-Cjq7nrwu.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-Bwh6DJTK.js → assert-7J6_LVD3.js} +1 -1
- package/dist/dashboard/assets/assert-7J6_LVD3.js.gz +0 -0
- package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
- package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-BTyGk_DL.js} +1 -1
- package/dist/dashboard/assets/badge-BTyGk_DL.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-bujTWYM4.js} +1 -1
- package/dist/dashboard/assets/banner-bujTWYM4.js.gz +0 -0
- package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
- package/dist/dashboard/assets/{card-BTdbRFFg.js → card-B0Bnx5CA.js} +1 -1
- package/dist/dashboard/assets/card-B0Bnx5CA.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-DE2ug812.js +1 -0
- package/dist/dashboard/assets/cli-tools-DE2ug812.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.js +1 -0
- package/dist/dashboard/assets/clipboard-button-R_rArH8S.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-BWIP_EvW.js} +1 -1
- package/dist/dashboard/assets/dialog-BWIP_EvW.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-BKcgVugF.js} +1 -1
- package/dist/dashboard/assets/drawer-BKcgVugF.js.gz +0 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js +4 -0
- package/dist/dashboard/assets/entry-7dpgvpJ3.js.gz +0 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js +1 -0
- package/dist/dashboard/assets/entry-CcWkGR4a.js.gz +0 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.js +1 -0
- package/dist/dashboard/assets/entry-kTiTFrvh.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-Booc5mNe.js +1 -0
- package/dist/dashboard/assets/github-badge-Booc5mNe.js.gz +0 -0
- package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
- package/dist/dashboard/assets/{index-C4hXfAMs.js → index-D70pChfn.js} +2 -2
- package/dist/dashboard/assets/index-D70pChfn.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1TCqM2D.js} +1 -1
- package/dist/dashboard/assets/inline-D1TCqM2D.js.gz +0 -0
- package/dist/dashboard/assets/{input-Bn4_kNav.js → input-B9oyU93y.js} +1 -1
- package/dist/dashboard/assets/input-B9oyU93y.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-BArzXirQ.js} +1 -1
- package/dist/dashboard/assets/layout-BArzXirQ.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-BVZzaoHF.js} +1 -1
- package/dist/dashboard/assets/list-row-BVZzaoHF.js.gz +0 -0
- package/dist/dashboard/assets/{live-YlifPYox.js → live-EmuIayqp.js} +1 -1
- package/dist/dashboard/assets/live-EmuIayqp.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-CVvHZ-qz.js → model-bans-CSPQ9xB0.js} +1 -1
- package/dist/dashboard/assets/model-bans-CSPQ9xB0.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/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/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-jntw9-Ui.js → routing-BExgxxBU.js} +1 -1
- package/dist/dashboard/assets/routing-BExgxxBU.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-CGUuTQmj.js → select-C9mMoEpJ.js} +1 -1
- package/dist/dashboard/assets/select-C9mMoEpJ.js.gz +0 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js +1 -0
- package/dist/dashboard/assets/settings-C4_Gn_wq.js.gz +0 -0
- package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
- package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js.gz +0 -0
- package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
- package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-Ca5HxI5i.js} +1 -1
- package/dist/dashboard/assets/stack-Ca5HxI5i.js.gz +0 -0
- package/dist/dashboard/assets/{state-DHKdg-pm.js → state-CzUPfLXo.js} +1 -1
- package/dist/dashboard/assets/state-CzUPfLXo.js.gz +0 -0
- package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-DOUIky_i.js} +1 -1
- package/dist/dashboard/assets/switch-DOUIky_i.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-C76WrMhc.js +2 -0
- package/dist/dashboard/assets/toast-C76WrMhc.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-D87FZwH0.js} +1 -1
- package/dist/dashboard/assets/useMutation-D87FZwH0.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 +123 -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/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/package.json +25 -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 +227 -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 +13 -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/spawn.ts +101 -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 +49 -0
- package/src/boot/app.ts +3 -1
- package/src/boot/dependencies.ts +36 -13
- package/src/config/providers.ts +27 -1
- package/src/config/spec.ts +11 -0
- 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/observability/contracts.ts +54 -22
- package/src/console/observability/routes.ts +31 -6
- package/src/console/observability/store.ts +68 -28
- package/src/console/providers/catalog/public-model-store.ts +91 -60
- package/src/console/providers/catalog/store.ts +5 -1
- package/src/console/quota/account-quota-tenant.ts +9 -6
- 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/network/outbound-fetch.ts +113 -25
- package/src/network/tls/bun-client-hello.ts +112 -0
- package/src/observability/event-log.ts +29 -8
- package/src/observability/metrics.ts +40 -13
- package/src/observability/runtime-metrics.ts +7 -0
- package/src/persistence/schema.ts +40 -2
- package/src/protocol/response/codex.ts +9 -75
- 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 +4 -0
- package/src/providers/discovery/catalog/catalog-format.ts +20 -0
- package/src/providers/discovery/models-dev-catalog.ts +39 -22
- package/src/providers/discovery/probe-wire.ts +29 -5
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +3 -2
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +4 -0
- 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/provider.ts +2 -2
- package/src/providers/integrations/buddy/codebuddy/shared.ts +106 -73
- package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
- package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
- package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
- package/src/providers/integrations/codex/codex.ts +5 -8
- package/src/providers/integrations/factory/factory-oauth.ts +7 -11
- package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
- package/src/providers/integrations/opencode/provider.ts +42 -3
- package/src/providers/integrations/qoder/qoder-oauth.ts +3 -9
- package/src/providers/integrations/shared/install-id.ts +29 -2
- package/src/providers/operations/client-versions.ts +11 -1
- package/src/providers/operations/model-catalog-cache.ts +18 -3
- package/src/providers/operations/provider-catalog-service.ts +76 -0
- package/src/transport/dispatch/attempt-finalize.ts +7 -1
- package/src/transport/dispatch/attempt-loop.ts +9 -7
- package/src/transport/dispatch/empty-stream.ts +17 -26
- package/src/transport/failure-policy.ts +20 -0
- package/src/transport/routing/route-catalog.ts +42 -3
- 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/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/ProviderDetailPage-BK8i5B5X.js +0 -3
- 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/SortableList-Bol1ASmI.js +0 -1
- package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
- package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
- package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
- 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/mutation-BIizFZav.js +0 -1
- package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
- package/dist/dashboard/assets/settings-BAS6QbmJ.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
|
}
|
|
@@ -274,6 +274,10 @@ export const PROVIDER_CAPABILITIES = {
|
|
|
274
274
|
loadAdapter: async () => createApiKeyAdapter((await import("./integrations/opencode/provider")).OPENCODE_GO_SPEC),
|
|
275
275
|
loadModels: async () => (await import("./integrations/opencode/provider")).OPENCODE_GO_MODELS,
|
|
276
276
|
loadModelDiscovery: openAIModelDiscovery("opencode-go", { transformBaseUrl: (baseUrl) => `${baseUrl.replace(/\/+$/, "")}/zen/go/v1`, headers: (credential) => ({ authorization: `Bearer ${credential}` }) }),
|
|
277
|
+
// The Go plan publishes a real usage surface; the anonymous free tier has
|
|
278
|
+
// none (it is rate-limited by egress IP, not by a balance), so this is
|
|
279
|
+
// deliberately registered on the keyed tier only.
|
|
280
|
+
loadQuotaCollector: async () => (await import("./integrations/opencode/opencode-go-quota")).fetchOpenCodeGoQuota,
|
|
277
281
|
},
|
|
278
282
|
agentrouter: {
|
|
279
283
|
loadAdapter: async () => (await import("./integrations/agentrouter/agentrouter")).createAgentRouterAdapter(),
|
|
@@ -166,6 +166,16 @@ const decoder = new TextDecoder();
|
|
|
166
166
|
|
|
167
167
|
const FLAG_REASONING = 1;
|
|
168
168
|
const FLAG_TOOL_CALL = 2;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Ceiling on {@link PackedCatalog}'s row-metadata cache.
|
|
172
|
+
*
|
|
173
|
+
* Sized well under the catalog's 8941 rows: a gateway resolves a working set of
|
|
174
|
+
* models, not the whole catalog, and every entry is rebuilt from the blob in a
|
|
175
|
+
* few typed-array reads. Holding more than this trades RAM for reads measured
|
|
176
|
+
* in nanoseconds.
|
|
177
|
+
*/
|
|
178
|
+
export const METADATA_CACHE_MAX_ENTRIES = 2048;
|
|
169
179
|
const SLASH = 0x2f;
|
|
170
180
|
|
|
171
181
|
/** Lowercases and trims an ASCII id into the exact bytes the blob stores. */
|
|
@@ -182,6 +192,12 @@ export function lowerAscii(value: string): Uint8Array {
|
|
|
182
192
|
* Holds typed-array views (zero-copy) and nothing else. Metadata objects are
|
|
183
193
|
* created only for rows a caller actually resolves, and cached, so a long-lived
|
|
184
194
|
* process pays for the handful of ids it queried rather than for all 8090.
|
|
195
|
+
*
|
|
196
|
+
* That cache is **bounded**: an entry is rebuilt from the blob in a few
|
|
197
|
+
* typed-array reads, so holding one at the cost of another is never worth it,
|
|
198
|
+
* and an unbounded memo over 8941 rows is memory the operator pays for at idle
|
|
199
|
+
* whether or not those rows are ever queried again. Bounded LRU, oldest
|
|
200
|
+
* insertion evicted first — the same shape the rest of the codebase uses.
|
|
185
201
|
*/
|
|
186
202
|
export class PackedCatalog {
|
|
187
203
|
private readonly bytes: Uint8Array;
|
|
@@ -399,6 +415,10 @@ export class PackedCatalog {
|
|
|
399
415
|
reasoning: (flags & FLAG_REASONING) !== 0,
|
|
400
416
|
toolCall: (flags & FLAG_TOOL_CALL) !== 0,
|
|
401
417
|
};
|
|
418
|
+
if (this.metadataCache.size >= METADATA_CACHE_MAX_ENTRIES) {
|
|
419
|
+
const oldest = this.metadataCache.keys().next().value;
|
|
420
|
+
if (oldest !== undefined) this.metadataCache.delete(oldest);
|
|
421
|
+
}
|
|
402
422
|
this.metadataCache.set(index, metadata);
|
|
403
423
|
return metadata;
|
|
404
424
|
}
|
|
@@ -134,25 +134,38 @@ const UNKNOWN_COST: ModelsDevModelCost = {
|
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
*
|
|
137
|
+
* The packed catalog, decompressed on first use and memoized.
|
|
138
138
|
*
|
|
139
139
|
* Brotli + base64, not `JSON.parse`: the blob is columnar, so this allocates
|
|
140
140
|
* one buffer and takes typed-array views over it rather than materialising
|
|
141
141
|
* 8900 objects. `CATALOG_BLOB_BYTES` is the decompressed length, checked here
|
|
142
|
-
* so a consumer paired with a half-regenerated blob fails
|
|
143
|
-
* resolving a truncated catalog.
|
|
142
|
+
* so a consumer paired with a half-regenerated blob fails on first access
|
|
143
|
+
* rather than resolving a truncated catalog.
|
|
144
|
+
*
|
|
145
|
+
* The decode is lazy on purpose. It is ~105 KB of base64 that expands to a
|
|
146
|
+
* much larger buffer, and it used to run at module load — so every process,
|
|
147
|
+
* including the ones that never resolve a model (a CLI subcommand, a migration
|
|
148
|
+
* run, a test that only touches the console), paid it on the boot path.
|
|
149
|
+
* Deferring it moves that cost to the first catalog lookup and pays it once.
|
|
144
150
|
*/
|
|
145
|
-
|
|
146
|
-
if (CATALOG_BYTES.byteLength !== CATALOG_BLOB_BYTES) {
|
|
147
|
-
throw new Error(
|
|
148
|
-
`The packed model catalog is ${CATALOG_BYTES.byteLength} B, expected ` +
|
|
149
|
-
`${CATALOG_BLOB_BYTES} B — regenerate it.`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
151
|
+
let catalogCache: PackedCatalog | undefined;
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
function catalog(): PackedCatalog {
|
|
154
|
+
const cached = catalogCache;
|
|
155
|
+
if (cached !== undefined) return cached;
|
|
156
|
+
const bytes = brotliDecompressSync(Buffer.from(CATALOG_BLOB_BROTLI_BASE64, "base64"));
|
|
157
|
+
if (bytes.byteLength !== CATALOG_BLOB_BYTES) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`The packed model catalog is ${bytes.byteLength} B, expected ` +
|
|
160
|
+
`${CATALOG_BLOB_BYTES} B — regenerate it.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
// `PackedCatalog` copies into a 4-byte-aligned buffer of its own, so the
|
|
164
|
+
// decompressed buffer is dropped here rather than held beside the copy.
|
|
165
|
+
const packed = new PackedCatalog(bytes);
|
|
166
|
+
catalogCache = packed;
|
|
167
|
+
return packed;
|
|
168
|
+
}
|
|
156
169
|
|
|
157
170
|
/**
|
|
158
171
|
* Resolves model metadata and pricing exclusively from the packed base catalog
|
|
@@ -160,9 +173,13 @@ const CATALOG = new PackedCatalog(CATALOG_BYTES);
|
|
|
160
173
|
*/
|
|
161
174
|
export class ModelsDevCatalog {
|
|
162
175
|
/** When the generator fetched the upstream catalogs. */
|
|
163
|
-
|
|
176
|
+
get fetchedAt(): string {
|
|
177
|
+
return catalog().fetchedAt;
|
|
178
|
+
}
|
|
164
179
|
/** Every provider key the catalog files. */
|
|
165
|
-
|
|
180
|
+
get providers(): readonly string[] {
|
|
181
|
+
return catalog().providers;
|
|
182
|
+
}
|
|
166
183
|
|
|
167
184
|
/**
|
|
168
185
|
* Metadata for an exact `(providerIndex, idLower)` pair, or `undefined`.
|
|
@@ -172,19 +189,19 @@ export class ModelsDevCatalog {
|
|
|
172
189
|
* matches exactly keeps winning.
|
|
173
190
|
*/
|
|
174
191
|
private exactMetadata(providerLower: string, idLower: Uint8Array): ModelsDevModelMetadata | undefined {
|
|
175
|
-
const direct =
|
|
176
|
-
if (direct >= 0) return
|
|
192
|
+
const direct = catalog().exactIndex(catalog().providerIndex(providerLower), idLower);
|
|
193
|
+
if (direct >= 0) return catalog().metadataAt(direct);
|
|
177
194
|
const mapped = CATALOG_PROVIDER_IDS[providerLower];
|
|
178
195
|
if (mapped === undefined) return undefined;
|
|
179
|
-
return
|
|
196
|
+
return catalog().metadataAt(catalog().exactIndex(catalog().providerIndex(mapped), idLower));
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
/** Metadata rows whose *bare* id equals `bare`, in catalog order. */
|
|
183
200
|
private metadataForBare(bare: Uint8Array): ModelsDevModelMetadata[] {
|
|
184
|
-
const [start, end] =
|
|
201
|
+
const [start, end] = catalog().bareRange(bare);
|
|
185
202
|
const out: ModelsDevModelMetadata[] = [];
|
|
186
203
|
for (let position = start; position < end; position += 1) {
|
|
187
|
-
const metadata =
|
|
204
|
+
const metadata = catalog().metadataAt(catalog().bareOrderAt(position));
|
|
188
205
|
if (metadata !== undefined) out.push(metadata);
|
|
189
206
|
}
|
|
190
207
|
return out;
|
|
@@ -327,8 +344,8 @@ export class ModelsDevCatalog {
|
|
|
327
344
|
return own;
|
|
328
345
|
}
|
|
329
346
|
const idLower = lowerAscii(modelId);
|
|
330
|
-
const row =
|
|
331
|
-
const global = row >= 0 ?
|
|
347
|
+
const row = catalog().globalCostRow(bareBytes(idLower));
|
|
348
|
+
const global = row >= 0 ? catalog().rowCost(row) : undefined;
|
|
332
349
|
if (global !== undefined && global.pricing_model !== "unknown") return global;
|
|
333
350
|
return own ?? global ?? UNKNOWN_COST;
|
|
334
351
|
}
|
|
@@ -172,17 +172,41 @@ export function staticEndpointForWire(
|
|
|
172
172
|
return catalog.get(providerId)?.find((def) => def.wireFamily === wireFamily)?.endpointPath;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Compiled-pattern memo.
|
|
177
|
+
*
|
|
178
|
+
* `matchModelWireFamily` runs per model on every discovery pass and on every
|
|
179
|
+
* manual registration, and it used to compile each rule's `RegExp` inside that
|
|
180
|
+
* loop — so the same handful of patterns were recompiled once per model per
|
|
181
|
+
* pass. Keyed by the pattern source, which is exactly what `new RegExp(source)`
|
|
182
|
+
* derives from.
|
|
183
|
+
*
|
|
184
|
+
* A pattern that fails to compile is not memoized; the tolerance stays at read
|
|
185
|
+
* time because an invalid pattern is rejected at write time, and a stored one
|
|
186
|
+
* must not throw during a listing.
|
|
187
|
+
*/
|
|
188
|
+
const compiledPatternCache = new Map<string, RegExp>();
|
|
189
|
+
|
|
190
|
+
function compilePattern(pattern: string): RegExp | undefined {
|
|
191
|
+
const cached = compiledPatternCache.get(pattern);
|
|
192
|
+
if (cached !== undefined) return cached;
|
|
193
|
+
try {
|
|
194
|
+
const compiled = new RegExp(pattern);
|
|
195
|
+
compiledPatternCache.set(pattern, compiled);
|
|
196
|
+
return compiled;
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
175
202
|
function matchModelWireFamily(
|
|
176
203
|
modelId: string,
|
|
177
204
|
rules: ReadonlyArray<{ pattern: string; wire_family: WireFamily }> | undefined,
|
|
178
205
|
): WireFamily | undefined {
|
|
179
206
|
if (!rules?.length) return undefined;
|
|
180
207
|
for (const rule of rules) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
} catch {
|
|
184
|
-
// Invalid pattern is rejected at write time; be tolerant at read time.
|
|
185
|
-
}
|
|
208
|
+
const pattern = compilePattern(rule.pattern);
|
|
209
|
+
if (pattern !== undefined && pattern.test(modelId)) return rule.wire_family as WireFamily;
|
|
186
210
|
}
|
|
187
211
|
return undefined;
|
|
188
212
|
}
|