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
package/src/boot/dependencies.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { RedisClient } from "../persistence/redis";
|
|
|
7
7
|
import {
|
|
8
8
|
bundledModelCatalog,
|
|
9
9
|
liveProviderUpstreamHosts,
|
|
10
|
+
markCatalogSeeded,
|
|
10
11
|
refreshActiveProviderCatalogs,
|
|
11
12
|
registerByokProviders,
|
|
12
13
|
retireUnbundledProviders,
|
|
@@ -61,6 +62,7 @@ import {
|
|
|
61
62
|
} from "../workers/buddy-growth-tasks";
|
|
62
63
|
import { createAccountSecretResolver } from "../providers/operations/provider-credential-service";
|
|
63
64
|
import { quotaCacheSize } from "../console/quota/cache";
|
|
65
|
+
import { modelCatalogCacheSize } from "../providers/operations/model-catalog-cache";
|
|
64
66
|
import { preferencesReaderFor } from "../persistence/tenant-preferences";
|
|
65
67
|
import { sweepExpiredCooldowns } from "../providers/operations/account-health-service";
|
|
66
68
|
import { DrizzleTelemetryStore } from "../persistence/stores/telemetry-store";
|
|
@@ -130,7 +132,9 @@ const OAUTH_REFRESH_INTER_ITEM_DELAY_MS = 1_500;
|
|
|
130
132
|
* Client identities are refreshed out of band so provider dispatch never waits
|
|
131
133
|
* on npm/provider version endpoints. Resolver TTLs deduplicate the sources.
|
|
132
134
|
*/
|
|
133
|
-
|
|
135
|
+
// Matched to the resolver's own 30 min cache TTL: at 15 min every second pass
|
|
136
|
+
// was a pure cache read that could not discover anything new.
|
|
137
|
+
const CLIENT_VERSION_MONITOR_INTERVAL_MS = 30 * 60_000;
|
|
134
138
|
const CLIENT_VERSION_INITIAL_DELAY_MS = 10_000;
|
|
135
139
|
|
|
136
140
|
export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
@@ -166,6 +170,9 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
166
170
|
await measureStartupPhase("bundled-model-seed", () =>
|
|
167
171
|
seedBundledModels(db, bundledCatalog.modelsByProvider),
|
|
168
172
|
);
|
|
173
|
+
// The boot seed above is unconditional, so record it: the snapshot builder's
|
|
174
|
+
// first rebuild would otherwise re-seed an identical catalog.
|
|
175
|
+
markCatalogSeeded();
|
|
169
176
|
const snapshotService = new InMemoryRouteSnapshotService(
|
|
170
177
|
createDatabaseSnapshotBuilder(db, () => refreshActiveProviderCatalogs(db, registry)),
|
|
171
178
|
);
|
|
@@ -280,7 +287,11 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
280
287
|
});
|
|
281
288
|
scheduledTasks.register({
|
|
282
289
|
name: "console-session-sweep",
|
|
283
|
-
|
|
290
|
+
// Sessions live 48 h, so an hourly sweep deleted nothing 47 times out of
|
|
291
|
+
// 48 — each pass a full scan of the session table for rows that cannot yet
|
|
292
|
+
// be expired. Half the session lifetime is the point where a pass can
|
|
293
|
+
// actually find work, and still leaves a full grace period after it.
|
|
294
|
+
intervalMs: 12 * 60 * 60_000,
|
|
284
295
|
run: () => deleteExpiredSessions(db),
|
|
285
296
|
});
|
|
286
297
|
scheduledTasks.register({
|
|
@@ -309,6 +320,7 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
309
320
|
routingRoundRobinEntries: () => routingEngine.roundRobinEntries(),
|
|
310
321
|
ipAbuseKeys: () => (ipStore instanceof InMemoryIpAbuseStore ? ipStore.keyCount() : 0),
|
|
311
322
|
quotaCacheEntries: () => quotaCacheSize(),
|
|
323
|
+
modelCatalogEntries: () => modelCatalogCacheSize(),
|
|
312
324
|
// Late-bound via bindInFlight once the request state store exists.
|
|
313
325
|
inFlight: () => 0,
|
|
314
326
|
});
|
|
@@ -322,7 +334,11 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
322
334
|
});
|
|
323
335
|
scheduledTasks.register({
|
|
324
336
|
name: "pool-agent-sweep",
|
|
325
|
-
|
|
337
|
+
// `resolveAgent` already self-sweeps every 30 s, so a 1 min timer added a
|
|
338
|
+
// second pass that almost always found nothing to release. 5 min is well
|
|
339
|
+
// inside the idle timeout this exists to enforce, and outside the path
|
|
340
|
+
// that already runs.
|
|
341
|
+
intervalMs: 5 * 60_000,
|
|
326
342
|
run: () => poolAgentResolver.sweepIdleAgents(),
|
|
327
343
|
});
|
|
328
344
|
const oauthRefreshService = new OAuthRefreshService(db);
|
|
@@ -338,7 +354,12 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
338
354
|
// credential — no upstream call — so it runs often and cheaply.
|
|
339
355
|
scheduledTasks.register({
|
|
340
356
|
name: "account-enrichment-sweep",
|
|
341
|
-
|
|
357
|
+
// Converges via `identity_checked_at`: once every stored credential has
|
|
358
|
+
// been parsed, the listing query returns nothing — and a 30 s timer kept
|
|
359
|
+
// running that empty query forever, two million times a year. 10 min is
|
|
360
|
+
// still instant for the case that matters (a batch import, where the label
|
|
361
|
+
// and email show up on the next tick) and costs nothing when idle.
|
|
362
|
+
intervalMs: 10 * 60_000,
|
|
342
363
|
run: () => accountIdentitySweep({ db }),
|
|
343
364
|
});
|
|
344
365
|
scheduledTasks.register({
|
|
@@ -434,9 +455,11 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
434
455
|
// an account unhealthy — a growth endpoint that 404s is a display miss, not a
|
|
435
456
|
// dispatch failure.
|
|
436
457
|
//
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
458
|
+
// Daily, not hourly: each claim is once per account per day and is
|
|
459
|
+
// ledger-guarded, so at an hourly cadence 23 of every 24 passes were a full
|
|
460
|
+
// scan of active accounts plus a ledger read per account that resolved to
|
|
461
|
+
// nothing. Once a day is the shortest interval where a pass can still find
|
|
462
|
+
// work, which is the only reason to run at all.
|
|
440
463
|
const buddyTaskDeps = {
|
|
441
464
|
db,
|
|
442
465
|
resolveCredential: quotaResolveCredential,
|
|
@@ -447,37 +470,37 @@ export async function buildProductionDeps(): Promise<ProductionDeps> {
|
|
|
447
470
|
};
|
|
448
471
|
scheduledTasks.register({
|
|
449
472
|
name: "buddy-streak",
|
|
450
|
-
intervalMs: 60 * 60_000,
|
|
473
|
+
intervalMs: 24 * 60 * 60_000,
|
|
451
474
|
initialDelayMs: 5 * 60_000,
|
|
452
475
|
run: () => buddyStreakTask(buddyTaskDeps),
|
|
453
476
|
});
|
|
454
477
|
scheduledTasks.register({
|
|
455
478
|
name: "buddy-redeem",
|
|
456
|
-
intervalMs: 60 * 60_000,
|
|
479
|
+
intervalMs: 24 * 60 * 60_000,
|
|
457
480
|
initialDelayMs: 8 * 60_000,
|
|
458
481
|
run: () => buddyRedeemTask(buddyTaskDeps),
|
|
459
482
|
});
|
|
460
483
|
scheduledTasks.register({
|
|
461
484
|
name: "buddy-lottery",
|
|
462
|
-
intervalMs: 60 * 60_000,
|
|
485
|
+
intervalMs: 24 * 60 * 60_000,
|
|
463
486
|
initialDelayMs: 11 * 60_000,
|
|
464
487
|
run: () => buddyLotteryTask(buddyTaskDeps),
|
|
465
488
|
});
|
|
466
489
|
scheduledTasks.register({
|
|
467
490
|
name: "buddy-travel",
|
|
468
|
-
intervalMs: 60 * 60_000,
|
|
491
|
+
intervalMs: 24 * 60 * 60_000,
|
|
469
492
|
initialDelayMs: 14 * 60_000,
|
|
470
493
|
run: () => buddyTravelTask(buddyTaskDeps),
|
|
471
494
|
});
|
|
472
495
|
scheduledTasks.register({
|
|
473
496
|
name: "buddy-adopt",
|
|
474
|
-
intervalMs: 60 * 60_000,
|
|
497
|
+
intervalMs: 24 * 60 * 60_000,
|
|
475
498
|
initialDelayMs: 17 * 60_000,
|
|
476
499
|
run: () => buddyAdoptTask(buddyTaskDeps),
|
|
477
500
|
});
|
|
478
501
|
scheduledTasks.register({
|
|
479
502
|
name: "buddy-trial",
|
|
480
|
-
intervalMs:
|
|
503
|
+
intervalMs: 24 * 60 * 60_000,
|
|
481
504
|
initialDelayMs: 20 * 60_000,
|
|
482
505
|
run: () => buddyTrialTask(buddyTaskDeps),
|
|
483
506
|
});
|
package/src/config/providers.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* callback URL builders built on the public origin.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { CONFIG_SPEC, readRequired, readText } from "./spec";
|
|
7
|
+
import { CONFIG_SPEC, readList, readRequired, readText } from "./spec";
|
|
8
8
|
|
|
9
9
|
/** Reads and normalizes the public OAuth origin. */
|
|
10
10
|
export function requirePublicOrigin(): string {
|
|
@@ -48,6 +48,32 @@ export function resolveKimiQuotaBaseUrl(): string {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Operator override for the OpenCode free-tier ids upstream refuses, or
|
|
53
|
+
* `undefined` when unset — meaning the bundled, measured list stands.
|
|
54
|
+
*
|
|
55
|
+
* The single token `none` is distinct from unset and from an empty string: it
|
|
56
|
+
* means "trust the listing completely", which is the reference gateway's whole
|
|
57
|
+
* approach. Collapsing it into "no override" would silently re-block models
|
|
58
|
+
* the operator just asked to unblock.
|
|
59
|
+
*/
|
|
60
|
+
export function resolveOpenCodeUnavailableFree():
|
|
61
|
+
| readonly string[]
|
|
62
|
+
| "none"
|
|
63
|
+
| undefined {
|
|
64
|
+
const raw = process.env.CARTETHYIA_OPENCODE_UNAVAILABLE_FREE?.trim();
|
|
65
|
+
if (raw === undefined || raw === "") return undefined;
|
|
66
|
+
if (raw.toLowerCase() === "none") return "none";
|
|
67
|
+
const parsed = readList(
|
|
68
|
+
"CARTETHYIA_OPENCODE_UNAVAILABLE_FREE",
|
|
69
|
+
CONFIG_SPEC.CARTETHYIA_OPENCODE_UNAVAILABLE_FREE,
|
|
70
|
+
);
|
|
71
|
+
// Separators only: readList drops the empties and yields nothing. Treating
|
|
72
|
+
// that as "trust everything" would un-block every refused model on a typo,
|
|
73
|
+
// so it falls back to the bundled list instead.
|
|
74
|
+
return parsed === undefined || parsed.length === 0 ? undefined : parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/**
|
|
52
78
|
* GitHub Enterprise domain for the Copilot device login, or `""` for github.com.
|
|
53
79
|
*
|
package/src/config/spec.ts
CHANGED
|
@@ -225,6 +225,17 @@ export const CONFIG_SPEC = {
|
|
|
225
225
|
* an operator on one has no other way to point the login at it.
|
|
226
226
|
*/
|
|
227
227
|
CARTETHYIA_GITHUB_ENTERPRISE_DOMAIN: { kind: "text", default: "" },
|
|
228
|
+
/**
|
|
229
|
+
* Free-tier OpenCode ids the listing still advertises but upstream refuses.
|
|
230
|
+
* Replaces the bundled list outright; the single token `none` trusts the
|
|
231
|
+
* listing completely.
|
|
232
|
+
*
|
|
233
|
+
* The bundled ids were measured, not declared — upstream can start serving
|
|
234
|
+
* a refused id or start refusing a served one at any time, and either way
|
|
235
|
+
* the correct answer stopped being the one compiled in. This is the escape
|
|
236
|
+
* hatch that needs no deploy.
|
|
237
|
+
*/
|
|
238
|
+
CARTETHYIA_OPENCODE_UNAVAILABLE_FREE: { kind: "list" },
|
|
228
239
|
} as const satisfies Readonly<Record<string, ConfigEntry>>;
|
|
229
240
|
|
|
230
241
|
// ─── Spec readers ───────────────────────────────────────────────────────────
|
|
@@ -18,6 +18,7 @@ import { Elysia, t } from "elysia";
|
|
|
18
18
|
import type { ConsoleAccessResolver } from "../auth/access";
|
|
19
19
|
import { invalidateApiKeyCache } from "../../security/api-key-auth";
|
|
20
20
|
import { ConsoleDomainError, errorResponse, requireAnyScope } from "../shared/errors";
|
|
21
|
+
import { NO_COMPRESS_HEADER } from "../response-compression";
|
|
21
22
|
import { MAX_BACKUP_BYTES } from "./contracts";
|
|
22
23
|
import type { BackupSection } from "./contracts";
|
|
23
24
|
import { DELETE_ALL_SCOPES, compressBackup } from "./store";
|
|
@@ -124,6 +125,13 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
124
125
|
}
|
|
125
126
|
const bytes = compressBackup(payload);
|
|
126
127
|
set.headers["content-type"] = "application/json; charset=utf-8";
|
|
128
|
+
// Opt out of the response wrapper's gzip rather than declaring
|
|
129
|
+
// `content-encoding: gzip`: the bytes here are base64 of a gzip
|
|
130
|
+
// stream, and a client told "this is gzip" tries to inflate the
|
|
131
|
+
// base64 text and fails. The payload inside is already compressed,
|
|
132
|
+
// and a second pass over base64 would find nothing to gain while
|
|
133
|
+
// paying a synchronous `gzipSync` over the whole multi-MB body.
|
|
134
|
+
set.headers[NO_COMPRESS_HEADER] = "1";
|
|
127
135
|
return { compressed: true, filename: `cartethyia_backup_${stamp}.json.gz`, bytes: bytes.toString("base64") };
|
|
128
136
|
} catch (error) {
|
|
129
137
|
return errorResponse(error, set, "Backup export failed");
|
|
@@ -486,9 +486,9 @@ export const TOOL_REGISTRY = {
|
|
|
486
486
|
roleKind: "primary",
|
|
487
487
|
},
|
|
488
488
|
{
|
|
489
|
-
id: "gpt-5.
|
|
490
|
-
name: "GPT-5.
|
|
491
|
-
alias: "gpt-5.
|
|
489
|
+
id: "gpt-5.6-sol",
|
|
490
|
+
name: "GPT-5.6 Sol",
|
|
491
|
+
alias: "gpt-5.6-sol",
|
|
492
492
|
roleLabel: "Fallback Agent",
|
|
493
493
|
roleKind: "secondary",
|
|
494
494
|
},
|
|
@@ -505,7 +505,7 @@ export const TOOL_REGISTRY = {
|
|
|
505
505
|
settingsFile: "~/.factory/settings.json",
|
|
506
506
|
defaultModels: [
|
|
507
507
|
{ id: "claude/claude-sonnet-5", name: "Claude Sonnet 5", alias: "sonnet" },
|
|
508
|
-
{ id: "gpt-5.
|
|
508
|
+
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", alias: "gpt-5.6-sol" },
|
|
509
509
|
],
|
|
510
510
|
},
|
|
511
511
|
|
|
@@ -540,7 +540,7 @@ export const TOOL_REGISTRY = {
|
|
|
540
540
|
surface: "chat" as const,
|
|
541
541
|
settingsFile: "Code/User/chatLanguageModels.json",
|
|
542
542
|
defaultModels: [
|
|
543
|
-
{ id: "gpt-5.
|
|
543
|
+
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", alias: "gpt-5.6-sol" },
|
|
544
544
|
{ id: "claude/claude-sonnet-5", name: "Claude Sonnet 5", alias: "sonnet" },
|
|
545
545
|
],
|
|
546
546
|
},
|
|
@@ -703,7 +703,7 @@ amp --model "{{model}}"`,
|
|
|
703
703
|
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/configuration/model-providers/",
|
|
704
704
|
defaultModels: [
|
|
705
705
|
{ id: "claude/claude-sonnet-5", name: "Claude Sonnet 5", alias: "sonnet" },
|
|
706
|
-
{ id: "gpt-5.
|
|
706
|
+
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol", alias: "gpt-5.6-sol" },
|
|
707
707
|
],
|
|
708
708
|
notes: [
|
|
709
709
|
{
|
|
@@ -209,9 +209,11 @@ export function createConsoleRouter(deps: ConsoleApiCompositionDeps): Elysia {
|
|
|
209
209
|
// nothing to compress and no size to judge. `wrap` sits outside the router
|
|
210
210
|
// and sees the finished `Response`, which is what this needs.
|
|
211
211
|
//
|
|
212
|
-
// Scoped
|
|
213
|
-
//
|
|
214
|
-
//
|
|
212
|
+
// Scoped by path *inside* `gzipResponse`, not by this instance: Elysia's
|
|
213
|
+
// `use()` merges a child's `hoc` into the root app's ext, so this wrap is
|
|
214
|
+
// reached by `/v1/*` as well. The allowlist is `/console/api/` + `/metrics`.
|
|
215
|
+
// `/v1/*` is deliberately excluded, because a streaming completion must not
|
|
216
|
+
// be buffered and proxied bodies are the provider's own bytes. See
|
|
215
217
|
// `response-compression.ts` for the full reasoning and the measured win.
|
|
216
218
|
console.wrap(
|
|
217
219
|
(fetch) => (request: Request, ...rest: unknown[]) =>
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
X_FRAME_OPTIONS,
|
|
5
5
|
dashboardContentSecurityPolicy,
|
|
6
6
|
} from "../security/outbound-headers";
|
|
7
|
+
import { acceptsGzip } from "./response-compression";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Represents the result of serving a static file.
|
|
@@ -148,9 +149,39 @@ function entryDocumentFor(pathname: string, entries: Readonly<Record<string, str
|
|
|
148
149
|
* - Serves hashed assets with immutable cache headers and documents with no-cache
|
|
149
150
|
* - Returns 404 for missing asset-like files
|
|
150
151
|
*/
|
|
152
|
+
/**
|
|
153
|
+
* The `.gz` sibling of an asset, when one exists and the client offered gzip.
|
|
154
|
+
*
|
|
155
|
+
* Restricted to hashed assets — see the check below for why — and best-effort:
|
|
156
|
+
* a build that emitted no `.gz` simply serves the plain file.
|
|
157
|
+
*/
|
|
158
|
+
async function readPrecompressed(
|
|
159
|
+
fullPath: string,
|
|
160
|
+
acceptEncoding: string | undefined | null,
|
|
161
|
+
): Promise<Uint8Array | undefined> {
|
|
162
|
+
if (acceptEncoding === undefined || acceptEncoding === null) return undefined;
|
|
163
|
+
if (!acceptsGzip(acceptEncoding)) return undefined;
|
|
164
|
+
// Only hashed assets: those are the immutable ones, and they are what the
|
|
165
|
+
// Vite build precompresses. An unhashed asset is served with a revalidating
|
|
166
|
+
// cache header precisely because it may be replaced in place, and a `.gz`
|
|
167
|
+
// left beside a file that was swapped afterwards would not match it.
|
|
168
|
+
if (!hasHashInFilename(fullPath)) return undefined;
|
|
169
|
+
const file = Bun.file(`${fullPath}.gz`);
|
|
170
|
+
if (!(await file.exists())) return undefined;
|
|
171
|
+
return await file.bytes();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Builds the static handler.
|
|
176
|
+
*
|
|
177
|
+
* The returned function takes an optional `acceptEncoding` so the caller can
|
|
178
|
+
* pass the request's header: a precompressed `.gz` sibling is only usable when
|
|
179
|
+
* the client asking for it offered gzip. Omitting it serves the plain file,
|
|
180
|
+
* which is what the tests and any non-HTTP caller want.
|
|
181
|
+
*/
|
|
151
182
|
export function createStaticHandler(
|
|
152
183
|
config: StaticHandlerConfig,
|
|
153
|
-
): (pathname: string) => Promise<StaticServeResult> {
|
|
184
|
+
): (pathname: string, acceptEncoding?: string | null) => Promise<StaticServeResult> {
|
|
154
185
|
const buildDirResolved = resolve(config.buildDir);
|
|
155
186
|
const buildDirNorm = normalizePathSeparators(buildDirResolved);
|
|
156
187
|
const entries = config.entries ?? DEFAULT_ENTRY_DOCUMENTS;
|
|
@@ -173,7 +204,10 @@ export function createStaticHandler(
|
|
|
173
204
|
};
|
|
174
205
|
};
|
|
175
206
|
|
|
176
|
-
const serve = async (
|
|
207
|
+
const serve = async (
|
|
208
|
+
pathname: string,
|
|
209
|
+
acceptEncoding?: string | null,
|
|
210
|
+
): Promise<StaticServeResult> => {
|
|
177
211
|
// Reject the reserved `/console/api` namespace, including its bare prefix.
|
|
178
212
|
if (pathname === "/console/api" || pathname.startsWith("/console/api/")) {
|
|
179
213
|
return {
|
|
@@ -241,6 +275,28 @@ export function createStaticHandler(
|
|
|
241
275
|
cacheControl = "public, max-age=3600";
|
|
242
276
|
}
|
|
243
277
|
|
|
278
|
+
// A precompressed sibling is served when the client asked for gzip
|
|
279
|
+
// and the build emitted one. The bytes were compressed at build time
|
|
280
|
+
// (level 9), so this is a disk read rather than a per-request
|
|
281
|
+
// compress — and a build with no `.gz` falls through to the plain
|
|
282
|
+
// file, which is why there is no error path here.
|
|
283
|
+
const precompressed = await readPrecompressed(fullPath, acceptEncoding);
|
|
284
|
+
if (precompressed !== undefined) {
|
|
285
|
+
return {
|
|
286
|
+
status: 200,
|
|
287
|
+
body: precompressed,
|
|
288
|
+
headers: {
|
|
289
|
+
"content-type": mimeType,
|
|
290
|
+
"cache-control": cacheControl,
|
|
291
|
+
"content-encoding": "gzip",
|
|
292
|
+
// Without this a shared cache would hand the gzipped bytes to a
|
|
293
|
+
// client that never offered gzip: the URL is identical either
|
|
294
|
+
// way, so `accept-encoding` is the only thing that differs.
|
|
295
|
+
vary: "accept-encoding",
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
244
300
|
return {
|
|
245
301
|
status: 200,
|
|
246
302
|
body: data,
|
|
@@ -260,8 +316,8 @@ export function createStaticHandler(
|
|
|
260
316
|
return { status: 404, body: undefined, headers: {} };
|
|
261
317
|
};
|
|
262
318
|
|
|
263
|
-
return async (pathname: string): Promise<StaticServeResult> =>
|
|
264
|
-
applyStaticSecurityHeaders(await serve(pathname));
|
|
319
|
+
return async (pathname: string, acceptEncoding?: string | null): Promise<StaticServeResult> =>
|
|
320
|
+
applyStaticSecurityHeaders(await serve(pathname, acceptEncoding));
|
|
265
321
|
}
|
|
266
322
|
|
|
267
323
|
/**
|
|
@@ -328,13 +384,27 @@ function getMimeType(filePath: string): string {
|
|
|
328
384
|
}
|
|
329
385
|
|
|
330
386
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
387
|
+
* True when a filename carries a build hash, so its bytes are immutable.
|
|
388
|
+
*
|
|
389
|
+
* Matched on the two separator forms a bundler actually emits — `app.a1b2c3d4.js`
|
|
390
|
+
* and the modern Vite spelling `app-BCnqn3N3.js` — plus `_`, and then required
|
|
391
|
+
* to *look generated*: the segment must contain a digit or mix upper and lower
|
|
392
|
+
* case. The second half is what keeps a plain word out of the set, since
|
|
393
|
+
* `console-backgrounds.jpg` and `openai-light.svg` both match the separator
|
|
394
|
+
* alone and are emphatically not immutable.
|
|
395
|
+
*
|
|
396
|
+
* Getting this wrong in either direction costs real money: a hashed asset
|
|
397
|
+
* served with a revalidating header is re-downloaded after every deploy, and an
|
|
398
|
+
* unhashed one served `immutable` is pinned in the browser cache until it
|
|
399
|
+
* expires. Measured against a real build, this matches 198 of 288 files — all
|
|
400
|
+
* of them hashed bundles — and rejects every icon, photo, and document.
|
|
333
401
|
*/
|
|
334
402
|
function hasHashInFilename(filePath: string): boolean {
|
|
335
|
-
const filename = filePath.split(/[/\\]/).pop()
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
403
|
+
const filename = filePath.split(/[/\\]/).pop() ?? "";
|
|
404
|
+
const match = /[._-]([A-Za-z0-9_-]{8,})\./.exec(filename);
|
|
405
|
+
if (match === null) return false;
|
|
406
|
+
const segment = match[1] ?? "";
|
|
407
|
+
const hasDigit = /[0-9]/.test(segment);
|
|
408
|
+
const hasMixedCase = /[a-z]/.test(segment) && /[A-Z]/.test(segment);
|
|
409
|
+
return hasDigit || hasMixedCase;
|
|
340
410
|
}
|
|
@@ -2,6 +2,7 @@ import type { ConsoleAccessResolver } from "../auth/access";
|
|
|
2
2
|
// Telemetry domain: system health, usage, and telemetry-event read API.
|
|
3
3
|
|
|
4
4
|
import { ConsoleDomainError, requireTenantScope } from "../shared/errors";
|
|
5
|
+
import { conditionalResponse, type CachedResult } from "../shared/read-through-cache";
|
|
5
6
|
import { parseQueryLimit } from "../shared/query";
|
|
6
7
|
import type { AccessDecision } from "../../security/access-control";
|
|
7
8
|
import { USAGE_DIMENSIONS, type UsageDimension } from "./usage-dimensions";
|
|
@@ -262,8 +263,15 @@ export interface TelemetryEventListPage {
|
|
|
262
263
|
nextCursor?: string;
|
|
263
264
|
}
|
|
264
265
|
export interface ObservabilityStore {
|
|
265
|
-
|
|
266
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Every member below that is served from the read-through cache returns a
|
|
268
|
+
* {@link CachedResult}, carrying the strong validator for the bytes the
|
|
269
|
+
* client would otherwise receive. A repeated poll is then a `304` instead of
|
|
270
|
+
* a full payload. The reads that are per-row and already bounded — the event
|
|
271
|
+
* list and the detail views — have no cache entry and no validator.
|
|
272
|
+
*/
|
|
273
|
+
health(tenantId: string): Promise<CachedResult<SystemHealthResponse>>;
|
|
274
|
+
usage(tenantId: string, period: string): Promise<CachedResult<UsageResponse>>;
|
|
267
275
|
listEvents(tenantId: string, limit: number, cursor?: string): Promise<TelemetryEventListPage>;
|
|
268
276
|
getEvent(
|
|
269
277
|
tenantId: string,
|
|
@@ -271,20 +279,20 @@ export interface ObservabilityStore {
|
|
|
271
279
|
includePayload: boolean,
|
|
272
280
|
createdAt?: string,
|
|
273
281
|
): Promise<TelemetryEventDetail | undefined>;
|
|
274
|
-
usageSummary(tenantId: string, period: string): Promise<UsageSummaryResponse
|
|
275
|
-
usageChart(tenantId: string, period: string): Promise<UsageChartResponse
|
|
282
|
+
usageSummary(tenantId: string, period: string): Promise<CachedResult<UsageSummaryResponse>>;
|
|
283
|
+
usageChart(tenantId: string, period: string): Promise<CachedResult<UsageChartResponse>>;
|
|
276
284
|
usageBy(
|
|
277
285
|
tenantId: string,
|
|
278
286
|
dimension: UsageDimension,
|
|
279
287
|
period: string,
|
|
280
|
-
): Promise<UsageByResponse
|
|
281
|
-
usageCache(tenantId: string, period: string): Promise<UsageCacheResponse
|
|
288
|
+
): Promise<CachedResult<UsageByResponse>>;
|
|
289
|
+
usageCache(tenantId: string, period: string): Promise<CachedResult<UsageCacheResponse>>;
|
|
282
290
|
usageRequests(
|
|
283
291
|
tenantId: string,
|
|
284
292
|
period: string,
|
|
285
293
|
limit: number,
|
|
286
294
|
httpStatus?: number,
|
|
287
|
-
): Promise<UsageRequestsResponse
|
|
295
|
+
): Promise<CachedResult<UsageRequestsResponse>>;
|
|
288
296
|
usageRequestDetail(tenantId: string, requestId: string): Promise<UsageRequestDetail | undefined>;
|
|
289
297
|
}
|
|
290
298
|
export interface ObservabilityConfig {
|
|
@@ -294,12 +302,25 @@ export interface ObservabilityConfig {
|
|
|
294
302
|
|
|
295
303
|
export function createObservabilityOperations(config: ObservabilityConfig) {
|
|
296
304
|
const operations = {
|
|
297
|
-
|
|
305
|
+
/**
|
|
306
|
+
* The cached reads below answer with a `Response` rather than a plain
|
|
307
|
+
* object, because a repeated poll is a `304` and a `304` has no body to
|
|
308
|
+
* serialize. `ifNoneMatch` is the client's `If-None-Match`; a match costs
|
|
309
|
+
* ~200 bytes instead of the whole payload.
|
|
310
|
+
*/
|
|
311
|
+
async getSystemHealth(
|
|
312
|
+
access: AccessDecision | undefined,
|
|
313
|
+
ifNoneMatch: string | null,
|
|
314
|
+
): Promise<Response> {
|
|
298
315
|
const a = requireTenantScope(access, "dashboard:read");
|
|
299
|
-
const
|
|
300
|
-
return
|
|
316
|
+
const result = await config.store.health(a.tenantId);
|
|
317
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
301
318
|
},
|
|
302
|
-
async getTenantUsage(
|
|
319
|
+
async getTenantUsage(
|
|
320
|
+
access: AccessDecision | undefined,
|
|
321
|
+
period = "7d",
|
|
322
|
+
ifNoneMatch: string | null = null,
|
|
323
|
+
): Promise<Response> {
|
|
303
324
|
const a = requireTenantScope(access, "dashboard:read");
|
|
304
325
|
if (!isSupportedUsagePeriod(period)) {
|
|
305
326
|
throw new ConsoleDomainError(
|
|
@@ -308,7 +329,8 @@ export function createObservabilityOperations(config: ObservabilityConfig) {
|
|
|
308
329
|
`Unsupported usage period: ${period}. Use "24h", "7d", "30d", or "all".`,
|
|
309
330
|
);
|
|
310
331
|
}
|
|
311
|
-
|
|
332
|
+
const result = await config.store.usage(a.tenantId, period);
|
|
333
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
312
334
|
},
|
|
313
335
|
async listEvents(
|
|
314
336
|
access: AccessDecision | undefined,
|
|
@@ -354,22 +376,27 @@ export function createObservabilityOperations(config: ObservabilityConfig) {
|
|
|
354
376
|
async getUsageSummary(
|
|
355
377
|
access: AccessDecision | undefined,
|
|
356
378
|
period = "24h",
|
|
357
|
-
|
|
379
|
+
ifNoneMatch: string | null = null,
|
|
380
|
+
): Promise<Response> {
|
|
358
381
|
const tenantId = operations.requireTenant(access);
|
|
359
|
-
|
|
382
|
+
const result = await config.store.usageSummary(tenantId, operations.requirePeriod(period));
|
|
383
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
360
384
|
},
|
|
361
385
|
async getUsageChart(
|
|
362
386
|
access: AccessDecision | undefined,
|
|
363
387
|
period = "24h",
|
|
364
|
-
|
|
388
|
+
ifNoneMatch: string | null = null,
|
|
389
|
+
): Promise<Response> {
|
|
365
390
|
const tenantId = operations.requireTenant(access);
|
|
366
|
-
|
|
391
|
+
const result = await config.store.usageChart(tenantId, operations.requirePeriod(period));
|
|
392
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
367
393
|
},
|
|
368
394
|
async getUsageBy(
|
|
369
395
|
access: AccessDecision | undefined,
|
|
370
396
|
dimension: UsageDimension,
|
|
371
397
|
period = "24h",
|
|
372
|
-
|
|
398
|
+
ifNoneMatch: string | null = null,
|
|
399
|
+
): Promise<Response> {
|
|
373
400
|
const tenantId = operations.requireTenant(access);
|
|
374
401
|
if (!(USAGE_DIMENSIONS as readonly string[]).includes(dimension)) {
|
|
375
402
|
throw new ConsoleDomainError(
|
|
@@ -378,21 +405,25 @@ export function createObservabilityOperations(config: ObservabilityConfig) {
|
|
|
378
405
|
`Unsupported usage dimension: ${dimension}. Use ${USAGE_DIMENSIONS.map((d) => `"${d}"`).join(", ")}.`,
|
|
379
406
|
);
|
|
380
407
|
}
|
|
381
|
-
|
|
408
|
+
const result = await config.store.usageBy(tenantId, dimension, operations.requirePeriod(period));
|
|
409
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
382
410
|
},
|
|
383
411
|
async getUsageCache(
|
|
384
412
|
access: AccessDecision | undefined,
|
|
385
413
|
period = "24h",
|
|
386
|
-
|
|
414
|
+
ifNoneMatch: string | null = null,
|
|
415
|
+
): Promise<Response> {
|
|
387
416
|
const tenantId = operations.requireTenant(access);
|
|
388
|
-
|
|
417
|
+
const result = await config.store.usageCache(tenantId, operations.requirePeriod(period));
|
|
418
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
389
419
|
},
|
|
390
420
|
async listUsageRequests(
|
|
391
421
|
access: AccessDecision | undefined,
|
|
392
422
|
period = "24h",
|
|
393
423
|
limit?: string,
|
|
394
424
|
httpStatus?: string,
|
|
395
|
-
|
|
425
|
+
ifNoneMatch: string | null = null,
|
|
426
|
+
): Promise<Response> {
|
|
396
427
|
const tenantId = operations.requireTenant(access);
|
|
397
428
|
let parsedStatus: number | undefined;
|
|
398
429
|
if (httpStatus !== undefined) {
|
|
@@ -401,12 +432,13 @@ export function createObservabilityOperations(config: ObservabilityConfig) {
|
|
|
401
432
|
throw new ConsoleDomainError("invalid_request", 400, "Invalid HTTP status filter");
|
|
402
433
|
}
|
|
403
434
|
}
|
|
404
|
-
|
|
435
|
+
const result = await config.store.usageRequests(
|
|
405
436
|
tenantId,
|
|
406
437
|
operations.requirePeriod(period),
|
|
407
438
|
parseQueryLimit(limit, 50, 500),
|
|
408
439
|
parsedStatus,
|
|
409
440
|
);
|
|
441
|
+
return conditionalResponse(ifNoneMatch, result, result.value);
|
|
410
442
|
},
|
|
411
443
|
async getUsageRequestDetail(
|
|
412
444
|
access: AccessDecision | undefined,
|
|
@@ -23,16 +23,31 @@ export function createObservabilityRoutes(config: ObservabilityConfig): Elysia {
|
|
|
23
23
|
return new Elysia()
|
|
24
24
|
.error(consoleErrorHandler("Observability operation failed"))
|
|
25
25
|
.get("/system/health", async ({ request }) => {
|
|
26
|
-
return await factory.getSystemHealth(
|
|
26
|
+
return await factory.getSystemHealth(
|
|
27
|
+
config.accessResolver(request),
|
|
28
|
+
request.headers.get("if-none-match"),
|
|
29
|
+
);
|
|
27
30
|
})
|
|
28
31
|
.get("/system/usage", { query: usageQuery }, async ({ request, query }) => {
|
|
29
|
-
return await factory.getTenantUsage(
|
|
32
|
+
return await factory.getTenantUsage(
|
|
33
|
+
config.accessResolver(request),
|
|
34
|
+
query.period,
|
|
35
|
+
request.headers.get("if-none-match"),
|
|
36
|
+
);
|
|
30
37
|
})
|
|
31
38
|
.get("/system/usage/summary", { query: usageQuery }, async ({ request, query }) => {
|
|
32
|
-
return await factory.getUsageSummary(
|
|
39
|
+
return await factory.getUsageSummary(
|
|
40
|
+
config.accessResolver(request),
|
|
41
|
+
query.period,
|
|
42
|
+
request.headers.get("if-none-match"),
|
|
43
|
+
);
|
|
33
44
|
})
|
|
34
45
|
.get("/system/usage/chart", { query: usageQuery }, async ({ request, query }) => {
|
|
35
|
-
return await factory.getUsageChart(
|
|
46
|
+
return await factory.getUsageChart(
|
|
47
|
+
config.accessResolver(request),
|
|
48
|
+
query.period,
|
|
49
|
+
request.headers.get("if-none-match"),
|
|
50
|
+
);
|
|
36
51
|
})
|
|
37
52
|
// One `by-<dimension>` route per member of `USAGE_DIMENSIONS`, so a new
|
|
38
53
|
// dimension cannot be added to the validator without its route existing.
|
|
@@ -48,11 +63,20 @@ export function createObservabilityRoutes(config: ObservabilityConfig): Elysia {
|
|
|
48
63
|
`Unknown usage dimension: ${dimension}`,
|
|
49
64
|
);
|
|
50
65
|
}
|
|
51
|
-
return await factory.getUsageBy(
|
|
66
|
+
return await factory.getUsageBy(
|
|
67
|
+
config.accessResolver(request),
|
|
68
|
+
dimension,
|
|
69
|
+
query.period,
|
|
70
|
+
request.headers.get("if-none-match"),
|
|
71
|
+
);
|
|
52
72
|
},
|
|
53
73
|
)
|
|
54
74
|
.get("/system/usage/cache", { query: usageQuery }, async ({ request, query }) => {
|
|
55
|
-
return await factory.getUsageCache(
|
|
75
|
+
return await factory.getUsageCache(
|
|
76
|
+
config.accessResolver(request),
|
|
77
|
+
query.period,
|
|
78
|
+
request.headers.get("if-none-match"),
|
|
79
|
+
);
|
|
56
80
|
})
|
|
57
81
|
.get("/system/usage/requests", { query: usageRequestsQuery }, async ({ request, query }) => {
|
|
58
82
|
return await factory.listUsageRequests(
|
|
@@ -60,6 +84,7 @@ export function createObservabilityRoutes(config: ObservabilityConfig): Elysia {
|
|
|
60
84
|
query.period,
|
|
61
85
|
query.limit,
|
|
62
86
|
query.httpStatus,
|
|
87
|
+
request.headers.get("if-none-match"),
|
|
63
88
|
);
|
|
64
89
|
})
|
|
65
90
|
.get("/system/usage/requests/:requestId", async ({ request, params }) => {
|