cartethyia 2.5.1 → 2.5.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 +5 -1
- package/dist/dashboard/assets/{AccountCooldown-ChpdzxRH.js → AccountCooldown-Bjo92Qat.js} +1 -1
- package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
- package/dist/dashboard/assets/{BannedPage-ChqnTRpZ.js → BannedPage-hfCzi-Ux.js} +1 -1
- package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BXUaRCQQ.js → CliToolsPage-DEDyKBlC.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +1 -0
- package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js +1 -0
- package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js +1 -0
- package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js +1 -0
- package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +1 -0
- package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js +1 -0
- package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +1 -0
- package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +1 -0
- package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js +1 -0
- package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js +1 -0
- package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-DqK5E26W.js → StudioPage-CmkUrgdj.js} +3 -3
- package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js +56 -0
- package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-BmDbsior.js → api-keys-uNywYlfv.js} +1 -1
- package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
- package/dist/dashboard/assets/{app-C-Y4oZT0.js → app-CafWtVXA.js} +1 -1
- package/dist/dashboard/assets/{app-C-Y4oZT0.js.gz → app-CafWtVXA.js.gz} +0 -0
- package/dist/dashboard/assets/app-DxivpfYG.css +2 -0
- package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
- package/dist/dashboard/assets/{assert-BHfaei-X.js → assert-NHwYq95t.js} +1 -1
- package/dist/dashboard/assets/{assert-BHfaei-X.js.gz → assert-NHwYq95t.js.gz} +0 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js +1 -0
- package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js +1 -0
- package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
- package/dist/dashboard/assets/card--AtFedAq.js +1 -0
- package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-B0wBq-RL.js → cli-tools-B3e7dx3N.js} +1 -1
- package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js +1 -0
- package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js +1 -0
- package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js +1 -0
- package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
- package/dist/dashboard/assets/entry-CkryrIea.js +4 -0
- package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js +1 -0
- package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
- package/dist/dashboard/assets/{entry-D4O2229X.js → entry-jy9_4-Ke.js} +1 -1
- package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js +1 -0
- package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
- package/dist/dashboard/assets/{index-CcD1eSJ6.js → index-BydyKaDz.js} +2 -2
- package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js +1 -0
- package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
- package/dist/dashboard/assets/input-BG41et9G.js +1 -0
- package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js +1 -0
- package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js +1 -0
- package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
- package/dist/dashboard/assets/{live-yfEJFF9T.js → live-BJGYRBnR.js} +1 -1
- package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CT-TxSNX.js → model-bans-DeVLDA7I.js} +1 -1
- package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js +2 -0
- package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
- package/dist/dashboard/assets/{routing-Cs6jl49L.js → routing-CcusqXA7.js} +1 -1
- package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js +41 -0
- package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
- package/dist/dashboard/assets/{settings-CfcTcOyU.js → settings-mpmXTgUa.js} +1 -1
- package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js +1 -0
- package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js +1 -0
- package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js +1 -0
- package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BQKMcJ1H.js → useMutation-w4u4CVue.js} +1 -1
- package/dist/dashboard/assets/useMutation-w4u4CVue.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 +51 -54
- package/package.json +1 -1
- package/scripts/cli/command/console-menu.ts +248 -246
- package/scripts/cli/command/tray.ts +208 -204
- package/src/boot/app.ts +3 -2
- package/src/config/spec.ts +26 -2
- package/src/console/backup/routes.ts +52 -1
- package/src/console/backup/service.ts +60 -4
- package/src/console/observability/contracts.ts +11 -1
- package/src/console/observability/store.ts +9 -6
- package/src/console/share/share-router.ts +6 -9
- package/src/observability/payload-capture.ts +366 -344
- package/src/observability/payload-store.ts +12 -2
- package/src/observability/telemetry-status.ts +28 -16
- package/src/persistence/stores/api-key-store.ts +6 -0
- package/src/persistence/stores/share-store.ts +583 -556
- package/src/persistence/stores/telemetry-store.ts +1 -1
- package/src/protocol/response/gemini.ts +18 -4
- package/src/providers/discovery/openrouter-free-roster.ts +3 -11
- package/src/providers/discovery/probe-phases.ts +42 -5
- package/src/providers/discovery/probing-service.ts +25 -2
- package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
- package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
- package/src/providers/integrations/claude/claude.ts +3 -1
- package/src/providers/integrations/cline/cline-quota.ts +8 -11
- package/src/providers/integrations/cloudflareai/cloudflareai-models.ts +3 -1
- package/src/providers/integrations/inferhub.ts +3 -1
- package/src/providers/integrations/kilo/kilo.ts +0 -1
- package/src/providers/integrations/kiro/kiro-catalog.ts +8 -2
- package/src/providers/integrations/opencode/provider.ts +532 -532
- package/src/providers/integrations/openrouter/provider.ts +5 -15
- package/src/providers/integrations/tokenharbor.ts +2 -2
- package/src/providers/integrations/trae/trae-catalog.ts +5 -3
- package/src/providers/operations/account-health-service.ts +23 -0
- package/src/providers/operations/client-versions.ts +9 -2
- package/src/providers/provider-metadata.ts +7 -1
- package/src/security/api-key-auth.ts +23 -0
- package/src/transport/dispatch/attempt-finalize.ts +46 -5
- package/src/transport/dispatch/proxy-request.ts +9 -0
- package/src/transport/dispatch/streaming/stream.ts +16 -2
- package/src/transport/guards/auth-guard.ts +2 -3
- package/src/transport/routing/route-catalog.ts +806 -799
- package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
- package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js +0 -1
- package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js +0 -1
- package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js +0 -1
- package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js +0 -1
- package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +0 -1
- package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +0 -1
- package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +0 -1
- package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +0 -7
- package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +0 -1
- package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js +0 -1
- package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js +0 -1
- package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +0 -1
- package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +0 -56
- package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
- package/dist/dashboard/assets/app-BbpSarVV.css +0 -2
- package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
- package/dist/dashboard/assets/badge-DpXT4Ixt.js +0 -1
- package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
- package/dist/dashboard/assets/banner-CSAAWLJv.js +0 -1
- package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
- package/dist/dashboard/assets/card-DEhRXXw4.js +0 -1
- package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
- package/dist/dashboard/assets/check-D69FzdvC.js +0 -1
- package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +0 -1
- package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js +0 -1
- package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BvQeBMfP.js +0 -1
- package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CMwPXwQB.js +0 -1
- package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
- package/dist/dashboard/assets/entry--yj84zk3.js +0 -4
- package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
- package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
- package/dist/dashboard/assets/entry-DKvr5Q9K.js +0 -1
- package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-zliLvBwr.js +0 -1
- package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
- package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
- package/dist/dashboard/assets/inline-D1IlZZ8H.js +0 -1
- package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
- package/dist/dashboard/assets/input-BpVZdrbc.js +0 -1
- package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
- package/dist/dashboard/assets/layout-BioLW7gN.js +0 -1
- package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
- package/dist/dashboard/assets/list-row-CwVcGAIc.js +0 -1
- package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
- package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js +0 -1
- package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
- package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
- package/dist/dashboard/assets/select-XPJ-dIw3.js +0 -41
- package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
- package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
- package/dist/dashboard/assets/stack-C9_uUjaA.js +0 -1
- package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
- package/dist/dashboard/assets/state-DA0B95UQ.js +0 -1
- package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
- package/dist/dashboard/assets/switch-Cittxy-x.js +0 -1
- package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
- package/dist/dashboard/assets/toast-CNt_jzO4.js +0 -2
- package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
|
@@ -1,204 +1,208 @@
|
|
|
1
|
-
import { isNewerRelease } from "../../../src/console/updates/version-compare";
|
|
2
|
-
import { resolvePackagePath } from "../../../src/config/package-paths";
|
|
3
|
-
import { resolvePort } from "../../../src/config/runtime";
|
|
4
|
-
import { ensureDrainToken } from "../gateway/drain-token";
|
|
5
|
-
import { startGateway } from "../gateway/spawn";
|
|
6
|
-
import { stopGateway } from "../gateway/stop";
|
|
7
|
-
import { readInstalledVersion, readLatestPublishedVersion } from "../support/version";
|
|
8
|
-
import { reportTrayStartup } from "../support/startup-signal";
|
|
9
|
-
import { createTray } from "../tray";
|
|
10
|
-
import { openUrl } from "../ui/background";
|
|
11
|
-
import { exportBackup, resetConsoleAccount } from "./admin";
|
|
12
|
-
import { printUpdateInstructions } from "./update";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
|
|
16
|
-
* on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
|
|
17
|
-
* an empty console tab open for the tray's whole lifetime, and that window is
|
|
18
|
-
* owned by Windows Terminal — not by this process, so it cannot be found and
|
|
19
|
-
* hidden from inside.
|
|
20
|
-
*
|
|
21
|
-
* The gateway it starts is detached, which is what makes the health poll below
|
|
22
|
-
* necessary — see {@link TRAY_HEALTH_POLL_MS}.
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* How often the tray asks whether the gateway is still answering.
|
|
27
|
-
*
|
|
28
|
-
* 10 s: often enough that an operator who stops the gateway by hand sees the
|
|
29
|
-
* icon go away, and far below the cost of a request. A detached gateway can no
|
|
30
|
-
* longer be waited on, so this poll is the only signal left.
|
|
31
|
-
*/
|
|
32
|
-
const TRAY_HEALTH_POLL_MS = 10_000;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Consecutive misses to forgive before a dead gateway closes the tray.
|
|
36
|
-
*
|
|
37
|
-
* One refusal is not a stopped gateway — a probe can lose a race with startup
|
|
38
|
-
* or a slow request — and closing the icon on one would make startup feel
|
|
39
|
-
* random.
|
|
40
|
-
*/
|
|
41
|
-
const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
|
|
42
|
-
|
|
43
|
-
export async function startTray(): Promise<void> {
|
|
44
|
-
const port = resolvePort();
|
|
45
|
-
const drainToken = ensureDrainToken();
|
|
46
|
-
const dashboardUrl = `http://127.0.0.1:${port}/console`;
|
|
47
|
-
const repositoryUrl = "https://github.com/risunCode/cartethyia";
|
|
48
|
-
const currentVersion = await readInstalledVersion();
|
|
49
|
-
|
|
50
|
-
let gateway: Bun.Subprocess | null = null;
|
|
51
|
-
let closing = false;
|
|
52
|
-
let busy = false;
|
|
53
|
-
let resetArmed = false;
|
|
54
|
-
let latestVersion: string | null = null;
|
|
55
|
-
let closePromise: Promise<void> | undefined;
|
|
56
|
-
|
|
57
|
-
const tray = createTray({
|
|
58
|
-
iconPath: resolvePackagePath(
|
|
59
|
-
"scripts",
|
|
60
|
-
"cli",
|
|
61
|
-
"tray",
|
|
62
|
-
"assets",
|
|
63
|
-
process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
|
|
64
|
-
),
|
|
65
|
-
tooltip: `Cartethyia ${currentVersion}`,
|
|
66
|
-
items: [
|
|
67
|
-
{ kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: true },
|
|
68
|
-
{ kind: "separator" },
|
|
69
|
-
{ kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
|
|
70
|
-
{ kind: "action", id: "reset", title: "Reset console password" },
|
|
71
|
-
{ kind: "action", id: "backup", title: "Backup" },
|
|
72
|
-
{ kind: "separator" },
|
|
73
|
-
{ kind: "action", id: "exit", title: "Exit" },
|
|
74
|
-
],
|
|
75
|
-
onSelect: (id: string) => void handleSelect(id),
|
|
76
|
-
onExit: () => void close(1),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const close = (exitCode = 0): Promise<void> => {
|
|
80
|
-
if (closePromise) return closePromise;
|
|
81
|
-
closing = true;
|
|
82
|
-
closePromise = (async () => {
|
|
83
|
-
const current: Bun.Subprocess | null = gateway;
|
|
84
|
-
gateway = null;
|
|
85
|
-
if (current) await stopGateway(current, port, drainToken);
|
|
86
|
-
await tray.close();
|
|
87
|
-
process.exitCode = exitCode;
|
|
88
|
-
})();
|
|
89
|
-
return closePromise;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const refreshVersion = async (): Promise<void> => {
|
|
93
|
-
const fetched = await readLatestPublishedVersion();
|
|
94
|
-
if (closing) return;
|
|
95
|
-
latestVersion = fetched;
|
|
96
|
-
if (fetched === null) {
|
|
97
|
-
tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
const newer = isNewerRelease(currentVersion, fetched);
|
|
101
|
-
tray.update(
|
|
102
|
-
"update",
|
|
103
|
-
newer
|
|
104
|
-
? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
|
|
105
|
-
: `Cartethyia ${currentVersion} \u00b7 up to date`,
|
|
106
|
-
// Always clickable: the row opens the repository, and only offers the
|
|
107
|
-
// update command when there is something newer.
|
|
108
|
-
true,
|
|
109
|
-
);
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const runUpdate = async (): Promise<void> => {
|
|
113
|
-
if (busy) return;
|
|
114
|
-
busy = true;
|
|
115
|
-
const latest = await readLatestPublishedVersion();
|
|
116
|
-
await close();
|
|
117
|
-
printUpdateInstructions(currentVersion, latest);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
1
|
+
import { isNewerRelease } from "../../../src/console/updates/version-compare";
|
|
2
|
+
import { resolvePackagePath } from "../../../src/config/package-paths";
|
|
3
|
+
import { resolvePort } from "../../../src/config/runtime";
|
|
4
|
+
import { ensureDrainToken } from "../gateway/drain-token";
|
|
5
|
+
import { startGateway } from "../gateway/spawn";
|
|
6
|
+
import { stopGateway } from "../gateway/stop";
|
|
7
|
+
import { readInstalledVersion, readLatestPublishedVersion } from "../support/version";
|
|
8
|
+
import { reportTrayStartup } from "../support/startup-signal";
|
|
9
|
+
import { createTray } from "../tray";
|
|
10
|
+
import { openUrl } from "../ui/background";
|
|
11
|
+
import { exportBackup, resetConsoleAccount } from "./admin";
|
|
12
|
+
import { printUpdateInstructions } from "./update";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Runs the gateway with a tray icon. The tray is platform-specific (`tray/`):
|
|
16
|
+
* on Windows it is a PowerShell NotifyIcon, because a Node tray library leaves
|
|
17
|
+
* an empty console tab open for the tray's whole lifetime, and that window is
|
|
18
|
+
* owned by Windows Terminal — not by this process, so it cannot be found and
|
|
19
|
+
* hidden from inside.
|
|
20
|
+
*
|
|
21
|
+
* The gateway it starts is detached, which is what makes the health poll below
|
|
22
|
+
* necessary — see {@link TRAY_HEALTH_POLL_MS}.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How often the tray asks whether the gateway is still answering.
|
|
27
|
+
*
|
|
28
|
+
* 10 s: often enough that an operator who stops the gateway by hand sees the
|
|
29
|
+
* icon go away, and far below the cost of a request. A detached gateway can no
|
|
30
|
+
* longer be waited on, so this poll is the only signal left.
|
|
31
|
+
*/
|
|
32
|
+
const TRAY_HEALTH_POLL_MS = 10_000;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Consecutive misses to forgive before a dead gateway closes the tray.
|
|
36
|
+
*
|
|
37
|
+
* One refusal is not a stopped gateway — a probe can lose a race with startup
|
|
38
|
+
* or a slow request — and closing the icon on one would make startup feel
|
|
39
|
+
* random.
|
|
40
|
+
*/
|
|
41
|
+
const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
|
|
42
|
+
|
|
43
|
+
export async function startTray(): Promise<void> {
|
|
44
|
+
const port = resolvePort();
|
|
45
|
+
const drainToken = ensureDrainToken();
|
|
46
|
+
const dashboardUrl = `http://127.0.0.1:${port}/console`;
|
|
47
|
+
const repositoryUrl = "https://github.com/risunCode/cartethyia";
|
|
48
|
+
const currentVersion = await readInstalledVersion();
|
|
49
|
+
|
|
50
|
+
let gateway: Bun.Subprocess | null = null;
|
|
51
|
+
let closing = false;
|
|
52
|
+
let busy = false;
|
|
53
|
+
let resetArmed = false;
|
|
54
|
+
let latestVersion: string | null = null;
|
|
55
|
+
let closePromise: Promise<void> | undefined;
|
|
56
|
+
|
|
57
|
+
const tray = createTray({
|
|
58
|
+
iconPath: resolvePackagePath(
|
|
59
|
+
"scripts",
|
|
60
|
+
"cli",
|
|
61
|
+
"tray",
|
|
62
|
+
"assets",
|
|
63
|
+
process.platform === "win32" ? "cartethyia.ico" : "cartethyia.png",
|
|
64
|
+
),
|
|
65
|
+
tooltip: `Cartethyia ${currentVersion}`,
|
|
66
|
+
items: [
|
|
67
|
+
{ kind: "action", id: "update", title: `Cartethyia ${currentVersion} \u00b7 checking\u2026`, enabled: true },
|
|
68
|
+
{ kind: "separator" },
|
|
69
|
+
{ kind: "action", id: "console", title: `Open Console \u00b7 ${dashboardUrl}` },
|
|
70
|
+
{ kind: "action", id: "reset", title: "Reset console password" },
|
|
71
|
+
{ kind: "action", id: "backup", title: "Backup" },
|
|
72
|
+
{ kind: "separator" },
|
|
73
|
+
{ kind: "action", id: "exit", title: "Exit" },
|
|
74
|
+
],
|
|
75
|
+
onSelect: (id: string) => void handleSelect(id),
|
|
76
|
+
onExit: () => void close(1),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const close = (exitCode = 0): Promise<void> => {
|
|
80
|
+
if (closePromise) return closePromise;
|
|
81
|
+
closing = true;
|
|
82
|
+
closePromise = (async () => {
|
|
83
|
+
const current: Bun.Subprocess | null = gateway;
|
|
84
|
+
gateway = null;
|
|
85
|
+
if (current) await stopGateway(current, port, drainToken);
|
|
86
|
+
await tray.close();
|
|
87
|
+
process.exitCode = exitCode;
|
|
88
|
+
})();
|
|
89
|
+
return closePromise;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const refreshVersion = async (): Promise<void> => {
|
|
93
|
+
const fetched = await readLatestPublishedVersion();
|
|
94
|
+
if (closing) return;
|
|
95
|
+
latestVersion = fetched;
|
|
96
|
+
if (fetched === null) {
|
|
97
|
+
tray.update("update", `Cartethyia ${currentVersion} \u00b7 check for updates`, true);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const newer = isNewerRelease(currentVersion, fetched);
|
|
101
|
+
tray.update(
|
|
102
|
+
"update",
|
|
103
|
+
newer
|
|
104
|
+
? `Cartethyia ${currentVersion} \u00b7 update to ${fetched}`
|
|
105
|
+
: `Cartethyia ${currentVersion} \u00b7 up to date`,
|
|
106
|
+
// Always clickable: the row opens the repository, and only offers the
|
|
107
|
+
// update command when there is something newer.
|
|
108
|
+
true,
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const runUpdate = async (): Promise<void> => {
|
|
113
|
+
if (busy) return;
|
|
114
|
+
busy = true;
|
|
115
|
+
const latest = await readLatestPublishedVersion();
|
|
116
|
+
await close();
|
|
117
|
+
printUpdateInstructions(currentVersion, latest);
|
|
118
|
+
// The tray has no terminal of its own, so these instructions are only
|
|
119
|
+
// visible when it was started from one. Hold that terminal open long
|
|
120
|
+
// enough to copy the command, then exit.
|
|
121
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 180_000));
|
|
122
|
+
process.exit(0);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
async function handleSelect(id: string): Promise<void> {
|
|
126
|
+
if (closing) return;
|
|
127
|
+
if (id === "exit") return void (await close());
|
|
128
|
+
if (id === "console") return void (await openUrl(dashboardUrl));
|
|
129
|
+
if (id === "backup") {
|
|
130
|
+
const result = await exportBackup(port, drainToken);
|
|
131
|
+
if (result !== null) {
|
|
132
|
+
console.log(
|
|
133
|
+
`Backup written to ${result.path} (${(result.bytes / 1024).toFixed(1)} KiB). Telemetry is not included.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (id === "reset") {
|
|
139
|
+
// A tray click cannot ask for the typed confirmation the menu uses, so
|
|
140
|
+
// the first click arms the action and the second one runs it.
|
|
141
|
+
if (!resetArmed) {
|
|
142
|
+
resetArmed = true;
|
|
143
|
+
tray.update("reset", "Reset console password \u00b7 click again to delete", true);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
resetArmed = false;
|
|
147
|
+
tray.update("reset", "Reset console password", true);
|
|
148
|
+
await resetConsoleAccount(port, drainToken);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (id === "update") {
|
|
152
|
+
// Nothing to install: the row is the way to the repository instead.
|
|
153
|
+
if (latestVersion === null || !isNewerRelease(currentVersion, latestVersion)) {
|
|
154
|
+
await openUrl(repositoryUrl);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await runUpdate();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
process.once("SIGINT", () => void close());
|
|
162
|
+
process.once("SIGTERM", () => void close());
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
await tray.ready();
|
|
166
|
+
// Detached, unlike every other caller: the gateway must outlive this
|
|
167
|
+
// process. Attached, it dies with the tray — and the tray dies with the
|
|
168
|
+
// terminal that launched it, so closing that terminal took the gateway
|
|
169
|
+
// down too and "Hide to tray" left nothing serving. `stopGateway` drains
|
|
170
|
+
// over HTTP before it ever reaches for `kill()`, so detaching costs the
|
|
171
|
+
// exit path nothing: the tray can still stop the gateway on Exit.
|
|
172
|
+
gateway = await startGateway(port, drainToken, { detached: true });
|
|
173
|
+
// The launcher that spawned this tray is blocked on a signal file: it
|
|
174
|
+
// cannot read our stdio, because the tray is detached. Without this write
|
|
175
|
+
// it waits out its 60 s deadline and reports a failure for a tray that is
|
|
176
|
+
// running fine.
|
|
177
|
+
reportTrayStartup({ status: "ready", url: dashboardUrl });
|
|
178
|
+
console.log(`Cartethyia is running at ${dashboardUrl}`);
|
|
179
|
+
void refreshVersion();
|
|
180
|
+
|
|
181
|
+
let misses = 0;
|
|
182
|
+
// A detached child's `exited` never settles on Windows, so the old
|
|
183
|
+
// `await current.exited` would have parked here forever and the tray would
|
|
184
|
+
// never notice the gateway going away. Poll its health endpoint instead:
|
|
185
|
+
// that answers for a process we no longer own, which is the whole point of
|
|
186
|
+
// detaching it. The probe is the same one startup used, so a gateway that
|
|
187
|
+
// is up reads as up regardless of who spawned it.
|
|
188
|
+
while (!closing) {
|
|
189
|
+
await Bun.sleep(TRAY_HEALTH_POLL_MS);
|
|
190
|
+
if (closing || gateway === null) continue;
|
|
191
|
+
const alive = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
192
|
+
signal: AbortSignal.timeout(2_000),
|
|
193
|
+
}).then((response) => response.ok).catch(() => false);
|
|
194
|
+
if (alive) {
|
|
195
|
+
misses = 0;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
misses += 1;
|
|
199
|
+
if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
|
|
200
|
+
gateway = null;
|
|
201
|
+
console.error("Cartethyia stopped answering; the tray is closing.");
|
|
202
|
+
await close(1);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
await close(1);
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
package/src/boot/app.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { GatewayError, publicGatewayErrorBody } from "../protocol/gateway-error"
|
|
|
15
15
|
import { shutdownError, shutdownNotice } from "../transport/shutdown-notice";
|
|
16
16
|
import type { CanonicalAdapter } from "../transport/contracts";
|
|
17
17
|
import type { ApiKeyAuthorizationSnapshot } from "../security/api-key-auth";
|
|
18
|
+
import { unknownApiKeyError } from "../security/api-key-auth";
|
|
18
19
|
import { getDbHandle } from "../persistence/boot";
|
|
19
20
|
import { isPgHandle } from "../persistence/db-pg";
|
|
20
21
|
import { resolveRedisClient } from "../persistence/redis";
|
|
@@ -408,7 +409,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
|
|
|
408
409
|
const handleModelsList = async ({ request }: { request: Request }): Promise<Response> => {
|
|
409
410
|
const state = requestStateStore.require(request);
|
|
410
411
|
const auth = state.authorization;
|
|
411
|
-
if (!auth) throw
|
|
412
|
+
if (!auth) throw unknownApiKeyError();
|
|
412
413
|
const data = await queryAllowedModels(auth);
|
|
413
414
|
return new Response(JSON.stringify({ object: "list", data }), {
|
|
414
415
|
headers: {
|
|
@@ -431,7 +432,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
|
|
|
431
432
|
}): Promise<Response> => {
|
|
432
433
|
const state = requestStateStore.require(request);
|
|
433
434
|
const auth = state.authorization;
|
|
434
|
-
if (!auth) throw
|
|
435
|
+
if (!auth) throw unknownApiKeyError();
|
|
435
436
|
const raw = params?.["*"] ?? params?.id ?? "";
|
|
436
437
|
const targetId = new URL(request.url).searchParams.get("id") ?? raw;
|
|
437
438
|
const found = await publicModelStore.getPublicModelDetail(
|
package/src/config/spec.ts
CHANGED
|
@@ -157,7 +157,14 @@ export const CONFIG_SPEC = {
|
|
|
157
157
|
},
|
|
158
158
|
|
|
159
159
|
// Upstream request timeout + retry backoff
|
|
160
|
-
|
|
160
|
+
//
|
|
161
|
+
// Strictly under 90s: a client whose own deadline is 90s must never be
|
|
162
|
+
// waiting on a gateway that is still holding a request open past it, and a
|
|
163
|
+
// provider that has not answered within 75s is not answering — the request
|
|
164
|
+
// is queued upstream or the egress is stuck, and the honest move is to fail
|
|
165
|
+
// over to another account rather than burn the caller's whole budget on one.
|
|
166
|
+
// 120s outlived the clients that call this, which cap lower.
|
|
167
|
+
CARTETHYIA_UPSTREAM_TIMEOUT_MS: { kind: "int", default: 75_000, min: 5_000, max: 600_000 },
|
|
161
168
|
CARTETHYIA_STREAM_STALL_TIMEOUT_MS: {
|
|
162
169
|
kind: "int",
|
|
163
170
|
default: 360_000,
|
|
@@ -181,7 +188,24 @@ export const CONFIG_SPEC = {
|
|
|
181
188
|
CARTETHYIA_ACCOUNT_QUOTA_COOLDOWN_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
182
189
|
CARTETHYIA_ACCOUNT_MODEL_CAPACITY_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
183
190
|
CARTETHYIA_ACCOUNT_TRANSIENT_COOLDOWN_MS: { kind: "int", default: 30_000, min: 0, max: 86_400_000 },
|
|
184
|
-
|
|
191
|
+
/**
|
|
192
|
+
* An unmapped upstream failure cools the account down for 15 minutes.
|
|
193
|
+
*
|
|
194
|
+
* It used to be 60s, which put the account straight back into rotation and
|
|
195
|
+
* re-dialled an upstream that had just answered something this gateway does
|
|
196
|
+
* not understand — so the same unknown error was retried every minute, and
|
|
197
|
+
* an account that was actually broken contributed a steady stream of failed
|
|
198
|
+
* attempts instead of one. Fifteen minutes is long enough for a transient
|
|
199
|
+
* provider condition to clear on its own and short enough that no operator
|
|
200
|
+
* action is needed to recover it. It is still a cooldown with a deadline,
|
|
201
|
+
* never `disabled`, so nothing here can strand an account.
|
|
202
|
+
*/
|
|
203
|
+
CARTETHYIA_ACCOUNT_UNCLASSIFIED_COOLDOWN_MS: {
|
|
204
|
+
kind: "int",
|
|
205
|
+
default: 900_000,
|
|
206
|
+
min: 0,
|
|
207
|
+
max: 86_400_000,
|
|
208
|
+
},
|
|
185
209
|
CARTETHYIA_ACCOUNT_COOLDOWN_ESCALATION_MAX_MS: { kind: "int", default: 3_600_000, min: 0, max: 86_400_000 },
|
|
186
210
|
CARTETHYIA_POOL_COOLDOWN_MS: { kind: "int", default: 120_000, min: 0, max: 86_400_000 },
|
|
187
211
|
CARTETHYIA_ACCOUNT_AUTH_REJECTED_COOLDOWN_MS: { kind: "int", default: 300_000, min: 0, max: 86_400_000 },
|
|
@@ -78,6 +78,24 @@ const BACKUP_SCOPES = ["dashboard:write"] as const;
|
|
|
78
78
|
const importBody = t.Object({
|
|
79
79
|
password: t.String(),
|
|
80
80
|
backup: t.Unknown(),
|
|
81
|
+
/**
|
|
82
|
+
* Table names to restore. Absent or empty restores every table in the file,
|
|
83
|
+
* which is what the dashboard's checklist defaults to and what the API did
|
|
84
|
+
* before selection existed.
|
|
85
|
+
*/
|
|
86
|
+
tables: t.Optional(t.Array(t.String())),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The inspect body: a file, no password.
|
|
91
|
+
*
|
|
92
|
+
* Inspection writes nothing and reads no secret — it only reports which tables
|
|
93
|
+
* the file carries and which of them the live schema has — so it needs no
|
|
94
|
+
* re-authentication. Requiring the console password here would mean typing it
|
|
95
|
+
* twice for one restore.
|
|
96
|
+
*/
|
|
97
|
+
const inspectBody = t.Object({
|
|
98
|
+
backup: t.Unknown(),
|
|
81
99
|
});
|
|
82
100
|
|
|
83
101
|
const exportQuery = t.Object({
|
|
@@ -164,7 +182,12 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
164
182
|
// from nowhere else. A request body may not name the tenant it writes
|
|
165
183
|
// into: that would let any holder of the scope import into, and
|
|
166
184
|
// overwrite, a tenant they are not.
|
|
167
|
-
const result = await config.backupFor(request).restore(
|
|
185
|
+
const result = await config.backupFor(request).restore(
|
|
186
|
+
body.password,
|
|
187
|
+
body.backup,
|
|
188
|
+
access.tenantId,
|
|
189
|
+
body.tables === undefined ? undefined : { tables: body.tables },
|
|
190
|
+
);
|
|
168
191
|
// Routing-visible write: the restore has committed, so the cached
|
|
169
192
|
// snapshot must be rebuilt before the next `/v1/*` dispatch.
|
|
170
193
|
await config.snapshotInvalidator?.invalidate();
|
|
@@ -214,5 +237,33 @@ export function createBackupRoutes(config: BackupRoutesConfig): Elysia {
|
|
|
214
237
|
} catch (error) {
|
|
215
238
|
return errorResponse(error, set, "Delete all failed");
|
|
216
239
|
}
|
|
240
|
+
})
|
|
241
|
+
.post("/inspect", { body: inspectBody }, async ({ request, body, set }) => {
|
|
242
|
+
try {
|
|
243
|
+
const access = requireAnyScope(config.accessResolver(request), BACKUP_SCOPES);
|
|
244
|
+
if (access.tenantId === null) {
|
|
245
|
+
throw new ConsoleDomainError("tenant_required", 403, "Tenant isolation required");
|
|
246
|
+
}
|
|
247
|
+
// Same byte bound as the import, for the same reason: the file is
|
|
248
|
+
// parsed here too, and an unbounded one would be buffered before any
|
|
249
|
+
// validation could refuse it.
|
|
250
|
+
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
251
|
+
const actualBytes = JSON.stringify(body ?? null).length;
|
|
252
|
+
if (
|
|
253
|
+
(Number.isFinite(declared) && declared > MAX_BACKUP_BYTES) ||
|
|
254
|
+
actualBytes > MAX_BACKUP_BYTES
|
|
255
|
+
) {
|
|
256
|
+
set.status = 413;
|
|
257
|
+
return {
|
|
258
|
+
error: `backup exceeds the ${MAX_BACKUP_BYTES} byte limit`,
|
|
259
|
+
code: "request_too_large",
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
// Reads nothing and writes nothing: the checklist the restore wizard
|
|
263
|
+
// shows, so an operator can pick sections before typing a password.
|
|
264
|
+
return config.backupFor(request).inspect(body.backup);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return errorResponse(error, set, "Backup inspection failed");
|
|
267
|
+
}
|
|
217
268
|
}) as unknown as Elysia;
|
|
218
269
|
}
|