wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
{
  "version": 3,
  "sources": ["/mnt/vss/_work/1/s/sdk/core/core-rest-pipeline/src/util/tokenCycler.ts"],
  "sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type { AccessToken, GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { delay } from \"@azure/core-util\";\n\n/**\n * A function that gets a promise of an access token and allows providing\n * options.\n *\n * @param options - the options to pass to the underlying token provider\n */\nexport type AccessTokenGetter = (\n  scopes: string | string[],\n  options: GetTokenOptions,\n) => Promise<AccessToken>;\n\nexport interface TokenCyclerOptions {\n  /**\n   * The window of time before token expiration during which the token will be\n   * considered unusable due to risk of the token expiring before sending the\n   * request.\n   *\n   * This will only become meaningful if the refresh fails for over\n   * (refreshWindow - forcedRefreshWindow) milliseconds.\n   */\n  forcedRefreshWindowInMs: number;\n  /**\n   * Interval in milliseconds to retry failed token refreshes.\n   */\n  retryIntervalInMs: number;\n  /**\n   * The window of time before token expiration during which\n   * we will attempt to refresh the token.\n   */\n  refreshWindowInMs: number;\n}\n\n// Default options for the cycler if none are provided\nexport const DEFAULT_CYCLER_OPTIONS: TokenCyclerOptions = {\n  forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires\n  retryIntervalInMs: 3000, // Allow refresh attempts every 3s\n  refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry\n};\n\n/**\n * Converts an an unreliable access token getter (which may resolve with null)\n * into an AccessTokenGetter by retrying the unreliable getter in a regular\n * interval.\n *\n * @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.\n * @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.\n * @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.\n * @returns - A promise that, if it resolves, will resolve with an access token.\n */\nasync function beginRefresh(\n  getAccessToken: () => Promise<AccessToken | null>,\n  retryIntervalInMs: number,\n  refreshTimeout: number,\n): Promise<AccessToken> {\n  // This wrapper handles exceptions gracefully as long as we haven't exceeded\n  // the timeout.\n  async function tryGetAccessToken(): Promise<AccessToken | null> {\n    if (Date.now() < refreshTimeout) {\n      try {\n        return await getAccessToken();\n      } catch {\n        return null;\n      }\n    } else {\n      const finalToken = await getAccessToken();\n\n      // Timeout is up, so throw if it's still null\n      if (finalToken === null) {\n        throw new Error(\"Failed to refresh access token.\");\n      }\n\n      return finalToken;\n    }\n  }\n\n  let token: AccessToken | null = await tryGetAccessToken();\n\n  while (token === null) {\n    await delay(retryIntervalInMs);\n\n    token = await tryGetAccessToken();\n  }\n\n  return token;\n}\n\n/**\n * Creates a token cycler from a credential, scopes, and optional settings.\n *\n * A token cycler represents a way to reliably retrieve a valid access token\n * from a TokenCredential. It will handle initializing the token, refreshing it\n * when it nears expiration, and synchronizes refresh attempts to avoid\n * concurrency hazards.\n *\n * @param credential - the underlying TokenCredential that provides the access\n * token\n * @param tokenCyclerOptions - optionally override default settings for the cycler\n *\n * @returns - a function that reliably produces a valid access token\n */\nexport function createTokenCycler(\n  credential: TokenCredential,\n  tokenCyclerOptions?: Partial<TokenCyclerOptions>,\n): AccessTokenGetter {\n  let refreshWorker: Promise<AccessToken> | null = null;\n  let token: AccessToken | null = null;\n  let tenantId: string | undefined;\n\n  const options = {\n    ...DEFAULT_CYCLER_OPTIONS,\n    ...tokenCyclerOptions,\n  };\n\n  /**\n   * This little holder defines several predicates that we use to construct\n   * the rules of refreshing the token.\n   */\n  const cycler = {\n    /**\n     * Produces true if a refresh job is currently in progress.\n     */\n    get isRefreshing(): boolean {\n      return refreshWorker !== null;\n    },\n    /**\n     * Produces true if the cycler SHOULD refresh (we are within the refresh\n     * window and not already refreshing)\n     */\n    get shouldRefresh(): boolean {\n      if (cycler.isRefreshing) {\n        return false;\n      }\n      if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {\n        return true;\n      }\n\n      return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();\n    },\n    /**\n     * Produces true if the cycler MUST refresh (null or nearly-expired\n     * token).\n     */\n    get mustRefresh(): boolean {\n      return (\n        token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now()\n      );\n    },\n  };\n\n  /**\n   * Starts a refresh job or returns the existing job if one is already\n   * running.\n   */\n  function refresh(\n    scopes: string | string[],\n    getTokenOptions: GetTokenOptions,\n  ): Promise<AccessToken> {\n    if (!cycler.isRefreshing) {\n      // We bind `scopes` here to avoid passing it around a lot\n      const tryGetAccessToken = (): Promise<AccessToken | null> =>\n        credential.getToken(scopes, getTokenOptions);\n\n      // Take advantage of promise chaining to insert an assignment to `token`\n      // before the refresh can be considered done.\n      refreshWorker = beginRefresh(\n        tryGetAccessToken,\n        options.retryIntervalInMs,\n        // If we don't have a token, then we should timeout immediately\n        token?.expiresOnTimestamp ?? Date.now(),\n      )\n        .then((_token) => {\n          refreshWorker = null;\n          token = _token;\n          tenantId = getTokenOptions.tenantId;\n          return token;\n        })\n        .catch((reason) => {\n          // We also should reset the refresher if we enter a failed state.  All\n          // existing awaiters will throw, but subsequent requests will start a\n          // new retry chain.\n          refreshWorker = null;\n          token = null;\n          tenantId = undefined;\n          throw reason;\n        });\n    }\n\n    return refreshWorker as Promise<AccessToken>;\n  }\n\n  return async (scopes: string | string[], tokenOptions: GetTokenOptions): Promise<AccessToken> => {\n    //\n    // Simple rules:\n    // - If we MUST refresh, then return the refresh task, blocking\n    //   the pipeline until a token is available.\n    // - If we SHOULD refresh, then run refresh but don't return it\n    //   (we can still use the cached token).\n    // - Return the token, since it's fine if we didn't return in\n    //   step 1.\n    //\n\n    const hasClaimChallenge = Boolean(tokenOptions.claims);\n    const tenantIdChanged = tenantId !== tokenOptions.tenantId;\n\n    if (hasClaimChallenge) {\n      // If we've received a claim, we know the existing token isn't valid\n      // We want to clear it so that that refresh worker won't use the old expiration time as a timeout\n      token = null;\n    }\n\n    // If the tenantId passed in token options is different to the one we have\n    // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to\n    // refresh the token with the new tenantId or token.\n    const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;\n\n    if (mustRefresh) {\n      return refresh(scopes, tokenOptions);\n    }\n\n    if (cycler.shouldRefresh) {\n      refresh(scopes, tokenOptions);\n    }\n\n    return token as AccessToken;\n  };\n}\n"],
  "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,uBAAsB;AAmCf,MAAM,yBAA6C;AAAA,EACxD,yBAAyB;AAAA;AAAA,EACzB,mBAAmB;AAAA;AAAA,EACnB,mBAAmB,MAAO,KAAK;AAAA;AACjC;AAYA,eAAe,aACb,gBACA,mBACA,gBACsB;AAGtB,iBAAe,oBAAiD;AAC9D,QAAI,KAAK,IAAI,IAAI,gBAAgB;AAC/B,UAAI;AACF,eAAO,MAAM,eAAe;AAAA,MAC9B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,YAAM,aAAa,MAAM,eAAe;AAGxC,UAAI,eAAe,MAAM;AACvB,cAAM,IAAI,MAAM,iCAAiC;AAAA,MACnD;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAA4B,MAAM,kBAAkB;AAExD,SAAO,UAAU,MAAM;AACrB,cAAM,wBAAM,iBAAiB;AAE7B,YAAQ,MAAM,kBAAkB;AAAA,EAClC;AAEA,SAAO;AACT;AAgBO,SAAS,kBACd,YACA,oBACmB;AACnB,MAAI,gBAA6C;AACjD,MAAI,QAA4B;AAChC,MAAI;AAEJ,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AAMA,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA,IAIb,IAAI,eAAwB;AAC1B,aAAO,kBAAkB;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,gBAAyB;AAC3B,UAAI,OAAO,cAAc;AACvB,eAAO;AAAA,MACT;AACA,UAAI,OAAO,yBAAyB,MAAM,wBAAwB,KAAK,IAAI,GAAG;AAC5E,eAAO;AAAA,MACT;AAEA,cAAQ,OAAO,sBAAsB,KAAK,QAAQ,oBAAoB,KAAK,IAAI;AAAA,IACjF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,cAAuB;AACzB,aACE,UAAU,QAAQ,MAAM,qBAAqB,QAAQ,0BAA0B,KAAK,IAAI;AAAA,IAE5F;AAAA,EACF;AAMA,WAAS,QACP,QACA,iBACsB;AACtB,QAAI,CAAC,OAAO,cAAc;AAExB,YAAM,oBAAoB,MACxB,WAAW,SAAS,QAAQ,eAAe;AAI7C,sBAAgB;AAAA,QACd;AAAA,QACA,QAAQ;AAAA;AAAA,QAER,OAAO,sBAAsB,KAAK,IAAI;AAAA,MACxC,EACG,KAAK,CAAC,WAAW;AAChB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW,gBAAgB;AAC3B,eAAO;AAAA,MACT,CAAC,EACA,MAAM,CAAC,WAAW;AAIjB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW;AACX,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,QAA2B,iBAAwD;AAW/F,UAAM,oBAAoB,QAAQ,aAAa,MAAM;AACrD,UAAM,kBAAkB,aAAa,aAAa;AAElD,QAAI,mBAAmB;AAGrB,cAAQ;AAAA,IACV;AAKA,UAAM,cAAc,mBAAmB,qBAAqB,OAAO;AAEnE,QAAI,aAAa;AACf,aAAO,QAAQ,QAAQ,YAAY;AAAA,IACrC;AAEA,QAAI,OAAO,eAAe;AACxB,cAAQ,QAAQ,YAAY;AAAA,IAC9B;AAEA,WAAO;AAAA,EACT;AACF;",
  "names": []
}