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.
- package/.env.example +106 -113
- package/dist/dashboard/assets/{BannedPage-1v5rzKMl.js → BannedPage-P76QLGJA.js} +1 -1
- package/dist/dashboard/assets/BannedPage-P76QLGJA.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolDetailPage-chMg74ct.js → CliToolDetailPage-DdBggJS9.js} +1 -1
- package/dist/dashboard/assets/CliToolDetailPage-DdBggJS9.js.gz +0 -0
- package/dist/dashboard/assets/{CliToolsPage-CKG-Vxrt.js → CliToolsPage-aN1iXhhP.js} +1 -1
- package/dist/dashboard/assets/CliToolsPage-aN1iXhhP.js.gz +0 -0
- package/dist/dashboard/assets/{CombosPage-JYmVKzIx.js → CombosPage-DE-F1nxZ.js} +1 -1
- package/dist/dashboard/assets/CombosPage-DE-F1nxZ.js.gz +0 -0
- package/dist/dashboard/assets/{ConsoleLogPage-CPflWDry.js → ConsoleLogPage-5SpmfLyw.js} +1 -1
- package/dist/dashboard/assets/ConsoleLogPage-5SpmfLyw.js.gz +0 -0
- package/dist/dashboard/assets/{CustomizationPage-B0EYUE47.js → CustomizationPage-3CUxMy2X.js} +1 -1
- package/dist/dashboard/assets/CustomizationPage-3CUxMy2X.js.gz +0 -0
- package/dist/dashboard/assets/{HealthEventsModal-F1c4lkNG.js → HealthEventsModal-BkmJQKC0.js} +1 -1
- package/dist/dashboard/assets/HealthEventsModal-BkmJQKC0.js.gz +0 -0
- package/dist/dashboard/assets/{LoginPage-BYTpzcoU.js → LoginPage-B_0FUKWc.js} +1 -1
- package/dist/dashboard/assets/LoginPage-B_0FUKWc.js.gz +0 -0
- package/dist/dashboard/assets/{ModelPicker-CA1kLs45.js → ModelPicker-DKVslqG3.js} +1 -1
- package/dist/dashboard/assets/ModelPicker-DKVslqG3.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js +1 -0
- package/dist/dashboard/assets/OverviewPage-D-H4MEiT.js.gz +0 -0
- package/dist/dashboard/assets/{ProviderDetailPage-CM4MNy45.js → ProviderDetailPage-CEJw6Guo.js} +3 -3
- package/dist/dashboard/assets/ProviderDetailPage-CEJw6Guo.js.gz +0 -0
- package/dist/dashboard/assets/{ProvidersPage-Bsoqg2HG.js → ProvidersPage-CEplAR-i.js} +1 -1
- package/dist/dashboard/assets/ProvidersPage-CEplAR-i.js.gz +0 -0
- package/dist/dashboard/assets/{ProxyPage-8G3doV7j.js → ProxyPage-CAN7hy0f.js} +1 -1
- package/dist/dashboard/assets/ProxyPage-CAN7hy0f.js.gz +0 -0
- package/dist/dashboard/assets/{QuotaPage-CVIQq6JG.js → QuotaPage-BjldQwIU.js} +1 -1
- package/dist/dashboard/assets/QuotaPage-BjldQwIU.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js +1 -0
- package/dist/dashboard/assets/SettingsPage-DUIoLRxk.js.gz +0 -0
- package/dist/dashboard/assets/{SetupPage-BCx07-bj.js → SetupPage-_bCaTM0v.js} +1 -1
- package/dist/dashboard/assets/SetupPage-_bCaTM0v.js.gz +0 -0
- package/dist/dashboard/assets/{SortableList-MhkB6msu.js → SortableList-fI7JkdIB.js} +1 -1
- package/dist/dashboard/assets/SortableList-fI7JkdIB.js.gz +0 -0
- package/dist/dashboard/assets/{StudioPage-CTqK6Nsj.js → StudioPage-CHVZqskZ.js} +1 -1
- package/dist/dashboard/assets/StudioPage-CHVZqskZ.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js +56 -0
- package/dist/dashboard/assets/UsagePage-DiFBymHj.js.gz +0 -0
- package/dist/dashboard/assets/{api-keys-C23S9sgp.js → api-keys-DFY_fZU4.js} +1 -1
- package/dist/dashboard/assets/api-keys-DFY_fZU4.js.gz +0 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css +2 -0
- package/dist/dashboard/assets/app-BOJ1_xUB.css.gz +0 -0
- package/dist/dashboard/assets/{assert-DU6qrMA_.js → assert-CwdnZARt.js} +1 -1
- package/dist/dashboard/assets/assert-CwdnZARt.js.gz +0 -0
- package/dist/dashboard/assets/{badge-Bkz_-RrM.js → badge-Dr-uf0Au.js} +1 -1
- package/dist/dashboard/assets/badge-Dr-uf0Au.js.gz +0 -0
- package/dist/dashboard/assets/{banner-C-7y3VKV.js → banner-BddmMdoS.js} +1 -1
- package/dist/dashboard/assets/banner-BddmMdoS.js.gz +0 -0
- package/dist/dashboard/assets/{card-BOPBuBzD.js → card-CK0PXWlX.js} +1 -1
- package/dist/dashboard/assets/card-CK0PXWlX.js.gz +0 -0
- package/dist/dashboard/assets/{cli-tools-BaFUuFFF.js → cli-tools-CcB6mGh2.js} +1 -1
- package/dist/dashboard/assets/cli-tools-CcB6mGh2.js.gz +0 -0
- package/dist/dashboard/assets/{clipboard-button-e-Mlk0FH.js → clipboard-button-DGb2P9Yx.js} +1 -1
- package/dist/dashboard/assets/clipboard-button-DGb2P9Yx.js.gz +0 -0
- package/dist/dashboard/assets/{dialog-Iec_ywbD.js → dialog-LkfuL50u.js} +1 -1
- package/dist/dashboard/assets/dialog-LkfuL50u.js.gz +0 -0
- package/dist/dashboard/assets/{drawer-Bs8MIIHc.js → drawer-ZbEZ_84c.js} +1 -1
- package/dist/dashboard/assets/drawer-ZbEZ_84c.js.gz +0 -0
- package/dist/dashboard/assets/{entry-DcobaKFu.js → entry-BJfsVMvA.js} +1 -1
- package/dist/dashboard/assets/entry-BJfsVMvA.js.gz +0 -0
- package/dist/dashboard/assets/{entry-CBHzQ6Qh.js → entry-CWBY5A0f.js} +3 -3
- package/dist/dashboard/assets/entry-CWBY5A0f.js.gz +0 -0
- package/dist/dashboard/assets/{entry-BRszm4_Z.js → entry-CvC1-Zqx.js} +1 -1
- package/dist/dashboard/assets/entry-CvC1-Zqx.js.gz +0 -0
- package/dist/dashboard/assets/{github-badge-BuDsrsPn.js → github-badge-BxpznFLt.js} +1 -1
- package/dist/dashboard/assets/github-badge-BxpznFLt.js.gz +0 -0
- package/dist/dashboard/assets/{index-Ck4uwuYb.js → index-CDzP3ovJ.js} +2 -2
- package/dist/dashboard/assets/index-CDzP3ovJ.js.gz +0 -0
- package/dist/dashboard/assets/{inline-DToMICwl.js → inline-Ae13MQ5w.js} +1 -1
- package/dist/dashboard/assets/inline-Ae13MQ5w.js.gz +0 -0
- package/dist/dashboard/assets/{input-WlyHWeG2.js → input-D23fo7cp.js} +1 -1
- package/dist/dashboard/assets/input-D23fo7cp.js.gz +0 -0
- package/dist/dashboard/assets/{layout-DaJ5baJr.js → layout-CSsxjj5P.js} +1 -1
- package/dist/dashboard/assets/layout-CSsxjj5P.js.gz +0 -0
- package/dist/dashboard/assets/{list-row-B3oSK13z.js → list-row-dmbwZMDY.js} +1 -1
- package/dist/dashboard/assets/list-row-dmbwZMDY.js.gz +0 -0
- package/dist/dashboard/assets/{live-ta6GMb-8.js → live-BG1oUsZZ.js} +1 -1
- package/dist/dashboard/assets/live-BG1oUsZZ.js.gz +0 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +1 -0
- package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
- package/dist/dashboard/assets/{mutation-g_fduiNw.js → mutation-DSF-e4ho.js} +1 -1
- package/dist/dashboard/assets/mutation-DSF-e4ho.js.gz +0 -0
- package/dist/dashboard/assets/{numbers-Dulqgys5.js → numbers-Bm_aECY7.js} +1 -1
- package/dist/dashboard/assets/{numbers-Dulqgys5.js.gz → numbers-Bm_aECY7.js.gz} +0 -0
- package/dist/dashboard/assets/{query-keys-UETqfytp.js → query-keys-CF56nYIV.js} +1 -1
- package/dist/dashboard/assets/query-keys-CF56nYIV.js.gz +0 -0
- package/dist/dashboard/assets/{routing-BPUo-W8d.js → routing-CeA71n7G.js} +1 -1
- package/dist/dashboard/assets/routing-CeA71n7G.js.gz +0 -0
- package/dist/dashboard/assets/{select-BrrUDUPc.js → select-KIT0rC6G.js} +1 -1
- package/dist/dashboard/assets/select-KIT0rC6G.js.gz +0 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js +1 -0
- package/dist/dashboard/assets/settings-B_2gkePf.js.gz +0 -0
- package/dist/dashboard/assets/{stack-B1w5BM0u.js → stack-D7WOSzcX.js} +1 -1
- package/dist/dashboard/assets/stack-D7WOSzcX.js.gz +0 -0
- package/dist/dashboard/assets/{state-DhZBfWgx.js → state-BBxtKZOQ.js} +1 -1
- package/dist/dashboard/assets/state-BBxtKZOQ.js.gz +0 -0
- package/dist/dashboard/assets/{switch-yD7D99Lg.js → switch-DNS_D5Ea.js} +1 -1
- package/dist/dashboard/assets/switch-DNS_D5Ea.js.gz +0 -0
- package/dist/dashboard/assets/{useMutation-EDFRkn1i.js → useMutation-BgoxdoMk.js} +1 -1
- package/dist/dashboard/assets/useMutation-BgoxdoMk.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/package.json +96 -96
- package/src/boot/app.ts +714 -722
- package/src/boot/dependencies.ts +729 -750
- package/src/config/resources.ts +70 -137
- package/src/config/security.ts +37 -73
- package/src/config/spec.ts +358 -378
- package/src/console/console-router.ts +225 -233
- package/src/console/domain-registration.ts +381 -391
- package/src/console/remote-support/contracts.ts +3 -1
- package/src/console/remote-support/service.ts +115 -6
- package/src/main.ts +167 -169
- package/src/observability/metrics.ts +467 -457
- package/src/observability/runtime-metrics.ts +269 -254
- package/src/protocol/gateway-error.ts +232 -233
- package/src/providers/integrations/commandcode/commandcode.ts +134 -104
- package/src/providers/integrations/commandcode/discovery.ts +6 -5
- package/src/providers/integrations/qoder/qoder.ts +597 -23
- package/src/providers/operations/account-health-service.ts +52 -3
- package/src/security/abuse.ts +17 -0
- package/src/transport/guards/auth-guard.ts +109 -127
- package/src/transport/guards/pipeline.ts +220 -226
- package/src/transport/guards/request-context.ts +238 -318
- package/src/transport/request/inflight.ts +21 -2
- package/src/workers/scheduler.ts +73 -6
- package/dist/dashboard/assets/BannedPage-1v5rzKMl.js.gz +0 -0
- package/dist/dashboard/assets/CliToolDetailPage-chMg74ct.js.gz +0 -0
- package/dist/dashboard/assets/CliToolsPage-CKG-Vxrt.js.gz +0 -0
- package/dist/dashboard/assets/CombosPage-JYmVKzIx.js.gz +0 -0
- package/dist/dashboard/assets/ConsoleLogPage-CPflWDry.js.gz +0 -0
- package/dist/dashboard/assets/CustomizationPage-B0EYUE47.js.gz +0 -0
- package/dist/dashboard/assets/HealthEventsModal-F1c4lkNG.js.gz +0 -0
- package/dist/dashboard/assets/LoginPage-BYTpzcoU.js.gz +0 -0
- package/dist/dashboard/assets/ModelPicker-CA1kLs45.js.gz +0 -0
- package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js +0 -1
- package/dist/dashboard/assets/OverviewPage-DIYZRAcb.js.gz +0 -0
- package/dist/dashboard/assets/ProviderDetailPage-CM4MNy45.js.gz +0 -0
- package/dist/dashboard/assets/ProvidersPage-Bsoqg2HG.js.gz +0 -0
- package/dist/dashboard/assets/ProxyPage-8G3doV7j.js.gz +0 -0
- package/dist/dashboard/assets/QuotaPage-CVIQq6JG.js.gz +0 -0
- package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js +0 -1
- package/dist/dashboard/assets/SettingsPage-BgXvRN2Q.js.gz +0 -0
- package/dist/dashboard/assets/SetupPage-BCx07-bj.js.gz +0 -0
- package/dist/dashboard/assets/SortableList-MhkB6msu.js.gz +0 -0
- package/dist/dashboard/assets/StudioPage-CTqK6Nsj.js.gz +0 -0
- package/dist/dashboard/assets/UsagePage-Do65OWbr.js +0 -56
- package/dist/dashboard/assets/UsagePage-Do65OWbr.js.gz +0 -0
- package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
- package/dist/dashboard/assets/app-BUSiYL6F.css +0 -2
- package/dist/dashboard/assets/app-BUSiYL6F.css.gz +0 -0
- package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
- package/dist/dashboard/assets/badge-Bkz_-RrM.js.gz +0 -0
- package/dist/dashboard/assets/banner-C-7y3VKV.js.gz +0 -0
- package/dist/dashboard/assets/card-BOPBuBzD.js.gz +0 -0
- package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
- package/dist/dashboard/assets/clipboard-button-e-Mlk0FH.js.gz +0 -0
- package/dist/dashboard/assets/dialog-Iec_ywbD.js.gz +0 -0
- package/dist/dashboard/assets/drawer-Bs8MIIHc.js.gz +0 -0
- package/dist/dashboard/assets/entry-BRszm4_Z.js.gz +0 -0
- package/dist/dashboard/assets/entry-CBHzQ6Qh.js.gz +0 -0
- package/dist/dashboard/assets/entry-DcobaKFu.js.gz +0 -0
- package/dist/dashboard/assets/github-badge-BuDsrsPn.js.gz +0 -0
- package/dist/dashboard/assets/index-Ck4uwuYb.js.gz +0 -0
- package/dist/dashboard/assets/inline-DToMICwl.js.gz +0 -0
- package/dist/dashboard/assets/input-WlyHWeG2.js.gz +0 -0
- package/dist/dashboard/assets/layout-DaJ5baJr.js.gz +0 -0
- package/dist/dashboard/assets/list-row-B3oSK13z.js.gz +0 -0
- package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js +0 -1
- package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
- package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
- package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
- package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
- package/dist/dashboard/assets/select-BrrUDUPc.js.gz +0 -0
- package/dist/dashboard/assets/settings--3hxSyry.js +0 -1
- package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
- package/dist/dashboard/assets/stack-B1w5BM0u.js.gz +0 -0
- package/dist/dashboard/assets/state-DhZBfWgx.js.gz +0 -0
- package/dist/dashboard/assets/switch-yD7D99Lg.js.gz +0 -0
- package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
- package/src/console/admin/model-abuse/contracts.ts +0 -38
- package/src/console/admin/model-abuse/routes.ts +0 -97
- package/src/security/model-abuse.ts +0 -704
- package/src/transport/guards/concurrency-guard.ts +0 -125
- /package/dist/dashboard/assets/{app-MdVfwraB.js → app-CaXZ9SCn.js} +0 -0
- /package/dist/dashboard/assets/{app-MdVfwraB.js.gz → app-CaXZ9SCn.js.gz} +0 -0
|
@@ -1,318 +1,238 @@
|
|
|
1
|
-
// Per-request context, client identity, canonical parse, and route preparation.
|
|
2
|
-
import { Elysia } from "elysia";
|
|
3
|
-
import { GatewayError } from "../../protocol/gateway-error";
|
|
4
|
-
import type { TrustedProxyBoundary } from "../../config/network";
|
|
5
|
-
import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
|
|
6
|
-
import type { ProxyRequestStateStore } from "../request/state";
|
|
7
|
-
import { fastPathname } from "../pathname";
|
|
8
|
-
import type { ProxyRequestPreparer } from "../request/preparer";
|
|
9
|
-
import type { CanonicalAdapter } from "../contracts";
|
|
10
|
-
import type { SurfaceAdapterRegistry } from "../surface/adapters";
|
|
11
|
-
import { resolveClientIdentity } from "../../security/ip-boundary";
|
|
12
|
-
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
13
|
-
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
14
|
-
import { log } from "../../observability/logger";
|
|
15
|
-
import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
|
|
16
|
-
import { isOpaqueBodyRoute } from "../contracts";
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
readonly
|
|
71
|
-
readonly
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
request
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// only in `full` mode (see `captureTerminalPayload`). For `none`/`metadata`
|
|
240
|
-
// drop the reference now: held until cleanup, it would otherwise stay
|
|
241
|
-
// resident for the entire upstream stream.
|
|
242
|
-
if (!(await deps.keepsIngressBody(ingress.authorization?.tenantId ?? null))) {
|
|
243
|
-
ingress.releaseIngressBody();
|
|
244
|
-
}
|
|
245
|
-
})
|
|
246
|
-
.as("plugin");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function createProxyRoutePreparationMiddleware(deps: {
|
|
250
|
-
readonly stateStore: ProxyRequestStateStore;
|
|
251
|
-
readonly preparer: ProxyRequestPreparer;
|
|
252
|
-
/**
|
|
253
|
-
* Graduated strikes for repeated invalid-model requests. The preparation step
|
|
254
|
-
* is the single choke point where a request is rejected for naming a model the
|
|
255
|
-
* key may not use (`isModelAllowed`) or a model that resolves to nothing
|
|
256
|
-
* (`modelNotFoundError`), so it is where a strike is recorded and where a
|
|
257
|
-
* valid model clears one.
|
|
258
|
-
*/
|
|
259
|
-
readonly modelStrikes?: ModelStrikeService;
|
|
260
|
-
}): Elysia {
|
|
261
|
-
return new Elysia()
|
|
262
|
-
.beforeHandle(async ({ request }) => {
|
|
263
|
-
const path = fastPathname(request.url);
|
|
264
|
-
if (!path.startsWith("/v1/")) return;
|
|
265
|
-
if (request.method === "GET" || request.method === "HEAD") return;
|
|
266
|
-
const isJsonRoute = isJsonProxyRoutePath(path);
|
|
267
|
-
if (!isJsonRoute || path === "/v1/responses/compact" || isOpaqueBodyRoute(path)) return;
|
|
268
|
-
const state = deps.stateStore.require(request);
|
|
269
|
-
if (!state.authorization || !state.canonicalRequest)
|
|
270
|
-
throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
|
|
271
|
-
const strikes = deps.modelStrikes;
|
|
272
|
-
const ip = state.clientIdentity?.address;
|
|
273
|
-
// The key rides along so a strike is charged to the caller that made it.
|
|
274
|
-
// Without it, one recipient of a shared link could escalate a ban onto
|
|
275
|
-
// every other caller behind the same address.
|
|
276
|
-
const strikeIdentity =
|
|
277
|
-
strikes && ip !== undefined
|
|
278
|
-
? { strikes, ip, apiKeyId: state.authorization?.id ?? null }
|
|
279
|
-
: undefined;
|
|
280
|
-
try {
|
|
281
|
-
state.preparedRequest = await deps.preparer.prepare({
|
|
282
|
-
canonicalRequest: state.canonicalRequest,
|
|
283
|
-
authorization: state.authorization,
|
|
284
|
-
deadlineMs: state.deadlineMs,
|
|
285
|
-
signal: state.abortController.signal,
|
|
286
|
-
...(state.clientUserAgent === undefined
|
|
287
|
-
? {}
|
|
288
|
-
: { clientUserAgent: state.clientUserAgent }),
|
|
289
|
-
});
|
|
290
|
-
} catch (error) {
|
|
291
|
-
// Only a *model* rejection is a strike: an invalid model the key may not
|
|
292
|
-
// use, or a name that resolves to nothing. A store outage, an aborted
|
|
293
|
-
// request, or any other failure is not the caller probing for models.
|
|
294
|
-
if (strikeIdentity && isModelRejection(error)) {
|
|
295
|
-
const outcome = await strikeIdentity.strikes
|
|
296
|
-
.noteInvalid({ ip: strikeIdentity.ip, apiKeyId: strikeIdentity.apiKeyId })
|
|
297
|
-
.catch(() => null);
|
|
298
|
-
if (outcome?.banned === true) throw modelAbuseBannedError();
|
|
299
|
-
if (outcome) {
|
|
300
|
-
const snap = state.authorization?.snapshot;
|
|
301
|
-
const raw = snap?.model_list;
|
|
302
|
-
const hint = Array.isArray(raw) ? (raw as readonly string[]) : raw instanceof Set ? [...raw] : undefined;
|
|
303
|
-
throw withModelWarning(error, outcome, strikeIdentity.strikes.limit, {
|
|
304
|
-
allowedModels: hint,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
throw error;
|
|
309
|
-
}
|
|
310
|
-
// A valid model does not reset the invalid-attempt window. Old attempts
|
|
311
|
-
// leave only when they age out of that window.
|
|
312
|
-
if (strikeIdentity)
|
|
313
|
-
void strikeIdentity.strikes
|
|
314
|
-
.noteValid({ ip: strikeIdentity.ip })
|
|
315
|
-
.catch(() => undefined);
|
|
316
|
-
})
|
|
317
|
-
.as("plugin");
|
|
318
|
-
}
|
|
1
|
+
// Per-request context, client identity, canonical parse, and route preparation.
|
|
2
|
+
import { Elysia } from "elysia";
|
|
3
|
+
import { GatewayError } from "../../protocol/gateway-error";
|
|
4
|
+
import type { TrustedProxyBoundary } from "../../config/network";
|
|
5
|
+
import { resolveUpstreamTimeoutMs } from "../../config/timeouts";
|
|
6
|
+
import type { ProxyRequestStateStore } from "../request/state";
|
|
7
|
+
import { fastPathname } from "../pathname";
|
|
8
|
+
import type { ProxyRequestPreparer } from "../request/preparer";
|
|
9
|
+
import type { CanonicalAdapter } from "../contracts";
|
|
10
|
+
import type { SurfaceAdapterRegistry } from "../surface/adapters";
|
|
11
|
+
import { resolveClientIdentity } from "../../security/ip-boundary";
|
|
12
|
+
import { GATEWAY_SECURITY_HEADERS } from "../../security/outbound-headers";
|
|
13
|
+
import { pushStructuredConsoleLog } from "../../observability/log-ring";
|
|
14
|
+
import { log } from "../../observability/logger";
|
|
15
|
+
import { isJsonProxyRoutePath, isProxyDispatchRoute } from "./body-policy";
|
|
16
|
+
import { isOpaqueBodyRoute } from "../contracts";
|
|
17
|
+
import { peerAddressUnavailableError } from "./guard-errors";
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
interface RequestApp {
|
|
22
|
+
request(
|
|
23
|
+
handler: (context: { request: Request; set?: { headers: Record<string, string> } }) => void,
|
|
24
|
+
): RequestApp;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface RequestContextDeps {
|
|
28
|
+
readonly stateStore: ProxyRequestStateStore;
|
|
29
|
+
readonly clock?: () => number;
|
|
30
|
+
/** Explicit deadline override; defaults to `CARTETHYIA_UPSTREAM_TIMEOUT_MS` (120s). */
|
|
31
|
+
readonly requestDeadlineMs?: number;
|
|
32
|
+
readonly maxBodyBytes?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Initializes per-request `ProxyRequestState` before any other hook runs.
|
|
37
|
+
* Only gateway (`/v1/*`) requests get state: this middleware is mounted at
|
|
38
|
+
* the composition root so it also observes health checks, console traffic,
|
|
39
|
+
* and dashboard assets, but none of those use proxy state and none of them
|
|
40
|
+
* run the `/v1`-scoped cleanup hooks — initializing for them leaked a state,
|
|
41
|
+
* a deadline timer, a live-controller entry, and an in-flight count per
|
|
42
|
+
* request that was never reclaimed. Cleanup (abort + WeakMap eviction) is
|
|
43
|
+
* intentionally NOT done here via `afterResponse`: Elysia's afterResponse
|
|
44
|
+
* hook order across plugins is not something this composition can guarantee
|
|
45
|
+
* ahead of `createTelemetryLifecycleMiddleware`'s afterResponse (now in
|
|
46
|
+
* lifecycle.ts), which still needs to read the state. The WeakMap entry is
|
|
47
|
+
* reclaimed by GC once `request` is unreferenced; nothing needs to run after
|
|
48
|
+
* the response is sent.
|
|
49
|
+
*/
|
|
50
|
+
export function createRequestContextMiddleware(deps: RequestContextDeps): Elysia {
|
|
51
|
+
const app = new Elysia() as unknown as RequestApp;
|
|
52
|
+
app.request(({ request, set }) => {
|
|
53
|
+
const pathname = fastPathname(request.url);
|
|
54
|
+
if (pathname !== "/v1" && !pathname.startsWith("/v1/")) return;
|
|
55
|
+
const state = deps.stateStore.initialize(
|
|
56
|
+
request,
|
|
57
|
+
(deps.clock ?? (() => Date.now()))(),
|
|
58
|
+
deps.requestDeadlineMs ?? resolveUpstreamTimeoutMs(),
|
|
59
|
+
);
|
|
60
|
+
if (set) {
|
|
61
|
+
set.headers["x-request-id"] = state.requestId;
|
|
62
|
+
Object.assign(set.headers, GATEWAY_SECURITY_HEADERS);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return app as unknown as Elysia;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createClientIdentityMiddleware(deps: {
|
|
69
|
+
readonly stateStore: ProxyRequestStateStore;
|
|
70
|
+
readonly trustedProxyBoundary: TrustedProxyBoundary;
|
|
71
|
+
readonly resolvePeerAddress?: (request: Request) => string | null;
|
|
72
|
+
}): Elysia {
|
|
73
|
+
// `beforeHandle`, not `onRequest`: plugin-scoped `onRequest` hooks do not
|
|
74
|
+
// fire for these routes under Elysia 2 beta (same scoping gap that muted
|
|
75
|
+
// plugin `afterResponse` before `registerTelemetryLifecycle`), which left
|
|
76
|
+
// `clientIdentity` unset and every gateway row without a client IP.
|
|
77
|
+
// `beforeHandle` hooks from `.use()` plugins run in registration order, so
|
|
78
|
+
// pipeline position still guarantees this executes before IP-abuse checks.
|
|
79
|
+
return new Elysia()
|
|
80
|
+
.beforeHandle(({ request, server }) => {
|
|
81
|
+
const peer = deps.resolvePeerAddress
|
|
82
|
+
? deps.resolvePeerAddress(request)
|
|
83
|
+
: (server?.requestIP(request)?.address ?? null);
|
|
84
|
+
if (!peer)
|
|
85
|
+
throw peerAddressUnavailableError();
|
|
86
|
+
const address = resolveClientIdentity(request, deps.trustedProxyBoundary, peer);
|
|
87
|
+
const state = deps.stateStore.require(request);
|
|
88
|
+
state.clientIdentity = {
|
|
89
|
+
address,
|
|
90
|
+
source: address === peer ? "tcp-peer" : "trusted-forwarded-header",
|
|
91
|
+
};
|
|
92
|
+
const path = state.ingressPath;
|
|
93
|
+
if (!isProxyDispatchRoute(path)) return;
|
|
94
|
+
// The ingress policy stage already decoded the body, so the client's
|
|
95
|
+
// requested model is readable here — before canonical parsing runs. It is
|
|
96
|
+
// read defensively and only for display: routing still resolves the model
|
|
97
|
+
// through the surface adapter, and a body that is not an object simply
|
|
98
|
+
// yields no model rather than failing the request.
|
|
99
|
+
const body = state.ingressBody;
|
|
100
|
+
const requestedModel =
|
|
101
|
+
body !== null && typeof body === "object" && !Array.isArray(body)
|
|
102
|
+
? (body as Record<string, unknown>)["model"]
|
|
103
|
+
: undefined;
|
|
104
|
+
const userAgent = request.headers.get("user-agent");
|
|
105
|
+
pushStructuredConsoleLog("info", "Incoming proxy request", {
|
|
106
|
+
event: "request_start",
|
|
107
|
+
requestId: state.requestId,
|
|
108
|
+
method: request.method,
|
|
109
|
+
endpoint: path,
|
|
110
|
+
clientIp: address,
|
|
111
|
+
...(typeof requestedModel === "string" && requestedModel.length > 0
|
|
112
|
+
? { model: requestedModel }
|
|
113
|
+
: {}),
|
|
114
|
+
...(userAgent === null || userAgent.length === 0
|
|
115
|
+
? {}
|
|
116
|
+
: { userAgent: userAgent.slice(0, 512) }),
|
|
117
|
+
});
|
|
118
|
+
})
|
|
119
|
+
.as("plugin");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createCanonicalRequestMiddleware(deps: {
|
|
123
|
+
readonly stateStore: ProxyRequestStateStore;
|
|
124
|
+
readonly surfaceRegistry: SurfaceAdapterRegistry;
|
|
125
|
+
readonly adapters: ReadonlyMap<string, CanonicalAdapter>;
|
|
126
|
+
/**
|
|
127
|
+
* Whether this tenant's payload-capture mode keeps request bodies
|
|
128
|
+
* (`full`). `none`/`metadata` never read the request body at finalize, so
|
|
129
|
+
* the parsed ingress body can be released as soon as the canonical request
|
|
130
|
+
* exists instead of being held — across the whole upstream stream. Injected
|
|
131
|
+
* so this stage does not import the capture policy or the database.
|
|
132
|
+
*/
|
|
133
|
+
readonly keepsIngressBody: (tenantId: string | null) => Promise<boolean>;
|
|
134
|
+
}): Elysia {
|
|
135
|
+
return new Elysia()
|
|
136
|
+
.beforeHandle(async ({ request }) => {
|
|
137
|
+
const path = fastPathname(request.url);
|
|
138
|
+
if (!path.startsWith("/v1/")) return;
|
|
139
|
+
if (request.method === "GET" || request.method === "HEAD") return;
|
|
140
|
+
// `/v1/completions` needs no separate arm: it is already in
|
|
141
|
+
// the JSON routes table, so the body-policy predicate covers it.
|
|
142
|
+
// Native routes (compact, System One) read a body but are never parsed
|
|
143
|
+
// into a canonical request — their bodies are opaque by contract.
|
|
144
|
+
if (!isJsonProxyRoutePath(path) || path === "/v1/responses/compact" || isOpaqueBodyRoute(path))
|
|
145
|
+
return;
|
|
146
|
+
// Model discovery and other GET/multimodal routes must not go through canonical parsing.
|
|
147
|
+
const state = deps.stateStore.require(request);
|
|
148
|
+
// Single-read invariant: the ingress policy middleware already decoded the
|
|
149
|
+
// body once into `state.ingressBody`. Never re-read `request.body` here —
|
|
150
|
+
// it is already consumed/locked. `undefined` means the ingress stage did
|
|
151
|
+
// not run (reduced composition); fail closed. An explicit `null` (JSON
|
|
152
|
+
// literal `null` body) is a valid single-read value and flows to the
|
|
153
|
+
// surface adapter's parse error rather than triggering a second read.
|
|
154
|
+
if (state.ingressBody === undefined)
|
|
155
|
+
throw new GatewayError("admission_unavailable", 503, "The request body was never read, so this route cannot be served. Resend the request.");
|
|
156
|
+
const body = state.ingressBody;
|
|
157
|
+
const headers: Record<string, string> = {};
|
|
158
|
+
request.headers.forEach((value, key) => {
|
|
159
|
+
headers[key] = value;
|
|
160
|
+
});
|
|
161
|
+
const detection = deps.surfaceRegistry.detectOnce({ body, headers, path });
|
|
162
|
+
if (detection.disagreement) {
|
|
163
|
+
// A path-vs-body disagreement is expected traffic, not a fault: a
|
|
164
|
+
// standard chat body ({model, messages[], max_tokens}) also matches
|
|
165
|
+
// the messages body shape, so nearly every chat request with
|
|
166
|
+
// max_tokens "disagrees". Warn only when the explicit header is
|
|
167
|
+
// involved — a marker contradicting the path means the client is
|
|
168
|
+
// genuinely confused about which surface it wants.
|
|
169
|
+
const markerInvolved = detection.signals.some((s) => s.source === "explicit_marker");
|
|
170
|
+
if (markerInvolved) {
|
|
171
|
+
log.warn("[surface] conflicting signals detected for surface routing", {
|
|
172
|
+
resolved: detection.surface,
|
|
173
|
+
winning_source: detection.winning_source,
|
|
174
|
+
signals: detection.signals,
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
log.debug("[surface] ambiguous body shape resolved by endpoint path", {
|
|
178
|
+
resolved: detection.surface,
|
|
179
|
+
signals: detection.signals,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const adapter = deps.adapters.get(detection.surface);
|
|
184
|
+
if (!adapter)
|
|
185
|
+
throw new GatewayError(
|
|
186
|
+
"capability_unsupported",
|
|
187
|
+
400,
|
|
188
|
+
`Surface adapter unavailable: ${detection.surface}`,
|
|
189
|
+
{ surface: detection.surface },
|
|
190
|
+
);
|
|
191
|
+
const ingress = deps.stateStore.require(request);
|
|
192
|
+
const userAgent = headers["user-agent"];
|
|
193
|
+
if (userAgent) ingress.clientUserAgent = userAgent.slice(0, 512);
|
|
194
|
+
ingress.canonicalRequest = adapter.parse({ body, headers, path });
|
|
195
|
+
// The parsed body is the largest per-request value, and once the
|
|
196
|
+
// canonical request exists the only later reader is payload capture — and
|
|
197
|
+
// only in `full` mode (see `captureTerminalPayload`). For `none`/`metadata`
|
|
198
|
+
// drop the reference now: held until cleanup, it would otherwise stay
|
|
199
|
+
// resident for the entire upstream stream.
|
|
200
|
+
if (!(await deps.keepsIngressBody(ingress.authorization?.tenantId ?? null))) {
|
|
201
|
+
ingress.releaseIngressBody();
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.as("plugin");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createProxyRoutePreparationMiddleware(deps: {
|
|
208
|
+
readonly stateStore: ProxyRequestStateStore;
|
|
209
|
+
readonly preparer: ProxyRequestPreparer;
|
|
210
|
+
}): Elysia {
|
|
211
|
+
return new Elysia()
|
|
212
|
+
.beforeHandle(async ({ request }) => {
|
|
213
|
+
const path = fastPathname(request.url);
|
|
214
|
+
if (!path.startsWith("/v1/")) return;
|
|
215
|
+
if (request.method === "GET" || request.method === "HEAD") return;
|
|
216
|
+
const isJsonRoute = isJsonProxyRoutePath(path);
|
|
217
|
+
if (!isJsonRoute || path === "/v1/responses/compact" || isOpaqueBodyRoute(path)) return;
|
|
218
|
+
const state = deps.stateStore.require(request);
|
|
219
|
+
if (!state.authorization || !state.canonicalRequest)
|
|
220
|
+
throw new GatewayError("admission_unavailable", 503, "The request was not authenticated and parsed, so this route cannot be served. Resend the request.");
|
|
221
|
+
// The key rides along so a strike is charged to the caller that made it.
|
|
222
|
+
// Without it, one recipient of a shared link could escalate a ban onto
|
|
223
|
+
try {
|
|
224
|
+
state.preparedRequest = await deps.preparer.prepare({
|
|
225
|
+
canonicalRequest: state.canonicalRequest,
|
|
226
|
+
authorization: state.authorization,
|
|
227
|
+
deadlineMs: state.deadlineMs,
|
|
228
|
+
signal: state.abortController.signal,
|
|
229
|
+
...(state.clientUserAgent === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: { clientUserAgent: state.clientUserAgent }),
|
|
232
|
+
});
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
.as("plugin");
|
|
238
|
+
}
|
|
@@ -22,12 +22,23 @@ import { metrics } from "../../observability/metrics";
|
|
|
22
22
|
export interface InFlightSnapshot {
|
|
23
23
|
readonly inFlight: number;
|
|
24
24
|
readonly uniqueIps: number;
|
|
25
|
+
/**
|
|
26
|
+
* How long the longest-lived flight has been tracked, in milliseconds.
|
|
27
|
+
*
|
|
28
|
+
* The count alone cannot tell a healthy queue from a leak: six flights
|
|
29
|
+
* could be six requests in progress or six that never finished. The age
|
|
30
|
+
* separates them — a flight far past its own deadline is the backstop's
|
|
31
|
+
* job, so a growing age with a flat count means the sweep is not running.
|
|
32
|
+
*/
|
|
33
|
+
readonly oldestAgeMs: number;
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
interface Flight {
|
|
28
37
|
readonly clientIp: string;
|
|
29
38
|
/** Hard deadline after which the backstop sweep releases this flight. */
|
|
30
39
|
expiresAtMs: number;
|
|
40
|
+
/** When this flight was tracked, for the age reported in a snapshot. */
|
|
41
|
+
readonly startedAtMs: number;
|
|
31
42
|
}
|
|
32
43
|
|
|
33
44
|
export interface InFlightRegistry {
|
|
@@ -50,14 +61,18 @@ export function createInFlightRegistry(): InFlightRegistry {
|
|
|
50
61
|
|
|
51
62
|
const snapshot = (): InFlightSnapshot => {
|
|
52
63
|
let uniqueIps = 0;
|
|
64
|
+
let oldest = 0;
|
|
65
|
+
const now = Date.now();
|
|
53
66
|
const seen = new Set<string>();
|
|
54
67
|
for (const flight of flights.values()) {
|
|
55
68
|
if (!seen.has(flight.clientIp)) {
|
|
56
69
|
seen.add(flight.clientIp);
|
|
57
70
|
uniqueIps += 1;
|
|
58
71
|
}
|
|
72
|
+
const age = now - flight.startedAtMs;
|
|
73
|
+
if (age > oldest) oldest = age;
|
|
59
74
|
}
|
|
60
|
-
return { inFlight: flights.size, uniqueIps };
|
|
75
|
+
return { inFlight: flights.size, uniqueIps, oldestAgeMs: oldest };
|
|
61
76
|
};
|
|
62
77
|
|
|
63
78
|
const publish = (): void => {
|
|
@@ -68,7 +83,11 @@ export function createInFlightRegistry(): InFlightRegistry {
|
|
|
68
83
|
|
|
69
84
|
return {
|
|
70
85
|
track(requestId, clientIp, expiresAtMs) {
|
|
71
|
-
flights.set(requestId, {
|
|
86
|
+
flights.set(requestId, {
|
|
87
|
+
clientIp: clientIp.length > 0 ? clientIp : "unknown",
|
|
88
|
+
expiresAtMs,
|
|
89
|
+
startedAtMs: Date.now(),
|
|
90
|
+
});
|
|
72
91
|
publish();
|
|
73
92
|
},
|
|
74
93
|
untrack(requestId) {
|