UNPKG

@jhzhu89/azure-client-pool

Version:

Azure client lifecycle management with intelligent caching and authentication

1 lines 77.7 kB
{"version":3,"sources":["../src/types.ts","../src/utils/cache.ts","../src/utils/logging.ts","../src/client-pool/pool.ts","../src/credentials/application-strategy.ts","../src/credentials/delegated-strategy.ts","../src/credentials/credential-factory.ts","../src/credentials/manager.ts","../src/auth/context.ts","../src/auth/jwt/validator.ts","../src/auth/jwt/token.ts","../src/utils/errors.ts","../src/config/configuration.ts","../src/config/sources/environment.ts","../src/config/sources/app-config.ts","../src/config/manager.ts","../src/client-pool/provider.ts","../src/client-pool/request-mapper.ts"],"sourcesContent":["import { type TokenCredential } from \"@azure/identity\";\n\nexport const AuthMode = {\n Application: \"application\",\n Delegated: \"delegated\",\n Composite: \"composite\",\n} as const;\n\nexport type AuthMode = (typeof AuthMode)[keyof typeof AuthMode];\n\nexport const CredentialType = {\n Application: \"application\",\n Delegated: \"delegated\",\n} as const;\n\nexport type CredentialType =\n (typeof CredentialType)[keyof typeof CredentialType];\n\nexport interface ApplicationAuthRequest {\n readonly mode: typeof AuthMode.Application;\n}\n\nexport interface DelegatedAuthRequest {\n readonly mode: typeof AuthMode.Delegated;\n readonly userAssertion: string;\n}\n\nexport interface CompositeAuthRequest {\n readonly mode: typeof AuthMode.Composite;\n readonly userAssertion: string;\n}\n\nexport type AuthRequest =\n | ApplicationAuthRequest\n | DelegatedAuthRequest\n | CompositeAuthRequest;\n\nexport interface CredentialProvider {\n getCredential(authType: CredentialType): Promise<TokenCredential>;\n}\n\nexport interface ClientFactory<TClient, TOptions = void> {\n createClient(\n credentialProvider: CredentialProvider,\n options?: TOptions,\n ): Promise<TClient>;\n getClientFingerprint?(options?: TOptions): string | undefined;\n}\n\nexport const ApplicationAuthStrategy = {\n Cli: \"cli\",\n ManagedIdentity: \"mi\",\n Chain: \"chain\",\n} as const;\n\nexport type ApplicationAuthStrategy =\n (typeof ApplicationAuthStrategy)[keyof typeof ApplicationAuthStrategy];\n","import TTLCache from \"@isaacs/ttlcache\";\nimport { createHash } from \"crypto\";\nimport { getLogger } from \"../utils/logging.js\";\n\nconst logger = getLogger(\"cache-manager\");\n\nconst CACHE_KEY_TRUNCATE_LENGTH = 50;\n\ninterface Disposable {\n dispose?: () => void | Promise<void>;\n [Symbol.asyncDispose]?: () => Promise<void>;\n [Symbol.dispose]?: () => void;\n}\n\nfunction hasDispose(obj: unknown): obj is Disposable {\n return (\n obj != null &&\n typeof obj === \"object\" &&\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (typeof (obj as any).dispose === \"function\" ||\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n typeof (obj as any)[Symbol.asyncDispose] === \"function\" ||\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n typeof (obj as any)[Symbol.dispose] === \"function\")\n );\n}\n\nexport interface CacheConfig {\n maxSize: number;\n slidingTtl: number;\n}\n\nexport interface CacheOptions {\n slidingTtl?: number;\n absoluteTtl?: number;\n contextInfo?: Record<string, unknown>;\n}\n\ninterface CacheEntry<T> {\n value: T;\n absoluteExpiresAt?: number;\n}\n\nexport class CacheManager<T> {\n private cache: TTLCache<string, CacheEntry<T>>;\n private pendingRequests: Map<string, Promise<T>>;\n private slidingTtl: number;\n\n constructor(\n config: CacheConfig,\n private cacheType: string,\n ) {\n this.pendingRequests = new Map();\n this.slidingTtl = config.slidingTtl;\n\n this.cache = new TTLCache<string, CacheEntry<T>>({\n max: config.maxSize,\n ttl: config.slidingTtl,\n updateAgeOnGet: true,\n checkAgeOnGet: true,\n dispose: (entry: CacheEntry<T>, key: string, reason: string) => {\n this.disposeEntry(entry, key, reason).catch((error: unknown) => {\n logger.error(`Error disposing ${this.cacheType} cache entry`, {\n cacheType: this.cacheType,\n cacheKey: this.createLoggableKey(key),\n error: error instanceof Error ? error.message : String(error),\n reason,\n });\n });\n },\n });\n }\n\n private async disposeEntry(\n entry: CacheEntry<T>,\n cacheKey: string,\n reason: string,\n ): Promise<void> {\n if (!hasDispose(entry.value)) {\n return;\n }\n\n try {\n logger.debug(`Disposing ${this.cacheType} cache entry`, {\n cacheType: this.cacheType,\n cacheKey: this.createLoggableKey(cacheKey),\n reason,\n valueType: entry.value?.constructor?.name || \"Unknown\",\n });\n\n const value = entry.value as Disposable;\n\n if (value[Symbol.asyncDispose]) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n await value[Symbol.asyncDispose]!();\n } else if (value[Symbol.dispose]) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n value[Symbol.dispose]!();\n } else if (value.dispose) {\n await value.dispose();\n }\n\n logger.debug(`${this.cacheType} cache entry disposed successfully`, {\n cacheType: this.cacheType,\n cacheKey: this.createLoggableKey(cacheKey),\n reason,\n });\n } catch (error) {\n logger.warn(\n `${this.cacheType} cache entry dispose failed, continuing cleanup`,\n {\n cacheType: this.cacheType,\n cacheKey: this.createLoggableKey(cacheKey),\n error: error instanceof Error ? error.message : String(error),\n reason,\n },\n );\n }\n }\n\n private createCacheEntry(value: T, absoluteTtl?: number): CacheEntry<T> {\n const entry: CacheEntry<T> = { value };\n if (absoluteTtl !== undefined) {\n entry.absoluteExpiresAt = Date.now() + absoluteTtl;\n }\n return entry;\n }\n\n async getOrCreate(\n cacheKey: string,\n factory: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n const cached = this.cache.get(cacheKey);\n if (cached) {\n // Check if cached item has expired based on its absolute TTL\n if (\n cached.absoluteExpiresAt !== undefined &&\n cached.absoluteExpiresAt <= Date.now()\n ) {\n return this.createInternal(cacheKey, factory, options);\n }\n\n logger.debug(`${this.cacheType} cache hit`, {\n cacheKey: this.createLoggableKey(cacheKey),\n ...options?.contextInfo,\n });\n return cached.value;\n }\n\n const existingPromise = this.pendingRequests.get(cacheKey);\n if (existingPromise) {\n logger.debug(`Found pending ${this.cacheType} request`, {\n cacheKey: this.createLoggableKey(cacheKey),\n ...options?.contextInfo,\n });\n return existingPromise;\n }\n\n logger.debug(`${this.cacheType} cache miss, creating new entry`, {\n cacheKey: this.createLoggableKey(cacheKey),\n ...options?.contextInfo,\n });\n\n const promise = this.createInternal(cacheKey, factory, options);\n this.pendingRequests.set(cacheKey, promise);\n\n try {\n return await promise;\n } finally {\n this.pendingRequests.delete(cacheKey);\n }\n }\n\n private async createInternal(\n cacheKey: string,\n factory: () => Promise<T>,\n options?: CacheOptions,\n ): Promise<T> {\n const value = await factory();\n\n let slidingTtl = options?.slidingTtl ?? this.slidingTtl;\n const absoluteTtl = options?.absoluteTtl;\n\n if (absoluteTtl !== undefined && absoluteTtl <= 0) {\n this.cache.delete(cacheKey);\n return value;\n }\n\n if (absoluteTtl !== undefined && slidingTtl > absoluteTtl) {\n logger.debug(\n `${this.cacheType} slidingTtl adjusted from ${slidingTtl}ms to ${absoluteTtl}ms`,\n {\n cacheKey: this.createLoggableKey(cacheKey),\n },\n );\n slidingTtl = absoluteTtl;\n }\n\n const effectiveTtl = Math.max(slidingTtl, 1);\n\n const entry = this.createCacheEntry(value, absoluteTtl);\n this.cache.set(cacheKey, entry, { ttl: effectiveTtl });\n\n const logData: Record<string, unknown> = {\n cacheKey: this.createLoggableKey(cacheKey),\n slidingTTL: slidingTtl,\n effectiveTTL: effectiveTtl,\n ...options?.contextInfo,\n };\n\n if (absoluteTtl !== undefined) {\n logData.absoluteTTL = absoluteTtl;\n }\n\n logger.debug(`${this.cacheType} entry created and cached`, logData);\n\n return value;\n }\n\n clear(): void {\n this.cache.clear();\n this.pendingRequests.clear();\n logger.debug(`${this.cacheType} cache cleared`);\n }\n\n delete(cacheKey: string): boolean {\n const deleted = this.cache.delete(cacheKey);\n logger.debug(`${this.cacheType} cache entry removed`, {\n cacheKey: this.createLoggableKey(cacheKey),\n deleted,\n cacheSize: this.cache.size,\n });\n return deleted;\n }\n\n getStats() {\n return {\n size: this.cache.size,\n maxSize: this.cache.max,\n pendingRequests: this.pendingRequests.size,\n };\n }\n\n private createLoggableKey(rawKey: string): string {\n return rawKey.length > CACHE_KEY_TRUNCATE_LENGTH\n ? rawKey.substring(0, CACHE_KEY_TRUNCATE_LENGTH) + \"...\"\n : rawKey;\n }\n}\n\nexport function createStableCacheKey(rawKey: string): string {\n return createHash(\"md5\").update(rawKey, \"utf8\").digest(\"base64url\");\n}\n","import { pino, type Logger as PinoLogger } from \"pino\";\n\nexport interface Logger {\n debug(message: string, context?: Record<string, unknown>): void;\n info(message: string, context?: Record<string, unknown>): void;\n warn(message: string, context?: Record<string, unknown>): void;\n error(message: string, context?: Record<string, unknown>): void;\n child?(context: Record<string, unknown>): Logger;\n}\n\nfunction createPinoAdapter(pinoLogger: PinoLogger): Logger {\n return {\n debug: (message: string, context?: Record<string, unknown>) =>\n pinoLogger.debug(context, message),\n info: (message: string, context?: Record<string, unknown>) =>\n pinoLogger.info(context, message),\n warn: (message: string, context?: Record<string, unknown>) =>\n pinoLogger.warn(context, message),\n error: (message: string, context?: Record<string, unknown>) =>\n pinoLogger.error(context, message),\n child: (context: Record<string, unknown>) =>\n createPinoAdapter(pinoLogger.child(context)),\n };\n}\n\nlet rootLogger: Logger;\n\nfunction initializeLogger(): Logger {\n const pinoLogger = pino({\n level: process.env.LOG_LEVEL || \"info\",\n ...(process.env.NODE_ENV === \"development\" && {\n transport: {\n target: \"pino-pretty\",\n options: {\n colorize: true,\n translateTime: \"HH:MM:ss\",\n ignore: \"pid,hostname\",\n },\n },\n }),\n });\n\n return createPinoAdapter(pinoLogger);\n}\n\nfunction getRootLogger(): Logger {\n if (!rootLogger) {\n rootLogger = initializeLogger();\n }\n return rootLogger;\n}\n\nexport function getLogger(component?: string): Logger {\n const logger = getRootLogger();\n return component ? logger.child?.({ component }) || logger : logger;\n}\n\nexport function setRootLogger(logger: Logger): void {\n rootLogger = logger;\n}\n","import { type ClientFactory, AuthMode, CredentialType } from \"../types.js\";\nimport { type ClientManagerConfig } from \"../config/configuration.js\";\nimport {\n type AuthContext,\n type TokenBasedAuthContext,\n} from \"../auth/context.js\";\nimport { CredentialManager } from \"../credentials/manager.js\";\nimport {\n CacheManager,\n createStableCacheKey,\n type CacheOptions,\n} from \"../utils/cache.js\";\nimport { getLogger } from \"../utils/logging.js\";\n\nconst logger = getLogger(\"client-pool\");\n\nexport class ClientPool<TClient, TOptions = void> {\n private clientCache: CacheManager<TClient>;\n\n constructor(\n private clientFactory: ClientFactory<TClient, TOptions>,\n private credentialManager: CredentialManager,\n private config: ClientManagerConfig,\n ) {\n this.clientCache = new CacheManager<TClient>(\n {\n maxSize: config.clientCache.maxSize,\n slidingTtl: config.clientCache.slidingTtl,\n },\n \"client-pool\",\n );\n }\n\n async getClient(\n authContext: AuthContext,\n options?: TOptions,\n ): Promise<TClient> {\n const rawCacheKey = this.generateRawCacheKey(authContext, options);\n const cacheKey = createStableCacheKey(rawCacheKey);\n\n logger.debug(\"Getting client from cache\", {\n rawCacheKey,\n });\n\n const cacheOptions: CacheOptions = {\n contextInfo: {\n authMode: authContext.mode,\n userObjectId:\n \"userObjectId\" in authContext ? authContext.userObjectId : undefined,\n tenantId: \"tenantId\" in authContext ? authContext.tenantId : undefined,\n },\n };\n\n if (authContext.mode !== AuthMode.Application) {\n const tokenContext = authContext as TokenBasedAuthContext;\n const now = Date.now();\n const tokenRemainingTime = tokenContext.expiresAt - now;\n const bufferMs = this.config.clientCache.bufferMs;\n\n cacheOptions.absoluteTtl = Math.max(tokenRemainingTime - bufferMs, 0);\n\n logger.debug(\"Using dynamic TTL for token-based auth\", {\n tokenExpiresAt: new Date(tokenContext.expiresAt).toISOString(),\n tokenRemainingTime: Math.floor(tokenRemainingTime / 1000),\n bufferMs: Math.floor(bufferMs / 1000),\n customTtl: Math.floor(cacheOptions.absoluteTtl / 1000),\n });\n } else {\n const applicationTtl = 2 * 60 * 60 * 1000;\n cacheOptions.absoluteTtl = applicationTtl;\n\n logger.debug(\"Using fixed TTL for application auth\", {\n applicationTtl: Math.floor(applicationTtl / 1000),\n });\n }\n\n return this.clientCache.getOrCreate(\n cacheKey,\n async () => {\n logger.debug(\"Creating new client\", {\n authMode: authContext.mode,\n userObjectId:\n \"userObjectId\" in authContext\n ? authContext.userObjectId\n : undefined,\n tenantId:\n \"tenantId\" in authContext ? authContext.tenantId : undefined,\n });\n\n const credentialProvider = {\n getCredential: (authType: CredentialType) =>\n this.credentialManager.getCredential(authContext, authType),\n };\n return this.clientFactory.createClient(credentialProvider, options);\n },\n cacheOptions,\n );\n }\n\n private generateRawCacheKey(\n authContext: AuthContext,\n options?: TOptions,\n ): string {\n const parts: string[] = [this.config.cacheKeyPrefix, authContext.mode];\n\n if (authContext.mode !== AuthMode.Application) {\n const tokenContext = authContext as TokenBasedAuthContext;\n parts.push(\n tokenContext.tenantId || \"unknown\",\n tokenContext.userObjectId || \"unknown\",\n );\n }\n\n const clientFingerprint =\n this.clientFactory.getClientFingerprint?.(options);\n if (clientFingerprint) {\n parts.push(clientFingerprint);\n } else if (options !== undefined) {\n const optionsHash = createStableCacheKey(JSON.stringify(options));\n parts.push(optionsHash);\n }\n\n return parts.join(\"::\");\n }\n\n async clearCache(): Promise<void> {\n this.clientCache.clear();\n logger.debug(\"Client pool cache cleared\");\n }\n\n async removeCachedClient(\n authContext: AuthContext,\n options?: TOptions,\n ): Promise<boolean> {\n const rawCacheKey = this.generateRawCacheKey(authContext, options);\n const cacheKey = createStableCacheKey(rawCacheKey);\n const removed = this.clientCache.delete(cacheKey);\n\n logger.debug(\"Removed specific client from cache\", {\n rawCacheKey,\n removed,\n });\n\n return removed;\n }\n\n getCacheStats() {\n return this.clientCache.getStats();\n }\n}\n","import { type TokenCredential } from \"@azure/identity\";\nimport {\n AzureCliCredential,\n ManagedIdentityCredential,\n ChainedTokenCredential,\n} from \"@azure/identity\";\nimport { type ApplicationAuthConfig } from \"../config/configuration.js\";\nimport { ApplicationAuthStrategy } from \"../types.js\";\nimport { getLogger } from \"../utils/logging.js\";\n\nconst logger = getLogger(\"application-strategy\");\n\nexport class ApplicationCredentialStrategy {\n constructor(private readonly config: ApplicationAuthConfig) {}\n\n createCredential(): TokenCredential {\n logger.debug(\"Creating application credential\", {\n strategy: this.config.strategy,\n hasManagedIdentityClientId: !!this.config.managedIdentityClientId,\n });\n\n switch (this.config.strategy) {\n case ApplicationAuthStrategy.Cli:\n logger.debug(\"Using Azure CLI credential\");\n return new AzureCliCredential();\n\n case ApplicationAuthStrategy.ManagedIdentity:\n logger.debug(\"Using Managed Identity credential\", {\n clientId: this.config.managedIdentityClientId,\n });\n return new ManagedIdentityCredential(\n this.config.managedIdentityClientId\n ? { clientId: this.config.managedIdentityClientId }\n : undefined,\n );\n\n case ApplicationAuthStrategy.Chain:\n logger.debug(\"Using chained credential (CLI -> ManagedIdentity)\", {\n clientId: this.config.managedIdentityClientId,\n });\n return new ChainedTokenCredential(\n new AzureCliCredential(),\n new ManagedIdentityCredential(\n this.config.managedIdentityClientId\n ? { clientId: this.config.managedIdentityClientId }\n : undefined,\n ),\n );\n\n default:\n logger.error(\"Unsupported application strategy\", {\n strategy: this.config.strategy,\n });\n throw new Error(\n `Unsupported application strategy: ${this.config.strategy}`,\n );\n }\n }\n}\n","import {\n OnBehalfOfCredential,\n type OnBehalfOfCredentialCertificateOptions,\n type OnBehalfOfCredentialSecretOptions,\n} from \"@azure/identity\";\nimport { type DelegatedAuthConfig } from \"../config/configuration.js\";\nimport { type TokenBasedAuthContext } from \"../auth/context.js\";\nimport { getLogger } from \"../utils/logging.js\";\nimport * as fs from \"fs\";\nimport * as crypto from \"crypto\";\n\nconst logger = getLogger(\"delegated-strategy\");\n\nexport class DelegatedCredentialStrategy {\n private readonly effectiveCertPath: string | undefined;\n\n constructor(private readonly config: DelegatedAuthConfig) {\n if (config.certificatePem) {\n this.effectiveCertPath = this.createCertificateFile(\n config.certificatePem,\n );\n } else {\n this.effectiveCertPath = config.certificatePath;\n }\n }\n\n private createCertificateFile(certificatePem: string): string {\n const hash = crypto\n .createHash(\"sha256\")\n .update(certificatePem)\n .digest(\"hex\")\n .substring(0, 16);\n\n const certPath = `/dev/shm/azure-cert-${hash}-${process.pid}.pem`;\n\n if (this.isValidCertificateFile(certPath, certificatePem)) {\n return certPath;\n }\n\n const tempPath = `${certPath}.tmp.${Date.now()}.${Math.random().toString(36)}`;\n\n fs.writeFileSync(tempPath, certificatePem, { mode: 0o600 });\n fs.renameSync(tempPath, certPath);\n\n return certPath;\n }\n\n private isValidCertificateFile(\n certPath: string,\n certificatePem: string,\n ): boolean {\n if (!fs.existsSync(certPath)) {\n return false;\n }\n\n try {\n const existingContent = fs.readFileSync(certPath, \"utf8\");\n return existingContent === certificatePem;\n } catch {\n return false;\n }\n }\n\n createOBOCredential(context: TokenBasedAuthContext): OnBehalfOfCredential {\n const now = Date.now();\n\n if (context.expiresAt <= now) {\n const expiredAt = new Date(context.expiresAt).toISOString();\n\n logger.error(\"User assertion token has expired\", {\n tenantId: context.tenantId,\n userObjectId: context.userObjectId,\n expiredAt,\n });\n\n throw new Error(\n `User assertion token expired at ${expiredAt}. Please refresh the token and try again.`,\n );\n }\n\n const baseOptions = {\n tenantId: context.tenantId,\n clientId: this.config.clientId,\n userAssertionToken: context.userAssertion,\n };\n\n if (this.effectiveCertPath) {\n logger.debug(\"Using certificate-based OBO credential\");\n return new OnBehalfOfCredential(\n this.createCertificateOptions(baseOptions),\n );\n }\n\n logger.debug(\"Using secret-based OBO credential\");\n return new OnBehalfOfCredential(this.createSecretOptions(baseOptions));\n }\n\n private createCertificateOptions(baseOptions: {\n tenantId: string;\n clientId: string;\n userAssertionToken: string;\n }): OnBehalfOfCredentialCertificateOptions {\n if (!this.effectiveCertPath) {\n throw new Error(\n \"Certificate is required for certificate-based authentication\",\n );\n }\n\n return {\n ...baseOptions,\n certificatePath: this.effectiveCertPath,\n sendCertificateChain: true,\n };\n }\n\n private createSecretOptions(baseOptions: {\n tenantId: string;\n clientId: string;\n userAssertionToken: string;\n }): OnBehalfOfCredentialSecretOptions {\n if (!this.config.clientSecret) {\n logger.error(\"Client secret is missing for secret-based authentication\");\n throw new Error(\n \"Client secret is required for secret-based authentication\",\n );\n }\n logger.debug(\"Building secret options\");\n return {\n ...baseOptions,\n clientSecret: this.config.clientSecret,\n };\n }\n}\n","import { type TokenCredential } from \"@azure/identity\";\nimport {\n type DelegatedAuthConfig,\n type ApplicationAuthConfig,\n} from \"../config/configuration.js\";\nimport { type TokenBasedAuthContext } from \"../auth/context.js\";\nimport { ApplicationCredentialStrategy } from \"./application-strategy.js\";\nimport { DelegatedCredentialStrategy } from \"./delegated-strategy.js\";\n\nexport class CredentialFactory {\n private readonly applicationStrategy: ApplicationCredentialStrategy;\n private readonly delegatedStrategy: DelegatedCredentialStrategy | null;\n\n private constructor(\n applicationConfig: ApplicationAuthConfig,\n delegatedConfig?: DelegatedAuthConfig,\n ) {\n this.applicationStrategy = new ApplicationCredentialStrategy(\n applicationConfig,\n );\n this.delegatedStrategy = delegatedConfig\n ? new DelegatedCredentialStrategy(delegatedConfig)\n : null;\n }\n\n static async create(\n applicationConfig: ApplicationAuthConfig,\n delegatedConfig?: DelegatedAuthConfig,\n ): Promise<CredentialFactory> {\n return new CredentialFactory(applicationConfig, delegatedConfig);\n }\n\n createApplicationCredential(): TokenCredential {\n return this.applicationStrategy.createCredential();\n }\n\n createDelegatedCredential(context: TokenBasedAuthContext): TokenCredential {\n if (!this.delegatedStrategy) {\n throw new Error(\n \"Delegated authentication not configured. Please provide delegated auth configuration with: \" +\n \"clientId, tenantId, and either clientSecret OR certificate configuration (certificatePath/certificatePem).\",\n );\n }\n return this.delegatedStrategy.createOBOCredential(context);\n }\n}\n","import { type TokenCredential } from \"@azure/identity\";\nimport {\n type ClientManagerConfig,\n type DelegatedAuthConfig,\n type ApplicationAuthConfig,\n} from \"../config/configuration.js\";\nimport {\n type AuthContext,\n type TokenBasedAuthContext,\n} from \"../auth/context.js\";\nimport { CredentialType, AuthMode } from \"../types.js\";\nimport { CacheManager, createStableCacheKey } from \"../utils/cache.js\";\nimport { getLogger } from \"../utils/logging.js\";\nimport { CredentialFactory } from \"./credential-factory.js\";\n\nconst logger = getLogger(\"credential-manager\");\n\ntype CredentialCache = CacheManager<TokenCredential>;\n\ninterface CacheStats {\n applicationCredentials: {\n size: number;\n maxSize: number;\n pendingRequests: number;\n };\n}\n\nexport class CredentialManager {\n private readonly applicationCredentialCache: CredentialCache;\n private readonly credentialFactory: CredentialFactory;\n\n private constructor(\n credentialFactory: CredentialFactory,\n private readonly config: ClientManagerConfig,\n ) {\n this.credentialFactory = credentialFactory;\n this.applicationCredentialCache = this.createCredentialCache(\n \"application-credential\",\n );\n }\n\n static async create(\n applicationConfig: ApplicationAuthConfig,\n config: ClientManagerConfig,\n delegatedConfig?: DelegatedAuthConfig,\n ): Promise<CredentialManager> {\n const credentialFactory = await CredentialFactory.create(\n applicationConfig,\n delegatedConfig,\n );\n return new CredentialManager(credentialFactory, config);\n }\n\n private createCredentialCache(cacheType: string): CredentialCache {\n return new CacheManager<TokenCredential>(\n {\n maxSize: this.config.credentialCache.maxSize,\n slidingTtl: this.config.credentialCache.slidingTtl,\n },\n cacheType,\n );\n }\n\n async getCredential(\n authContext: AuthContext,\n authType: CredentialType,\n ): Promise<TokenCredential> {\n switch (authType) {\n case CredentialType.Application:\n return this.getApplicationCredential();\n case CredentialType.Delegated:\n return this.getDelegatedCredential(authContext);\n default:\n throw new Error(`Unsupported auth type: ${authType}`);\n }\n }\n\n async getApplicationCredential(): Promise<TokenCredential> {\n const rawCacheKey = this.createApplicationRawCacheKey();\n const cacheKey = createStableCacheKey(rawCacheKey);\n\n logger.debug(\"Getting application credential from cache\", {\n rawCacheKey,\n });\n\n return this.applicationCredentialCache.getOrCreate(\n cacheKey,\n async () => this.credentialFactory.createApplicationCredential(),\n {\n absoluteTtl: this.config.credentialCache.absoluteTtl,\n contextInfo: { authType: CredentialType.Application },\n },\n );\n }\n\n async getDelegatedCredential(\n authContext: AuthContext,\n ): Promise<TokenCredential> {\n const tokenContext = this.validateAndGetTokenContext(authContext);\n\n const now = Date.now();\n if (tokenContext.expiresAt <= now) {\n const expiredAt = new Date(tokenContext.expiresAt).toISOString();\n\n logger.error(\"User assertion token has expired\", {\n tenantId: tokenContext.tenantId,\n userObjectId: tokenContext.userObjectId,\n expiredAt,\n });\n\n throw new Error(\n `User assertion token expired at ${expiredAt}. Please refresh the token and try again.`,\n );\n }\n\n logger.debug(\"Creating delegated credential without caching\", {\n tenantId: tokenContext.tenantId,\n userObjectId: tokenContext.userObjectId,\n tokenExpiresAt: new Date(tokenContext.expiresAt).toISOString(),\n });\n\n return this.credentialFactory.createDelegatedCredential(tokenContext);\n }\n\n private validateAndGetTokenContext(\n authContext: AuthContext,\n ): TokenBasedAuthContext {\n if (authContext.mode === AuthMode.Application) {\n throw new Error(\n \"Cannot provide delegated credentials with ApplicationAuthContext\",\n );\n }\n return authContext as TokenBasedAuthContext;\n }\n\n private createApplicationRawCacheKey(): string {\n const parts = [this.config.cacheKeyPrefix, CredentialType.Application];\n return parts.join(\"::\");\n }\n\n clearCache(): void {\n this.applicationCredentialCache.clear();\n logger.debug(\"Application credential cache cleared\");\n }\n\n getCacheStats(): CacheStats {\n return {\n applicationCredentials: this.applicationCredentialCache.getStats(),\n };\n }\n}\n","import { ParsedJwtToken } from \"./jwt/token.js\";\nimport { AuthMode } from \"../types.js\";\n\ninterface BaseAuthContext {\n readonly mode: AuthMode;\n}\n\nexport interface ApplicationAuthContext extends BaseAuthContext {\n readonly mode: typeof AuthMode.Application;\n}\n\nexport interface TokenBasedAuthContext extends BaseAuthContext {\n readonly mode: typeof AuthMode.Delegated | typeof AuthMode.Composite;\n readonly tenantId: string;\n readonly userObjectId: string;\n readonly userAssertion: string;\n readonly expiresAt: number;\n}\n\nexport type AuthContext = ApplicationAuthContext | TokenBasedAuthContext;\n\nexport const AuthContextFactory = {\n application: (): ApplicationAuthContext => ({\n mode: AuthMode.Application,\n }),\n\n delegated: (token: ParsedJwtToken): TokenBasedAuthContext => ({\n mode: AuthMode.Delegated,\n tenantId: token.tenantId,\n userObjectId: token.userObjectId,\n userAssertion: token.rawToken,\n expiresAt: token.expiresAt,\n }),\n\n composite: (token: ParsedJwtToken): TokenBasedAuthContext => ({\n mode: AuthMode.Composite,\n tenantId: token.tenantId,\n userObjectId: token.userObjectId,\n userAssertion: token.rawToken,\n expiresAt: token.expiresAt,\n }),\n};\n","import jwt, { type JwtHeader, type VerifyErrors } from \"jsonwebtoken\";\nimport jwksClient from \"jwks-rsa\";\nimport {\n type ParsedJwtToken,\n type JwtUserClaims,\n ParsedJwtToken as Token,\n} from \"./token.js\";\nimport { AuthError, AUTH_ERROR_CODES } from \"../../utils/errors.js\";\nimport { getLogger } from \"../../utils/logging.js\";\nimport { type JwtConfig } from \"../../config/configuration.js\";\n\nconst logger = getLogger(\"jwt-validator\");\n\nexport class JwtHandler {\n private readonly jwksClient: jwksClient.JwksClient;\n private readonly config: {\n clientId: string;\n tenantId: string;\n audience: string;\n issuer: string;\n clockTolerance: number;\n };\n\n constructor(config: JwtConfig) {\n this.config = {\n clientId: config.clientId,\n tenantId: config.tenantId,\n audience: config.audience ?? config.clientId,\n issuer: config.issuer ?? `https://sts.windows.net/${config.tenantId}/`,\n clockTolerance: config.clockTolerance,\n };\n\n this.jwksClient = jwksClient({\n jwksUri: `https://login.microsoftonline.com/${config.tenantId}/discovery/v2.0/keys`,\n cache: true,\n cacheMaxAge: config.cacheMaxAge,\n rateLimit: true,\n jwksRequestsPerMinute: config.jwksRequestsPerMinute,\n });\n }\n\n async validateToken(accessToken: string): Promise<ParsedJwtToken> {\n try {\n logger.debug(\"Validating JWT token\", { tenantId: this.config.tenantId });\n const payload = await this.verifyToken(accessToken);\n const claims = this.extractClaims(payload);\n logger.debug(\"JWT token validated\", {\n tenantId: claims.tenantId,\n userObjectId: claims.userObjectId,\n expiresAt: new Date(claims.expiresAt).toISOString(),\n });\n return new Token(claims, accessToken);\n } catch (error) {\n logger.error(\"Token validation failed\", {\n tenantId: this.config.tenantId,\n error: error instanceof Error ? error.message : String(error),\n });\n throw new AuthError(\n AUTH_ERROR_CODES.jwt_validation_failed,\n `Token validation failed: ${error instanceof Error ? error.message : error}`,\n );\n }\n }\n\n async isValid(accessToken: string): Promise<boolean> {\n try {\n await this.validateToken(accessToken);\n return true;\n } catch {\n return false;\n }\n }\n\n private verifyToken(accessToken: string): Promise<Record<string, unknown>> {\n return new Promise((resolve, reject) => {\n const getKey = (\n header: JwtHeader,\n callback: (err: Error | null, key?: string) => void,\n ) => {\n this.jwksClient.getSigningKey(header.kid as string, (err, key) => {\n callback(err, key?.getPublicKey());\n });\n };\n\n jwt.verify(\n accessToken,\n getKey,\n {\n audience: this.config.audience,\n issuer: this.config.issuer,\n algorithms: [\"RS256\"],\n clockTolerance: this.config.clockTolerance,\n },\n (\n err: VerifyErrors | null,\n decoded: string | Record<string, unknown> | undefined,\n ) => {\n if (err) return reject(err);\n\n const payload = decoded as Record<string, unknown>;\n if (payload.tid !== this.config.tenantId) {\n return reject(new Error(\"Invalid tenant\"));\n }\n\n resolve(payload);\n },\n );\n });\n }\n\n private extractClaims(payload: Record<string, unknown>): JwtUserClaims {\n if (!payload.oid || !payload.tid || !payload.exp) {\n throw new Error(\"Missing required claims\");\n }\n\n return {\n userObjectId: payload.oid as string,\n tenantId: payload.tid as string,\n expiresAt: (payload.exp as number) * 1000,\n };\n }\n}\n","export interface JwtUserClaims {\n userObjectId: string;\n tenantId: string;\n expiresAt: number;\n}\n\nexport class ParsedJwtToken {\n constructor(\n public readonly claims: JwtUserClaims,\n public readonly rawToken: string,\n ) {}\n\n get userObjectId(): string {\n return this.claims.userObjectId;\n }\n get tenantId(): string {\n return this.claims.tenantId;\n }\n get expiresAt(): number {\n return this.claims.expiresAt;\n }\n}\n","export const AUTH_ERROR_CODES = {\n jwt_validation_failed: \"jwt_validation_failed\",\n token_exchange_failed: \"token_exchange_failed\",\n invalid_access_token: \"invalid_access_token\",\n} as const;\n\nexport type AuthErrorCode =\n (typeof AUTH_ERROR_CODES)[keyof typeof AUTH_ERROR_CODES];\n\nexport class AuthError extends Error {\n public readonly timestamp: Date;\n\n constructor(\n public readonly code: AuthErrorCode,\n message: string,\n public readonly context?: Record<string, unknown>,\n ) {\n super(message);\n this.name = \"AuthError\";\n this.timestamp = new Date();\n\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, AuthError);\n }\n }\n}\n","import { z } from \"zod\";\nimport { ApplicationAuthStrategy } from \"../types.js\";\n\nconst SECONDS = 1000;\nconst MINUTES = 60 * SECONDS;\nconst HOURS = 60 * MINUTES;\n\nexport const DEFAULT_CONFIG = {\n azure: {},\n jwt: {\n clockTolerance: 300,\n cacheMaxAge: 24 * HOURS,\n jwksRequestsPerMinute: 10,\n },\n cache: {\n keyPrefix: \"client\",\n clientCacheSlidingTtl: 45 * MINUTES,\n clientCacheMaxSize: 100,\n clientCacheBufferMs: 15 * SECONDS,\n credentialCacheSlidingTtl: 2 * HOURS,\n credentialCacheMaxSize: 200,\n credentialCacheAbsoluteTtl: 12 * HOURS,\n },\n} as const;\n\nconst azureSchema = z\n .object({\n clientId: z.string().optional(),\n tenantId: z.string().optional(),\n clientSecret: z.string().optional(),\n certificatePath: z.string().optional(),\n certificatePem: z.string().optional(),\n managedIdentityClientId: z.string().optional(),\n applicationAuthStrategy: z.enum([\"cli\", \"mi\", \"chain\"]).optional(),\n })\n .default(DEFAULT_CONFIG.azure);\n\nconst jwtSchema = z\n .object({\n audience: z.string().optional(),\n issuer: z.string().optional(),\n clockTolerance: z.coerce\n .number()\n .default(DEFAULT_CONFIG.jwt.clockTolerance),\n cacheMaxAge: z.coerce.number().default(DEFAULT_CONFIG.jwt.cacheMaxAge),\n jwksRequestsPerMinute: z.coerce\n .number()\n .default(DEFAULT_CONFIG.jwt.jwksRequestsPerMinute),\n })\n .default(DEFAULT_CONFIG.jwt);\n\nconst cacheSchema = z\n .object({\n keyPrefix: z.string().default(DEFAULT_CONFIG.cache.keyPrefix),\n clientCacheSlidingTtl: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.clientCacheSlidingTtl),\n clientCacheMaxSize: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.clientCacheMaxSize),\n clientCacheBufferMs: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.clientCacheBufferMs),\n credentialCacheSlidingTtl: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.credentialCacheSlidingTtl),\n credentialCacheMaxSize: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.credentialCacheMaxSize),\n credentialCacheAbsoluteTtl: z.coerce\n .number()\n .default(DEFAULT_CONFIG.cache.credentialCacheAbsoluteTtl),\n })\n .default(DEFAULT_CONFIG.cache);\n\nexport const configSchema = z.object({\n azure: azureSchema,\n jwt: jwtSchema,\n cache: cacheSchema,\n});\n\nexport type ClientPoolConfiguration = z.infer<typeof configSchema>;\n\nexport interface JwtConfig {\n clientId: string;\n tenantId: string;\n audience?: string;\n issuer?: string;\n clockTolerance: number;\n cacheMaxAge: number;\n jwksRequestsPerMinute: number;\n}\n\nexport interface ClientCacheConfig {\n slidingTtl: number;\n maxSize: number;\n bufferMs: number;\n}\n\nexport interface CredentialCacheConfig {\n slidingTtl: number;\n maxSize: number;\n absoluteTtl: number;\n}\n\nexport interface ClientManagerConfig {\n cacheKeyPrefix: string;\n clientCache: ClientCacheConfig;\n credentialCache: CredentialCacheConfig;\n}\n\nexport interface ApplicationAuthConfig {\n managedIdentityClientId?: string;\n strategy: ApplicationAuthStrategy;\n}\n\nexport interface DelegatedAuthConfig {\n clientId: string;\n tenantId: string;\n clientSecret?: string;\n certificatePath?: string;\n certificatePem?: string;\n}\n","import { ConfigurationSource } from \"../source.js\";\n\nexport class EnvironmentSource implements ConfigurationSource {\n async load() {\n return {\n azure: {\n clientId: process.env.AZURE_CLIENT_ID,\n tenantId: process.env.AZURE_TENANT_ID,\n clientSecret: process.env.AZURE_CLIENT_SECRET,\n certificatePath: process.env.AZURE_CLIENT_CERTIFICATE_PATH,\n certificatePem: process.env.AZURE_CLIENT_CERTIFICATE_PEM,\n managedIdentityClientId: process.env.AZURE_MANAGED_IDENTITY_CLIENT_ID,\n applicationAuthStrategy: process.env.AZURE_APPLICATION_AUTH_STRATEGY,\n },\n jwt: {\n audience: process.env.JWT_AUDIENCE,\n issuer: process.env.JWT_ISSUER,\n clockTolerance: process.env.JWT_CLOCK_TOLERANCE,\n cacheMaxAge: process.env.JWT_CACHE_MAX_AGE,\n jwksRequestsPerMinute: process.env.JWKS_REQUESTS_PER_MINUTE,\n },\n cache: {\n keyPrefix: process.env.CACHE_KEY_PREFIX,\n clientCacheSlidingTtl: process.env.CACHE_CLIENT_SLIDING_TTL,\n clientCacheMaxSize: process.env.CACHE_CLIENT_MAX_SIZE,\n clientCacheBufferMs: process.env.CACHE_CLIENT_BUFFER_MS,\n credentialCacheSlidingTtl: process.env.CACHE_CREDENTIAL_SLIDING_TTL,\n credentialCacheMaxSize: process.env.CACHE_CREDENTIAL_MAX_SIZE,\n credentialCacheAbsoluteTtl: process.env.CACHE_CREDENTIAL_ABSOLUTE_TTL,\n },\n };\n }\n}\n","import { load } from \"@azure/app-configuration-provider\";\nimport {\n AzureCliCredential,\n ManagedIdentityCredential,\n ChainedTokenCredential,\n TokenCredential,\n} from \"@azure/identity\";\nimport { ConfigurationSource } from \"../source.js\";\nimport { getLogger } from \"../../utils/logging.js\";\n\nconst logger = getLogger(\"app-config-source\");\n\nexport class AppConfigSource implements ConfigurationSource {\n private endpoint: string;\n private keyPrefix: string;\n private labelFilter: string;\n\n constructor() {\n const endpoint = process.env.AZURE_APPCONFIG_ENDPOINT;\n if (!endpoint) {\n throw new Error(\n \"AZURE_APPCONFIG_ENDPOINT environment variable is required\",\n );\n }\n this.endpoint = endpoint;\n this.keyPrefix = process.env.AZURE_APPCONFIG_KEY_PREFIX || \"clientPool:\";\n this.labelFilter = process.env.AZURE_APPCONFIG_LABEL_FILTER || \"\";\n\n logger.debug(\"AppConfigSource initialized\", {\n endpoint: this.endpoint,\n keyPrefix: this.keyPrefix,\n labelFilter: this.labelFilter || \"(none)\",\n });\n }\n\n async load() {\n logger.debug(\"Loading configuration from Azure App Configuration\");\n const credential = this.createCredential();\n\n try {\n const settings = await load(this.endpoint, credential, {\n selectors: [\n {\n keyFilter: this.keyPrefix + \"*\",\n ...(this.labelFilter && { labelFilter: this.labelFilter }),\n },\n ],\n trimKeyPrefixes: [this.keyPrefix],\n keyVaultOptions: {\n credential: credential,\n },\n });\n\n const config = settings.constructConfigurationObject({ separator: \":\" });\n logger.debug(\"Configuration loaded from App Configuration\", {\n configKeys: Object.keys(config),\n });\n\n // Debug certificate data from Key Vault references\n const certificateKeys = Object.keys(config).filter(\n (key) =>\n key.toLowerCase().includes(\"cert\") ||\n key.toLowerCase().includes(\"pfx\") ||\n key.toLowerCase().includes(\"private\") ||\n key.toLowerCase().includes(\"key\"),\n );\n\n certificateKeys.forEach((key) => {\n const value = config[key];\n if (typeof value === \"string\" && value.length > 100) {\n logger.debug(\"Certificate-related config detected\", {\n key,\n valueLength: value.length,\n startsWithPem: value.startsWith(\"-----BEGIN\"),\n startsWithMii: value.startsWith(\"MII\"),\n firstChars: value.substring(0, 50),\n lastChars: value.substring(value.length - 50),\n isProbablyBase64: /^[A-Za-z0-9+/]+=*$/.test(\n value.replace(/\\s/g, \"\"),\n ),\n });\n }\n });\n\n return config;\n } catch (error) {\n logger.error(\"Failed to load configuration from App Configuration\", {\n error: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n });\n throw error;\n }\n }\n\n private createCredential(): TokenCredential {\n const credentials: TokenCredential[] = [new AzureCliCredential()];\n\n const miClientId = process.env.AZURE_MI_CLIENT_ID;\n if (miClientId) {\n logger.debug(\"Adding ManagedIdentityCredential with client ID\", {\n miClientId,\n });\n credentials.push(new ManagedIdentityCredential({ clientId: miClientId }));\n } else {\n credentials.push(new ManagedIdentityCredential());\n }\n\n return new ChainedTokenCredential(...credentials);\n }\n}\n","import { ApplicationAuthStrategy } from \"../types.js\";\nimport {\n configSchema,\n type ClientPoolConfiguration,\n type ApplicationAuthConfig,\n type DelegatedAuthConfig,\n type JwtConfig,\n type ClientManagerConfig,\n} from \"./configuration.js\";\nimport { ConfigurationSource } from \"./source.js\";\nimport { EnvironmentSource, AppConfigSource } from \"./sources/index.js\";\nimport { getLogger } from \"../utils/logging.js\";\n\nconst logger = getLogger(\"config-manager\");\n\nexport class ConfigurationManager {\n private source: ConfigurationSource;\n private configPromise?: Promise<ClientPoolConfiguration>;\n\n constructor(source?: ConfigurationSource) {\n this.source = source || this.createConfigSource();\n }\n\n async getConfiguration(): Promise<ClientPoolConfiguration> {\n if (!this.configPromise) {\n logger.debug(\"Loading configuration for first time\");\n this.configPromise = this.loadAndValidateConfig();\n }\n return this.configPromise;\n }\n\n private async loadAndValidateConfig(): Promise<ClientPoolConfiguration> {\n logger.debug(\"Loading raw configuration from source\");\n const raw = await this.source.load();\n const config = configSchema.parse(raw);\n this.validate(config);\n return config;\n }\n\n private createConfigSource(): ConfigurationSource {\n if (process.env.AZURE_APPCONFIG_ENDPOINT) {\n logger.debug(\"Using AppConfigSource\", {\n endpoint: process.env.AZURE_APPCONFIG_ENDPOINT,\n });\n return new AppConfigSource();\n }\n return new EnvironmentSource();\n }\n\n private validate(config: ClientPoolConfiguration): void {\n if (\n config.jwt.clockTolerance < 0 ||\n config.jwt.cacheMaxAge <= 0 ||\n config.jwt.jwksRequestsPerMinute <= 0\n ) {\n throw new Error(\"JWT configuration must have valid values\");\n }\n }\n\n async getApplicationAuthConfig(): Promise<ApplicationAuthConfig> {\n const config = await this.getConfiguration();\n return {\n strategy:\n config.azure.applicationAuthStrategy || ApplicationAuthStrategy.Chain,\n ...(config.azure.managedIdentityClientId && {\n managedIdentityClientId: config.azure.managedIdentityClientId,\n }),\n };\n }\n\n async getDelegatedAuthConfig(): Promise<DelegatedAuthConfig | undefined> {\n const config = await this.getConfiguration();\n const {\n clientId,\n tenantId,\n clientSecret,\n certificatePath,\n certificatePem,\n } = config.azure;\n\n const hasSecret = !!clientSecret;\n const hasCertificatePath = !!certificatePath;\n const hasCertificatePem = !!certificatePem;\n const hasCredentials = hasSecret || hasCertificatePath || hasCertificatePem;\n const hasRequiredFields = !!clientId && !!tenantId;\n\n if (!hasCredentials || !hasRequiredFields) {\n return undefined;\n }\n\n let selectedCredential = {};\n if (hasCredentials) {\n if (hasCertificatePem) {\n selectedCredential = {\n certificatePem,\n };\n } else if (hasCertificatePath) {\n selectedCredential = {\n certificatePath,\n };\n } else if (hasSecret) {\n selectedCredential = { clientSecret };\n }\n }\n\n return {\n clientId,\n tenantId,\n ...selectedCredential,\n };\n }\n\n async getJwtConfig(): Promise<JwtConfig | undefined> {\n const config = await this.getConfiguration();\n\n if (!config.azure.clientId || !config.azure.tenantId) {\n return undefined;\n }\n\n return {\n clientId: config.azure.clientId,\n tenantId: config.azure.tenantId,\n clockTolerance: config.jwt.clockTolerance,\n cacheMaxAge: config.jwt.cacheMaxAge,\n jwksRequestsPerMinute: config.jwt.jwksRequestsPerMinute,\n ...(config.jwt.audience && { audience: config.jwt.audience }),\n ...(config.jwt.issuer && { issuer: config.jwt.issuer }),\n };\n }\n\n async getClientManagerConfig(): Promise<ClientManagerConfig> {\n const config = await this.getConfiguration();\n return {\n cacheKeyPrefix: config.cache.keyPrefix,\n clientCache: {\n slidingTtl: config.cache.clientCacheSlidingTtl,\n maxSize: config.cache.clientCacheMaxSize,\n bufferMs: config.cache.clientCacheBufferMs,\n },\n credentialCache: {\n slidingTtl: config.cache.credentialCacheSlidingTtl,\n maxSize: config.cache.credentialCacheMaxSize,\n absoluteTtl: config.cache.credentialCacheAbsoluteTtl,\n },\n };\n }\n}\n","import { type ClientFactory, AuthMode } from \"../types.js\";\nimport { ClientPool } from \"./pool.js\";\nimport { CredentialManager } from \"../credentials/manager.js\";\nimport { type AuthRequest } from \"../types.js\";\nimport { type AuthContext, AuthContextFactory } from \"../auth/context.js\";\nimport { JwtHandler } from \"../auth/jwt/validator.js\";\nimport { ConfigurationManager } from \"../config/manager.js\";\nimport { ConfigurationSource } from \"../config/source.js\";\nimport {\n type RequestMapper,\n type AuthRequestFactory,\n} from \"./request-mapper.js\";\n\nexport interface ClientProvider<TClient, TOptions = void> {\n getClient(authRequest: AuthRequest, options?: TOptions): Promise<TClient>;\n invalidateClientCache(\n authRequest: AuthRequest,\n options?: TOptions,\n ): Promise<boolean>;\n}\n\nclass ClientProviderImpl<TClient, TOptions = void>\n implements ClientProvider<TClient, TOptions>\n{\n constructor(\n private clientPool: ClientPool<TClient, TOptions>,\n private jwtHandler?: JwtHandler,\n ) {}\n\n async getClient(\n authRequest: AuthRequest,\n options?: TOptions,\n ): Promise<TClient> {\n const context = await this.createAuthContext(authRequest);\n return await this.clientPool.getClient(context, options);\n }\n\n async invalidateClientCache(\n authRequest: AuthRequest,\n options?: TOptions,\n ): Promise<boolean> {\n const context = await this.createAuthContext(authRequest);\n return await this.clientPool.removeCachedClient(context, options);\n }\n\n private async createAuthContext(\n authRequest: AuthRequest,\n ): Promise<AuthContext> {\n switch (authRequest.mode) {\n case AuthMode.Application: {\n return AuthContextFactory.application();\n }\n\n case AuthMode.Delegated: {\n if (!this.jwtHandler) {\n throw new Error(\n \"JwtHandler is required for delegated authentication\",\n );\n }\n const parsedToken = await this.jwtHandler.validateToken(\n authRequest.userAssertion,\n );\n return AuthContextFactory.deleg