Add automatic token renewal for Moon/NookLink and fix authentication limit with automatic token renewal

This commit is contained in:
Samuel Elliott 2022-12-19 09:15:47 +00:00
parent 4fcecd9f5b
commit 14e7793b7c
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
10 changed files with 384 additions and 38 deletions

View File

@ -56,6 +56,31 @@ const data = await moon.renewToken(na_session_token);
// data should be saved and reused
```
#### `MoonApi.onTokenExpired`
Function called when a `401 Unauthorized` response is received from the API.
This function should either call `MoonApi.loginWithSessionToken` to renew the token, then return the `MoonAuthData` object, or call `MoonApi.renewToken`.
```ts
import MoonApi, { MoonAuthData } from 'nxapi/moon';
import { Response } from 'node-fetch';
const moon = MoonApi.createWithSavedToken(...);
let auth_data: MoonAuthData;
const na_session_token: string;
moon.onTokenExpired = async (error: MoonError, response: Response) => {
const data = await MoonApi.loginWithSessionToken(na_session_token);
// data is a plain object of type MoonAuthData
// data should be saved and reused
auth_data = data;
return data;
};
```
### API types
`nxapi/moon` exports all API types from [src/api/moon-types.ts](../../src/api/moon-types.ts).

View File

@ -76,6 +76,31 @@ const {nooklinkuser, data} = await nooklink.createUserClient(user_id);
// data is a plain object of type NooklinkUserAuthData
```
#### `NooklinkApi.onTokenExpired`
Function called when a `401 Unauthorized` response is received from the API.
This function should either call `NooklinkApi.loginWithWebServiceToken` or `NooklinkApi.loginWithCoral` to renew the token, then return the `NooklinkAuthData` object, or call `NooklinkApi.renewTokenWithWebServiceToken` or `NooklinkApi.renewTokenWithCoral`.
```ts
import NooklinkApi, { NooklinkAuthData, WebServiceError } from 'nxapi/nooklink';
import { Response } from 'node-fetch';
const nooklink = NooklinkApi.createWithSavedToken(...);
let auth_data: NooklinkAuthData;
const na_session_token: string;
nooklink.onTokenExpired = async (error: WebServiceError, response: Response) => {
const data = await NooklinkApi.loginWithSessionToken(na_session_token);
// data is a plain object of type NooklinkAuthData
// data should be saved and reused
auth_data = data;
return data;
};
```
### `NooklinkUserApi`
NookLink ACNH-level API client. An instance of this class should not be created directly; instead `NooklinkApi.createUserClient` or one of the `createWith*` static methods should be used.
@ -110,6 +135,37 @@ const nooklinkuser = NooklinkUserApi.createWithCliTokenData(data);
// nooklinkuser instanceof NooklinkUserApi
```
#### `NooklinkUserApi.onTokenExpired`
Function called when a `401 Unauthorized` response is received from the API.
This function should either call `NooklinkUserApi.getToken` to renew the token, then return the `PartialNooklinkUserAuthData` object, or call `NooklinkUserApi.renewToken`.
```ts
import NooklinkApi, { NooklinkAuthData, NooklinkUserApi, PartialNooklinkUserAuthData, WebServiceError } from 'nxapi/nooklink';
import { Response } from 'node-fetch';
const nooklink: NooklinkApi;
const nooklinkuser = NooklinkUserApi.createWithSavedToken(...);
let auth_data: NooklinkUserAuthData;
const na_session_token: string;
nooklinkuser.onTokenExpired = async (error: WebServiceError, response: Response) => {
const data = await nooklinkuser.getToken(nooklink);
// data is a plain object of type PartialNooklinkUserAuthData
// data should be saved and reused
const new_auth_data = Object.assign({}, auth_data, data);
// new_auth_data is a plain object of type NooklinkUserAuthData
// new_auth_data should be saved and reused
auth_data = new_auth_data;
return data;
};
```
### API types
`nxapi/nooklink` exports all API types from [src/api/nooklink-types.ts](../../src/api/nooklink-types.ts).

View File

@ -1,7 +1,7 @@
import fetch from 'node-fetch';
import fetch, { Response } from 'node-fetch';
import createDebug from 'debug';
import { getNintendoAccountToken, getNintendoAccountUser, NintendoAccountToken, NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import { DailySummaries, Devices, MonthlySummaries, MonthlySummary, MoonError, ParentalControlSettingState, SmartDevices, User } from './moon-types.js';
import { timeoutSignal } from '../util/misc.js';
@ -16,6 +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<MoonAuthData | PartialMoonAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
protected constructor(
public token: string,
public naId: string,
@ -24,7 +28,15 @@ export default class MoonApi {
readonly znma_useragent = ZNMA_USER_AGENT,
) {}
async fetch<T extends object>(url: string, method = 'GET', body?: string, headers?: object) {
async fetch<T extends object>(
url: string, method = 'GET', body?: string, headers?: object,
/** @internal */ _autoRenewToken = true,
/** @internal */ _attempt = 0
): Promise<HasResponse<T, Response>> {
if (this._renewToken && _autoRenewToken) {
await this._renewToken;
}
const [signal, cancel] = timeoutSignal();
const response = await fetch(MOON_URL + url, {
method,
@ -49,6 +61,18 @@ export default class MoonApi {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
const data = await response.json() as MoonError;
// _renewToken will be awaited when calling fetch
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => {
if (data) this.setTokenWithSavedToken(data);
}).finally(() => {
this._renewToken = null;
});
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
}
if (response.status !== 200) {
throw new ErrorResponse('[moon] Non-200 status code', response, await response.text());
}
@ -92,13 +116,15 @@ export default class MoonApi {
async renewToken(token: string) {
const data = await MoonApi.loginWithSessionToken(token);
this.token = data.nintendoAccountToken.access_token!;
this.naId = data.user.id;
this.setTokenWithSavedToken(data);
return data;
}
private setTokenWithSavedToken(data: MoonAuthData | PartialMoonAuthData) {
this.token = data.nintendoAccountToken.access_token!;
if ('user' in data) this.naId = data.user.id;
}
static async createWithSessionToken(token: string) {
const data = await this.loginWithSessionToken(token);
return {moon: this.createWithSavedToken(data), data};
@ -145,3 +171,6 @@ export interface MoonAuthData {
znma_build: string;
znma_useragent: string;
}
export interface PartialMoonAuthData {
nintendoAccountToken: NintendoAccountToken;
}

View File

@ -1,8 +1,8 @@
import fetch from 'node-fetch';
import fetch, { Response } from 'node-fetch';
import createDebug from 'debug';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse } from './util.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import CoralApi from './coral.js';
import { WebServiceError, Users, AuthToken, UserProfile, Newspapers, Newspaper, Emoticons, Reaction, IslandProfile } from './nooklink-types.js';
import { timeoutSignal } from '../util/misc.js';
@ -17,13 +17,25 @@ const NOOKLINK_URL = NOOKLINK_WEBSERVICE_URL + '/api';
const BLANCO_VERSION = '2.1.1';
export default class NooklinkApi {
onTokenExpired: ((data: WebServiceError, res: Response) => Promise<NooklinkAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
protected constructor(
public gtoken: string,
public useragent: string,
readonly client_version = BLANCO_VERSION,
) {}
async fetch<T extends object>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
async fetch<T extends object>(
url: string, method = 'GET', body?: string | FormData, headers?: object,
/** @internal */ _autoRenewToken = true,
/** @internal */ _attempt = 0
): Promise<HasResponse<T, Response>> {
if (this._renewToken && _autoRenewToken) {
await this._renewToken;
}
const [signal, cancel] = timeoutSignal();
const response = await fetch(NOOKLINK_URL + url, {
method,
@ -44,6 +56,18 @@ export default class NooklinkApi {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
const data = await response.json() as WebServiceError;
// _renewToken will be awaited when calling fetch
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => {
if (data) this.setTokenWithSavedToken(data);
}).finally(() => {
this._renewToken = null;
});
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
}
if (response.status !== 200 && response.status !== 201) {
throw new ErrorResponse('[nooklink] Non-200/201 status code', response, await response.text());
}
@ -71,6 +95,22 @@ export default class NooklinkApi {
return NooklinkUserApi._createWithNooklinkApi(this, user_id);
}
async renewTokenWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await NooklinkApi.loginWithCoral(nso, user);
this.setTokenWithSavedToken(data);
return data;
}
async renewTokenWithWebServiceToken(webserviceToken: WebServiceToken, user: NintendoAccountUser) {
const data = await NooklinkApi.loginWithWebServiceToken(webserviceToken, user);
this.setTokenWithSavedToken(data);
return data;
}
private setTokenWithSavedToken(data: NooklinkAuthData) {
this.gtoken = data.gtoken;
}
static async createWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await this.loginWithCoral(nso, user);
return {nooklink: this.createWithSavedToken(data), data};
@ -155,6 +195,10 @@ export default class NooklinkApi {
}
export class NooklinkUserApi {
onTokenExpired: ((data: WebServiceError, res: Response) => Promise<NooklinkUserAuthData | PartialNooklinkUserAuthData | void>) | null = null;
/** @internal */
_renewToken: Promise<void> | null = null;
protected constructor(
public user_id: string,
public auth_token: string,
@ -164,7 +208,15 @@ export class NooklinkUserApi {
readonly client_version = BLANCO_VERSION,
) {}
async fetch<T extends object>(url: string, method = 'GET', body?: string | FormData, headers?: object) {
async fetch<T extends object>(
url: string, method = 'GET', body?: string | FormData, headers?: object,
/** @internal */ _autoRenewToken = true,
/** @internal */ _attempt = 0
): Promise<HasResponse<T, Response>> {
if (this._renewToken && _autoRenewToken) {
await this._renewToken;
}
const [signal, cancel] = timeoutSignal();
const response = await fetch(NOOKLINK_URL + url, {
method,
@ -186,6 +238,18 @@ export class NooklinkUserApi {
debug('fetch %s %s, response %s', method, url, response.status);
if (response.status === 401 && _autoRenewToken && !_attempt && this.onTokenExpired) {
const data = await response.json() as WebServiceError;
// _renewToken will be awaited when calling fetch
this._renewToken = this._renewToken ?? this.onTokenExpired.call(null, data, response).then(data => {
if (data) this.setTokenWithSavedToken(data);
}).finally(() => {
this._renewToken = null;
});
return this.fetch(url, method, body, headers, _autoRenewToken, _attempt + 1);
}
if (response.status !== 200 && response.status !== 201) {
throw new ErrorResponse('[nooklink] Non-200/201 status code', response, await response.text());
}
@ -243,6 +307,28 @@ export class NooklinkUserApi {
return this.postMessage(reaction.label, MessageType.EMOTICON);
}
async getToken(client: NooklinkApi): Promise<PartialNooklinkUserAuthData> {
const token = await client.getAuthToken(this.user_id);
return {
gtoken: client.gtoken,
user_id: this.user_id,
token,
};
}
async renewToken(client: NooklinkApi) {
const data = await this.getToken(client);
this.setTokenWithSavedToken(data);
return data;
}
private setTokenWithSavedToken(data: NooklinkUserAuthData | PartialNooklinkUserAuthData) {
this.user_id = data.user_id;
this.auth_token = data.token.token;
this.gtoken = data.gtoken;
}
/** @internal */
static async _loginWithNooklinkApi(client: NooklinkApi, user_id: string): Promise<NooklinkUserAuthData> {
const token = await client.getAuthToken(user_id);
@ -300,6 +386,8 @@ export interface NooklinkUserAuthData {
token: AuthToken;
language: string;
}
export type PartialNooklinkUserAuthData =
Pick<NooklinkUserAuthData, 'gtoken' | 'user_id' | 'token'>;
export interface NooklinkUserCliTokenData {
gtoken: string;

View File

@ -73,7 +73,7 @@ export async function getToken(
expires_at: Date.now() + (data.credential.expiresIn * 1000),
};
nso.onTokenExpired = createTokenExpiredHandler(storage, token, nso, existingToken);
nso.onTokenExpired = createTokenExpiredHandler(storage, token, nso, {existingToken});
await storage.setItem('NsoToken.' + token, existingToken);
await storage.setItem('NintendoAccountToken.' + data.user.id, token);
@ -90,30 +90,38 @@ export async function getToken(
new ZncProxyApi(proxy_url, token) :
CoralApi.createWithSavedToken(existingToken);
nso.onTokenExpired = createTokenExpiredHandler(storage, token, nso, existingToken);
nso.onTokenExpired = createTokenExpiredHandler(storage, token, nso, {existingToken});
return {nso, data: existingToken};
}
function createTokenExpiredHandler(
storage: persist.LocalStorage, token: string, nso: CoralApi, existingToken: SavedToken
storage: persist.LocalStorage, token: string, nso: CoralApi,
renew_token_data: {existingToken: SavedToken}, ratelimit = true
) {
return (data: CoralErrorResponse, response: Response) => {
debug('Token expired', existingToken.user.id, data);
return renewToken(storage, token, nso, existingToken);
debug('Token expired', renew_token_data.existingToken.user.id, data);
return renewToken(storage, token, nso, renew_token_data, ratelimit);
};
}
async function renewToken(
storage: persist.LocalStorage, token: string, nso: CoralApi, previousToken: SavedToken
storage: persist.LocalStorage, token: string, nso: CoralApi,
renew_token_data: {existingToken: SavedToken}, ratelimit = true
) {
const data = await nso.renewToken(token, previousToken.user);
if (ratelimit) {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
await checkUseLimit(storage, 'coral', jwt.payload.sub, ratelimit);
}
const data = await nso.renewToken(token, renew_token_data.existingToken.user);
const existingToken: SavedToken = {
...previousToken,
...renew_token_data.existingToken,
...data,
expires_at: Date.now() + (data.credential.expiresIn * 1000),
};
await storage.setItem('NsoToken.' + token, existingToken);
renew_token_data.existingToken = existingToken;
}

View File

@ -1,10 +1,12 @@
import createDebug from 'debug';
import * as persist from 'node-persist';
import { Response } from 'node-fetch';
import { MoonAuthData, ZNMA_CLIENT_ID } from '../../api/moon.js';
import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js';
import { Jwt } from '../../util/jwt.js';
import MoonApi from '../../api/moon.js';
import { checkUseLimit, LIMIT_REQUESTS, SHOULD_LIMIT_USE } from './util.js';
import { MoonError } from '../../api/moon-types.js';
const debug = createDebug('nxapi:auth:moon');
@ -53,6 +55,8 @@ export async function getPctlToken(storage: persist.LocalStorage, token: string,
expires_at: Date.now() + (data.nintendoAccountToken.expires_in * 1000),
};
moon.onTokenExpired = createTokenExpiredHandler(storage, token, moon, {existingToken});
await storage.setItem('MoonToken.' + token, existingToken);
await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, token);
@ -61,9 +65,40 @@ export async function getPctlToken(storage: persist.LocalStorage, token: string,
debug('Using existing token');
await storage.setItem('NintendoAccountToken-pctl.' + existingToken.user.id, token);
const moon = MoonApi.createWithSavedToken(existingToken);
moon.onTokenExpired = createTokenExpiredHandler(storage, token, moon, {existingToken});
return {
moon: MoonApi.createWithSavedToken(existingToken),
data: existingToken,
return {moon, data: existingToken};
}
function createTokenExpiredHandler(
storage: persist.LocalStorage, token: string, moon: MoonApi,
renew_token_data: {existingToken: SavedMoonToken}, ratelimit = true
) {
return (data: MoonError, response: Response) => {
debug('Token expired', renew_token_data.existingToken.user.id, data);
return renewToken(storage, token, moon, renew_token_data, ratelimit);
};
}
async function renewToken(
storage: persist.LocalStorage, token: string, moon: MoonApi,
renew_token_data: {existingToken: SavedMoonToken}, ratelimit = true
) {
if (ratelimit) {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
await checkUseLimit(storage, 'moon', jwt.payload.sub, ratelimit, [LIMIT_REQUESTS, LIMIT_PERIOD]);
}
const data = await moon.renewToken(token);
const existingToken: SavedMoonToken = {
...renew_token_data.existingToken,
...data,
expires_at: Date.now() + (data.nintendoAccountToken.expires_in * 1000),
};
await storage.setItem('MoonToken.' + token, existingToken);
renew_token_data.existingToken = existingToken;
}

View File

@ -1,8 +1,9 @@
import createDebug from 'debug';
import persist from 'node-persist';
import { Response } from 'node-fetch';
import { getToken, Login } from './coral.js';
import NooklinkApi, { NooklinkAuthData, NooklinkUserApi, NooklinkUserAuthData } from '../../api/nooklink.js';
import { Users } from '../../api/nooklink-types.js';
import { Users, WebServiceError } from '../../api/nooklink-types.js';
import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js';
import { Jwt } from '../../util/jwt.js';
import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js';
@ -51,20 +52,64 @@ export async function getWebServiceToken(
await storage.setItem('NookToken.' + token, existingToken);
return {
nooklink: NooklinkApi.createWithSavedToken(existingToken),
data: existingToken,
};
const nooklink = NooklinkApi.createWithSavedToken(existingToken);
nooklink.onTokenExpired = createTokenExpiredHandler(storage, token, nooklink, {
existingToken,
znc_proxy_url: proxy_url,
});
return {nooklink, data: existingToken};
}
debug('Using existing web service token');
return {
nooklink: NooklinkApi.createWithSavedToken(existingToken),
data: existingToken,
const nooklink = NooklinkApi.createWithSavedToken(existingToken);
nooklink.onTokenExpired = createTokenExpiredHandler(storage, token, nooklink, {
existingToken,
znc_proxy_url: proxy_url,
});
return {nooklink, data: existingToken};
}
function createTokenExpiredHandler(
storage: persist.LocalStorage, token: string, nooklink: NooklinkApi,
renew_token_data: {existingToken: SavedToken; znc_proxy_url?: string},
ratelimit = true
) {
return (data: WebServiceError, response: Response) => {
debug('Token expired, renewing', data);
return renewToken(storage, token, nooklink, renew_token_data, ratelimit);
};
}
async function renewToken(
storage: persist.LocalStorage, token: string, nooklink: NooklinkApi,
renew_token_data: {existingToken: SavedToken; znc_proxy_url?: string},
ratelimit = true
) {
if (ratelimit) {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
await checkUseLimit(storage, 'nooklink', jwt.payload.sub);
}
const {nso, data} = await getToken(storage, token, renew_token_data.znc_proxy_url);
if (data[Login]) {
const announcements = await nso.getAnnouncements();
const friends = await nso.getFriendList();
const webservices = await nso.getWebServices();
const activeevent = await nso.getActiveEvent();
}
const existingToken: SavedToken = await nooklink.renewTokenWithCoral(nso, data.user);
await storage.setItem('NookToken.' + token, existingToken);
renew_token_data.existingToken = existingToken;
}
export interface SavedUserToken extends NooklinkUserAuthData {}
type PromiseValue<T> = T extends PromiseLike<infer R> ? R : never;
@ -122,6 +167,12 @@ export async function getUserToken(
const {nooklinkuser, data} = await nooklink.createUserClient(user);
const existingToken: SavedUserToken = data;
nooklinkuser.onTokenExpired = createUserTokenExpiredHandler(storage, nintendoAccountToken, nooklinkuser, {
existingToken,
znc_proxy_url: proxy_url,
nooklink,
});
await storage.setItem('NookAuthToken.' + nintendoAccountToken + '.' + user, existingToken);
return {nooklinkuser, data: existingToken};
@ -129,8 +180,52 @@ export async function getUserToken(
debug('Using existing NookLink auth token');
return {
nooklinkuser: NooklinkUserApi.createWithSavedToken(existingToken),
data: existingToken,
const nooklinkuser = NooklinkUserApi.createWithSavedToken(existingToken);
nooklinkuser.onTokenExpired = createUserTokenExpiredHandler(storage, nintendoAccountToken, nooklinkuser, {
existingToken,
znc_proxy_url: proxy_url,
nooklink: null,
});
return {nooklinkuser, data: existingToken};
}
function createUserTokenExpiredHandler(
storage: persist.LocalStorage, token: string, nooklinkuser: NooklinkUserApi,
renew_token_data: {existingToken: SavedUserToken; znc_proxy_url?: string; nooklink: NooklinkApi | null},
ratelimit = true
) {
return (data: WebServiceError, response: Response) => {
debug('Token expired', nooklinkuser.user_id, data);
return renewUserToken(storage, token, nooklinkuser, renew_token_data);
};
}
async function renewUserToken(
storage: persist.LocalStorage, token: string, nooklinkuser: NooklinkUserApi,
renew_token_data: {existingToken: SavedUserToken; znc_proxy_url?: string; nooklink: NooklinkApi | null},
ratelimit = true
) {
if (!renew_token_data.nooklink) {
const wst = await getWebServiceToken(storage, token, renew_token_data.znc_proxy_url, true, ratelimit);
const {nooklink, data: webserviceToken} = wst;
renew_token_data.nooklink = nooklink;
}
if (ratelimit) {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
await checkUseLimit(storage, 'nooklink-user', jwt.payload.sub);
}
const data = await nooklinkuser.renewToken(renew_token_data.nooklink);
const existingToken: SavedUserToken = {
...renew_token_data.existingToken,
...data,
};
await storage.setItem('NookAuthToken.' + token + '.' + nooklinkuser.user_id, existingToken);
renew_token_data.existingToken = existingToken;
}

View File

@ -76,28 +76,36 @@ export async function getBulletToken(
function createTokenExpiredHandler(
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
data: {existingToken: SavedBulletToken; znc_proxy_url?: string},
ratelimit = true
) {
return (response: Response) => {
debug('Token expired, renewing');
return renewToken(storage, token, splatnet, data);
return renewToken(storage, token, splatnet, data, ratelimit);
};
}
function createTokenShouldRenewHandler(
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
data: {existingToken: SavedBulletToken; znc_proxy_url?: string},
ratelimit = true
) {
return (remaining: number, response: Response) => {
debug('Token will expire in %d seconds, renewing', remaining);
return renewToken(storage, token, splatnet, data);
return renewToken(storage, token, splatnet, data, ratelimit);
};
}
async function renewToken(
storage: persist.LocalStorage, token: string, splatnet: SplatNet3Api,
renew_token_data: {existingToken: SavedBulletToken; znc_proxy_url?: string}
renew_token_data: {existingToken: SavedBulletToken; znc_proxy_url?: string},
ratelimit = true
) {
if (ratelimit) {
const [jwt, sig] = Jwt.decode<NintendoAccountSessionTokenJwtPayload>(token);
await checkUseLimit(storage, 'splatnet3', jwt.payload.sub);
}
try {
const data: SavedToken | undefined = await storage.getItem('NsoToken.' + token);

View File

@ -1,6 +1,7 @@
export {
default,
MoonAuthData,
PartialMoonAuthData,
} from '../api/moon.js';
export * from '../api/moon-types.js';

View File

@ -4,6 +4,7 @@ export {
NooklinkUserApi,
NooklinkUserAuthData,
PartialNooklinkUserAuthData,
NooklinkUserCliTokenData,
MessageType,