diff --git a/src/api/coral.ts b/src/api/coral.ts index cf6765b..f9ffd5c 100644 --- a/src/api/coral.ts +++ b/src/api/coral.ts @@ -41,9 +41,11 @@ export interface ResultData { } export default class CoralApi { - onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise) | null = null; + onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + /** @internal */ + _token_expired = false; protected constructor( public token: string, @@ -57,8 +59,18 @@ export default class CoralApi { async fetch( url: string, method = 'GET', body?: string, headers?: object, /** @internal */ _autoRenewToken = true, - /** @internal */ _attempt = 0 + /** @internal */ _attempt = 0, ): Promise> { + if (this._token_expired && _autoRenewToken && !this._renewToken) { + if (!this.onTokenExpired || _attempt) throw new Error('Token expired'); + + this._renewToken = this.onTokenExpired.call(null).then(data => { + if (data) this.setTokenWithSavedToken(data); + }).finally(() => { + this._renewToken = null; + }); + } + if (this._renewToken && _autoRenewToken) { await this._renewToken; } @@ -86,6 +98,7 @@ export default class CoralApi { const data = await response.json() as CoralResponse; if (data.status === CoralStatus.TOKEN_EXPIRED && _autoRenewToken && !_attempt && this.onTokenExpired) { + this._token_expired = true; // _renewToken will be awaited when calling fetch this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => { if (data) this.setTokenWithSavedToken(data); @@ -286,6 +299,7 @@ export default class CoralApi { this.token = data.credential.accessToken; this.coral_user_id = '' + data.nsoAccount.user.id; if ('user' in data) this.na_id = data.user.id; + this._token_expired = false; } static async createWithSessionToken(token: string, useragent = getAdditionalUserAgents()) { diff --git a/src/api/moon.ts b/src/api/moon.ts index d5d0c0d..506dbf9 100644 --- a/src/api/moon.ts +++ b/src/api/moon.ts @@ -16,9 +16,10 @@ const ZNMA_USER_AGENT = 'moon_ANDROID/' + ZNMA_VERSION + ' (com.nintendo.znma; b '; ANDROID 26)'; export default class MoonApi { - onTokenExpired: ((data: MoonError, res: Response) => Promise) | null = null; + onTokenExpired: ((data?: MoonError, res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + protected _token_expired = false; protected constructor( public token: string, @@ -31,8 +32,18 @@ export default class MoonApi { async fetch( url: string, method = 'GET', body?: string, headers?: object, /** @internal */ _autoRenewToken = true, - /** @internal */ _attempt = 0 + /** @internal */ _attempt = 0, ): Promise> { + if (this._token_expired && _autoRenewToken && !this._renewToken) { + if (!this.onTokenExpired || _attempt) throw new Error('Token expired'); + + this._renewToken = this.onTokenExpired.call(null).then(data => { + if (data) this.setTokenWithSavedToken(data); + }).finally(() => { + this._renewToken = null; + }); + } + if (this._renewToken && _autoRenewToken) { await this._renewToken; } @@ -62,6 +73,7 @@ export default class MoonApi { debug('fetch %s %s, response %s', method, url, response.status); if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) { + this._token_expired = true; const data = await response.json() as MoonError; // _renewToken will be awaited when calling fetch @@ -123,6 +135,7 @@ export default class MoonApi { private setTokenWithSavedToken(data: MoonAuthData | PartialMoonAuthData) { this.token = data.nintendoAccountToken.access_token!; if ('user' in data) this.naId = data.user.id; + this._token_expired = false; } static async createWithSessionToken(token: string) { diff --git a/src/api/nooklink.ts b/src/api/nooklink.ts index 9854f37..4219b83 100644 --- a/src/api/nooklink.ts +++ b/src/api/nooklink.ts @@ -17,9 +17,10 @@ const NOOKLINK_URL = NOOKLINK_WEBSERVICE_URL + '/api'; const BLANCO_VERSION = '2.1.1'; export default class NooklinkApi { - onTokenExpired: ((data: WebServiceError, res: Response) => Promise) | null = null; + onTokenExpired: ((data?: WebServiceError, res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + protected _token_expired = false; protected constructor( public gtoken: string, @@ -30,8 +31,18 @@ export default class NooklinkApi { async fetch( url: string, method = 'GET', body?: string | FormData, headers?: object, /** @internal */ _autoRenewToken = true, - /** @internal */ _attempt = 0 + /** @internal */ _attempt = 0, ): Promise> { + if (this._token_expired && _autoRenewToken && !this._renewToken) { + if (!this.onTokenExpired || _attempt) throw new Error('Token expired'); + + this._renewToken = this.onTokenExpired.call(null).then(data => { + if (data) this.setTokenWithSavedToken(data); + }).finally(() => { + this._renewToken = null; + }); + } + if (this._renewToken && _autoRenewToken) { await this._renewToken; } @@ -57,6 +68,7 @@ export default class NooklinkApi { debug('fetch %s %s, response %s', method, url, response.status); if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) { + this._token_expired = true; const data = await response.json() as WebServiceError; // _renewToken will be awaited when calling fetch @@ -109,6 +121,7 @@ export default class NooklinkApi { private setTokenWithSavedToken(data: NooklinkAuthData) { this.gtoken = data.gtoken; + this._token_expired = false; } static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) { @@ -195,9 +208,10 @@ export default class NooklinkApi { } export class NooklinkUserApi { - onTokenExpired: ((data: WebServiceError, res: Response) => Promise) | null = null; + onTokenExpired: ((data?: WebServiceError, res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + protected _token_expired = false; protected constructor( public user_id: string, @@ -211,8 +225,18 @@ export class NooklinkUserApi { async fetch( url: string, method = 'GET', body?: string | FormData, headers?: object, /** @internal */ _autoRenewToken = true, - /** @internal */ _attempt = 0 + /** @internal */ _attempt = 0, ): Promise> { + if (this._token_expired && _autoRenewToken && !this._renewToken) { + if (!this.onTokenExpired || _attempt) throw new Error('Token expired'); + + this._renewToken = this.onTokenExpired.call(null).then(data => { + if (data) this.setTokenWithSavedToken(data); + }).finally(() => { + this._renewToken = null; + }); + } + if (this._renewToken && _autoRenewToken) { await this._renewToken; } @@ -239,6 +263,7 @@ export class NooklinkUserApi { debug('fetch %s %s, response %s', method, url, response.status); if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) { + this._token_expired = true; const data = await response.json() as WebServiceError; // _renewToken will be awaited when calling fetch @@ -327,6 +352,7 @@ export class NooklinkUserApi { this.user_id = data.user_id; this.auth_token = data.token.token; this.gtoken = data.gtoken; + this._token_expired = false; } /** @internal */ diff --git a/src/api/splatnet2.ts b/src/api/splatnet2.ts index ae7d7b9..354c8a6 100644 --- a/src/api/splatnet2.ts +++ b/src/api/splatnet2.ts @@ -25,6 +25,8 @@ export const updateIksmSessionLastUsed: { } = {}; export default class SplatNet2Api { + protected _session_expired = false; + protected constructor( public iksm_session: string, public unique_id: string, @@ -32,6 +34,10 @@ export default class SplatNet2Api { ) {} async fetch(url: string, method = 'GET', body?: string | FormData, headers?: object) { + if (this._session_expired) { + throw new Error('Session expired'); + } + const [signal, cancel] = timeoutSignal(); const response = await fetch(SPLATNET2_URL + url, { method, @@ -52,6 +58,10 @@ export default class SplatNet2Api { debug('fetch %s %s, response %s', method, url, response.status); + if (response.status === 401) { + this._session_expired = true; + } + if (response.status !== 200) { throw new ErrorResponse('[splatnet2] Non-200 status code', response, await response.text()); } diff --git a/src/api/splatnet3.ts b/src/api/splatnet3.ts index 6756b6e..39594cc 100644 --- a/src/api/splatnet3.ts +++ b/src/api/splatnet3.ts @@ -74,9 +74,10 @@ enum MapQueriesMode { export default class SplatNet3Api { onTokenShouldRenew: ((remaining: number, res: Response) => Promise) | null = null; - onTokenExpired: ((res: Response) => Promise) | null = null; + onTokenExpired: ((res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + protected _token_expired = false; graphql_strict = process.env.NXAPI_SPLATNET3_STRICT !== '0'; @@ -93,8 +94,18 @@ export default class SplatNet3Api { async fetch( url: string, method = 'GET', body?: string | FormData, headers?: object, /** @internal */ _log?: string, - /** @internal */ _attempt = 0 + /** @internal */ _attempt = 0, ): Promise> { + if (this._token_expired && !this._renewToken) { + if (!this.onTokenExpired || _attempt) throw new Error('Token expired'); + + this._renewToken = this.onTokenExpired.call(null).then(data => { + if (data) this.setTokenWithSavedToken(data); + }).finally(() => { + this._renewToken = null; + }); + } + if (this._renewToken) { await this._renewToken; } @@ -121,6 +132,7 @@ export default class SplatNet3Api { response.status, version); if (response.status === 401 && !_attempt && this.onTokenExpired) { + this._token_expired = true; // _renewToken will be awaited when calling fetch this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, response).then(data => { if (data) this.setTokenWithSavedToken(data); @@ -933,6 +945,7 @@ export default class SplatNet3Api { this.version = data.version; this.language = data.bullet_token.lang; this.useragent = data.useragent; + this._token_expired = false; } static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) { diff --git a/src/api/znc-proxy.ts b/src/api/znc-proxy.ts index d208d9c..40c31c7 100644 --- a/src/api/znc-proxy.ts +++ b/src/api/znc-proxy.ts @@ -12,10 +12,12 @@ const debug = createDebug('nxapi:api:znc-proxy'); export default class ZncProxyApi implements CoralApi { // Not used by ZncProxyApi - onTokenExpired: ((data: CoralErrorResponse, res: Response) => Promise) | null = null; + onTokenExpired: ((data?: CoralErrorResponse, res?: Response) => Promise) | null = null; /** @internal */ _renewToken: Promise | null = null; + /** @internal */ + _token_expired = false; /** @internal */ na_id = ''; /** @internal */ diff --git a/src/client/splatnet3.ts b/src/client/splatnet3.ts index 1e9015c..4dfc71e 100644 --- a/src/client/splatnet3.ts +++ b/src/client/splatnet3.ts @@ -148,7 +148,7 @@ function createTokenExpiredHandler( data: {coral: Coral; auth_data: SavedToken; znc_proxy_url?: string}, ratelimit = true ) { - return (response: Response) => { + return (response?: Response) => { debug('Token expired, renewing'); return renewToken(session, splatnet, data, ratelimit); }; diff --git a/src/common/auth/coral.ts b/src/common/auth/coral.ts index dce8b35..4111117 100644 --- a/src/common/auth/coral.ts +++ b/src/common/auth/coral.ts @@ -99,7 +99,7 @@ function createTokenExpiredHandler( storage: persist.LocalStorage, token: string, nso: CoralApi, renew_token_data: {existingToken: SavedToken}, ratelimit = true ) { - return (data: CoralErrorResponse, response: Response) => { + return (data?: CoralErrorResponse, response?: Response) => { debug('Token expired', renew_token_data.existingToken.user.id, data); return renewToken(storage, token, nso, renew_token_data, ratelimit); }; diff --git a/src/common/auth/moon.ts b/src/common/auth/moon.ts index 0eed3c9..6bbf568 100644 --- a/src/common/auth/moon.ts +++ b/src/common/auth/moon.ts @@ -76,7 +76,7 @@ function createTokenExpiredHandler( storage: persist.LocalStorage, token: string, moon: MoonApi, renew_token_data: {existingToken: SavedMoonToken}, ratelimit = true ) { - return (data: MoonError, response: Response) => { + return (data?: MoonError, response?: Response) => { debug('Token expired', renew_token_data.existingToken.user.id, data); return renewToken(storage, token, moon, renew_token_data, ratelimit); }; diff --git a/src/common/auth/nooklink.ts b/src/common/auth/nooklink.ts index 33be277..aa17bb4 100644 --- a/src/common/auth/nooklink.ts +++ b/src/common/auth/nooklink.ts @@ -79,7 +79,7 @@ function createTokenExpiredHandler( renew_token_data: {existingToken: SavedToken; znc_proxy_url?: string}, ratelimit = true ) { - return (data: WebServiceError, response: Response) => { + return (data?: WebServiceError, response?: Response) => { debug('Token expired, renewing', data); return renewToken(storage, token, nooklink, renew_token_data, ratelimit); }; @@ -196,7 +196,7 @@ function createUserTokenExpiredHandler( renew_token_data: {existingToken: SavedUserToken; znc_proxy_url?: string; nooklink: NooklinkApi | null}, ratelimit = true ) { - return (data: WebServiceError, response: Response) => { + return (data?: WebServiceError, response?: Response) => { debug('Token expired', nooklinkuser.user_id, data); return renewUserToken(storage, token, nooklinkuser, renew_token_data); }; diff --git a/src/common/auth/splatnet3.ts b/src/common/auth/splatnet3.ts index d2897cf..f1b3343 100644 --- a/src/common/auth/splatnet3.ts +++ b/src/common/auth/splatnet3.ts @@ -79,7 +79,7 @@ function createTokenExpiredHandler( data: {existingToken: SavedBulletToken; znc_proxy_url?: string}, ratelimit = true ) { - return (response: Response) => { + return (response?: Response) => { debug('Token expired, renewing'); return renewToken(storage, token, splatnet, data, ratelimit); };