chore: scaffold monorepo and migrate uhd_web design system (W0-A)
- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
commit
b22c440139
138 files changed
+14455
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import pino, { type Logger } from "pino";
|
||||
|
||||
export const REDACTED = "[REDACTED]";
|
||||
|
||||
// 字段名包含这些词即视为敏感(数值型如 token 计数除外)
|
||||
const SENSITIVE_KEY = /passkey|token|cookie|authorization|password|secret|api[-_]?key/i;
|
||||
// URL query 中的敏感参数
|
||||
const SENSITIVE_QUERY = /([?&](?:passkey|token|access_token|api_key|apikey|auth|sign)=)[^&#\s"'<>]*/gi;
|
||||
|
||||
/** 脱敏字符串中的 URL query 参数 */
|
||||
export function redactString(s: string): string {
|
||||
return s.replace(SENSITIVE_QUERY, `$1${REDACTED}`);
|
||||
}
|
||||
|
||||
/** 递归脱敏:敏感字段整体替换,其余字符串做 URL query 脱敏 */
|
||||
export function redact(value: unknown, depth = 0): unknown {
|
||||
if (depth > 8) return value;
|
||||
if (typeof value === "string") return redactString(value);
|
||||
if (Array.isArray(value)) return value.map((v) => redact(v, depth + 1));
|
||||
if (value && typeof value === "object") {
|
||||
const out: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (SENSITIVE_KEY.test(k) && typeof v !== "number" && v != null) out[k] = REDACTED;
|
||||
else out[k] = redact(v, depth + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export interface LoggerOptions {
|
||||
level?: string;
|
||||
pretty?: boolean;
|
||||
/** 测试用:自定义输出流 */
|
||||
destination?: pino.DestinationStream;
|
||||
}
|
||||
|
||||
export function createLogger(opts: LoggerOptions = {}): Logger {
|
||||
const options: pino.LoggerOptions = {
|
||||
level: opts.level ?? "info",
|
||||
formatters: {
|
||||
// 所有结构化字段(含 err 序列化结果)统一过一遍脱敏
|
||||
log: (obj) => redact(obj) as Record<string, unknown>,
|
||||
},
|
||||
hooks: {
|
||||
// msg 文本也脱敏
|
||||
logMethod(args, method) {
|
||||
const cleaned = args.map((a) => (typeof a === "string" ? redactString(a) : a)) as typeof args;
|
||||
return method.apply(this, cleaned);
|
||||
},
|
||||
},
|
||||
};
|
||||
if (opts.destination) return pino(options, opts.destination);
|
||||
if (opts.pretty) {
|
||||
return pino({ ...options, transport: { target: "pino-pretty", options: { translateTime: "SYS:HH:MM:ss" } } });
|
||||
}
|
||||
return pino(options);
|
||||
}
|
||||
Reference in new issue
Block a user