diff --git a/packages/runtimeuse/src/utils.test.ts b/packages/runtimeuse/src/utils.test.ts index 31f0d7c..298ed32 100644 --- a/packages/runtimeuse/src/utils.test.ts +++ b/packages/runtimeuse/src/utils.test.ts @@ -116,5 +116,15 @@ describe("redactSecrets", () => { redactSecrets("my-secret-key", ["secret", "my-secret-key"]), ).toBe("my-[REDACTED]-key"); }); + + it("handles circular references without stack overflow", () => { + const obj: Record = { key: "SECRET" }; + obj.self = obj; + const result = redactSecrets(obj, ["SECRET"]) as Record; + expect(result.key).toBe("[REDACTED]"); + // Circular ref points to the redacted result, not the original + expect(result.self).toBe(result); + expect((result.self as Record).key).toBe("[REDACTED]"); + }); }); }); diff --git a/packages/runtimeuse/src/utils.ts b/packages/runtimeuse/src/utils.ts index 8958f29..eb18e4f 100644 --- a/packages/runtimeuse/src/utils.ts +++ b/packages/runtimeuse/src/utils.ts @@ -9,27 +9,41 @@ export async function sleep(ms: number) { export function redactSecrets(value: T, secrets: string[]): T { if (secrets.length === 0) return value; - if (typeof value === "string") { - let redacted: string = value; - for (const secret of secrets) { - if (secret && redacted.includes(secret)) { - redacted = redacted.replaceAll(secret, "[REDACTED]"); + const seen = new WeakMap(); + + function redact(val: U): U { + if (typeof val === "string") { + let redacted: string = val; + for (const secret of secrets) { + if (secret && redacted.includes(secret)) { + redacted = redacted.replaceAll(secret, "[REDACTED]"); + } } + return redacted as U; } - return redacted as T; - } - if (Array.isArray(value)) { - return value.map((item) => redactSecrets(item, secrets)) as T; - } + if (val !== null && typeof val === "object") { + if (seen.has(val as object)) return seen.get(val as object) as U; - if (value !== null && typeof value === "object") { - const result: Record = {}; - for (const [key, val] of Object.entries(value)) { - result[key] = redactSecrets(val, secrets); + if (Array.isArray(val)) { + const result: unknown[] = []; + seen.set(val as object, result); + for (const item of val) { + result.push(redact(item)); + } + return result as U; + } + + const result: Record = {}; + seen.set(val as object, result); + for (const [key, v] of Object.entries(val)) { + result[key] = redact(v); + } + return result as U; } - return result as T; + + return val; } - return value; + return redact(value); }