/** * Serialization * * Safe JSON serialization handling circular references, functions, and errors. * Also provides action sanitization to remove sensitive data before storage/exposure. */ import type { Action } from "@gizmo-ai/runtime"; /** * Check if a value can be serialized to JSON * * Returns false for values containing functions, which can't be sent over SSE. * Used to auto-filter slices that can't be broadcast (e.g., agent.tools with execute functions). */ export function isSerializable(value: unknown): boolean { if (value === null || value === undefined) return true; if (typeof value === "function") return false; if (Array.isArray(value)) { return value.every(isSerializable); } if (typeof value === "object") { return Object.values(value).every(isSerializable); } return true; } /** * Safely serialize a value to JSON * * Handles: * - Circular references (replaced with "[Circular]") * - Functions (replaced with "[Function]") * - Error objects (serialized as { name, message, stack }) */ export function safeSerialize(value: unknown): string { const seen = new WeakSet(); return JSON.stringify(value, (_key, val) => { // Handle circular references if (typeof val === "object" && val !== null) { if (seen.has(val)) { return "[Circular]"; } seen.add(val); } // Handle functions (tools have execute functions) if (typeof val === "function") { return "[Function]"; } // Handle errors if (val instanceof Error) { return { name: val.name, message: val.message, stack: val.stack, }; } return val; }); } /** * Sensitive key patterns to redact */ const SENSITIVE_KEYS = new Set([ 'password', 'apikey', 'api_key', 'token', 'secret', 'authorization', 'auth', 'credentials', 'key', 'bearer', 'access_token', 'refresh_token', 'private_key', 'apitoken' ]); /** * Remove sensitive data from an object recursively */ function sanitizeObject(obj: unknown): unknown { if (Array.isArray(obj)) { return obj.map(sanitizeObject); } // Handle Error objects - serialize to { name, message, stack } if (obj instanceof Error) { return { name: obj.name, message: obj.message, stack: obj.stack, }; } if (typeof obj === 'object' && obj !== null) { const cleaned: Record = {}; for (const [key, value] of Object.entries(obj)) { // Skip sensitive keys if (SENSITIVE_KEYS.has(key.toLowerCase())) { cleaned[key] = '[REDACTED]'; } else { cleaned[key] = sanitizeObject(value); } } return cleaned; } return obj; } /** * Remove sensitive data from actions before storing/exposing * * Recursively redacts known sensitive fields like passwords, API keys, tokens, etc. */ export function sanitizeAction(action: Action): Action { const sanitized: Action = { ...action }; // Remove potential credentials from payload (if present) if ('payload' in sanitized && sanitized.payload && typeof sanitized.payload === 'object') { sanitized.payload = sanitizeObject(sanitized.payload); } return sanitized; }