UNPKG

@jhzhu89/azure-client-pool

Version:

Azure client lifecycle management with intelligent caching and authentication

1,183 lines (1,162 loc) 40.5 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { AuthMode: () => AuthMode, CredentialType: () => CredentialType, McpRequestMapper: () => McpRequestMapper, createClientProvider: () => createClientProvider, createClientProviderWithMapper: () => createClientProviderWithMapper, getLogger: () => getLogger, setRootLogger: () => setRootLogger }); module.exports = __toCommonJS(index_exports); // src/types.ts var AuthMode = { Application: "application", Delegated: "delegated", Composite: "composite" }; var CredentialType = { Application: "application", Delegated: "delegated" }; var ApplicationAuthStrategy = { Cli: "cli", ManagedIdentity: "mi", Chain: "chain" }; // src/utils/cache.ts var import_ttlcache = __toESM(require("@isaacs/ttlcache")); var import_crypto = require("crypto"); // src/utils/logging.ts var import_pino = require("pino"); function createPinoAdapter(pinoLogger) { return { debug: (message, context) => pinoLogger.debug(context, message), info: (message, context) => pinoLogger.info(context, message), warn: (message, context) => pinoLogger.warn(context, message), error: (message, context) => pinoLogger.error(context, message), child: (context) => createPinoAdapter(pinoLogger.child(context)) }; } var rootLogger; function initializeLogger() { const pinoLogger = (0, import_pino.pino)({ level: process.env.LOG_LEVEL || "info", ...process.env.NODE_ENV === "development" && { transport: { target: "pino-pretty", options: { colorize: true, translateTime: "HH:MM:ss", ignore: "pid,hostname" } } } }); return createPinoAdapter(pinoLogger); } function getRootLogger() { if (!rootLogger) { rootLogger = initializeLogger(); } return rootLogger; } function getLogger(component) { const logger9 = getRootLogger(); return component ? logger9.child?.({ component }) || logger9 : logger9; } function setRootLogger(logger9) { rootLogger = logger9; } // src/utils/cache.ts var logger = getLogger("cache-manager"); var CACHE_KEY_TRUNCATE_LENGTH = 50; function hasDispose(obj) { return obj != null && typeof obj === "object" && // eslint-disable-next-line @typescript-eslint/no-explicit-any (typeof obj.dispose === "function" || // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof obj[Symbol.asyncDispose] === "function" || // eslint-disable-next-line @typescript-eslint/no-explicit-any typeof obj[Symbol.dispose] === "function"); } var CacheManager = class { constructor(config, cacheType) { this.cacheType = cacheType; this.pendingRequests = /* @__PURE__ */ new Map(); this.slidingTtl = config.slidingTtl; this.cache = new import_ttlcache.default({ max: config.maxSize, ttl: config.slidingTtl, updateAgeOnGet: true, checkAgeOnGet: true, dispose: (entry, key, reason) => { this.disposeEntry(entry, key, reason).catch((error) => { logger.error(`Error disposing ${this.cacheType} cache entry`, { cacheType: this.cacheType, cacheKey: this.createLoggableKey(key), error: error instanceof Error ? error.message : String(error), reason }); }); } }); } cache; pendingRequests; slidingTtl; async disposeEntry(entry, cacheKey, reason) { if (!hasDispose(entry.value)) { return; } try { logger.debug(`Disposing ${this.cacheType} cache entry`, { cacheType: this.cacheType, cacheKey: this.createLoggableKey(cacheKey), reason, valueType: entry.value?.constructor?.name || "Unknown" }); const value = entry.value; if (value[Symbol.asyncDispose]) { await value[Symbol.asyncDispose](); } else if (value[Symbol.dispose]) { value[Symbol.dispose](); } else if (value.dispose) { await value.dispose(); } logger.debug(`${this.cacheType} cache entry disposed successfully`, { cacheType: this.cacheType, cacheKey: this.createLoggableKey(cacheKey), reason }); } catch (error) { logger.warn( `${this.cacheType} cache entry dispose failed, continuing cleanup`, { cacheType: this.cacheType, cacheKey: this.createLoggableKey(cacheKey), error: error instanceof Error ? error.message : String(error), reason } ); } } createCacheEntry(value, absoluteTtl) { const entry = { value }; if (absoluteTtl !== void 0) { entry.absoluteExpiresAt = Date.now() + absoluteTtl; } return entry; } async getOrCreate(cacheKey, factory, options) { const cached = this.cache.get(cacheKey); if (cached) { if (cached.absoluteExpiresAt !== void 0 && cached.absoluteExpiresAt <= Date.now()) { return this.createInternal(cacheKey, factory, options); } logger.debug(`${this.cacheType} cache hit`, { cacheKey: this.createLoggableKey(cacheKey), ...options?.contextInfo }); return cached.value; } const existingPromise = this.pendingRequests.get(cacheKey); if (existingPromise) { logger.debug(`Found pending ${this.cacheType} request`, { cacheKey: this.createLoggableKey(cacheKey), ...options?.contextInfo }); return existingPromise; } logger.debug(`${this.cacheType} cache miss, creating new entry`, { cacheKey: this.createLoggableKey(cacheKey), ...options?.contextInfo }); const promise = this.createInternal(cacheKey, factory, options); this.pendingRequests.set(cacheKey, promise); try { return await promise; } finally { this.pendingRequests.delete(cacheKey); } } async createInternal(cacheKey, factory, options) { const value = await factory(); let slidingTtl = options?.slidingTtl ?? this.slidingTtl; const absoluteTtl = options?.absoluteTtl; if (absoluteTtl !== void 0 && absoluteTtl <= 0) { this.cache.delete(cacheKey); return value; } if (absoluteTtl !== void 0 && slidingTtl > absoluteTtl) { logger.debug( `${this.cacheType} slidingTtl adjusted from ${slidingTtl}ms to ${absoluteTtl}ms`, { cacheKey: this.createLoggableKey(cacheKey) } ); slidingTtl = absoluteTtl; } const effectiveTtl = Math.max(slidingTtl, 1); const entry = this.createCacheEntry(value, absoluteTtl); this.cache.set(cacheKey, entry, { ttl: effectiveTtl }); const logData = { cacheKey: this.createLoggableKey(cacheKey), slidingTTL: slidingTtl, effectiveTTL: effectiveTtl, ...options?.contextInfo }; if (absoluteTtl !== void 0) { logData.absoluteTTL = absoluteTtl; } logger.debug(`${this.cacheType} entry created and cached`, logData); return value; } clear() { this.cache.clear(); this.pendingRequests.clear(); logger.debug(`${this.cacheType} cache cleared`); } delete(cacheKey) { const deleted = this.cache.delete(cacheKey); logger.debug(`${this.cacheType} cache entry removed`, { cacheKey: this.createLoggableKey(cacheKey), deleted, cacheSize: this.cache.size }); return deleted; } getStats() { return { size: this.cache.size, maxSize: this.cache.max, pendingRequests: this.pendingRequests.size }; } createLoggableKey(rawKey) { return rawKey.length > CACHE_KEY_TRUNCATE_LENGTH ? rawKey.substring(0, CACHE_KEY_TRUNCATE_LENGTH) + "..." : rawKey; } }; function createStableCacheKey(rawKey) { return (0, import_crypto.createHash)("md5").update(rawKey, "utf8").digest("base64url"); } // src/client-pool/pool.ts var logger2 = getLogger("client-pool"); var ClientPool = class { constructor(clientFactory, credentialManager, config) { this.clientFactory = clientFactory; this.credentialManager = credentialManager; this.config = config; this.clientCache = new CacheManager( { maxSize: config.clientCache.maxSize, slidingTtl: config.clientCache.slidingTtl }, "client-pool" ); } clientCache; async getClient(authContext, options) { const rawCacheKey = this.generateRawCacheKey(authContext, options); const cacheKey = createStableCacheKey(rawCacheKey); logger2.debug("Getting client from cache", { rawCacheKey }); const cacheOptions = { contextInfo: { authMode: authContext.mode, userObjectId: "userObjectId" in authContext ? authContext.userObjectId : void 0, tenantId: "tenantId" in authContext ? authContext.tenantId : void 0 } }; if (authContext.mode !== AuthMode.Application) { const tokenContext = authContext; const now = Date.now(); const tokenRemainingTime = tokenContext.expiresAt - now; const bufferMs = this.config.clientCache.bufferMs; cacheOptions.absoluteTtl = Math.max(tokenRemainingTime - bufferMs, 0); logger2.debug("Using dynamic TTL for token-based auth", { tokenExpiresAt: new Date(tokenContext.expiresAt).toISOString(), tokenRemainingTime: Math.floor(tokenRemainingTime / 1e3), bufferMs: Math.floor(bufferMs / 1e3), customTtl: Math.floor(cacheOptions.absoluteTtl / 1e3) }); } else { const applicationTtl = 2 * 60 * 60 * 1e3; cacheOptions.absoluteTtl = applicationTtl; logger2.debug("Using fixed TTL for application auth", { applicationTtl: Math.floor(applicationTtl / 1e3) }); } return this.clientCache.getOrCreate( cacheKey, async () => { logger2.debug("Creating new client", { authMode: authContext.mode, userObjectId: "userObjectId" in authContext ? authContext.userObjectId : void 0, tenantId: "tenantId" in authContext ? authContext.tenantId : void 0 }); const credentialProvider = { getCredential: (authType) => this.credentialManager.getCredential(authContext, authType) }; return this.clientFactory.createClient(credentialProvider, options); }, cacheOptions ); } generateRawCacheKey(authContext, options) { const parts = [this.config.cacheKeyPrefix, authContext.mode]; if (authContext.mode !== AuthMode.Application) { const tokenContext = authContext; parts.push( tokenContext.tenantId || "unknown", tokenContext.userObjectId || "unknown" ); } const clientFingerprint = this.clientFactory.getClientFingerprint?.(options); if (clientFingerprint) { parts.push(clientFingerprint); } else if (options !== void 0) { const optionsHash = createStableCacheKey(JSON.stringify(options)); parts.push(optionsHash); } return parts.join("::"); } async clearCache() { this.clientCache.clear(); logger2.debug("Client pool cache cleared"); } async removeCachedClient(authContext, options) { const rawCacheKey = this.generateRawCacheKey(authContext, options); const cacheKey = createStableCacheKey(rawCacheKey); const removed = this.clientCache.delete(cacheKey); logger2.debug("Removed specific client from cache", { rawCacheKey, removed }); return removed; } getCacheStats() { return this.clientCache.getStats(); } }; // src/credentials/application-strategy.ts var import_identity = require("@azure/identity"); var logger3 = getLogger("application-strategy"); var ApplicationCredentialStrategy = class { constructor(config) { this.config = config; } createCredential() { logger3.debug("Creating application credential", { strategy: this.config.strategy, hasManagedIdentityClientId: !!this.config.managedIdentityClientId }); switch (this.config.strategy) { case ApplicationAuthStrategy.Cli: logger3.debug("Using Azure CLI credential"); return new import_identity.AzureCliCredential(); case ApplicationAuthStrategy.ManagedIdentity: logger3.debug("Using Managed Identity credential", { clientId: this.config.managedIdentityClientId }); return new import_identity.ManagedIdentityCredential( this.config.managedIdentityClientId ? { clientId: this.config.managedIdentityClientId } : void 0 ); case ApplicationAuthStrategy.Chain: logger3.debug("Using chained credential (CLI -> ManagedIdentity)", { clientId: this.config.managedIdentityClientId }); return new import_identity.ChainedTokenCredential( new import_identity.AzureCliCredential(), new import_identity.ManagedIdentityCredential( this.config.managedIdentityClientId ? { clientId: this.config.managedIdentityClientId } : void 0 ) ); default: logger3.error("Unsupported application strategy", { strategy: this.config.strategy }); throw new Error( `Unsupported application strategy: ${this.config.strategy}` ); } } }; // src/credentials/delegated-strategy.ts var import_identity2 = require("@azure/identity"); var fs = __toESM(require("fs")); var crypto = __toESM(require("crypto")); var logger4 = getLogger("delegated-strategy"); var DelegatedCredentialStrategy = class { constructor(config) { this.config = config; if (config.certificatePem) { this.effectiveCertPath = this.createCertificateFile( config.certificatePem ); } else { this.effectiveCertPath = config.certificatePath; } } effectiveCertPath; createCertificateFile(certificatePem) { const hash = crypto.createHash("sha256").update(certificatePem).digest("hex").substring(0, 16); const certPath = `/dev/shm/azure-cert-${hash}-${process.pid}.pem`; if (this.isValidCertificateFile(certPath, certificatePem)) { return certPath; } const tempPath = `${certPath}.tmp.${Date.now()}.${Math.random().toString(36)}`; fs.writeFileSync(tempPath, certificatePem, { mode: 384 }); fs.renameSync(tempPath, certPath); return certPath; } isValidCertificateFile(certPath, certificatePem) { if (!fs.existsSync(certPath)) { return false; } try { const existingContent = fs.readFileSync(certPath, "utf8"); return existingContent === certificatePem; } catch { return false; } } createOBOCredential(context) { const now = Date.now(); if (context.expiresAt <= now) { const expiredAt = new Date(context.expiresAt).toISOString(); logger4.error("User assertion token has expired", { tenantId: context.tenantId, userObjectId: context.userObjectId, expiredAt }); throw new Error( `User assertion token expired at ${expiredAt}. Please refresh the token and try again.` ); } const baseOptions = { tenantId: context.tenantId, clientId: this.config.clientId, userAssertionToken: context.userAssertion }; if (this.effectiveCertPath) { logger4.debug("Using certificate-based OBO credential"); return new import_identity2.OnBehalfOfCredential( this.createCertificateOptions(baseOptions) ); } logger4.debug("Using secret-based OBO credential"); return new import_identity2.OnBehalfOfCredential(this.createSecretOptions(baseOptions)); } createCertificateOptions(baseOptions) { if (!this.effectiveCertPath) { throw new Error( "Certificate is required for certificate-based authentication" ); } return { ...baseOptions, certificatePath: this.effectiveCertPath, sendCertificateChain: true }; } createSecretOptions(baseOptions) { if (!this.config.clientSecret) { logger4.error("Client secret is missing for secret-based authentication"); throw new Error( "Client secret is required for secret-based authentication" ); } logger4.debug("Building secret options"); return { ...baseOptions, clientSecret: this.config.clientSecret }; } }; // src/credentials/credential-factory.ts var CredentialFactory = class _CredentialFactory { applicationStrategy; delegatedStrategy; constructor(applicationConfig, delegatedConfig) { this.applicationStrategy = new ApplicationCredentialStrategy( applicationConfig ); this.delegatedStrategy = delegatedConfig ? new DelegatedCredentialStrategy(delegatedConfig) : null; } static async create(applicationConfig, delegatedConfig) { return new _CredentialFactory(applicationConfig, delegatedConfig); } createApplicationCredential() { return this.applicationStrategy.createCredential(); } createDelegatedCredential(context) { if (!this.delegatedStrategy) { throw new Error( "Delegated authentication not configured. Please provide delegated auth configuration with: clientId, tenantId, and either clientSecret OR certificate configuration (certificatePath/certificatePem)." ); } return this.delegatedStrategy.createOBOCredential(context); } }; // src/credentials/manager.ts var logger5 = getLogger("credential-manager"); var CredentialManager = class _CredentialManager { constructor(credentialFactory, config) { this.config = config; this.credentialFactory = credentialFactory; this.applicationCredentialCache = this.createCredentialCache( "application-credential" ); } applicationCredentialCache; credentialFactory; static async create(applicationConfig, config, delegatedConfig) { const credentialFactory = await CredentialFactory.create( applicationConfig, delegatedConfig ); return new _CredentialManager(credentialFactory, config); } createCredentialCache(cacheType) { return new CacheManager( { maxSize: this.config.credentialCache.maxSize, slidingTtl: this.config.credentialCache.slidingTtl }, cacheType ); } async getCredential(authContext, authType) { switch (authType) { case CredentialType.Application: return this.getApplicationCredential(); case CredentialType.Delegated: return this.getDelegatedCredential(authContext); default: throw new Error(`Unsupported auth type: ${authType}`); } } async getApplicationCredential() { const rawCacheKey = this.createApplicationRawCacheKey(); const cacheKey = createStableCacheKey(rawCacheKey); logger5.debug("Getting application credential from cache", { rawCacheKey }); return this.applicationCredentialCache.getOrCreate( cacheKey, async () => this.credentialFactory.createApplicationCredential(), { absoluteTtl: this.config.credentialCache.absoluteTtl, contextInfo: { authType: CredentialType.Application } } ); } async getDelegatedCredential(authContext) { const tokenContext = this.validateAndGetTokenContext(authContext); const now = Date.now(); if (tokenContext.expiresAt <= now) { const expiredAt = new Date(tokenContext.expiresAt).toISOString(); logger5.error("User assertion token has expired", { tenantId: tokenContext.tenantId, userObjectId: tokenContext.userObjectId, expiredAt }); throw new Error( `User assertion token expired at ${expiredAt}. Please refresh the token and try again.` ); } logger5.debug("Creating delegated credential without caching", { tenantId: tokenContext.tenantId, userObjectId: tokenContext.userObjectId, tokenExpiresAt: new Date(tokenContext.expiresAt).toISOString() }); return this.credentialFactory.createDelegatedCredential(tokenContext); } validateAndGetTokenContext(authContext) { if (authContext.mode === AuthMode.Application) { throw new Error( "Cannot provide delegated credentials with ApplicationAuthContext" ); } return authContext; } createApplicationRawCacheKey() { const parts = [this.config.cacheKeyPrefix, CredentialType.Application]; return parts.join("::"); } clearCache() { this.applicationCredentialCache.clear(); logger5.debug("Application credential cache cleared"); } getCacheStats() { return { applicationCredentials: this.applicationCredentialCache.getStats() }; } }; // src/auth/context.ts var AuthContextFactory = { application: () => ({ mode: AuthMode.Application }), delegated: (token) => ({ mode: AuthMode.Delegated, tenantId: token.tenantId, userObjectId: token.userObjectId, userAssertion: token.rawToken, expiresAt: token.expiresAt }), composite: (token) => ({ mode: AuthMode.Composite, tenantId: token.tenantId, userObjectId: token.userObjectId, userAssertion: token.rawToken, expiresAt: token.expiresAt }) }; // src/auth/jwt/validator.ts var import_jsonwebtoken = __toESM(require("jsonwebtoken")); var import_jwks_rsa = __toESM(require("jwks-rsa")); // src/auth/jwt/token.ts var ParsedJwtToken = class { constructor(claims, rawToken) { this.claims = claims; this.rawToken = rawToken; } get userObjectId() { return this.claims.userObjectId; } get tenantId() { return this.claims.tenantId; } get expiresAt() { return this.claims.expiresAt; } }; // src/utils/errors.ts var AUTH_ERROR_CODES = { jwt_validation_failed: "jwt_validation_failed", token_exchange_failed: "token_exchange_failed", invalid_access_token: "invalid_access_token" }; var AuthError = class _AuthError extends Error { constructor(code, message, context) { super(message); this.code = code; this.context = context; this.name = "AuthError"; this.timestamp = /* @__PURE__ */ new Date(); if (Error.captureStackTrace) { Error.captureStackTrace(this, _AuthError); } } timestamp; }; // src/auth/jwt/validator.ts var logger6 = getLogger("jwt-validator"); var JwtHandler = class { jwksClient; config; constructor(config) { this.config = { clientId: config.clientId, tenantId: config.tenantId, audience: config.audience ?? config.clientId, issuer: config.issuer ?? `https://sts.windows.net/${config.tenantId}/`, clockTolerance: config.clockTolerance }; this.jwksClient = (0, import_jwks_rsa.default)({ jwksUri: `https://login.microsoftonline.com/${config.tenantId}/discovery/v2.0/keys`, cache: true, cacheMaxAge: config.cacheMaxAge, rateLimit: true, jwksRequestsPerMinute: config.jwksRequestsPerMinute }); } async validateToken(accessToken) { try { logger6.debug("Validating JWT token", { tenantId: this.config.tenantId }); const payload = await this.verifyToken(accessToken); const claims = this.extractClaims(payload); logger6.debug("JWT token validated", { tenantId: claims.tenantId, userObjectId: claims.userObjectId, expiresAt: new Date(claims.expiresAt).toISOString() }); return new ParsedJwtToken(claims, accessToken); } catch (error) { logger6.error("Token validation failed", { tenantId: this.config.tenantId, error: error instanceof Error ? error.message : String(error) }); throw new AuthError( AUTH_ERROR_CODES.jwt_validation_failed, `Token validation failed: ${error instanceof Error ? error.message : error}` ); } } async isValid(accessToken) { try { await this.validateToken(accessToken); return true; } catch { return false; } } verifyToken(accessToken) { return new Promise((resolve, reject) => { const getKey = (header, callback) => { this.jwksClient.getSigningKey(header.kid, (err, key) => { callback(err, key?.getPublicKey()); }); }; import_jsonwebtoken.default.verify( accessToken, getKey, { audience: this.config.audience, issuer: this.config.issuer, algorithms: ["RS256"], clockTolerance: this.config.clockTolerance }, (err, decoded) => { if (err) return reject(err); const payload = decoded; if (payload.tid !== this.config.tenantId) { return reject(new Error("Invalid tenant")); } resolve(payload); } ); }); } extractClaims(payload) { if (!payload.oid || !payload.tid || !payload.exp) { throw new Error("Missing required claims"); } return { userObjectId: payload.oid, tenantId: payload.tid, expiresAt: payload.exp * 1e3 }; } }; // src/config/configuration.ts var import_zod = require("zod"); var SECONDS = 1e3; var MINUTES = 60 * SECONDS; var HOURS = 60 * MINUTES; var DEFAULT_CONFIG = { azure: {}, jwt: { clockTolerance: 300, cacheMaxAge: 24 * HOURS, jwksRequestsPerMinute: 10 }, cache: { keyPrefix: "client", clientCacheSlidingTtl: 45 * MINUTES, clientCacheMaxSize: 100, clientCacheBufferMs: 15 * SECONDS, credentialCacheSlidingTtl: 2 * HOURS, credentialCacheMaxSize: 200, credentialCacheAbsoluteTtl: 12 * HOURS } }; var azureSchema = import_zod.z.object({ clientId: import_zod.z.string().optional(), tenantId: import_zod.z.string().optional(), clientSecret: import_zod.z.string().optional(), certificatePath: import_zod.z.string().optional(), certificatePem: import_zod.z.string().optional(), managedIdentityClientId: import_zod.z.string().optional(), applicationAuthStrategy: import_zod.z.enum(["cli", "mi", "chain"]).optional() }).default(DEFAULT_CONFIG.azure); var jwtSchema = import_zod.z.object({ audience: import_zod.z.string().optional(), issuer: import_zod.z.string().optional(), clockTolerance: import_zod.z.coerce.number().default(DEFAULT_CONFIG.jwt.clockTolerance), cacheMaxAge: import_zod.z.coerce.number().default(DEFAULT_CONFIG.jwt.cacheMaxAge), jwksRequestsPerMinute: import_zod.z.coerce.number().default(DEFAULT_CONFIG.jwt.jwksRequestsPerMinute) }).default(DEFAULT_CONFIG.jwt); var cacheSchema = import_zod.z.object({ keyPrefix: import_zod.z.string().default(DEFAULT_CONFIG.cache.keyPrefix), clientCacheSlidingTtl: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.clientCacheSlidingTtl), clientCacheMaxSize: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.clientCacheMaxSize), clientCacheBufferMs: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.clientCacheBufferMs), credentialCacheSlidingTtl: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.credentialCacheSlidingTtl), credentialCacheMaxSize: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.credentialCacheMaxSize), credentialCacheAbsoluteTtl: import_zod.z.coerce.number().default(DEFAULT_CONFIG.cache.credentialCacheAbsoluteTtl) }).default(DEFAULT_CONFIG.cache); var configSchema = import_zod.z.object({ azure: azureSchema, jwt: jwtSchema, cache: cacheSchema }); // src/config/sources/environment.ts var EnvironmentSource = class { async load() { return { azure: { clientId: process.env.AZURE_CLIENT_ID, tenantId: process.env.AZURE_TENANT_ID, clientSecret: process.env.AZURE_CLIENT_SECRET, certificatePath: process.env.AZURE_CLIENT_CERTIFICATE_PATH, certificatePem: process.env.AZURE_CLIENT_CERTIFICATE_PEM, managedIdentityClientId: process.env.AZURE_MANAGED_IDENTITY_CLIENT_ID, applicationAuthStrategy: process.env.AZURE_APPLICATION_AUTH_STRATEGY }, jwt: { audience: process.env.JWT_AUDIENCE, issuer: process.env.JWT_ISSUER, clockTolerance: process.env.JWT_CLOCK_TOLERANCE, cacheMaxAge: process.env.JWT_CACHE_MAX_AGE, jwksRequestsPerMinute: process.env.JWKS_REQUESTS_PER_MINUTE }, cache: { keyPrefix: process.env.CACHE_KEY_PREFIX, clientCacheSlidingTtl: process.env.CACHE_CLIENT_SLIDING_TTL, clientCacheMaxSize: process.env.CACHE_CLIENT_MAX_SIZE, clientCacheBufferMs: process.env.CACHE_CLIENT_BUFFER_MS, credentialCacheSlidingTtl: process.env.CACHE_CREDENTIAL_SLIDING_TTL, credentialCacheMaxSize: process.env.CACHE_CREDENTIAL_MAX_SIZE, credentialCacheAbsoluteTtl: process.env.CACHE_CREDENTIAL_ABSOLUTE_TTL } }; } }; // src/config/sources/app-config.ts var import_app_configuration_provider = require("@azure/app-configuration-provider"); var import_identity3 = require("@azure/identity"); var logger7 = getLogger("app-config-source"); var AppConfigSource = class { endpoint; keyPrefix; labelFilter; constructor() { const endpoint = process.env.AZURE_APPCONFIG_ENDPOINT; if (!endpoint) { throw new Error( "AZURE_APPCONFIG_ENDPOINT environment variable is required" ); } this.endpoint = endpoint; this.keyPrefix = process.env.AZURE_APPCONFIG_KEY_PREFIX || "clientPool:"; this.labelFilter = process.env.AZURE_APPCONFIG_LABEL_FILTER || ""; logger7.debug("AppConfigSource initialized", { endpoint: this.endpoint, keyPrefix: this.keyPrefix, labelFilter: this.labelFilter || "(none)" }); } async load() { logger7.debug("Loading configuration from Azure App Configuration"); const credential = this.createCredential(); try { const settings = await (0, import_app_configuration_provider.load)(this.endpoint, credential, { selectors: [ { keyFilter: this.keyPrefix + "*", ...this.labelFilter && { labelFilter: this.labelFilter } } ], trimKeyPrefixes: [this.keyPrefix], keyVaultOptions: { credential } }); const config = settings.constructConfigurationObject({ separator: ":" }); logger7.debug("Configuration loaded from App Configuration", { configKeys: Object.keys(config) }); const certificateKeys = Object.keys(config).filter( (key) => key.toLowerCase().includes("cert") || key.toLowerCase().includes("pfx") || key.toLowerCase().includes("private") || key.toLowerCase().includes("key") ); certificateKeys.forEach((key) => { const value = config[key]; if (typeof value === "string" && value.length > 100) { logger7.debug("Certificate-related config detected", { key, valueLength: value.length, startsWithPem: value.startsWith("-----BEGIN"), startsWithMii: value.startsWith("MII"), firstChars: value.substring(0, 50), lastChars: value.substring(value.length - 50), isProbablyBase64: /^[A-Za-z0-9+/]+=*$/.test( value.replace(/\s/g, "") ) }); } }); return config; } catch (error) { logger7.error("Failed to load configuration from App Configuration", { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : void 0 }); throw error; } } createCredential() { const credentials = [new import_identity3.AzureCliCredential()]; const miClientId = process.env.AZURE_MI_CLIENT_ID; if (miClientId) { logger7.debug("Adding ManagedIdentityCredential with client ID", { miClientId }); credentials.push(new import_identity3.ManagedIdentityCredential({ clientId: miClientId })); } else { credentials.push(new import_identity3.ManagedIdentityCredential()); } return new import_identity3.ChainedTokenCredential(...credentials); } }; // src/config/manager.ts var logger8 = getLogger("config-manager"); var ConfigurationManager = class { source; configPromise; constructor(source) { this.source = source || this.createConfigSource(); } async getConfiguration() { if (!this.configPromise) { logger8.debug("Loading configuration for first time"); this.configPromise = this.loadAndValidateConfig(); } return this.configPromise; } async loadAndValidateConfig() { logger8.debug("Loading raw configuration from source"); const raw = await this.source.load(); const config = configSchema.parse(raw); this.validate(config); return config; } createConfigSource() { if (process.env.AZURE_APPCONFIG_ENDPOINT) { logger8.debug("Using AppConfigSource", { endpoint: process.env.AZURE_APPCONFIG_ENDPOINT }); return new AppConfigSource(); } return new EnvironmentSource(); } validate(config) { if (config.jwt.clockTolerance < 0 || config.jwt.cacheMaxAge <= 0 || config.jwt.jwksRequestsPerMinute <= 0) { throw new Error("JWT configuration must have valid values"); } } async getApplicationAuthConfig() { const config = await this.getConfiguration(); return { strategy: config.azure.applicationAuthStrategy || ApplicationAuthStrategy.Chain, ...config.azure.managedIdentityClientId && { managedIdentityClientId: config.azure.managedIdentityClientId } }; } async getDelegatedAuthConfig() { const config = await this.getConfiguration(); const { clientId, tenantId, clientSecret, certificatePath, certificatePem } = config.azure; const hasSecret = !!clientSecret; const hasCertificatePath = !!certificatePath; const hasCertificatePem = !!certificatePem; const hasCredentials = hasSecret || hasCertificatePath || hasCertificatePem; const hasRequiredFields = !!clientId && !!tenantId; if (!hasCredentials || !hasRequiredFields) { return void 0; } let selectedCredential = {}; if (hasCredentials) { if (hasCertificatePem) { selectedCredential = { certificatePem }; } else if (hasCertificatePath) { selectedCredential = { certificatePath }; } else if (hasSecret) { selectedCredential = { clientSecret }; } } return { clientId, tenantId, ...selectedCredential }; } async getJwtConfig() { const config = await this.getConfiguration(); if (!config.azure.clientId || !config.azure.tenantId) { return void 0; } return { clientId: config.azure.clientId, tenantId: config.azure.tenantId, clockTolerance: config.jwt.clockTolerance, cacheMaxAge: config.jwt.cacheMaxAge, jwksRequestsPerMinute: config.jwt.jwksRequestsPerMinute, ...config.jwt.audience && { audience: config.jwt.audience }, ...config.jwt.issuer && { issuer: config.jwt.issuer } }; } async getClientManagerConfig() { const config = await this.getConfiguration(); return { cacheKeyPrefix: config.cache.keyPrefix, clientCache: { slidingTtl: config.cache.clientCacheSlidingTtl, maxSize: config.cache.clientCacheMaxSize, bufferMs: config.cache.clientCacheBufferMs }, credentialCache: { slidingTtl: config.cache.credentialCacheSlidingTtl, maxSize: config.cache.credentialCacheMaxSize, absoluteTtl: config.cache.credentialCacheAbsoluteTtl } }; } }; // src/client-pool/provider.ts var ClientProviderImpl = class { constructor(clientPool, jwtHandler) { this.clientPool = clientPool; this.jwtHandler = jwtHandler; } async getClient(authRequest, options) { const context = await this.createAuthContext(authRequest); return await this.clientPool.getClient(context, options); } async invalidateClientCache(authRequest, options) { const context = await this.createAuthContext(authRequest); return await this.clientPool.removeCachedClient(context, options); } async createAuthContext(authRequest) { switch (authRequest.mode) { case AuthMode.Application: { return AuthContextFactory.application(); } case AuthMode.Delegated: { if (!this.jwtHandler) { throw new Error( "JwtHandler is required for delegated authentication" ); } const parsedToken = await this.jwtHandler.validateToken( authRequest.userAssertion ); return AuthContextFactory.delegated(parsedToken); } case AuthMode.Composite: { if (!this.jwtHandler) { throw new Error( "JwtHandler is required for composite authentication" ); } const compositeToken = await this.jwtHandler.validateToken( authRequest.userAssertion ); return AuthContextFactory.composite(compositeToken); } default: { const _exhaustive = authRequest; throw new Error(`Unknown auth mode: ${_exhaustive}`); } } } }; async function createClientProvider(clientFactory, options) { const configManager = new ConfigurationManager(options?.configSource); const applicationConfig = await configManager.getApplicationAuthConfig(); const delegatedConfig = await configManager.getDelegatedAuthConfig(); const clientManagerConfig = await configManager.getClientManagerConfig(); const jwtConfig = await configManager.getJwtConfig(); const credentialManager = await CredentialManager.create( applicationConfig, clientManagerConfig, delegatedConfig ); const clientPool = new ClientPool( clientFactory, credentialManager, clientManagerConfig ); const jwtHandler = jwtConfig ? new JwtHandler(jwtConfig) : void 0; return new ClientProviderImpl(clientPool, jwtHandler); } async function createClientProviderWithMapper(clientFactory, requestMapper, authRequestFactory, options) { const clientProvider = await createClientProvider(clientFactory, options); return { getClient: async (request) => { const authData = requestMapper.extractAuthData(request); const authRequest = authRequestFactory(authData); const options2 = requestMapper.extractOptions ? requestMapper.extractOptions(request) : void 0; return await clientProvider.getClient(authRequest, options2); }, invalidateClientCache: async (request) => { const authData = requestMapper.extractAuthData(request); const authRequest = authRequestFactory(authData); const options2 = requestMapper.extractOptions ? requestMapper.extractOptions(request) : void 0; return await clientProvider.invalidateClientCache(authRequest, options2); } }; } // src/client-pool/request-mapper.ts var McpRequestMapper = class { extractAuthData(request) { const params = request.params; const arguments_ = params?.arguments; const authData = {}; if (arguments_?.user_assertion && typeof arguments_.user_assertion === "string") { authData.userAssertion = arguments_.user_assertion; } return authData; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AuthMode, CredentialType, McpRequestMapper, createClientProvider, createClientProviderWithMapper, getLogger, setRootLogger }); //# sourceMappingURL=index.js.map