1
20import type { PrismaClient } from "@prisma/client";
21import { asJson } from "../../lib/prisma.js";
22import { getLogger } from "../../logger.js";
23
24const log = getLogger("automation-log");
25
26
27export const LOG_DATA_MAX_BYTES = 32 * 1024;
28
29
30const SENSITIVE_KEYS = new Set([
31 "authorization",
32 "proxy-authorization",
33 "cookie",
34 "set-cookie",
35 "x-api-key",
36 "api-key",
37 "apikey",
38 "x-auth-token",
39 "authorization-header",
40 "secret",
41 "secret-hash",
42 "secret-encrypted",
43 "token",
44 "access-token",
45 "refresh-token",
46 "password",
47 "pass",
48 "passwd",
49 "private-key",
50 "signing-key",
51 "webhook-secret",
52 "x-hub-signature",
53 "x-webhook-signature",
54 "x-authorization"
55]);
56
57
61export function redact(value: unknown): unknown {
62 if (Array.isArray(value)) return value.map(redact);
63 if (value && typeof value === "object") {
64 const out: Record<string, unknown> = {};
65 for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
66 out[k] = SENSITIVE_KEYS.has(k.toLowerCase()) ? "***" : redact(v);
67 }
68 return out;
69 }
70 return value;
71}
72
73
79export function truncateLogData(data: unknown): unknown {
80 if (data === undefined || data === null) return data;
81 const json = JSON.stringify(data);
82 if (json === undefined) return data;
83 if (Buffer.byteLength(json, "utf8") <= LOG_DATA_MAX_BYTES) return data;
84
85 let truncated = false;
86 const shorten = (v: unknown): unknown => {
87 if (typeof v === "string") {
88 if (v.length > 500) {
89 truncated = true;
90 return `${v.slice(0, 500)}…[truncated ${v.length - 500} chars]`;
91 }
92 return v;
93 }
94 if (Array.isArray(v)) return v.map(shorten);
95 if (v && typeof v === "object") {
96 const out: Record<string, unknown> = {};
97 for (const [k, val] of Object.entries(v as Record<string, unknown>)) out[k] = shorten(val);
98 return out;
99 }
100 return v;
101 };
102 const result = shorten(data);
103 if (result && typeof result === "object" && !Array.isArray(result)) {
104 (result as Record<string, unknown>).truncated = true;
105 }
106
107
108 if (JSON.stringify(result) && Buffer.byteLength(JSON.stringify(result), "utf8") > LOG_DATA_MAX_BYTES) {
109 return {
110 truncated: true,
111 note: "automation log data exceeded the 32 KB cap and was truncated"
112 };
113 }
114 void truncated;
115 return result;
116}
117
118
122export async function runLog(
123 prisma: PrismaClient,
124 runId: string,
125 level: "info" | "warn" | "error",
126 message: string,
127 data?: Record<string, unknown>
128): Promise<void> {
129 try {
130 const safeData = data ? (truncateLogData(redact(data)) as Record<string, unknown>) : undefined;
131 await prisma.automationLog.create({
132 data: { runId, level, message, data: asJson(safeData ?? undefined) }
133 });
134 } catch (err) {
135 log.warn({ err: (err as Error).message, runId, level }, "automation log write failed (non-fatal)");
136 }
137}
138