@azure/msal-node-extensions
Version:
 
1,380 lines (1,350 loc) • 83.7 kB
JavaScript
/*! @azure/msal-node-extensions v5.2.2 2026-05-19 */
'use strict';
'use strict';
var fs = require('fs');
var process$1 = require('process');
var path = require('path');
var module$1 = require('module');
var keytar = require('keytar');
var msalNodeRuntime = require('@azure/msal-node-runtime');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const Constants = {
/**
* An existing file was the target of an operation that required that the target not exist
*/
EEXIST_ERROR: "EEXIST",
/**
* No such file or directory: Commonly raised by fs operations to indicate that a component
* of the specified pathname does not exist. No entity (file or directory) could be found
* by the given path
*/
ENOENT_ERROR: "ENOENT",
/**
* Operation not permitted. An attempt was made to perform an operation that requires
* elevated privileges.
*/
EPERM_ERROR: "EPERM",
/**
* Default service name for using MSAL Keytar
*/
DEFAULT_SERVICE_NAME: "msal-node-extensions",
/**
* Test data used to verify underlying persistence mechanism
*/
PERSISTENCE_TEST_DATA: "Dummy data to verify underlying persistence mechanism",
/**
* This is the value of a the guid if the process is being ran by the root user
*/
LINUX_ROOT_USER_GUID: 0,
/**
* List of environment variables
*/
ENVIRONMENT: {
HOME: "HOME",
LOGNAME: "LOGNAME",
USER: "USER",
LNAME: "LNAME",
USERNAME: "USERNAME",
PLATFORM: "platform",
LOCAL_APPLICATION_DATA: "LOCALAPPDATA",
},
// Name of the default cache file
DEFAULT_CACHE_FILE_NAME: "cache.json",
};
const Platform = {
WINDOWS: "win32",
LINUX: "linux",
MACOS: "darwin",
};
const ErrorCodes = {
INTERATION_REQUIRED_ERROR_CODE: "interaction_required",
SERVER_UNAVAILABLE: "server_unavailable",
UNKNOWN: "unknown_error",
};
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Error thrown when trying to write MSAL cache to persistence.
*/
class PersistenceError extends Error {
constructor(errorCode, errorMessage) {
const errorString = errorMessage
? `${errorCode}: ${errorMessage}`
: errorCode;
super(errorString);
Object.setPrototypeOf(this, PersistenceError.prototype);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.name = "PersistenceError";
}
/**
* Error thrown when trying to access the file system.
*/
static createFileSystemError(errorCode, errorMessage) {
return new PersistenceError(errorCode, errorMessage);
}
/**
* Error thrown when trying to write, load, or delete data from secret service on linux.
* Libsecret is used to access secret service.
*/
static createLibSecretError(errorMessage) {
return new PersistenceError("GnomeKeyringError", errorMessage);
}
/**
* Error thrown when trying to write, load, or delete data from keychain on macOs.
*/
static createKeychainPersistenceError(errorMessage) {
return new PersistenceError("KeychainError", errorMessage);
}
/**
* Error thrown when trying to encrypt or decrypt data using DPAPI on Windows.
*/
static createFilePersistenceWithDPAPIError(errorMessage) {
return new PersistenceError("DPAPIEncryptedFileError", errorMessage);
}
/**
* Error thrown when using the cross platform lock.
*/
static createCrossPlatformLockError(errorMessage) {
return new PersistenceError("CrossPlatformLockError", errorMessage);
}
/**
* Throw cache persistence error
*
* @param errorMessage string
* @returns PersistenceError
*/
static createCachePersistenceError(errorMessage) {
return new PersistenceError("CachePersistenceError", errorMessage);
}
/**
* Throw unsupported error
*
* @param errorMessage string
* @returns PersistenceError
*/
static createNotSupportedError(errorMessage) {
return new PersistenceError("NotSupportedError", errorMessage);
}
/**
* Throw persistence not verified error
*
* @param errorMessage string
* @returns PersistenceError
*/
static createPersistenceNotVerifiedError(errorMessage) {
return new PersistenceError("PersistenceNotVerifiedError", errorMessage);
}
/**
* Throw persistence creation validation error
*
* @param errorMessage string
* @returns PersistenceError
*/
static createPersistenceNotValidatedError(errorMessage) {
return new PersistenceError("PersistenceNotValidatedError", errorMessage);
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Returns whether or not the given object is a Node.js error
*/
const isNodeError = (error) => {
return !!error && typeof error === "object" && error.hasOwnProperty("code");
};
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Cross-process lock that works on all platforms.
*/
class CrossPlatformLock {
constructor(lockFilePath, logger, lockOptions) {
this.lockFilePath = lockFilePath;
this.retryNumber = lockOptions ? lockOptions.retryNumber : 500;
this.retryDelay = lockOptions ? lockOptions.retryDelay : 100;
this.logger = logger;
}
/**
* Locks cache from read or writes by creating file with same path and name as
* cache file but with .lockfile extension. If another process has already created
* the lockfile, will back off and retry based on configuration settings set by CrossPlatformLockOptions
*/
async lock() {
for (let tryCount = 0; tryCount < this.retryNumber; tryCount++) {
try {
this.logger.info(`Pid ${process$1.pid} trying to acquire lock`, "");
this.lockFileHandle = await fs.promises.open(this.lockFilePath, "wx+");
this.logger.info(`Pid ${process$1.pid} acquired lock`, "");
await this.lockFileHandle.write(process$1.pid.toString());
return;
}
catch (err) {
if (isNodeError(err)) {
if (err.code === Constants.EEXIST_ERROR ||
err.code === Constants.EPERM_ERROR) {
this.logger.info(err.message, "");
await this.sleep(this.retryDelay);
}
else {
this.logger.error(`${process$1.pid} was not able to acquire lock. Ran into error: ${err.message}`, "");
throw PersistenceError.createCrossPlatformLockError(err.message);
}
}
else {
throw err;
}
}
}
this.logger.error(`${process$1.pid} was not able to acquire lock. Exceeded amount of retries set in the options`, "");
throw PersistenceError.createCrossPlatformLockError("Not able to acquire lock. Exceeded amount of retries set in options");
}
/**
* unlocks cache file by deleting .lockfile.
*/
async unlock() {
try {
if (this.lockFileHandle) {
// if we have a file handle to the .lockfile, delete lock file
await fs.promises.unlink(this.lockFilePath);
await this.lockFileHandle.close();
this.logger.info("lockfile deleted", "");
}
else {
this.logger.warning("lockfile handle does not exist, so lockfile could not be deleted", "");
}
}
catch (err) {
if (isNodeError(err)) {
if (err.code === Constants.ENOENT_ERROR) {
this.logger.info("Tried to unlock but lockfile does not exist", "");
}
else {
this.logger.error(`${process$1.pid} was not able to release lock. Ran into error: ${err.message}`, "");
throw PersistenceError.createCrossPlatformLockError(err.message);
}
}
else {
throw err;
}
}
}
sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* MSAL cache plugin which enables callers to write the MSAL cache to disk on Windows,
* macOs, and Linux.
*
* - Persistence can be one of:
* - FilePersistence: Writes and reads from an unencrypted file. Can be used on Windows,
* macOs, or Linux.
* - FilePersistenceWithDataProtection: Used on Windows, writes and reads from file encrypted
* with windows dpapi-addon.
* - KeychainPersistence: Used on macOs, writes and reads from keychain.
* - LibSecretPersistence: Used on linux, writes and reads from secret service API. Requires
* libsecret be installed.
*/
class PersistenceCachePlugin {
constructor(persistence, lockOptions) {
this.persistence = persistence;
// initialize logger
this.logger = persistence.getLogger();
// create file lock
this.lockFilePath = `${this.persistence.getFilePath()}.lockfile`;
this.crossPlatformLock = new CrossPlatformLock(this.lockFilePath, this.logger, lockOptions);
// initialize default values
this.lastSync = 0;
this.currentCache = null;
}
/**
* Reads from storage and saves an in-memory copy. If persistence has not been updated
* since last time data was read, in memory copy is used.
*
* If cacheContext.cacheHasChanged === true, then file lock is created and not deleted until
* afterCacheAccess() is called, to prevent the cache file from changing in between
* beforeCacheAccess() and afterCacheAccess().
*/
async beforeCacheAccess(cacheContext) {
this.logger.info("Executing before cache access", "");
const reloadNecessary = await this.persistence.reloadNecessary(this.lastSync);
if (!reloadNecessary && this.currentCache !== null) {
if (cacheContext.cacheHasChanged) {
this.logger.verbose("Cache context has changed", "");
await this.crossPlatformLock.lock();
}
return;
}
try {
this.logger.info(`Reload necessary. Last sync time: ${this.lastSync}`, "");
await this.crossPlatformLock.lock();
this.currentCache = await this.persistence.load();
this.lastSync = new Date().getTime();
if (this.currentCache) {
cacheContext.tokenCache.deserialize(this.currentCache);
}
else {
this.logger.info("Cache empty.", "");
}
this.logger.info(`Last sync time updated to: ${this.lastSync}`, "");
}
finally {
if (!cacheContext.cacheHasChanged) {
await this.crossPlatformLock.unlock();
this.logger.info(`Pid ${process$1.pid} released lock`, "");
}
else {
this.logger.info(`Pid ${process$1.pid} beforeCacheAccess did not release lock`, "");
}
}
}
/**
* Writes to storage if MSAL in memory copy of cache has been changed.
*/
async afterCacheAccess(cacheContext) {
this.logger.info("Executing after cache access", "");
try {
if (cacheContext.cacheHasChanged) {
this.logger.info("Msal in-memory cache has changed. Writing changes to persistence", "");
this.currentCache = cacheContext.tokenCache.serialize();
await this.persistence.save(this.currentCache);
}
else {
this.logger.info("Msal in-memory cache has not changed. Did not write to persistence", "");
}
}
finally {
await this.crossPlatformLock.unlock();
this.logger.info(`Pid ${process$1.pid} afterCacheAccess released lock`, "");
}
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/**
* we considered making this "enum" in the request instead of string, however it looks like the allowed list of
* prompt values kept changing over past couple of years. There are some undocumented prompt values for some
* internal partners too, hence the choice of generic "string" type instead of the "enum"
*/
const PromptValue = {
LOGIN: "login",
SELECT_ACCOUNT: "select_account",
NONE: "none",
CREATE: "create"};
/**
* Separators used in cache
*/
const CACHE_KEY_SEPARATOR = "-";
const SERVER_TELEM_SCHEMA_VERSION = 5;
const SERVER_TELEM_MAX_LAST_HEADER_BYTES = 330; // ESTS limit is 350B, set to 330 to provide a 20B buffer,
const SERVER_TELEM_MAX_CACHED_ERRORS = 50; // Limit the number of errors that can be stored to prevent uncontrolled size gains
const SERVER_TELEM_CACHE_KEY = "server-telemetry";
const SERVER_TELEM_CATEGORY_SEPARATOR = "|";
const SERVER_TELEM_VALUE_SEPARATOR = ",";
const SERVER_TELEM_OVERFLOW_TRUE = "1";
const SERVER_TELEM_OVERFLOW_FALSE = "0";
const SERVER_TELEM_UNKNOWN_ERROR = "unknown_error";
/**
* Type of the authentication request
*/
const AuthenticationScheme = {
BEARER: "Bearer",
POP: "pop"};
/**
* Specifies the reason for fetching the access token from the identity provider
*/
const CacheOutcome = {
// When a token is found in the cache or the cache is not supposed to be hit when making the request
NOT_APPLICABLE: "0"};
/*! @azure/msal-common v16.6.2 2026-05-19 */
const X_CLIENT_EXTRA_SKU = "x-client-xtra-sku";
const RESOURCE = "resource";
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
function getDefaultErrorMessage(code) {
return `See https://aka.ms/msal.js.errors#${code} for details`;
}
/**
* General error class thrown by the MSAL.js library.
*/
class AuthError extends Error {
constructor(errorCode, errorMessage, suberror) {
const message = errorMessage ||
(errorCode ? getDefaultErrorMessage(errorCode) : "");
const errorString = message ? `${errorCode}: ${message}` : errorCode;
super(errorString);
Object.setPrototypeOf(this, AuthError.prototype);
this.errorCode = errorCode || "";
this.errorMessage = message || "";
this.subError = suberror || "";
this.name = "AuthError";
}
setCorrelationId(correlationId) {
this.correlationId = correlationId;
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Error thrown when there is an error in configuration of the MSAL.js library.
*/
class ClientConfigurationError extends AuthError {
constructor(errorCode) {
super(errorCode);
this.name = "ClientConfigurationError";
Object.setPrototypeOf(this, ClientConfigurationError.prototype);
}
}
function createClientConfigurationError(errorCode) {
return new ClientConfigurationError(errorCode);
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* ClientAuthErrorMessage class containing string constants used by error codes and messages.
*/
/**
* Error thrown when there is an error in the client code running on the browser.
*/
class ClientAuthError extends AuthError {
constructor(errorCode, additionalMessage) {
super(errorCode, additionalMessage);
this.name = "ClientAuthError";
Object.setPrototypeOf(this, ClientAuthError.prototype);
}
}
function createClientAuthError(errorCode, additionalMessage) {
return new ClientAuthError(errorCode, additionalMessage);
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
const untrustedAuthority = "untrusted_authority";
/*! @azure/msal-common v16.6.2 2026-05-19 */
const noAccountFound = "no_account_found";
const noNetworkConnectivity = "no_network_connectivity";
const userCanceled = "user_canceled";
const platformBrokerError = "platform_broker_error";
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Log message level.
*/
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["Error"] = 0] = "Error";
LogLevel[LogLevel["Warning"] = 1] = "Warning";
LogLevel[LogLevel["Info"] = 2] = "Info";
LogLevel[LogLevel["Verbose"] = 3] = "Verbose";
LogLevel[LogLevel["Trace"] = 4] = "Trace";
})(LogLevel || (LogLevel = {}));
// Shared cache state for better minification - using Map's insertion order for LRU
const CACHE_CAPACITY = 50;
const MAX_LOGS_PER_CORRELATION = 500;
const correlationCache = new Map();
/**
* Mark correlation ID as recently used by moving it to end of Map
* @param correlationId
* @param {CorrelationLogData} data
*/
function markAsRecentlyUsed(correlationId, data) {
correlationCache.delete(correlationId);
correlationCache.set(correlationId, data);
}
/**
* Add log message to cache for specific correlation ID
* @param correlationId
* @param {LoggedMessage} loggedMessage
*/
function addLogToCache(correlationId, loggedMessage) {
const currentTime = Date.now();
let data = correlationCache.get(correlationId);
if (data) {
// Mark as recently used
markAsRecentlyUsed(correlationId, data);
}
else {
// Create new entry
data = { logs: [], firstEventTime: currentTime };
correlationCache.set(correlationId, data);
// Remove LRU (first entry) if capacity exceeded
if (correlationCache.size > CACHE_CAPACITY) {
const firstKey = correlationCache.keys().next().value;
if (firstKey) {
correlationCache.delete(firstKey);
}
}
}
// Add log to the data, maintaining max logs per correlation
data.logs.push({
...loggedMessage,
milliseconds: currentTime - data.firstEventTime,
});
if (data.logs.length > MAX_LOGS_PER_CORRELATION) {
data.logs.shift(); // Remove oldest log
}
}
/**
* Checks if a string is already a hashed logging string (6 alphanumeric characters)
*/
function isHashedString(str) {
if (str.length !== 6) {
return false;
}
for (let i = 0; i < str.length; i++) {
const char = str[i];
const isAlphaNumeric = (char >= "a" && char <= "z") ||
(char >= "A" && char <= "Z") ||
(char >= "0" && char <= "9");
if (!isAlphaNumeric) {
return false;
}
}
return true;
}
/**
* Class which facilitates logging of messages to a specific place.
*/
class Logger {
constructor(loggerOptions, packageName, packageVersion) {
// Current log level, defaults to info.
this.level = LogLevel.Info;
const defaultLoggerCallback = () => {
return;
};
const setLoggerOptions = loggerOptions || Logger.createDefaultLoggerOptions();
this.localCallback =
setLoggerOptions.loggerCallback || defaultLoggerCallback;
this.piiLoggingEnabled = setLoggerOptions.piiLoggingEnabled || false;
this.level =
typeof setLoggerOptions.logLevel === "number"
? setLoggerOptions.logLevel
: LogLevel.Info;
this.packageName = packageName || "";
this.packageVersion = packageVersion || "";
}
static createDefaultLoggerOptions() {
return {
loggerCallback: () => {
// allow users to not set loggerCallback
},
piiLoggingEnabled: false,
logLevel: LogLevel.Info,
};
}
/**
* Create new Logger with existing configurations.
*/
clone(packageName, packageVersion) {
return new Logger({
loggerCallback: this.localCallback,
piiLoggingEnabled: this.piiLoggingEnabled,
logLevel: this.level,
}, packageName, packageVersion);
}
/**
* Log message with required options.
*/
logMessage(logMessage, options) {
const correlationId = options.correlationId;
const isHashedInput = isHashedString(logMessage);
if (isHashedInput) {
const loggedMessage = {
hash: logMessage,
level: options.logLevel,
containsPii: options.containsPii || false,
milliseconds: 0, // Will be calculated in addLogToCache
};
addLogToCache(correlationId, loggedMessage);
}
if (options.logLevel > this.level ||
(!this.piiLoggingEnabled && options.containsPii)) {
return;
}
const timestamp = new Date().toUTCString();
// Add correlationId to logs if set, correlationId provided on log messages take precedence
const logHeader = `[${timestamp}] : [${correlationId}]`;
const log = `${logHeader} : ${this.packageName}@${this.packageVersion} : ${LogLevel[options.logLevel]} - ${logMessage}`;
this.executeCallback(options.logLevel, log, options.containsPii || false);
}
/**
* Execute callback with message.
*/
executeCallback(level, message, containsPii) {
if (this.localCallback) {
this.localCallback(level, message, containsPii);
}
}
/**
* Logs error messages.
*/
error(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Error,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs error messages with PII.
*/
errorPii(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Error,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs warning messages.
*/
warning(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs warning messages with PII.
*/
warningPii(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Warning,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs info messages.
*/
info(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs info messages with PII.
*/
infoPii(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Info,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs verbose messages.
*/
verbose(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs verbose messages with PII.
*/
verbosePii(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Verbose,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Logs trace messages.
*/
trace(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: false,
correlationId: correlationId,
});
}
/**
* Logs trace messages with PII.
*/
tracePii(message, correlationId) {
this.logMessage(message, {
logLevel: LogLevel.Trace,
containsPii: true,
correlationId: correlationId,
});
}
/**
* Returns whether PII Logging is enabled or not.
*/
isPiiLoggingEnabled() {
return this.piiLoggingEnabled || false;
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/**
* Convert seconds to JS Date object. Seconds can be in a number or string format or undefined (will still return a date).
* @param seconds
*/
function toDateFromSeconds(seconds) {
if (seconds) {
return new Date(Number(seconds) * 1000);
}
return new Date();
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/**
* Error thrown when user interaction is required.
*/
class InteractionRequiredAuthError extends AuthError {
constructor(errorCode, errorMessage, subError, timestamp, traceId, correlationId, claims, errorNo) {
super(errorCode, errorMessage, subError);
Object.setPrototypeOf(this, InteractionRequiredAuthError.prototype);
this.timestamp = timestamp || "";
this.traceId = traceId || "";
this.correlationId = correlationId || "";
this.claims = claims || "";
this.name = "InteractionRequiredAuthError";
this.errorNo = errorNo;
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Error thrown when there is an error with the server code, for example, unavailability.
*/
class ServerError extends AuthError {
constructor(errorCode, errorMessage, subError, errorNo, status) {
super(errorCode, errorMessage, subError);
this.name = "ServerError";
this.errorNo = errorNo;
this.status = status;
Object.setPrototypeOf(this, ServerError.prototype);
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Converts a numeric tag from MSAL Runtime to a 5-character string representation.
* Tags are encoded as 30-bit values (6 bits per character) using a custom symbol space.
* @param tag - The numeric tag to convert
* @returns The string representation of the tag
*/
function tagToString(tag) {
if (tag === 0) {
return "UNTAG";
}
const tagSymbolSpace = "abcdefghijklmnopqrstuvwxyz0123456789****************************";
let tagBuffer = "*****";
const chars = [
tagSymbolSpace[(tag >> 24) & 0x3f],
tagSymbolSpace[(tag >> 18) & 0x3f],
tagSymbolSpace[(tag >> 12) & 0x3f],
tagSymbolSpace[(tag >> 6) & 0x3f],
tagSymbolSpace[(tag >> 0) & 0x3f],
];
tagBuffer = chars.join("");
return tagBuffer;
}
/**
* Error class for MSAL Runtime errors that preserves detailed broker information
*/
class PlatformBrokerError extends AuthError {
constructor(errorStatus, errorContext, errorCode, errorTag) {
const tagString = tagToString(errorTag);
const enhancedErrorContext = errorContext
? `${errorContext} (Error Code: ${errorCode}, Tag: ${tagString})`
: `(Error Code: ${errorCode}, Tag: ${tagString})`;
super(errorStatus, enhancedErrorContext);
this.name = "PlatformBrokerError";
this.statusCode = errorCode;
this.tag = tagString;
Object.setPrototypeOf(this, PlatformBrokerError.prototype);
}
}
/*! @azure/msal-common v16.6.2 2026-05-19 */
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
const skuGroupSeparator = ",";
const skuValueSeparator = "|";
function makeExtraSkuString(params) {
const { skus, libraryName, libraryVersion, extensionName, extensionVersion, } = params;
const skuMap = new Map([
[0, [libraryName, libraryVersion]],
[2, [extensionName, extensionVersion]],
]);
let skuArr = [];
if (skus?.length) {
skuArr = skus.split(skuGroupSeparator);
// Ignore invalid input sku param
if (skuArr.length < 4) {
return skus;
}
}
else {
skuArr = Array.from({ length: 4 }, () => skuValueSeparator);
}
skuMap.forEach((value, key) => {
if (value.length === 2 && value[0]?.length && value[1]?.length) {
setSku({
skuArr,
index: key,
skuName: value[0],
skuVersion: value[1],
});
}
});
return skuArr.join(skuGroupSeparator);
}
function setSku(params) {
const { skuArr, index, skuName, skuVersion } = params;
if (index >= skuArr.length) {
return;
}
skuArr[index] = [skuName, skuVersion].join(skuValueSeparator);
}
/** @internal */
class ServerTelemetryManager {
constructor(telemetryRequest, cacheManager) {
this.cacheOutcome = CacheOutcome.NOT_APPLICABLE;
this.cacheManager = cacheManager;
this.apiId = telemetryRequest.apiId;
this.correlationId = telemetryRequest.correlationId;
this.wrapperSKU = telemetryRequest.wrapperSKU || "";
this.wrapperVer = telemetryRequest.wrapperVer || "";
this.telemetryCacheKey =
SERVER_TELEM_CACHE_KEY +
CACHE_KEY_SEPARATOR +
telemetryRequest.clientId;
}
/**
* API to add MSER Telemetry to request
*/
generateCurrentRequestHeaderValue() {
const request = `${this.apiId}${SERVER_TELEM_VALUE_SEPARATOR}${this.cacheOutcome}`;
const platformFieldsArr = [this.wrapperSKU, this.wrapperVer];
const nativeBrokerErrorCode = this.getNativeBrokerErrorCode();
if (nativeBrokerErrorCode?.length) {
platformFieldsArr.push(`broker_error=${nativeBrokerErrorCode}`);
}
const platformFields = platformFieldsArr.join(SERVER_TELEM_VALUE_SEPARATOR);
const regionDiscoveryFields = this.getRegionDiscoveryFields();
const requestWithRegionDiscoveryFields = [
request,
regionDiscoveryFields,
].join(SERVER_TELEM_VALUE_SEPARATOR);
return [
SERVER_TELEM_SCHEMA_VERSION,
requestWithRegionDiscoveryFields,
platformFields,
].join(SERVER_TELEM_CATEGORY_SEPARATOR);
}
/**
* API to add MSER Telemetry for the last failed request
*/
generateLastRequestHeaderValue() {
const lastRequests = this.getLastRequests();
const maxErrors = ServerTelemetryManager.maxErrorsToSend(lastRequests);
const failedRequests = lastRequests.failedRequests
.slice(0, 2 * maxErrors)
.join(SERVER_TELEM_VALUE_SEPARATOR);
const errors = lastRequests.errors
.slice(0, maxErrors)
.join(SERVER_TELEM_VALUE_SEPARATOR);
const errorCount = lastRequests.errors.length;
// Indicate whether this header contains all data or partial data
const overflow = maxErrors < errorCount
? SERVER_TELEM_OVERFLOW_TRUE
: SERVER_TELEM_OVERFLOW_FALSE;
const platformFields = [errorCount, overflow].join(SERVER_TELEM_VALUE_SEPARATOR);
return [
SERVER_TELEM_SCHEMA_VERSION,
lastRequests.cacheHits,
failedRequests,
errors,
platformFields,
].join(SERVER_TELEM_CATEGORY_SEPARATOR);
}
/**
* API to cache token failures for MSER data capture
* @param error
*/
cacheFailedRequest(error) {
const lastRequests = this.getLastRequests();
if (lastRequests.errors.length >=
SERVER_TELEM_MAX_CACHED_ERRORS) {
// Remove a cached error to make room, first in first out
lastRequests.failedRequests.shift(); // apiId
lastRequests.failedRequests.shift(); // correlationId
lastRequests.errors.shift();
}
lastRequests.failedRequests.push(this.apiId, this.correlationId);
if (error instanceof Error && !!error && error.toString()) {
if (error instanceof AuthError) {
if (error.subError) {
lastRequests.errors.push(error.subError);
}
else if (error.errorCode) {
lastRequests.errors.push(error.errorCode);
}
else {
lastRequests.errors.push(error.toString());
}
}
else {
lastRequests.errors.push(error.toString());
}
}
else {
lastRequests.errors.push(SERVER_TELEM_UNKNOWN_ERROR);
}
this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId);
return;
}
/**
* Update server telemetry cache entry by incrementing cache hit counter
*/
incrementCacheHits() {
const lastRequests = this.getLastRequests();
lastRequests.cacheHits += 1;
this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId);
return lastRequests.cacheHits;
}
/**
* Get the server telemetry entity from cache or initialize a new one
*/
getLastRequests() {
const initialValue = {
failedRequests: [],
errors: [],
cacheHits: 0,
};
const lastRequests = this.cacheManager.getServerTelemetry(this.telemetryCacheKey, this.correlationId);
return lastRequests || initialValue;
}
/**
* Remove server telemetry cache entry
*/
clearTelemetryCache() {
const lastRequests = this.getLastRequests();
const numErrorsFlushed = ServerTelemetryManager.maxErrorsToSend(lastRequests);
const errorCount = lastRequests.errors.length;
if (numErrorsFlushed === errorCount) {
// All errors were sent on last request, clear Telemetry cache
this.cacheManager.removeItem(this.telemetryCacheKey, this.correlationId);
}
else {
// Partial data was flushed to server, construct a new telemetry cache item with errors that were not flushed
const serverTelemEntity = {
failedRequests: lastRequests.failedRequests.slice(numErrorsFlushed * 2),
errors: lastRequests.errors.slice(numErrorsFlushed),
cacheHits: 0,
};
this.cacheManager.setServerTelemetry(this.telemetryCacheKey, serverTelemEntity, this.correlationId);
}
}
/**
* Returns the maximum number of errors that can be flushed to the server in the next network request
* @param serverTelemetryEntity
*/
static maxErrorsToSend(serverTelemetryEntity) {
let i;
let maxErrors = 0;
let dataSize = 0;
const errorCount = serverTelemetryEntity.errors.length;
for (i = 0; i < errorCount; i++) {
// failedRequests parameter contains pairs of apiId and correlationId, multiply index by 2 to preserve pairs
const apiId = serverTelemetryEntity.failedRequests[2 * i] || "";
const correlationId = serverTelemetryEntity.failedRequests[2 * i + 1] || "";
const errorCode = serverTelemetryEntity.errors[i] || "";
// Count number of characters that would be added to header, each character is 1 byte. Add 3 at the end to account for separators
dataSize +=
apiId.toString().length +
correlationId.toString().length +
errorCode.length +
3;
if (dataSize < SERVER_TELEM_MAX_LAST_HEADER_BYTES) {
// Adding this entry to the header would still keep header size below the limit
maxErrors += 1;
}
else {
break;
}
}
return maxErrors;
}
/**
* Get the region discovery fields
*
* @returns string
*/
getRegionDiscoveryFields() {
const regionDiscoveryFields = [];
regionDiscoveryFields.push(this.regionUsed || "");
regionDiscoveryFields.push(this.regionSource || "");
regionDiscoveryFields.push(this.regionOutcome || "");
return regionDiscoveryFields.join(",");
}
/**
* Update the region discovery metadata
*
* @param regionDiscoveryMetadata
* @returns void
*/
updateRegionDiscoveryMetadata(regionDiscoveryMetadata) {
this.regionUsed = regionDiscoveryMetadata.region_used;
this.regionSource = regionDiscoveryMetadata.region_source;
this.regionOutcome = regionDiscoveryMetadata.region_outcome;
}
/**
* Set cache outcome
*/
setCacheOutcome(cacheOutcome) {
this.cacheOutcome = cacheOutcome;
}
setNativeBrokerErrorCode(errorCode) {
const lastRequests = this.getLastRequests();
lastRequests.nativeBrokerErrorCode = errorCode;
this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId);
}
getNativeBrokerErrorCode() {
return this.getLastRequests().nativeBrokerErrorCode;
}
clearNativeBrokerErrorCode() {
const lastRequests = this.getLastRequests();
delete lastRequests.nativeBrokerErrorCode;
this.cacheManager.setServerTelemetry(this.telemetryCacheKey, lastRequests, this.correlationId);
}
static makeExtraSkuString(params) {
return makeExtraSkuString(params);
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
class BasePersistence {
async verifyPersistence() {
// We are using a different location for the test to avoid overriding the functional cache
const persistenceValidator = await this.createForPersistenceValidation();
try {
await persistenceValidator.save(Constants.PERSISTENCE_TEST_DATA);
const retrievedDummyData = await persistenceValidator.load();
if (!retrievedDummyData) {
throw PersistenceError.createCachePersistenceError("Persistence check failed. Data was written but it could not be read. " +
"Possible cause: on Linux, LibSecret is installed but D-Bus isn't running \
because it cannot be started over SSH.");
}
if (retrievedDummyData !== Constants.PERSISTENCE_TEST_DATA) {
throw PersistenceError.createCachePersistenceError(`Persistence check failed. Data written ${Constants.PERSISTENCE_TEST_DATA} is different \
from data read ${retrievedDummyData}`);
}
await persistenceValidator.delete();
return true;
}
catch (e) {
throw PersistenceError.createCachePersistenceError(`Verifing persistence failed with the error: ${e}`);
}
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Reads and writes data to file specified by file location. File contents are not
* encrypted.
*
* If file or directory has not been created, it FilePersistence.create() will create
* file and any directories in the path recursively.
*/
class FilePersistence extends BasePersistence {
constructor(fileLocation, loggerOptions) {
super();
this.logger = new Logger(loggerOptions || FilePersistence.createDefaultLoggerOptions());
this.filePath = fileLocation;
}
static async create(fileLocation, loggerOptions) {
const filePersistence = new FilePersistence(fileLocation, loggerOptions);
await filePersistence.createCacheFile();
return filePersistence;
}
async save(contents) {
try {
await fs.promises.writeFile(this.getFilePath(), contents, "utf-8");
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
async saveBuffer(contents) {
try {
await fs.promises.writeFile(this.getFilePath(), contents);
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
async load() {
try {
return await fs.promises.readFile(this.getFilePath(), "utf-8");
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
async loadBuffer() {
try {
return await fs.promises.readFile(this.getFilePath());
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
async delete() {
try {
await fs.promises.unlink(this.getFilePath());
return true;
}
catch (err) {
if (isNodeError(err)) {
if (err.code === Constants.ENOENT_ERROR) {
// file does not exist, so it was not deleted
this.logger.warning("Cache file does not exist, so it could not be deleted", "");
return false;
}
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
getFilePath() {
return this.filePath;
}
async reloadNecessary(lastSync) {
return lastSync < (await this.timeLastModified());
}
getLogger() {
return this.logger;
}
createForPersistenceValidation() {
const testCacheFileLocation = `${path.dirname(this.filePath)}/test.cache`;
return FilePersistence.create(testCacheFileLocation);
}
static createDefaultLoggerOptions() {
return {
loggerCallback: () => {
// allow users to not set loggerCallback
},
piiLoggingEnabled: false,
logLevel: LogLevel.Info,
};
}
async timeLastModified() {
try {
const stats = await fs.promises.stat(this.filePath);
return stats.mtime.getTime();
}
catch (err) {
if (isNodeError(err)) {
if (err.code === Constants.ENOENT_ERROR) {
// file does not exist, so it's never been modified
this.logger.verbose("Cache file does not exist", "");
return 0;
}
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
else {
throw err;
}
}
}
async createCacheFile() {
await this.createFileDirectory();
// File is created only if it does not exist
const fileHandle = await fs.promises.open(this.filePath, "a");
await fileHandle.close();
this.logger.info(`File created at ${this.filePath}`, "");
}
async createFileDirectory() {
try {
await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
}
catch (err) {
if (isNodeError(err)) {
if (err.code === Constants.EEXIST_ERROR) {
this.logger.info(`Directory ${path.dirname(this.filePath)} already exists`, "");
}
else {
throw PersistenceError.createFileSystemError(err.code || ErrorCodes.UNKNOWN, err.message);
}
}
else {
throw err;
}
}
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
class UnavailableDpapi {
constructor(errorMessage) {
this.errorMessage = errorMessage;
}
protectData() {
throw new Error(this.errorMessage);
}
unprotectData() {
throw new Error(this.errorMessage);
}
}
let Dpapi;
if (process.platform !== "win32") {
Dpapi = new UnavailableDpapi("Dpapi is not supported on this platform");
}
else {
// In .mjs files, require is not defined. We need to use createRequire to get a require function
const safeRequire = typeof require !== "undefined"
? require
: module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('msal-node-extensions.cjs', document.baseURI).href)));
try {
Dpapi = safeRequire(`../bin/${process.arch}/dpapi`);
}
catch (e) {
Dpapi = new UnavailableDpapi("Dpapi bindings unavailable");
}
}
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Specifies the scope of the data protection - either the current user or the local
* machine.
*
* You do not need a key to protect or unprotect the data.
* If you set the Scope to CurrentUser, only applications running on your credentials can
* unprotect the data; however, that means that any application running on your credentials
* can access the protected data. If you set the Scope to LocalMachine, any full-trust
* application on the computer can unprotect, access, and modify the data.
*
*/
const DataProtectionScope = {
CurrentUser: "CurrentUser",
LocalMachine: "LocalMachine",
};
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Uses CryptProtectData and CryptUnprotectData on Windows to encrypt and decrypt file contents.
*
* scope: Scope of the data protection. Either local user or the current machine
* optionalEntropy: Password or other additional entropy used to encrypt the data
*/
class FilePersistenceWithDataProtection extends BasePersistence {
constructor(filePersistence, scope, optionalEntropy) {
super();
this.scope = scope;
this.optionalEntropy = optionalEntropy
? Buffer.from(optionalEntropy, "utf-8")
: null;
this.filePersistence = filePersistence;
}
static async create(fileLocation, scope, optionalEntropy, loggerOptions) {
const filePersistence = await FilePersistence.create(fileLocation, loggerOptions);
const persistence = new FilePersistenceWithDataProtection(filePersistence, scope, optionalEntropy);
return persistence;
}
async save(contents) {
try {
const encryptedContents = Dpapi.protectData(Buffer.from(contents, "utf-8"), this.optionalEntropy, this.scope.toString());
await this.filePersistence.saveBuffer(encryptedContents);
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFilePersistenceWithDPAPIError(err.message);
}
else {
throw err;
}
}
}
async load() {
try {
const encryptedContents = await this.filePersistence.loadBuffer();
if (typeof encryptedContents === "undefined" ||
!encryptedContents ||
0 === encryptedContents.length) {
this.filePersistence
.getLogger()
.info("Encrypted contents loaded from file were null or empty", "");
return null;
}
return Dpapi.unprotectData(encryptedContents, this.optionalEntropy, this.scope.toString()).toString();
}
catch (err) {
if (isNodeError(err)) {
throw PersistenceError.createFilePersistenceWithDPAPIError(err.message);
}
else {