From 644261d5a66b39b38fbe5b4d38257a5bc2bb31e6 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 26 May 2026 22:06:00 -0700 Subject: [PATCH 1/7] Define SimpleWebAuthnLogger interface --- packages/server/src/helpers/logging.ts | 30 ++++++++++++++------------ 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/server/src/helpers/logging.ts b/packages/server/src/helpers/logging.ts index b06fdcb2..873f7f9a 100644 --- a/packages/server/src/helpers/logging.ts +++ b/packages/server/src/helpers/logging.ts @@ -1,20 +1,22 @@ -// const defaultLogger = debug('SimpleWebAuthn'); - /** - * Generate an instance of a `debug` logger that extends off of the "simplewebauthn" namespace for - * consistent naming. + * A basic logging interface that enables projects to capture logging output from SimpleWebAuthn + * using whatever logging method is appropriate for the project. * - * See https://www.npmjs.com/package/debug for information on how to control logging output when - * using @simplewebauthn/server + * For example, a project using `console` statements to capture logs can use the following + * implementation of this interface: * - * Example: - * - * ``` - * const log = getLogger('mds'); - * log('hello'); // simplewebauthn:mds hello +0ms + * ```ts + * const ConsoleLogger: SimpleWebAuthnLogger = { + * debug(message: string, ...args: unknown[]) { console.debug(message, ...args); }, + * info(message: string, ...args: unknown[]) { console.info(message, ...args); }, + * warn(message: string, ...args: unknown[]) { console.warn(message, ...args); }, + * error(message: string, ...args: unknown[]) { console.error(message, ...args); }, + * }; * ``` */ -export function getLogger(_name: string): (message: string, ..._rest: unknown[]) => void { - // This is a noop for now while I search for a better debug logger technique - return (_message, ..._rest) => {}; +export interface SimpleWebAuthnLogger { + debug: (message: string, ...args: unknown[]) => void; + info: (message: string, ...args: unknown[]) => void; + warn: (message: string, ...args: unknown[]) => void; + error: (message: string, ...args: unknown[]) => void; } From 915b554d305bc1160bbc154c870de27f39bf4811 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 26 May 2026 22:06:26 -0700 Subject: [PATCH 2/7] Create DefaultNoopLogger instance --- packages/server/src/helpers/logging.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/server/src/helpers/logging.ts b/packages/server/src/helpers/logging.ts index 873f7f9a..b3b9a77a 100644 --- a/packages/server/src/helpers/logging.ts +++ b/packages/server/src/helpers/logging.ts @@ -20,3 +20,14 @@ export interface SimpleWebAuthnLogger { warn: (message: string, ...args: unknown[]) => void; error: (message: string, ...args: unknown[]) => void; } + +/** + * A logger instance that doesn't do anything. Useful as a default argument when no custom instance + * of the `SimpleWebAuthnLogger` interface is specified. + */ +export const DefaultNoopLogger: SimpleWebAuthnLogger = { + debug() {}, + info() {}, + warn() {}, + error() {}, +}; From 9bdb8d65f8277f1ef8f7a68080555f9ca2b69b13 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 26 May 2026 22:06:42 -0700 Subject: [PATCH 3/7] Update MetadataService to accept and use logger --- .../server/src/services/metadataService.ts | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/packages/server/src/services/metadataService.ts b/packages/server/src/services/metadataService.ts index e6d60778..078750d0 100644 --- a/packages/server/src/services/metadataService.ts +++ b/packages/server/src/services/metadataService.ts @@ -1,7 +1,7 @@ import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString.ts'; import type { MetadataBLOBPayloadEntry, MetadataStatement } from '../metadata/mdsTypes.ts'; import { verifyMDSBlob } from '../metadata/verifyMDSBlob.ts'; -import { getLogger } from '../helpers/logging.ts'; +import { DefaultNoopLogger, type SimpleWebAuthnLogger } from '../helpers/logging.ts'; import { fetch } from '../helpers/fetch.ts'; import type { Uint8Array_ } from '../types/index.ts'; @@ -44,8 +44,6 @@ enum SERVICE_STATE { */ export type VerificationMode = 'permissive' | 'strict'; -const log = getLogger('MetadataService'); - interface MetadataService { /** * Prepare the service to handle remote MDS servers and/or cache local metadata statements. @@ -65,6 +63,7 @@ interface MetadataService { mdsServers?: string[]; statements?: MetadataStatement[]; verificationMode?: VerificationMode; + logger?: SimpleWebAuthnLogger; }): Promise; /** * Get a metadata statement for a given AAGUID. @@ -86,18 +85,27 @@ export class BaseMetadataService implements MetadataService { private statementCache: { [aaguid: string]: CachedBLOBEntry } = {}; private state: SERVICE_STATE = SERVICE_STATE.DISABLED; private verificationMode: VerificationMode = 'strict'; + private logger: SimpleWebAuthnLogger = DefaultNoopLogger; async initialize( opts: { mdsServers?: string[]; statements?: MetadataStatement[]; verificationMode?: VerificationMode; + logger?: SimpleWebAuthnLogger; } = {}, ): Promise { // Reset statement cache this.statementCache = {}; - const { mdsServers = [defaultURLMDS], statements, verificationMode } = opts; + const { + mdsServers = [defaultURLMDS], + statements, + verificationMode, + logger = DefaultNoopLogger, + } = opts; + + this.logger = logger; this.setState(SERVICE_STATE.REFRESHING); @@ -124,7 +132,7 @@ export class BaseMetadataService implements MetadataService { } }); - log(`Cached ${statementsAdded} local statements`); + this.logger.info(`Cached ${statementsAdded} local statements`); } /** @@ -149,7 +157,7 @@ export class BaseMetadataService implements MetadataService { await this.verifyBlob(blob, cachedMDS); } catch (err) { // Notify of the error and move on - log(`Could not download BLOB from ${url}:`, err); + this.logger.error(`Could not download BLOB from ${url}:`, err); numServers -= 1; } } @@ -157,7 +165,7 @@ export class BaseMetadataService implements MetadataService { // Calculate the difference to get the total number of new statements we successfully added const newCacheCount = Object.keys(this.statementCache).length; const cacheDiff = newCacheCount - currentCacheCount; - log( + this.logger.info( `Cached ${cacheDiff} statements from ${numServers} metadata server(s)`, ); } @@ -288,7 +296,7 @@ export class BaseMetadataService implements MetadataService { // TODO (Feb 2026): It'd be more actionable for devs if a specific error was raised here, // then this message was logged higher up when it can include the array index of the stale // blob. - log( + this.logger.warn( `⚠️ This MDS blob (serial: ${payload.no}) contains stale data as of ${parsedNextUpdate.toISOString()}. Please consider re-initializing MetadataService with a newer MDS blob.`, ); } @@ -337,11 +345,11 @@ export class BaseMetadataService implements MetadataService { this.state = newState; if (newState === SERVICE_STATE.DISABLED) { - log('MetadataService is DISABLED'); + this.logger.debug('MetadataService is DISABLED'); } else if (newState === SERVICE_STATE.REFRESHING) { - log('MetadataService is REFRESHING'); + this.logger.debug('MetadataService is REFRESHING'); } else if (newState === SERVICE_STATE.READY) { - log('MetadataService is READY'); + this.logger.debug('MetadataService is READY'); } } } From b4422e2b983f2ae764aecb978ef949826aab948c Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 27 May 2026 09:17:32 -0700 Subject: [PATCH 4/7] Allow logging levels to be defined independently --- packages/server/src/helpers/index.ts | 1 + packages/server/src/helpers/logging.ts | 43 +++++++++++++++++++++----- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/packages/server/src/helpers/index.ts b/packages/server/src/helpers/index.ts index 2e20d519..6c1c0e2a 100644 --- a/packages/server/src/helpers/index.ts +++ b/packages/server/src/helpers/index.ts @@ -15,3 +15,4 @@ export * from './verifySignature.ts'; export * from './iso/index.ts'; export * from '../metadata/verifyMDSBlob.ts'; export * as cose from './cose.ts'; +export { type SimpleWebAuthnLogger } from './logging.ts'; diff --git a/packages/server/src/helpers/logging.ts b/packages/server/src/helpers/logging.ts index b3b9a77a..3067fb95 100644 --- a/packages/server/src/helpers/logging.ts +++ b/packages/server/src/helpers/logging.ts @@ -1,13 +1,14 @@ /** * A basic logging interface that enables projects to capture logging output from SimpleWebAuthn - * using whatever logging method is appropriate for the project. + * using whatever logging method is appropriate for the project. Logging levels can be defined + * independently to only capture desired levels. * * For example, a project using `console` statements to capture logs can use the following * implementation of this interface: * * ```ts * const ConsoleLogger: SimpleWebAuthnLogger = { - * debug(message: string, ...args: unknown[]) { console.debug(message, ...args); }, + * // debug(message: string, ...args: unknown[]) { console.debug(message, ...args); }, * info(message: string, ...args: unknown[]) { console.info(message, ...args); }, * warn(message: string, ...args: unknown[]) { console.warn(message, ...args); }, * error(message: string, ...args: unknown[]) { console.error(message, ...args); }, @@ -15,19 +16,47 @@ * ``` */ export interface SimpleWebAuthnLogger { - debug: (message: string, ...args: unknown[]) => void; - info: (message: string, ...args: unknown[]) => void; - warn: (message: string, ...args: unknown[]) => void; - error: (message: string, ...args: unknown[]) => void; + debug?: (message: string, ...args: unknown[]) => void; + info?: (message: string, ...args: unknown[]) => void; + warn?: (message: string, ...args: unknown[]) => void; + error?: (message: string, ...args: unknown[]) => void; } /** * A logger instance that doesn't do anything. Useful as a default argument when no custom instance * of the `SimpleWebAuthnLogger` interface is specified. */ -export const DefaultNoopLogger: SimpleWebAuthnLogger = { +export const DefaultNoopLogger: Required = { debug() {}, info() {}, warn() {}, error() {}, }; + +/** + * Generate an instance of SimpleWebAuthnLogger that defines all methods. Any logging method not + * defined on `logger` will be a no-op. + */ +export function buildLoggerAllMethods( + logger: SimpleWebAuthnLogger, +): Required { + const toReturn: Required = { ...DefaultNoopLogger }; + + if (logger.debug) { + toReturn.debug = logger.debug; + } + + if (logger.info) { + toReturn.info = logger.info; + } + + if (logger.warn) { + toReturn.warn = logger.warn; + } + + if (logger.error) { + toReturn.error = logger.error; + } + + return toReturn; +} From 25928cd8abf74918188ea7935c1e6b6b49a01a22 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Wed, 27 May 2026 09:17:47 -0700 Subject: [PATCH 5/7] Update MetadataService --- packages/server/src/services/metadataService.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/server/src/services/metadataService.ts b/packages/server/src/services/metadataService.ts index 078750d0..4c6add66 100644 --- a/packages/server/src/services/metadataService.ts +++ b/packages/server/src/services/metadataService.ts @@ -1,7 +1,11 @@ import { convertAAGUIDToString } from '../helpers/convertAAGUIDToString.ts'; import type { MetadataBLOBPayloadEntry, MetadataStatement } from '../metadata/mdsTypes.ts'; import { verifyMDSBlob } from '../metadata/verifyMDSBlob.ts'; -import { DefaultNoopLogger, type SimpleWebAuthnLogger } from '../helpers/logging.ts'; +import { + buildLoggerAllMethods, + DefaultNoopLogger, + type SimpleWebAuthnLogger, +} from '../helpers/logging.ts'; import { fetch } from '../helpers/fetch.ts'; import type { Uint8Array_ } from '../types/index.ts'; @@ -85,7 +89,7 @@ export class BaseMetadataService implements MetadataService { private statementCache: { [aaguid: string]: CachedBLOBEntry } = {}; private state: SERVICE_STATE = SERVICE_STATE.DISABLED; private verificationMode: VerificationMode = 'strict'; - private logger: SimpleWebAuthnLogger = DefaultNoopLogger; + private logger: Required = DefaultNoopLogger; async initialize( opts: { @@ -105,7 +109,7 @@ export class BaseMetadataService implements MetadataService { logger = DefaultNoopLogger, } = opts; - this.logger = logger; + this.logger = buildLoggerAllMethods(logger); this.setState(SERVICE_STATE.REFRESHING); From fd66f2f3977cbadc975b00ba4cfebce0ea280522 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sun, 14 Jun 2026 05:22:19 -0700 Subject: [PATCH 6/7] Add logging tests --- packages/server/src/helpers/logging.test.ts | 23 +++++++++++++++++++ .../src/services/metadataService.test.ts | 21 ++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 packages/server/src/helpers/logging.test.ts diff --git a/packages/server/src/helpers/logging.test.ts b/packages/server/src/helpers/logging.test.ts new file mode 100644 index 00000000..97f13070 --- /dev/null +++ b/packages/server/src/helpers/logging.test.ts @@ -0,0 +1,23 @@ +import { assertEquals } from '@std/assert'; +import { assertSpyCall, spy } from '@std/testing/mock'; + +import { buildLoggerAllMethods } from './logging.ts'; + +Deno.test('should define default methods for undefined methods', () => { + const logger = buildLoggerAllMethods({}); + + assertEquals(typeof logger.debug, 'function'); + assertEquals(typeof logger.info, 'function'); + assertEquals(typeof logger.warn, 'function'); + assertEquals(typeof logger.error, 'function'); +}); + +Deno.test('should use provided logger methods', () => { + const _debugSpy = spy(); + const logger = buildLoggerAllMethods({ debug: _debugSpy }); + + logger.debug('SimpleWebAuthn'); + + assertEquals(logger.debug, _debugSpy); + assertSpyCall(_debugSpy, 0, { args: ['SimpleWebAuthn'], returned: undefined }); +}); diff --git a/packages/server/src/services/metadataService.test.ts b/packages/server/src/services/metadataService.test.ts index c2a0a4bc..b7631ad1 100644 --- a/packages/server/src/services/metadataService.test.ts +++ b/packages/server/src/services/metadataService.test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertRejects } from '@std/assert'; import { afterEach, beforeEach, describe, it } from '@std/testing/bdd'; -import { assertSpyCallArg, assertSpyCalls, type Stub, stub } from '@std/testing/mock'; +import { assertSpyCallArg, assertSpyCalls, spy, type Stub, stub } from '@std/testing/mock'; import { _fetchInternals } from '../helpers/fetch.ts'; @@ -95,6 +95,25 @@ describe('Method: getStatement()', () => { }); }); +describe('Behavior: logging', () => { + it('should use provided logger', async () => { + const _debugSpy = spy(); + + await MetadataService.initialize({ + mdsServers: [], + statements: [], + logger: { debug: _debugSpy }, + }); + + /** + * NOTE TO FUTURE SELF: As of June 2026 the content of the logging is less important here than + * the fact that the service used the provided logger to communicate internal goings on. + */ + assertSpyCallArg(_debugSpy, 0, 0, 'MetadataService is REFRESHING'); + assertSpyCallArg(_debugSpy, 1, 0, 'MetadataService is READY'); + }); +}); + const localStatementAAGUID = '91dfead7-959e-4475-ad26-9b0d482be089'; const localStatement: MetadataStatement = { legalHeader: 'https://fidoalliance.org/metadata/metadata-statement-legal-header/', From 1bb691269368616d48c3c58cbb93ed57201c43d2 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Sun, 14 Jun 2026 05:24:21 -0700 Subject: [PATCH 7/7] Tweak spy call assertion --- packages/server/src/helpers/logging.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/helpers/logging.test.ts b/packages/server/src/helpers/logging.test.ts index 97f13070..46abcdc1 100644 --- a/packages/server/src/helpers/logging.test.ts +++ b/packages/server/src/helpers/logging.test.ts @@ -1,5 +1,5 @@ import { assertEquals } from '@std/assert'; -import { assertSpyCall, spy } from '@std/testing/mock'; +import { assertSpyCallArg, spy } from '@std/testing/mock'; import { buildLoggerAllMethods } from './logging.ts'; @@ -19,5 +19,5 @@ Deno.test('should use provided logger methods', () => { logger.debug('SimpleWebAuthn'); assertEquals(logger.debug, _debugSpy); - assertSpyCall(_debugSpy, 0, { args: ['SimpleWebAuthn'], returned: undefined }); + assertSpyCallArg(_debugSpy, 0, 0, 'SimpleWebAuthn'); });