import process from 'node:process'; import fetch from 'node-fetch'; import createDebug from 'debug'; import { ErrorResponse } from './util.js'; import { timeoutSignal } from '../util/misc.js'; import { getUserAgent } from '../util/useragent.js'; const debugS2s = createDebug('nxapi:api:s2s'); const debugFlapg = createDebug('nxapi:api:flapg'); const debugImink = createDebug('nxapi:api:imink'); const debugZncaApi = createDebug('nxapi:api:znca-api'); export abstract class ZncaApi { constructor( public useragent?: string ) {} abstract genf(token: string, timestamp: string, uuid: string, type: FlapgIid): Promise; } // // splatnet2statink + flapg // export async function getLoginHash(token: string, timestamp: string | number, useragent?: string) { const { default: { coral_auth: { splatnet2statink: config } } } = await import('../common/remote-config.js'); if (!config) throw new Error('Remote configuration prevents splatnet2statink API use'); debugS2s('Getting login hash'); const [signal, cancel] = timeoutSignal(); const response = await fetch('https://elifessler.com/s2s/api/gen2', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': getUserAgent(useragent), }, body: new URLSearchParams({ naIdToken: token, timestamp: '' + timestamp, }).toString(), signal, }).finally(cancel); const data = await response.json() as LoginHashApiResponse | LoginHashApiError; if ('error' in data) { throw new ErrorResponse('[s2s] ' + data.error, response, data); } debugS2s('Got login hash "%s"', data.hash, data); return data.hash; } export interface LoginHashApiResponse { hash: string; } export interface LoginHashApiError { error: string; } export async function flapg( token: string, timestamp: string | number, guid: string, iid: FlapgIid, useragent?: string ) { const { default: { coral_auth: { flapg: config } } } = await import('../common/remote-config.js'); if (!config) throw new Error('Remote configuration prevents flapg API use'); const hash = await getLoginHash(token, timestamp, useragent); debugFlapg('Getting f parameter', { token, timestamp, guid, iid, }); const [signal, cancel] = timeoutSignal(); const response = await fetch('https://flapg.com/ika2/api/login?public', { headers: { 'User-Agent': getUserAgent(useragent), 'x-token': token, 'x-time': '' + timestamp, 'x-guid': guid, 'x-hash': hash, 'x-ver': '3', 'x-iid': iid, }, signal, }).finally(cancel); const data = await response.json() as FlapgApiResponse; debugFlapg('Got f parameter "%s"', data.result.f); return data; } export enum FlapgIid { /** Nintendo Switch Online app token */ NSO = 'nso', /** Web service token */ APP = 'app', } export interface FlapgApiResponse { result: { f: string; p1: string; p2: string; p3: string; }; } export class ZncaApiFlapg extends ZncaApi { async getLoginHash(id_token: string, timestamp: string) { return getLoginHash(id_token, timestamp, this.useragent); } async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) { const result = await flapg(token, timestamp, uuid, type, this.useragent); return { provider: 'flapg' as const, token, timestamp, uuid, type, f: result.result.f, result, }; } } // // imink // export async function iminkf( token: string, timestamp: string | number, uuid: string, hash_method: '1' | '2', useragent?: string ) { const { default: { coral_auth: { imink: config } } } = await import('../common/remote-config.js'); if (!config) throw new Error('Remote configuration prevents imink API use'); debugImink('Getting f parameter', { token, timestamp, uuid, hash_method, }); const req: IminkFRequest = { hash_method, token, timestamp: '' + timestamp, request_id: uuid, }; const [signal, cancel] = timeoutSignal(); const response = await fetch('https://api.imink.app/f', { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': getUserAgent(useragent), }, body: JSON.stringify(req), signal, }).finally(cancel); const data = await response.json() as IminkFResponse | IminkFError; if ('error' in data) { throw new ErrorResponse('[imink] ' + data.reason, response, data); } debugImink('Got f parameter "%s"', data.f); return data; } export interface IminkFRequest { timestamp: string; request_id: string; hash_method: '1' | '2'; token: string; } export interface IminkFResponse { f: string; } export interface IminkFError { reason: string; error: true; } export class ZncaApiImink extends ZncaApi { async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) { const result = await iminkf(token, timestamp, uuid, type === FlapgIid.APP ? '2' : '1', this.useragent); return { provider: 'imink' as const, token, timestamp, uuid, type, f: result.f, result, }; } } // // nxapi znca API server // export async function genf( url: string, token: string, timestamp: string | number, uuid: string, type: FlapgIid, useragent?: string ) { debugZncaApi('Getting f parameter', { url, token, timestamp, uuid, }); const req: AndroidZncaFRequest = { type, token, timestamp: '' + timestamp, uuid, }; const [signal, cancel] = timeoutSignal(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': getUserAgent(useragent), }, body: JSON.stringify(req), signal, }).finally(cancel); const data = await response.json() as AndroidZncaFResponse | AndroidZncaFError; if ('error' in data) { debugZncaApi('Error getting f parameter "%s"', data.error); throw new ErrorResponse('[znca-api] ' + data.error, response, data); } debugZncaApi('Got f parameter "%s"', data.f); return data; } export interface AndroidZncaFRequest { type: FlapgIid; token: string; timestamp: string; uuid: string; } export interface AndroidZncaFResponse { f: string; } export interface AndroidZncaFError { error: string; } export class ZncaApiNxapi extends ZncaApi { constructor(readonly url: string, useragent?: string) { super(useragent); } async genf(token: string, timestamp: string, uuid: string, type: FlapgIid) { const result = await genf(this.url + '/f', token, timestamp, uuid, type, this.useragent); return { provider: 'nxapi' as const, url: this.url + '/f', token, timestamp, uuid, type, f: result.f, result, }; } } export async function f( token: string, timestamp: string | number, uuid: string, type: FlapgIid, useragent?: string ): Promise { const provider = getPreferredZncaApiFromEnvironment(useragent) ?? await getDefaultZncaApi(useragent); return provider.genf(token, '' + timestamp, uuid, type); } export type FResult = { provider: string; token: string; timestamp: string; uuid: string; type: FlapgIid; f: string; result: unknown; } & ({ provider: 'flapg'; result: FlapgApiResponse; } | { provider: 'imink'; result: IminkFResponse; } | { provider: 'nxapi'; url: string; result: AndroidZncaFResponse; }); export function getPreferredZncaApiFromEnvironment(useragent?: string): ZncaApi | null { if (process.env.NXAPI_ZNCA_API) { if (process.env.NXAPI_ZNCA_API === 'flapg') { return new ZncaApiFlapg(useragent); } if (process.env.NXAPI_ZNCA_API === 'imink') { return new ZncaApiImink(useragent); } throw new Error('Unknown znca API provider'); } if (process.env.ZNCA_API_URL) { return new ZncaApiNxapi(process.env.ZNCA_API_URL, useragent); } return null; } export async function getDefaultZncaApi(useragent?: string) { const { default: { coral_auth: { default: provider } } } = await import('../common/remote-config.js'); if (provider === 'flapg') { return new ZncaApiFlapg(useragent); } if (provider === 'imink') { return new ZncaApiImink(useragent); } if (provider[0] === 'nxapi') { return new ZncaApiNxapi(provider[1], useragent); } throw new Error('Invalid znca API provider'); }