@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.
Files changed (51) hide show
  1. package/dist/{chunk-OGCE6O2X.js → chunk-7LDU7F5P.js} +31 -3
  2. package/dist/chunk-7LDU7F5P.js.map +1 -0
  3. package/dist/chunk-O3D7DABC.js +2314 -0
  4. package/dist/chunk-O3D7DABC.js.map +1 -0
  5. package/dist/{chunk-57MLICFR.js → chunk-YFQ7SGE4.js} +18 -6
  6. package/dist/chunk-YFQ7SGE4.js.map +1 -0
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +13129 -3496
  9. package/dist/index.js.map +1 -1
  10. package/dist/migrate.js +1 -1
  11. package/dist/provision-roles.d.ts +1836 -222
  12. package/dist/provision-roles.js +1 -1
  13. package/dist/{schema-Dsz6UHNv.d.ts → schema-Dp2MbBxx.d.ts} +7480 -3305
  14. package/dist/schema.d.ts +1 -1
  15. package/dist/schema.js +33 -5
  16. package/drizzle/0043_credit_balance_observability.sql +22 -0
  17. package/drizzle/0044_reap_dead_turn_holders.sql +104 -0
  18. package/drizzle/0045_workspace_captures.sql +83 -0
  19. package/drizzle/0045_workspace_memory_v1.sql +71 -0
  20. package/drizzle/0046_variable_sets_rename.sql +56 -0
  21. package/drizzle/0047_rigs.sql +151 -0
  22. package/drizzle/0048_rig_runtime.sql +9 -0
  23. package/drizzle/0049_enrollment_went_offline.sql +28 -0
  24. package/drizzle/0050_enrollment_op_stream.sql +1 -0
  25. package/drizzle/0051_codex_pin_source.sql +48 -0
  26. package/drizzle/0052_file_upload_cleanup.sql +91 -0
  27. package/drizzle/0053_codex_credential_leases.sql +230 -0
  28. package/drizzle/0054_session_pins.sql +85 -0
  29. package/drizzle/0055_session_list_snapshots.sql +73 -0
  30. package/drizzle/0056_workspace_model_policies.sql +48 -0
  31. package/drizzle/0057_durable_queue_control.sql +536 -0
  32. package/drizzle/0058_turn_admission_usage_enrollment.sql +158 -0
  33. package/drizzle/0059_workspace_pause_control_kind.sql +12 -0
  34. package/drizzle/0060_session_system_update_deferral.sql +10 -0
  35. package/drizzle/0061_session_workflow_wake_outbox.sql +157 -0
  36. package/drizzle/0062_session_list_snapshot_reaper.sql +48 -0
  37. package/package.json +17 -13
  38. package/src/codex-token-resolver.ts +58 -23
  39. package/src/connection-token-resolver.ts +146 -57
  40. package/src/environment-crypto.ts +5 -1
  41. package/src/event-payload-sanitizer.ts +29 -1
  42. package/src/index.ts +22460 -6492
  43. package/src/memory-domain.ts +218 -0
  44. package/src/migrate.ts +58 -3
  45. package/src/provision-roles.ts +46 -17
  46. package/src/schema.ts +2557 -1121
  47. package/src/session-control-cutover-audit.ts +1203 -0
  48. package/dist/chunk-57MLICFR.js.map +0 -1
  49. package/dist/chunk-OGCE6O2X.js.map +0 -1
  50. package/dist/chunk-ZIUCA2IO.js +0 -1268
  51. package/dist/chunk-ZIUCA2IO.js.map +0 -1
@@ -1,4 +1,8 @@
1
- import { environmentsEncryptionKeyBytes, type McpServerConnectionRef, type Settings } from "@opengeni/config";
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
- status: "auth_needed";
19
- reason: "missing_connection" | "expired" | "insufficient_scope" | "refresh_failed";
20
- providerDomain: string;
21
- connectionId?: string;
22
- scopes?: string[];
23
- resource?: string;
24
- authorizationUrl?: string;
25
- };
26
- type AuthNeededReason = Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>["reason"];
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 (input: ResolveConnectionCredentialInput): Promise<ConnectionCredentialForBroker | null> => {
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 (cred: ConnectionCredentialForBroker, ref: McpServerConnectionRef): Promise<ResolveConnectionCredentialResult> => {
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 (cred: ConnectionCredentialForBroker, ref: McpServerConnectionRef): Promise<ConnectionCredentialForBroker> => {
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 = (cred: ConnectionCredentialForBroker, ref: McpServerConnectionRef): Promise<ConnectionCredentialForBroker> => {
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.setStatus(db, input.workspaceId, "needs_reauth", error instanceof Error ? error.message : String(error), {
197
- id: cred.id,
198
- version: cred.version,
199
- }).catch(() => undefined);
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 error instanceof ConnectionRefreshHttpError
223
- && error.httpStatus >= 400 && error.httpStatus < 500
224
- && error.httpStatus !== 408 && error.httpStatus !== 429;
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(ref: McpServerConnectionRef, reason: AuthNeededReason, connectionId?: string): ResolveConnectionCredentialResult {
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(cred: ConnectionCredentialForBroker, ref: McpServerConnectionRef): ResolveConnectionCredentialResult {
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(ref, cred.expiresAt && cred.expiresAt.getTime() <= Date.now() ? "expired" : "refresh_failed", cred.id);
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
- const tokenType = stringValue((cred.credential as { token_type?: unknown }).token_type) || "Bearer";
276
- return { authorization: `${tokenType} ${accessToken}` };
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<{ credential: Record<string, unknown>; expiresAt: Date | null; grantedScopes?: string[] }> {
337
+ ): Promise<{
338
+ credential: Record<string, unknown>;
339
+ expiresAt: Date | null;
340
+ grantedScopes?: string[];
341
+ }> {
286
342
  if (cred.kind !== "oauth2") {
287
- return { credential: cred.credential, expiresAt: cred.expiresAt, grantedScopes: cred.grantedScopes };
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
- ?? stringValue((cred.metadata as { tokenEndpoint?: unknown }).tokenEndpoint)
293
- ?? stringValue((cred.metadata as { token_endpoint?: unknown }).token_endpoint);
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
- ?? stringValue((cred.metadata as { clientId?: unknown }).clientId)
309
- ?? stringValue((cred.metadata as { client_id?: unknown }).client_id);
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 = stringValue((cred.credential as { token_endpoint_auth_method?: unknown }).token_endpoint_auth_method) ?? "none";
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 = ref.resource ?? stringValue((cred.credential as { resource?: unknown }).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: stringValue(payload.token_type) ?? stringValue((cred.credential as { token_type?: unknown }).token_type) ?? "Bearer",
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 ? { client_secret: clientSecret, token_endpoint_auth_method: authMethod } : {}),
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(payload: Record<string, unknown>, fallback: Date | null): Date | null {
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 (settings.integrationsAllowPrivateNetworkTargets || ["local", "test"].includes(settings.environment)) {
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 normalized === "::1"
427
- || normalized === "::"
428
- || normalized.startsWith("fc")
429
- || normalized.startsWith("fd")
430
- || normalized.startsWith("fe8")
431
- || normalized.startsWith("fe9")
432
- || normalized.startsWith("fea")
433
- || normalized.startsWith("feb");
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 (!Number.isInteger(high) || !Number.isInteger(low) || high < 0 || high > 0xffff || low < 0 || low > 0xffff) {
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 (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
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 a === 0
476
- || a === 10
477
- || a === 127
478
- || (a === 169 && b === 254)
479
- || (a === 172 && b >= 16 && b <= 31)
480
- || (a === 192 && b === 168);
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([cipher.update(plaintext, "utf8"), cipher.final(), cipher.getAuthTag()]);
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]) => [sanitizeEventString(key), sanitizeSensitiveEventField(key, value)] as const,
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);