@opengeni/db 0.6.0 → 0.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/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
- package/dist/chunk-7LDU7F5P.js.map +1 -0
- package/dist/chunk-O3D7DABC.js +2314 -0
- package/dist/chunk-O3D7DABC.js.map +1 -0
- package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
- package/dist/chunk-YFQ7SGE4.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13129 -3496
- package/dist/index.js.map +1 -1
- package/dist/migrate.js +1 -1
- package/dist/provision-roles.d.ts +1836 -222
- package/dist/provision-roles.js +1 -1
- package/dist/{schema-Dsz6UHNv.d.ts → schema-Dp2MbBxx.d.ts} +7480 -3305
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +33 -5
- package/drizzle/0043_credit_balance_observability.sql +22 -0
- package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
- package/drizzle/0045_workspace_captures.sql +83 -0
- package/drizzle/0045_workspace_memory_v1.sql +71 -0
- package/drizzle/0046_variable_sets_rename.sql +56 -0
- package/drizzle/0047_rigs.sql +151 -0
- package/drizzle/0048_rig_runtime.sql +9 -0
- package/drizzle/0049_enrollment_went_offline.sql +28 -0
- package/drizzle/0050_enrollment_op_stream.sql +1 -0
- package/drizzle/0051_codex_pin_source.sql +48 -0
- package/drizzle/0052_file_upload_cleanup.sql +91 -0
- package/drizzle/0053_codex_credential_leases.sql +230 -0
- package/drizzle/0054_session_pins.sql +85 -0
- package/drizzle/0055_session_list_snapshots.sql +73 -0
- package/drizzle/0056_workspace_model_policies.sql +48 -0
- package/drizzle/0057_durable_queue_control.sql +536 -0
- package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
- package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
- package/drizzle/0060_session_system_update_deferral.sql +10 -0
- package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
- package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
- package/package.json +17 -13
- package/src/codex-token-resolver.ts +58 -23
- package/src/connection-token-resolver.ts +146 -57
- package/src/environment-crypto.ts +5 -1
- package/src/event-payload-sanitizer.ts +29 -1
- package/src/index.ts +22460 -6492
- package/src/memory-domain.ts +218 -0
- package/src/migrate.ts +58 -3
- package/src/provision-roles.ts +46 -17
- package/src/schema.ts +2557 -1121
- package/src/session-control-cutover-audit.ts +1203 -0
- package/dist/chunk-57MLICFR.js.map +0 -1
- package/dist/chunk-OGCE6O2X.js.map +0 -1
- package/dist/chunk-ZIUCA2IO.js +0 -1268
- package/dist/chunk-ZIUCA2IO.js.map +0 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
environmentsEncryptionKeyBytes,
|
|
3
|
+
type McpServerConnectionRef,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
2
6
|
import { Buffer } from "node:buffer";
|
|
3
7
|
import { lookup } from "node:dns/promises";
|
|
4
8
|
import { isIP } from "node:net";
|
|
@@ -15,15 +19,18 @@ import {
|
|
|
15
19
|
export type ResolveConnectionCredentialResult =
|
|
16
20
|
| { status: "ok"; headers: Record<string, string>; connectionId: string; expiresAt?: Date | null }
|
|
17
21
|
| {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type AuthNeededReason = Extract<
|
|
22
|
+
status: "auth_needed";
|
|
23
|
+
reason: "missing_connection" | "expired" | "insufficient_scope" | "refresh_failed";
|
|
24
|
+
providerDomain: string;
|
|
25
|
+
connectionId?: string;
|
|
26
|
+
scopes?: string[];
|
|
27
|
+
resource?: string;
|
|
28
|
+
authorizationUrl?: string;
|
|
29
|
+
};
|
|
30
|
+
type AuthNeededReason = Extract<
|
|
31
|
+
ResolveConnectionCredentialResult,
|
|
32
|
+
{ status: "auth_needed" }
|
|
33
|
+
>["reason"];
|
|
27
34
|
|
|
28
35
|
export type ResolveConnectionCredentialInput = {
|
|
29
36
|
workspaceId: string;
|
|
@@ -64,7 +71,9 @@ export function buildConnectionTokenResolver(
|
|
|
64
71
|
settings: Settings,
|
|
65
72
|
deps: ConnectionBrokerDeps = defaultDeps,
|
|
66
73
|
): (input: ResolveConnectionCredentialInput) => Promise<ResolveConnectionCredentialResult> {
|
|
67
|
-
const load = async (
|
|
74
|
+
const load = async (
|
|
75
|
+
input: ResolveConnectionCredentialInput,
|
|
76
|
+
): Promise<ConnectionCredentialForBroker | null> => {
|
|
68
77
|
const request: Parameters<typeof loadConnectionCredentialForBroker>[2] = {
|
|
69
78
|
workspaceId: input.workspaceId,
|
|
70
79
|
providerDomain: input.connectionRef.providerDomain,
|
|
@@ -83,7 +92,10 @@ export function buildConnectionTokenResolver(
|
|
|
83
92
|
return deps.loadCredential(db, settings, request);
|
|
84
93
|
};
|
|
85
94
|
|
|
86
|
-
const snapshot = async (
|
|
95
|
+
const snapshot = async (
|
|
96
|
+
cred: ConnectionCredentialForBroker,
|
|
97
|
+
ref: McpServerConnectionRef,
|
|
98
|
+
): Promise<ResolveConnectionCredentialResult> => {
|
|
87
99
|
if (cred.status !== "active") {
|
|
88
100
|
return authNeededForStatus(cred, ref);
|
|
89
101
|
}
|
|
@@ -118,7 +130,10 @@ export function buildConnectionTokenResolver(
|
|
|
118
130
|
};
|
|
119
131
|
};
|
|
120
132
|
|
|
121
|
-
const performRefresh = async (
|
|
133
|
+
const performRefresh = async (
|
|
134
|
+
cred: ConnectionCredentialForBroker,
|
|
135
|
+
ref: McpServerConnectionRef,
|
|
136
|
+
): Promise<ConnectionCredentialForBroker> => {
|
|
122
137
|
const key = deps.keyBytes(settings);
|
|
123
138
|
if (!key) {
|
|
124
139
|
throw new Error("OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured");
|
|
@@ -157,7 +172,10 @@ export function buildConnectionTokenResolver(
|
|
|
157
172
|
throw new Error("connection credential changed during token refresh");
|
|
158
173
|
};
|
|
159
174
|
|
|
160
|
-
const refreshSingleFlight = (
|
|
175
|
+
const refreshSingleFlight = (
|
|
176
|
+
cred: ConnectionCredentialForBroker,
|
|
177
|
+
ref: McpServerConnectionRef,
|
|
178
|
+
): Promise<ConnectionCredentialForBroker> => {
|
|
161
179
|
const key = `${cred.id}:${cred.version}`;
|
|
162
180
|
const existing = inflight.get(key);
|
|
163
181
|
if (existing) {
|
|
@@ -193,10 +211,18 @@ export function buildConnectionTokenResolver(
|
|
|
193
211
|
// Only a rejected grant may poison the connection; transient failures
|
|
194
212
|
// (network errors, AS 5xx) leave it active so the next resolve retries.
|
|
195
213
|
if (isPermanentRefreshError(error)) {
|
|
196
|
-
await deps
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
await deps
|
|
215
|
+
.setStatus(
|
|
216
|
+
db,
|
|
217
|
+
input.workspaceId,
|
|
218
|
+
"needs_reauth",
|
|
219
|
+
error instanceof Error ? error.message : String(error),
|
|
220
|
+
{
|
|
221
|
+
id: cred.id,
|
|
222
|
+
version: cred.version,
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
.catch(() => undefined);
|
|
200
226
|
}
|
|
201
227
|
return authNeeded(ref, "refresh_failed", cred.id);
|
|
202
228
|
}
|
|
@@ -219,9 +245,13 @@ export class ConnectionRefreshHttpError extends Error {
|
|
|
219
245
|
// forward. 429 (throttling) and 408 are transient despite being 4xx; network
|
|
220
246
|
// failures and AS 5xx are likewise retryable.
|
|
221
247
|
function isPermanentRefreshError(error: unknown): boolean {
|
|
222
|
-
return
|
|
223
|
-
|
|
224
|
-
|
|
248
|
+
return (
|
|
249
|
+
error instanceof ConnectionRefreshHttpError &&
|
|
250
|
+
error.httpStatus >= 400 &&
|
|
251
|
+
error.httpStatus < 500 &&
|
|
252
|
+
error.httpStatus !== 408 &&
|
|
253
|
+
error.httpStatus !== 429
|
|
254
|
+
);
|
|
225
255
|
}
|
|
226
256
|
|
|
227
257
|
function shouldRefresh(cred: ConnectionCredentialForBroker, force: boolean, now: Date): boolean {
|
|
@@ -237,7 +267,11 @@ function shouldRefresh(cred: ConnectionCredentialForBroker, force: boolean, now:
|
|
|
237
267
|
return cred.expiresAt.getTime() <= now.getTime() + REFRESH_WINDOW_MS;
|
|
238
268
|
}
|
|
239
269
|
|
|
240
|
-
function authNeeded(
|
|
270
|
+
function authNeeded(
|
|
271
|
+
ref: McpServerConnectionRef,
|
|
272
|
+
reason: AuthNeededReason,
|
|
273
|
+
connectionId?: string,
|
|
274
|
+
): ResolveConnectionCredentialResult {
|
|
241
275
|
return {
|
|
242
276
|
status: "auth_needed",
|
|
243
277
|
reason,
|
|
@@ -248,11 +282,18 @@ function authNeeded(ref: McpServerConnectionRef, reason: AuthNeededReason, conne
|
|
|
248
282
|
};
|
|
249
283
|
}
|
|
250
284
|
|
|
251
|
-
function authNeededForStatus(
|
|
285
|
+
function authNeededForStatus(
|
|
286
|
+
cred: ConnectionCredentialForBroker,
|
|
287
|
+
ref: McpServerConnectionRef,
|
|
288
|
+
): ResolveConnectionCredentialResult {
|
|
252
289
|
if (cred.status === "revoked") {
|
|
253
290
|
return authNeeded(ref, "missing_connection", cred.id);
|
|
254
291
|
}
|
|
255
|
-
return authNeeded(
|
|
292
|
+
return authNeeded(
|
|
293
|
+
ref,
|
|
294
|
+
cred.expiresAt && cred.expiresAt.getTime() <= Date.now() ? "expired" : "refresh_failed",
|
|
295
|
+
cred.id,
|
|
296
|
+
);
|
|
256
297
|
}
|
|
257
298
|
|
|
258
299
|
function missingRequestedScopes(requested: string[] | undefined, granted: string[]): string[] {
|
|
@@ -263,6 +304,16 @@ function missingRequestedScopes(requested: string[] | undefined, granted: string
|
|
|
263
304
|
return requested.filter((scope) => !grantedSet.has(scope));
|
|
264
305
|
}
|
|
265
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Normalizes an OAuth token_type into the Authorization scheme to send. RFC 6750
|
|
309
|
+
* says the scheme is case-insensitive, but some MCP servers (e.g. Linear) reject
|
|
310
|
+
* a lowercase `bearer` — so a `bearer`/`BEARER` (or absent) token_type is sent as
|
|
311
|
+
* the canonical `Bearer`. A non-bearer scheme is passed through unchanged.
|
|
312
|
+
*/
|
|
313
|
+
export function normalizeBearerScheme(tokenType: string | null | undefined): string {
|
|
314
|
+
return !tokenType || /^bearer$/i.test(tokenType) ? "Bearer" : tokenType;
|
|
315
|
+
}
|
|
316
|
+
|
|
266
317
|
function headersForCredential(cred: ConnectionCredentialForBroker): Record<string, string> | null {
|
|
267
318
|
if (cred.kind === "api_key") {
|
|
268
319
|
return stringRecord((cred.credential as { headers?: unknown }).headers);
|
|
@@ -272,8 +323,9 @@ function headersForCredential(cred: ConnectionCredentialForBroker): Record<strin
|
|
|
272
323
|
if (!accessToken) {
|
|
273
324
|
return null;
|
|
274
325
|
}
|
|
275
|
-
|
|
276
|
-
|
|
326
|
+
return {
|
|
327
|
+
authorization: `${normalizeBearerScheme(stringValue((cred.credential as { token_type?: unknown }).token_type))} ${accessToken}`,
|
|
328
|
+
};
|
|
277
329
|
}
|
|
278
330
|
return stringRecord((cred.credential as { headers?: unknown }).headers);
|
|
279
331
|
}
|
|
@@ -282,15 +334,23 @@ export async function refreshOAuthConnectionCredential(
|
|
|
282
334
|
cred: ConnectionCredentialForBroker,
|
|
283
335
|
ref: McpServerConnectionRef,
|
|
284
336
|
settings?: Settings,
|
|
285
|
-
): Promise<{
|
|
337
|
+
): Promise<{
|
|
338
|
+
credential: Record<string, unknown>;
|
|
339
|
+
expiresAt: Date | null;
|
|
340
|
+
grantedScopes?: string[];
|
|
341
|
+
}> {
|
|
286
342
|
if (cred.kind !== "oauth2") {
|
|
287
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
credential: cred.credential,
|
|
345
|
+
expiresAt: cred.expiresAt,
|
|
346
|
+
grantedScopes: cred.grantedScopes,
|
|
347
|
+
};
|
|
288
348
|
}
|
|
289
349
|
const refreshToken = stringValue((cred.credential as { refresh_token?: unknown }).refresh_token);
|
|
290
350
|
const tokenEndpoint =
|
|
291
|
-
stringValue((cred.credential as { token_endpoint?: unknown }).token_endpoint)
|
|
292
|
-
|
|
293
|
-
|
|
351
|
+
stringValue((cred.credential as { token_endpoint?: unknown }).token_endpoint) ??
|
|
352
|
+
stringValue((cred.metadata as { tokenEndpoint?: unknown }).tokenEndpoint) ??
|
|
353
|
+
stringValue((cred.metadata as { token_endpoint?: unknown }).token_endpoint);
|
|
294
354
|
if (!refreshToken || !tokenEndpoint) {
|
|
295
355
|
throw new Error("connection has no refresh token endpoint");
|
|
296
356
|
}
|
|
@@ -304,11 +364,14 @@ export async function refreshOAuthConnectionCredential(
|
|
|
304
364
|
// in the token request body (RFC 6749 §3.2.1); grant flows persist the
|
|
305
365
|
// client_id they authorized with into the bundle/metadata.
|
|
306
366
|
const clientId =
|
|
307
|
-
stringValue((cred.credential as { client_id?: unknown }).client_id)
|
|
308
|
-
|
|
309
|
-
|
|
367
|
+
stringValue((cred.credential as { client_id?: unknown }).client_id) ??
|
|
368
|
+
stringValue((cred.metadata as { clientId?: unknown }).clientId) ??
|
|
369
|
+
stringValue((cred.metadata as { client_id?: unknown }).client_id);
|
|
310
370
|
const clientSecret = stringValue((cred.credential as { client_secret?: unknown }).client_secret);
|
|
311
|
-
const authMethod =
|
|
371
|
+
const authMethod =
|
|
372
|
+
stringValue(
|
|
373
|
+
(cred.credential as { token_endpoint_auth_method?: unknown }).token_endpoint_auth_method,
|
|
374
|
+
) ?? "none";
|
|
312
375
|
if (clientId) {
|
|
313
376
|
body.set("client_id", clientId);
|
|
314
377
|
}
|
|
@@ -318,7 +381,8 @@ export async function refreshOAuthConnectionCredential(
|
|
|
318
381
|
} else if (clientId && clientSecret && authMethod === "client_secret_basic") {
|
|
319
382
|
headers.authorization = `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`;
|
|
320
383
|
}
|
|
321
|
-
const resource =
|
|
384
|
+
const resource =
|
|
385
|
+
ref.resource ?? stringValue((cred.credential as { resource?: unknown }).resource);
|
|
322
386
|
if (resource) {
|
|
323
387
|
body.set("resource", resource);
|
|
324
388
|
}
|
|
@@ -337,7 +401,7 @@ export async function refreshOAuthConnectionCredential(
|
|
|
337
401
|
if (!response.ok) {
|
|
338
402
|
throw new ConnectionRefreshHttpError(response.status);
|
|
339
403
|
}
|
|
340
|
-
const payload = await response.json() as Record<string, unknown>;
|
|
404
|
+
const payload = (await response.json()) as Record<string, unknown>;
|
|
341
405
|
const accessToken = stringValue(payload.access_token);
|
|
342
406
|
if (!accessToken) {
|
|
343
407
|
throw new Error("connection refresh response did not include access_token");
|
|
@@ -348,11 +412,16 @@ export async function refreshOAuthConnectionCredential(
|
|
|
348
412
|
...cred.credential,
|
|
349
413
|
access_token: accessToken,
|
|
350
414
|
refresh_token: stringValue(payload.refresh_token) ?? refreshToken,
|
|
351
|
-
token_type:
|
|
415
|
+
token_type:
|
|
416
|
+
stringValue(payload.token_type) ??
|
|
417
|
+
stringValue((cred.credential as { token_type?: unknown }).token_type) ??
|
|
418
|
+
"Bearer",
|
|
352
419
|
...(expiresAt ? { expires_at: expiresAt.toISOString() } : {}),
|
|
353
420
|
...(resource ? { resource } : {}),
|
|
354
421
|
...(scopeText ? { scope: scopeText } : {}),
|
|
355
|
-
...(clientSecret
|
|
422
|
+
...(clientSecret
|
|
423
|
+
? { client_secret: clientSecret, token_endpoint_auth_method: authMethod }
|
|
424
|
+
: {}),
|
|
356
425
|
};
|
|
357
426
|
return {
|
|
358
427
|
credential: nextCredential,
|
|
@@ -361,7 +430,10 @@ export async function refreshOAuthConnectionCredential(
|
|
|
361
430
|
};
|
|
362
431
|
}
|
|
363
432
|
|
|
364
|
-
function expiresAtFromTokenResponse(
|
|
433
|
+
function expiresAtFromTokenResponse(
|
|
434
|
+
payload: Record<string, unknown>,
|
|
435
|
+
fallback: Date | null,
|
|
436
|
+
): Date | null {
|
|
365
437
|
const expiresAt = stringValue(payload.expires_at);
|
|
366
438
|
if (expiresAt) {
|
|
367
439
|
const parsed = new Date(expiresAt);
|
|
@@ -393,7 +465,10 @@ function stringValue(value: unknown): string | undefined {
|
|
|
393
465
|
}
|
|
394
466
|
|
|
395
467
|
async function assertOAuthEndpointAllowed(rawUrl: string, settings: Settings): Promise<void> {
|
|
396
|
-
if (
|
|
468
|
+
if (
|
|
469
|
+
settings.integrationsAllowPrivateNetworkTargets ||
|
|
470
|
+
["local", "test"].includes(settings.environment)
|
|
471
|
+
) {
|
|
397
472
|
return;
|
|
398
473
|
}
|
|
399
474
|
const url = new URL(rawUrl);
|
|
@@ -423,14 +498,16 @@ export function isPrivateAddress(address: string): boolean {
|
|
|
423
498
|
if (isIP(normalized) !== 6) {
|
|
424
499
|
return true;
|
|
425
500
|
}
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
501
|
+
return (
|
|
502
|
+
normalized === "::1" ||
|
|
503
|
+
normalized === "::" ||
|
|
504
|
+
normalized.startsWith("fc") ||
|
|
505
|
+
normalized.startsWith("fd") ||
|
|
506
|
+
normalized.startsWith("fe8") ||
|
|
507
|
+
normalized.startsWith("fe9") ||
|
|
508
|
+
normalized.startsWith("fea") ||
|
|
509
|
+
normalized.startsWith("feb")
|
|
510
|
+
);
|
|
434
511
|
}
|
|
435
512
|
return isPrivateIpv4Address(normalized);
|
|
436
513
|
}
|
|
@@ -457,7 +534,14 @@ function ipv4FromMappedIpv6(address: string): string | null {
|
|
|
457
534
|
}
|
|
458
535
|
const high = Number.parseInt(parts[0]!, 16);
|
|
459
536
|
const low = Number.parseInt(parts[1]!, 16);
|
|
460
|
-
if (
|
|
537
|
+
if (
|
|
538
|
+
!Number.isInteger(high) ||
|
|
539
|
+
!Number.isInteger(low) ||
|
|
540
|
+
high < 0 ||
|
|
541
|
+
high > 0xffff ||
|
|
542
|
+
low < 0 ||
|
|
543
|
+
low > 0xffff
|
|
544
|
+
) {
|
|
461
545
|
return null;
|
|
462
546
|
}
|
|
463
547
|
return `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;
|
|
@@ -468,14 +552,19 @@ function isPrivateIpv4Address(address: string): boolean {
|
|
|
468
552
|
return true;
|
|
469
553
|
}
|
|
470
554
|
const parts = address.split(".").map((part) => Number(part));
|
|
471
|
-
if (
|
|
555
|
+
if (
|
|
556
|
+
parts.length !== 4 ||
|
|
557
|
+
parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
|
|
558
|
+
) {
|
|
472
559
|
return true;
|
|
473
560
|
}
|
|
474
561
|
const [a, b] = parts as [number, number, number, number];
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
562
|
+
return (
|
|
563
|
+
a === 0 ||
|
|
564
|
+
a === 10 ||
|
|
565
|
+
a === 127 ||
|
|
566
|
+
(a === 169 && b === 254) ||
|
|
567
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
568
|
+
(a === 192 && b === 168)
|
|
569
|
+
);
|
|
481
570
|
}
|
|
@@ -13,7 +13,11 @@ export function encryptEnvironmentValue(key: Uint8Array, plaintext: string): str
|
|
|
13
13
|
assertKey(key);
|
|
14
14
|
const iv = randomBytes(IV_BYTES);
|
|
15
15
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
16
|
-
const ciphertext = Buffer.concat([
|
|
16
|
+
const ciphertext = Buffer.concat([
|
|
17
|
+
cipher.update(plaintext, "utf8"),
|
|
18
|
+
cipher.final(),
|
|
19
|
+
cipher.getAuthTag(),
|
|
20
|
+
]);
|
|
17
21
|
return `${VERSION_PREFIX}:${iv.toString("base64")}:${ciphertext.toString("base64")}`;
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -97,13 +97,41 @@ export function sanitizeEventPayload<T>(payload: T): T {
|
|
|
97
97
|
}
|
|
98
98
|
if (payload && typeof payload === "object") {
|
|
99
99
|
const entries = Object.entries(payload as Record<string, unknown>).map(
|
|
100
|
-
([key, value]) =>
|
|
100
|
+
([key, value]) =>
|
|
101
|
+
[sanitizeEventString(key), sanitizeSensitiveEventField(key, value)] as const,
|
|
101
102
|
);
|
|
102
103
|
return Object.fromEntries(entries) as unknown as T;
|
|
103
104
|
}
|
|
104
105
|
return payload;
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Make model-facing conversation data safe for Postgres without redacting it.
|
|
110
|
+
*
|
|
111
|
+
* Session events are an audit/UI projection and deliberately redact fields such
|
|
112
|
+
* as `token` and `authorization`. Conversation history is the replay source for
|
|
113
|
+
* the model, so applying event redaction there silently changes tool arguments
|
|
114
|
+
* and can make recovery diverge from the call that actually ran. This walker
|
|
115
|
+
* performs only the database-safety repair (NUL removal and UTF-16 repair).
|
|
116
|
+
*/
|
|
117
|
+
export function sanitizeModelPayload<T>(payload: T): T {
|
|
118
|
+
if (typeof payload === "string") {
|
|
119
|
+
return sanitizeEventString(payload) as unknown as T;
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(payload)) {
|
|
122
|
+
return payload.map((item) => sanitizeModelPayload(item)) as unknown as T;
|
|
123
|
+
}
|
|
124
|
+
if (payload && typeof payload === "object") {
|
|
125
|
+
return Object.fromEntries(
|
|
126
|
+
Object.entries(payload as Record<string, unknown>).map(([key, value]) => [
|
|
127
|
+
sanitizeEventString(key),
|
|
128
|
+
sanitizeModelPayload(value),
|
|
129
|
+
]),
|
|
130
|
+
) as unknown as T;
|
|
131
|
+
}
|
|
132
|
+
return payload;
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
function sanitizeSensitiveEventField(key: string, value: unknown): unknown {
|
|
108
136
|
if (key === "mcpServers") {
|
|
109
137
|
return sanitizeSessionMcpServerList(value);
|