cartethyia 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +2 -0
- package/dist/dashboard/assets/{BannedPage-D-J75hBM.js → BannedPage-2TJD37Hb.js} +1 -1
- package/dist/dashboard/assets/BannedPage-2TJD37Hb.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-xefJNRmJ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js +1 -0
- package/dist/dashboard/assets/CliToolsPage-Rj9iHQ3F.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-QoJ6foxo.js → CombosPage-JEbYJFxw.js} +1 -1
- package/dist/dashboard/assets/CombosPage-JEbYJFxw.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-DpNRay4C.js → ConsoleLogPage-C3xx_inM.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-C3xx_inM.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-Dw306gJU.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-jaBo1TWV.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-DubdQcvX.js → LoginPage-C4w7vea-.js} +1 -1
- package/dist/dashboard/assets/LoginPage-C4w7vea-.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BtPcAfot.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-BtPcAfot.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js +1 -0
- package/dist/dashboard/assets/OverviewPage-CGC5gNs1.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js +4 -0
- package/dist/dashboard/assets/ProviderDetailPage-DQcthNGt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-0OsXRGXe.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js +9 -0
- package/dist/dashboard/assets/ProxyPage-DdPc8slb.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-ATpaxrbl.js → QuotaPage-Ci5O4BnH.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-Ci5O4BnH.js.gz +0 -0
- package/dist/dashboard/assets/{SettingsPage-CETophpx.js → SettingsPage-CDZjTt87.js} +1 -1
- package/dist/dashboard/assets/SettingsPage-CDZjTt87.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js +1 -0
- package/dist/dashboard/assets/SetupPage-BQS0Rx_u.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CAK-EN9z.js → SortableList-BAL5npBC.js} +1 -1
- package/dist/dashboard/assets/SortableList-BAL5npBC.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-B7W2_RhT.js → StudioPage-TP7voAQ0.js} +3 -3
- package/dist/dashboard/assets/StudioPage-TP7voAQ0.js.gz +0 -0
- package/dist/dashboard/assets/{UsagePage-6T4EFDjW.js → UsagePage-Cj_fEzhg.js} +13 -13
- package/dist/dashboard/assets/UsagePage-Cj_fEzhg.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-CUi6UXsR.js → api-keys-MbI7e7rt.js} +1 -1
- package/dist/dashboard/assets/api-keys-MbI7e7rt.js.gz +0 -0
- package/dist/dashboard/assets/app-B2vyPoII.css +2 -0
- package/dist/dashboard/assets/app-B2vyPoII.css.gz +0 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
- package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
- package/dist/dashboard/assets/{assert-67xXH5rf.js → assert-C0YYuZJ4.js} +1 -1
- package/dist/dashboard/assets/assert-C0YYuZJ4.js.gz +0 -0
- package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-B_DMpMJN.js} +1 -1
- package/dist/dashboard/assets/badge-B_DMpMJN.js.gz +0 -0
- package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DMrMPmth.js} +1 -1
- package/dist/dashboard/assets/banner-DMrMPmth.js.gz +0 -0
- package/dist/dashboard/assets/{card-Cca1XC6G.js → card-Be5Uzamx.js} +1 -1
- package/dist/dashboard/assets/card-Be5Uzamx.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-KvsWa1Jl.js → cli-tools-muFRphDu.js} +1 -1
- package/dist/dashboard/assets/cli-tools-muFRphDu.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DOfrVmsi.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DOfrVmsi.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-C9omdjQt.js} +1 -1
- package/dist/dashboard/assets/dialog-C9omdjQt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js +1 -0
- package/dist/dashboard/assets/drawer-Bm8WKaGY.js.gz +0 -0
- package/dist/dashboard/assets/{entry-_xHo5x6r.js → entry-DS90gUks.js} +1 -1
- package/dist/dashboard/assets/entry-DS90gUks.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BLBLxX0h.js → entry-j40LJotO.js} +3 -3
- package/dist/dashboard/assets/entry-j40LJotO.js.gz +0 -0
- package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-q5HwZ8nN.js} +1 -1
- package/dist/dashboard/assets/entry-q5HwZ8nN.js.gz +0 -0
- package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/{formatters-mpBPqAQ8.js → formatters-BTScrQpe.js} +1 -1
- package/dist/dashboard/assets/formatters-BTScrQpe.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-Orjuzemr.js} +1 -1
- package/dist/dashboard/assets/github-badge-Orjuzemr.js.gz +0 -0
- package/dist/dashboard/assets/{index-D6QzEC_n.js → index-cPrX5vn6.js} +2 -2
- package/dist/dashboard/assets/index-cPrX5vn6.js.gz +0 -0
- package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-Dvux8w-0.js} +1 -1
- package/dist/dashboard/assets/inline-Dvux8w-0.js.gz +0 -0
- package/dist/dashboard/assets/{input-C_ILAvBb.js → input-CU1Rgf3p.js} +1 -1
- package/dist/dashboard/assets/input-CU1Rgf3p.js.gz +0 -0
- package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-C_7A6H55.js} +1 -1
- package/dist/dashboard/assets/layout-C_7A6H55.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-CzFvogbj.js} +1 -1
- package/dist/dashboard/assets/list-row-CzFvogbj.js.gz +0 -0
- package/dist/dashboard/assets/{live-D0mm--ME.js → live-KaG15HLQ.js} +1 -1
- package/dist/dashboard/assets/live-KaG15HLQ.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-JJGQm8TD.js} +1 -1
- package/dist/dashboard/assets/mutation-JJGQm8TD.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-BNDZnsUE.js} +1 -1
- package/dist/dashboard/assets/numbers-BNDZnsUE.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-CLaNz8Xz.js} +1 -1
- package/dist/dashboard/assets/query-keys-CLaNz8Xz.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BnIpi1IJ.js → routing-BEH32QOC.js} +1 -1
- package/dist/dashboard/assets/routing-BEH32QOC.js.gz +0 -0
- package/dist/dashboard/assets/{select-Cj5Nw43g.js → select-B3iTetHN.js} +1 -1
- package/dist/dashboard/assets/select-B3iTetHN.js.gz +0 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
- package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
- package/dist/dashboard/assets/{settings-BAFhDgX8.js → settings-Dt3Eqbnq.js} +1 -1
- package/dist/dashboard/assets/settings-Dt3Eqbnq.js.gz +0 -0
- package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-KrOrm7JZ.js} +1 -1
- package/dist/dashboard/assets/stack-KrOrm7JZ.js.gz +0 -0
- package/dist/dashboard/assets/{state-CWoo7ty5.js → state-DLS7IDMy.js} +1 -1
- package/dist/dashboard/assets/state-DLS7IDMy.js.gz +0 -0
- package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-42ItvbAs.js} +1 -1
- package/dist/dashboard/assets/switch-42ItvbAs.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-Dmnfh1Ue.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dmnfh1Ue.js.gz +0 -0
- package/dist/dashboard/index.html +3 -2
- package/dist/dashboard/index.html.gz +0 -0
- package/dist/dashboard/precompressed-assets.json +117 -115
- package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
- package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
- package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
- package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
- package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +22 -0
- package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
- package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
- package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
- package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
- package/package.json +1 -1
- package/src/config/network.ts +1 -1
- package/src/console/api-keys/bucket-spend.ts +7 -7
- package/src/console/api-keys/contracts.ts +423 -410
- package/src/console/api-keys/routes.ts +118 -19
- package/src/console/backup/export-endpoint.ts +8 -11
- package/src/console/backup/routes.ts +40 -8
- package/src/console/backup/service.ts +11 -6
- package/src/console/backup/store.ts +384 -88
- package/src/console/cli-tools/contracts.ts +10 -4
- package/src/console/dashboard-assets.ts +16 -1
- package/src/console/domain-registration.ts +2 -0
- package/src/console/quota/account-quota-tenant.ts +8 -1
- package/src/console/quota/account-quota.ts +4 -1
- package/src/console/quota/native-identity.ts +189 -0
- package/src/console/remote-support/contracts.ts +12 -6
- package/src/console/remote-support/service.ts +2 -2
- package/src/console/share/share-router.ts +39 -6
- package/src/console/share/share-stats.ts +64 -25
- package/src/console/share/share-usage.ts +24 -7
- package/src/console/system-prompt/contracts.ts +102 -0
- package/src/console/system-prompt/routes.ts +165 -0
- package/src/main.ts +11 -0
- package/src/network/outbound-fetch.ts +281 -226
- package/src/network/pool/agent.ts +7 -0
- package/src/observability/event-query.ts +118 -7
- package/src/persistence/db-pg.ts +42 -0
- package/src/persistence/schema.ts +102 -0
- package/src/persistence/stores/api-key-store.ts +753 -581
- package/src/persistence/stores/share-store.ts +65 -13
- package/src/persistence/stores/telemetry-store.ts +57 -1
- package/src/persistence/system-prompt-store.ts +104 -0
- package/src/protocol/reasoning.ts +65 -1
- package/src/protocol/request/chat.ts +1 -0
- package/src/providers/default-registry.ts +1 -0
- package/src/providers/integrations/antigravity/antigravity.ts +20 -23
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
- package/src/providers/integrations/buddy/buddy-media.ts +5 -1
- package/src/providers/integrations/buddy/codebuddy/provider.ts +5 -1
- package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -1
- package/src/providers/integrations/cline/cline.ts +7 -0
- package/src/providers/integrations/github/github.ts +20 -1
- package/src/providers/integrations/hermes.ts +5 -1
- package/src/providers/integrations/kilo/kilo.ts +4 -0
- package/src/providers/integrations/ollamacloud-models.ts +50 -0
- package/src/providers/integrations/openrouter/provider.ts +4 -0
- package/src/providers/integrations/qoder/qoder-checkin.ts +224 -51
- package/src/providers/integrations/qoder/qoder-native-identity-store.ts +156 -0
- package/src/providers/integrations/qoder/qoder-oauth.ts +24 -4
- package/src/providers/integrations/qoder/qoder-realm.ts +20 -0
- package/src/providers/integrations/qoder/qoder-text-tools.ts +277 -0
- package/src/providers/integrations/qoder/qoder-umid.ts +288 -0
- package/src/providers/integrations/qoder/qoder.ts +1375 -1641
- package/src/providers/operations/account-error-classification.ts +1682 -1660
- package/src/providers/operations/account-health-service.ts +619 -621
- package/src/providers/operations/client-versions.ts +37 -22
- package/src/providers/operations/provider-catalog-service.ts +11 -1
- package/src/providers/provider-contracts.ts +15 -2
- package/src/security/access-control.ts +19 -0
- package/src/security/api-key-auth.ts +14 -1
- package/src/security/cli-client-fingerprint.ts +9 -2
- package/src/transport/dispatch/empty-stream.ts +224 -224
- package/src/transport/dispatch/proxy-request.ts +31 -2
- package/src/transport/dispatch/streaming/stream.ts +16 -1
- package/src/transport/dispatch/upstream.ts +10 -0
- package/src/transport/failure-policy.ts +33 -0
- package/src/transport/request/preparer.ts +17 -1
- package/src/transport/request/system-prompt-injection.ts +89 -0
- package/src/transport/request/tenant-preferences.ts +43 -0
- package/src/transport/routing/admission.ts +13 -3
- package/src/transport/routing/route-catalog.ts +41 -12
- package/src/transport/routing/route-model.ts +53 -0
- package/src/transport/routing/router.ts +306 -228
- package/src/transport/surface/responses/encode.ts +2 -3
- package/src/transport/surface/responses/parse.ts +49 -7
- package/src/types/undici.d.ts +33 -0
- package/src/workers/daily-checkin.ts +2 -0
- package/dist/dashboard/assets/BannedPage-D-J75hBM.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-CVe6RaBH.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js +0 -1
- package/dist/dashboard/assets/CliToolsPage-Dd8ZSamp.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-QoJ6foxo.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-DpNRay4C.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-CKUFTcW4.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-CzSLiDzr.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DubdQcvX.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DuWzAQFL.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js +0 -4
- package/dist/dashboard/assets/ProviderDetailPage-BKEKe2Fm.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-Bwotu8UJ.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js +0 -9
- package/dist/dashboard/assets/ProxyPage-D7hpWbg9.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-ATpaxrbl.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CETophpx.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js +0 -1
- package/dist/dashboard/assets/SetupPage-BdgECjqq.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CAK-EN9z.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-B7W2_RhT.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-6T4EFDjW.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
- package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
- package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
- package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
- package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
- package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
- package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
- package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
- package/dist/dashboard/assets/entry-BLBLxX0h.js.gz +0 -0
- package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
- package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
- package/dist/dashboard/assets/formatters-mpBPqAQ8.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
- package/dist/dashboard/assets/index-D6QzEC_n.js.gz +0 -0
- package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
- package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
- package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
- package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
- package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
- package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
- package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
- package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
- package/dist/dashboard/assets/select-Cj5Nw43g.js.gz +0 -0
- package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
- package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
- package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
- package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js → app-wvwCi4Eh.js} +0 -0
- /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-wvwCi4Eh.js.gz} +0 -0
|
@@ -13,7 +13,12 @@ import { ConsoleDomainError, errorResponse, requireTenantScope } from "../shared
|
|
|
13
13
|
import { literalUnion } from "../shared/elysia-schema";
|
|
14
14
|
import { API_KEY_MODES, API_KEY_MODEL_ACCESS_MODES, type ShareLinkKind } from "../../persistence/schema";
|
|
15
15
|
import type { ApiKeyConfig, ApiKeyResponse, UpdateApiKeyResponse } from "./contracts";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ApiKeyPatch,
|
|
18
|
+
ApiKeyRecord,
|
|
19
|
+
SharedChildSortColumn,
|
|
20
|
+
SharedChildrenPageOptions,
|
|
21
|
+
} from "../../persistence/stores/api-key-store";
|
|
17
22
|
import {
|
|
18
23
|
generateApiKeySecret,
|
|
19
24
|
isDefaultGatewayApiKey,
|
|
@@ -34,6 +39,39 @@ import {
|
|
|
34
39
|
import type { SharedKeyActivityDetail, SharedKeySummary } from "../share/share-usage";
|
|
35
40
|
import { popupImageBytes } from "./share-popup-image";
|
|
36
41
|
|
|
42
|
+
const SHARED_CHILD_SORT_COLUMNS = new Set<SharedChildSortColumn>([
|
|
43
|
+
"createdAt",
|
|
44
|
+
"label",
|
|
45
|
+
"keyPrefix",
|
|
46
|
+
"issuedClientIp",
|
|
47
|
+
"revokedAt",
|
|
48
|
+
"enabled",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** Parses and bounds the paginated share-child list query. */
|
|
52
|
+
function parseSharedChildrenQuery(query: URLSearchParams): SharedChildrenPageOptions {
|
|
53
|
+
const limitRaw = query.get("limit");
|
|
54
|
+
const offsetRaw = query.get("offset");
|
|
55
|
+
const sortRaw = query.get("sort");
|
|
56
|
+
const orderRaw = query.get("order");
|
|
57
|
+
const statusRaw = query.get("status");
|
|
58
|
+
const limit = limitRaw === null ? 50 : Number(limitRaw);
|
|
59
|
+
const offset = offsetRaw === null ? 0 : Number(offsetRaw);
|
|
60
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
|
|
61
|
+
throw new ConsoleDomainError("invalid_request", 400, "limit must be an integer between 1 and 500");
|
|
62
|
+
}
|
|
63
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
64
|
+
throw new ConsoleDomainError("invalid_request", 400, "offset must be a non-negative integer");
|
|
65
|
+
}
|
|
66
|
+
const sortBy = (sortRaw ?? "createdAt") as SharedChildSortColumn;
|
|
67
|
+
if (!SHARED_CHILD_SORT_COLUMNS.has(sortBy)) {
|
|
68
|
+
throw new ConsoleDomainError("invalid_request", 400, `unsupported sort column: ${String(sortBy)}`);
|
|
69
|
+
}
|
|
70
|
+
const order = orderRaw === "asc" ? "asc" : "desc";
|
|
71
|
+
const status = statusRaw === "active" || statusRaw === "revoked" ? statusRaw : "all";
|
|
72
|
+
return { limit, offset, sortBy, order, status };
|
|
73
|
+
}
|
|
74
|
+
|
|
37
75
|
export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
38
76
|
const operations = {
|
|
39
77
|
async listKeys(access: AccessDecision | undefined): Promise<ApiKeyResponse[]> {
|
|
@@ -44,18 +82,19 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
44
82
|
);
|
|
45
83
|
// A share template's own counter never moves for child traffic, so
|
|
46
84
|
// reporting it raw would show a template as untouched while the gateway
|
|
47
|
-
// is refusing its recipients on the parent budget. Report the figure
|
|
48
|
-
// gateway actually enforces: the template plus every
|
|
49
|
-
//
|
|
50
|
-
|
|
85
|
+
// is refusing its recipients on the parent budget. Report the figure
|
|
86
|
+
// the gateway actually enforces: the template's counter plus every
|
|
87
|
+
// telemetry total the family ever recorded — revoked/deleted children
|
|
88
|
+
// included (their spend is already spent), so the number survives
|
|
89
|
+
// cleanup and matches the share page.
|
|
51
90
|
return Promise.all(
|
|
52
91
|
topLevel.map(async (record) => {
|
|
53
92
|
const response = sanitizeApiKeyResponse(record);
|
|
54
|
-
if (record.keyMode !== "share"
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
if (record.keyMode !== "share") return response;
|
|
94
|
+
const family = await config.store
|
|
95
|
+
.sumFamilyTelemetryConsumed(authorized.tenantId, record.id)
|
|
96
|
+
.catch(() => 0);
|
|
97
|
+
return { ...response, tokensConsumed: response.tokensConsumed + family };
|
|
59
98
|
}),
|
|
60
99
|
);
|
|
61
100
|
},
|
|
@@ -146,6 +185,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
146
185
|
...(request.monthlyTokenLimit == null ? {} : { monthlyTokenLimit: request.monthlyTokenLimit }),
|
|
147
186
|
...(request.lifetimeTokenBudget == null ? {} : { lifetimeTokenBudget: request.lifetimeTokenBudget }),
|
|
148
187
|
...(request.maxConcurrentRequests == null ? {} : { maxConcurrentRequests: request.maxConcurrentRequests }),
|
|
188
|
+
...(request.maxChildren == null ? {} : { maxChildren: request.maxChildren }),
|
|
149
189
|
...(request.modelPrefix === undefined ? {} : { modelPrefix: request.modelPrefix }),
|
|
150
190
|
modelAccessMode: request.modelAccessMode ?? "whitelist",
|
|
151
191
|
...(request.modelList === undefined ? {} : { modelList: request.modelList }),
|
|
@@ -280,6 +320,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
280
320
|
...(patchRequest.monthlyTokenLimit === undefined ? {} : { monthlyTokenLimit: patchRequest.monthlyTokenLimit }),
|
|
281
321
|
...(patchRequest.lifetimeTokenBudget === undefined ? {} : { lifetimeTokenBudget: patchRequest.lifetimeTokenBudget }),
|
|
282
322
|
...(patchRequest.maxConcurrentRequests === undefined ? {} : { maxConcurrentRequests: patchRequest.maxConcurrentRequests }),
|
|
323
|
+
...(patchRequest.maxChildren === undefined ? {} : { maxChildren: patchRequest.maxChildren }),
|
|
283
324
|
...(patchRequest.modelPrefix === undefined ? {} : { modelPrefix: patchRequest.modelPrefix }),
|
|
284
325
|
...(patchRequest.modelAccessMode === undefined ? {} : { modelAccessMode: patchRequest.modelAccessMode }),
|
|
285
326
|
...(patchRequest.modelList === undefined ? {} : { modelList: patchRequest.modelList }),
|
|
@@ -376,7 +417,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
376
417
|
});
|
|
377
418
|
return { ...sanitizeApiKeyResponse(updated), ...(secret === undefined ? {} : { secret }) };
|
|
378
419
|
},
|
|
379
|
-
async
|
|
420
|
+
async deleteKey(access: AccessDecision | undefined, keyId: string): Promise<{ success: true }> {
|
|
380
421
|
const authorized = requireTenantScope(access, "dashboard:write");
|
|
381
422
|
const target = await config.store.get(authorized.tenantId, keyId);
|
|
382
423
|
if (!target)
|
|
@@ -401,7 +442,7 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
401
442
|
]);
|
|
402
443
|
await config.auditSink?.record({
|
|
403
444
|
access: authorized,
|
|
404
|
-
action: "api_key.
|
|
445
|
+
action: "api_key.deleted",
|
|
405
446
|
target: keyId,
|
|
406
447
|
});
|
|
407
448
|
return { success: true };
|
|
@@ -643,7 +684,14 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
643
684
|
async listSharedKeys(
|
|
644
685
|
access: AccessDecision | undefined,
|
|
645
686
|
parentKeyId: string,
|
|
646
|
-
|
|
687
|
+
query: URLSearchParams,
|
|
688
|
+
): Promise<{
|
|
689
|
+
rows: readonly SharedKeySummary[];
|
|
690
|
+
total: number;
|
|
691
|
+
active: number;
|
|
692
|
+
revoked: number;
|
|
693
|
+
consumed: number;
|
|
694
|
+
}> {
|
|
647
695
|
const authorized = requireTenantScope(access, "dashboard:read");
|
|
648
696
|
const activity = config.shareActivity;
|
|
649
697
|
if (!activity)
|
|
@@ -652,8 +700,14 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
652
700
|
if (!parent || parent.revokedAt !== undefined ||
|
|
653
701
|
parent.keyMode !== "share" || parent.parentKeyId !== undefined)
|
|
654
702
|
throw new ConsoleDomainError("key_not_found", 404, "Share template not found");
|
|
655
|
-
const
|
|
656
|
-
|
|
703
|
+
const options = parseSharedChildrenQuery(query);
|
|
704
|
+
const page = await config.store.listChildrenPage(authorized.tenantId, parentKeyId, options);
|
|
705
|
+
const rows = await activity.getSharedKeySummaries(
|
|
706
|
+
authorized.tenantId,
|
|
707
|
+
page.rows,
|
|
708
|
+
parentKeyId,
|
|
709
|
+
);
|
|
710
|
+
return { rows, total: page.total, active: page.active, revoked: page.revoked, consumed: page.consumed };
|
|
657
711
|
},
|
|
658
712
|
async getSharedKeyActivity(
|
|
659
713
|
access: AccessDecision | undefined,
|
|
@@ -668,11 +722,37 @@ export function createApiKeyOperations(config: ApiKeyConfig) {
|
|
|
668
722
|
if (!parent || parent.revokedAt !== undefined ||
|
|
669
723
|
parent.keyMode !== "share" || parent.parentKeyId !== undefined)
|
|
670
724
|
throw new ConsoleDomainError("key_not_found", 404, "Share template not found");
|
|
671
|
-
|
|
672
|
-
|
|
725
|
+
// Membership is one indexed row read, not a scan of the whole family.
|
|
726
|
+
const child = await config.store.get(authorized.tenantId, childKeyId);
|
|
727
|
+
if (!child || child.parentKeyId !== parentKeyId)
|
|
673
728
|
throw new ConsoleDomainError("key_not_found", 404, "Shared API key not found");
|
|
674
729
|
return activity.getSharedKeyDetail(authorized.tenantId, childKeyId);
|
|
675
730
|
},
|
|
731
|
+
async revokeSharedScope(
|
|
732
|
+
access: AccessDecision | undefined,
|
|
733
|
+
parentKeyId: string,
|
|
734
|
+
scope: "active" | "unused",
|
|
735
|
+
): Promise<{ success: true; deleted: number }> {
|
|
736
|
+
const authorized = requireTenantScope(access, "dashboard:write");
|
|
737
|
+
const parent = await config.store.get(authorized.tenantId, parentKeyId);
|
|
738
|
+
if (!parent || parent.keyMode !== "share" || parent.parentKeyId !== undefined)
|
|
739
|
+
throw new ConsoleDomainError("key_not_found", 404, "Share template not found");
|
|
740
|
+
const deleted = await config.store.revokeChildrenByScope(
|
|
741
|
+
authorized.tenantId,
|
|
742
|
+
parentKeyId,
|
|
743
|
+
scope,
|
|
744
|
+
);
|
|
745
|
+
// Raw batched deletes bypass the per-key purge path; drop the whole auth
|
|
746
|
+
// cache so no resolved identity survives for a removed recipient.
|
|
747
|
+
invalidateApiKeyCache();
|
|
748
|
+
await config.auditSink?.record({
|
|
749
|
+
access: authorized,
|
|
750
|
+
action: "api_key.share_revoked_scope",
|
|
751
|
+
target: parentKeyId,
|
|
752
|
+
detail: { scope, deleted },
|
|
753
|
+
});
|
|
754
|
+
return { success: true, deleted };
|
|
755
|
+
},
|
|
676
756
|
async revokeShare(
|
|
677
757
|
access: AccessDecision | undefined,
|
|
678
758
|
keyId: string,
|
|
@@ -798,7 +878,11 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
798
878
|
})
|
|
799
879
|
.get("/:keyId/shared-keys", async ({ request, params, set }) => {
|
|
800
880
|
try {
|
|
801
|
-
return await factory.listSharedKeys(
|
|
881
|
+
return await factory.listSharedKeys(
|
|
882
|
+
config.accessResolver(request),
|
|
883
|
+
params.keyId,
|
|
884
|
+
new URL(request.url).searchParams,
|
|
885
|
+
);
|
|
802
886
|
} catch (error) {
|
|
803
887
|
return errorResponse(error, set, "API-key operation failed");
|
|
804
888
|
}
|
|
@@ -814,6 +898,21 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
814
898
|
return errorResponse(error, set, "API-key operation failed");
|
|
815
899
|
}
|
|
816
900
|
})
|
|
901
|
+
.post(
|
|
902
|
+
"/:keyId/shared-keys/revoke-scope",
|
|
903
|
+
{ body: t.Object({ scope: t.Union([t.Literal("active"), t.Literal("unused")]) }) },
|
|
904
|
+
async ({ request, params, body, set }) => {
|
|
905
|
+
try {
|
|
906
|
+
return await factory.revokeSharedScope(
|
|
907
|
+
config.accessResolver(request),
|
|
908
|
+
params.keyId,
|
|
909
|
+
body.scope,
|
|
910
|
+
);
|
|
911
|
+
} catch (error) {
|
|
912
|
+
return errorResponse(error, set, "API-key operation failed");
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
)
|
|
817
916
|
.delete("/:keyId/shares/:shareId", async ({ request, params, set }) => {
|
|
818
917
|
try {
|
|
819
918
|
return await factory.revokeShare(
|
|
@@ -846,7 +945,7 @@ export function createApiKeyRoutes(config: ApiKeyConfig): Elysia {
|
|
|
846
945
|
})
|
|
847
946
|
.delete("/:keyId", async ({ request, params, set }) => {
|
|
848
947
|
try {
|
|
849
|
-
return await factory.
|
|
948
|
+
return await factory.deleteKey(config.accessResolver(request), params.keyId);
|
|
850
949
|
} catch (error) {
|
|
851
950
|
return errorResponse(error, set, "API-key operation failed");
|
|
852
951
|
}
|
|
@@ -24,7 +24,7 @@ import { join } from "node:path";
|
|
|
24
24
|
import { GatewayError } from "../../protocol/gateway-error";
|
|
25
25
|
import { isLoopbackPeer, tokenMatches } from "../../transport/drain-endpoint";
|
|
26
26
|
import { BACKUP_SECTIONS, tablesForSection } from "./contracts";
|
|
27
|
-
import {
|
|
27
|
+
import { compressBackupStream, exportBackupStream } from "./store";
|
|
28
28
|
import type { CartethyiaDatabase } from "../../persistence/db-handle";
|
|
29
29
|
|
|
30
30
|
export interface BackupExportDeps {
|
|
@@ -67,16 +67,13 @@ export function createBackupExportHandler(
|
|
|
67
67
|
if (tenantId === null)
|
|
68
68
|
throw new GatewayError("invalid_request", 409, "no tenant exists; run setup first");
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// backup are the same file format and either can be restored by the other.
|
|
78
|
-
const json = JSON.stringify(payload);
|
|
79
|
-
const gzipped = Bun.gzipSync(new TextEncoder().encode(json));
|
|
70
|
+
// One gzip pass over the streamed document: a tenant with hundreds of
|
|
71
|
+
// thousands of rows is read page by page and only the compressed bytes
|
|
72
|
+
// land in the buffer — the same file format a console backup produces, so
|
|
73
|
+
// a CLI backup and a dashboard backup remain restorable by the other.
|
|
74
|
+
const stream = await exportBackupStream(deps.db, tablesForSection("config"), tenantId);
|
|
75
|
+
const gzipped = await compressBackupStream(stream.jsonChunks);
|
|
76
|
+
const counts = await stream.counts;
|
|
80
77
|
const target = directory ?? downloadDirectory();
|
|
81
78
|
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
82
79
|
const path = join(target, `cartethyia-backup-${stamp}.json.gz`);
|
|
@@ -22,7 +22,7 @@ import { ConsoleDomainError, errorResponse, requireAnyScope } from "../shared/er
|
|
|
22
22
|
import { NO_COMPRESS_HEADER } from "../response-compression";
|
|
23
23
|
import { MAX_BACKUP_BYTES } from "./contracts";
|
|
24
24
|
import type { BackupSection } from "./contracts";
|
|
25
|
-
import { DELETE_ALL_SCOPES,
|
|
25
|
+
import { DELETE_ALL_SCOPES, compressBackupStream } from "./store";
|
|
26
26
|
import type { BackupService } from "./service";
|
|
27
27
|
import type { AuditRecorder } from "../auth/audit-recorder";
|
|
28
28
|
|
|
@@ -48,7 +48,7 @@ export interface BackupRoutesConfig {
|
|
|
48
48
|
/**
|
|
49
49
|
* The tenant's API keys, read after a restore so their admission counters can
|
|
50
50
|
* be purged. A restore replaces `api_keys` wholesale, so a key that was
|
|
51
|
-
* revoked after the backup — and had its counters purged by `
|
|
51
|
+
* revoked after the backup — and had its counters purged by `deleteKey` — can
|
|
52
52
|
* come back with `revoked_at = NULL`; without purging, its lifetime counter
|
|
53
53
|
* is re-seeded from the restored total, granting a fresh full budget on top
|
|
54
54
|
* of everything already spent. Optional so a host that never restores keys
|
|
@@ -118,6 +118,27 @@ function parseSections(raw: string | undefined): readonly BackupSection[] | unde
|
|
|
118
118
|
.filter((part) => part.length > 0) as readonly BackupSection[];
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Streams the export document into the response body as it is written; one
|
|
123
|
+
* row-page of the tenant's data is in memory at a time.
|
|
124
|
+
*/
|
|
125
|
+
function payloadDocument(chunks: AsyncGenerator<string, void, void>): ReadableStream<Uint8Array> {
|
|
126
|
+
const encoder = new TextEncoder();
|
|
127
|
+
return new ReadableStream<Uint8Array>({
|
|
128
|
+
async pull(controller) {
|
|
129
|
+
const next = await chunks.next();
|
|
130
|
+
if (next.done) {
|
|
131
|
+
controller.close();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
controller.enqueue(encoder.encode(next.value));
|
|
135
|
+
},
|
|
136
|
+
async cancel() {
|
|
137
|
+
await chunks.return();
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
121
142
|
export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
122
143
|
return new Elysia({ prefix: "/backup" })
|
|
123
144
|
.get("/export", { query: exportQuery }, async ({ request, query, set }) => {
|
|
@@ -129,7 +150,7 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
129
150
|
const backup = config.backupFor(request);
|
|
130
151
|
const sections = parseSections(query.sections);
|
|
131
152
|
const compressed = query.compressed === "1";
|
|
132
|
-
const
|
|
153
|
+
const exportStream = await backup.export({
|
|
133
154
|
password: query.password,
|
|
134
155
|
tenantId: access.tenantId,
|
|
135
156
|
compressed,
|
|
@@ -138,11 +159,22 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
138
159
|
const stamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-");
|
|
139
160
|
set.headers["cache-control"] = "no-store";
|
|
140
161
|
if (!compressed) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
// The document is written through the stream, so a tenant with
|
|
163
|
+
// hundreds of thousands of rows is serialized here while only one
|
|
164
|
+
// row-page of it is ever in memory. The headers live on the
|
|
165
|
+
// Response itself: a native Response carries the stream, and the
|
|
166
|
+
// response wrapper would otherwise buffer it whole to gzip it —
|
|
167
|
+
// the same trade its ceiling path already made for large exports
|
|
168
|
+
// (rebuilt, uncompressed) — so this branch opts out of it.
|
|
169
|
+
const headers = new Headers({
|
|
170
|
+
"cache-control": "no-store",
|
|
171
|
+
"content-disposition": `attachment; filename="cartethyia_backup_${stamp}.json"`,
|
|
172
|
+
"content-type": "application/json; charset=utf-8",
|
|
173
|
+
[NO_COMPRESS_HEADER]: "1",
|
|
174
|
+
});
|
|
175
|
+
return new Response(payloadDocument(exportStream.jsonChunks), { headers });
|
|
144
176
|
}
|
|
145
|
-
const bytes =
|
|
177
|
+
const bytes = await compressBackupStream(exportStream.jsonChunks);
|
|
146
178
|
set.headers["content-type"] = "application/json; charset=utf-8";
|
|
147
179
|
// Opt out of the response wrapper's gzip rather than declaring
|
|
148
180
|
// `content-encoding: gzip`: the bytes here are base64 of a gzip
|
|
@@ -193,7 +225,7 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
193
225
|
// snapshot must be rebuilt before the next `/v1/*` dispatch.
|
|
194
226
|
await config.snapshotInvalidator?.invalidate();
|
|
195
227
|
// A restore can re-insert keys the tenant revoked after the backup, and
|
|
196
|
-
// `
|
|
228
|
+
// `deleteKey` purged their counters when they went away. Drop the whole
|
|
197
229
|
// auth cache (the restore is rare, and a stale entry for any key must
|
|
198
230
|
// not survive it) and purge admission state for every key the tenant
|
|
199
231
|
// now has, so a resurrected key cannot start with a fresh budget.
|
|
@@ -40,7 +40,8 @@ import {
|
|
|
40
40
|
applyRestore,
|
|
41
41
|
decompressBackup,
|
|
42
42
|
deleteAll,
|
|
43
|
-
|
|
43
|
+
exportBackupStream,
|
|
44
|
+
type BackupExportStream,
|
|
44
45
|
type DeleteAllResult,
|
|
45
46
|
type DeleteAllScope,
|
|
46
47
|
} from "./store";
|
|
@@ -269,17 +270,16 @@ async function omitRowsForUnavailableProviders(
|
|
|
269
270
|
export class BackupService {
|
|
270
271
|
constructor(private readonly options: BackupServiceOptions) {}
|
|
271
272
|
|
|
272
|
-
async export(options: ExportOptions): Promise<
|
|
273
|
+
async export(options: ExportOptions): Promise<BackupExportStream> {
|
|
273
274
|
if (!(await this.options.verifyPassword(options.password))) {
|
|
274
275
|
throw new ConsoleDomainError("unauthorized", 401, "password is incorrect");
|
|
275
276
|
}
|
|
276
277
|
const sections = sectionsOrDefault(options.sections);
|
|
277
|
-
|
|
278
|
+
return exportBackupStream(
|
|
278
279
|
this.options.db,
|
|
279
280
|
sections.flatMap((section) => tablesForSection(section)),
|
|
280
281
|
options.tenantId,
|
|
281
282
|
);
|
|
282
|
-
return { payload, counts };
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
/**
|
|
@@ -453,11 +453,16 @@ export class BackupService {
|
|
|
453
453
|
sections: readonly BackupSection[] | undefined,
|
|
454
454
|
tenantId: string,
|
|
455
455
|
): Promise<Record<string, number>> {
|
|
456
|
-
const
|
|
456
|
+
const stream = exportBackupStream(
|
|
457
457
|
this.options.db,
|
|
458
458
|
sectionsOrDefault(sections).flatMap((section) => tablesForSection(section)),
|
|
459
459
|
tenantId,
|
|
460
460
|
);
|
|
461
|
-
|
|
461
|
+
// Walk the stream so the tally settles, discarding the bytes: a preview
|
|
462
|
+
// must not hold the payload either.
|
|
463
|
+
for await (const _chunk of stream.jsonChunks) {
|
|
464
|
+
// discard
|
|
465
|
+
}
|
|
466
|
+
return stream.counts;
|
|
462
467
|
}
|
|
463
468
|
}
|