diff --git a/README.md b/README.md index 8aa33b3..0e59666 100644 --- a/README.md +++ b/README.md @@ -758,7 +758,7 @@ nxapi exports it's API library and types. [See src/exports.](src/exports) Example authenticating to the Nintendo Switch Online app: -> This is a simplified example of authenticating to the Coral API and using cached tokens. More logic is required to ensure you are using these APIs properly - [see src/common/auth/nso.ts for the authentication functions used in nxapi's CLI and Electron app](src/common/auth/nso.ts). +> This is a simplified example of authenticating to the Coral API and using cached tokens. More logic is required to ensure you are using these APIs properly - [see src/common/auth/coral.ts for the authentication functions used in nxapi's CLI and Electron app](src/common/auth/coral.ts). ```ts import { addUserAgent } from 'nxapi'; diff --git a/src/api/znc-proxy.ts b/src/api/znc-proxy.ts index 7af41c3..3509ef0 100644 --- a/src/api/znc-proxy.ts +++ b/src/api/znc-proxy.ts @@ -4,7 +4,7 @@ import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, Prese import { ErrorResponse } from './util.js'; import CoralApi from './coral.js'; import { NintendoAccountUser } from './na.js'; -import { SavedToken } from '../common/auth/nso.js'; +import { SavedToken } from '../common/auth/coral.js'; import { timeoutSignal } from '../util/misc.js'; import { getAdditionalUserAgents, getUserAgent } from '../util/useragent.js'; diff --git a/src/app/browser/util.tsx b/src/app/browser/util.tsx index bc21c9c..5b46e98 100644 --- a/src/app/browser/util.tsx +++ b/src/app/browser/util.tsx @@ -6,7 +6,7 @@ import { ErrorResponse } from '../../api/util.js'; import { DiscordPresence } from '../../discord/util.js'; import ipc, { events } from './ipc.js'; import { NintendoAccountUser } from '../../api/na.js'; -import { SavedToken } from '../../common/auth/nso.js'; +import { SavedToken } from '../../common/auth/coral.js'; import { SavedMoonToken } from '../../common/auth/moon.js'; import { BACKGROUND_COLOUR_MAIN_DARK, BACKGROUND_COLOUR_MAIN_LIGHT, DEFAULT_ACCENT_COLOUR } from './constants.js'; diff --git a/src/app/main/index.ts b/src/app/main/index.ts index acf084b..84369a0 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -175,7 +175,8 @@ export class Store extends EventEmitter { ) { super(); - this.users = Users.coral(storage, process.env.ZNC_PROXY_URL); + // ratelimit = false, as most users.get calls are triggered by user interaction (or at startup) + this.users = Users.coral(storage, process.env.ZNC_PROXY_URL, false); } async saveMonitorState(monitors: PresenceMonitorManager) { diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts index 6c54841..48fa96b 100644 --- a/src/app/main/menu.ts +++ b/src/app/main/menu.ts @@ -5,7 +5,7 @@ import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js'; import { App } from './index.js'; import { WebService } from '../../api/coral-types.js'; import openWebService from './webservices.js'; -import { SavedToken } from '../../common/auth/nso.js'; +import { SavedToken } from '../../common/auth/coral.js'; import { SavedMoonToken } from '../../common/auth/moon.js'; import { dev, dir } from '../../util/product.js'; import { EmbeddedPresenceMonitor, EmbeddedProxyPresenceMonitor } from './monitor.js'; diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts index 34f7d8d..1781712 100644 --- a/src/app/main/na-auth.ts +++ b/src/app/main/na-auth.ts @@ -6,7 +6,7 @@ import { BrowserWindow, dialog, MessageBoxOptions, Notification, session, shell import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js'; import { ZNCA_CLIENT_ID } from '../../api/coral.js'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; -import { getToken, SavedToken } from '../../common/auth/nso.js'; +import { getToken, SavedToken } from '../../common/auth/coral.js'; import { getPctlToken, SavedMoonToken } from '../../common/auth/moon.js'; import { Jwt } from '../../util/jwt.js'; import { tryGetNativeImageFromUrl } from './util.js'; diff --git a/src/app/main/webservices.ts b/src/app/main/webservices.ts index cbad5a4..da8faa6 100644 --- a/src/app/main/webservices.ts +++ b/src/app/main/webservices.ts @@ -10,7 +10,7 @@ import { dev } from '../../util/product.js'; import { WebService } from '../../api/coral-types.js'; import { Store } from './index.js'; import type { NativeShareRequest, NativeShareUrlRequest } from '../preload-webservice/znca-js-api.js'; -import { SavedToken } from '../../common/auth/nso.js'; +import { SavedToken } from '../../common/auth/coral.js'; import { createWebServiceWindow } from './windows.js'; const debug = createDebug('app:main:webservices'); diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index e226a2d..bd2ec5a 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -5,7 +5,7 @@ import createDebug from 'debug'; import type { User } from 'discord-rpc'; import type { SharingItem } from '../main/electron.js'; import type { DiscordPresenceConfiguration, DiscordPresenceSource, WindowConfiguration } from '../common/types.js'; -import type { SavedToken } from '../../common/auth/nso.js'; +import type { SavedToken } from '../../common/auth/coral.js'; import type { SavedMoonToken } from '../../common/auth/moon.js'; import type { UpdateCacheData } from '../../common/update.js'; import type { Announcements, CurrentUser, Friend, GetActiveEventResult, WebService, WebServices } from '../../api/coral-types.js'; diff --git a/src/cli/nso/add-friend.ts b/src/cli/nso/add-friend.ts index eeff4f0..43716ea 100644 --- a/src/cli/nso/add-friend.ts +++ b/src/cli/nso/add-friend.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:add-friend'); diff --git a/src/cli/nso/announcements.ts b/src/cli/nso/announcements.ts index c5b346f..03ab741 100644 --- a/src/cli/nso/announcements.ts +++ b/src/cli/nso/announcements.ts @@ -3,7 +3,7 @@ import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:announcements'); diff --git a/src/cli/nso/auth.ts b/src/cli/nso/auth.ts index 61abce0..9d85f41 100644 --- a/src/cli/nso/auth.ts +++ b/src/cli/nso/auth.ts @@ -3,7 +3,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import { getNintendoAccountSessionToken } from '../../api/na.js'; import { ZNCA_CLIENT_ID } from '../../api/coral.js'; import prompt from '../util/prompt.js'; diff --git a/src/cli/nso/friendcode.ts b/src/cli/nso/friendcode.ts index b442394..6b6dd8b 100644 --- a/src/cli/nso/friendcode.ts +++ b/src/cli/nso/friendcode.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:friendcode'); diff --git a/src/cli/nso/friends.ts b/src/cli/nso/friends.ts index c23dce0..4b5e290 100644 --- a/src/cli/nso/friends.ts +++ b/src/cli/nso/friends.ts @@ -5,7 +5,7 @@ import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; import { hrduration } from '../../util/misc.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:friends'); diff --git a/src/cli/nso/http-server.ts b/src/cli/nso/http-server.ts index a8f5f3d..2307092 100644 --- a/src/cli/nso/http-server.ts +++ b/src/cli/nso/http-server.ts @@ -9,7 +9,7 @@ import CoralApi from '../../api/coral.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken, SavedToken } from '../../common/auth/nso.js'; +import { getToken, SavedToken } from '../../common/auth/coral.js'; import { NotificationManager, PresenceEvent, ZncNotifications } from '../../common/notify.js'; import { product } from '../../util/product.js'; import { parseListenAddress } from '../../util/net.js'; diff --git a/src/cli/nso/lookup.ts b/src/cli/nso/lookup.ts index d6d4001..46a11a3 100644 --- a/src/cli/nso/lookup.ts +++ b/src/cli/nso/lookup.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:lookup'); diff --git a/src/cli/nso/notify.ts b/src/cli/nso/notify.ts index 84e92c4..f18d020 100644 --- a/src/cli/nso/notify.ts +++ b/src/cli/nso/notify.ts @@ -4,7 +4,7 @@ import persist from 'node-persist'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import { getIksmToken } from '../../common/auth/splatnet2.js'; import { EmbeddedSplatNet2Monitor, NotificationManager, ZncNotifications } from '../../common/notify.js'; import { CurrentUser, Friend, Game } from '../../api/coral-types.js'; diff --git a/src/cli/nso/permissions.ts b/src/cli/nso/permissions.ts index f7c3a35..1d820aa 100644 --- a/src/cli/nso/permissions.ts +++ b/src/cli/nso/permissions.ts @@ -3,7 +3,7 @@ import { PresencePermissions } from '../../api/coral-types.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:permissions'); diff --git a/src/cli/nso/presence.ts b/src/cli/nso/presence.ts index e43f314..5a977a1 100644 --- a/src/cli/nso/presence.ts +++ b/src/cli/nso/presence.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import { DiscordPresencePlayTime } from '../../discord/util.js'; import { handleEnableSplatNet2Monitoring, TerminalNotificationManager } from './notify.js'; import { ZncDiscordPresence, ZncProxyDiscordPresence } from '../../common/presence.js'; diff --git a/src/cli/nso/token.ts b/src/cli/nso/token.ts index 530bd0a..b299eab 100644 --- a/src/cli/nso/token.ts +++ b/src/cli/nso/token.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import prompt from '../util/prompt.js'; const debug = createDebug('cli:nso:token'); diff --git a/src/cli/nso/user.ts b/src/cli/nso/user.ts index e538bd1..fbe4d77 100644 --- a/src/cli/nso/user.ts +++ b/src/cli/nso/user.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:user'); diff --git a/src/cli/nso/webservices.ts b/src/cli/nso/webservices.ts index 91691bb..d72f573 100644 --- a/src/cli/nso/webservices.ts +++ b/src/cli/nso/webservices.ts @@ -3,7 +3,7 @@ import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:webservices'); diff --git a/src/cli/nso/webservicetoken.ts b/src/cli/nso/webservicetoken.ts index 5c18382..dadb087 100644 --- a/src/cli/nso/webservicetoken.ts +++ b/src/cli/nso/webservicetoken.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; const debug = createDebug('cli:nso:webservicetoken'); diff --git a/src/cli/nso/znc-proxy-tokens.ts b/src/cli/nso/znc-proxy-tokens.ts index 32b2b83..a8c315f 100644 --- a/src/cli/nso/znc-proxy-tokens.ts +++ b/src/cli/nso/znc-proxy-tokens.ts @@ -2,7 +2,7 @@ import createDebug from 'debug'; import fetch from 'node-fetch'; import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import { AuthPolicy, AuthToken } from '../../api/znc-proxy.js'; import { Argv } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; diff --git a/src/cli/users.ts b/src/cli/users.ts index 7f3d729..a300f3b 100644 --- a/src/cli/users.ts +++ b/src/cli/users.ts @@ -3,7 +3,7 @@ import Table from './util/table.js'; import type { Arguments as ParentArguments } from '../cli.js'; import { Argv } from '../util/yargs.js'; import { initStorage } from '../util/storage.js'; -import { SavedToken } from '../common/auth/nso.js'; +import { SavedToken } from '../common/auth/coral.js'; import { SavedMoonToken } from '../common/auth/moon.js'; const debug = createDebug('cli:users'); diff --git a/src/cli/util/discord-activity.ts b/src/cli/util/discord-activity.ts index b794129..460a7c0 100644 --- a/src/cli/util/discord-activity.ts +++ b/src/cli/util/discord-activity.ts @@ -7,7 +7,7 @@ import type { Arguments as ParentArguments } from '../util.js'; import { DiscordPresenceContext, DiscordPresencePlayTime, getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js'; import { ArgumentsCamelCase, Argv, YargsArguments } from '../../util/yargs.js'; import { initStorage } from '../../util/storage.js'; -import { getToken } from '../../common/auth/nso.js'; +import { getToken } from '../../common/auth/coral.js'; import { timeoutSignal } from '../../util/misc.js'; import { getUserAgent } from '../../util/useragent.js'; diff --git a/src/common/auth/nso.ts b/src/common/auth/coral.ts similarity index 87% rename from src/common/auth/nso.ts rename to src/common/auth/coral.ts index 12aaecb..927d3c5 100644 --- a/src/common/auth/nso.ts +++ b/src/common/auth/coral.ts @@ -7,6 +7,7 @@ import { Jwt } from '../../util/jwt.js'; import { AccountLogin, CoralErrorResponse } from '../../api/coral-types.js'; import CoralApi, { ZNCA_CLIENT_ID } from '../../api/coral.js'; import ZncProxyApi from '../../api/znc-proxy.js'; +import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js'; const debug = createDebug('nxapi:auth:nso'); @@ -24,15 +25,21 @@ export interface SavedToken { proxy_url?: string; } -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url: string): Promise<{ +export async function getToken( + storage: persist.LocalStorage, token: string, proxy_url: string, ratelimit?: boolean +): Promise<{ nso: ZncProxyApi; data: SavedToken; }> -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string): Promise<{ +export async function getToken( + storage: persist.LocalStorage, token: string, proxy_url?: string, ratelimit?: boolean +): Promise<{ nso: CoralApi; data: SavedToken; }> -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string) { +export async function getToken( + storage: persist.LocalStorage, token: string, proxy_url?: string, ratelimit = SHOULD_LIMIT_USE +) { if (!token) { console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nxapi nso token`.'); throw new Error('Invalid token'); @@ -58,6 +65,8 @@ export async function getToken(storage: persist.LocalStorage, token: string, pro const existingToken: SavedToken | undefined = await storage.getItem('NsoToken.' + token); if (!existingToken || existingToken.expires_at <= Date.now()) { + if (ratelimit) await checkUseLimit(storage, 'coral', jwt.payload.sub); + console.warn('Authenticating to Nintendo Switch Online app'); debug('Authenticating to znc with session token'); diff --git a/src/common/auth/moon.ts b/src/common/auth/moon.ts index 5c3810a..fc037b4 100644 --- a/src/common/auth/moon.ts +++ b/src/common/auth/moon.ts @@ -4,6 +4,7 @@ import { ZNMA_CLIENT_ID } from '../../api/moon.js'; import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; import { Jwt } from '../../util/jwt.js'; import MoonApi from '../../api/moon.js'; +import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js'; const debug = createDebug('nxapi:auth:moon'); @@ -14,7 +15,7 @@ export interface SavedMoonToken { expires_at: number; } -export async function getPctlToken(storage: persist.LocalStorage, token: string) { +export async function getPctlToken(storage: persist.LocalStorage, token: string, ratelimit = SHOULD_LIMIT_USE) { if (!token) { console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nxapi pctl auth`.'); throw new Error('Invalid token'); @@ -40,6 +41,8 @@ export async function getPctlToken(storage: persist.LocalStorage, token: string) const existingToken: SavedMoonToken | undefined = await storage.getItem('MoonToken.' + token); if (!existingToken || existingToken.expires_at <= Date.now()) { + if (ratelimit) await checkUseLimit(storage, 'moon', jwt.payload.sub); + console.warn('Authenticating to Nintendo Switch Parental Controls app'); debug('Authenticating to pctl with session token'); diff --git a/src/common/auth/nooklink.ts b/src/common/auth/nooklink.ts index 9b91ab3..b571e1d 100644 --- a/src/common/auth/nooklink.ts +++ b/src/common/auth/nooklink.ts @@ -1,9 +1,12 @@ import createDebug from 'debug'; import persist from 'node-persist'; -import { getToken } from './nso.js'; +import { getToken } from './coral.js'; import NooklinkApi, { NooklinkUserApi } from '../../api/nooklink.js'; import { AuthToken, Users } from '../../api/nooklink-types.js'; import { WebServiceToken } from '../../api/coral-types.js'; +import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js'; +import { Jwt } from '../../util/jwt.js'; +import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js'; const debug = createDebug('nxapi:auth:nooklink'); @@ -19,7 +22,8 @@ export interface SavedToken { } export async function getWebServiceToken( - storage: persist.LocalStorage, token: string, proxy_url?: string, allow_fetch_token = false + storage: persist.LocalStorage, token: string, proxy_url?: string, + allow_fetch_token = false, ratelimit = SHOULD_LIMIT_USE ) { if (!token) { console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nxapi nso token`.'); @@ -30,7 +34,12 @@ export async function getWebServiceToken( if (!existingToken || existingToken.expires_at <= Date.now()) { if (!allow_fetch_token) { - throw new Error('No valid _gtoken cookie'); + throw new Error('No valid NookLink web service token'); + } + + if (ratelimit) { + const [jwt, sig] = Jwt.decode(token); + await checkUseLimit(storage, 'nooklink', jwt.payload.sub); } console.warn('Authenticating to NookLink'); @@ -66,7 +75,7 @@ type PromiseValue = T extends PromiseLike ? R : never; export async function getUserToken( storage: persist.LocalStorage, nintendoAccountToken: string, user?: string, - proxy_url?: string, allow_fetch_token = false + proxy_url?: string, allow_fetch_token = false, ratelimit = SHOULD_LIMIT_USE ) { let wst: PromiseValue> | null = null; @@ -106,6 +115,11 @@ export async function getUserToken( if (!wst) wst = await getWebServiceToken(storage, nintendoAccountToken, proxy_url, allow_fetch_token); const {nooklink, data: webserviceToken} = wst; + if (ratelimit) { + const [jwt, sig] = Jwt.decode(nintendoAccountToken); + await checkUseLimit(storage, 'nooklink-user', jwt.payload.sub); + } + console.warn('Authenticating to NookLink as user %s', user); debug('Authenticating to NookLink as user %s', user); diff --git a/src/common/auth/splatnet2.ts b/src/common/auth/splatnet2.ts index 827517e..7b989b9 100644 --- a/src/common/auth/splatnet2.ts +++ b/src/common/auth/splatnet2.ts @@ -2,9 +2,12 @@ import process from 'node:process'; import * as fs from 'node:fs'; import createDebug from 'debug'; import persist from 'node-persist'; -import { getToken } from './nso.js'; +import { getToken } from './coral.js'; import SplatNet2Api, { updateIksmSessionLastUsed } from '../../api/splatnet2.js'; import { WebServiceToken } from '../../api/coral-types.js'; +import { checkUseLimit, SHOULD_LIMIT_USE } from './util.js'; +import { Jwt } from '../../util/jwt.js'; +import { NintendoAccountSessionTokenJwtPayload } from '../../api/na.js'; const debug = createDebug('nxapi:auth:splatnet2'); @@ -27,7 +30,10 @@ export interface SavedIksmSessionToken { last_used?: number; } -export async function getIksmToken(storage: persist.LocalStorage, token: string, proxy_url?: string, allow_fetch_token = false) { +export async function getIksmToken( + storage: persist.LocalStorage, token: string, proxy_url?: string, + allow_fetch_token = false, ratelimit = SHOULD_LIMIT_USE +) { if (!token) { console.error('No token set. Set a Nintendo Account session token using the `--token` option or by running `nxapi nso token`.'); throw new Error('Invalid token'); @@ -44,6 +50,11 @@ export async function getIksmToken(storage: persist.LocalStorage, token: string, throw new Error('No valid iksm_session cookie'); } + if (ratelimit) { + const [jwt, sig] = Jwt.decode(token); + await checkUseLimit(storage, 'splatnet2', jwt.payload.sub); + } + console.warn('Authenticating to SplatNet 2'); debug('Authenticating to SplatNet 2'); diff --git a/src/common/auth/util.ts b/src/common/auth/util.ts new file mode 100644 index 0000000..51918ec --- /dev/null +++ b/src/common/auth/util.ts @@ -0,0 +1,30 @@ +import createDebug from 'debug'; +import * as persist from 'node-persist'; + +const debug = createDebug('nxapi:auth:util'); + +// If the parent process is a terminal, then the user is attempting to run the command manually, +// so we shouldn't restrict how many attempts they can use. If not, the command is being run by +// a script/some other program, which should be limited in case it continues to run the command +// if it fails. The Electron app overrides this as the parent process (probably) won't be a +// terminal, but most attempts to call getToken won't be automated. +export const SHOULD_LIMIT_USE = !process.stdout.isTTY; +const LIMIT_REQUESTS = 4; +const LIMIT_PERIOD = 60 * 60 * 1000; // 60 minutes + +type RateLimitAttempts = number[]; + +export async function checkUseLimit( + storage: persist.LocalStorage, + key: string, user: string +) { + let attempts: RateLimitAttempts = await storage.getItem('RateLimitAttempts-' + key + '.' + user) ?? []; + attempts = attempts.filter(a => a >= Date.now() - LIMIT_PERIOD); + + if (attempts.length >= LIMIT_REQUESTS) { + throw new Error('Too many attempts to authenticate'); + } + + attempts.unshift(Date.now()); + await storage.setItem('RateLimitAttempts-' + key + '.' + user, attempts); +} diff --git a/src/common/notify.ts b/src/common/notify.ts index ab2f029..03cb6d7 100644 --- a/src/common/notify.ts +++ b/src/common/notify.ts @@ -4,7 +4,7 @@ import CoralApi from '../api/coral.js'; import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, CoralErrorResponse } from '../api/coral-types.js'; import ZncProxyApi from '../api/znc-proxy.js'; import { ErrorResponse } from '../api/util.js'; -import { SavedToken } from './auth/nso.js'; +import { SavedToken } from './auth/coral.js'; import { SplatNet2RecordsMonitor } from './splatnet2/monitor.js'; import Loop, { LoopResult } from '../util/loop.js'; import { getTitleIdFromEcUrl, hrduration } from '../util/misc.js'; diff --git a/src/common/users.ts b/src/common/users.ts index 4e1f305..d62a7bd 100644 --- a/src/common/users.ts +++ b/src/common/users.ts @@ -3,9 +3,7 @@ import * as persist from 'node-persist'; import CoralApi from '../api/coral.js'; import ZncProxyApi from '../api/znc-proxy.js'; import { Announcements, Friends, GetActiveEventResult, WebServices, CoralSuccessResponse } from '../api/coral-types.js'; -import { getToken, SavedToken } from './auth/nso.js'; -import { Jwt } from '../util/jwt.js'; -import { NintendoAccountSessionTokenJwtPayload } from '../api/na.js'; +import { getToken, SavedToken } from './auth/coral.js'; const debug = createDebug('nxapi:users'); @@ -24,11 +22,6 @@ export default class Users { } async get(token: string): Promise { - if (debug.enabled) { - const [jwt, sig] = Jwt.decode(token); - debug('Getting user for token', jwt.payload.sub); - } - const existing = this.users.get(token); if (existing && existing.expires_at >= Date.now()) { @@ -47,11 +40,11 @@ export default class Users { return promise; } - static coral(storage: persist.LocalStorage, znc_proxy_url: string): Users> - static coral(storage: persist.LocalStorage, znc_proxy_url?: string): Users - static coral(storage: persist.LocalStorage, znc_proxy_url?: string) { + static coral(storage: persist.LocalStorage, znc_proxy_url: string, ratelimit?: boolean): Users> + static coral(storage: persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean): Users + static coral(storage: persist.LocalStorage, znc_proxy_url?: string, ratelimit?: boolean) { return new Users(async token => { - const {nso, data} = await getToken(storage, token, znc_proxy_url); + const {nso, data} = await getToken(storage, token, znc_proxy_url, ratelimit); const [announcements, friends, webservices, active_event] = await Promise.all([ nso.getAnnouncements(),