diff --git a/src/api/znc-proxy.ts b/src/api/znc-proxy.ts index 79a0db1..1db7e22 100644 --- a/src/api/znc-proxy.ts +++ b/src/api/znc-proxy.ts @@ -3,8 +3,9 @@ import createDebug from 'debug'; import { ActiveEvent, Announcements, CurrentUser, Event, Friend, Presence, PresencePermissions, User, WebService, WebServiceToken, ZncStatus, ZncSuccessResponse } from './znc-types.js'; import { ErrorResponse } from './util.js'; import ZncApi from './znc.js'; -import { SavedToken, version } from '../util.js'; +import { version } from '../util.js'; import { NintendoAccountUser } from './na.js'; +import { SavedToken } from '../cli/nso/util.js'; const debug = createDebug('api:znc-proxy'); diff --git a/src/app/browser/app.tsx b/src/app/browser/app.tsx index c4cd5c6..521751d 100644 --- a/src/app/browser/app.tsx +++ b/src/app/browser/app.tsx @@ -1,10 +1,15 @@ import React, { useCallback } from 'react'; import { StyleSheet, Text, useColorScheme, View } from 'react-native'; import { NintendoAccountUser } from '../../api/na.js'; -import { SavedMoonToken, SavedToken } from '../../util.js'; +import { SavedToken } from '../../cli/nso/util.js'; +import { SavedMoonToken } from '../../cli/pctl/util.js'; import ipc from './ipc.js'; import { useAsync } from './util.js'; +export interface AppProps { + // +} + async function getAccounts() { const ids = await ipc.listNintendoAccounts(); @@ -29,7 +34,7 @@ async function getAccounts() { return accounts; } -function App() { +function App(props: AppProps) { const theme = useColorScheme() === 'light' ? light : dark; const [users] = useAsync(useCallback(() => getAccounts(), [ipc])); diff --git a/src/app/browser/index.ts b/src/app/browser/index.ts index 47646e7..4f40eb1 100644 --- a/src/app/browser/index.ts +++ b/src/app/browser/index.ts @@ -1,4 +1,5 @@ import { AppRegistry } from 'react-native'; +import { config } from './ipc.js'; import App from './app.jsx'; AppRegistry.registerComponent('App', () => App); @@ -8,6 +9,7 @@ const rootTag = window.document.createElement('div'); rootTag.style.minHeight = '100vh'; window.document.body.appendChild(rootTag); -AppRegistry.runApplication('App', { +AppRegistry.runApplication(config.type, { rootTag, + initialProps: config.props, }); diff --git a/src/app/browser/ipc.ts b/src/app/browser/ipc.ts index 4120e9c..87b332b 100644 --- a/src/app/browser/ipc.ts +++ b/src/app/browser/ipc.ts @@ -1,5 +1,9 @@ +import type { EventEmitter } from 'events'; +import createDebug from 'debug'; import type { NxapiElectronIpc } from '../preload/index.js'; +const debug = createDebug('app:browser:ipc'); + declare global { interface Window { nxapiElectronIpc: NxapiElectronIpc; @@ -9,3 +13,7 @@ declare global { const ipc = window.nxapiElectronIpc; export default ipc; + +export const config = ipc.getWindowData(); + +debug('Window configuration', config); diff --git a/src/app/common/types.ts b/src/app/common/types.ts new file mode 100644 index 0000000..03ce10e --- /dev/null +++ b/src/app/common/types.ts @@ -0,0 +1,14 @@ +import type { AppProps } from '../browser/app.js'; + +export enum WindowType { + MAIN_WINDOW = 'App', +} + +interface WindowProps { + [WindowType.MAIN_WINDOW]: AppProps; +} + +export interface WindowConfiguration { + type: T; + props: WindowProps[T]; +} diff --git a/src/app/main/index.ts b/src/app/main/index.ts index 4463737..8d6477a 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -6,32 +6,25 @@ import * as persist from 'node-persist'; import fetch from 'node-fetch'; import dotenv from 'dotenv'; import dotenvExpand from 'dotenv-expand'; -import { dev, dir, getToken, initStorage, LoopResult, paths } from '../../util.js'; +import { dir, initStorage, LoopResult, paths } from '../../util.js'; import MenuApp from './menu.js'; -import { ZncDiscordPresence } from '../../cli/nso/presence.js'; import { WebServiceIpc } from './webservices.js'; +import { createWindow, getWindowConfiguration } from './windows.js'; +import { WindowType } from '../common/types.js'; import { CurrentUser, Friend, Game, ZncErrorResponse } from '../../api/znc-types.js'; -import { NotificationManager } from '../../cli/nso/notify.js'; import { ErrorResponse } from '../../api/util.js'; +import { ZncDiscordPresence } from '../../cli/nso/presence.js'; +import { NotificationManager } from '../../cli/nso/notify.js'; +import { getToken } from '../../cli/nso/util.js'; const debug = createDebug('app:main'); -export const bundlepath = path.resolve(dir, 'dist', 'bundle'); +export const bundlepath = path.resolve(dir, 'dist', 'app', 'bundle'); -function createWindow() { - // Create the browser window. - const mainWindow = new BrowserWindow({ - width: 800, - height: 600, - vibrancy: 'content', - webPreferences: { - preload: path.join(bundlepath, 'preload.cjs'), - scrollBounce: true, - }, - }); +function createMainWindow() { + const window = createWindow(WindowType.MAIN_WINDOW, {}); - mainWindow.loadFile(path.join(bundlepath, 'index.html')); - if (dev) mainWindow.webContents.openDevTools(); + return window; } app.whenReady().then(async () => { @@ -47,6 +40,8 @@ app.whenReady().then(async () => { const storage = await initStorage(process.env.NXAPI_DATA_PATH ?? paths.data); const store = new Store(storage); + ipcMain.on('nxapi:browser:getwindowdata', e => e.returnValue = getWindowConfiguration(e.sender)); + ipcMain.handle('nxapi:accounts:list', () => storage.getItem('NintendoAccountIds')); ipcMain.handle('nxapi:nso:gettoken', (e, id: string) => storage.getItem('NintendoAccountToken.' + id)); ipcMain.handle('nxapi:nso:getcachedtoken', (e, token: string) => storage.getItem('NsoToken.' + token)); @@ -71,7 +66,7 @@ app.whenReady().then(async () => { const menu = new MenuApp(store, monitors); app.on('activate', () => { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); + if (BrowserWindow.getAllWindows().length === 0) createMainWindow(); }); debug('App started'); @@ -248,7 +243,7 @@ export class EmbeddedPresenceMonitor extends ZncDiscordPresence { async handleError(err: ErrorResponse | NodeJS.ErrnoException): Promise { try { - return super.handleError(err); + return await super.handleError(err); } catch (err) { if (err instanceof Error) { dialog.showErrorBox(err.name, err.stack ?? err.message); diff --git a/src/app/main/menu.ts b/src/app/main/menu.ts index 37d6aa6..08408b0 100644 --- a/src/app/main/menu.ts +++ b/src/app/main/menu.ts @@ -2,9 +2,10 @@ import { app, dialog, Menu, Tray, nativeImage, MenuItem } from '../electron.js'; import createDebug from 'debug'; import { addNsoAccount, addPctlAccount } from './na-auth.js'; import { PresenceMonitorManager, Store } from './index.js'; -import { getToken, SavedMoonToken, SavedToken } from '../../util.js'; import { WebService } from '../../api/znc-types.js'; import openWebService from './webservices.js'; +import { getToken, SavedToken } from '../../cli/nso/util.js'; +import { SavedMoonToken } from '../../cli/pctl/util.js'; const debug = createDebug('app:main:menu'); diff --git a/src/app/main/na-auth.ts b/src/app/main/na-auth.ts index cde7d1a..bbbccaa 100644 --- a/src/app/main/na-auth.ts +++ b/src/app/main/na-auth.ts @@ -6,8 +6,9 @@ import { BrowserWindow, nativeImage, Notification, session, shell } from '../ele import { getNintendoAccountSessionToken, NintendoAccountSessionToken } from '../../api/na.js'; import { Jwt } from '../../api/util.js'; import { ZNCA_CLIENT_ID } from '../../api/znc.js'; -import { getPctlToken, getToken, SavedMoonToken, SavedToken } from '../../util.js'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; +import { getToken, SavedToken } from '../../cli/nso/util.js'; +import { getPctlToken, SavedMoonToken } from '../../cli/pctl/util.js'; const debug = createDebug('app:main:na-auth'); diff --git a/src/app/main/webservices.ts b/src/app/main/webservices.ts index 861e77a..06230d5 100644 --- a/src/app/main/webservices.ts +++ b/src/app/main/webservices.ts @@ -2,10 +2,11 @@ import * as path from 'path'; import createDebug from 'debug'; import { BrowserWindow, IpcMainInvokeEvent, session, shell, WebContents } from '../electron.js'; import ZncApi from '../../api/znc.js'; -import { dev, SavedToken } from '../../util.js'; +import { dev } from '../../util.js'; import { WebService } from '../../api/znc-types.js'; import { bundlepath, Store } from './index.js'; import type { NativeShareRequest, NativeShareUrlRequest } from '../preload-webservice/znca-js-api.js'; +import { SavedToken } from '../../cli/nso/util.js'; const debug = createDebug('app:main:webservices'); diff --git a/src/app/main/windows.ts b/src/app/main/windows.ts new file mode 100644 index 0000000..aa24df0 --- /dev/null +++ b/src/app/main/windows.ts @@ -0,0 +1,42 @@ +import { app, BrowserWindow, dialog, ipcMain, nativeImage, Notification, WebContents } from '../electron.js'; +import * as path from 'path'; +import { bundlepath } from './index.js'; +import { dev } from '../../util.js'; +import { WindowConfiguration, WindowType } from '../common/types.js'; + +const windows = new Map(); + +export function createWindow(type: T, props: WindowConfiguration['props']) { + // Create the browser window + const window = new BrowserWindow({ + width: 800, + height: 600, + vibrancy: 'content', + webPreferences: { + preload: path.join(bundlepath, 'preload.cjs'), + scrollBounce: true, + }, + }); + + const data: WindowConfiguration = { + type, + props, + }; + + windows.set(window.webContents, data); + + window.loadFile(path.join(bundlepath, 'index.html')); + if (dev) window.webContents.openDevTools(); + + return window; +} + +export function getWindowConfiguration(webcontents: WebContents): WindowConfiguration { + const data = windows.get(webcontents); + + if (!data) { + throw new Error('Unknown window'); + } + + return data; +} diff --git a/src/app/preload/index.ts b/src/app/preload/index.ts index 9e46a7c..ff7899e 100644 --- a/src/app/preload/index.ts +++ b/src/app/preload/index.ts @@ -1,8 +1,12 @@ import { contextBridge, ipcRenderer } from 'electron'; import { EventEmitter } from 'events'; -import { SavedMoonToken, SavedToken } from '../../util.js'; +import { WindowConfiguration } from '../common/types.js'; +import { SavedToken } from '../../cli/nso/util.js'; +import { SavedMoonToken } from '../../cli/pctl/util.js'; const ipc = { + getWindowData: () => ipcRenderer.sendSync('nxapi:browser:getwindowdata') as WindowConfiguration, + listNintendoAccounts: () => ipcRenderer.invoke('nxapi:accounts:list') as Promise, getNintendoAccountNsoToken: (id: string) => ipcRenderer.invoke('nxapi:nso:gettoken', id) as Promise, getSavedNsoToken: (token: string) => ipcRenderer.invoke('nxapi:nso:getcachedtoken', token) as Promise, diff --git a/src/cli/nooklink/island.ts b/src/cli/nooklink/island.ts index 2d83f88..e1ef98e 100644 --- a/src/cli/nooklink/island.ts +++ b/src/cli/nooklink/island.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nooklink.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getUserToken, getWebServiceToken } from './util.js'; diff --git a/src/cli/nooklink/newspaper.ts b/src/cli/nooklink/newspaper.ts index 8fc740e..37525e6 100644 --- a/src/cli/nooklink/newspaper.ts +++ b/src/cli/nooklink/newspaper.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nooklink.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getUserToken } from './util.js'; diff --git a/src/cli/nooklink/newspapers.ts b/src/cli/nooklink/newspapers.ts index 8fe846a..36da340 100644 --- a/src/cli/nooklink/newspapers.ts +++ b/src/cli/nooklink/newspapers.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nooklink.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getUserToken } from './util.js'; diff --git a/src/cli/nooklink/reactions.ts b/src/cli/nooklink/reactions.ts index b3476d9..9f0fb26 100644 --- a/src/cli/nooklink/reactions.ts +++ b/src/cli/nooklink/reactions.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nooklink.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getUserToken } from './util.js'; diff --git a/src/cli/nooklink/users.ts b/src/cli/nooklink/users.ts index d266dc7..ad94dc0 100644 --- a/src/cli/nooklink/users.ts +++ b/src/cli/nooklink/users.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nooklink.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getWebServiceToken } from './util.js'; diff --git a/src/cli/nooklink/util.ts b/src/cli/nooklink/util.ts index 56b6970..ae93e47 100644 --- a/src/cli/nooklink/util.ts +++ b/src/cli/nooklink/util.ts @@ -1,6 +1,6 @@ import createDebug from 'debug'; import persist from 'node-persist'; -import { getToken } from '../../util.js'; +import { getToken } from '../nso/util.js'; import NooklinkApi, { NooklinkUserApi } from '../../api/nooklink.js'; import { AuthToken, Users } from '../../api/nooklink-types.js'; import { WebServiceToken } from '../../api/znc-types.js'; diff --git a/src/cli/nso/announcements.ts b/src/cli/nso/announcements.ts index cd3c69c..3b029c2 100644 --- a/src/cli/nso/announcements.ts +++ b/src/cli/nso/announcements.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:announcements'); diff --git a/src/cli/nso/auth.ts b/src/cli/nso/auth.ts index 4889b2e..ad9f0ad 100644 --- a/src/cli/nso/auth.ts +++ b/src/cli/nso/auth.ts @@ -2,7 +2,8 @@ import * as util from 'util'; import createDebug from 'debug'; import * as crypto from 'crypto'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; import { getNintendoAccountSessionToken } from '../../api/na.js'; import { ZNCA_CLIENT_ID } from '../../api/znc.js'; diff --git a/src/cli/nso/friends.ts b/src/cli/nso/friends.ts index ae5af69..dbba4ad 100644 --- a/src/cli/nso/friends.ts +++ b/src/cli/nso/friends.ts @@ -1,9 +1,9 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import { PresenceState } from '../../api/znc-types.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, hrduration, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, hrduration, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:friends'); diff --git a/src/cli/nso/http-server.ts b/src/cli/nso/http-server.ts index 38f14ad..f626327 100644 --- a/src/cli/nso/http-server.ts +++ b/src/cli/nso/http-server.ts @@ -6,7 +6,8 @@ import { v4 as uuidgen } from 'uuid'; import { Announcement, CurrentUser, Friend, GetActiveEventResult, Presence, WebService } from '../../api/znc-types.js'; import ZncApi from '../../api/znc.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, SavedToken, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken, SavedToken } from './util.js'; import { NotificationManager, ZncNotifications } from './notify.js'; declare global { diff --git a/src/cli/nso/notify.ts b/src/cli/nso/notify.ts index 8aa6aad..bf9aea3 100644 --- a/src/cli/nso/notify.ts +++ b/src/cli/nso/notify.ts @@ -5,7 +5,8 @@ import * as path from 'path'; import { ActiveEvent, Announcements, CurrentUser, Friend, Game, Presence, PresenceState, WebServices, ZncErrorResponse } from '../../api/znc-types.js'; import ZncApi from '../../api/znc.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getTitleIdFromEcUrl, getToken, hrduration, initStorage, Loop, LoopResult, SavedToken, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, getTitleIdFromEcUrl, hrduration, initStorage, Loop, LoopResult, YargsArguments } from '../../util.js'; +import { getToken, SavedToken } from './util.js'; import ZncProxyApi from '../../api/znc-proxy.js'; import { SplatNet2RecordsMonitor } from '../splatnet2/monitor.js'; import { getIksmToken } from '../splatnet2/util.js'; diff --git a/src/cli/nso/permissions.ts b/src/cli/nso/permissions.ts index df6219c..dbe4950 100644 --- a/src/cli/nso/permissions.ts +++ b/src/cli/nso/permissions.ts @@ -1,7 +1,8 @@ import createDebug from 'debug'; import { PresencePermissions } from '../../api/znc-types.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:permissions'); diff --git a/src/cli/nso/presence.ts b/src/cli/nso/presence.ts index 3c30b12..c19be53 100644 --- a/src/cli/nso/presence.ts +++ b/src/cli/nso/presence.ts @@ -4,7 +4,8 @@ import DiscordRPC from 'discord-rpc'; import { ActiveEvent, CurrentUser, Friend, Presence, PresenceState, ZncErrorResponse } from '../../api/znc-types.js'; import ZncApi from '../../api/znc.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, LoopResult, SavedToken, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, LoopResult, YargsArguments } from '../../util.js'; +import { getToken, SavedToken } from './util.js'; import { DiscordPresencePlayTime, DiscordPresenceContext, getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js'; import { handleEnableSplatNet2Monitoring, ZncNotifications } from './notify.js'; import { ErrorResponse } from '../../index.js'; diff --git a/src/cli/nso/token.ts b/src/cli/nso/token.ts index 5ffe8cc..583e216 100644 --- a/src/cli/nso/token.ts +++ b/src/cli/nso/token.ts @@ -1,7 +1,8 @@ import * as util from 'util'; import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:token'); diff --git a/src/cli/nso/user.ts b/src/cli/nso/user.ts index 9e5cc83..eb1af20 100644 --- a/src/cli/nso/user.ts +++ b/src/cli/nso/user.ts @@ -1,6 +1,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:user'); diff --git a/src/cli/nso/util.ts b/src/cli/nso/util.ts new file mode 100644 index 0000000..a972070 --- /dev/null +++ b/src/cli/nso/util.ts @@ -0,0 +1,86 @@ +import createDebug from 'debug'; +import * as persist from 'node-persist'; +import { FlapgApiResponse } from '../../api/f.js'; +import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; +import { Jwt } from '../../api/util.js'; +import { AccountLogin } from '../../api/znc-types.js'; +import ZncApi, { ZNCA_CLIENT_ID } from '../../api/znc.js'; +import ZncProxyApi from '../../api/znc-proxy.js'; + +const debug = createDebug('cli:nso'); + +export interface SavedToken { + uuid: string; + timestamp: string; + nintendoAccountToken: NintendoAccountToken; + user: NintendoAccountUser; + flapg: FlapgApiResponse['result']; + nsoAccount: AccountLogin; + credential: AccountLogin['webApiServerCredential']; + + expires_at: number; + proxy_url?: string; +} + +export async function getToken(storage: persist.LocalStorage, token: string, proxy_url: string): Promise<{ + nso: ZncProxyApi; + data: SavedToken; +}> +export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string): Promise<{ + nso: ZncApi; + data: SavedToken; +}> +export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string) { + 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'); + } + + const [jwt, sig] = Jwt.decode(token); + + if (jwt.payload.iss !== 'https://accounts.nintendo.com') { + throw new Error('Invalid Nintendo Account session token issuer'); + } + if (jwt.payload.typ !== 'session_token') { + throw new Error('Invalid Nintendo Account session token type'); + } + if (jwt.payload.aud !== ZNCA_CLIENT_ID) { + throw new Error('Invalid Nintendo Account session token audience'); + } + if (jwt.payload.exp <= (Date.now() / 1000)) { + throw new Error('Nintendo Account session token expired'); + } + + // Nintendo Account session tokens use a HMAC SHA256 signature, so we can't verify this is valid + + const existingToken: SavedToken | undefined = await storage.getItem('NsoToken.' + token); + + if (!existingToken || existingToken.expires_at <= Date.now()) { + console.warn('Authenticating to Nintendo Switch Online app'); + debug('Authenticating to znc with session token'); + + const {nso, data} = proxy_url ? + await ZncProxyApi.createWithSessionToken(proxy_url, token) : + await ZncApi.createWithSessionToken(token); + + const existingToken: SavedToken = { + ...data, + expires_at: Date.now() + (data.credential.expiresIn * 1000), + }; + + await storage.setItem('NsoToken.' + token, existingToken); + await storage.setItem('NintendoAccountToken.' + data.user.id, token); + + return {nso, data: existingToken}; + } + + debug('Using existing token'); + await storage.setItem('NintendoAccountToken.' + existingToken.user.id, token); + + return { + nso: proxy_url ? + new ZncProxyApi(proxy_url, token) : + new ZncApi(existingToken.credential.accessToken), + data: existingToken, + }; +} diff --git a/src/cli/nso/webservices.ts b/src/cli/nso/webservices.ts index 7c486d3..95e0934 100644 --- a/src/cli/nso/webservices.ts +++ b/src/cli/nso/webservices.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.js'; const debug = createDebug('cli:nso:webservices'); diff --git a/src/cli/nso/webservicetoken.ts b/src/cli/nso/webservicetoken.ts index c528449..07b0a71 100644 --- a/src/cli/nso/webservicetoken.ts +++ b/src/cli/nso/webservicetoken.ts @@ -1,6 +1,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../nso.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from './util.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 1680333..c2a02d9 100644 --- a/src/cli/nso/znc-proxy-tokens.ts +++ b/src/cli/nso/znc-proxy-tokens.ts @@ -1,9 +1,9 @@ import createDebug from 'debug'; import fetch from 'node-fetch'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../nso.js'; -import { Argv, getToken, initStorage } from '../../util.js'; +import { Argv, initStorage } from '../../util.js'; +import { getToken } from './util.js'; import { AuthPolicy, AuthToken } from './http-server.js'; const debug = createDebug('cli:nso:znc-proxy-tokens'); diff --git a/src/cli/pctl/auth.ts b/src/cli/pctl/auth.ts index 0365815..07c3b52 100644 --- a/src/cli/pctl/auth.ts +++ b/src/cli/pctl/auth.ts @@ -1,8 +1,9 @@ import * as util from 'util'; -import createDebug from 'debug'; import * as crypto from 'crypto'; +import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; import { getNintendoAccountSessionToken } from '../../api/na.js'; import { ZNMA_CLIENT_ID } from '../../api/moon.js'; diff --git a/src/cli/pctl/daily-summaries.ts b/src/cli/pctl/daily-summaries.ts index f478e5b..cabb6b7 100644 --- a/src/cli/pctl/daily-summaries.ts +++ b/src/cli/pctl/daily-summaries.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, hrduration, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, hrduration, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:daily-summaries'); diff --git a/src/cli/pctl/devices.ts b/src/cli/pctl/devices.ts index 5619d32..f0b74bd 100644 --- a/src/cli/pctl/devices.ts +++ b/src/cli/pctl/devices.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:devices'); diff --git a/src/cli/pctl/dump-summaries.ts b/src/cli/pctl/dump-summaries.ts index 992c32a..0e85fc2 100644 --- a/src/cli/pctl/dump-summaries.ts +++ b/src/cli/pctl/dump-summaries.ts @@ -1,9 +1,10 @@ -import createDebug from 'debug'; +import * as path from 'path'; import * as fs from 'fs/promises'; +import createDebug from 'debug'; import mkdirp from 'mkdirp'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; -import * as path from 'path'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; import { DailySummaryResult } from '../../api/moon-types.js'; import MoonApi from '../../api/moon.js'; diff --git a/src/cli/pctl/monthly-summaries.ts b/src/cli/pctl/monthly-summaries.ts index 55f2dc0..c5663c7 100644 --- a/src/cli/pctl/monthly-summaries.ts +++ b/src/cli/pctl/monthly-summaries.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:monthly-summaries'); diff --git a/src/cli/pctl/monthly-summary.ts b/src/cli/pctl/monthly-summary.ts index 21892bd..7e0acb2 100644 --- a/src/cli/pctl/monthly-summary.ts +++ b/src/cli/pctl/monthly-summary.ts @@ -1,8 +1,8 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:monthly-summary'); diff --git a/src/cli/pctl/settings.ts b/src/cli/pctl/settings.ts index 3f79b83..e043c52 100644 --- a/src/cli/pctl/settings.ts +++ b/src/cli/pctl/settings.ts @@ -1,6 +1,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:settings'); diff --git a/src/cli/pctl/token.ts b/src/cli/pctl/token.ts index 118ae50..502bb80 100644 --- a/src/cli/pctl/token.ts +++ b/src/cli/pctl/token.ts @@ -1,7 +1,8 @@ import * as util from 'util'; import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:token'); diff --git a/src/cli/pctl/user.ts b/src/cli/pctl/user.ts index e76bb56..5247885 100644 --- a/src/cli/pctl/user.ts +++ b/src/cli/pctl/user.ts @@ -1,6 +1,7 @@ import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../../cli.js'; -import { ArgumentsCamelCase, Argv, getPctlToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getPctlToken } from './util.js'; const debug = createDebug('cli:pctl:user'); diff --git a/src/cli/pctl/util.ts b/src/cli/pctl/util.ts new file mode 100644 index 0000000..fb7200b --- /dev/null +++ b/src/cli/pctl/util.ts @@ -0,0 +1,66 @@ +import createDebug from 'debug'; +import * as persist from 'node-persist'; +import { ZNMA_CLIENT_ID } from '../../api/moon.js'; +import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from '../../api/na.js'; +import { Jwt } from '../../api/util.js'; +import MoonApi from '../../api/moon.js'; + +const debug = createDebug('cli:nso'); + +export interface SavedMoonToken { + nintendoAccountToken: NintendoAccountToken; + user: NintendoAccountUser; + + expires_at: number; +} + +export async function getPctlToken(storage: persist.LocalStorage, token: string) { + 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'); + } + + const [jwt, sig] = Jwt.decode(token); + + if (jwt.payload.iss !== 'https://accounts.nintendo.com') { + throw new Error('Invalid Nintendo Account session token issuer'); + } + if (jwt.payload.typ !== 'session_token') { + throw new Error('Invalid Nintendo Account session token type'); + } + if (jwt.payload.aud !== ZNMA_CLIENT_ID) { + throw new Error('Invalid Nintendo Account session token audience'); + } + if (jwt.payload.exp <= (Date.now() / 1000)) { + throw new Error('Nintendo Account session token expired'); + } + + // Nintendo Account session tokens use a HMAC SHA256 signature, so we can't verify this is valid + + const existingToken: SavedMoonToken | undefined = await storage.getItem('MoonToken.' + token); + + if (!existingToken || existingToken.expires_at <= Date.now()) { + console.warn('Authenticating to Nintendo Switch Parental Controls app'); + debug('Authenticating to pctl with session token'); + + const {moon, data} = await MoonApi.createWithSessionToken(token); + + const existingToken: SavedMoonToken = { + ...data, + expires_at: Date.now() + (data.nintendoAccountToken.expires_in * 1000), + }; + + await storage.setItem('MoonToken.' + token, existingToken); + await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, token); + + return {moon, data: existingToken}; + } + + debug('Using existing token'); + await storage.setItem('NintendoAccountToken-pctl.' + existingToken.user.id, token); + + return { + moon: new MoonApi(existingToken.nintendoAccountToken.access_token!, existingToken.user.id), + data: existingToken, + }; +} diff --git a/src/cli/splatnet2/battles.ts b/src/cli/splatnet2/battles.ts index 5294b7d..9aa4c47 100644 --- a/src/cli/splatnet2/battles.ts +++ b/src/cli/splatnet2/battles.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/splatnet2/challenges.ts b/src/cli/splatnet2/challenges.ts index 0b16a05..362d878 100644 --- a/src/cli/splatnet2/challenges.ts +++ b/src/cli/splatnet2/challenges.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/splatnet2/hero.ts b/src/cli/splatnet2/hero.ts index 81d0a7b..3ede163 100644 --- a/src/cli/splatnet2/hero.ts +++ b/src/cli/splatnet2/hero.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/splatnet2/schedule.ts b/src/cli/splatnet2/schedule.ts index 299866e..869e71c 100644 --- a/src/cli/splatnet2/schedule.ts +++ b/src/cli/splatnet2/schedule.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/splatnet2/stages.ts b/src/cli/splatnet2/stages.ts index 51b06c2..b5ef6e0 100644 --- a/src/cli/splatnet2/stages.ts +++ b/src/cli/splatnet2/stages.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/splatnet2/util.ts b/src/cli/splatnet2/util.ts index 4af44da..1517395 100644 --- a/src/cli/splatnet2/util.ts +++ b/src/cli/splatnet2/util.ts @@ -1,7 +1,7 @@ import createDebug from 'debug'; import persist from 'node-persist'; import * as fs from 'fs'; -import { getToken } from '../../util.js'; +import { getToken } from '../nso/util.js'; import SplatNet2Api from '../../api/splatnet2.js'; import { WebServiceToken } from '../../api/znc-types.js'; diff --git a/src/cli/splatnet2/weapons.ts b/src/cli/splatnet2/weapons.ts index ae4a815..a416e26 100644 --- a/src/cli/splatnet2/weapons.ts +++ b/src/cli/splatnet2/weapons.ts @@ -1,6 +1,5 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from '../util/table.js'; import type { Arguments as ParentArguments } from '../splatnet2.js'; import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; import { getIksmToken } from './util.js'; diff --git a/src/cli/users.ts b/src/cli/users.ts index ce58f30..2995a9a 100644 --- a/src/cli/users.ts +++ b/src/cli/users.ts @@ -1,8 +1,9 @@ import createDebug from 'debug'; -// @ts-expect-error -import Table from 'cli-table/lib/index.js'; +import Table from './util/table.js'; import type { Arguments as ParentArguments } from '../cli.js'; -import { Argv, initStorage, SavedMoonToken, SavedToken } from '../util.js'; +import { Argv, initStorage } from '../util.js'; +import { SavedToken } from './nso/util.js'; +import { SavedMoonToken } from './pctl/util.js'; const debug = createDebug('cli:users'); diff --git a/src/cli/util/captureid.ts b/src/cli/util/captureid.ts index c23ce33..45f9634 100644 --- a/src/cli/util/captureid.ts +++ b/src/cli/util/captureid.ts @@ -1,4 +1,5 @@ import * as crypto from 'crypto'; +import { Buffer } from 'buffer'; import createDebug from 'debug'; import type { Arguments as ParentArguments } from '../../cli.js'; import { Argv } from '../../util.js'; diff --git a/src/cli/util/discord-activity.ts b/src/cli/util/discord-activity.ts index fa93edb..f4c0fb6 100644 --- a/src/cli/util/discord-activity.ts +++ b/src/cli/util/discord-activity.ts @@ -4,7 +4,8 @@ import { getPresenceFromUrl } from '../../api/znc-proxy.js'; import { ActiveEvent, CurrentUser, Friend, Game, Presence, PresenceState } from '../../api/znc-types.js'; import type { Arguments as ParentArguments } from '../../cli.js'; import { DiscordPresenceContext, DiscordPresencePlayTime, getDiscordPresence, getInactiveDiscordPresence } from '../../discord/util.js'; -import { ArgumentsCamelCase, Argv, getToken, initStorage, YargsArguments } from '../../util.js'; +import { ArgumentsCamelCase, Argv, initStorage, YargsArguments } from '../../util.js'; +import { getToken } from '../nso/util.js'; const debug = createDebug('cli:util:discord-activity'); diff --git a/src/cli/util/table.ts b/src/cli/util/table.ts new file mode 100644 index 0000000..e075bb9 --- /dev/null +++ b/src/cli/util/table.ts @@ -0,0 +1,127 @@ +// @ts-expect-error +import __Table from 'cli-table/lib/index.js'; + +export type Cell = string | number; +export type Row = Cell[]; + +export enum CellAlign { + LEFT = 'left', + MIDDLE = 'middle', + RIGHT = 'right', +} + +export enum Style { + BLACK = 'black', + RED = 'red', + GREEN = 'green', + YELLOW = 'yellow', + BLUE = 'blue', + MAGENTA = 'magenta', + CYAN = 'cyan', + WHITE = 'white', + GREY = 'grey', + + BRIGHTRED = 'brightRed', + BRIGHTGREEN = 'brightGreen', + BRIGHTYELLOW = 'brightYellow', + BRIGHTBLUE = 'brightBlue', + BRIGHTMAGENTA = 'brightMagenta', + BRIGHTCYAN = 'brightCyan', + BRIGHTWHITE = 'brightWhite', + + BG_BLACK = 'bgBlack', + BG_RED = 'bgRed', + BG_GREEN = 'bgGreen', + BG_YELLOW = 'bgYellow', + BG_BLUE = 'bgBlue', + BG_MAGENTA = 'bgMagenta', + BG_CYAN = 'bgCyan', + BG_WHITE = 'bgWhite', + BG_GREY = 'bgGrey', + + BG_BRIGHTRED = 'bgBrightRed', + BG_BRIGHTGREEN = 'bgBrightGreen', + BG_BRIGHTYELLOW = 'bgBrightYellow', + BG_BRIGHTBLUE = 'bgBrightBlue', + BG_BRIGHTMAGENTA = 'bgBrightMagenta', + BG_BRIGHTCYAN = 'bgBrightCyan', + BG_BRIGHTWHITE = 'bgBrightWhite', + + RESET = 'reset', + BOLD = 'bold', + DIM = 'dim', + ITALIC = 'italic', + UNDERLINE = 'underline', + INVERSE = 'inverse', + HIDDEN = 'hidden', + STRIKETHROUGH = 'strikethrough', + + RAINBOW = 'rainbow', + ZEBRA = 'zebra', + AMERICA = 'america', + TRAP = 'trap', + RANDOM = 'random', +} + +export interface TableOptions { + chars: { + 'top': string; + 'top-mid': string; + 'top-left': string; + 'top-right': string; + 'bottom': string; + 'bottom-mid': string; + 'bottom-left': string; + 'bottom-right': string; + 'left': string; + 'left-mid': string; + 'mid': string; + 'mid-mid': string; + 'right': string; + 'right-mid': string; + 'middle': string; + }; + truncate: string; + colWidths: number[]; + colAligns: CellAlign[]; + style: { + 'padding-left': number; + 'padding-right': number; + /** @default [Style.RED] */ + head: Style[]; + /** @default [Style.GREY] */ + border: Style[]; + compact: boolean; + }; + head: Row; + rows: Row[]; +} + +type RecursivePartial = { + [P in keyof T]?: (T[P] extends Array ? T[P] : RecursivePartial) | undefined; +}; + +export type PartialTableOptions = RecursivePartial; + +declare class Table extends Array { + options: TableOptions; + constructor(options: PartialTableOptions); + /** + * Width getter + */ + get width(): number; + /** + * Render to a string. + */ + render(): string; + /** + * Render to a string. + */ + toString(): string; + static version: string; +} + +const _Table: typeof Table = __Table; +type _Table = Table; + +export default _Table; diff --git a/src/util.ts b/src/util.ts index df9280f..80f2fef 100644 --- a/src/util.ts +++ b/src/util.ts @@ -2,23 +2,21 @@ import * as path from 'path'; import * as fs from 'fs'; import * as child_process from 'child_process'; import * as yargs from 'yargs'; -import type * as yargstypes from '../node_modules/@types/yargs/index.js'; +// import type * as yargstypes from '../node_modules/@types/yargs/index.js'; import createDebug from 'debug'; import persist from 'node-persist'; import getPaths from 'env-paths'; import fetch from 'node-fetch'; -import { FlapgApiResponse } from './api/f.js'; -import { NintendoAccountSessionTokenJwtPayload, NintendoAccountToken, NintendoAccountUser } from './api/na.js'; -import { AccountLogin } from './api/znc-types.js'; -import ZncApi, { ZNCA_CLIENT_ID } from './api/znc.js'; -import ZncProxyApi from './api/znc-proxy.js'; -import MoonApi, { ZNMA_CLIENT_ID } from './api/moon.js'; -import { Jwks, Jwt } from './api/util.js'; +import { Jwks } from './api/util.js'; const debug = createDebug('cli'); export const paths = getPaths('nxapi'); +// +// Package/version info +// + export const dir = path.resolve(decodeURI(import.meta.url.substr(process.platform === 'win32' ? 8 : 7)), '..', '..'); export const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')); export const version = pkg.version; @@ -42,29 +40,40 @@ export const git = (() => { })(); export const dev = !!git || process.env.NODE_ENV === 'development'; +// +// Yargs types +// + export type YargsArguments = T extends yargs.Argv ? R : any; export type Argv = yargs.Argv; -export type ArgumentsCamelCase = yargstypes.ArgumentsCamelCase; +// export type ArgumentsCamelCase = yargstypes.ArgumentsCamelCase; -export interface SavedToken { - uuid: string; - timestamp: string; - nintendoAccountToken: NintendoAccountToken; - user: NintendoAccountUser; - flapg: FlapgApiResponse['result']; - nsoAccount: AccountLogin; - credential: AccountLogin['webApiServerCredential']; +/** Convert literal string types like 'foo-bar' to 'FooBar' */ +type PascalCase = string extends S ? + string : S extends `${infer T}-${infer U}` ? + `${Capitalize}${PascalCase}` : Capitalize; - expires_at: number; - proxy_url?: string; -} +/** Convert literal string types like 'foo-bar' to 'fooBar' */ +type CamelCase = string extends S ? + string : S extends `${infer T}-${infer U}` ? + `${T}${PascalCase}` : S; -export interface SavedMoonToken { - nintendoAccountToken: NintendoAccountToken; - user: NintendoAccountUser; +/** Convert literal string types like 'foo-bar' to 'fooBar', allowing all `PropertyKey` types */ +type CamelCaseKey = K extends string ? Exclude, ''> : K; - expires_at: number; -} +/** Arguments type, with camelcased keys */ +export type ArgumentsCamelCase = { [key in keyof T as key | CamelCaseKey]: T[key] } & { + /** Non-option arguments */ + _: Array; + /** The script name or node command */ + $0: string; + /** All remaining options */ + [argName: string]: unknown; +}; + +// +// Other +// export async function initStorage(dir: string) { const storage = persist.create({ @@ -75,120 +84,6 @@ export async function initStorage(dir: string) { return storage; } -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url: string): Promise<{ - nso: ZncProxyApi; - data: SavedToken; -}> -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string): Promise<{ - nso: ZncApi; - data: SavedToken; -}> -export async function getToken(storage: persist.LocalStorage, token: string, proxy_url?: string) { - 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'); - } - - const [jwt, sig] = Jwt.decode(token); - - if (jwt.payload.iss !== 'https://accounts.nintendo.com') { - throw new Error('Invalid Nintendo Account session token issuer'); - } - if (jwt.payload.typ !== 'session_token') { - throw new Error('Invalid Nintendo Account session token type'); - } - if (jwt.payload.aud !== ZNCA_CLIENT_ID) { - throw new Error('Invalid Nintendo Account session token audience'); - } - if (jwt.payload.exp <= (Date.now() / 1000)) { - throw new Error('Nintendo Account session token expired'); - } - - // Nintendo Account session tokens use a HMAC SHA256 signature, so we can't verify this is valid - - const existingToken: SavedToken | undefined = await storage.getItem('NsoToken.' + token); - - if (!existingToken || existingToken.expires_at <= Date.now()) { - console.warn('Authenticating to Nintendo Switch Online app'); - debug('Authenticating to znc with session token'); - - const {nso, data} = proxy_url ? - await ZncProxyApi.createWithSessionToken(proxy_url, token) : - await ZncApi.createWithSessionToken(token); - - const existingToken: SavedToken = { - ...data, - expires_at: Date.now() + (data.credential.expiresIn * 1000), - }; - - await storage.setItem('NsoToken.' + token, existingToken); - await storage.setItem('NintendoAccountToken.' + data.user.id, token); - - return {nso, data: existingToken}; - } - - debug('Using existing token'); - await storage.setItem('NintendoAccountToken.' + existingToken.user.id, token); - - return { - nso: proxy_url ? - new ZncProxyApi(proxy_url, token) : - new ZncApi(existingToken.credential.accessToken), - data: existingToken, - }; -} - -export async function getPctlToken(storage: persist.LocalStorage, token: string) { - 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'); - } - - const [jwt, sig] = Jwt.decode(token); - - if (jwt.payload.iss !== 'https://accounts.nintendo.com') { - throw new Error('Invalid Nintendo Account session token issuer'); - } - if (jwt.payload.typ !== 'session_token') { - throw new Error('Invalid Nintendo Account session token type'); - } - if (jwt.payload.aud !== ZNMA_CLIENT_ID) { - throw new Error('Invalid Nintendo Account session token audience'); - } - if (jwt.payload.exp <= (Date.now() / 1000)) { - throw new Error('Nintendo Account session token expired'); - } - - // Nintendo Account session tokens use a HMAC SHA256 signature, so we can't verify this is valid - - const existingToken: SavedMoonToken | undefined = await storage.getItem('MoonToken.' + token); - - if (!existingToken || existingToken.expires_at <= Date.now()) { - console.warn('Authenticating to Nintendo Switch Parental Controls app'); - debug('Authenticating to pctl with session token'); - - const {moon, data} = await MoonApi.createWithSessionToken(token); - - const existingToken: SavedMoonToken = { - ...data, - expires_at: Date.now() + (data.nintendoAccountToken.expires_in * 1000), - }; - - await storage.setItem('MoonToken.' + token, existingToken); - await storage.setItem('NintendoAccountToken-pctl.' + data.user.id, token); - - return {moon, data: existingToken}; - } - - debug('Using existing token'); - await storage.setItem('NintendoAccountToken-pctl.' + existingToken.user.id, token); - - return { - moon: new MoonApi(existingToken.nintendoAccountToken.access_token!, existingToken.user.id), - data: existingToken, - }; -} - export function getTitleIdFromEcUrl(url: string) { const match = url.match(/^https:\/\/ec\.nintendo\.com\/apps\/([0-9a-f]{16})\//); return match?.[1] ?? null; @@ -272,6 +167,12 @@ export enum LoopResult { OK_SKIP_INTERVAL = LoopRunOkSkipInterval as any, } +// +// JSON Web Key Sets +// +// Used for verifying JSON Web Tokens +// + interface SavedJwks { jwks: Jwks; expires_at: number;