UNPKG

oauth-entra-id

Version:

🛡️ A Secure, Performant, and Feature-Rich OAuth 2.0 Integration for Microsoft Entra ID — Fully Abstracted and Production-Ready.

1,432 lines • 59.9 kB
'use strict';

var zod = require('zod');
var cipherKit = require('cipher-kit');
var compressKit = require('compress-kit');
var msalNode = require('@azure/msal-node');
var jwksRsa = require('jwks-rsa');
var node = require('cipher-kit/node');
var webApi = require('cipher-kit/web-api');
var jwt = require('jsonwebtoken');

function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }

var jwt__default = /*#__PURE__*/_interopDefault(jwt);

// src/error.ts
function $isStr(value) {
  return value !== null && value !== void 0 && typeof value === "string" && value.trim().length > 0;
}
function $isObj(value) {
  return typeof value === "object" && value !== null && value !== void 0 && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
}
var base64urlWithDotRegex = /^[A-Za-z0-9._-]+$/;
var zStr = zod.z.string().trim();
var zUuid = zod.z.uuid();
var zUrl = zod.z.url();
var zEmail = zod.z.email({ pattern: zod.z.regexes.html5Email });
zod.z.base64url();
var zLooseBase64 = zStr.regex(base64urlWithDotRegex);
zStr.regex(compressKit.COMPRESSION_REGEX.GENERAL);
var zLoginPrompt = zod.z.enum(["email", "select-account", "sso"]);
var zTimeUnit = zod.z.enum(["ms", "sec"]);
var zCryptoType = zod.z.enum(["web-api", "node"]);
var zAccessTokenExpiry = zod.z.number().positive();
var zRefreshTokenExpiry = zod.z.number().min(3600);
var zOneOrMoreUrls = zod.z.union([zUrl.max(2048).transform((url) => [url]), zod.z.array(zUrl.max(2048)).min(1)]);
var zEncrypted = zStr.max(4096).regex(cipherKit.ENCRYPTION_REGEX.GENERAL);
var zJwt = zod.z.jwt().max(4096);
var zTenantId = zod.z.union([zod.z.literal("common"), zUuid]);
var zScope = zStr.min(3).max(128);
var zEncryptionKey = zStr.min(32).max(64);
var zServiceName = zStr.min(1).max(64);
var zAzure = zod.z.object({
  clientId: zUuid,
  tenantId: zTenantId,
  scopes: zod.z.array(zScope).min(1),
  clientSecret: zStr.min(32).max(128),
  downstreamServices: zod.z.array(
    zod.z.object({
      serviceName: zServiceName,
      scope: zScope,
      serviceUrl: zOneOrMoreUrls,
      encryptionKey: zEncryptionKey,
      cryptoType: zCryptoType.default("node"),
      accessTokenExpiry: zAccessTokenExpiry.default(3600)
    })
  ).min(1).optional(),
  b2bApps: zod.z.array(zod.z.object({ appName: zServiceName, scope: zScope })).min(1).optional()
});
var zConfig = zod.z.object({
  azure: zod.z.union([zAzure.transform((azure) => [azure]), zod.z.array(zAzure).min(1)]),
  frontendUrl: zOneOrMoreUrls,
  serverCallbackUrl: zUrl.max(2048),
  encryptionKey: zEncryptionKey,
  advanced: zod.z.object({
    loginPrompt: zLoginPrompt.default("sso"),
    acceptB2BRequests: zod.z.boolean().default(false),
    cryptoType: zCryptoType.default("node"),
    disableCompression: zod.z.boolean().default(false),
    cookies: zod.z.object({
      timeUnit: zTimeUnit.default("sec"),
      disableSecure: zod.z.boolean().default(false),
      disableSameSite: zod.z.boolean().default(false),
      accessTokenExpiry: zAccessTokenExpiry.default(3600),
      refreshTokenExpiry: zRefreshTokenExpiry.default(2592e3)
    }).prefault({})
  }).prefault({})
});
var zJwtClientConfigBase = zod.z.object({ clientId: zUuid, tenantId: zTenantId });
var zJwtClientConfig = zod.z.object({
  azure: zod.z.union([
    zJwtClientConfigBase,
    zJwtClientConfigBase.extend({
      clientSecret: zStr.min(32).max(128),
      b2bApps: zod.z.array(zod.z.object({ appName: zServiceName, scope: zScope })).min(1).optional()
    })
  ])
});
var zState = zod.z.object({
  azureId: zUuid,
  frontendUrl: zUrl.max(2048),
  codeVerifier: zStr.max(256),
  nonce: zUuid,
  email: zEmail.max(320).optional(),
  prompt: zod.z.enum(["login", "select_account"]).optional(),
  ticketId: zUuid
});
var zInjectedData = zod.z.record(zStr, zod.z.any()).optional();
var zAtStruct = zod.z.object({
  at: zJwt,
  inj: zStr.max(4096).optional(),
  exp: zod.z.number().int().positive(),
  aid: zUuid
});
var zRtStruct = zod.z.object({
  rt: zLooseBase64,
  exp: zod.z.number().int().positive(),
  aid: zUuid
});
var zMethods = {
  getAuthUrl: zod.z.object({
    loginPrompt: zLoginPrompt.optional(),
    email: zEmail.max(320).optional(),
    frontendUrl: zUrl.max(2048).optional(),
    azureId: zUuid.optional()
  }).default({}),
  getTokenByCode: zod.z.object({
    code: zStr.max(2048).regex(base64urlWithDotRegex),
    state: zEncrypted
  }),
  getLogoutUrl: zod.z.object({
    frontendUrl: zUrl.max(2048).optional(),
    azureId: zUuid.optional()
  }).default({}),
  tryGetB2BToken: zod.z.union([
    zod.z.object({
      azureId: zUuid.optional(),
      app: zServiceName
    }).transform((data) => ({
      azureId: data.azureId,
      apps: [data.app]
    })),
    zod.z.object({
      azureId: zUuid.optional(),
      apps: zod.z.array(zServiceName).min(1)
    })
  ]),
  getTokenOnBehalfOf: zod.z.union([
    zod.z.object({
      accessToken: zod.z.union([zJwt, zEncrypted]),
      service: zServiceName,
      azureId: zUuid.optional()
    }).transform((data) => ({
      accessToken: data.accessToken,
      services: [data.service],
      azureId: data.azureId
    })),
    zod.z.object({
      accessToken: zod.z.union([zJwt, zEncrypted]),
      services: zod.z.array(zServiceName).min(1),
      azureId: zUuid.optional()
    })
  ])
};

// src/error.ts
function $ok(result) {
  if ($isObj(result)) {
    return { success: true, ...result };
  }
  return { success: true, result };
}
function $err(err) {
  if (err instanceof OAuthError) {
    return {
      success: false,
      error: { message: err.message, description: err.description, statusCode: err.statusCode }
    };
  }
  return {
    success: false,
    error: "msg" in err && "desc" in err ? { message: err.msg, description: err.desc, statusCode: err.status ?? 400 } : { message: err.message, description: err.description, statusCode: err.statusCode ?? 400 }
  };
}
var OAuthError = class extends Error {
  constructor(err) {
    if ("error" in err && "success" in err) {
      super(err.error.message);
      this.statusCode = err.error.statusCode;
      this.description = err.error.description;
    } else if ("msg" in err && "desc" in err) {
      super(err.msg);
      this.statusCode = err.status ?? 400;
      this.description = err.desc;
    } else if ("message" in err && "description" in err && "statusCode" in err) {
      super(err.message);
      this.statusCode = err.statusCode;
      this.description = err.description;
    } else {
      super("An unknown error occurred");
      this.statusCode = 500;
      this.description = "An unknown error occurred";
    }
    this.name = "OAuthError";
    Object.setPrototypeOf(this, new.target.prototype);
    if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
  }
};
function $stringErr(err) {
  switch (true) {
    case err instanceof OAuthError:
      return `OAuthError: ${err.message} (${err.statusCode}) - ${err.description}`;
    case err instanceof zod.ZodError:
      return `ZodError (Schema Validation): ${err.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "root"}: ${issue.message}`).join(". ")}`;
    case err instanceof Error:
      return `Error ${err.name}: ${err.message} - ${err.stack ?? "No stack trace available"}`;
    case typeof err === "string":
      return `String Error: ${err}`;
    case (typeof err === "object" && err !== null):
      switch (true) {
        case ("success" in err && err.success === false && "error" in err && typeof err.error === "object" && err.error && "message" in err.error && "description" in err.error):
          return `ResultErr Error: ${err.error.message}${"statusCode" in err.error ? ` (${err.error.statusCode})` : ""} - ${err.error.description}`;
        case ("message" in err && "description" in err):
          return `ResultErr Error: ${err.message}${"statusCode" in err ? ` (${err.statusCode})` : ""} - ${err.description}`;
        case ("msg" in err && "desc" in err):
          return `ResultErr Error: ${err.msg}${"status" in err ? ` (${err.status})` : ""} - ${err.desc}`;
        default:
          try {
            return `Object Error: ${JSON.stringify(err, (_, v) => typeof v === "bigint" ? v.toString() : v)}`;
          } catch {
            return `Object Error: [Unserializable] ${String(err)}`;
          }
      }
    default:
      return `Unknown Error: ${String(err)}`;
  }
}

// src/utils/cookie-options.ts
var ACCESS_TOKEN_NAME = "at";
var REFRESH_TOKEN_NAME = "rt";
function $getCookieOptions(params) {
  const timeFrame = params.timeUnit === "sec" ? 1 : 1e3;
  const baseOptions = {
    httpOnly: true,
    secure: params.secure,
    sameSite: params.sameSite ? "strict" : params.secure ? "none" : void 0,
    path: "/"
  };
  return {
    accessTokenOptions: { ...baseOptions, maxAge: params.atExp * timeFrame },
    refreshTokenOptions: { ...baseOptions, maxAge: params.rtExp * timeFrame },
    deleteTokenOptions: { ...baseOptions, maxAge: 0 }
  };
}
function $getCookieNames(clientId, secure) {
  return {
    accessTokenName: `${`${secure ? "__Host-" : ""}${ACCESS_TOKEN_NAME}-${clientId}`}`,
    refreshTokenName: `${`${secure ? "__Host-" : ""}${REFRESH_TOKEN_NAME}-${clientId}`}`
  };
}
function $generateUuid(cryptoType) {
  const uuid = cryptoType === "node" ? node.generateUuid() : webApi.generateUuid();
  if (uuid.error) return $err({ msg: "Failed to generate UUID", desc: $stringErr(uuid.error) });
  return $ok({ uuid: uuid.result });
}
function $newSecretKeys(cryptoType, keys) {
  if (cryptoType === "web-api") {
    return $ok({ secretKeys: keys });
  }
  const secretKeys = {};
  for (const name of Object.keys(keys)) {
    const { secretKey, error } = node.createSecretKey(keys[name]);
    if (error) return $err({ msg: `Failed to create ${name} key`, desc: `Key Creation - ${$stringErr(error)}` });
    secretKeys[name] = secretKey;
  }
  return $ok({ secretKeys });
}
async function $encrypt(cryptoType, data, key) {
  if (!data) return $err({ msg: "Invalid data", desc: "Empty string to encrypt" });
  if (cryptoType === "node") {
    if (!$isStr(key) && !node.isNodeKey(key)) {
      return $err({ msg: "Invalid key type", desc: "Expected NodeKey or string" });
    }
    const { secretKey: secretKey2, error: secretKeyError2 } = node.createSecretKey(key);
    if (secretKeyError2) {
      return $err({ msg: "Failed to create Node secret key", desc: `Key Creation - ${$stringErr(secretKeyError2)}` });
    }
    const encrypted2 = node.encrypt(data, secretKey2);
    if (encrypted2.error) return $err({ msg: "Encryption failed", desc: `Encryption - ${$stringErr(encrypted2.error)}` });
    return $ok({ encrypted: encrypted2.result, newSecretKey: void 0 });
  }
  if (!$isStr(key) && !webApi.isWebApiKey(key)) {
    return $err({ msg: "Invalid key type", desc: "Expected string or WebApiKey" });
  }
  const { secretKey, error: secretKeyError } = await webApi.createSecretKey(key);
  if (secretKeyError) return $err({ msg: "Failed to create Web API secret key", desc: $stringErr(secretKeyError) });
  const encrypted = await webApi.encrypt(data, secretKey);
  if (encrypted.error) return $err({ msg: "Encryption failed", desc: `Encryption - ${$stringErr(encrypted.error)}` });
  return $ok({ encrypted: encrypted.result, newSecretKey: secretKey });
}
async function $decrypt(cryptoType, encrypted, key) {
  if (!encrypted) return $err({ msg: "Invalid data", desc: "Empty string to decrypt" });
  if (cryptoType === "node") {
    if (!$isStr(key) && !node.isNodeKey(key)) {
      return $err({ msg: "Invalid key type", desc: "Expected NodeKey or string" });
    }
    const { secretKey: secretKey2, error: secretKeyError2 } = node.createSecretKey(key);
    if (secretKeyError2) return $err({ msg: "Failed to create Node secret key", desc: $stringErr(secretKeyError2) });
    const decrypted2 = node.decrypt(encrypted, secretKey2);
    if (decrypted2.error) return $err({ msg: "Decryption failed", desc: `Decryption - ${$stringErr(decrypted2.error)}` });
    return $ok({ result: decrypted2.result, newSecretKey: void 0 });
  }
  if (!$isStr(key) && !webApi.isWebApiKey(key)) {
    return $err({ msg: "Invalid key type", desc: "Expected string or WebApiKey" });
  }
  const { secretKey, error: secretKeyError } = await webApi.createSecretKey(key);
  if (secretKeyError) return $err({ msg: "Failed to create Web API secret key", desc: $stringErr(secretKeyError) });
  const decrypted = await webApi.decrypt(encrypted, secretKey);
  if (decrypted.error) return $err({ msg: "Decryption failed", desc: `Decryption - ${$stringErr(decrypted.error)}` });
  return $ok({ result: decrypted.result, newSecretKey: secretKey });
}
async function $encryptObj(cryptoType, obj, key) {
  if (!obj) return $err({ msg: "Invalid data", desc: "Empty object to encrypt" });
  const { result, error } = cipherKit.stringifyObj(obj);
  if (error) return $err({ msg: "Failed to stringify object", desc: $stringErr(error) });
  return await $encrypt(cryptoType, result, key);
}
async function $decryptObj(cryptoType, encrypted, key) {
  if (!encrypted) return $err({ msg: "Invalid data", desc: "Empty string to decrypt" });
  const decrypted = await $decrypt(cryptoType, encrypted, key);
  if (decrypted.error) return decrypted;
  const { result, error } = cipherKit.parseToObj(decrypted.result);
  if (error) return $err({ msg: "Failed to parse object", desc: $stringErr(error) });
  return $ok({ result, newSecretKey: decrypted.newSecretKey });
}

// src/utils/config.ts
function $oauthConfig(configuration) {
  const { data: config, error: configError } = zConfig.safeParse(configuration);
  if (configError) return $err({ msg: "Invalid config", desc: $stringErr(configError), status: 500 });
  const frontendUrls = config.frontendUrl;
  const frontUrlObjects = frontendUrls.map((url) => new URL(url));
  const serverUrlObject = new URL(config.serverCallbackUrl);
  const frontendHosts = new Set(frontUrlObjects.map((url) => url.host));
  const serverHost = serverUrlObject.host;
  const { cookies } = config.advanced;
  const baseCookieOptions = $getCookieOptions({
    timeUnit: cookies.timeUnit,
    atExp: cookies.accessTokenExpiry,
    rtExp: cookies.refreshTokenExpiry,
    secure: !cookies.disableSecure && [serverUrlObject, ...frontUrlObjects].every((url) => url.protocol === "https:"),
    sameSite: !cookies.disableSameSite && frontendHosts.size === 1 && frontendHosts.has(serverHost)
  });
  try {
    const azures = config.azure.map((azure) => {
      const cca = $createCca({
        clientId: azure.clientId,
        tenantId: azure.tenantId,
        clientSecret: azure.clientSecret
      });
      const b2b = $getB2B(azure.b2bApps);
      const obo = $getObo({
        oboServices: azure.downstreamServices,
        secure: baseCookieOptions.accessTokenOptions.secure,
        sameSite: baseCookieOptions.accessTokenOptions.sameSite,
        atExp: config.advanced.cookies.accessTokenExpiry,
        serverUrlObject
      });
      const cookieNames = $getCookieNames(azure.clientId, baseCookieOptions.accessTokenOptions.secure);
      return {
        clientId: azure.clientId,
        tenantId: azure.tenantId,
        scopes: azure.scopes,
        cookiesNames: cookieNames,
        cca,
        b2b: b2b.map,
        b2bNames: b2b.names,
        obo: obo.map,
        oboNames: obo.names
      };
    }).filter((azure) => !!azure);
    if (azures.length === 0) {
      throw new OAuthError({
        msg: "No valid Azure configurations found",
        desc: "Ensure at least one Azure configuration is provided in the config file.",
        status: 500
      });
    }
    const { secretKeys: encryptionKeys, error: secretKeysError } = $newSecretKeys(config.advanced.cryptoType, {
      accessToken: `access-token-${config.encryptionKey}`,
      refreshToken: `refresh-token-${config.encryptionKey}`,
      state: `state-${config.encryptionKey}`,
      ticket: `ticket-${config.encryptionKey}`
    });
    if (secretKeysError) return $err(secretKeysError);
    const msalCryptoProvider = new msalNode.CryptoProvider();
    const { jwksClient, error: jwksError } = $createJwks(azures.length === 1 ? azures[0].tenantId : "common");
    if (jwksError) return $err(jwksError);
    const settings = {
      loginPrompt: config.advanced.loginPrompt,
      acceptB2BRequests: config.advanced.acceptB2BRequests,
      cryptoType: config.advanced.cryptoType,
      disableCompression: config.advanced.disableCompression,
      b2bApps: azures.some((azure) => azure.b2bNames) ? azures.map((azure) => ({ azureId: azure.clientId, names: azure.b2bNames })).filter((azure) => !!azure) : void 0,
      downstreamServices: azures.some((azure) => azure.oboNames) ? azures.map((azure) => ({ azureId: azure.clientId, names: azure.oboNames })).filter((azure) => !!azure) : void 0,
      azures: azures.map((azure) => ({ azureId: azure.clientId, tenantId: azure.tenantId })),
      cookies: {
        timeUnit: config.advanced.cookies.timeUnit,
        isSecure: baseCookieOptions.accessTokenOptions.secure,
        isSameSite: baseCookieOptions.accessTokenOptions.sameSite === "strict",
        accessTokenName: azures[0].cookiesNames.accessTokenName,
        accessTokenExpiry: config.advanced.cookies.accessTokenExpiry,
        refreshTokenName: azures[0].cookiesNames.refreshTokenName,
        refreshTokenExpiry: config.advanced.cookies.refreshTokenExpiry,
        cookieNames: azures.map((azure) => ({
          azureId: azure.clientId,
          accessTokenName: azure.cookiesNames.accessTokenName,
          refreshTokenName: azure.cookiesNames.refreshTokenName
        })),
        deleteOptions: baseCookieOptions.deleteTokenOptions
      }
    };
    return $ok({
      azures,
      frontendUrls,
      frontendWhitelist: frontendHosts,
      serverCallbackUrl: config.serverCallbackUrl,
      baseCookieOptions,
      encryptionKeys,
      msalCryptoProvider,
      jwksClient,
      settings
    });
  } catch (error) {
    if (error instanceof OAuthError) return $err(error);
    return $err({
      msg: "Failed to create Azure configurations",
      desc: `OAuth Provider Constructor - ${$stringErr(error)}`,
      status: 500
    });
  }
}
function $jwtClientConfig(config) {
  const { data: parsedConfig, error: configError } = zJwtClientConfig.safeParse(config);
  if (configError) return $err({ msg: "Invalid config", desc: $stringErr(configError), status: 500 });
  const { jwksClient, error: jwksError } = $createJwks(parsedConfig.azure.tenantId);
  if (jwksError) return $err(jwksError);
  if (!("clientSecret" in parsedConfig.azure)) {
    return $ok({
      azure: {
        clientId: parsedConfig.azure.clientId,
        tenantId: parsedConfig.azure.tenantId,
        cca: void 0,
        b2bApps: void 0
      },
      jwksClient
    });
  }
  try {
    const b2b = $getB2B(parsedConfig.azure.b2bApps);
    const cca = $createCca({
      clientId: parsedConfig.azure.clientId,
      tenantId: parsedConfig.azure.tenantId,
      clientSecret: parsedConfig.azure.clientSecret
    });
    return $ok({
      azure: {
        clientId: parsedConfig.azure.clientId,
        tenantId: parsedConfig.azure.tenantId,
        cca,
        b2bApps: b2b.map
      },
      jwksClient
    });
  } catch (error) {
    if (error instanceof OAuthError) return $err(error);
    return $err({
      msg: "Failed to create Azure configuration",
      desc: `OAuth Lite Provider Constructor - ${$stringErr(error)}`,
      status: 500
    });
  }
}
function $createJwks(tenantId) {
  try {
    return $ok({
      jwksClient: new jwksRsa.JwksClient({
        cache: true,
        cacheMaxEntries: 5,
        cacheMaxAge: 10 * 60 * 1e3,
        jwksUri: `https://login.microsoftonline.com/${tenantId}/discovery/v2.0/keys`
      })
    });
  } catch (error) {
    return $err({ msg: "Failed to create JWKS client", desc: $stringErr(error), status: 500 });
  }
}
function $createCca(params) {
  try {
    return new msalNode.ConfidentialClientApplication({
      auth: {
        clientId: params.clientId,
        authority: `https://login.microsoftonline.com/${params.tenantId}`,
        clientSecret: params.clientSecret
      }
    });
  } catch (error) {
    throw new OAuthError({
      msg: "Failed to create Confidential Client Application",
      desc: $stringErr(error),
      status: 500
    });
  }
}
function $getB2B(b2bApps) {
  if (!b2bApps) return { map: void 0, names: void 0 };
  const map = new Map(
    b2bApps.map((app) => [
      app.appName,
      { appName: app.appName, scope: app.scope, token: null, exp: null, aud: null, msalResponse: null }
    ])
  );
  const names = Array.from(map.keys());
  if (names.length !== b2bApps.length) {
    throw new OAuthError({ msg: "Invalid config", desc: "B2B has duplicates", status: 500 });
  }
  return { map, names };
}
function $getObo({
  oboServices,
  secure,
  sameSite,
  atExp,
  serverUrlObject
}) {
  if (!oboServices) return { map: void 0, names: void 0 };
  const map = new Map(
    oboServices.map((service) => {
      const serviceUrlObjects = service.serviceUrl.map((url) => new URL(url));
      const serviceUrlHosts = new Set(serviceUrlObjects.map((url) => url.host));
      return [
        service.serviceName,
        {
          serviceName: service.serviceName,
          scope: service.scope,
          encryptionKey: service.encryptionKey,
          cryptoType: service.cryptoType,
          isSecure: secure && [serverUrlObject, ...serviceUrlObjects].every((url) => url.protocol === "https:"),
          isSamesite: sameSite === "strict" && serviceUrlHosts.size === 1 && serviceUrlHosts.has(serverUrlObject.host),
          atExp: service.accessTokenExpiry ?? atExp
        }
      ];
    })
  );
  const names = Array.from(map.keys());
  if (names.length !== oboServices.length) {
    throw new OAuthError({ msg: "Invalid config", desc: "OBO has duplicates", status: 500 });
  }
  return { map, names };
}
function $extractDataFromPayload(payload) {
  if (!payload || typeof payload === "string") {
    return $err({ msg: "Unauthorized", desc: "Payload is a string or null", status: 401 });
  }
  const isApp = payload.sub === payload.oid;
  return $ok({
    meta: {
      audience: payload.aud,
      issuer: payload.iss,
      subject: payload.sub,
      issuedAt: payload.iat,
      expiration: payload.exp,
      uniqueId: payload.oid,
      azureId: payload.aud,
      tenantId: payload.tid,
      roles: payload.roles,
      uniqueTokenId: payload.uti,
      ...isApp ? {
        isApp: true,
        appId: payload.azp
      } : {
        isApp: false,
        name: payload.name,
        email: payload.preferred_username
      }
    }
  });
}
async function $verifyJwt({
  jwtToken,
  azure,
  jwksClient
}) {
  const { kid, tenantId, issuer, error } = $getKeyIdAndExtra(jwtToken);
  if (error) return $err({ msg: "Unauthorized", desc: `Key ID Extraction - ${$stringErr(error)}`, status: 401 });
  if (azure.tenantId !== "common" && tenantId !== azure.tenantId) {
    return $err({
      msg: "Unauthorized",
      desc: `Invalid tenant ID (tid) claim, expected: ${azure.tenantId}, got: ${tenantId}`,
      status: 401
    });
  }
  if (issuer !== `https://login.microsoftonline.com/${tenantId}/v2.0`) {
    return $err({
      msg: "Unauthorized",
      desc: `Invalid issuer (iss) claim, expected: https://login.microsoftonline.com/${tenantId}/v2.0, got: ${issuer}`,
      status: 401
    });
  }
  try {
    const publicKey = await $getPublicKey(jwksClient, kid);
    const decodedJwt = jwt__default.default.verify(jwtToken, publicKey, {
      algorithms: ["RS256"],
      audience: azure.clientId,
      complete: true
    });
    const { meta, error: error2 } = $extractDataFromPayload(decodedJwt.payload);
    if (error2) return $err(error2);
    return $ok({ payload: decodedJwt.payload, meta });
  } catch (err) {
    return $err({
      msg: "Unauthorized",
      desc: `Failed to verify JWT token. Check your Azure Portal, make sure the 'accessTokenAcceptedVersion' is set to '2' in the 'Manifest' area. Error: ${err instanceof Error ? err.message : err}`,
      status: 401
    });
  }
}
function $getPublicKey(jwksClient, kid) {
  return new Promise((resolve, reject) => {
    jwksClient.getSigningKey(kid, (err, key) => {
      if (err || !key) {
        reject(new Error("Error retrieving signing key"));
        return;
      }
      const publicKey = key.getPublicKey();
      if (!publicKey) {
        reject(new Error("Public key not found"));
        return;
      }
      resolve(publicKey);
    });
  });
}
function $decodeJwt(jwtToken) {
  if (!$isStr(jwtToken)) return $err({ msg: "Invalid JWT token", desc: "Empty JWT" });
  try {
    const decodedJwt = jwt__default.default.decode(jwtToken, { complete: true });
    if (!decodedJwt) return $err({ msg: "Invalid JWT token", desc: "Couldn't decode JWT token" });
    return $ok({ decodedJwt });
  } catch (error) {
    return $err({
      msg: "Invalid JWT token",
      desc: `Decoding error: ${error instanceof Error ? error.message : typeof error === "string" ? error : String(error)}`
    });
  }
}
function $getExpiry(jwtToken) {
  const { decodedJwt, error } = $decodeJwt(jwtToken);
  if (error) return $err(error);
  if (typeof decodedJwt.payload === "string") {
    return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
  }
  const clientId = decodedJwt.payload.aud;
  if (typeof clientId !== "string")
    return $err({
      msg: "Invalid JWT token",
      desc: `Invalid audience (aud) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
    });
  const exp = decodedJwt.payload.exp;
  if (typeof exp !== "number")
    return $err({
      msg: "Invalid JWT token",
      desc: `Invalid expiration (exp) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
    });
  return $ok({ clientId, exp });
}
function $getKeyIdAndExtra(jwtToken) {
  const { decodedJwt, error } = $decodeJwt(jwtToken);
  if (error) return $err(error);
  try {
    const kid = decodedJwt.header.kid;
    if (typeof kid !== "string")
      return $err({
        msg: "Invalid JWT token",
        desc: `Invalid key ID (kid) claim, header: ${JSON.stringify(decodedJwt.header)}`
      });
    if (typeof decodedJwt.payload === "string") {
      return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
    }
    const tenantId = decodedJwt.payload.tid;
    if (typeof tenantId !== "string")
      return $err({
        msg: "Invalid JWT token",
        desc: `Invalid tenant ID (tid) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
      });
    const issuer = decodedJwt.payload.iss;
    if (typeof issuer !== "string")
      return $err({
        msg: "Invalid JWT token",
        desc: `Invalid issuer (iss) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
      });
    return $ok({ kid, tenantId, issuer });
  } catch (error2) {
    return $err({
      msg: "Invalid JWT token",
      desc: `Error extracting key ID (kid), tenant ID (tid), and issuer (iss): ${error2 instanceof Error ? error2.message : String(error2)}`
    });
  }
}
function $getClientId(jwtToken) {
  const { decodedJwt, error } = $decodeJwt(jwtToken);
  if (error) return $err(error);
  if (typeof decodedJwt.payload === "string") {
    return $err({ msg: "Invalid JWT token", desc: "Couldn't get the JWT payload" });
  }
  const clientId = decodedJwt.payload.aud;
  if (typeof clientId !== "string")
    return $err({
      msg: "Invalid JWT token",
      desc: `Invalid audience (aud) claim, payload: ${JSON.stringify(decodedJwt.payload)}`
    });
  return $ok({ clientId });
}

// src/utils/encrypt-tokens.ts
async function $encryptAccessToken(value, params) {
  const { data: accessToken, error: jwtError } = zJwt.safeParse(value);
  if (jwtError) return $err({ msg: "Invalid access token format", desc: $stringErr(jwtError) });
  const { data: dataToInject, error: injectError } = zInjectedData.safeParse(params.dataToInject);
  if (injectError) return $err({ msg: "Invalid injected data format", desc: $stringErr(injectError) });
  const injectedData = dataToInject && Object.keys(dataToInject).length !== 0 && !params.disableCompression ? compressKit.compressObj(dataToInject) : void 0;
  if (injectedData?.error) {
    return $err({ msg: "Failed to compress injected data", desc: `Compression - ${$stringErr(injectedData.error)}` });
  }
  const struct = {
    at: accessToken,
    inj: injectedData?.result,
    exp: Date.now() + params.expiry * 1e3,
    aid: params.azureId
  };
  const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, struct, params.key);
  if (error) return $err({ msg: "Failed to encrypt access token", desc: `Encryption - ${$stringErr(error)}` });
  if (params.isOtherKey === false) params.$updateSecretKey("accessToken", newSecretKey);
  if (encrypted.length > 4096) {
    return $err({
      msg: "Token too long",
      desc: `Encrypted access token exceeds 4096 characters. Encrypted length: ${encrypted.length}, original length: ${accessToken.length}, injected data length: ${injectedData?.result.length ?? 0}`
    });
  }
  return $ok({ encrypted });
}
async function $decryptAccessToken(value, params) {
  const { data: jwtToken, success: jwtSuccess } = zJwt.safeParse(value);
  if (jwtSuccess) {
    const { clientId, error: jwtError } = $getClientId(jwtToken);
    if (jwtError) return $err(jwtError);
    return $ok({ decrypted: jwtToken, azureId: clientId, injectedData: void 0, wasEncrypted: false });
  }
  const { data: encryptedAt, error: encryptedAtError } = zEncrypted.safeParse(value);
  if (encryptedAtError) return $err({ msg: "Unauthorized", desc: $stringErr(encryptedAtError) });
  const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedAt, params.key);
  if (error) return $err({ msg: "Failed to decrypt access token", desc: `Decryption - ${$stringErr(error)}` });
  params.$updateSecretKey("accessToken", newSecretKey);
  const { data: atStruct, error: atStructError } = zAtStruct.safeParse(result);
  if (atStructError) return $err({ msg: "Invalid access token format", desc: $stringErr(atStructError) });
  if (atStruct.exp < Date.now()) {
    return $err({
      msg: "Access token expired",
      desc: `Access token expired at ${new Date(atStruct.exp).toISOString()}`
    });
  }
  const decompressedInjectedData = atStruct.inj ? compressKit.decompressObj(atStruct.inj) : void 0;
  if (decompressedInjectedData?.error) {
    return $err({
      msg: "Failed to decompress injected data",
      desc: `Decompression - ${$stringErr(decompressedInjectedData.error)}`
    });
  }
  return $ok({
    decrypted: atStruct.at,
    azureId: atStruct.aid,
    injectedData: decompressedInjectedData ? decompressedInjectedData.result : void 0,
    wasEncrypted: true
  });
}
async function $encryptRefreshToken(value, params) {
  const { data, error: parseError } = zLooseBase64.safeParse(value);
  if (parseError) return $err({ msg: "Invalid refresh token format", desc: $stringErr(parseError) });
  const struct = {
    rt: data,
    exp: Date.now() + params.expiry * 1e3,
    aid: params.azureId
  };
  const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, struct, params.key);
  if (error) return $err({ msg: "Failed to encrypt refresh token", desc: `Encryption - ${$stringErr(error)}` });
  params.$updateSecretKey("refreshToken", newSecretKey);
  if (encrypted.length > 4096) {
    return $err({
      msg: "Invalid format",
      desc: `Encrypted refresh token exceeds 4096 characters. Encrypted length: ${encrypted.length}, original length: ${data.length}`
    });
  }
  return $ok({ encrypted });
}
async function $decryptRefreshToken(value, params) {
  const { data: encryptedRefreshToken, error: encryptedRefreshTokenError } = zEncrypted.safeParse(value);
  if (encryptedRefreshTokenError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedRefreshTokenError) });
  const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedRefreshToken, params.key);
  if (error) return $err({ msg: "Failed to decrypt refresh token", desc: `Decryption - ${$stringErr(error)}` });
  const { data: rtStruct, error: rtStructError } = zRtStruct.safeParse(result);
  if (rtStructError) return $err({ msg: "Invalid format", desc: $stringErr(rtStructError) });
  if (rtStruct.exp < Date.now()) {
    return $err({ msg: "Invalid Params", desc: `Refresh token expired at ${new Date(rtStruct.exp).toISOString()}` });
  }
  params.$updateSecretKey("refreshToken", newSecretKey);
  return $ok({ decrypted: rtStruct.rt, azureId: rtStruct.aid });
}
async function $encryptState(value, params) {
  const { data, error: parseError } = zState.safeParse(value);
  if (parseError) return $err({ msg: "Invalid format", desc: $stringErr(parseError) });
  const { encrypted, newSecretKey, error } = await $encryptObj(params.cryptoType, data, params.key);
  if (error) return $err({ msg: "Failed to encrypt state", desc: `Encryption - ${$stringErr(error)}` });
  params.$updateSecretKey("state", newSecretKey);
  return $ok({ encrypted });
}
async function $decryptState(value, params) {
  const { data: encryptedState, error: encryptedStateError } = zEncrypted.safeParse(value);
  if (encryptedStateError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedStateError) });
  const { result, newSecretKey, error } = await $decryptObj(params.cryptoType, encryptedState, params.key);
  if (error) return $err({ msg: "Failed to decrypt state", desc: $stringErr(error) });
  params.$updateSecretKey("state", newSecretKey);
  const { data: state, error: stateError } = zState.safeParse(result);
  if (stateError) return $err({ msg: "Invalid format", desc: $stringErr(stateError) });
  return $ok({ decrypted: state });
}
async function $encryptTicket(ticketId, params) {
  const { data, error: parseError } = zUuid.safeParse(ticketId);
  if (parseError) return $err({ msg: "Invalid format", desc: $stringErr(parseError) });
  const { encrypted, newSecretKey, error } = await $encrypt(params.cryptoType, data, params.key);
  if (error) return $err({ msg: "Failed to encrypt ticket", desc: `Encryption - ${$stringErr(error)}` });
  params.$updateSecretKey("ticket", newSecretKey);
  return $ok({ encrypted });
}
async function $decryptTicket(value, params) {
  const { data, error: encryptedStateError } = zEncrypted.safeParse(value);
  if (encryptedStateError) return $err({ msg: "Invalid format", desc: $stringErr(encryptedStateError) });
  const { result, newSecretKey, error } = await $decrypt(params.cryptoType, data, params.key);
  if (error) return $err({ msg: "Failed to decrypt ticket", desc: `Decryption - ${$stringErr(error)}` });
  params.$updateSecretKey("ticket", newSecretKey);
  const { data: ticketId, error: stateError } = zUuid.safeParse(result);
  if (stateError) return $err({ msg: "Invalid format", desc: $stringErr(stateError) });
  return $ok({ decrypted: ticketId });
}

// src/utils/helpers.ts
var TIME_SKEW = 5 * 60;
function $transformToMsalPrompt(prompt, email) {
  if (email || prompt === "email") return "login";
  if (prompt === "select-account") return "select_account";
  return void 0;
}
async function $mapAndFilter(items, callback) {
  return (await Promise.all(
    items.map(async (item) => {
      try {
        return await callback(item);
      } catch {
        return null;
      }
    })
  )).filter((result) => !!result);
}

// src/core.ts
var OAuthProvider = class {
  /**
   * @param configuration The OAuth configuration object:
   * - `azure`: clientId, tenantId, scopes, clientSecret, B2B apps, and downstream services. Can be an array of Azure configurations.
   * - `frontendUrl`: allowed redirect URIs
   * - `serverCallbackUrl`: your server’s Azure callback endpoint
   * - `encryptionKey`: 32 characters base encryption secret
   * - `advanced`: optional behaviors
   * @throws {OAuthError} if the config fails validation or has duplicate service names
   */
  constructor(configuration) {
    const result = $oauthConfig(configuration);
    if (result.error) throw new OAuthError(result.error);
    this.azures = result.azures;
    this.frontendUrls = result.frontendUrls;
    this.frontendWhitelist = result.frontendWhitelist;
    this.serverCallbackUrl = result.serverCallbackUrl;
    this.baseCookieOptions = result.baseCookieOptions;
    this.encryptionKeys = result.encryptionKeys;
    this.msalCryptoProvider = result.msalCryptoProvider;
    this.jwksClient = result.jwksClient;
    this.settings = result.settings;
  }
  /**
   * Generate an OAuth2 authorization URL for user login (PKCE-backed).
   *
   * @param params (optional) - Parameters to customize the auth URL:
   * - `loginPrompt` (optional) - Override the default prompt (`sso`|`email`|`select-account`)
   * - `email` (optional) - Email address to pre-fill the login form
   * - `frontendUrl` (optional) - Frontend URL override to redirect the user after authentication
   * - `azureId` (optional) - Azure configuration ID to use, relevant if multiple Azure configurations (Defaults to the first one)
   * @returns A result containing the authorization URL and a ticket (which is used for bearer flow only)
   * @throws {OAuthError} if something goes wrong.
   */
  async getAuthUrl(params) {
    const { data: parsedParams, error: paramsError } = zMethods.getAuthUrl.safeParse(params);
    if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
    const { azure, error: azureError } = this.$getAzure({
      azureId: parsedParams.azureId,
      fallbackToDefault: true,
      status: 400
    });
    if (azureError) throw new OAuthError(azureError);
    if (parsedParams.loginPrompt === "email" && !parsedParams.email) {
      throw new OAuthError({ msg: "Invalid params", desc: 'Email is required when loginPrompt is set to "email"' });
    }
    if (parsedParams.frontendUrl && !this.frontendWhitelist.has(new URL(parsedParams.frontendUrl).host)) {
      throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
    }
    const { uuid: ticketId, error: uuidError } = $generateUuid(this.settings.cryptoType);
    if (uuidError) throw new OAuthError(uuidError);
    try {
      const [pkce, { encrypted, error: ticketError }] = await Promise.all([
        this.msalCryptoProvider.generatePkceCodes(),
        this.$encryptToken("ticket", ticketId)
      ]);
      if (ticketError) throw new OAuthError(ticketError);
      const prompt = $transformToMsalPrompt(parsedParams.loginPrompt ?? this.settings.loginPrompt, parsedParams.email);
      const params2 = { nonce: this.msalCryptoProvider.createNewGuid(), loginHint: parsedParams.email, prompt };
      const { encrypted: encryptedState, error: encryptError } = await this.$encryptToken("state", {
        azureId: azure.clientId,
        frontendUrl: parsedParams.frontendUrl ?? this.frontendUrls[0],
        codeVerifier: pkce.verifier,
        ticketId,
        ...params2
      });
      if (encryptError) throw new OAuthError(encryptError);
      const authUrl = await azure.cca.getAuthCodeUrl({
        ...params2,
        state: encryptedState,
        scopes: azure.scopes,
        redirectUri: this.serverCallbackUrl,
        responseMode: "form_post",
        codeChallengeMethod: "S256",
        codeChallenge: pkce.challenge
      });
      if (new URL(authUrl).hostname !== "login.microsoftonline.com") {
        throw new OAuthError({
          msg: "Invalid auth URL",
          desc: "The generated auth URL does not point to Microsoft Entra ID",
          status: 500
        });
      }
      return { authUrl, ticket: encrypted };
    } catch (error) {
      if (error instanceof OAuthError) throw error;
      throw new OAuthError({ msg: "Failed to generate auth URL", desc: `Auth URL Generation - ${$stringErr(error)}` });
    }
  }
  /**
   * Exchange an authorization code for encrypted tokens and metadata.
   *
   * @param params - The parameters containing the authorization code and state.
   * - `code` - The authorization code received from the OAuth flow.
   * - `state` -  The state parameter received from Microsoft.
   * @returns A result containing the access token, refresh token (if available), frontend URL, and MSAL response.
   * @throws {OAuthError} if something goes wrong.
   */
  async getTokenByCode(params) {
    const { data: parsedParams, error: paramsError } = zMethods.getTokenByCode.safeParse(params);
    if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
    const { decrypted: state, error: decryptError } = await this.$decryptToken("state", parsedParams.state);
    if (decryptError) throw new OAuthError(decryptError);
    const { azure, error: azureError } = this.$getAzure({ azureId: state.azureId, status: 400 });
    if (azureError) throw new OAuthError(azureError);
    if (!this.frontendWhitelist.has(new URL(state.frontendUrl).host)) {
      throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
    }
    try {
      const msalResponse = await azure.cca.acquireTokenByCode({
        code: parsedParams.code,
        scopes: azure.scopes,
        redirectUri: this.serverCallbackUrl,
        ...state
      });
      const { encryptedAccessToken, encryptedRefreshToken, error } = await this.$extractTokens(azure, msalResponse);
      if (error) throw new OAuthError(error);
      return {
        accessToken: {
          name: azure.cookiesNames.accessTokenName,
          value: encryptedAccessToken,
          options: this.baseCookieOptions.accessTokenOptions
        },
        refreshToken: encryptedRefreshToken ? {
          name: azure.cookiesNames.refreshTokenName,
          value: encryptedRefreshToken,
          options: this.baseCookieOptions.refreshTokenOptions
        } : null,
        frontendUrl: state.frontendUrl,
        ticketId: state.ticketId,
        msalResponse
      };
    } catch (err) {
      if (err instanceof OAuthError) throw err;
      throw new OAuthError({
        msg: "Failed to get token by code",
        desc: `Token Exchange - ${$stringErr(err)} ,Make sure to check Azure credentials (client ID, tenant ID, client secret) and scopes.`
      });
    }
  }
  /**
   * Build a logout URL and cookie-deletion instructions.
   *
   * @param params (optional) - Parameters to customize the logout URL:
   * - `frontendUrl` (optional) - Frontend URL override to redirect the user after log out
   * - `azureId` (optional) - Azure configuration ID to use, relevant if multiple Azure configurations (Defaults to the first one)
   * @returns A result containing the logout URL and cookie deletion instructions.
   * @throws {OAuthError} if something goes wrong.
   */
  async getLogoutUrl(params) {
    const { data: parsedParams, error: paramsError } = zMethods.getLogoutUrl.safeParse(params);
    if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
    const { azure, error: azureError } = this.$getAzure({
      azureId: parsedParams.azureId,
      fallbackToDefault: true,
      status: 400
    });
    if (azureError) throw new OAuthError(azureError);
    if (parsedParams.frontendUrl && !this.frontendWhitelist.has(new URL(parsedParams.frontendUrl).host)) {
      throw new OAuthError({ msg: "Forbidden", desc: "Unlisted host frontend URL", status: 403 });
    }
    const logoutUrl = new URL(`https://login.microsoftonline.com/${azure.tenantId}/oauth2/v2.0/logout`);
    logoutUrl.searchParams.set("post_logout_redirect_uri", parsedParams.frontendUrl ?? this.frontendUrls[0]);
    return {
      logoutUrl: logoutUrl.toString(),
      deleteAccessToken: {
        name: azure.cookiesNames.accessTokenName,
        value: "",
        options: this.baseCookieOptions.deleteTokenOptions
      },
      deleteRefreshToken: {
        name: azure.cookiesNames.refreshTokenName,
        value: "",
        options: this.baseCookieOptions.deleteTokenOptions
      }
    };
  }
  /**
   * Verify the access token (either encrypted or in JWT format) and extract its payload.
   * Make sure that user access tokens are encrypted and app tokens aren't
   *
   * @param accessToken - The access token string either encrypted or in JWT format
   * @returns A result containing the raw access token, its payload, any injected data, and whether it is an app token.
   * @template T - Type of any injected data in the encrypted token
   */
  async verifyAccessToken(accessToken) {
    const { decrypted, azureId, injectedData, wasEncrypted, error } = await this.$decryptToken(
      "accessToken",
      accessToken
    );
    if (error) return $err({ msg: "Unauthorized", desc: `Token Decryption - ${$stringErr(error)}`, status: 401 });
    const { azure, error: azureError } = this.$getAzure({ azureId });
    if (azureError) return $err(azureError);
    const at = await $verifyJwt({ jwtToken: decrypted, azure, jwksClient: this.jwksClient });
    if (at.error) return $err(at.error);
    if (this.settings.acceptB2BRequests === false && at.meta.isApp === true) {
      return $err({
        msg: "B2B requests not allowed",
        desc: "B2B requests are not allowed, please enable them in the configuration",
        status: 403
      });
    }
    if (at.meta.isApp === wasEncrypted) {
      return $err({
        msg: "Unauthorized",
        desc: "User tokens must be encrypted, app tokens must not be encrypted",
        status: 401
      });
    }
    return $ok({
      rawJwt: decrypted,
      payload: at.payload,
      meta: at.meta,
      injectedData,
      hasInjectedData: !!injectedData
    });
  }
  /**
   * Verifies and uses the refresh token to get new set of access and refresh tokens.
   *
   * @param refreshToken - Encrypted refresh-token value
   * @returns A result containing the new access token, optional new refresh token, the raw access token, its payload, and the MSAL response.
   */
  async tryRefreshTokens(refreshToken) {
    if (!refreshToken) {
      return $err({ msg: "Unauthorized", desc: "Refresh token is required", status: 401 });
    }
    const {
      decrypted: rawRefreshToken,
      azureId,
      error: decryptError
    } = await this.$decryptToken("refreshToken", refreshToken);
    if (decryptError) {
      return $err({ msg: "Unauthorized", desc: `Refresh Token Decryption - ${$stringErr(decryptError)}`, status: 401 });
    }
    const { azure, error: azureError } = this.$getAzure({ azureId });
    if (azureError) return $err(azureError);
    try {
      const msalResponse = await azure.cca.acquireTokenByRefreshToken({
        refreshToken: rawRefreshToken,
        scopes: azure.scopes,
        forceCache: true
      });
      if (!msalResponse) {
        return $err({ msg: "Unauthorized", desc: "Failed to refresh token, no msal response", status: 401 });
      }
      const at = await $verifyJwt({
        jwtToken: msalResponse.accessToken,
        jwksClient: this.jwksClient,
        azure
      });
      if (at.error) {
        return $err({ msg: "Unauthorized", desc: `Access Token Verification - ${$stringErr(at.error)}`, status: 401 });
      }
      const { encryptedAccessToken, encryptedRefreshToken, error } = await this.$extractTokens(azure, msalResponse);
      if (error) return $err({ msg: "Unauthorized", desc: `Extract Tokens - ${$stringErr(error)}`, status: 401 });
      return $ok({
        rawJwt: msalResponse.accessToken,
        payload: at.payload,
        meta: at.meta,
        newAccessToken: {
          name: azure.cookiesNames.accessTokenName,
          value: encryptedAccessToken,
          options: this.baseCookieOptions.accessTokenOptions
        },
        newRefreshToken: encryptedRefreshToken ? {
          name: azure.cookiesNames.refreshTokenName,
          value: encryptedRefreshToken,
          options: this.baseCookieOptions.refreshTokenOptions
        } : null,
        msalResponse
      });
    } catch (err) {
      if (err instanceof OAuthError) return $err(err);
      return $err({ msg: "Unauthorized", desc: `Token Refresh - ${$stringErr(err)}`, status: 401 });
    }
  }
  /**
   * Inject non-sensitive metadata into the access token.
   *
   * @param params - The parameters containing the access token and data to inject.
   * - `accessToken` - The encrypted access token to inject data into.
   * - `data` - The data to inject into the access token.
   * @returns A result containing the new encrypted access token with injected data and the injected data.
   * @template T - Type of the data to inject into the access token.
   */
  async tryInjectData(params) {
    const { decrypted: rawAccessToken, azureId, error } = await this.$decryptToken("accessToken", params.accessToken);
    if (error) return $err(error);
    const { azure, error: azureError } = this.$getAzure({ azureId });
    if (azureError) return $err(azureError);
    const { data: dataToInject, error: dataToInjectError } = zInjectedData.safeParse(params.data);
    if (dataToInjectError) return $err({ msg: "Invalid Params", desc: $stringErr(dataToInjectError) });
    const { encrypted, error: encryptError } = await this.$encryptToken("accessToken", rawAccessToken, {
      azureId,
      expiry: this.settings.cookies.accessTokenExpiry,
      dataToInject
    });
    if (encryptError) return $err(encryptError);
    return $ok({
      newAccessToken: {
        name: azure.cookiesNames.accessTokenName,
        value: encrypted,
        options: this.baseCookieOptions.accessTokenOptions
      },
      injectedData: dataToInject
    });
  }
  /**
   * Decrypts a ticket and returns the ticket ID.
   * Useful for bearer flow.
   *
   * @param ticket - The encrypted ticket string to decrypt (generated by getAuthUrl).
   * @returns A result containing the ticket ID (returned by getTokenByCode).
   */
  async tryDecryptTicket(ticket) {
    const { decrypted, error } = await this.$decryptToken("ticket", ticket);
    if (error) return $err(error);
    return $ok({ ticketId: decrypted });
  }
  async tryGetB2BToken(params) {
    const { data: parsedParams, error: paramsError } = zMethods.tryGetB2BToken.safeParse(params);
    if (paramsError) return $err({ msg: "Invalid Params", desc: $stringErr(paramsError) });
    const { azure, error: azureError } = this.$getAzure({
      azureId: parsedParams.azureId,
      fallbackToDefault: true,
      status: 400
    });
    if (azureError) throw new OAuthError(azureError);
    if (!azure.b2b) return $err({ msg: "Misconfiguration", desc: "B2B apps not configured", status: 500 });
    const apps = parsedParams.apps.map((app) => azure.b2b?.get(app)).filter((app) => !!app);
    if (!apps || apps.length === 0) {
      return $err({ msg: "Invalid Params", desc: "B2B app not found", status: 400 });
    }
    try {
      const results = await $mapAndFilter(apps, async (app) => {
        if (app.token && app.exp > Date.now() / 1e3) {
          return {
            clientId: azure.clientId,
            appName: app.appName,
            appId: app.aud,
            token: app.token,
            msalResponse: app.msalResponse,
            isCached: true,
            expiresAt: app.exp
          };
        }
        const msalResponse = await azure.cca.acquireTokenByClientCredential({ scopes: [app.scope], skipCache: true });
        if (!msalResponse) return null;
        const { clientId, exp, error: audError } = $getExpiry(msalResponse.accessToken);
        if (audError) return null;
        azure.b2b?.set(app.appName, {
          appName: app.appName,
          scope: app.scope,
          token: msalResponse.accessToken,
          exp: exp - TIME_SKEW,
          aud: clientId,
          msalResponse
        });
        return {
          clientId: azure.clientId,
          appName: app.appName,
          appId: clientId,
          token: msalResponse.accessToken,
          msalResponse,
          isCached: false,
          expiresAt: 0
        };
      });
      if (!results || results.length === 0) {
        return $err({ msg: "Internal Server Error", desc: "Failed to get B2B token", status: 500 });
      }
      return $ok("app" in params ? { result: results[0] } : { results });
    } catch (err) {
      if (err instanceof OAuthError) return $err(err);
      return $err({ msg: "Internal Server Error", desc: $stringErr(err), status: 500 });
    }
  }
  async getTokenOnBehalfOf(params) {
    const { data: parsedParams, error: paramsError } = zMethods.getTokenOnBehalfOf.safeParse(params);
    if (paramsError) throw new OAuthError({ msg: "Invalid Params", desc: $stringErr(paramsError) });
    const { azure, error: azureError } = this.$getAzure({
      azureId: parsedParams.azureId,
      fallbackToDefault: true,
      status: 400
    });
    if (azureError) throw new OAuthError(azureError);
    if (!azure.obo) throw new OAuthError({ msg: "Misconfiguration", desc: "OBO services not configured", status: 500 });
    const services = parsedParams.services.map((service) => azure.obo?.get(service)).filter((service) => !!service);
    if (!services || services.length === 0) {
      throw new OAuthError({ msg: "Invalid Params", desc: "OBO service not found", status: 400 });
    }
    const { decrypted: rawAccessToken, error } = await this.$decryptToken("accessToken", parsedParams.accessToken);
    if (error) throw new OAuthError(error);
    try {
      const results = await $mapAndFilter(services, async (service) => {
        const msalResponse = await azure.cca.acquireTokenOnBehalfOf({
          oboAssertion: rawAccessToken,
          scopes: [service.scope],
          skipCache: true
        });
        if (!msalResponse) return null;
        const { clientId, error: audError } = $getExpiry(msalResponse.accessToken);
        if (audError) return null;
        const { encrypted, error: error2 } = await this.$encryptToken("accessToken", msalResponse.accessToken, {
          azureId: clientId,
          expiry: service.atExp,
          cryptoType: service.cryptoType,
          otherSecretKey: `access-token-${service.encryptionKey}`
        });
        if (error2) return null;
        const cookieOptions = $getCookieOptions({
          secure: service.isSecure,
          sameSite: service.isSamesite,
          timeUnit: this.settings.cookies.timeUnit,
          atExp: service.atExp,
          rtExp: 0
          // No refresh token
        });
        const { accessTokenName } = $getCookieNames(clientId, service.isSecure);
        return {
          clientId: azure.clientId,
          serviceName: service.serviceName,
          serviceId: clientId,
          accessToken: { name: accessTokenName, value: encrypted, options: cookieOptions.accessTokenOptions },
          msalResponse
        };
      });
      if (!results || results.length === 0) {
        throw new OAuthError({ msg: "Internal Server Error", desc: "Failed to get OBO token", status: 500 });
      }
      return "service" in params ? { result: results[0] } : { results };
    } catch (error2) {
      if (error2 instanceof OAuthError) throw error2;
      throw new OAuthError({ msg: "Internal Server Error", desc: $stringErr(error2), status: 500 });
    }
  }
  $getAzure({
    azureId,
    fallbackToDefault = false,
    status = 401
  }) {
    const azure = azureId ? this.azures.find((azure2) => azure2.clientId === azureId) : void 0;
    if (azure) return $ok({ azure });
    if (fallbackToDefault) return $ok({ azure: this.azures[0] });
    return $err({
      msg: status === 401 ? "Unauthorized" : "Invalid Params",
      desc: "Azure configuration not found for the given client ID",
      status
    });
  }
  /** Extracts and encrypts both tokens */
  async $extractTokens(azure, msalResponse) {
    const [accessTokenRes, refreshTokenRes] = await Promise.all([
      this.$encryptToken("accessToken", msalResponse.accessToken, {
        azureId: azure.clientId,
        expiry: this.settings.cookies.accessTokenExpiry
      }),
      this.$obtainRefreshToken(azure, msalResponse)
    ]);
    if (accessTokenRes.error) return $err(accessTokenRes.error);
    return $ok({
      encryptedAccessToken: accessTokenRes.encrypted,
      encryptedRefreshToken: refreshTokenRes.encrypted ?? null
    });
  }
  /** Extracts the refresh token from the cache that msal created, and removes the account from the cache. */
  async $obtainRefreshToken(azure, msalResponse) {
    try {
      const cache = azure.cca.getTokenCache();
      const serializedCache = JSON.parse(cache.serialize());
      const refreshTokens = serializedCache?.RefreshToken ?? {};
      const keys = typeof refreshTokens === "object" ? Object.keys(refreshTokens) : [];
      const refreshTokenKey = keys.find((key) => key.startsWith(msalResponse.uniqueId));
      if (msalResponse.account) await cache.removeAccount(msalResponse.account);
      const refreshToken = refreshTokenKey ? refreshTokens[refreshTokenKey].secret : void 0;
      return await this.$encryptToken("refreshToken", refreshToken, {
        azureId: azure.clientId,
        expiry: this.settings.cookies.refreshTokenExpiry
      });
    } catch {
      return $err({
        msg: "Failed to obtain refresh token",
        desc: "Failed to obtain refresh token from MSAL cache",
        status: 500
      });
    }
  }
  /** Updates the secret key for a specific token type if it is a string. */
  $updateSecretKey(keyType, secretKey) {
    if (this.settings.cryptoType !== "web-api" || !secretKey) return;
    const currentKey = this.encryptionKeys[keyType];
    if (typeof currentKey === "string") {
      this.encryptionKeys[keyType] = secretKey;
    }
  }
  async $encryptToken(keyType, value, params) {
    const baseParams = {
      key: params?.otherSecretKey ?? this.encryptionKeys[keyType],
      cryptoType: params?.cryptoType ?? this.settings.cryptoType,
      $updateSecretKey: this.$updateSecretKey.bind(this)
    };
    switch (keyType) {
      case "accessToken":
        return $encryptAccessToken(value, {
          ...baseParams,
          expiry: params?.expiry,
          azureId: params?.azureId,
          isOtherKey: !!params?.otherSecretKey,
          dataToInject: params?.dataToInject,
          disableCompression: this.settings.disableCompression
        });
      case "refreshToken":
        return $encryptRefreshToken(value, {
          ...baseParams,
          expiry: params?.expiry,
          azureId: params?.azureId
        });
      case "state":
        return $encryptState(value, baseParams);
      case "ticket":
        return $encryptTicket(value, baseParams);
      default:
        return $err({
          msg: "Invalid encryption key type",
          desc: `Key type '${keyType}' is not supported for encryption`
        });
    }
  }
  async $decryptToken(keyType, value) {
    const baseParams = {
      key: this.encryptionKeys[keyType],
      cryptoType: this.settings.cryptoType,
      $updateSecretKey: this.$updateSecretKey.bind(this)
    };
    switch (keyType) {
      case "accessToken":
        return $decryptAccessToken(value, baseParams);
      case "refreshToken":
        return $decryptRefreshToken(value, baseParams);
      case "state":
        return $decryptState(value, baseParams);
      case "ticket":
        return $decryptTicket(value, baseParams);
      default:
        return $err({
          msg: "Invalid encryption key type",
          desc: `Key type '${keyType}' is not supported for encryption`
        });
    }
  }
};

exports.$err = $err;
exports.$getExpiry = $getExpiry;
exports.$jwtClientConfig = $jwtClientConfig;
exports.$mapAndFilter = $mapAndFilter;
exports.$ok = $ok;
exports.$stringErr = $stringErr;
exports.$verifyJwt = $verifyJwt;
exports.OAuthError = OAuthError;
exports.OAuthProvider = OAuthProvider;
exports.TIME_SKEW = TIME_SKEW;
exports.zJwt = zJwt;
exports.zMethods = zMethods;
//# sourceMappingURL=chunk-7MTPXR5V.cjs.map
//# sourceMappingURL=chunk-7MTPXR5V.cjs.map