@jhzhu89/azure-client-pool
Version:
Azure client lifecycle management with intelligent caching and authentication
1,183 lines (1,162 loc) • 40.5 kB
JavaScript
"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