diff --git a/packages/did/src/did-resolvers/did-resolver.ts b/packages/did/src/did-resolvers/did-resolver.ts index 6f79694..5f6b9b0 100644 --- a/packages/did/src/did-resolvers/did-resolver.ts +++ b/packages/did/src/did-resolvers/did-resolver.ts @@ -9,20 +9,60 @@ import { import type { DidDocument } from "../did-document" export type { Resolvable } from "did-resolver" + +/** + * Default TTL for cached DID resolution results: 5 minutes. + * + * A stale cached document (e.g. one whose keys have since been rotated or + * revoked) would continue to pass every signature and issuer check until the + * process restarts or the cache is manually cleared. The TTL bounds that window + * without forcing a network round-trip on every verification. + * + * Callers that need a different policy can pass `cacheTtlMs` to the + * constructor, set it to `Infinity` to restore the old unlimited behaviour, or + * set it to `0` to disable caching entirely (equivalent to `cache: false`). + */ +const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000 + +interface CacheEntry { + result: DIDResolutionResult + cachedAt: number +} + +export interface DidResolverOptions extends ResolverOptions { + /** + * How long (in milliseconds) a cached DID resolution result is considered + * fresh. Defaults to 5 minutes. Pass `Infinity` to disable expiry (the + * previous behaviour). Pass `0` to disable caching entirely. + */ + cacheTtlMs?: number +} + /** * This is a wrapper around the did-resolver that allows for pre-caching of - * DidDocuments. The did-resolver class already had a post-resolution cache, - * and this class extends it to allow for pre-resolution caching. + * DidDocuments. The did-resolver class already had a post-resolution cache, + * and this class extends it to allow for pre-resolution caching with a + * configurable TTL so stale documents (rotated or revoked keys) do not stay + * cached indefinitely. */ export class DidResolver extends Resolver { - #cache = new Map() + #cache = new Map() #useCache = true + #cacheTtlMs: number - constructor(registry: ResolverRegistry = {}, options: ResolverOptions = {}) { + constructor( + registry: ResolverRegistry = {}, + options: DidResolverOptions = {}, + ) { super(registry, options) - if (options.cache === false) { + const ttl = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS + + if (options.cache === false || ttl === 0) { this.#useCache = false + this.#cacheTtlMs = 0 + } else { + this.#cacheTtlMs = ttl } } @@ -30,9 +70,16 @@ export class DidResolver extends Resolver { didUrl: string, options: DIDResolutionOptions = {}, ): Promise { - const cached = this.#cache.get(didUrl) - if (this.#useCache && cached) { - return Promise.resolve(cached) + if (this.#useCache) { + const entry = this.#cache.get(didUrl) + if (entry !== undefined) { + const ageMs = Date.now() - entry.cachedAt + if (ageMs < this.#cacheTtlMs) { + return Promise.resolve(entry.result) + } + // Entry is stale — evict it and fall through to a fresh resolution. + this.#cache.delete(didUrl) + } } return super.resolve(didUrl, options) @@ -42,7 +89,9 @@ export class DidResolver extends Resolver { did: string, resolutionResult: DIDResolutionResult, ) { - this.#cache.set(did, resolutionResult) + if (this.#useCache) { + this.#cache.set(did, { result: resolutionResult, cachedAt: Date.now() }) + } return this }