cartethyia 2.5.10 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
- package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
- package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
- package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
- package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
- package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
- package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
- package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
- package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
- package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
- package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
- package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
- package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
- package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
- package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
- package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
- package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
- package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
- package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
- package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
- package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
- package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
- package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
- package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
- package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
- package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
- package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
- package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
- package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
- package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
- package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
- package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
- package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
- package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
- package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
- package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
- package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
- package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
- package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
- package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
- package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
- package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
- package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
- package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
- package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
- package/dist/dashboard/assets/useMutation-EDFRkn1i.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 -51
- package/migrations/README.md +52 -52
- package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
- package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
- package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
- package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
- package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
- package/package.json +96 -95
- package/scripts/cli/admin.ts +126 -128
- package/scripts/cli/autostart.ts +237 -237
- package/scripts/cli/background.ts +120 -110
- package/scripts/cli/bin/carte.cjs +8 -8
- package/scripts/cli/bin/cartethyia.cjs +167 -167
- package/scripts/cli/console-menu.ts +39 -30
- package/scripts/cli/controller.ts +145 -0
- package/scripts/cli/env.ts +99 -99
- package/scripts/cli/gateway.ts +264 -260
- package/scripts/cli/instance.ts +234 -224
- package/scripts/cli/main.ts +188 -167
- package/scripts/cli/platform.ts +64 -64
- package/scripts/cli/prompt.ts +217 -79
- package/scripts/cli/services.ts +88 -88
- package/scripts/cli/setup.ts +445 -445
- package/scripts/cli/start.ts +85 -85
- package/scripts/cli/startup-signal.ts +23 -23
- package/scripts/cli/telemetry.ts +70 -0
- package/scripts/cli/tray/index.ts +263 -263
- package/scripts/cli/tray/windows.ps1 +107 -107
- package/scripts/cli/tray.ts +29 -4
- package/scripts/cli/update.ts +39 -39
- package/scripts/cli/version.ts +67 -67
- package/scripts/cli/wordmark.ts +58 -58
- package/src/boot/app.ts +16 -1
- package/src/boot/dependencies.ts +128 -25
- package/src/config/resources.ts +137 -0
- package/src/config/runtime.ts +133 -76
- package/src/config/spec.ts +30 -29
- package/src/config/telemetry.ts +0 -25
- package/src/console/api-keys/bucket-spend.ts +14 -17
- package/src/console/api-keys/contracts.ts +410 -410
- package/src/console/api-keys/routes.ts +12 -0
- package/src/console/audit/store.ts +2 -3
- package/src/console/auth/setup-service.ts +8 -1
- package/src/console/backup/routes.ts +7 -0
- package/src/console/backup/service.ts +463 -463
- package/src/console/backup/store.ts +16 -11
- package/src/console/backup/validate.ts +1 -4
- package/src/console/cli-tools/fs-ops.ts +18 -2
- package/src/console/data-management/contracts.ts +1 -8
- package/src/console/data-management/operations.ts +61 -48
- package/src/console/data-management/routes.ts +4 -2
- package/src/console/domain-registration.ts +25 -3
- package/src/console/inferhub/client.ts +139 -0
- package/src/console/inferhub/key-store.ts +129 -0
- package/src/console/inferhub/normalize.ts +347 -0
- package/src/console/inferhub/routes.ts +411 -0
- package/src/console/inferhub/store.ts +109 -0
- package/src/console/inferhub/types.ts +183 -0
- package/src/console/observability/contracts.ts +2 -41
- package/src/console/observability/store.ts +176 -371
- package/src/console/providers/catalog/public-model-store.ts +0 -0
- package/src/console/providers/catalog/store.ts +23 -36
- package/src/console/quota/account-quota-shared.ts +52 -52
- package/src/console/quota/account-quota-tenant.ts +16 -24
- package/src/console/quota/refresh.ts +441 -441
- package/src/console/routing/pools/import-parse.ts +206 -206
- package/src/console/routing/pools/operations.ts +543 -543
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/routes.ts +181 -181
- package/src/console/settings/contracts.ts +42 -40
- package/src/console/settings/store.ts +25 -25
- package/src/console/share/share-stats.ts +22 -4
- package/src/console/share/share-usage.ts +30 -4
- package/src/console/shared/cursor.ts +15 -0
- package/src/console/updates/routes.ts +12 -0
- package/src/main.ts +13 -5
- package/src/network/pool/resolver.ts +1104 -1104
- package/src/observability/client-name.ts +22 -0
- package/src/observability/event-log.ts +5 -29
- package/src/observability/event-query.ts +690 -0
- package/src/observability/log-ring.ts +26 -2
- package/src/observability/log-sanitize.ts +118 -0
- package/src/observability/logger.ts +17 -13
- package/src/observability/metrics.ts +35 -0
- package/src/observability/payload-capture.ts +114 -41
- package/src/observability/payload-store.ts +249 -24
- package/src/observability/raw-event-writer.ts +118 -0
- package/src/observability/runtime-metrics.ts +71 -1
- package/src/observability/telemetry-buffer.ts +100 -17
- package/src/observability/telemetry-reconciliation.ts +417 -0
- package/src/persistence/boot.ts +20 -9
- package/src/persistence/db-pg.ts +13 -0
- package/src/persistence/db-sqlite.ts +569 -497
- package/src/persistence/dialect.ts +3 -2
- package/src/persistence/migrate.ts +181 -181
- package/src/persistence/schema.ts +1496 -1406
- package/src/persistence/shared-cache.ts +5 -1
- package/src/persistence/stores/api-key-store.ts +581 -581
- package/src/persistence/stores/telemetry-store.ts +124 -63
- package/src/protocol/reasoning-text.ts +30 -0
- package/src/protocol/response/chat.ts +33 -7
- package/src/protocol/response/codex.ts +14 -2
- package/src/protocol/response/messages.ts +5 -2
- package/src/providers/authentication/oauth-flow-store.ts +3 -10
- package/src/providers/bundled-discovery-providers.ts +122 -122
- package/src/providers/default-registry.ts +10 -0
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +43 -7
- package/src/providers/discovery/probing-service.ts +70 -32
- package/src/providers/integrations/antigravity/antigravity.ts +9 -2
- package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
- package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
- package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
- package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
- package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
- package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
- package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
- package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
- package/src/providers/integrations/claude/claude-quota.ts +2 -2
- package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
- package/src/providers/integrations/claude/claude.ts +3 -4
- package/src/providers/integrations/cline/cline.ts +94 -8
- package/src/providers/integrations/codex/codex.ts +61 -37
- package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
- package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
- package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
- package/src/providers/integrations/cursor/cursor-request.ts +11 -0
- package/src/providers/integrations/cursor/cursor.ts +29 -17
- package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
- package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
- package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
- package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
- package/src/providers/integrations/grok/grok-quota.ts +279 -279
- package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
- package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
- package/src/providers/integrations/hermes.ts +7 -1
- package/src/providers/integrations/inferhub.ts +201 -73
- package/src/providers/integrations/kilo/kilo.ts +152 -152
- package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
- package/src/providers/integrations/openrouter/provider.ts +129 -129
- package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
- package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
- package/src/providers/integrations/trae/trae-oauth.ts +4 -26
- package/src/providers/integrations/trae/trae-quota.ts +389 -384
- package/src/providers/integrations/trae/trae-realm.ts +15 -2
- package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
- package/src/providers/integrations/zcode/oauth.ts +1 -1
- package/src/providers/operations/account-health-service.ts +605 -279
- package/src/providers/provider-metadata.ts +12 -1
- package/src/providers/provider-model-definition.ts +373 -373
- package/src/providers/quota/quota-contracts.ts +3 -7
- package/src/providers/search/search-provider.ts +2 -6
- package/src/security/admission/contracts.ts +10 -0
- package/src/security/admission/in-memory-store.ts +89 -14
- package/src/security/admission/lua.ts +191 -182
- package/src/security/admission/redis-store.ts +30 -0
- package/src/security/admission/service.ts +18 -0
- package/src/security/api-key-auth-cache.ts +96 -0
- package/src/security/api-key-auth.ts +11 -100
- package/src/security/client-router-fingerprint.ts +3 -7
- package/src/security/ip-boundary.ts +7 -3
- package/src/shared/coerce.ts +67 -0
- package/src/shared/hash.ts +6 -0
- package/src/shared/time.ts +7 -0
- package/src/transport/dispatch/attempt-finalize.ts +26 -138
- package/src/transport/dispatch/attempt-loop.ts +103 -81
- package/src/transport/dispatch/capture-policy.ts +78 -0
- package/src/transport/dispatch/proxy-request.ts +4 -2
- package/src/transport/dispatch/streaming/encoder.ts +111 -5
- package/src/transport/dispatch/streaming/stream.ts +19 -2
- package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
- package/src/transport/failure-policy.ts +27 -0
- package/src/transport/guards/abuse-guard.ts +2 -2
- package/src/transport/guards/auth-guard.ts +17 -0
- package/src/transport/guards/body-policy.ts +3 -2
- package/src/transport/guards/concurrency-guard.ts +89 -13
- package/src/transport/guards/early-rejection-capture.ts +43 -3
- package/src/transport/guards/error-lifecycle.ts +10 -2
- package/src/transport/guards/guard-errors.ts +23 -0
- package/src/transport/guards/headroom-guard.ts +41 -7
- package/src/transport/guards/pipeline.ts +24 -0
- package/src/transport/guards/request-context.ts +2 -1
- package/src/transport/routing/admission.ts +51 -9
- package/src/transport/routing/bounded-lru.ts +49 -0
- package/src/transport/routing/candidate-index.ts +88 -0
- package/src/transport/routing/reservations.ts +121 -7
- package/src/transport/routing/round-robin.ts +10 -22
- package/src/transport/routing/route-catalog.ts +26 -13
- package/src/transport/routing/route-model.ts +22 -2
- package/src/transport/routing/router.ts +53 -31
- package/src/transport/surface/chat/encode.ts +2 -3
- package/src/transport/surface/chat/parse.ts +2 -1
- package/src/transport/surface/dialects.ts +11 -3
- package/src/transport/surface/messages/encode.ts +2 -1
- package/src/transport/surface/messages/parse.ts +3 -1
- package/src/transport/surface/messages/stream.ts +2 -6
- package/src/workers/maintenance-lease.ts +200 -0
- package/src/workers/scheduler.ts +101 -1
- package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
- package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
- package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
- package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
- package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
- package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
- package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
- package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
- package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
- package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
- package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
- package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
- package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
- package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
- package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
- package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
- package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
- package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
- package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
- package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
- package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
- package/src/observability/event-compaction.ts +0 -317
- package/src/observability/event-day-index.ts +0 -439
package/scripts/cli/instance.ts
CHANGED
|
@@ -1,224 +1,234 @@
|
|
|
1
|
-
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
|
|
4
|
-
const RUNTIME_STATE_FILE = "runtime.json";
|
|
5
|
-
|
|
6
|
-
interface RuntimeState {
|
|
7
|
-
readonly drainToken: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function statePath(): string | null {
|
|
11
|
-
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
12
|
-
return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function readState(): RuntimeState | null {
|
|
16
|
-
const path = statePath();
|
|
17
|
-
if (path === null || !existsSync(path)) return null;
|
|
18
|
-
try {
|
|
19
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
-
if (parsed !== null && typeof parsed === "object" && "drainToken" in parsed &&
|
|
21
|
-
typeof parsed.drainToken === "string" && parsed.drainToken.length > 0) {
|
|
22
|
-
return { drainToken: parsed.drainToken };
|
|
23
|
-
}
|
|
24
|
-
} catch {
|
|
25
|
-
// Corrupt state reads as no token and is replaced on next start.
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Returns the install's persistent drain token, creating it on first use. */
|
|
31
|
-
export function ensureDrainToken(): string {
|
|
32
|
-
const existing = readState();
|
|
33
|
-
if (existing !== null) return existing.drainToken;
|
|
34
|
-
const token = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "");
|
|
35
|
-
const path = statePath();
|
|
36
|
-
if (path !== null) {
|
|
37
|
-
try {
|
|
38
|
-
writeFileSync(path, JSON.stringify({ drainToken: token }), { encoding: "utf8", mode: 0o600 });
|
|
39
|
-
} catch {
|
|
40
|
-
// A read-only config directory only loses stop-from-another-process.
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return token;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function readPersistedDrainToken(): string | null {
|
|
47
|
-
return readState()?.drainToken ?? null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* A marker that one gateway owns this install's port.
|
|
52
|
-
*
|
|
53
|
-
* The port probe alone cannot tell a healthy gateway from a stranger's process:
|
|
54
|
-
* something answering on 12800 is not necessarily ours, so a start that found
|
|
55
|
-
* the port busy either failed outright or — worse — would have to guess before
|
|
56
|
-
* stopping anything. The lock names the PID and the token it was written with,
|
|
57
|
-
* which is what makes "is this our gateway?" answerable without guessing.
|
|
58
|
-
*
|
|
59
|
-
* It is advisory, not an OS lock: a stale file from a crashed process is the
|
|
60
|
-
* normal case this exists to recover from, so presence alone never blocks a
|
|
61
|
-
* start. Liveness is decided by probing the port, and identity by the token.
|
|
62
|
-
*/
|
|
63
|
-
export interface InstanceLock {
|
|
64
|
-
readonly pid: number;
|
|
65
|
-
readonly port: number;
|
|
66
|
-
/** Fingerprint of the drain token, never the token itself. */
|
|
67
|
-
readonly tokenFingerprint: string;
|
|
68
|
-
readonly startedAt: string;
|
|
69
|
-
readonly executable: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const LOCK_FILE = "instance.json";
|
|
73
|
-
|
|
74
|
-
function lockPath(): string | null {
|
|
75
|
-
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
76
|
-
return configDir ? join(configDir, LOCK_FILE) : null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** A short, non-reversible fingerprint so the token itself is never written. */
|
|
80
|
-
export function fingerprint(token: string): string {
|
|
81
|
-
let hash = 0;
|
|
82
|
-
for (let i = 0; i < token.length; i += 1) {
|
|
83
|
-
hash = (hash * 31 + token.charCodeAt(i)) | 0;
|
|
84
|
-
}
|
|
85
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function writeInstanceLock(port: number, pid: number, token: string, executable = ""): void {
|
|
89
|
-
const path = lockPath();
|
|
90
|
-
if (path === null) return;
|
|
91
|
-
const lock: InstanceLock = {
|
|
92
|
-
pid,
|
|
93
|
-
port,
|
|
94
|
-
tokenFingerprint: fingerprint(token),
|
|
95
|
-
startedAt: new Date().toISOString(),
|
|
96
|
-
executable,
|
|
97
|
-
};
|
|
98
|
-
try {
|
|
99
|
-
writeFileSync(path, JSON.stringify(lock), { encoding: "utf8", mode: 0o600 });
|
|
100
|
-
} catch {
|
|
101
|
-
// A read-only config directory costs the marker, not the run.
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function readInstanceLock(): InstanceLock | null {
|
|
106
|
-
const path = lockPath();
|
|
107
|
-
if (path === null || !existsSync(path)) return null;
|
|
108
|
-
try {
|
|
109
|
-
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
110
|
-
if (
|
|
111
|
-
parsed !== null &&
|
|
112
|
-
typeof parsed === "object" &&
|
|
113
|
-
"pid" in parsed &&
|
|
114
|
-
typeof parsed.pid === "number" &&
|
|
115
|
-
"port" in parsed &&
|
|
116
|
-
typeof parsed.port === "number" &&
|
|
117
|
-
"tokenFingerprint" in parsed &&
|
|
118
|
-
typeof parsed.tokenFingerprint === "string"
|
|
119
|
-
) {
|
|
120
|
-
const startedAt = "startedAt" in parsed && typeof parsed.startedAt === "string" ? parsed.startedAt : "";
|
|
121
|
-
return {
|
|
122
|
-
pid: parsed.pid,
|
|
123
|
-
port: parsed.port,
|
|
124
|
-
tokenFingerprint: parsed.tokenFingerprint,
|
|
125
|
-
startedAt,
|
|
126
|
-
executable: "executable" in parsed && typeof parsed.executable === "string" ? parsed.executable : "",
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
} catch {
|
|
130
|
-
// A corrupt marker reads as none.
|
|
131
|
-
}
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function clearInstanceLock(): void {
|
|
136
|
-
const path = lockPath();
|
|
137
|
-
if (path === null) return;
|
|
138
|
-
try {
|
|
139
|
-
unlinkSync(path);
|
|
140
|
-
} catch {
|
|
141
|
-
// Already gone, or the directory is read-only.
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Whether a process id is alive. `0` is never a real pid. */
|
|
146
|
-
export function pidAlive(pid: number): boolean {
|
|
147
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
148
|
-
|
|
149
|
-
process.kill(pid, 0)
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
1
|
+
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
const RUNTIME_STATE_FILE = "runtime.json";
|
|
5
|
+
|
|
6
|
+
interface RuntimeState {
|
|
7
|
+
readonly drainToken: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function statePath(): string | null {
|
|
11
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
12
|
+
return configDir ? join(configDir, RUNTIME_STATE_FILE) : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readState(): RuntimeState | null {
|
|
16
|
+
const path = statePath();
|
|
17
|
+
if (path === null || !existsSync(path)) return null;
|
|
18
|
+
try {
|
|
19
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
20
|
+
if (parsed !== null && typeof parsed === "object" && "drainToken" in parsed &&
|
|
21
|
+
typeof parsed.drainToken === "string" && parsed.drainToken.length > 0) {
|
|
22
|
+
return { drainToken: parsed.drainToken };
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Corrupt state reads as no token and is replaced on next start.
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns the install's persistent drain token, creating it on first use. */
|
|
31
|
+
export function ensureDrainToken(): string {
|
|
32
|
+
const existing = readState();
|
|
33
|
+
if (existing !== null) return existing.drainToken;
|
|
34
|
+
const token = crypto.randomUUID().replace(/-/g, "") + crypto.randomUUID().replace(/-/g, "");
|
|
35
|
+
const path = statePath();
|
|
36
|
+
if (path !== null) {
|
|
37
|
+
try {
|
|
38
|
+
writeFileSync(path, JSON.stringify({ drainToken: token }), { encoding: "utf8", mode: 0o600 });
|
|
39
|
+
} catch {
|
|
40
|
+
// A read-only config directory only loses stop-from-another-process.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return token;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function readPersistedDrainToken(): string | null {
|
|
47
|
+
return readState()?.drainToken ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A marker that one gateway owns this install's port.
|
|
52
|
+
*
|
|
53
|
+
* The port probe alone cannot tell a healthy gateway from a stranger's process:
|
|
54
|
+
* something answering on 12800 is not necessarily ours, so a start that found
|
|
55
|
+
* the port busy either failed outright or — worse — would have to guess before
|
|
56
|
+
* stopping anything. The lock names the PID and the token it was written with,
|
|
57
|
+
* which is what makes "is this our gateway?" answerable without guessing.
|
|
58
|
+
*
|
|
59
|
+
* It is advisory, not an OS lock: a stale file from a crashed process is the
|
|
60
|
+
* normal case this exists to recover from, so presence alone never blocks a
|
|
61
|
+
* start. Liveness is decided by probing the port, and identity by the token.
|
|
62
|
+
*/
|
|
63
|
+
export interface InstanceLock {
|
|
64
|
+
readonly pid: number;
|
|
65
|
+
readonly port: number;
|
|
66
|
+
/** Fingerprint of the drain token, never the token itself. */
|
|
67
|
+
readonly tokenFingerprint: string;
|
|
68
|
+
readonly startedAt: string;
|
|
69
|
+
readonly executable: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const LOCK_FILE = "instance.json";
|
|
73
|
+
|
|
74
|
+
function lockPath(): string | null {
|
|
75
|
+
const configDir = process.env.CARTETHYIA_CONFIG_DIR;
|
|
76
|
+
return configDir ? join(configDir, LOCK_FILE) : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A short, non-reversible fingerprint so the token itself is never written. */
|
|
80
|
+
export function fingerprint(token: string): string {
|
|
81
|
+
let hash = 0;
|
|
82
|
+
for (let i = 0; i < token.length; i += 1) {
|
|
83
|
+
hash = (hash * 31 + token.charCodeAt(i)) | 0;
|
|
84
|
+
}
|
|
85
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function writeInstanceLock(port: number, pid: number, token: string, executable = ""): void {
|
|
89
|
+
const path = lockPath();
|
|
90
|
+
if (path === null) return;
|
|
91
|
+
const lock: InstanceLock = {
|
|
92
|
+
pid,
|
|
93
|
+
port,
|
|
94
|
+
tokenFingerprint: fingerprint(token),
|
|
95
|
+
startedAt: new Date().toISOString(),
|
|
96
|
+
executable,
|
|
97
|
+
};
|
|
98
|
+
try {
|
|
99
|
+
writeFileSync(path, JSON.stringify(lock), { encoding: "utf8", mode: 0o600 });
|
|
100
|
+
} catch {
|
|
101
|
+
// A read-only config directory costs the marker, not the run.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function readInstanceLock(): InstanceLock | null {
|
|
106
|
+
const path = lockPath();
|
|
107
|
+
if (path === null || !existsSync(path)) return null;
|
|
108
|
+
try {
|
|
109
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
110
|
+
if (
|
|
111
|
+
parsed !== null &&
|
|
112
|
+
typeof parsed === "object" &&
|
|
113
|
+
"pid" in parsed &&
|
|
114
|
+
typeof parsed.pid === "number" &&
|
|
115
|
+
"port" in parsed &&
|
|
116
|
+
typeof parsed.port === "number" &&
|
|
117
|
+
"tokenFingerprint" in parsed &&
|
|
118
|
+
typeof parsed.tokenFingerprint === "string"
|
|
119
|
+
) {
|
|
120
|
+
const startedAt = "startedAt" in parsed && typeof parsed.startedAt === "string" ? parsed.startedAt : "";
|
|
121
|
+
return {
|
|
122
|
+
pid: parsed.pid,
|
|
123
|
+
port: parsed.port,
|
|
124
|
+
tokenFingerprint: parsed.tokenFingerprint,
|
|
125
|
+
startedAt,
|
|
126
|
+
executable: "executable" in parsed && typeof parsed.executable === "string" ? parsed.executable : "",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
// A corrupt marker reads as none.
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function clearInstanceLock(): void {
|
|
136
|
+
const path = lockPath();
|
|
137
|
+
if (path === null) return;
|
|
138
|
+
try {
|
|
139
|
+
unlinkSync(path);
|
|
140
|
+
} catch {
|
|
141
|
+
// Already gone, or the directory is read-only.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Whether a process id is alive. `0` is never a real pid. */
|
|
146
|
+
export function pidAlive(pid: number): boolean {
|
|
147
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
148
|
+
if (process.platform === "win32") {
|
|
149
|
+
// Bun's process.kill(pid, 0) reports dead Windows PIDs as alive. That
|
|
150
|
+
// leaves controller.json pinning a closed carte forever. tasklist is the
|
|
151
|
+
// OS authority and needs no process handle or elevated privilege.
|
|
152
|
+
const result = Bun.spawnSync(["tasklist.exe", "/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], {
|
|
153
|
+
stdout: "pipe",
|
|
154
|
+
stderr: "ignore",
|
|
155
|
+
});
|
|
156
|
+
return result.exitCode === 0 && result.stdout.toString().startsWith('"');
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
process.kill(pid, 0);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface RunningGateway { pid: number; port: number; executable: string; }
|
|
167
|
+
|
|
168
|
+
interface InstanceDependencies {
|
|
169
|
+
alive: (pid: number) => boolean;
|
|
170
|
+
fetch: (input: string, init?: RequestInit) => Promise<Response>;
|
|
171
|
+
kill: (pid: number) => void;
|
|
172
|
+
sleep: (ms: number) => Promise<void>;
|
|
173
|
+
}
|
|
174
|
+
const instanceDependencies: InstanceDependencies = {
|
|
175
|
+
alive: pidAlive,
|
|
176
|
+
fetch: (input, init) => fetch(input, init),
|
|
177
|
+
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
178
|
+
sleep: Bun.sleep,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export async function findRunningGateway(port: number, token: string): Promise<RunningGateway | null> {
|
|
182
|
+
const lock = readInstanceLock();
|
|
183
|
+
if (lock === null || lock.port !== port || lock.tokenFingerprint !== fingerprint(token)) return null;
|
|
184
|
+
if (!instanceDependencies.alive(lock.pid)) {
|
|
185
|
+
clearInstanceLock();
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
const response = await instanceDependencies.fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1_000) });
|
|
190
|
+
if (!response.ok) return null;
|
|
191
|
+
return { pid: lock.pid, port, executable: lock.executable };
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export async function attachGateway(port: number, token: string): Promise<RunningGateway | null> {
|
|
198
|
+
return findRunningGateway(port, token);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function stopAttachedGateway(instance: RunningGateway, token: string): Promise<void> {
|
|
202
|
+
const lock = readInstanceLock();
|
|
203
|
+
if (lock === null || lock.pid !== instance.pid || lock.port !== instance.port || lock.tokenFingerprint !== fingerprint(token)) {
|
|
204
|
+
throw new Error("The gateway is not owned by this installation; refusing to stop it.");
|
|
205
|
+
}
|
|
206
|
+
if (!instanceDependencies.alive(instance.pid)) {
|
|
207
|
+
clearInstanceLock();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
let accepted = false;
|
|
211
|
+
try {
|
|
212
|
+
const response = await instanceDependencies.fetch(`http://127.0.0.1:${instance.port}/admin/drain`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: { "x-drain-token": token },
|
|
215
|
+
signal: AbortSignal.timeout(2_000),
|
|
216
|
+
});
|
|
217
|
+
accepted = response.status === 202;
|
|
218
|
+
if (!accepted) throw new Error(`Gateway drain was refused (HTTP ${response.status}); process left untouched.`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (error instanceof Error && error.message.includes("process left untouched")) throw error;
|
|
221
|
+
}
|
|
222
|
+
if (!accepted) instanceDependencies.kill(instance.pid);
|
|
223
|
+
const deadline = Date.now() + 30_000;
|
|
224
|
+
while (instanceDependencies.alive(instance.pid) && Date.now() < deadline) await instanceDependencies.sleep(200);
|
|
225
|
+
if (instanceDependencies.alive(instance.pid)) {
|
|
226
|
+
if (accepted) instanceDependencies.kill(instance.pid);
|
|
227
|
+
throw new Error("Gateway did not stop within the drain timeout");
|
|
228
|
+
}
|
|
229
|
+
clearInstanceLock();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export const instanceTestInternals = {
|
|
233
|
+
dependencies: instanceDependencies,
|
|
234
|
+
};
|