cartethyia 2.6.6 → 2.7.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.
Files changed (189) hide show
  1. package/.env.example +106 -113
  2. package/dist/dashboard/assets/{BannedPage-1v5rzKMl.js → BannedPage-P76QLGJA.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-chMg74ct.js → CliToolDetailPage-DdBggJS9.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-CKG-Vxrt.js → CliToolsPage-aN1iXhhP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-JYmVKzIx.js → CombosPage-DE-F1nxZ.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CPflWDry.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-B0EYUE47.js → CustomizationPage-3CUxMy2X.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-F1c4lkNG.js → HealthEventsModal-BkmJQKC0.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BYTpzcoU.js → LoginPage-B_0FUKWc.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-DKVslqG3.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
  22. package/dist/dashboard/assets/{ProviderDetailPage-CM4MNy45.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
  23. package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-Bsoqg2HG.js → ProvidersPage-CEplAR-i.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-8G3doV7j.js → ProxyPage-CAN7hy0f.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
  28. package/dist/dashboard/assets/{QuotaPage-CVIQq6JG.js → QuotaPage-BjldQwIU.js} +1 -1
  29. package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-BCx07-bj.js → SetupPage-_bCaTM0v.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-MhkB6msu.js → SortableList-fI7JkdIB.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CTqK6Nsj.js → StudioPage-CHVZqskZ.js} +1 -1
  37. package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-DiFBymHj.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-DFY_fZU4.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-BOJ1_xUB.css +2 -0
  43. package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
  44. package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-CwdnZARt.js} +1 -1
  45. package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
  46. package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-Dr-uf0Au.js} +1 -1
  47. package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
  48. package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BddmMdoS.js} +1 -1
  49. package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
  50. package/dist/dashboard/assets/{card-BOPBuBzD.js → card-CK0PXWlX.js} +1 -1
  51. package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
  52. package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-CcB6mGh2.js} +1 -1
  53. package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
  54. package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DGb2P9Yx.js} +1 -1
  55. package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
  56. package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-LkfuL50u.js} +1 -1
  57. package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
  58. package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-ZbEZ_84c.js} +1 -1
  59. package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
  60. package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-BJfsVMvA.js} +1 -1
  61. package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-CBHzQ6Qh.js → entry-CWBY5A0f.js} +3 -3
  63. package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-CvC1-Zqx.js} +1 -1
  65. package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
  66. package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-BxpznFLt.js} +1 -1
  67. package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
  68. package/dist/dashboard/assets/{index-Ck4uwuYb.js → index-CDzP3ovJ.js} +2 -2
  69. package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
  70. package/dist/dashboard/assets/{inline-DToMICwl.js → inline-Ae13MQ5w.js} +1 -1
  71. package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
  72. package/dist/dashboard/assets/{input-WlyHWeG2.js → input-D23fo7cp.js} +1 -1
  73. package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
  74. package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CSsxjj5P.js} +1 -1
  75. package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
  76. package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-dmbwZMDY.js} +1 -1
  77. package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
  78. package/dist/dashboard/assets/{live-ta6GMb-8.js → live-BG1oUsZZ.js} +1 -1
  79. package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
  80. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
  81. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  82. package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-DSF-e4ho.js} +1 -1
  83. package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-Bm_aECY7.js} +1 -1
  85. package/dist/dashboard/assets/{numbers-Dulqgys5.js.gz → numbers-Bm_aECY7.js.gz} +0 -0
  86. package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CF56nYIV.js} +1 -1
  87. package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
  88. package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-CeA71n7G.js} +1 -1
  89. package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
  90. package/dist/dashboard/assets/{select-BrrUDUPc.js → select-KIT0rC6G.js} +1 -1
  91. package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
  92. package/dist/dashboard/assets/settings-B_2gkePf.js +1 -0
  93. package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
  94. package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-D7WOSzcX.js} +1 -1
  95. package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
  96. package/dist/dashboard/assets/{state-DhZBfWgx.js → state-BBxtKZOQ.js} +1 -1
  97. package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
  98. package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-DNS_D5Ea.js} +1 -1
  99. package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-BgoxdoMk.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-BgoxdoMk.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -51
  105. package/package.json +96 -96
  106. package/src/boot/app.ts +714 -722
  107. package/src/boot/dependencies.ts +729 -750
  108. package/src/config/resources.ts +70 -137
  109. package/src/config/security.ts +37 -73
  110. package/src/config/spec.ts +358 -378
  111. package/src/console/console-router.ts +225 -233
  112. package/src/console/domain-registration.ts +381 -391
  113. package/src/console/remote-support/contracts.ts +3 -1
  114. package/src/console/remote-support/service.ts +115 -6
  115. package/src/main.ts +167 -169
  116. package/src/observability/metrics.ts +467 -457
  117. package/src/observability/runtime-metrics.ts +269 -254
  118. package/src/protocol/gateway-error.ts +232 -233
  119. package/src/providers/integrations/commandcode/commandcode.ts +134 -104
  120. package/src/providers/integrations/commandcode/discovery.ts +6 -5
  121. package/src/providers/integrations/qoder/qoder.ts +597 -23
  122. package/src/providers/operations/account-health-service.ts +52 -3
  123. package/src/security/abuse.ts +17 -0
  124. package/src/transport/guards/auth-guard.ts +109 -127
  125. package/src/transport/guards/pipeline.ts +220 -226
  126. package/src/transport/guards/request-context.ts +238 -318
  127. package/src/transport/request/inflight.ts +21 -2
  128. package/src/workers/scheduler.ts +73 -6
  129. package/dist/dashboard/assets/BannedPage-1v5rzKMl.js.gz +0 -0
  130. package/dist/dashboard/assets/CliToolDetailPage-chMg74ct.js.gz +0 -0
  131. package/dist/dashboard/assets/CliToolsPage-CKG-Vxrt.js.gz +0 -0
  132. package/dist/dashboard/assets/CombosPage-JYmVKzIx.js.gz +0 -0
  133. package/dist/dashboard/assets/ConsoleLogPage-CPflWDry.js.gz +0 -0
  134. package/dist/dashboard/assets/CustomizationPage-B0EYUE47.js.gz +0 -0
  135. package/dist/dashboard/assets/HealthEventsModal-F1c4lkNG.js.gz +0 -0
  136. package/dist/dashboard/assets/LoginPage-BYTpzcoU.js.gz +0 -0
  137. package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
  138. package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js +0 -1
  139. package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js.gz +0 -0
  140. package/dist/dashboard/assets/ProviderDetailPage-CM4MNy45.js.gz +0 -0
  141. package/dist/dashboard/assets/ProvidersPage-Bsoqg2HG.js.gz +0 -0
  142. package/dist/dashboard/assets/ProxyPage-8G3doV7j.js.gz +0 -0
  143. package/dist/dashboard/assets/QuotaPage-CVIQq6JG.js.gz +0 -0
  144. package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js +0 -1
  145. package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js.gz +0 -0
  146. package/dist/dashboard/assets/SetupPage-BCx07-bj.js.gz +0 -0
  147. package/dist/dashboard/assets/SortableList-MhkB6msu.js.gz +0 -0
  148. package/dist/dashboard/assets/StudioPage-CTqK6Nsj.js.gz +0 -0
  149. package/dist/dashboard/assets/UsagePage-Do65OWbr.js +0 -56
  150. package/dist/dashboard/assets/UsagePage-Do65OWbr.js.gz +0 -0
  151. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  152. package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
  153. package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
  154. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  155. package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
  156. package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
  157. package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
  158. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  159. package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
  160. package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
  161. package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
  162. package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
  163. package/dist/dashboard/assets/entry-CBHzQ6Qh.js.gz +0 -0
  164. package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
  165. package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
  166. package/dist/dashboard/assets/index-Ck4uwuYb.js.gz +0 -0
  167. package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
  168. package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
  169. package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
  170. package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
  171. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  172. package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
  173. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  174. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  175. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  176. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  177. package/dist/dashboard/assets/select-BrrUDUPc.js.gz +0 -0
  178. package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
  179. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  180. package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
  181. package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
  182. package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
  183. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  184. package/src/console/admin/model-abuse/contracts.ts +0 -38
  185. package/src/console/admin/model-abuse/routes.ts +0 -97
  186. package/src/security/model-abuse.ts +0 -704
  187. package/src/transport/guards/concurrency-guard.ts +0 -125
  188. /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
  189. /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
@@ -1,226 +1,220 @@
1
- import { Elysia } from "elysia";
2
- import { createIngressPolicyMiddleware } from "./body-policy";
3
- import {
4
- createCanonicalRequestMiddleware,
5
- createClientIdentityMiddleware,
6
- createProxyRoutePreparationMiddleware,
7
- createRequestContextMiddleware,
8
- } from "./request-context";
9
- import type { CanonicalAdapter, ShutdownDrainSource } from "../contracts";
10
- import { createApiKeyAuthenticationMiddleware } from "./auth-guard";
11
- import { createDependencyReadinessMiddleware } from "./readiness-guard";
12
- import { createIpAbuseProtectionMiddleware } from "./abuse-guard";
13
- import { createProcessConcurrencyMiddleware } from "./concurrency-guard";
14
- import { createHeadroomGuard } from "./headroom-guard";
15
- import {
16
- createErrorNormalizationMiddleware,
17
- registerRequestCleanup,
18
- registerTelemetryLifecycle,
19
- type AfterResponseApp,
20
- } from "./error-lifecycle";
21
- import type { ReadinessCheckResult } from "../../persistence/readiness";
22
- import type { IpAbuseProtectionService } from "../../security/abuse";
23
- import type { ProxyRequestPreparer } from "../request/preparer";
24
- import { ProxyRequestStateStore } from "../request/state";
25
- import type { SurfaceAdapterRegistry } from "../surface/adapters";
26
- import type { TrustedProxyBoundary } from "../../config/network";
27
- import type { CartethyiaDatabase } from "../../persistence/db-handle";
28
- import type { RedisClient } from "../../persistence/redis";
29
- import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
30
- import type { ModelStrikeService } from "../../security/model-abuse";
31
- import type { RuntimeMemorySnapshot } from "../../observability/runtime-metrics";
32
- import {
33
- TelemetryPayloadCapture,
34
- payloadCaptureKeepsBodies,
35
- payloadRetentionMs,
36
- resolvePayloadCapture,
37
- } from "../../observability/payload-capture";
38
-
39
- /** Shared state/context carried by every ordered transport stage. */
40
- export interface TransportPipelineContext {
41
- readonly db: CartethyiaDatabase;
42
- readonly stateStore: ProxyRequestStateStore;
43
- readonly surfaceRegistry: SurfaceAdapterRegistry;
44
- readonly adapters: ReadonlyMap<string, CanonicalAdapter>;
45
- readonly preparer: ProxyRequestPreparer;
46
- readonly readiness: () => Promise<ReadinessCheckResult>;
47
- readonly trustedProxyBoundary: TrustedProxyBoundary;
48
- readonly resolvePeerAddress?: (request: Request) => string | null;
49
- readonly ipAbuseProtection?: IpAbuseProtectionService;
50
- readonly maxBodyBytes?: number;
51
- readonly requestDeadlineMs?: number;
52
- readonly verifiedHttps?: boolean;
53
- /** Graduated strikes for repeated invalid-model requests. */
54
- readonly modelStrikes?: ModelStrikeService;
55
- readonly shutdownCoordinator?: ShutdownDrainSource;
56
- readonly telemetry?: TelemetryBatchBuffer;
57
- /**
58
- * Shared client for the headroom guard's shed marker. Optional: the shed
59
- * decision is local, and Redis is only written for fleet visibility.
60
- */
61
- readonly redis?: RedisClient | undefined;
62
- /**
63
- * The sampled memory state the headroom guard sheds against.
64
- *
65
- * Supplied by the runtime sampler. When absent the guard cannot compare RSS
66
- * against a limit and does not shed — so this must be wired in production,
67
- * and the sampler must be initialized at boot even before its first tick.
68
- */
69
- readonly memorySnapshot?: () => RuntimeMemorySnapshot | undefined;
70
- /**
71
- * Whether invoke is switched off for a tenant. Optional: without it the
72
- * gateway never refuses on the kill switch, which is the pre-change
73
- * behaviour rather than a failure.
74
- */
75
- readonly isInvokeDisabled?: (tenantId: string) => Promise<boolean | undefined>;
76
- }
77
-
78
- /**
79
- * The single ingress owner. `mountRoot` applies the process-wide request
80
- * context and error-normalization lifecycle; `createGateway` composes the
81
- * ordered fail-closed `/v1/*` stage chain plus the telemetry/cleanup
82
- * lifecycle around caller-registered gateway routes. Composition code
83
- * (`app.ts`) mounts — it never assembles middleware policy itself.
84
- */
85
- export interface TransportPipeline {
86
- /** Applies root-scoped request context and error normalization. */
87
- mountRoot(app: Elysia<any, any, any, any, any, any, any, any>): void;
88
- /** Builds the complete `/v1/*` gateway plugin around registered routes. */
89
- createGateway(registerRoutes: (routes: Elysia<any, any, any, any, any, any, any, any>) => void): Elysia;
90
- }
91
-
92
- /**
93
- * Builds the one ordered middleware pipeline. Request context and error
94
- * normalization are mounted at the root; the remaining stages are mounted
95
- * before every `/v1/*` route, preserving fail-closed ordering.
96
- */
97
- export function createTransportPipeline(context: TransportPipelineContext): TransportPipeline {
98
- const stages: Elysia[] = [
99
- createProcessConcurrencyMiddleware(),
100
- createHeadroomGuard({
101
- store: context.stateStore,
102
- ...(context.memorySnapshot === undefined ? {} : { memorySnapshot: context.memorySnapshot }),
103
- ...(context.redis === undefined ? {} : { redis: context.redis }),
104
- }),
105
- createDependencyReadinessMiddleware({
106
- readiness: context.readiness,
107
- ...(context.shutdownCoordinator ? { shutdownCoordinator: context.shutdownCoordinator } : {}),
108
- }),
109
- createIngressPolicyMiddleware({
110
- stateStore: context.stateStore,
111
- ...(context.maxBodyBytes === undefined ? {} : { maxBodyBytes: context.maxBodyBytes }),
112
- }),
113
- createClientIdentityMiddleware({
114
- stateStore: context.stateStore,
115
- trustedProxyBoundary: context.trustedProxyBoundary,
116
- ...(context.resolvePeerAddress ? { resolvePeerAddress: context.resolvePeerAddress } : {}),
117
- }),
118
- createApiKeyAuthenticationMiddleware({
119
- db: context.db,
120
- stateStore: context.stateStore,
121
- ...(context.modelStrikes ? { modelStrikes: context.modelStrikes } : {}),
122
- ...(context.isInvokeDisabled
123
- ? { isInvokeDisabled: context.isInvokeDisabled }
124
- : {}),
125
- }),
126
- createCanonicalRequestMiddleware({
127
- stateStore: context.stateStore,
128
- surfaceRegistry: context.surfaceRegistry,
129
- adapters: context.adapters,
130
- keepsIngressBody: (tenantId) => payloadCaptureKeepsBodies(context.db, tenantId),
131
- }),
132
- createProxyRoutePreparationMiddleware({
133
- stateStore: context.stateStore,
134
- preparer: context.preparer,
135
- ...(context.modelStrikes ? { modelStrikes: context.modelStrikes } : {}),
136
- }),
137
- ];
138
- const mountRoot = (app: Elysia<any, any, any, any, any, any, any, any>): void => {
139
- app.use(
140
- createRequestContextMiddleware({
141
- stateStore: context.stateStore,
142
- ...(context.requestDeadlineMs === undefined
143
- ? {}
144
- : { requestDeadlineMs: context.requestDeadlineMs }),
145
- }),
146
- );
147
- // Root-mounted, not a gateway stage: its `request` hook must run for every
148
- // `/v1/*` request, including paths that match no route, or a caller can
149
- // hammer the gateway without ever being counted. See the middleware's own
150
- // doc for the measured bypass this closes.
151
- if (context.ipAbuseProtection) {
152
- app.use(
153
- createIpAbuseProtectionMiddleware({
154
- stateStore: context.stateStore,
155
- ipAbuseProtection: context.ipAbuseProtection,
156
- trustedProxyBoundary: context.trustedProxyBoundary,
157
- ...(context.resolvePeerAddress ? { resolvePeerAddress: context.resolvePeerAddress } : {}),
158
- }),
159
- );
160
- }
161
- app.use(
162
- createErrorNormalizationMiddleware({
163
- stateStore: context.stateStore,
164
- hsts: context.verifiedHttps === true,
165
- }),
166
- );
167
- // Finalize + cleanup live at the ROOT, not on the gateway plugin.
168
- //
169
- // A plugin-scoped `afterResponse` only fires for a request that matched a
170
- // registered route, so an unregistered `/v1/*` path — the cheapest thing a
171
- // scanner can send — was admitted by the root `request` hook (which does
172
- // run for every inbound request, that is why the in-flight count and the
173
- // IP-abuse counter are there) but never cleaned up. Measured: one
174
- // unmatched `/v1/*` request left `proxy_in_flight` permanently higher and
175
- // `activeCount()` permanently one larger, so the gauge only ever climbed
176
- // for the life of the process. The root hook runs for the unmatched path
177
- // too, which is what makes the increment and the decrement symmetric.
178
- //
179
- // Ordering is safe because a root `afterResponse` still runs *after* the
180
- // matched handler returns (measured: request -> handler -> root.afterResponse
181
- // -> plugin.afterResponse), so a dispatch route is finalized exactly as
182
- // before. Registering it only here — never also on the gateway — is what
183
- // keeps a matched route from finalizing twice.
184
- // `mountRoot` receives a fully-widened `Elysia<any, ...>`, whose overloaded
185
- // `afterResponse` signature is not structurally assignable to the minimal
186
- // `AfterResponseApp` hook interface even though the runtime shape matches
187
- // (Elysia passes `{ request }`). The cast is confined to this one call.
188
- const rootApp = app as unknown as AfterResponseApp;
189
- if (context.telemetry) {
190
- registerTelemetryLifecycle(rootApp, {
191
- stateStore: context.stateStore,
192
- telemetryBuffer: context.telemetry,
193
- // One capture for the process, shared with every request state so an
194
- // early rejection can record its payload from the finalize hook. It
195
- // resolves the tenant's retention itself because an early rejection has
196
- // no preference memo on its state — nothing resolved one before the
197
- // request was refused.
198
- payloadCapture: new TelemetryPayloadCapture(context.db, (tenantId) =>
199
- resolvePayloadCapture(context.db, tenantId).then(
200
- (resolved) => (resolved.ok ? resolved.retentionMs : payloadRetentionMs()),
201
- ),
202
- ),
203
- // The early-rejection capture resolves the tenant's capture mode
204
- // through the same preference path dispatched requests use, instead of
205
- // capturing bodies under the deployment default regardless of the
206
- // tenant's none/metadata choice.
207
- db: context.db,
208
- });
209
- } else {
210
- registerRequestCleanup(rootApp, { stateStore: context.stateStore });
211
- }
212
- };
213
- const createGateway = (
214
- registerRoutes: (routes: Elysia<any, any, any, any, any, any, any, any>) => void,
215
- ): Elysia<any, any, any, any, any, any, any, any> => {
216
- const gateway = new Elysia({ prefix: "/v1" });
217
- for (const plugin of stages) gateway.use(plugin);
218
- // No telemetry/cleanup registration here: the lifecycle is owned by
219
- // `mountRoot`. Registering a second copy on this plugin would finalize a
220
- // matched request twice, and relying on this copy alone would leak every
221
- // unmatched `/v1/*` request (see `mountRoot`).
222
- registerRoutes(gateway);
223
- return gateway;
224
- };
225
- return { mountRoot, createGateway };
226
- }
1
+ import { Elysia } from "elysia";
2
+ import { createIngressPolicyMiddleware } from "./body-policy";
3
+ import {
4
+ createCanonicalRequestMiddleware,
5
+ createClientIdentityMiddleware,
6
+ createProxyRoutePreparationMiddleware,
7
+ createRequestContextMiddleware,
8
+ } from "./request-context";
9
+ import type { CanonicalAdapter, ShutdownDrainSource } from "../contracts";
10
+ import { createApiKeyAuthenticationMiddleware } from "./auth-guard";
11
+ import { createDependencyReadinessMiddleware } from "./readiness-guard";
12
+ import { createIpAbuseProtectionMiddleware } from "./abuse-guard";
13
+ import { createHeadroomGuard } from "./headroom-guard";
14
+ import {
15
+ createErrorNormalizationMiddleware,
16
+ registerRequestCleanup,
17
+ registerTelemetryLifecycle,
18
+ type AfterResponseApp,
19
+ } from "./error-lifecycle";
20
+ import type { ReadinessCheckResult } from "../../persistence/readiness";
21
+ import type { IpAbuseProtectionService } from "../../security/abuse";
22
+ import type { ProxyRequestPreparer } from "../request/preparer";
23
+ import { ProxyRequestStateStore } from "../request/state";
24
+ import type { SurfaceAdapterRegistry } from "../surface/adapters";
25
+ import type { TrustedProxyBoundary } from "../../config/network";
26
+ import type { CartethyiaDatabase } from "../../persistence/db-handle";
27
+ import type { RedisClient } from "../../persistence/redis";
28
+ import type { TelemetryBatchBuffer } from "../../observability/telemetry-buffer";
29
+ import type { RuntimeMemorySnapshot } from "../../observability/runtime-metrics";
30
+ import {
31
+ TelemetryPayloadCapture,
32
+ payloadCaptureKeepsBodies,
33
+ payloadRetentionMs,
34
+ resolvePayloadCapture,
35
+ } from "../../observability/payload-capture";
36
+
37
+ /** Shared state/context carried by every ordered transport stage. */
38
+ export interface TransportPipelineContext {
39
+ readonly db: CartethyiaDatabase;
40
+ readonly stateStore: ProxyRequestStateStore;
41
+ readonly surfaceRegistry: SurfaceAdapterRegistry;
42
+ readonly adapters: ReadonlyMap<string, CanonicalAdapter>;
43
+ readonly preparer: ProxyRequestPreparer;
44
+ readonly readiness: () => Promise<ReadinessCheckResult>;
45
+ readonly trustedProxyBoundary: TrustedProxyBoundary;
46
+ readonly resolvePeerAddress?: (request: Request) => string | null;
47
+ readonly ipAbuseProtection?: IpAbuseProtectionService;
48
+ readonly maxBodyBytes?: number;
49
+ readonly requestDeadlineMs?: number;
50
+ readonly verifiedHttps?: boolean;
51
+ /** Graduated strikes for repeated invalid-model requests. */
52
+ readonly shutdownCoordinator?: ShutdownDrainSource;
53
+ readonly telemetry?: TelemetryBatchBuffer;
54
+ /**
55
+ * Shared client for the headroom guard's shed marker. Optional: the shed
56
+ * decision is local, and Redis is only written for fleet visibility.
57
+ */
58
+ readonly redis?: RedisClient | undefined;
59
+ /**
60
+ * The sampled memory state the headroom guard sheds against.
61
+ *
62
+ * Supplied by the runtime sampler. When absent the guard cannot compare RSS
63
+ * against a limit and does not shed — so this must be wired in production,
64
+ * and the sampler must be initialized at boot even before its first tick.
65
+ */
66
+ readonly memorySnapshot?: () => RuntimeMemorySnapshot | undefined;
67
+ /**
68
+ * Whether invoke is switched off for a tenant. Optional: without it the
69
+ * gateway never refuses on the kill switch, which is the pre-change
70
+ * behaviour rather than a failure.
71
+ */
72
+ readonly isInvokeDisabled?: (tenantId: string) => Promise<boolean | undefined>;
73
+ }
74
+
75
+ /**
76
+ * The single ingress owner. `mountRoot` applies the process-wide request
77
+ * context and error-normalization lifecycle; `createGateway` composes the
78
+ * ordered fail-closed `/v1/*` stage chain plus the telemetry/cleanup
79
+ * lifecycle around caller-registered gateway routes. Composition code
80
+ * (`app.ts`) mounts — it never assembles middleware policy itself.
81
+ */
82
+ export interface TransportPipeline {
83
+ /** Applies root-scoped request context and error normalization. */
84
+ mountRoot(app: Elysia<any, any, any, any, any, any, any, any>): void;
85
+ /** Builds the complete `/v1/*` gateway plugin around registered routes. */
86
+ createGateway(registerRoutes: (routes: Elysia<any, any, any, any, any, any, any, any>) => void): Elysia;
87
+ }
88
+
89
+ /**
90
+ * Builds the one ordered middleware pipeline. Request context and error
91
+ * normalization are mounted at the root; the remaining stages are mounted
92
+ * before every `/v1/*` route, preserving fail-closed ordering.
93
+ */
94
+ export function createTransportPipeline(context: TransportPipelineContext): TransportPipeline {
95
+ const stages: Elysia[] = [
96
+ createHeadroomGuard({
97
+ store: context.stateStore,
98
+ ...(context.memorySnapshot === undefined ? {} : { memorySnapshot: context.memorySnapshot }),
99
+ ...(context.redis === undefined ? {} : { redis: context.redis }),
100
+ }),
101
+ createDependencyReadinessMiddleware({
102
+ readiness: context.readiness,
103
+ ...(context.shutdownCoordinator ? { shutdownCoordinator: context.shutdownCoordinator } : {}),
104
+ }),
105
+ createIngressPolicyMiddleware({
106
+ stateStore: context.stateStore,
107
+ ...(context.maxBodyBytes === undefined ? {} : { maxBodyBytes: context.maxBodyBytes }),
108
+ }),
109
+ createClientIdentityMiddleware({
110
+ stateStore: context.stateStore,
111
+ trustedProxyBoundary: context.trustedProxyBoundary,
112
+ ...(context.resolvePeerAddress ? { resolvePeerAddress: context.resolvePeerAddress } : {}),
113
+ }),
114
+ createApiKeyAuthenticationMiddleware({
115
+ db: context.db,
116
+ stateStore: context.stateStore,
117
+ ...(context.isInvokeDisabled
118
+ ? { isInvokeDisabled: context.isInvokeDisabled }
119
+ : {}),
120
+ }),
121
+ createCanonicalRequestMiddleware({
122
+ stateStore: context.stateStore,
123
+ surfaceRegistry: context.surfaceRegistry,
124
+ adapters: context.adapters,
125
+ keepsIngressBody: (tenantId) => payloadCaptureKeepsBodies(context.db, tenantId),
126
+ }),
127
+ createProxyRoutePreparationMiddleware({
128
+ stateStore: context.stateStore,
129
+ preparer: context.preparer,
130
+ }),
131
+ ];
132
+ const mountRoot = (app: Elysia<any, any, any, any, any, any, any, any>): void => {
133
+ app.use(
134
+ createRequestContextMiddleware({
135
+ stateStore: context.stateStore,
136
+ ...(context.requestDeadlineMs === undefined
137
+ ? {}
138
+ : { requestDeadlineMs: context.requestDeadlineMs }),
139
+ }),
140
+ );
141
+ // Root-mounted, not a gateway stage: its `request` hook must run for every
142
+ // `/v1/*` request, including paths that match no route, or a caller can
143
+ // hammer the gateway without ever being counted. See the middleware's own
144
+ // doc for the measured bypass this closes.
145
+ if (context.ipAbuseProtection) {
146
+ app.use(
147
+ createIpAbuseProtectionMiddleware({
148
+ stateStore: context.stateStore,
149
+ ipAbuseProtection: context.ipAbuseProtection,
150
+ trustedProxyBoundary: context.trustedProxyBoundary,
151
+ ...(context.resolvePeerAddress ? { resolvePeerAddress: context.resolvePeerAddress } : {}),
152
+ }),
153
+ );
154
+ }
155
+ app.use(
156
+ createErrorNormalizationMiddleware({
157
+ stateStore: context.stateStore,
158
+ hsts: context.verifiedHttps === true,
159
+ }),
160
+ );
161
+ // Finalize + cleanup live at the ROOT, not on the gateway plugin.
162
+ //
163
+ // A plugin-scoped `afterResponse` only fires for a request that matched a
164
+ // registered route, so an unregistered `/v1/*` path — the cheapest thing a
165
+ // scanner can send — was admitted by the root `request` hook (which does
166
+ // run for every inbound request, that is why the in-flight count and the
167
+ // IP-abuse counter are there) but never cleaned up. Measured: one
168
+ // unmatched `/v1/*` request left `proxy_in_flight` permanently higher and
169
+ // `activeCount()` permanently one larger, so the gauge only ever climbed
170
+ // for the life of the process. The root hook runs for the unmatched path
171
+ // too, which is what makes the increment and the decrement symmetric.
172
+ //
173
+ // Ordering is safe because a root `afterResponse` still runs *after* the
174
+ // matched handler returns (measured: request -> handler -> root.afterResponse
175
+ // -> plugin.afterResponse), so a dispatch route is finalized exactly as
176
+ // before. Registering it only here — never also on the gateway — is what
177
+ // keeps a matched route from finalizing twice.
178
+ // `mountRoot` receives a fully-widened `Elysia<any, ...>`, whose overloaded
179
+ // `afterResponse` signature is not structurally assignable to the minimal
180
+ // `AfterResponseApp` hook interface even though the runtime shape matches
181
+ // (Elysia passes `{ request }`). The cast is confined to this one call.
182
+ const rootApp = app as unknown as AfterResponseApp;
183
+ if (context.telemetry) {
184
+ registerTelemetryLifecycle(rootApp, {
185
+ stateStore: context.stateStore,
186
+ telemetryBuffer: context.telemetry,
187
+ // One capture for the process, shared with every request state so an
188
+ // early rejection can record its payload from the finalize hook. It
189
+ // resolves the tenant's retention itself because an early rejection has
190
+ // no preference memo on its state — nothing resolved one before the
191
+ // request was refused.
192
+ payloadCapture: new TelemetryPayloadCapture(context.db, (tenantId) =>
193
+ resolvePayloadCapture(context.db, tenantId).then(
194
+ (resolved) => (resolved.ok ? resolved.retentionMs : payloadRetentionMs()),
195
+ ),
196
+ ),
197
+ // The early-rejection capture resolves the tenant's capture mode
198
+ // through the same preference path dispatched requests use, instead of
199
+ // capturing bodies under the deployment default regardless of the
200
+ // tenant's none/metadata choice.
201
+ db: context.db,
202
+ });
203
+ } else {
204
+ registerRequestCleanup(rootApp, { stateStore: context.stateStore });
205
+ }
206
+ };
207
+ const createGateway = (
208
+ registerRoutes: (routes: Elysia<any, any, any, any, any, any, any, any>) => void,
209
+ ): Elysia<any, any, any, any, any, any, any, any> => {
210
+ const gateway = new Elysia({ prefix: "/v1" });
211
+ for (const plugin of stages) gateway.use(plugin);
212
+ // No telemetry/cleanup registration here: the lifecycle is owned by
213
+ // `mountRoot`. Registering a second copy on this plugin would finalize a
214
+ // matched request twice, and relying on this copy alone would leak every
215
+ // unmatched `/v1/*` request (see `mountRoot`).
216
+ registerRoutes(gateway);
217
+ return gateway;
218
+ };
219
+ return { mountRoot, createGateway };
220
+ }