import os from "node:os"; export const CURRENT_USER_REDACTION_TOKEN = "[]"; interface CurrentUserRedactionOptions { replacement?: string; userNames?: string[]; homeDirs?: string[]; } function isPlainObject(value: unknown): value is Record { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function uniqueNonEmpty(values: Array) { return Array.from(new Set(values.map((value) => value?.trim() ?? "").filter(Boolean))); } function splitPathSegments(value: string) { return value.replace(/[\\/]+$/, "").split(/[\\/]+/).filter(Boolean); } function replaceLastPathSegment(pathValue: string, replacement: string) { const normalized = pathValue.replace(/[\\/]+$/, ""); const lastSeparator = Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")); if (lastSeparator < 0) return replacement; return `${normalized.slice(0, lastSeparator + 1)}${replacement}`; } function defaultUserNames() { const candidates = [ process.env.USER, process.env.LOGNAME, process.env.USERNAME, ]; try { candidates.push(os.userInfo().username); } catch { // Some environments do not expose userInfo; env vars are enough fallback. } return uniqueNonEmpty(candidates); } function defaultHomeDirs(userNames: string[]) { const candidates: Array = [ process.env.HOME, process.env.USERPROFILE, ]; try { candidates.push(os.homedir()); } catch { // Ignore and fall back to env hints below. } for (const userName of userNames) { candidates.push(`/Users/${userName}`); candidates.push(`/home/${userName}`); candidates.push(`C:\\Users\\${userName}`); } return uniqueNonEmpty(candidates); } function resolveCurrentUserCandidates(opts?: CurrentUserRedactionOptions) { const userNames = uniqueNonEmpty(opts?.userNames ?? defaultUserNames()); const homeDirs = uniqueNonEmpty(opts?.homeDirs ?? defaultHomeDirs(userNames)); const replacement = opts?.replacement?.trim() || CURRENT_USER_REDACTION_TOKEN; return { userNames, homeDirs, replacement }; } export function redactCurrentUserText(input: string, opts?: CurrentUserRedactionOptions) { if (!input) return input; const { userNames, homeDirs, replacement } = resolveCurrentUserCandidates(opts); let result = input; for (const homeDir of [...homeDirs].sort((a, b) => b.length - a.length)) { const lastSegment = splitPathSegments(homeDir).pop() ?? ""; const replacementDir = userNames.includes(lastSegment) ? replaceLastPathSegment(homeDir, replacement) : replacement; result = result.split(homeDir).join(replacementDir); } for (const userName of [...userNames].sort((a, b) => b.length - a.length)) { const pattern = new RegExp(`(?(value: T, opts?: CurrentUserRedactionOptions): T { if (typeof value === "string") { return redactCurrentUserText(value, opts) as T; } if (Array.isArray(value)) { return value.map((entry) => redactCurrentUserValue(entry, opts)) as T; } if (!isPlainObject(value)) { return value; } const redacted: Record = {}; for (const [key, entry] of Object.entries(value)) { redacted[key] = redactCurrentUserValue(entry, opts); } return redacted as T; }