cartethyia 2.5.4 → 2.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -4
- package/README.md +2 -2
- package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
- package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
- package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
- package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
- package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
- package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
- package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
- package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
- package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
- package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
- package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
- package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
- package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
- package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
- package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
- package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
- package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
- package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
- package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
- package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
- package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
- package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
- package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
- package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
- package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
- package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
- package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
- package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
- package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
- package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
- package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
- package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
- package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
- package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
- package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
- package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
- package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
- package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
- package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
- package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
- package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
- package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
- package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
- package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
- package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
- package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
- package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
- package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
- package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
- package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
- package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
- package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
- package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
- package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
- package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
- package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
- package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
- package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
- package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
- package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
- package/dist/dashboard/assets/tokens-COzario9.js +1 -0
- package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
- package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
- package/dist/dashboard/assets/useMutation-Dem0lXQj.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 +54 -54
- package/migrations/README.md +52 -52
- package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
- package/package.json +96 -95
- package/scripts/cli/admin.ts +125 -0
- package/scripts/cli/{ui/background.ts → background.ts} +1 -1
- package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
- package/scripts/cli/doctor.ts +37 -0
- package/scripts/cli/{support/env.ts → env.ts} +99 -99
- package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
- package/scripts/cli/instance.ts +224 -0
- package/scripts/cli/main.ts +61 -18
- package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
- package/scripts/cli/port.ts +100 -0
- package/scripts/cli/{support/services.ts → services.ts} +88 -88
- package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
- package/scripts/cli/{command/start.ts → start.ts} +10 -5
- package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
- package/scripts/cli/tray/index.ts +62 -12
- package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
- package/scripts/cli/{command/update.ts → update.ts} +39 -39
- package/scripts/cli/version.ts +67 -0
- package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
- package/src/boot/app.ts +697 -693
- package/src/boot/dependencies.ts +647 -646
- package/src/boot/timeout.ts +2 -13
- package/src/config/cooldowns.ts +186 -138
- package/src/config/network.ts +8 -14
- package/src/config/providers.ts +7 -10
- package/src/config/runtime.ts +76 -76
- package/src/config/security.ts +15 -0
- package/src/config/spec.ts +20 -30
- package/src/config/telemetry.ts +0 -20
- package/src/config/timeouts.ts +0 -6
- package/src/console/admin/model-abuse/contracts.ts +28 -6
- package/src/console/admin/model-abuse/routes.ts +56 -3
- package/src/console/api-keys/contracts.ts +410 -414
- package/src/console/api-keys/routes.ts +881 -880
- package/src/console/auth/credentials.ts +3 -3
- package/src/console/auth/setup-service.ts +2 -2
- package/src/console/backup/export-endpoint.ts +6 -2
- package/src/console/backup/nine-router.ts +2 -5
- package/src/console/backup/service.ts +4 -0
- package/src/console/backup/store.ts +192 -29
- package/src/console/backup/validate.ts +3 -45
- package/src/console/cli-tools/fs-ops.ts +487 -487
- package/src/console/console-router-guards.ts +1 -1
- package/src/console/data-management/contracts.ts +13 -6
- package/src/console/data-management/operations.ts +23 -18
- package/src/console/data-management/routes.ts +0 -1
- package/src/console/domain-registration.ts +10 -5
- package/src/console/observability/live.ts +22 -0
- package/src/console/observability/store.ts +39 -16
- package/src/console/observability/usage-dimensions.ts +4 -4
- package/src/console/providers/catalog/catalog-projections.ts +6 -7
- package/src/console/providers/catalog/model-operations.ts +5 -2
- package/src/console/providers/catalog/provider-operations.ts +4 -4
- package/src/console/providers/catalog/public-model-store.ts +8 -11
- package/src/console/providers/catalog/store.ts +1553 -1553
- package/src/console/providers/oauth/routes.ts +8 -12
- package/src/console/quota/account-quota-tenant.ts +6 -6
- package/src/console/quota/quota-view.ts +1 -1
- package/src/console/remote-support/contracts.ts +61 -0
- package/src/console/remote-support/routes.ts +65 -0
- package/src/console/remote-support/service.ts +207 -0
- package/src/console/response-compression.ts +0 -3
- package/src/console/routing/pools/probe-result.ts +3 -2
- package/src/console/routing/pools/probe-verdict.ts +132 -132
- package/src/console/routing/pools/store.ts +8 -8
- package/src/console/settings/contracts.ts +58 -2
- package/src/console/settings/store.ts +30 -3
- package/src/console/shared/cursor.ts +8 -8
- package/src/console/shared/errors.ts +1 -3
- package/src/network/http2-bidi.ts +2 -4
- package/src/network/pool/agent.ts +478 -478
- package/src/network/pool/selector.ts +5 -4
- package/src/network/retry.ts +1 -2
- package/src/network/ssrf.ts +5 -4
- package/src/observability/event-compaction.ts +80 -78
- package/src/observability/event-day-index.ts +13 -15
- package/src/observability/event-log.ts +4 -4
- package/src/observability/logger.ts +6 -6
- package/src/observability/payload-capture.ts +151 -33
- package/src/observability/payload-store.ts +397 -440
- package/src/observability/redaction.ts +5 -6
- package/src/observability/request-notify.ts +43 -0
- package/src/observability/telemetry-status.ts +8 -9
- package/src/observability/token-speed.ts +5 -4
- package/src/persistence/db-mode.ts +7 -7
- package/src/persistence/readiness.ts +1 -1
- package/src/persistence/schema.ts +1406 -1402
- package/src/persistence/shared-cache.ts +1 -2
- package/src/persistence/stores/api-key-store.ts +18 -6
- package/src/persistence/stores/share-store.ts +4 -3
- package/src/persistence/stores/telemetry-store.ts +236 -237
- package/src/protocol/canonical-model.ts +11 -3
- package/src/protocol/codecs.ts +138 -4
- package/src/protocol/gateway-error.ts +6 -4
- package/src/protocol/request/chat.ts +21 -1
- package/src/protocol/request/codex.ts +10 -1
- package/src/protocol/request/messages.ts +4 -5
- package/src/protocol/request/responses.ts +9 -1
- package/src/protocol/response/chat.ts +34 -4
- package/src/protocol/response/codex.ts +7 -4
- package/src/protocol/response/dsml.ts +140 -28
- package/src/protocol/response/responses.ts +11 -5
- package/src/protocol/stream-error-frames.ts +4 -5
- package/src/providers/authentication/jwt-validator.ts +9 -10
- package/src/providers/authentication/oauth-refresh-service.ts +9 -11
- package/src/providers/configured-openai-providers.ts +134 -136
- package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
- package/src/providers/discovery/catalog/catalog-format.ts +3 -6
- package/src/providers/discovery/models-dev-catalog.ts +354 -354
- package/src/providers/discovery/probe-phases.ts +4 -4
- package/src/providers/discovery/probe-wire.ts +6 -13
- package/src/providers/discovery/probing-service.ts +1117 -1045
- package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
- package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
- package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
- package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
- package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
- package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
- package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
- package/src/providers/integrations/buddy/buddy-media.ts +2 -2
- package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
- package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
- package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
- package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
- package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
- package/src/providers/integrations/claude/claude.ts +608 -608
- package/src/providers/integrations/cline/cline-quota.ts +132 -132
- package/src/providers/integrations/cline/cline.ts +537 -539
- package/src/providers/integrations/codex/codex-oauth.ts +1 -1
- package/src/providers/integrations/commandcode/commandcode.ts +674 -675
- package/src/providers/integrations/commandcode/discovery.ts +117 -116
- package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
- package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
- package/src/providers/integrations/cursor/cursor.ts +11 -163
- package/src/providers/integrations/factory/factory-models.ts +3 -5
- package/src/providers/integrations/grok/grok-install-id.ts +2 -3
- package/src/providers/integrations/grok/grok.ts +6 -7
- package/src/providers/integrations/hermes.ts +14 -18
- package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
- package/src/providers/integrations/kiro/kiro.ts +1 -1
- package/src/providers/integrations/mistral.ts +6 -6
- package/src/providers/integrations/opencode/fingerprint.ts +3 -3
- package/src/providers/integrations/opencode/provider.ts +8 -8
- package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
- package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
- package/src/providers/integrations/qoder/qoder.ts +884 -865
- package/src/providers/integrations/shared/connect.ts +6 -6
- package/src/providers/integrations/trae/trae-catalog.ts +1 -2
- package/src/providers/integrations/trae/trae.ts +2 -2
- package/src/providers/integrations/zcode/oauth.ts +6 -1
- package/src/providers/operations/account-health-service.ts +1528 -1523
- package/src/providers/operations/account-reset-service.ts +1 -1
- package/src/providers/operations/client-versions.ts +6 -6
- package/src/providers/operations/custom-cli-headers.ts +3 -4
- package/src/providers/operations/provider-catalog-seeder.ts +95 -95
- package/src/providers/operations/provider-catalog-service.ts +3 -4
- package/src/providers/provider-metadata.ts +4 -8
- package/src/providers/provider-registry.ts +6 -7
- package/src/providers/provider-usage.ts +1 -1
- package/src/security/abuse.ts +15 -10
- package/src/security/access-control.ts +144 -146
- package/src/security/admission/contracts.ts +11 -0
- package/src/security/admission/in-memory-store.ts +4 -3
- package/src/security/admission/lease-sweep.ts +6 -6
- package/src/security/admission/reasons.ts +5 -1
- package/src/security/admission/redis-store.ts +211 -210
- package/src/security/admission/service.ts +4 -0
- package/src/security/api-key-auth.ts +504 -504
- package/src/security/ip-boundary.ts +155 -15
- package/src/security/model-abuse.ts +366 -94
- package/src/security/outbound-headers.ts +3 -4
- package/src/shared/sleep.ts +4 -0
- package/src/transport/dispatch/attempt-finalize.ts +47 -5
- package/src/transport/dispatch/attempt-loop.ts +531 -530
- package/src/transport/dispatch/stream-bridge.ts +1 -3
- package/src/transport/dispatch/streaming/finalize.ts +1 -1
- package/src/transport/dispatch/streaming/stream.ts +5 -6
- package/src/transport/failure-policy.ts +162 -8
- package/src/transport/guards/auth-guard.ts +7 -4
- package/src/transport/guards/concurrency-guard.ts +0 -5
- package/src/transport/guards/error-lifecycle.ts +13 -6
- package/src/transport/guards/pipeline.ts +15 -3
- package/src/transport/guards/request-context.ts +8 -2
- package/src/transport/request/inflight.ts +5 -6
- package/src/transport/request/preparer.ts +2 -2
- package/src/transport/request/state.ts +12 -8
- package/src/transport/routing/admission.ts +4 -0
- package/src/transport/routing/route-catalog.ts +8 -1
- package/src/transport/routing/route-model.ts +46 -14
- package/src/transport/routing/router.ts +21 -14
- package/src/transport/surface/completion.ts +2 -3
- package/src/transport/surface/dialects.ts +5 -6
- package/src/transport/surface/responses/encode.ts +38 -0
- package/src/transport/surface/responses/parse.ts +4 -0
- package/src/transport/translation/capabilities.ts +2 -3
- package/src/transport/translation/quirks.ts +3 -3
- package/src/workers/buddy-growth-tasks.ts +5 -10
- package/src/workers/daily-checkin.ts +17 -23
- package/src/workers/quota-refresh-sweep.ts +26 -26
- package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
- package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
- package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
- package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
- package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
- package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
- package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
- package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
- package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
- package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
- package/dist/dashboard/assets/app-COlK95oi.css +0 -2
- package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
- package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
- package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
- package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
- package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
- package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
- package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
- package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
- package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
- package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
- package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
- package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
- package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
- package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
- package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
- package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
- package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
- package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
- package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
- package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
- package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
- package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
- package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
- package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
- package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
- package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
- package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
- package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
- package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
- package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
- package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
- package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
- package/scripts/cli/command/admin.ts +0 -65
- package/scripts/cli/command/doctor.ts +0 -257
- package/scripts/cli/gateway/drain-token.ts +0 -78
- package/scripts/cli/gateway/instance-lock.ts +0 -105
- package/scripts/cli/gateway/stop.ts +0 -51
- package/scripts/cli/support/bun.ts +0 -27
- package/scripts/cli/support/version.ts +0 -37
- /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
- /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
- /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
import { PACKAGE_ROOT } from "
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Absolute path of the repository root.
|
|
8
|
-
*
|
|
9
|
-
* Operational scripts and the npm runtime share one package-root resolver so
|
|
10
|
-
* assets and repository config do not depend on the caller's working directory.
|
|
11
|
-
*/
|
|
12
|
-
export const PROJECT_ROOT = PACKAGE_ROOT;
|
|
13
|
-
|
|
14
|
-
/** Resolve the active `.env`; npm CLI sets a user-owned config directory. */
|
|
15
|
-
export function resolveEnvPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
16
|
-
const configDir = env.CARTETHYIA_CONFIG_DIR?.trim();
|
|
17
|
-
return configDir ? resolve(configDir, ".env") : resolve(PROJECT_ROOT, ".env");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Absolute path of the active `.env`. */
|
|
21
|
-
export const ENV_PATH = resolveEnvPath();
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Reads a dotenv-style file without overriding already-exported variables.
|
|
25
|
-
*/
|
|
26
|
-
export async function readEnvFile(path: string): Promise<Record<string, string>> {
|
|
27
|
-
if (!existsSync(path)) return {};
|
|
28
|
-
const content = await readFile(path, "utf8");
|
|
29
|
-
const values: Record<string, string> = {};
|
|
30
|
-
for (const line of content.split(/\r?\n/)) {
|
|
31
|
-
if (line.startsWith("#")) continue;
|
|
32
|
-
const separator = line.indexOf("=");
|
|
33
|
-
if (separator <= 0) continue;
|
|
34
|
-
const key = line.slice(0, separator).trim();
|
|
35
|
-
const value = line.slice(separator + 1).trim();
|
|
36
|
-
if (key) values[key] = value;
|
|
37
|
-
}
|
|
38
|
-
return values;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Checks if a string contains a configured placeholder value.
|
|
43
|
-
*/
|
|
44
|
-
export function hasPlaceholderSecrets(value: string): boolean {
|
|
45
|
-
const normalized = value.toLowerCase();
|
|
46
|
-
return (
|
|
47
|
-
normalized.includes("replace-with") ||
|
|
48
|
-
normalized.includes("replace_me") ||
|
|
49
|
-
normalized.includes("your_")
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generates a 256-bit hex cartethyia encryption key.
|
|
55
|
-
*
|
|
56
|
-
* Hex is preferred over base64 because it never carries padding or `+`/`/`
|
|
57
|
-
* characters that confuse some dotenv shells, and it round-trips through
|
|
58
|
-
* every adapter that accepts either encoding. The install/setup helpers call
|
|
59
|
-
* this when the key is absent or still a placeholder.
|
|
60
|
-
*/
|
|
61
|
-
export function generateCartethyiaEncryptionKey(): string {
|
|
62
|
-
const bytes = new Uint8Array(32);
|
|
63
|
-
crypto.getRandomValues(bytes);
|
|
64
|
-
let hex = "";
|
|
65
|
-
for (let i = 0; i < bytes.length; i += 1) hex += bytes[i]!.toString(16).padStart(2, "0");
|
|
66
|
-
return hex;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Returns the mandatory-only `.env` body derived from `.env.example`.
|
|
71
|
-
*
|
|
72
|
-
* Mandatory means the key is assigned without a leading `#`. Optional entries
|
|
73
|
-
* (leading `#`) carry built-in defaults and must not be copied into a fresh
|
|
74
|
-
* `.env` — they would override the default with a literal `example` value.
|
|
75
|
-
* The generated body therefore contains only mandatory rows, plus a single
|
|
76
|
-
* `CARTETHYIA_ENCRYPTION_KEY` replacement when the caller supplies one.
|
|
77
|
-
*/
|
|
78
|
-
export async function mandatoryEnvBody(
|
|
79
|
-
examplePath: string,
|
|
80
|
-
overrides: Readonly<Record<string, string>> = {},
|
|
81
|
-
): Promise<string> {
|
|
82
|
-
const raw = await readFile(examplePath, "utf8");
|
|
83
|
-
const lines: string[] = [];
|
|
84
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
85
|
-
const trimmed = line.trim();
|
|
86
|
-
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
87
|
-
const sep = line.indexOf("=");
|
|
88
|
-
if (sep <= 0) continue;
|
|
89
|
-
const key = line.slice(0, sep).trim();
|
|
90
|
-
if (!key) continue;
|
|
91
|
-
const value = (overrides as Record<string, string>)[key] ?? line.slice(sep + 1).trim();
|
|
92
|
-
lines.push(`${key}=${value}`);
|
|
93
|
-
}
|
|
94
|
-
// Preserve overrides even when the example row was commented out (legacy case).
|
|
95
|
-
for (const [key, value] of Object.entries(overrides)) {
|
|
96
|
-
if (!lines.some((l) => l.startsWith(`${key}=`))) lines.push(`${key}=${value}`);
|
|
97
|
-
}
|
|
98
|
-
return `${lines.join("\n")}\n`;
|
|
99
|
-
}
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { PACKAGE_ROOT } from "../../src/config/package-paths";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Absolute path of the repository root.
|
|
8
|
+
*
|
|
9
|
+
* Operational scripts and the npm runtime share one package-root resolver so
|
|
10
|
+
* assets and repository config do not depend on the caller's working directory.
|
|
11
|
+
*/
|
|
12
|
+
export const PROJECT_ROOT = PACKAGE_ROOT;
|
|
13
|
+
|
|
14
|
+
/** Resolve the active `.env`; npm CLI sets a user-owned config directory. */
|
|
15
|
+
export function resolveEnvPath(env: NodeJS.ProcessEnv = process.env): string {
|
|
16
|
+
const configDir = env.CARTETHYIA_CONFIG_DIR?.trim();
|
|
17
|
+
return configDir ? resolve(configDir, ".env") : resolve(PROJECT_ROOT, ".env");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Absolute path of the active `.env`. */
|
|
21
|
+
export const ENV_PATH = resolveEnvPath();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Reads a dotenv-style file without overriding already-exported variables.
|
|
25
|
+
*/
|
|
26
|
+
export async function readEnvFile(path: string): Promise<Record<string, string>> {
|
|
27
|
+
if (!existsSync(path)) return {};
|
|
28
|
+
const content = await readFile(path, "utf8");
|
|
29
|
+
const values: Record<string, string> = {};
|
|
30
|
+
for (const line of content.split(/\r?\n/)) {
|
|
31
|
+
if (line.startsWith("#")) continue;
|
|
32
|
+
const separator = line.indexOf("=");
|
|
33
|
+
if (separator <= 0) continue;
|
|
34
|
+
const key = line.slice(0, separator).trim();
|
|
35
|
+
const value = line.slice(separator + 1).trim();
|
|
36
|
+
if (key) values[key] = value;
|
|
37
|
+
}
|
|
38
|
+
return values;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if a string contains a configured placeholder value.
|
|
43
|
+
*/
|
|
44
|
+
export function hasPlaceholderSecrets(value: string): boolean {
|
|
45
|
+
const normalized = value.toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
normalized.includes("replace-with") ||
|
|
48
|
+
normalized.includes("replace_me") ||
|
|
49
|
+
normalized.includes("your_")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Generates a 256-bit hex cartethyia encryption key.
|
|
55
|
+
*
|
|
56
|
+
* Hex is preferred over base64 because it never carries padding or `+`/`/`
|
|
57
|
+
* characters that confuse some dotenv shells, and it round-trips through
|
|
58
|
+
* every adapter that accepts either encoding. The install/setup helpers call
|
|
59
|
+
* this when the key is absent or still a placeholder.
|
|
60
|
+
*/
|
|
61
|
+
export function generateCartethyiaEncryptionKey(): string {
|
|
62
|
+
const bytes = new Uint8Array(32);
|
|
63
|
+
crypto.getRandomValues(bytes);
|
|
64
|
+
let hex = "";
|
|
65
|
+
for (let i = 0; i < bytes.length; i += 1) hex += bytes[i]!.toString(16).padStart(2, "0");
|
|
66
|
+
return hex;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the mandatory-only `.env` body derived from `.env.example`.
|
|
71
|
+
*
|
|
72
|
+
* Mandatory means the key is assigned without a leading `#`. Optional entries
|
|
73
|
+
* (leading `#`) carry built-in defaults and must not be copied into a fresh
|
|
74
|
+
* `.env` — they would override the default with a literal `example` value.
|
|
75
|
+
* The generated body therefore contains only mandatory rows, plus a single
|
|
76
|
+
* `CARTETHYIA_ENCRYPTION_KEY` replacement when the caller supplies one.
|
|
77
|
+
*/
|
|
78
|
+
export async function mandatoryEnvBody(
|
|
79
|
+
examplePath: string,
|
|
80
|
+
overrides: Readonly<Record<string, string>> = {},
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
const raw = await readFile(examplePath, "utf8");
|
|
83
|
+
const lines: string[] = [];
|
|
84
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
87
|
+
const sep = line.indexOf("=");
|
|
88
|
+
if (sep <= 0) continue;
|
|
89
|
+
const key = line.slice(0, sep).trim();
|
|
90
|
+
if (!key) continue;
|
|
91
|
+
const value = (overrides as Record<string, string>)[key] ?? line.slice(sep + 1).trim();
|
|
92
|
+
lines.push(`${key}=${value}`);
|
|
93
|
+
}
|
|
94
|
+
// Preserve overrides even when the example row was commented out (legacy case).
|
|
95
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
96
|
+
if (!lines.some((l) => l.startsWith(`${key}=`))) lines.push(`${key}=${value}`);
|
|
97
|
+
}
|
|
98
|
+
return `${lines.join("\n")}\n`;
|
|
99
|
+
}
|
|
@@ -1,13 +1,44 @@
|
|
|
1
|
-
import { openSync } from "node:fs";
|
|
1
|
+
import { openSync, statSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
3
|
clearInstanceLock,
|
|
4
4
|
fingerprint,
|
|
5
5
|
pidAlive,
|
|
6
6
|
readInstanceLock,
|
|
7
7
|
writeInstanceLock,
|
|
8
|
-
} from "./instance
|
|
8
|
+
} from "./instance";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { PACKAGE_ROOT, resolvePackagePath } from "
|
|
10
|
+
import { PACKAGE_ROOT, resolvePackagePath } from "../../src/config/package-paths";
|
|
11
|
+
import { compiledBinaryPath } from "../build/compile";
|
|
12
|
+
import { spawn as spawnDetached, type ChildProcess } from "node:child_process";
|
|
13
|
+
import {
|
|
14
|
+
blockedPortMessage,
|
|
15
|
+
gatewayLogPath,
|
|
16
|
+
listenPortErrorMessage,
|
|
17
|
+
probeListenPort,
|
|
18
|
+
startupFailureMessage,
|
|
19
|
+
} from "./port";
|
|
20
|
+
|
|
21
|
+
function detachedSubprocess(executable: readonly string[], cwd: string, env: NodeJS.ProcessEnv, logFd: number | null): Bun.Subprocess {
|
|
22
|
+
const child: ChildProcess = spawnDetached(executable[0]!, [...executable.slice(1)], {
|
|
23
|
+
cwd,
|
|
24
|
+
env,
|
|
25
|
+
detached: true,
|
|
26
|
+
windowsHide: true,
|
|
27
|
+
stdio: ["ignore", logFd ?? "ignore", logFd ?? "ignore"],
|
|
28
|
+
});
|
|
29
|
+
child.unref();
|
|
30
|
+
const exited = new Promise<number>((resolve) => {
|
|
31
|
+
child.once("exit", (code) => resolve(code ?? 1));
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
pid: child.pid!,
|
|
35
|
+
exited,
|
|
36
|
+
exitCode: null,
|
|
37
|
+
signalCode: null,
|
|
38
|
+
kill: () => child.kill(),
|
|
39
|
+
unref: () => child.unref(),
|
|
40
|
+
} as unknown as Bun.Subprocess;
|
|
41
|
+
}
|
|
11
42
|
|
|
12
43
|
/**
|
|
13
44
|
* Frees the port when what holds it is this install's own gateway.
|
|
@@ -82,19 +113,25 @@ export async function startGateway(
|
|
|
82
113
|
drainToken: string,
|
|
83
114
|
options: { readonly detached?: boolean } = {},
|
|
84
115
|
): Promise<Bun.Subprocess> {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
// A dead marker from the previous process must not turn its now-reused port
|
|
117
|
+
// into an ownership claim. Only identity + drain authorization may stop one.
|
|
118
|
+
const lock = readInstanceLock();
|
|
119
|
+
if (lock && !pidAlive(lock.pid)) clearInstanceLock();
|
|
120
|
+
const probe = await probeListenPort(port);
|
|
121
|
+
if (probe.status === "blocked") {
|
|
122
|
+
if (probe.code === "EADDRINUSE") {
|
|
123
|
+
const claimed = await claimPort(port, drainToken);
|
|
124
|
+
if (claimed !== null) {
|
|
125
|
+
if (claimed.startsWith("Port") && claimed.includes("nothing there answers")) {
|
|
126
|
+
throw new Error(blockedPortMessage(port));
|
|
127
|
+
}
|
|
128
|
+
throw new Error(claimed);
|
|
129
|
+
}
|
|
130
|
+
const released = await probeListenPort(port);
|
|
131
|
+
if (released.status === "blocked") throw new Error(blockedPortMessage(port));
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(listenPortErrorMessage(port, probe));
|
|
134
|
+
}
|
|
98
135
|
}
|
|
99
136
|
// The gateway logs a startup phase line per stage. In menu mode those lines
|
|
100
137
|
// would scroll through the same terminal the menu is drawing on, so they go
|
|
@@ -108,9 +145,22 @@ export async function startGateway(
|
|
|
108
145
|
// A read-only config directory costs only the log, not startup.
|
|
109
146
|
}
|
|
110
147
|
}
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
const logPath = gatewayLogPath(configDir);
|
|
149
|
+
let logOffset = 0;
|
|
150
|
+
if (logPath) {
|
|
151
|
+
try { logOffset = statSync(logPath).size; } catch { logOffset = 0; }
|
|
152
|
+
}
|
|
153
|
+
const configuredBinary = process.env.CARTETHYIA_GATEWAY_BINARY?.trim();
|
|
154
|
+
const defaultBinary = resolvePackagePath(compiledBinaryPath());
|
|
155
|
+
const binary = configuredBinary || defaultBinary;
|
|
156
|
+
const executable = await Bun.file(binary).exists()
|
|
157
|
+
? [binary]
|
|
158
|
+
: configuredBinary
|
|
159
|
+
? (() => { throw new Error(`Configured gateway binary does not exist: ${configuredBinary}`); })()
|
|
160
|
+
: [process.execPath, "run", resolvePackagePath("src", "main.ts")];
|
|
161
|
+
const child = options.detached
|
|
162
|
+
? detachedSubprocess(executable, PACKAGE_ROOT, { ...process.env, CARTETHYIA_DRAIN_TOKEN: drainToken }, logFd)
|
|
163
|
+
: Bun.spawn(executable, {
|
|
114
164
|
cwd: PACKAGE_ROOT,
|
|
115
165
|
env: { ...process.env, CARTETHYIA_DRAIN_TOKEN: drainToken },
|
|
116
166
|
// "ignore", not "inherit": the gateway does not read stdin, and in menu
|
|
@@ -123,9 +173,8 @@ export async function startGateway(
|
|
|
123
173
|
// terminal, so closing that terminal took the gateway down with it and
|
|
124
174
|
// "Hide to tray" left nothing running.
|
|
125
175
|
stderr: logFd ?? "ignore",
|
|
126
|
-
detached:
|
|
127
|
-
}
|
|
128
|
-
);
|
|
176
|
+
detached: false,
|
|
177
|
+
});
|
|
129
178
|
const startupController = new AbortController();
|
|
130
179
|
const ready = (async (): Promise<void> => {
|
|
131
180
|
const deadline = Date.now() + 30_000;
|
|
@@ -157,13 +206,14 @@ export async function startGateway(
|
|
|
157
206
|
try {
|
|
158
207
|
const result = await Promise.race([readyResult, exitedEarly]);
|
|
159
208
|
if (result.status === "exit") {
|
|
160
|
-
throw new Error(
|
|
209
|
+
throw new Error(startupFailureMessage(port, result.code, logPath, logOffset));
|
|
161
210
|
}
|
|
162
211
|
if (result.status === "failure") throw result.error;
|
|
163
212
|
// Recorded only once the child is actually serving: a marker written before
|
|
164
213
|
// readiness would name a gateway that never came up, and the next start
|
|
165
214
|
// would treat a live port as ours when it is not.
|
|
166
|
-
writeInstanceLock(port, child.pid, drainToken);
|
|
215
|
+
writeInstanceLock(port, child.pid, drainToken, executable[0]!);
|
|
216
|
+
if (options.detached) child.unref();
|
|
167
217
|
return child;
|
|
168
218
|
} catch (error) {
|
|
169
219
|
child.kill();
|
|
@@ -171,3 +221,41 @@ export async function startGateway(
|
|
|
171
221
|
throw error;
|
|
172
222
|
}
|
|
173
223
|
}
|
|
224
|
+
|
|
225
|
+
/** Stops a child gateway, draining in-flight responses before termination. */
|
|
226
|
+
export async function stopGateway(
|
|
227
|
+
current: Bun.Subprocess,
|
|
228
|
+
port: number,
|
|
229
|
+
drainToken: string,
|
|
230
|
+
): Promise<void> {
|
|
231
|
+
let drainAccepted = false;
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(`http://127.0.0.1:${port}/admin/drain`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: { "x-drain-token": drainToken },
|
|
236
|
+
signal: AbortSignal.timeout(2_000),
|
|
237
|
+
});
|
|
238
|
+
drainAccepted = response.status === 202;
|
|
239
|
+
} catch {
|
|
240
|
+
// The process may already be exiting.
|
|
241
|
+
}
|
|
242
|
+
if (!drainAccepted) {
|
|
243
|
+
console.error("Could not request a graceful gateway drain; terminating the gateway process.");
|
|
244
|
+
current.kill();
|
|
245
|
+
await current.exited;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let timer: Timer | undefined;
|
|
249
|
+
const drained = await Promise.race([
|
|
250
|
+
current.exited,
|
|
251
|
+
new Promise<null>((resolveExit) => {
|
|
252
|
+
timer = setTimeout(() => resolveExit(null), 30_000);
|
|
253
|
+
}),
|
|
254
|
+
]);
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
if (drained === null) {
|
|
257
|
+
console.error("Graceful drain timed out; terminating the gateway process.");
|
|
258
|
+
current.kill();
|
|
259
|
+
await current.exited;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,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
|
+
try {
|
|
149
|
+
process.kill(pid, 0);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface RunningGateway { pid: number; port: number; executable: string; }
|
|
157
|
+
|
|
158
|
+
interface InstanceDependencies {
|
|
159
|
+
alive: (pid: number) => boolean;
|
|
160
|
+
fetch: (input: string, init?: RequestInit) => Promise<Response>;
|
|
161
|
+
kill: (pid: number) => void;
|
|
162
|
+
sleep: (ms: number) => Promise<void>;
|
|
163
|
+
}
|
|
164
|
+
const instanceDependencies: InstanceDependencies = {
|
|
165
|
+
alive: pidAlive,
|
|
166
|
+
fetch: (input, init) => fetch(input, init),
|
|
167
|
+
kill: (pid) => process.kill(pid, "SIGTERM"),
|
|
168
|
+
sleep: Bun.sleep,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export async function findRunningGateway(port: number, token: string): Promise<RunningGateway | null> {
|
|
172
|
+
const lock = readInstanceLock();
|
|
173
|
+
if (lock === null || lock.port !== port || lock.tokenFingerprint !== fingerprint(token)) return null;
|
|
174
|
+
if (!instanceDependencies.alive(lock.pid)) {
|
|
175
|
+
clearInstanceLock();
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const response = await instanceDependencies.fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1_000) });
|
|
180
|
+
if (!response.ok) return null;
|
|
181
|
+
return { pid: lock.pid, port, executable: lock.executable };
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function attachGateway(port: number, token: string): Promise<RunningGateway | null> {
|
|
188
|
+
return findRunningGateway(port, token);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function stopAttachedGateway(instance: RunningGateway, token: string): Promise<void> {
|
|
192
|
+
const lock = readInstanceLock();
|
|
193
|
+
if (lock === null || lock.pid !== instance.pid || lock.port !== instance.port || lock.tokenFingerprint !== fingerprint(token)) {
|
|
194
|
+
throw new Error("The gateway is not owned by this installation; refusing to stop it.");
|
|
195
|
+
}
|
|
196
|
+
if (!instanceDependencies.alive(instance.pid)) {
|
|
197
|
+
clearInstanceLock();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
let accepted = false;
|
|
201
|
+
try {
|
|
202
|
+
const response = await instanceDependencies.fetch(`http://127.0.0.1:${instance.port}/admin/drain`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: { "x-drain-token": token },
|
|
205
|
+
signal: AbortSignal.timeout(2_000),
|
|
206
|
+
});
|
|
207
|
+
accepted = response.status === 202;
|
|
208
|
+
if (!accepted) throw new Error(`Gateway drain was refused (HTTP ${response.status}); process left untouched.`);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (error instanceof Error && error.message.includes("process left untouched")) throw error;
|
|
211
|
+
}
|
|
212
|
+
if (!accepted) instanceDependencies.kill(instance.pid);
|
|
213
|
+
const deadline = Date.now() + 30_000;
|
|
214
|
+
while (instanceDependencies.alive(instance.pid) && Date.now() < deadline) await instanceDependencies.sleep(200);
|
|
215
|
+
if (instanceDependencies.alive(instance.pid)) {
|
|
216
|
+
if (accepted) instanceDependencies.kill(instance.pid);
|
|
217
|
+
throw new Error("Gateway did not stop within the drain timeout");
|
|
218
|
+
}
|
|
219
|
+
clearInstanceLock();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const instanceTestInternals = {
|
|
223
|
+
dependencies: instanceDependencies,
|
|
224
|
+
};
|