mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Use cached web service token to renew SplatNet 3 tokens and fix retrying getting web service tokens after renewing coral token
This commit is contained in:
parent
79b1d9eaae
commit
e2ac4bbfb3
|
|
@ -54,8 +54,9 @@ import CoralApi, { CoralAuthData, PartialCoralAuthData } from 'nxapi/coral';
|
|||
|
||||
const coral: CoralApi;
|
||||
const auth_data: CoralAuthData;
|
||||
const na_session_token: string;
|
||||
|
||||
const data = await coral.renewToken(auth_data);
|
||||
const data = await coral.renewToken(na_session_token, auth_data.user);
|
||||
// data is a plain object of type PartialCoralAuthData
|
||||
|
||||
const new_auth_data = Object.assign({}, auth_data, data);
|
||||
|
|
@ -63,6 +64,34 @@ const new_auth_data = Object.assign({}, auth_data, data);
|
|||
// new_auth_data should be saved and reused
|
||||
```
|
||||
|
||||
#### `CoralApi.onTokenExpired`
|
||||
|
||||
Function called when a `9404 Token expired` response is received from the API.
|
||||
|
||||
This function should either call `CoralApi.getToken` to renew the token, then return the `PartialCoralAuthData` object, or call `CoralApi.renewToken`.
|
||||
|
||||
```ts
|
||||
import CoralApi, { CoralAuthData } from 'nxapi/coral';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
const coral = CoralApi.createWithSavedToken(...);
|
||||
let auth_data: CoralAuthData;
|
||||
const na_session_token: string;
|
||||
|
||||
coral.onTokenExpired = async (response: Response) => {
|
||||
const data = await coral.getToken(na_session_token, auth_data.user);
|
||||
// data is a plain object of type PartialCoralAuthData
|
||||
|
||||
const new_auth_data = Object.assign({}, auth_data, data);
|
||||
// new_auth_data is a plain object of type CoralAuthData
|
||||
// new_auth_data should be saved and reused
|
||||
|
||||
auth_data = new_auth_data;
|
||||
|
||||
return data;
|
||||
};
|
||||
```
|
||||
|
||||
### `ZncProxyApi`
|
||||
|
||||
nxapi API proxy server client. Instances of this class are generally compatible with `CoralApi`; nxapi's command line interface and Electron apps internally use either depending on whether the API proxy is enabled.
|
||||
|
|
|
|||
|
|
@ -56,6 +56,68 @@ const splatnet = SplatNet3Api.createWithCliTokenData(data);
|
|||
// splatnet instanceof SplatNet3Api
|
||||
```
|
||||
|
||||
#### `SplatNet3Api.onTokenExpired`
|
||||
|
||||
Function called when a `401 Unauthorized` response is received from the API, meaning the token has expired.
|
||||
|
||||
This function should either call `SplatNet3Api.loginWithWebServiceToken` or `SplatNet3Api.loginWithCoral` to renew the token, then return the `SplatNet3AuthData` object, or call `SplatNet3Api.renewTokenWithWebServiceToken` or `SplatNet3Api.renewTokenWithCoral`. An existing web service token should be used to avoid calling the imink/flapg API, and then fall back to issuing a new token.
|
||||
|
||||
```ts
|
||||
import { ErrorResponse } from 'nxapi';
|
||||
import CoralApi, { CoralAuthData } from 'nxapi/coral';
|
||||
import SplatNet3Api, { SplatNet3AuthData } from 'nxapi/splatnet3';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
let splatnet3_auth_data: SplatNet3AuthData;
|
||||
const splatnet = SplatNet3Api.createWithSavedToken(splatnet3_auth_data);
|
||||
|
||||
splatnet.onTokenExpired = async (response: Response) => {
|
||||
try {
|
||||
// This should be cached - using stale data is fine, as only the user data is used
|
||||
const coral_auth_data: CoralAuthData;
|
||||
|
||||
const data = await SplatNet3Api.loginWithWebServiceToken(splatnet3_auth_data.webserviceToken, coral_auth_data.user);
|
||||
// data is a plain object of type SplatNet3AuthData
|
||||
// data should be saved and reused
|
||||
|
||||
splatnet3_auth_data = data;
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
// `401 Unauthorized` from `/api/bullet_tokens` means the web service token has expired (or is invalid)
|
||||
if (err instanceof ErrorResponse && err.response.status === 401) {
|
||||
const coral: CoralApi;
|
||||
const coral_auth_data: CoralAuthData;
|
||||
|
||||
const data = await SplatNet3Api.loginWithCoral(coral, coral_auth_data.user);
|
||||
// data is a plain object of type SplatNet3AuthData
|
||||
// data should be saved and reused
|
||||
|
||||
splatnet3_auth_data = data;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### `SplatNet3Api.onTokenShouldRenew`
|
||||
|
||||
Function called when the `x-bullettoken-remaining` header received from the API is less than 300, meaning the token will expire in 5 minutes and should be renewed in the background.
|
||||
|
||||
```ts
|
||||
import SplatNet3Api, { SplatNet3AuthData } from 'nxapi/splatnet3';
|
||||
import { Response } from 'node-fetch';
|
||||
|
||||
const splatnet = SplatNet3Api.createWithSavedToken(...);
|
||||
|
||||
splatnet.onTokenShouldRenew = async (remaining: number, response: Response) => {
|
||||
// See SplatNet3Api.onTokenExpired for an example
|
||||
};
|
||||
```
|
||||
|
||||
### API types
|
||||
|
||||
`nxapi/splatnet3` exports all API types from [src/api/splatnet3-types.ts](../../src/api/splatnet3-types.ts).
|
||||
|
|
|
|||
|
|
@ -224,6 +224,7 @@ export default class CoralApi {
|
|||
return await this.call<WebServiceToken>('/v2/Game/GetWebServiceToken', req, false);
|
||||
} catch (err) {
|
||||
if (err instanceof ErrorResponse && err.data.status === CoralStatus.TOKEN_EXPIRED && !_attempt && this.onTokenExpired) {
|
||||
debug('Error getting web service token, renewing token before retrying', err);
|
||||
// _renewToken will be awaited when calling getWebServiceToken
|
||||
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, err.data, err.response as Response).then(data => {
|
||||
if (data) this.setTokenWithSavedToken(data);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import * as util from 'node:util';
|
|||
import { Response as NodeFetchResponse } from 'node-fetch';
|
||||
|
||||
export const ResponseSymbol = Symbol('Response');
|
||||
const ErrorResponseSymbol = Symbol('IsErrorResponse');
|
||||
|
||||
export interface ResponseData<R> {
|
||||
[ResponseSymbol]: R;
|
||||
|
|
@ -24,6 +25,8 @@ export class ErrorResponse<T = unknown> extends Error {
|
|||
) {
|
||||
super(message);
|
||||
|
||||
Object.defineProperty(this, ErrorResponseSymbol, {enumerable: false, value: ErrorResponseSymbol});
|
||||
|
||||
if (typeof body === 'string') {
|
||||
this.body = body;
|
||||
try {
|
||||
|
|
@ -51,9 +54,6 @@ export class ErrorResponse<T = unknown> extends Error {
|
|||
Object.defineProperty(ErrorResponse, Symbol.hasInstance, {
|
||||
configurable: true,
|
||||
value: (instance: ErrorResponse) => {
|
||||
return instance instanceof Error &&
|
||||
'response' in instance &&
|
||||
'body' in instance &&
|
||||
'data' in instance;
|
||||
return instance && ErrorResponseSymbol in instance;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,8 +53,10 @@ export async function getBulletToken(
|
|||
await storage.setItem('BulletToken.' + token, existingToken);
|
||||
|
||||
const splatnet = SplatNet3Api.createWithSavedToken(existingToken);
|
||||
splatnet.onTokenExpired = createTokenExpiredHandler(storage, token, splatnet, existingToken, proxy_url);
|
||||
splatnet.onTokenShouldRenew = createTokenShouldRenewHandler(storage, token, splatnet, existingToken, proxy_url);
|
||||
|
||||
const renew_token_data = {existingToken, znc_proxy_url: proxy_url};
|
||||
splatnet.onTokenExpired = createTokenExpiredHandler(storage, token, splatnet, renew_token_data);
|
||||
splatnet.onTokenShouldRenew = createTokenShouldRenewHandler(storage, token, splatnet, renew_token_data);
|
||||
|
||||
return {splatnet, data: existingToken};
|
||||
}
|
||||
|
|
@ -64,45 +66,47 @@ export async function getBulletToken(
|
|||
const splatnet = SplatNet3Api.createWithSavedToken(existingToken);
|
||||
|
||||
if (allow_fetch_token) {
|
||||
splatnet.onTokenExpired = createTokenExpiredHandler(storage, token, splatnet, existingToken, proxy_url);
|
||||
splatnet.onTokenShouldRenew = createTokenShouldRenewHandler(storage, token, splatnet, existingToken, proxy_url);
|
||||
const renew_token_data = {existingToken, znc_proxy_url: proxy_url};
|
||||
splatnet.onTokenExpired = createTokenExpiredHandler(storage, token, splatnet, renew_token_data);
|
||||
splatnet.onTokenShouldRenew = createTokenShouldRenewHandler(storage, token, splatnet, renew_token_data);
|
||||
}
|
||||
|
||||
return {splatnet, data: existingToken};
|
||||
}
|
||||
|
||||
function createTokenExpiredHandler(
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api, existingToken: SavedBulletToken,
|
||||
znc_proxy_url?: string
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
|
||||
data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
|
||||
) {
|
||||
return (response: Response) => {
|
||||
debug('Token expired, renewing');
|
||||
return renewToken(storage, token, splatnet, existingToken, znc_proxy_url);
|
||||
return renewToken(storage, token, splatnet, data);
|
||||
};
|
||||
}
|
||||
|
||||
function createTokenShouldRenewHandler(
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api, existingToken: SavedBulletToken,
|
||||
znc_proxy_url?: string
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
|
||||
data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
|
||||
) {
|
||||
return (remaining: number, response: Response) => {
|
||||
debug('Token will expire in %d seconds, renewing', remaining);
|
||||
return renewToken(storage, token, splatnet, existingToken, znc_proxy_url);
|
||||
return renewToken(storage, token, splatnet, data);
|
||||
};
|
||||
}
|
||||
|
||||
async function renewToken(
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api, previousToken: SavedBulletToken,
|
||||
znc_proxy_url?: string
|
||||
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
|
||||
renew_token_data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
|
||||
) {
|
||||
try {
|
||||
const data: SavedToken | undefined = await storage.getItem('NsoToken.' + token);
|
||||
|
||||
if (data) {
|
||||
const existingToken: SavedBulletToken =
|
||||
await splatnet.renewTokenWithWebServiceToken(previousToken.webserviceToken, data.user);
|
||||
await splatnet.renewTokenWithWebServiceToken(renew_token_data.existingToken.webserviceToken, data.user);
|
||||
|
||||
await storage.setItem('BulletToken.' + token, existingToken);
|
||||
renew_token_data.existingToken = existingToken;
|
||||
|
||||
return;
|
||||
} else {
|
||||
|
|
@ -117,7 +121,7 @@ async function renewToken(
|
|||
}
|
||||
}
|
||||
|
||||
const {nso, data} = await getToken(storage, token, znc_proxy_url);
|
||||
const {nso, data} = await getToken(storage, token, renew_token_data.znc_proxy_url);
|
||||
|
||||
if (data[Login]) {
|
||||
const announcements = await nso.getAnnouncements();
|
||||
|
|
@ -129,4 +133,5 @@ async function renewToken(
|
|||
const existingToken: SavedBulletToken = await splatnet.renewTokenWithCoral(nso, data.user);
|
||||
|
||||
await storage.setItem('BulletToken.' + token, existingToken);
|
||||
renew_token_data.existingToken = existingToken;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user