import { ErrorType } from './consts' import { ApiUrls, ApiUrlsV2, AUTH_API_PREFIX, AUTH_STATE_CHANGED_TYPE, DEFAULT_NODE_ACCESS_SCOPE, EVENTS, } from '../auth/consts' import { AuthClient, SimpleStorage } from './interface' import { Credentials, ResponseError, RequestOptions, RequestFunction, OAuth2ClientOptions, AuthClientRequestOptions, } from './models' import { uuidv4 } from '../utils/uuid' import { getPathName } from '../utils/index' import { SinglePromise } from '../utils/function/single-promise' import { weBtoa } from '../utils/base64' import { isMp } from '../utils/mp' import { AuthOptions } from '../auth/apis' import { ICloudbaseConfig } from '@cloudbase/types' import { langEvent } from '@cloudbase/utilities' const RequestIdHeaderName = 'x-request-id' const DeviceIdHeaderName = 'x-device-id' const DeviceIdSectionName = 'device_id' declare const wx: any export interface ToResponseErrorOptions { error?: ErrorType error_description?: string | null error_uri?: string | null details?: any | null } export const defaultRequest: RequestFunction = async function (url: string, options?: RequestOptions): Promise { let result: T | null = null let responseError: ResponseError | null = null try { // Objects must be copied to prevent modification of data such as body. const copyOptions = Object.assign({}, options) if (!copyOptions.method) { copyOptions.method = 'GET' } if (copyOptions.body && typeof copyOptions.body !== 'string') { copyOptions.body = JSON.stringify(copyOptions.body) } const responseResult: Response = await fetch(url, copyOptions) const jsonResponse = await responseResult.json() if (jsonResponse?.error) { responseError = jsonResponse as ResponseError responseError.error_uri = new URL(url).pathname } else { result = jsonResponse as T } } catch (error) { responseError = { error: ErrorType.UNREACHABLE, error_description: error.message, error_uri: new URL(url).pathname, } } if (responseError) { throw responseError } else { return result } } export const toResponseError = (error: ResponseError | Error, options?: ToResponseErrorOptions): ResponseError => { let responseError: ResponseError const formatOptions: ToResponseErrorOptions = options || {} if (error instanceof Error) { responseError = { error: formatOptions.error || ErrorType.LOCAL, error_description: formatOptions.error_description || error.message, error_uri: formatOptions.error_uri, details: formatOptions.details || error.stack, } } else { const formatError: ToResponseErrorOptions = error || {} responseError = { error: formatOptions.error || formatError.error || ErrorType.LOCAL, error_description: formatOptions.error_description || formatError.error_description, error_uri: formatOptions.error_uri || formatError.error_uri, details: formatOptions.details || formatError.details, } } return responseError } /** * Generate request id. * @return {string} */ export function generateRequestId(): string { return uuidv4() } /** * Default Storage. */ class DefaultStorage implements SimpleStorage { /** * 缓存key统一使用后缀区分 */ // eslint-disable-next-line @typescript-eslint/naming-convention private readonly _env: string constructor(opts?: { env: string }) { this._env = opts?.env || '' } /** * Get item. * @param {string} key */ async getItem(key: string): Promise { return window.localStorage.getItem(`${key}${this._env}`) } /** * Remove item. * @param {string} key */ async removeItem(key: string): Promise { window.localStorage.removeItem(`${key}${this._env}`) } /** * Set item. * @param {string} key * @param {string} value */ async setItem(key: string, value: string): Promise { window.localStorage.setItem(`${key}${this._env}`, value) } /** * Get item sync. * @param {string} key */ getItemSync(key: string): string | null { return window.localStorage.getItem(`${key}${this._env}`) } /** * Remove item sync. * @param {string} key */ removeItemSync(key: string): void { window.localStorage.removeItem(`${key}${this._env}`) } /** * Set item sync. * @param {string} key * @param {string} value */ setItemSync(key: string, value: string): void { window.localStorage.setItem(`${key}${this._env}`, value) } } export const defaultStorage = new DefaultStorage() interface LocalCredentialsOptions { tokenSectionName: string storage: SimpleStorage clientId: string credentials?: Credentials } /** * Check if credentials is expired. * @param {Credentials} credentials * @return {boolean} */ function isCredentialsExpired(credentials: Credentials): boolean { let isExpired = true if (credentials?.expires_at && credentials?.access_token) { isExpired = credentials.expires_at < new Date() } return isExpired } /** * Local credentials. * Local credentials, with memory cache and storage cache. * If the memory cache expires, the storage cache is automatically loaded. */ class LocalCredentials { private tokenSectionName: string private storage: SimpleStorage private clientId: string private credentials: Credentials | null = null // 来自accessKey的身份,不持久化 private accessKeyCredentials: Credentials | null = null private singlePromise: SinglePromise = null /** * constructor * @param {LocalCredentialsOptions} options */ constructor(options: LocalCredentialsOptions) { this.tokenSectionName = options.tokenSectionName this.storage = options.storage this.clientId = options.clientId this.singlePromise = new SinglePromise({ clientId: this.clientId }) this.credentials = options.credentials || null } public getStorageCredentialsSync(): Credentials | null { let credentials: Credentials = null const tokenStr: string = this.storage.getItemSync(this.tokenSectionName) if (tokenStr !== undefined && tokenStr !== null) { try { credentials = JSON.parse(tokenStr) if (credentials?.expires_at) { credentials.expires_at = new Date(credentials.expires_at) } } catch (error) { this.storage.removeItem(this.tokenSectionName) credentials = null } } return credentials } /** * setCredentials Provides an alternative fetch api request implementation with auth credentials * @param {Credentials} credentials */ public async setCredentials(credentials?: Credentials): Promise { if (credentials?.expires_in) { if (!credentials?.expires_at) { credentials.expires_at = new Date(Date.now() + (credentials.expires_in - 30) * 1000) } if (this.storage) { const tokenStr: string = JSON.stringify(credentials) await this.storage.setItem(this.tokenSectionName, tokenStr) } this.credentials = credentials } else { if (this.storage) { await this.storage.removeItem(this.tokenSectionName) } this.credentials = null } } /** * set credentials from accessKey * @param {Credentials} credentials */ public setAccessKeyCredentials(credentials?: Credentials) { this.accessKeyCredentials = credentials // node sdk 场景下直接使用accessKey身份 if (credentials.scope === DEFAULT_NODE_ACCESS_SCOPE) { this.credentials = credentials } } /** * Get credentials. * @return {Promise} */ public async getCredentials(): Promise { return this.singlePromise.run('getCredentials', async () => { if (isCredentialsExpired(this.credentials)) { const { credentials, isAccessKeyCredentials } = await this.getStorageCredentials() if (isAccessKeyCredentials) { return credentials } this.credentials = credentials } return this.credentials }) } /** * Get storage credentials. */ private async getStorageCredentials(): Promise<{ credentials: Credentials | null; isAccessKeyCredentials: boolean }> { return this.singlePromise.run('_getStorageCredentials', async () => { let credentials: Credentials = null let isAccessKeyCredentials = false const tokenStr: string = await this.storage.getItem(this.tokenSectionName) if (!!tokenStr) { try { credentials = JSON.parse(tokenStr) if (credentials?.expires_at) { credentials.expires_at = new Date(credentials.expires_at) } } catch (error) { await this.storage.removeItem(this.tokenSectionName) credentials = null } } else { credentials = this.accessKeyCredentials || null isAccessKeyCredentials = true } return { credentials, isAccessKeyCredentials } }) } } /** * OAuth2Client */ export class OAuth2Client implements AuthClient { private static defaultRetry = 2 private static minRetry = 0 private static maxRetry = 5 private static retryInterval = 1000 public localCredentials: LocalCredentials /** * Keeps track of the async client initialization. * When null or not yet resolved the auth state is `unknown` * Once resolved the auth state is known and it's safe to call any further client methods. */ public initializePromise: Promise<{ error: Error | null }> | null = null protected lockAcquired = false protected pendingInLock: Promise[] = [] protected logDebugMessages: boolean protected getInitialSession?: () => Promise<{ data: { session: Credentials; user?: any } | null error: Error | null }> private apiOrigin: string private apiPath: string private clientId: string private i18n: ICloudbaseConfig['i18n'] private retry: number private clientSecret?: string private baseRequest: (url: string, options?: RequestOptions) => Promise private storage: SimpleStorage private deviceID?: string private tokenInURL?: boolean private refreshTokenFunc: (refreshToken?: string, credentials?: Credentials) => Promise private headers?: { [key: string]: string } private singlePromise: SinglePromise = null private anonymousSignInFunc: (Credentials) => Promise private wxCloud: any private useWxCloud: boolean private eventBus: any private basicAuth: string private onCredentialsError: AuthOptions['onCredentialsError'] | undefined private onInitialSessionObtained?: (data: { session: Credentials; user?: any }, error?: any) => void | Promise /** * constructor * @param {OAuth2ClientOptions} options */ constructor(options: OAuth2ClientOptions) { if (!options.clientSecret) { options.clientSecret = '' } if (!options.clientId && options.env) { options.clientId = options.env } this.apiOrigin = options.apiOrigin this.apiPath = options.apiPath || AUTH_API_PREFIX this.clientId = options.clientId this.i18n = options.i18n this.eventBus = options.eventBus this.singlePromise = new SinglePromise({ clientId: this.clientId }) this.retry = this.formatRetry(options.retry, OAuth2Client.defaultRetry) if (options.baseRequest) { this.baseRequest = options.baseRequest } else { this.baseRequest = defaultRequest } this.tokenInURL = options.tokenInURL this.headers = options.headers // @ts-ignore this.storage = options.storage || defaultStorage this.localCredentials = new LocalCredentials({ tokenSectionName: `credentials_${options.clientId}`, storage: this.storage, clientId: options.clientId, }) this.clientSecret = options.clientSecret if (options.clientId) { this.basicAuth = `Basic ${weBtoa(`${options.clientId}:${this.clientSecret}`)}` } this.wxCloud = options.wxCloud try { if (isMp()) { this.useWxCloud = options.useWxCloud if (this.wxCloud === undefined && options.env) { wx.cloud.init({ env: options.env }) this.wxCloud = wx.cloud } } } catch (error) { // } this.refreshTokenFunc = options.refreshTokenFunc || this.defaultRefreshTokenFunc this.anonymousSignInFunc = options.anonymousSignInFunc this.onCredentialsError = options.onCredentialsError // New options for session detection this.getInitialSession = options.getInitialSession this.onInitialSessionObtained = options.onInitialSessionObtained this.logDebugMessages = options.debug ?? false langEvent.bus.on(langEvent.LANG_CHANGE_EVENT, (params: any) => { this.i18n = params.data?.i18n || this.i18n }) // Note: Auto-initialize is NOT called here to allow CloudbaseOAuth to set getInitialSession first // CloudbaseOAuth.constructor will call initialize() after setting up the callback } /** * Sets the getInitialSession callback. * This callback is called during initialize() to get the initial session (e.g., from OAuth callback in URL). * @param callback The callback function to get initial session */ public setGetInitialSession(callback: () => Promise<{ data: { session: Credentials; user?: any } | null error: Error | null }>,): void { this.getInitialSession = callback } /** * Sets the onInitialSessionObtained callback. * This callback is invoked after initial session is obtained and stored, * allowing upper layers to handle user info storage. * @param callback The callback function to handle session and user data */ public setOnInitialSessionObtained(callback: (data: { session: Credentials; user?: any }) => void | Promise,): void { this.onInitialSessionObtained = callback } /** * Initializes the client session either from the url or from storage. * This method is automatically called when instantiating the client with detectSessionInUrl=true, * but should also be called manually when checking for an error from an auth redirect. * @param onInitialSessionObtained Optional callback to set before initialization starts */ async initialize(func?: Promise<{ error: Error | null }>): Promise<{ error: Error | null }> { // eslint-disable-next-line @typescript-eslint/no-misused-promises if (this.initializePromise) { return await this.initializePromise } if (func !== undefined) { this.initializePromise = func return await this.initializePromise } this.initializePromise = (async () => await this._acquireLock(-1, async () => await this._initialize()))() return await this.initializePromise } /** * setCredentials Provides an alternative fetch api request implementation with auth credentials * @param {Credentials} credentials * @return {Promise} */ public async setCredentials(credentials?: Credentials): Promise { // If initialization is in progress, wait for it first await this.initializePromise return this._acquireLock(-1, async () => this.localCredentials.setCredentials(credentials)) } /** * set credentials from accessKey * @param {Credentials} credentials */ public setAccessKeyCredentials(credentials?: Credentials) { return this.localCredentials.setAccessKeyCredentials(credentials) } /** * getAccessToken return a validate access token */ public async getAccessToken(): Promise { // If initialization is in progress, wait for it first await this.initializePromise const credentials: Credentials = await this.getCredentials() // node sdk 场景下直接返回access_token if (credentials?.scope === DEFAULT_NODE_ACCESS_SCOPE) { return Promise.resolve(credentials.access_token) } if (credentials?.access_token) { return Promise.resolve(credentials.access_token) } const respErr: ResponseError = { error: ErrorType.UNAUTHENTICATED } return Promise.reject(respErr) } /** * request http like simple fetch api, exp:request('/v1/user/me', {withCredentials:true}) * @param {string} url * @param {AuthClientRequestOptions} options */ public async request(url: string, options?: AuthClientRequestOptions): Promise { if (!options) { options = {} } const retry: number = this.formatRetry(options.retry, this.retry) options.headers = { ...options.headers, [this.i18n?.LANG_HEADER_KEY]: this.i18n?.lang } if (this.headers) { options.headers = { ...this.headers, ...options.headers, } } if (!options.headers[RequestIdHeaderName]) { options.headers[RequestIdHeaderName] = generateRequestId() } if (!options.headers[DeviceIdHeaderName]) { const deviceId = await this.getDeviceId() options.headers[DeviceIdHeaderName] = deviceId } if (options?.withBasicAuth && this.basicAuth) { options.headers.Authorization = this.basicAuth } if (options?.withCredentials) { // Use custom getCredentials function if provided, otherwise use default // Custom getCredentials avoids default getCredentials() call which may cause deadlock during initialization const credentials = options.getCredentials ? await options.getCredentials() : await this.getCredentials() if (credentials) { if (this.tokenInURL) { if (url.indexOf('?') < 0) { url += '?' } url += `access_token=${credentials.access_token}` } else { options.headers.Authorization = `${credentials.token_type} ${credentials.access_token}` } } } else { if (this.clientId && url.indexOf('client_id') < 0) { url += url.indexOf('?') < 0 ? '?' : '&' url += `client_id=${this.clientId}` } } if (url.startsWith('/')) { url = `${this.apiOrigin}${this.apiPath}${url}` } let response: T | null = null const maxRequestTimes: number = retry + 1 for (let requestTime = 0; requestTime < maxRequestTimes; requestTime++) { try { if (options.useWxCloud || this.useWxCloud) { response = await this.wxCloudCallFunction(url, options) } else { response = await this.baseRequest(url, options) } if (!!(response as any)?.code) { throw { error: (response as any).code, error_description: (response as any).message, error_uri: new URL(url).pathname, } } break } catch (responseError) { try { responseError.requestId = responseError.requestId || options.headers[RequestIdHeaderName] || '' } catch (error) {} if (options.withCredentials && responseError && responseError.error === ErrorType.UNAUTHENTICATED) { await this.setCredentials(null) return Promise.reject(responseError) } if (requestTime === retry || !responseError || responseError.error !== 'unreachable') { return Promise.reject(responseError) } } await this.sleep(OAuth2Client.retryInterval) } return response } public async wxCloudCallFunction(url: string, options?: RequestOptions): Promise { let result: T | null = null let responseError: ResponseError | null = null try { let userAgent = '' try { userAgent = await wx.getRendererUserAgent() } catch (error) {} const { result: responseResult } = await this.wxCloud.callFunction({ name: 'httpOverCallFunction', data: { url, method: options.method, headers: { origin: 'https://servicewechat.com', 'User-Agent': userAgent, ...options.headers, }, body: options.body, }, }) if (responseResult?.body?.error_code) { responseError = responseResult?.body as ResponseError responseError.error_uri = getPathName(url) } else { result = responseResult?.body as T } } catch (error) { responseError = { error: ErrorType.UNREACHABLE, error_description: error.message, error_uri: getPathName(url), } } if (responseError) { throw responseError } else { return result } } /** * Get credentials. */ public async getCredentials(): Promise { // If initialization is in progress, wait for it first await this.initializePromise return this._acquireLock(-1, async () => this._getCredentials()) } /** * @deprecated 废弃接口,勿用. 如需获取凭证信息请使用 getCredentials 方法 */ public getCredentialsSync(): Credentials | null { const credentials: Credentials = this.localCredentials.getStorageCredentialsSync() return credentials } public getCredentialsAsync(): Promise { return this.getCredentials() } public async getScope(): Promise { const credentials: Credentials = await this.localCredentials.getCredentials() if (!credentials) { const msg = 'credentials not found' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } return credentials.scope } public async getGroups(): Promise { const credentials: Credentials = await this.localCredentials.getCredentials() if (!credentials) { const msg = 'credentials not found' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } return credentials.groups } /** * Refresh expired token. * @param {Credentials} credentials * @return {Promise} */ public async refreshToken(credentials: Credentials, options?: { throwError?: boolean }): Promise { // If initialization is in progress, wait for it first await this.initializePromise return this._acquireLock(-1, async () => this._refreshToken(credentials, options)) } /** * Internal refresh token method (called within lock) */ private async _refreshToken(credentials: Credentials, options?: { throwError?: boolean }): Promise { return this.singlePromise.run('_refreshToken', async () => { // node sdk 场景下不需要刷新 token if (credentials?.scope === DEFAULT_NODE_ACCESS_SCOPE) { return credentials } if (!credentials || !credentials.refresh_token) { const msg = 'no refresh token found in credentials' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } try { const newCredentials: Credentials = await this.refreshTokenFunc(credentials.refresh_token, credentials) await this.localCredentials.setCredentials(newCredentials) this.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.TOKEN_REFRESHED }) return newCredentials } catch (error) { if (options?.throwError) { throw error } if (error.error === ErrorType.INVALID_GRANT) { await this.localCredentials.setCredentials(null) const msg = error.error_description this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } this.onCredentialsError?.({ msg: error.error_description }) return Promise.reject(error) } }) } private async anonymousLogin(credentials: Credentials) { return this.singlePromise.run('_anonymousLogin', async () => { if (this.anonymousSignInFunc) { const c = await this.anonymousSignInFunc(credentials) credentials = c || (await this.localCredentials.getCredentials()) } else { credentials = await this.anonymousSignIn(credentials) } return credentials }) } /** * Check retry value. * @param {number} retry * @return {number} */ private checkRetry(retry: number): number { let responseError: ResponseError | null = null if (typeof retry !== 'number' || retry < OAuth2Client.minRetry || retry > OAuth2Client.maxRetry) { responseError = { error: ErrorType.UNREACHABLE, error_description: 'wrong options param: retry', } } if (responseError) { throw responseError } return retry } /** * Format retry value. * @param {number} retry * @param {number} defaultVale * @return {number} */ private formatRetry(retry: number, defaultVale: number): number { if (typeof retry === 'undefined') { return defaultVale } return this.checkRetry(retry) } /** * Sleep. * @param {number} ms * @return {Promise} */ private async sleep(ms: number): Promise { return new Promise((resolve) => { setTimeout(() => { resolve() }, ms) }) } /** * anonymous signIn * @param {Credentials} credentials * @return {Promise} */ private async anonymousSignIn(credentials: Credentials): Promise { return this.singlePromise.run('_anonymous', async () => { if (!credentials || credentials.scope !== 'anonymous') { return this.unAuthenticatedError('no anonymous in credentials') } try { const newCredentials: Credentials = await this.request(ApiUrls.AUTH_SIGN_IN_ANONYMOUSLY_URL, { method: 'POST', withBasicAuth: true, body: {}, }) await this.localCredentials.setCredentials(newCredentials) return newCredentials } catch (error) { if (error.error === ErrorType.INVALID_GRANT) { await this.localCredentials.setCredentials(null) return this.unAuthenticatedError(error.error_description) } return Promise.reject(error) } }) } /** * Default refresh token function. * @param {string} refreshToken * @return {Promise} */ private async defaultRefreshTokenFunc(refreshToken?: string, credentials?: Credentials): Promise { if (refreshToken === undefined || refreshToken === '') { const msg = 'refresh token not found' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } let url: string = ApiUrls.AUTH_TOKEN_URL if (credentials?.version === 'v2') { url = ApiUrlsV2.AUTH_TOKEN_URL } const newCredentials: Credentials = await this.request(url, { method: 'POST', body: { client_id: this.clientId, client_secret: this.clientSecret, grant_type: 'refresh_token', refresh_token: refreshToken, }, }) return { ...newCredentials, version: credentials?.version || 'v1' } } /** * Get deviceId */ private async getDeviceId(): Promise { if (this.deviceID) { return this.deviceID } let deviceId: string = await this.storage.getItem(DeviceIdSectionName) if (!(typeof deviceId === 'string' && deviceId.length >= 16 && deviceId.length <= 48)) { deviceId = uuidv4() await this.storage.setItem(DeviceIdSectionName, deviceId) } this.deviceID = deviceId return deviceId } /** * Generate unAuthenticated error. * @param {string} err * @return {Promise} */ private unAuthenticatedError(err?: string): Promise { const respErr: ResponseError = { error: ErrorType.UNAUTHENTICATED, error_description: err, } return Promise.reject(respErr) } /** * Debug logging helper */ private _debug(...args: any[]): void { if (this.logDebugMessages) { console.log('[OAuth2Client]', ...args) } } /** * IMPORTANT: * 1. Never throw in this method, as it is called from the constructor * 2. Never return a session from this method as it would be cached over * the whole lifetime of the client */ private async _initialize(): Promise<{ error: Error | null }> { try { // If no getInitialSession callback is set, nothing to do if (!this.getInitialSession) { this._debug('#_initialize()', 'no getInitialSession callback set, skipping') return { error: null } } this._debug('#_initialize()', 'calling getInitialSession callback') try { const { data, error } = await this.getInitialSession() if (data?.session) { this._debug('#_initialize()', 'session obtained from getInitialSession', data.session) await this.localCredentials.setCredentials(data?.session) } // Invoke callback for upper layer to handle user storage // This is called BEFORE lock is released so user storage is atomic with session if (this.onInitialSessionObtained) { this._debug('#_initialize()', 'calling onInitialSessionObtained callback') try { await this.onInitialSessionObtained(data, error) } catch (callbackError) { this._debug('#_initialize()', 'error in onInitialSessionObtained', callbackError) // Don't fail initialization if callback fails } } if (error) { this._debug('#_initialize()', 'error from getInitialSession', error) return { error } } return { error: null } } catch (err) { this._debug('#_initialize()', 'exception during getInitialSession', err) return { error: err instanceof Error ? err : new Error(String(err)) } } } catch (error) { this._debug('#_initialize()', 'unexpected error', error) return { error: error instanceof Error ? error : new Error(String(error)) } } finally { this._debug('#_initialize()', 'end') } } /** * Acquires a global lock based on the client ID. * This ensures that only one operation can modify credentials at a time. */ private async _acquireLock(acquireTimeout: number, fn: () => Promise): Promise { this._debug('#_acquireLock', 'begin', acquireTimeout) try { if (this.lockAcquired) { // Lock is already acquired, queue the operation const last = this.pendingInLock.length ? this.pendingInLock[this.pendingInLock.length - 1] : Promise.resolve() const result = (async () => { await last return await fn() })() this.pendingInLock.push((async () => { try { await result } catch (_e: any) { // we just care if it finished } })(),) return result } // Acquire the lock this._debug('#_acquireLock', 'acquiring lock for client', this.clientId) try { this.lockAcquired = true const result = fn() this.pendingInLock.push((async () => { try { await result } catch (_e: any) { // we just care if it finished } })(),) await result // keep draining the queue until there's nothing to wait on while (this.pendingInLock.length) { const waitOn = [...this.pendingInLock] await Promise.all(waitOn) this.pendingInLock.splice(0, waitOn.length) } return await result } finally { this._debug('#_acquireLock', 'releasing lock for client', this.clientId) this.lockAcquired = false } } finally { this._debug('#_acquireLock', 'end') } } /** * Internal method to get credentials (called within lock) */ private async _getCredentials(): Promise { let credentials: Credentials = await this.localCredentials.getCredentials() // node adapter 场景下,scope 为 DEFAULT_NODE_ACCESS_SCOPE,不需要刷新 token if (credentials.scope === DEFAULT_NODE_ACCESS_SCOPE) { return credentials } if (!credentials) { const msg = 'credentials not found' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } if (isCredentialsExpired(credentials)) { if (credentials.refresh_token) { try { credentials = await this._refreshToken(credentials) } catch (error) { if (credentials.scope === 'anonymous') { credentials = await this.anonymousLogin(credentials) } else { this.onCredentialsError?.({ msg: error.error_description }) return Promise.reject(error) } } } else if (credentials.scope === 'anonymous') { credentials = await this.anonymousLogin(credentials) } else { const msg = 'no refresh token found in credentials' this.onCredentialsError?.({ msg }) return this.unAuthenticatedError(msg) } } return credentials } }