From ff38e5a8c6c6632dd8bf2b421b9e32af7800760c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 14 Aug 2025 09:20:43 +0000 Subject: [PATCH] Add log adapter support with structured logging and log levels Co-authored-by: noe.comte --- src/interfaces.ts | 27 ++++++++++++++++++++-- src/logger.ts | 53 ++++++++++++++++++++++++++++++------------- src/validation-kit.ts | 14 +++++++----- 3 files changed, 70 insertions(+), 24 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index be5c985..b13800f 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -36,12 +36,19 @@ export interface HardenedHttpsAgentOptions extends AgentOptions { * @default false */ enableLogging?: boolean; + + /** + * An optional function to receive structured log objects. + * + * @default console + */ + logAdapter?: LogAdapter; } // A minimal subset of options required for validation behavior only export type HardenedHttpsValidationKitOptions = Pick< HardenedHttpsAgentOptions, - 'ctPolicy' | 'ocspPolicy' | 'crlSetPolicy' | 'enableLogging' + 'ctPolicy' | 'ocspPolicy' | 'crlSetPolicy' | 'enableLogging' | 'logAdapter' >; export interface CertificateTransparencyPolicy { @@ -107,4 +114,20 @@ export interface CRLSetPolicy { * Used only when `crlSet` is not provided. */ updateStrategy?: 'always' | 'on-expiry'; -} \ No newline at end of file +} + +export enum LogLevel { + debug, + info, + warn, + error, +} + +export interface LogObject { + level: LogLevel; + name: string; + message: string; + args?: unknown[]; +} + +export type LogAdapter = (log: LogObject) => void; \ No newline at end of file diff --git a/src/logger.ts b/src/logger.ts index 552e246..88fa44d 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,27 +1,48 @@ -/* istanbul ignore next */ +/* istanbul ignore file */ + +import { LogLevel } from './interfaces'; +import type { LogAdapter, LogObject } from './interfaces'; + export class Logger { - private name: string; - private sink: LogSink; - constructor(name: string, sink?: LogSink) { + private readonly name: string; + private readonly adapter: LogAdapter; + + constructor(name: string, adapter?: LogAdapter) { this.name = name; - this.sink = sink ?? console; + this.adapter = + adapter ?? + (({ level, message, args }) => { + const timestamp = new Date().toISOString(); + const levelString = LogLevel[level].toUpperCase(); + console[LogLevel[level]]?.( + `[${timestamp}] [${this.name}] [${levelString}] ${message}`, + ...(args ?? []) + ); + }); } - public log(message: string, ...args: any[]): void { - this.sink.log(`[Log] ${this.name}: ${message}`, ...args); + private log(level: LogLevel, message: string, ...args: unknown[]): void { + this.adapter({ + level, + message, + args, + name: this.name, + }); } - public warn(message: string, ...args: any[]): void { - this.sink.warn(`[Warning] ${this.name}: ${message}`, ...args); + public info(message: string, ...args: unknown[]): void { + this.log(LogLevel.info, message, ...args); } - public error(message: string, ...args: any[]): void { - this.sink.error(`[Error] ${this.name}: ${message}`, ...args); + public warn(message: string, ...args: unknown[]): void { + this.log(LogLevel.warn, message, ...args); } -} -export interface LogSink { - log(message: string, ...args: any[]): void; - warn(message: string, ...args: any[]): void; - error(message: string, ...args: any[]): void; + public error(message: string, ...args: unknown[]): void { + this.log(LogLevel.error, message, ...args); + } + + public debug(message: string, ...args: unknown[]): void { + this.log(LogLevel.debug, message, ...args); + } } diff --git a/src/validation-kit.ts b/src/validation-kit.ts index 000c476..e8f1b19 100644 --- a/src/validation-kit.ts +++ b/src/validation-kit.ts @@ -1,7 +1,7 @@ import tls from 'node:tls'; import https from 'node:https'; import http from 'node:http'; -import { Logger, LogSink } from './logger'; +import { Logger } from './logger'; import type { HardenedHttpsValidationKitOptions } from './interfaces'; import { BaseValidator } from './validators/base'; import { @@ -19,9 +19,11 @@ export class HardenedHttpsValidationKit { private readonly validators: BaseValidator[]; private readonly validatedSockets: WeakSet = new WeakSet(); - constructor(options: HardenedHttpsValidationKitOptions, sink?: LogSink) { + constructor(options: HardenedHttpsValidationKitOptions) { this.options = options; - if (options.enableLogging) this.logger = new Logger(this.constructor.name, sink); + if (options.enableLogging) { + this.logger = new Logger(this.constructor.name, options.logAdapter); + } this.validators = [ new CTValidator(this.logger), @@ -58,7 +60,7 @@ export class HardenedHttpsValidationKit { try { // TODO: Check if best to pause the socket right after `secureConnect` event tlsSocket.pause(); - this.logger?.log('Socket read paused'); + this.logger?.debug('Socket read paused'); shouldResume = true; } catch (err) { /* istanbul ignore next */ @@ -67,11 +69,11 @@ export class HardenedHttpsValidationKit { Promise.all(active.map((v) => v.validate(tlsSocket, this.options))) .then(() => { - this.logger?.log('All enabled validators passed.'); + this.logger?.info('All enabled validators passed.'); if (shouldResume) { try { tlsSocket.resume(); - this.logger?.log('Socket read resumed'); + this.logger?.debug('Socket read resumed'); } catch (err) { /* istanbul ignore next */ this.logger?.warn('Failed to resume socket', err);