52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { appendFileSync, closeSync, mkdirSync, openSync, readdirSync, unlinkSync } from 'fs'
|
|
import { join } from 'path'
|
|
|
|
export function redactLog(value: string): string {
|
|
return value
|
|
.replace(/(https?:\/\/)[^\s/@]+@/gi, '$1[REDACTED]@')
|
|
.replace(/\bBearer\s+[^\s"',;]+/gi, 'Bearer [REDACTED]')
|
|
.replace(/((?:[?&]|--)(?:[^\s=&]*token|password|secret|code|key)=)[^\s&#"']+/gi, '$1[REDACTED]')
|
|
.replace(/(["']?(?:[\w-]*token|password|secret|authorization|api[_-]?key)["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}]+)/gi, '$1[REDACTED]')
|
|
}
|
|
|
|
function localDate(now: Date): string {
|
|
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
|
|
}
|
|
|
|
export function createDailyLog(directories: string[], now: () => Date = () => new Date()) {
|
|
let activePath = ''
|
|
let activeDay = ''
|
|
|
|
function selectFile(): string {
|
|
const day = localDate(now())
|
|
if (activePath && activeDay === day) return activePath
|
|
for (const directory of [...new Set(directories)]) {
|
|
try {
|
|
mkdirSync(directory, { recursive: true })
|
|
const path = join(directory, `lms-${day}.log`)
|
|
closeSync(openSync(path, 'a', 0o600))
|
|
activePath = path
|
|
activeDay = day
|
|
for (const name of readdirSync(directory)) {
|
|
if (/^lms-\d{4}-\d{2}-\d{2}\.log$/.test(name) && name < `lms-${day}.log`) {
|
|
try { unlinkSync(join(directory, name)) } catch { /* Retention must not block startup. */ }
|
|
}
|
|
}
|
|
return path
|
|
} catch { /* Try the user-writable fallback directory. */ }
|
|
}
|
|
return ''
|
|
}
|
|
|
|
return {
|
|
path: selectFile,
|
|
write(kind: string, message: string): void {
|
|
try {
|
|
const path = selectFile()
|
|
if (!path) return
|
|
appendFileSync(path, `[${now().toISOString()}] [${redactLog(kind)}] ${redactLog(message)}\n`, { mode: 0o600 })
|
|
} catch { /* Diagnostics must not interrupt the app. */ }
|
|
}
|
|
}
|
|
}
|