@evomap/evolver-proxy 2.0.0-beta.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/README.md +49 -0
- package/dist/bin/envFile.d.ts +10 -0
- package/dist/bin/envFile.js +68 -0
- package/dist/bin/evolver-llm-proxy.d.ts +2 -0
- package/dist/bin/evolver-llm-proxy.js +111 -0
- package/dist/bin/evolver-proxy.d.ts +83 -0
- package/dist/bin/evolver-proxy.js +511 -0
- package/dist/bin/proxySettings.d.ts +15 -0
- package/dist/bin/proxySettings.js +84 -0
- package/dist/bin/proxyStorePath.d.ts +1 -0
- package/dist/bin/proxyStorePath.js +16 -0
- package/dist/daemon/atpConsent.d.ts +13 -0
- package/dist/daemon/atpConsent.js +60 -0
- package/dist/daemon/ipcConfig.d.ts +1 -0
- package/dist/daemon/ipcConfig.js +13 -0
- package/dist/daemon/proxyDaemon.d.ts +191 -0
- package/dist/daemon/proxyDaemon.js +1015 -0
- package/dist/daemon/selectHub.d.ts +16 -0
- package/dist/daemon/selectHub.js +30 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/lifecycle/deployGuard.d.ts +46 -0
- package/dist/lifecycle/deployGuard.js +53 -0
- package/dist/lifecycle/legacyNodeId.d.ts +96 -0
- package/dist/lifecycle/legacyNodeId.js +163 -0
- package/dist/lifecycle/manager.d.ts +106 -0
- package/dist/lifecycle/manager.js +390 -0
- package/dist/llm/bodyCapture.d.ts +31 -0
- package/dist/llm/bodyCapture.js +293 -0
- package/dist/llm/index.d.ts +3 -0
- package/dist/llm/index.js +3 -0
- package/dist/llm/server.d.ts +35 -0
- package/dist/llm/server.js +359 -0
- package/dist/llm/traceBackfill.d.ts +53 -0
- package/dist/llm/traceBackfill.js +525 -0
- package/dist/llm/traceConfig.d.ts +7 -0
- package/dist/llm/traceConfig.js +44 -0
- package/dist/llm/traceControl.d.ts +16 -0
- package/dist/llm/traceControl.js +85 -0
- package/dist/llm/traceEnvelope.d.ts +75 -0
- package/dist/llm/traceEnvelope.js +286 -0
- package/dist/llm/traceSink.d.ts +61 -0
- package/dist/llm/traceSink.js +278 -0
- package/dist/llm/traceUploadPayload.d.ts +14 -0
- package/dist/llm/traceUploadPayload.js +27 -0
- package/dist/llm/upstream.d.ts +68 -0
- package/dist/llm/upstream.js +491 -0
- package/dist/private/adapterLoader.d.ts +46 -0
- package/dist/private/adapterLoader.js +62 -0
- package/dist/private/privateRuntimeSmokeOptions.d.ts +15 -0
- package/dist/private/privateRuntimeSmokeOptions.js +132 -0
- package/dist/router/cachePassthrough.d.ts +3 -0
- package/dist/router/cachePassthrough.js +13 -0
- package/dist/router/features.d.ts +9 -0
- package/dist/router/features.js +52 -0
- package/dist/router/index.d.ts +6 -0
- package/dist/router/index.js +6 -0
- package/dist/router/messagesRoute.d.ts +138 -0
- package/dist/router/messagesRoute.js +753 -0
- package/dist/router/modelRouter.d.ts +49 -0
- package/dist/router/modelRouter.js +66 -0
- package/dist/router/providerRoutes.d.ts +42 -0
- package/dist/router/providerRoutes.js +1579 -0
- package/dist/router/sseScan.d.ts +56 -0
- package/dist/router/sseScan.js +543 -0
- package/dist/selfUpdate/executor.d.ts +90 -0
- package/dist/selfUpdate/executor.js +179 -0
- package/dist/selfUpdate/failureCodes.d.ts +33 -0
- package/dist/selfUpdate/failureCodes.js +84 -0
- package/dist/selfUpdate/index.d.ts +5 -0
- package/dist/selfUpdate/index.js +5 -0
- package/dist/selfUpdate/lastUpdate.d.ts +43 -0
- package/dist/selfUpdate/lastUpdate.js +195 -0
- package/dist/selfUpdate/policy.d.ts +3 -0
- package/dist/selfUpdate/policy.js +9 -0
- package/dist/selfUpdate/releaseBinary.d.ts +56 -0
- package/dist/selfUpdate/releaseBinary.js +498 -0
- package/dist/selfUpdate/version.d.ts +2 -0
- package/dist/selfUpdate/version.js +14 -0
- package/dist/sync/engine.d.ts +65 -0
- package/dist/sync/engine.js +461 -0
- package/package.json +41 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Native request/response BODY capture for LLM traces.
|
|
2
|
+
//
|
|
3
|
+
// V1-COMPATIBLE DEFAULT: body capture is enabled unless explicitly downgraded with EVOMAP_PROXY_TRACE=metadata/off
|
|
4
|
+
// or EVOLVER_LLM_TRACE_CAPTURE_BODIES=0. Captured bodies still go through redaction + size caps, and trace storage
|
|
5
|
+
// must fail closed into an encrypted envelope unless the operator explicitly disables trace encryption.
|
|
6
|
+
//
|
|
7
|
+
// COMPLIANCE GATE: enabling this on real user traffic is a product/compliance decision (user consent, disclosure,
|
|
8
|
+
// retention policy). Operators that need metadata-only traces must explicitly configure that downgrade so it is a
|
|
9
|
+
// deliberate deployment choice rather than an accidental default.
|
|
10
|
+
//
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
// Redaction here is best-effort structural scrubbing (emails, phones, card numbers, bearer/api keys, JWTs) plus a
|
|
13
|
+
// hard Hub/envelope-sized cap. It is NOT a substitute for a real downstream redaction/PII policy; it exists so
|
|
14
|
+
// that captured bodies are never stored fully raw, and so the `redaction` marker on a row reflects an actual pass.
|
|
15
|
+
// When a body exceeds the cap, do not keep a preview: downstream must treat the turn as incomplete, not as a
|
|
16
|
+
// smaller-but-usable body.
|
|
17
|
+
/** Bump when the redaction ruleset changes so downstream can tell which scrub a row went through. */
|
|
18
|
+
export const REDACTION_VERSION = 'evolver-redact-v2';
|
|
19
|
+
function truthy(value) {
|
|
20
|
+
const raw = String(value ?? '').trim().toLowerCase();
|
|
21
|
+
return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
|
|
22
|
+
}
|
|
23
|
+
function firstConfiguredEnv(env, names) {
|
|
24
|
+
for (const name of names) {
|
|
25
|
+
const value = env[name];
|
|
26
|
+
if (value !== undefined && value.trim() !== '')
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export function legacyProxyTraceFullEnabled(env = process.env) {
|
|
32
|
+
const raw = env['EVOMAP_PROXY_TRACE'];
|
|
33
|
+
if (raw === undefined || raw.trim() === '')
|
|
34
|
+
return true;
|
|
35
|
+
return raw.trim().toLowerCase() === 'full';
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Native body capture switch. Defaults to the v1-compatible full trace mode, but explicit v2 capture flags win so
|
|
39
|
+
* operators can disable body capture without also setting the legacy EVOMAP_PROXY_TRACE knob.
|
|
40
|
+
*/
|
|
41
|
+
export function captureBodiesEnabled(env = process.env) {
|
|
42
|
+
const explicitCapture = firstConfiguredEnv(env, [
|
|
43
|
+
'EVOLVER_LLM_TRACE_CAPTURE_BODIES',
|
|
44
|
+
'EVOMAP_PROXY_TRACE_CAPTURE_BODIES',
|
|
45
|
+
]);
|
|
46
|
+
if (explicitCapture !== undefined)
|
|
47
|
+
return truthy(explicitCapture);
|
|
48
|
+
return legacyProxyTraceFullEnabled(env);
|
|
49
|
+
}
|
|
50
|
+
/** Default per-body cap (bytes), aligned with the public Hub mailbox outbound envelope limit. A single trace may
|
|
51
|
+
* still exceed the final encrypted outbound envelope when it carries multiple large bodies; in that case the row
|
|
52
|
+
* is explicitly marked incomplete rather than silently downgraded to a preview. */
|
|
53
|
+
export const DEFAULT_TRACE_ENVELOPE_MAX_CHARS = 4 * 1024 * 1024;
|
|
54
|
+
const AUTHORIZATION_KEY_RE = /^(?:proxy[-_]?)?authorization$/i;
|
|
55
|
+
const AUTHORIZATION_VALUE_RE = /^(\s*[A-Za-z][A-Za-z0-9!#$%&'*+.^_`|~-]*\s+)[\s\S]+$/;
|
|
56
|
+
function redactAuthorizationValue(value) {
|
|
57
|
+
const match = AUTHORIZATION_VALUE_RE.exec(value);
|
|
58
|
+
if (match?.[1])
|
|
59
|
+
return `${match[1]}[REDACTED_TOKEN]`;
|
|
60
|
+
return value.trim() ? '[REDACTED_TOKEN]' : value;
|
|
61
|
+
}
|
|
62
|
+
export function bodyMaxChars(env = process.env) {
|
|
63
|
+
return positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_BODY_MAX_CHARS', 'EVOMAP_PROXY_TRACE_MAX_FIELD_BYTES'], traceEnvelopeMaxChars(env));
|
|
64
|
+
}
|
|
65
|
+
export function traceEnvelopeMaxChars(env = process.env) {
|
|
66
|
+
return positiveIntegerFromEnv(env, ['EVOLVER_LLM_TRACE_ENVELOPE_MAX_CHARS', 'EVOMAP_PROXY_TRACE_ENVELOPE_MAX_BYTES'], DEFAULT_TRACE_ENVELOPE_MAX_CHARS);
|
|
67
|
+
}
|
|
68
|
+
export function positiveIntegerFromEnv(env, names, fallback) {
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const raw = Number(env[name]);
|
|
71
|
+
if (Number.isSafeInteger(raw) && raw > 0)
|
|
72
|
+
return raw;
|
|
73
|
+
}
|
|
74
|
+
return fallback;
|
|
75
|
+
}
|
|
76
|
+
const REDACTORS = [
|
|
77
|
+
// Env/config dump lines such as FOO_API_KEY=... or "fooToken": "...". Keep the key name; replace only value.
|
|
78
|
+
{ re: /(\b[A-Za-z_][A-Za-z0-9_]*(?:API[_-]?KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|AUTH|PRIVATE[_-]?KEY)\b\s*[:=]\s*)(["']?)([^"'\s,}\]]{6,})\2/gi, with: '$1[REDACTED_SECRET]' },
|
|
79
|
+
// Plain upstream error pages often echo HTTP headers outside JSON.
|
|
80
|
+
{
|
|
81
|
+
re: /(\b(?:proxy-)?authorization\s*:\s*)([^\r\n]+)/gi,
|
|
82
|
+
with: (_match, prefix, value) => `${String(prefix)}${redactAuthorizationValue(String(value ?? ''))}`,
|
|
83
|
+
},
|
|
84
|
+
// JSON/text fragments such as {"Authorization":"Bearer ..."} or escaped {\"Proxy-Authorization\":\"Basic ...\"}.
|
|
85
|
+
{
|
|
86
|
+
re: /((?:\\?["'])\s*(?:proxy-)?authorization\s*(?:\\?["'])\s*:\s*(?:\\?["']))((?:(?!\\?["'])[\s\S])*?)((?:\\?["']))/gi,
|
|
87
|
+
with: (_match, prefix, value, suffix) => `${String(prefix)}${redactAuthorizationValue(String(value ?? ''))}${String(suffix)}`,
|
|
88
|
+
},
|
|
89
|
+
{ re: /(\bcookie\s*:\s*)[^\r\n]+/gi, with: '$1[REDACTED_COOKIE]' },
|
|
90
|
+
// PEM private keys.
|
|
91
|
+
{ re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g, with: '[REDACTED_PRIVATE_KEY]' },
|
|
92
|
+
// Bearer tokens / Authorization header values.
|
|
93
|
+
{ re: /\b[Bb]earer\s+[A-Za-z0-9._~+/-]{12,}=*/g, with: 'Bearer [REDACTED_TOKEN]' },
|
|
94
|
+
// Provider API keys (OpenAI sk-..., Anthropic sk-ant-..., generic long secret-ish keys).
|
|
95
|
+
{ re: /\bsk-(?:ant-)?[A-Za-z0-9._-]{16,}/g, with: '[REDACTED_API_KEY]' },
|
|
96
|
+
// GitHub personal access tokens.
|
|
97
|
+
{ re: /\bghp_[A-Za-z0-9_]{20,}\b/g, with: '[REDACTED_GITHUB_TOKEN]' },
|
|
98
|
+
{ re: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, with: '[REDACTED_GITHUB_TOKEN]' },
|
|
99
|
+
// Slack bot/user/app tokens.
|
|
100
|
+
{ re: /\bxox[abcprs]-[A-Za-z0-9-]{10,}\b/g, with: '[REDACTED_SLACK_TOKEN]' },
|
|
101
|
+
// Common key/value credentials in text, JSON, YAML, query strings, and form bodies.
|
|
102
|
+
{
|
|
103
|
+
re: /((?:"|')?\b(?:api[_-]?key|password|client[_-]?secret|access[_-]?token|refresh[_-]?token|id[_-]?token|token)\b(?:"|')?\s*[:=]\s*)("[^"\r\n]*"|'[^'\r\n]*')/gi,
|
|
104
|
+
with: '$1"[REDACTED_CREDENTIAL]"',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
re: /((?:"|')?\b(?:api[_-]?key|password|client[_-]?secret|access[_-]?token|refresh[_-]?token|id[_-]?token|token)\b(?:"|')?\s*[:=]\s*)(?!["'])[^\r\n,&}]+/gi,
|
|
108
|
+
with: '$1[REDACTED_CREDENTIAL]',
|
|
109
|
+
},
|
|
110
|
+
// AWS access key ids.
|
|
111
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b/g, with: '[REDACTED_AWS_KEY]' },
|
|
112
|
+
// JWTs (three base64url segments).
|
|
113
|
+
{ re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, with: '[REDACTED_JWT]' },
|
|
114
|
+
// Email addresses.
|
|
115
|
+
{ re: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, with: '[REDACTED_EMAIL]' },
|
|
116
|
+
// Credit-card-like 13-19 digit runs (allowing spaces/dashes).
|
|
117
|
+
{ re: /\b(?:\d[ -]?){13,19}\b/g, with: '[REDACTED_CARD]', structuredIdentifierSafe: false },
|
|
118
|
+
// Long digit runs that look like phone numbers / national ids (11+ digits, optional +).
|
|
119
|
+
{ re: /\+?\d[\d\s-]{9,}\d/g, with: '[REDACTED_NUMBER]', structuredIdentifierSafe: false },
|
|
120
|
+
];
|
|
121
|
+
function applyRedactors(input, options = {}) {
|
|
122
|
+
let out = input;
|
|
123
|
+
for (const r of REDACTORS) {
|
|
124
|
+
if (options.preserveStructuredIdentifierNumbers === true && r.structuredIdentifierSafe === false)
|
|
125
|
+
continue;
|
|
126
|
+
const replacement = r.with;
|
|
127
|
+
out = typeof replacement === 'string'
|
|
128
|
+
? out.replace(r.re, replacement)
|
|
129
|
+
: out.replace(r.re, (...args) => replacement(args[0], ...args.slice(1)));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** Apply the redaction ruleset to a string. Pure; safe on arbitrary text. */
|
|
134
|
+
export function redactText(input) {
|
|
135
|
+
return applyRedactors(input);
|
|
136
|
+
}
|
|
137
|
+
const SENSITIVE_KEY_RE = /(?:^|[_-])(?:api[_-]?key|token|secret|password|credential|authorization|auth|bearer|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|id[_-]?token|session[_-]?token|cookie|dsn)(?:$|[_-])|(?:api[_-]?key|token|secret|password|credential|authorization|auth|private[_-]?key)$/i;
|
|
138
|
+
const STRUCTURED_IDENTIFIER_KEY_RE = /(?:^|[_-])(?:id|ids|fingerprint|hash|timestamp|time)(?:$|[_-])|(?:^|[_-])(?:created|updated|occurred|started|ended)[_-]at(?:$|[_-])/i;
|
|
139
|
+
const STABLE_IDENTITY_KEY_RE = /(?:^|[_-])(?:user|session|device)[_-]?id(?:$|[_-])/i;
|
|
140
|
+
const MAX_REDACTION_DEPTH = 30;
|
|
141
|
+
function keyParts(key) {
|
|
142
|
+
return key
|
|
143
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
144
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
145
|
+
.toLowerCase();
|
|
146
|
+
}
|
|
147
|
+
function redactValueForKey(value) {
|
|
148
|
+
if (value === undefined || value === null)
|
|
149
|
+
return value;
|
|
150
|
+
if (Array.isArray(value))
|
|
151
|
+
return value.map(() => '[REDACTED_SECRET]');
|
|
152
|
+
if (typeof value === 'object')
|
|
153
|
+
return '[REDACTED_SECRET]';
|
|
154
|
+
return '[REDACTED_SECRET]';
|
|
155
|
+
}
|
|
156
|
+
function redactSensitiveKeyValue(key, value) {
|
|
157
|
+
if (AUTHORIZATION_KEY_RE.test(key) && typeof value === 'string') {
|
|
158
|
+
return redactAuthorizationValue(value);
|
|
159
|
+
}
|
|
160
|
+
return redactValueForKey(value);
|
|
161
|
+
}
|
|
162
|
+
function isStableIdentityKey(key) {
|
|
163
|
+
return STABLE_IDENTITY_KEY_RE.test(keyParts(key));
|
|
164
|
+
}
|
|
165
|
+
// Claude Code's metadata.user_id has the shape
|
|
166
|
+
// `user_<accountHash>_account__session_<sessionUuid>`. The session uuid changes
|
|
167
|
+
// every session, so hashing the whole value yields a different id per session,
|
|
168
|
+
// which breaks downstream cross-session de-duplication / anti-sybil. We strip
|
|
169
|
+
// the `__session_<uuid>` suffix and hash only the stable account portion so the
|
|
170
|
+
// same real account maps to the same redacted id across sessions. Non-Claude
|
|
171
|
+
// values (no `__session_` marker) fall back to hashing the whole value.
|
|
172
|
+
function stableIdentityHashSource(value) {
|
|
173
|
+
let serialized;
|
|
174
|
+
try {
|
|
175
|
+
serialized = typeof value === 'string' ? value : JSON.stringify(value);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
serialized = String(value);
|
|
179
|
+
}
|
|
180
|
+
const sessionMarker = serialized.indexOf('__session_');
|
|
181
|
+
if (sessionMarker > 0)
|
|
182
|
+
return serialized.slice(0, sessionMarker);
|
|
183
|
+
return serialized;
|
|
184
|
+
}
|
|
185
|
+
function stableIdentityPlaceholder(value) {
|
|
186
|
+
const source = stableIdentityHashSource(value);
|
|
187
|
+
const hash = createHash('sha256').update(`evomap-stable-identity-v1:${source}`, 'utf8').digest('hex').slice(0, 16);
|
|
188
|
+
return `[REDACTED_ID_SHA256:${hash}]`;
|
|
189
|
+
}
|
|
190
|
+
function redactStableIdentityValue(value) {
|
|
191
|
+
if (value === undefined || value === null)
|
|
192
|
+
return value;
|
|
193
|
+
return stableIdentityPlaceholder(value);
|
|
194
|
+
}
|
|
195
|
+
// Deterministic, cross-session-stable account hash. Reuses stableIdentityHashSource
|
|
196
|
+
// so it matches the in-place redacted user_id token, and lets us emit an explicit
|
|
197
|
+
// user_id_hash sibling for downstream de-dup/anti-sybil.
|
|
198
|
+
export function stableUserIdHash(value) {
|
|
199
|
+
const source = stableIdentityHashSource(value);
|
|
200
|
+
return createHash('sha256').update(`evomap-stable-identity-v1:${source}`, 'utf8').digest('hex').slice(0, 16);
|
|
201
|
+
}
|
|
202
|
+
const USER_ID_KEY_RE = /(?:^|[_-])user[_-]?id(?:$|[_-])/i;
|
|
203
|
+
function shouldPreserveStructuredIdentifierNumbers(key) {
|
|
204
|
+
if (key === undefined)
|
|
205
|
+
return false;
|
|
206
|
+
return STRUCTURED_IDENTIFIER_KEY_RE.test(keyParts(key));
|
|
207
|
+
}
|
|
208
|
+
function redactStructured(value, depth = 0, keyContext) {
|
|
209
|
+
if (value === undefined || value === null)
|
|
210
|
+
return value;
|
|
211
|
+
if (typeof value === 'string') {
|
|
212
|
+
return applyRedactors(value, {
|
|
213
|
+
preserveStructuredIdentifierNumbers: shouldPreserveStructuredIdentifierNumbers(keyContext),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
217
|
+
return value;
|
|
218
|
+
if (depth > MAX_REDACTION_DEPTH)
|
|
219
|
+
return '[MAX_REDACTION_DEPTH]';
|
|
220
|
+
if (Array.isArray(value))
|
|
221
|
+
return value.map((item) => redactStructured(item, depth + 1, keyContext));
|
|
222
|
+
if (typeof value !== 'object')
|
|
223
|
+
return value;
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const [key, child] of Object.entries(value)) {
|
|
226
|
+
out[key] = SENSITIVE_KEY_RE.test(key)
|
|
227
|
+
? redactSensitiveKeyValue(key, child)
|
|
228
|
+
: (isStableIdentityKey(key) ? redactStableIdentityValue(child) : redactStructured(child, depth + 1, key));
|
|
229
|
+
// Emit a stable, cross-session user_id_hash alongside the redacted user_id so
|
|
230
|
+
// downstream de-dup/anti-sybil has a deterministic per-account key.
|
|
231
|
+
if (USER_ID_KEY_RE.test(key) && (typeof child === 'string' || typeof child === 'number') && out['user_id_hash'] === undefined) {
|
|
232
|
+
out['user_id_hash'] = stableUserIdHash(child);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
function incompleteCaptureEnvelope(redactedChars, redactedBytes, maxBytes) {
|
|
238
|
+
return JSON.stringify({
|
|
239
|
+
truncated: true,
|
|
240
|
+
capture_complete: false,
|
|
241
|
+
body_omitted: true,
|
|
242
|
+
reason: 'body_exceeds_trace_body_max_bytes',
|
|
243
|
+
redacted_bytes: redactedBytes,
|
|
244
|
+
max_bytes: maxBytes,
|
|
245
|
+
omitted_bytes: redactedBytes,
|
|
246
|
+
excess_bytes: Math.max(0, redactedBytes - maxBytes),
|
|
247
|
+
redacted_chars: redactedChars,
|
|
248
|
+
max_chars: maxBytes,
|
|
249
|
+
omitted_chars: redactedChars,
|
|
250
|
+
excess_chars: Math.max(0, redactedChars - maxBytes),
|
|
251
|
+
redaction: REDACTION_VERSION,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function tryParseJson(value) {
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(value);
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Serialize a native request/response payload, run it through redaction, and cap its size. Never throws — capture
|
|
264
|
+
* must never break serving — returning a small error envelope instead. Returns undefined for empty/absent input.
|
|
265
|
+
*/
|
|
266
|
+
export function captureBody(value, env = process.env) {
|
|
267
|
+
if (value === undefined || value === null)
|
|
268
|
+
return undefined;
|
|
269
|
+
let serialized;
|
|
270
|
+
try {
|
|
271
|
+
if (typeof value === 'string') {
|
|
272
|
+
const parsed = tryParseJson(value);
|
|
273
|
+
serialized = parsed === undefined ? redactText(value) : JSON.stringify(redactStructured(parsed));
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
serialized = JSON.stringify(redactStructured(value));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return { body: '"[unserializable_body]"', truncated: false, redaction: REDACTION_VERSION };
|
|
281
|
+
}
|
|
282
|
+
if (!serialized)
|
|
283
|
+
return undefined;
|
|
284
|
+
const redacted = serialized;
|
|
285
|
+
const maxBytes = bodyMaxChars(env);
|
|
286
|
+
const redactedBytes = Buffer.byteLength(redacted, 'utf8');
|
|
287
|
+
const truncated = redactedBytes > maxBytes;
|
|
288
|
+
return {
|
|
289
|
+
body: truncated ? incompleteCaptureEnvelope(redacted.length, redactedBytes, maxBytes) : redacted,
|
|
290
|
+
truncated,
|
|
291
|
+
redaction: REDACTION_VERSION,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MessagesRequest, MessagesResponse, RouterLogger } from '../router/messagesRoute.js';
|
|
2
|
+
import type { ProviderRouteHandler } from '../router/providerRoutes.js';
|
|
3
|
+
export declare const DEFAULT_LLM_PORT = 19821;
|
|
4
|
+
/** /v1/messages bodies legitimately reach tens of MiB (long contexts); the 1 MiB IPC-style cap would break
|
|
5
|
+
* real clients. Still bounded — an unauthenticated local writer must not be able to balloon memory. */
|
|
6
|
+
export declare const DEFAULT_LLM_MAX_BODY_BYTES: number;
|
|
7
|
+
export interface LlmProxyServerOptions {
|
|
8
|
+
handler: (req: MessagesRequest) => Promise<MessagesResponse>;
|
|
9
|
+
routes?: Record<string, ProviderRouteHandler>;
|
|
10
|
+
/** Self-auth bearer token. Required: an unauthenticated LLM relay on loopback is an open proxy. */
|
|
11
|
+
token: string;
|
|
12
|
+
/** Base port; EADDRINUSE walks upward. 0 → kernel-assigned (tests). Default env EVOLVER_LLM_PORT or 19821. */
|
|
13
|
+
port?: number;
|
|
14
|
+
host?: string;
|
|
15
|
+
maxBodyBytes?: number;
|
|
16
|
+
logger?: RouterLogger;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
}
|
|
19
|
+
export declare class LlmProxyServer {
|
|
20
|
+
private readonly opts;
|
|
21
|
+
private server;
|
|
22
|
+
private actualPort;
|
|
23
|
+
private readonly log;
|
|
24
|
+
private readonly env;
|
|
25
|
+
private readonly maxBodyBytes;
|
|
26
|
+
constructor(opts: LlmProxyServerOptions);
|
|
27
|
+
private authed;
|
|
28
|
+
start(): Promise<{
|
|
29
|
+
port: number;
|
|
30
|
+
url: string;
|
|
31
|
+
}>;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
private handle;
|
|
34
|
+
private relayStream;
|
|
35
|
+
}
|