diff --git a/bbpay.ts b/bbpay.ts index 6042b58..7296343 100644 --- a/bbpay.ts +++ b/bbpay.ts @@ -20,12 +20,20 @@ const cache = new ProofCache(); const app = express(); app.use(cors()); app.use(express.json()); +/** Only what the caller actually sent; empty sections stay out of the line. */ +const received = (req: Request): Record => + Object.fromEntries( + Object.entries({ query: req.query, body: req.body }).filter( + ([, value]) => typeof value === 'object' && value !== null && Object.keys(value).length > 0, + ), + ); + if (config.debug) { - // Every incoming request and its outcome — success paths log nothing otherwise. + // Every incoming request, its parameters and its outcome — success paths log nothing otherwise. app.use((req, res, next) => { const started = Date.now(); res.on('finish', () => - log.info(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - started} ms)`), + log.info(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - started} ms)`, received(req)), ); next(); }); diff --git a/package.json b/package.json index c4d2c29..e7c17b0 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,7 @@ "dotenv-flow": "^4.1.0", "ethers": "^5.8.0", "express": "^5.2.1", - "simple-oauth2": "^5.1.0", - "tslog": "4" + "simple-oauth2": "^5.1.0" }, "devDependencies": { "@types/cors": "^2.8.17", diff --git a/src/config.ts b/src/config.ts index 71a14d4..3b1d692 100644 --- a/src/config.ts +++ b/src/config.ts @@ -54,8 +54,8 @@ export const loadPrimusConfig = (): Config['primus'] => ({ wallet: new Wallet(required('PRIVATE_KEY')), }); -export const loadConfig = (): Config => { - const primus = loadPrimusConfig(); +/** The BB side alone: enough for the admin console, which must boot without the prover's wallet. */ +export const loadBBConfig = (): Omit => { const apiUrl = required('ITP_API_URL').replace(/\/+$/, ''); const clientCrtPath = optional('CLIENT_CRT', 'cert.pem'); const clientKeyPath = optional('CLIENT_KEY', 'key.pem'); @@ -75,6 +75,7 @@ export const loadConfig = (): Config => { // Omitted rather than set to undefined: `ca` is optional, not optional-with-undefined. ...(caPath ? { ca: readPem('BB_CA', caPath) } : {}), }, - primus, }; }; + +export const loadConfig = (): Config => ({ ...loadBBConfig(), primus: loadPrimusConfig() }); diff --git a/src/log.ts b/src/log.ts index b77482a..6900892 100644 --- a/src/log.ts +++ b/src/log.ts @@ -1,14 +1,88 @@ -import { Logger } from 'tslog'; - /** - * The one logger: pretty and colored on a terminal, silent unless DEBUG=true, - * and every credential-looking key masked at any depth — the attestation - * record echoes live `Authorization` headers into the logs. + * The one logger: exactly one line per event — + * `2026-10-27 19:31:33.326 LEVEL message {json}` — colored on a terminal. + * Quiet unless DEBUG=true, except errors and fatal: those always reach stderr. + * Every credential-looking value is masked at any + * depth, in keys and in strings alike: the attestation record echoes live + * `Authorization` headers and app-keyed URLs into the logs. */ -export const log = new Logger({ - name: 'zkpix', - minLevel: /^(1|true)$/i.test(process.env.DEBUG ?? '') ? 'DEBUG' : 'FATAL', - maskValuesOfKeys: ['authorization', 'gw-dev-app-key'], - maskValuesOfKeysCaseInsensitive: true, - maskPlaceholder: '…', -}); +type Level = 'debug' | 'info' | 'warn' | 'error' | 'fatal'; + +const enabled = /^(1|true)$/i.test(process.env.DEBUG ?? ''); +const colors: Record = { debug: '90', info: '36', warn: '33', error: '31', fatal: '31;1' }; + +/** Credential keys masked wherever they appear, however deep. */ +const secretKey = /^(?:authorization|gw-dev-app-key)$/i; +/** The same credentials escaped into text: raw header lines, query URLs. */ +const secretsInText = [ + /(authorization["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\r\n]*)/gi, + /(gw-dev-app-key["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^&\s"']*)/gi, +]; + +const maskText = (text: string): string => + secretsInText + .reduce( + (out, pattern) => + out.replace(pattern, (_match, key: string, value: string) => + /^["']/.test(value) ? `${key}${value[0]}…${value[0]}` : `${key}…`, + ), + text, + ) + .replace(/[\r\n]+/g, ' '); + +const mask = (value: unknown, seen = new WeakSet()): unknown => { + if (value instanceof Error) return errorText(value); + if (typeof value === 'string') return maskText(value); + if (value === null || typeof value !== 'object') return value; + if (seen.has(value)) return '[circular]'; + seen.add(value); + if (Array.isArray(value)) return value.map((item) => mask(item, seen)); + return Object.fromEntries( + Object.entries(value).map(([key, item]) => [key, secretKey.test(key) ? '…' : mask(item, seen)]), + ); +}; + +/** Errors on one line: what failed, and where from — not seven stack frames. */ +const errorText = (error: Error): string => { + const frame = error.stack?.split('\n')[1]?.trim().replace(/^at\s+/, '') ?? ''; + return maskText(`${error.name}: ${error.message}${frame ? ` @ ${frame}` : ''}`); +}; + +/** Empty objects are noise; they render as nothing at all. */ +const render = (value: unknown): string => { + if (value instanceof Error) return errorText(value); + if (typeof value === 'object' && value !== null) { + if (Object.keys(value).length === 0) return ''; + try { + return JSON.stringify(mask(value)) ?? ''; + } catch { + return '[unserialisable]'; + } + } + return maskText(String(value)); +}; + +const emit = (level: Level, args: unknown[]): void => { + const stderr = level === 'error' || level === 'fatal'; + if (!enabled && !stderr) return; + const time = new Date().toISOString().replace('T', ' ').slice(0, 23); + const tag = level.toUpperCase().padEnd(5); + const line = args + .slice() + .sort((a, b) => Number(a instanceof Error) - Number(b instanceof Error)) // errors trail their message + .map(render) + .filter(Boolean) + .join(' '); + const tty = stderr ? process.stderr.isTTY : process.stdout.isTTY; + const text = `${time} ${tty ? `\x1b[${colors[level]}m${tag}\x1b[0m` : tag} ${line}`; + if (stderr) console.error(text); + else console.log(text); +}; + +export const log = { + debug: (...args: unknown[]): void => emit('debug', args), + info: (...args: unknown[]): void => emit('info', args), + warn: (...args: unknown[]): void => emit('warn', args), + error: (...args: unknown[]): void => emit('error', args), + fatal: (...args: unknown[]): void => emit('fatal', args), +};