diff --git a/src/api/coral.ts b/src/api/coral.ts index b3775dd..9531593 100644 --- a/src/api/coral.ts +++ b/src/api/coral.ts @@ -329,12 +329,13 @@ const NintendoAccountIdSymbol = Symbol('NintendoAccountId'); const ZncaApiSymbol = Symbol('ZncaApi'); const ZncaApiPromiseSymbol = Symbol('ZncaApiPromise'); -export const RequestFlagAddProductVersionSymbol = Symbol('RequestFlagAddProductVersionSymbol'); -export const RequestFlagAddPlatformSymbol = Symbol('RequestFlagAddPlatformSymbol'); -export const RequestFlagNoAuthenticationSymbol = Symbol('RequestFlagNoAuthenticationSymbol'); -export const RequestFlagNoEncryptionSymbol = Symbol('RequestFlagNoEncryptionSymbol'); -export const RequestFlagNoParameterSymbol = Symbol('RequestFlagNoParameterSymbol'); -export const RequestFlagRequestIdSymbol = Symbol('RequestFlagRequestIdSymbol'); +export const RequestFlagAddProductVersionSymbol = Symbol('RequestFlagAddProductVersion'); +export const RequestFlagAddPlatformSymbol = Symbol('RequestFlagAddPlatform'); +export const RequestFlagNoAuthenticationSymbol = Symbol('RequestFlagNoAuthentication'); +export const RequestFlagNoEncryptionSymbol = Symbol('RequestFlagNoEncryption'); +export const RequestFlagNoParameterSymbol = Symbol('RequestFlagNoParameter'); +export const RequestFlagRequestIdSymbol = Symbol('RequestFlagRequestId'); +export const RequestFlagNoAutoRenewTokenSymbol = Symbol('RequestFlagNoAutoRenewToken'); export interface RequestFlags { [RequestFlagAddProductVersionSymbol]: boolean; @@ -343,6 +344,7 @@ export interface RequestFlags { [RequestFlagNoEncryptionSymbol]: boolean; [RequestFlagNoParameterSymbol]: boolean; [RequestFlagRequestIdSymbol]: RequestFlagRequestId; + [RequestFlagNoAutoRenewTokenSymbol]: boolean; } export enum RequestFlagRequestId { NONE, @@ -431,16 +433,14 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf async fetch( url: URL | string, method = 'GET', body?: string | Uint8Array | EncryptedRequestBody, _headers?: HeadersInit, flags: Partial = {}, - /** @internal */ _autoRenewToken = true, ): Promise> { if (!this[ZncaApiSymbol]) await this.initZncaApi(); - return (new CoralApiRequest(this, url, method, body, _headers, flags, _autoRenewToken)).fetch(); + return (new CoralApiRequest(this, url, method, body, _headers, flags)).fetch(); } async call( url: string, parameter: R & Partial = {} as R, - /** @internal */ _autoRenewToken = true, ) { const body = {} as any; @@ -456,7 +456,7 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf if (ri !== RequestFlagRequestId.AFTER && !parameter[RequestFlagNoParameterSymbol]) body.parameter = parameter; - return this.fetch(url, 'POST', JSON.stringify(body), {}, parameter, _autoRenewToken); + return this.fetch(url, 'POST', JSON.stringify(body), {}, parameter); } async getCurrentUser() { @@ -509,7 +509,8 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf return await this.fetch('/v4/Game/GetWebServiceToken', 'POST', body, undefined, { [RequestFlagAddPlatformSymbol]: true, [RequestFlagAddProductVersionSymbol]: true, - }, false); + [RequestFlagNoAutoRenewTokenSymbol]: true, + }); } catch (err) { if (err instanceof CoralErrorResponse && err.status === CoralStatus.TOKEN_EXPIRED && !_attempt && this.onTokenExpired) { debug('Error getting web service token, renewing token before retrying', err); @@ -576,7 +577,8 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf [RequestFlagAddPlatformSymbol]: true, [RequestFlagAddProductVersionSymbol]: true, [RequestFlagNoAuthenticationSymbol]: true, - }, false); + [RequestFlagNoAutoRenewTokenSymbol]: true, + }); return { nintendoAccountToken, @@ -764,11 +766,10 @@ class CoralApiRequest { readonly body: string | Uint8Array | EncryptedRequestBody | undefined, readonly headers: HeadersInit | undefined, readonly flags: Partial, - readonly auto_renew_token: boolean, ) {} async fetch(_attempt = 0): Promise> { - if (this.auto_renew_token) { + if (!this.flags[RequestFlagNoAutoRenewTokenSymbol]) { if (this.coral._token_expired && !this.coral._renewToken) { if (!this.coral.onTokenExpired || _attempt) throw new Error('Token expired'); @@ -909,7 +910,7 @@ class CoralApiRequest { debug('fetch %s %s, response %s, status %d %s, correlationId %s', this.method, this.url, response.status, data.status, CoralStatus[data.status], data?.correlationId); - if (data.status === CoralStatus.TOKEN_EXPIRED && this.auto_renew_token && !_attempt && this.coral.onTokenExpired) { + if (data.status === CoralStatus.TOKEN_EXPIRED && !this.flags[RequestFlagNoAutoRenewTokenSymbol] && !_attempt && this.coral.onTokenExpired) { this.coral._token_expired = true; // _renewToken will be awaited when calling fetch this.coral._renewToken = this.coral._renewToken ?? this.coral.onTokenExpired.call(null, data, response).then(data => { diff --git a/src/api/f.ts b/src/api/f.ts index aa7b39d..6c70ae5 100644 --- a/src/api/f.ts +++ b/src/api/f.ts @@ -5,7 +5,9 @@ import { defineResponse, ErrorResponse } from './util.js'; import createDebug from '../util/debug.js'; import { timeoutSignal } from '../util/misc.js'; import { getUserAgent } from '../util/useragent.js'; +import { client_assertion_provider, ClientAssertionProviderInterface } from '../util/nxapi-auth.js'; import { ZNCA_VERSION } from './coral.js'; +import { AccountLoginParameter, AccountTokenParameter, WebServiceTokenParameter } from './coral-types.js'; const debugFlapg = createDebug('nxapi:api:flapg'); const debugImink = createDebug('nxapi:api:imink'); @@ -19,7 +21,26 @@ export abstract class ZncaApi { abstract genf( token: string, hash_method: HashMethod, user?: {na_id: string; coral_user_id?: string;}, + encrypt_request?: EncryptRequestOptions, ): Promise; + + encryptRequest?(url: string, token: string | null, data: string): Promise; + decryptResponse?(data: Uint8Array): Promise; + + supportsEncryption(): this is RequestEncryptionProvider { + return !!this.encryptRequest && !!this.decryptResponse; + } +} + +export interface RequestEncryptionProvider { + encryptRequest(url: string, token: string | null, data: string): Promise; + decryptResponse(data: Uint8Array): Promise; +} +export interface EncryptRequestResult { + data: Uint8Array; +} +export interface DecryptResponseResult { + data: string; } export enum HashMethod { @@ -194,71 +215,33 @@ export class ZncaApiImink extends ZncaApi { // nxapi znca API server // -export async function genf( - url: string, hash_method: HashMethod, - token: string, timestamp?: number, request_id?: string, - user?: {na_id: string; coral_user_id?: string;}, - app?: {platform?: string; version?: string;}, - useragent?: string, -) { - debugZncaApi('Getting f parameter', { - url, hash_method, token, timestamp, request_id, user, - znca_platform: app?.platform, znca_version: app?.version, - }); - - const req: AndroidZncaFRequest = { - hash_method: '' + hash_method as `${HashMethod}`, - token, - timestamp, - request_id, - ...user, - }; - - const headers = new Headers({ - 'Content-Type': 'application/json', - 'User-Agent': getUserAgent(useragent), - }); - if (app?.platform) headers.append('X-znca-Platform', app.platform); - if (app?.version) headers.append('X-znca-Version', app.version); - if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); - - const [signal, cancel] = timeoutSignal(); - const response = await fetch(url, { - method: 'POST', - headers, - body: JSON.stringify(req), - signal, - }).finally(cancel); - - if (response.status !== 200) { - throw await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code'); - } - - 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_message ?? data.error), response, data); - } - - debugZncaApi('Got f parameter', data, response.headers); - - return defineResponse(data, response); -} - export interface AndroidZncaFRequest { hash_method: '1' | '2'; token: string; timestamp?: string | number; request_id?: string; + encrypt_token_request?: EncryptRequestOptions; } export interface AndroidZncaFResponse { f: string; timestamp?: number; request_id?: string; + encrypted_token_request?: string; + warnings?: {error: string; error_message: string}[]; } + +export interface AndroidZncaEncryptRequestRequest { + url: string; + token: string | null; + data: string; +} + +export interface AndroidZncaDecryptResponseRequest { + data: string; +} + export interface AndroidZncaFError { error: string; error_message?: string; @@ -267,16 +250,172 @@ export interface AndroidZncaFError { warnings?: {error: string; error_message: string}[]; } -export class ZncaApiNxapi extends ZncaApi { - constructor(readonly url: string, readonly app?: {platform?: string; version?: string;}, useragent?: string) { +export interface TokenResponse { + access_token: string; + token_type: 'Bearer'; + expires_in: number; + scope?: string; + refresh_token?: string; +} + +interface ProtectedResourceMetadata { + resource: string; + authorization_servers?: string[]; + jwks_uri?: string; + scopes_supported?: string[]; + bearer_methods_supported?: string[]; + resource_signing_alg_values_supported?: string[]; + resource_name?: string; + resource_documentation?: string; + resource_policy_uri?: string; + resource_tos_uri?: string; +} + +interface AuthorisationServerMetadata { + issuer: string; + authorization_endpoint?: string; + token_endpoint?: string; + jwks_uri?: string; + registration_endpoint?: string; + scopes_supported?: string[]; + response_types_supported: string[]; + response_modes_supported?: string[]; + grant_types_supported?: string[]; + token_endpoint_auth_methods_supported?: string[]; + token_endpoint_auth_signing_alg_values_supported?: string[]; + service_documentation?: string; + ui_locales_supported?: string[]; + op_policy_uri?: string; + op_tos_uri?: string; + revocation_endpoint?: string; + revocation_endpoint_auth_methods_supported?: string[]; + revocation_endpoint_auth_signing_alg_values_supported?: string[]; + introspection_endpoint?: string; + introspection_endpoint_auth_methods_supported?: string[]; + introspection_endpoint_auth_signing_alg_values_supported?: string[]; + code_challenge_methods_supported?: string[]; +} + +export interface TokenData { + token: string; + expires_at: number; + refresh_token: string | null; + result: TokenResponse; +} +export interface ResourceData { + token_endpoint: string; + client_assertion_aud: string; + + resource_metadata: ProtectedResourceMetadata; + authorisation_server_metadata: AuthorisationServerMetadata; +} + +export class ZncaApiNxapi extends ZncaApi implements RequestEncryptionProvider { + client_assertion_provider: ClientAssertionProviderInterface | null = null; + client_credentials: + { assertion: string; assertion_type: string; } | + { id: string; secret: string; } | + null = null; + + token: TokenData | null = null; + refresh_token: string | null = null; + + protected_resource: ResourceData | null = null; + + headers = new Headers(); + + constructor( + readonly url: string, + readonly app?: {platform?: string; version?: string;}, + useragent?: string, + ) { super(useragent); + + this.headers.set('User-Agent', getUserAgent(useragent)); } - async genf(token: string, hash_method: HashMethod, user?: {na_id: string; coral_user_id?: string}) { - // const request_id = randomUUID(); + static create( + url: string, + app?: {platform?: string; version?: string;}, + useragent?: string, + ) { + const provider = new ZncaApiNxapi(url, app, useragent); - const result = await genf(this.url + '/f', hash_method, token, undefined, undefined, - user, this.app, this.useragent); + if (process.env.NXAPI_ZNCA_API_CLIENT_ID && process.env.NXAPI_ZNCA_API_CLIENT_SECRET) { + provider.client_credentials = { + id: process.env.NXAPI_ZNCA_API_CLIENT_ID, + secret: process.env.NXAPI_ZNCA_API_CLIENT_SECRET, + }; + } else if (process.env.NXAPI_ZNCA_API_CLIENT_ASSERTION) { + provider.client_credentials = { + assertion: process.env.NXAPI_ZNCA_API_CLIENT_ASSERTION, + assertion_type: process.env.NXAPI_ZNCA_API_CLIENT_ASSERTION_TYPE ?? + 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', + }; + } else { + provider.client_assertion_provider = client_assertion_provider; + } + + return provider; + } + + async genf( + token: string, hash_method: HashMethod, user?: {na_id: string; coral_user_id?: string}, + encrypt_token_request?: EncryptRequestOptions, + ): Promise { + if (!this.token) await this.authenticate(); + + debugZncaApi('Getting f parameter', { + url: this.url + '/f', hash_method, token, timestamp: undefined, request_id: undefined, user, + znca_platform: this.app?.platform, znca_version: this.app?.version, + encrypt_token_request, + }); + + const req: AndroidZncaFRequest = { + hash_method: '' + hash_method as `${HashMethod}`, + token, + ...user, + encrypt_token_request, + }; + + const headers = new Headers(this.headers); + headers.set('Content-Type', 'application/json'); + headers.set('Accept', 'application/json'); + if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); + if (this.app?.version) headers.append('X-znca-Version', this.app.version); + if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); + if (this.token) headers.append('Authorization', 'Bearer ' + this.token.token); + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(this.url + '/f', { + method: 'POST', + headers, + body: JSON.stringify(req), + signal, + }).finally(cancel); + + if (response.status !== 200) { + const err = await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code'); + + if (err.data?.error === 'invalid_token') { + this.token = null; + + return this.genf(token, hash_method, user, encrypt_token_request); + } + + throw err; + } + + 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_message ?? data.error), response, data); + } + + debugZncaApi('Got f parameter', data, response.headers); + + const result = defineResponse(data, response); return { provider: 'nxapi' as const, @@ -287,19 +426,282 @@ export class ZncaApiNxapi extends ZncaApi { f: result.f, user, result, + encrypt_request_result: result.encrypted_token_request ? Buffer.from(result.encrypted_token_request, 'base64') : undefined, }; } -} -export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, options?: ZncaApiOptions): Promise; -export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, useragent?: string): Promise; -export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, options?: ZncaApiOptions | string): Promise { - if (typeof options === 'string') options = {useragent: options}; - if (typeof hash_method === 'string') hash_method = parseInt(hash_method); + async encryptRequest(url: string, token: string | null, data: string): Promise { + if (!this.token) await this.authenticate(); - const provider = getPreferredZncaApiFromEnvironment(options) ?? await getDefaultZncaApi(options); + debugZncaApi('encrypting request', { url, data }); - return provider.genf(token, hash_method, options?.user); + const req: AndroidZncaEncryptRequestRequest = { + url, + token, + data, + }; + + const headers = new Headers(this.headers); + headers.set('Content-Type', 'application/json'); + headers.set('Accept', 'application/octet-stream'); + if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); + if (this.app?.version) headers.append('X-znca-Version', this.app.version); + if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); + if (this.token) headers.append('Authorization', 'Bearer ' + this.token); + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(this.url + '/encrypt-request', { + method: 'POST', + headers, + body: JSON.stringify(req), + signal, + }).finally(cancel); + + if (response.status !== 200) { + const err = await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code'); + + if (err.data?.error === 'invalid_token') { + this.token = null; + + return this.encryptRequest(url, token, data); + } + + throw err; + } + + const encrypted_data = new Uint8Array(await response.arrayBuffer()); + + const result = defineResponse(encrypted_data, response); + + return { + data: result, + }; + } + + async decryptResponse(data: Uint8Array): Promise { + if (!this.has_valid_token) await this.authenticate(); + + debugZncaApi('decrypting response', data); + + const req: AndroidZncaDecryptResponseRequest = { + data: Buffer.from(data).toString('base64'), + }; + + const headers = new Headers(this.headers); + headers.set('Content-Type', 'application/json'); + headers.set('Accept', 'text/plain'); + if (this.app?.platform) headers.append('X-znca-Platform', this.app.platform); + if (this.app?.version) headers.append('X-znca-Version', this.app.version); + if (ZNCA_VERSION) headers.append('X-znca-Client-Version', ZNCA_VERSION); + if (this.token) headers.append('Authorization', 'Bearer ' + this.token.token); + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(this.url + '/decrypt-response', { + method: 'POST', + headers, + body: JSON.stringify(req), + signal, + }).finally(cancel); + + if (response.status !== 200) { + const err = await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code'); + + if (err.data?.error === 'invalid_token') { + this.token = null; + + return this.decryptResponse(data); + } + + throw err; + } + + const decrypted_data = await response.text(); + + return { + data: decrypted_data, + + // @ts-expect-error + response, + }; + } + + private _authenticate: Promise | null = null; + + authenticate() { + return this._authenticate ?? (this._authenticate = this.getAccessToken() + .then(result => { + this.token = { + token: result.access_token, + expires_at: Date.now() + (result.expires_in * 1000), + refresh_token: result.refresh_token ?? this.refresh_token ?? null, + result, + }; + if (result.refresh_token) { + this.refresh_token = result.refresh_token; + } + }) + .finally(() => this._authenticate = null)); + } + + get has_valid_token() { + return !!this.token && this.token.expires_at > Date.now(); + } + + async getAccessToken(): Promise { + const resource = this.protected_resource ?? (this.protected_resource = await this.getProtectedResource()); + const refresh_token = this.refresh_token; + + debugZncaApi('fetching nxapi-znca-api token'); + + const headers = new Headers(this.headers); + headers.set('Accept', 'application/json'); + + const body = new URLSearchParams(); + + if (refresh_token) { + body.append('grant_type', 'refresh_token'); + body.append('refresh_token', refresh_token); + } else if (this.client_credentials && 'secret' in this.client_credentials) { + body.append('grant_type', 'client_credentials'); + body.append('client_id', this.client_credentials.id); + body.append('client_secret', this.client_credentials.secret); + body.append('scope', 'ca:gf ca:er ca:dr'); + } else if (this.client_credentials && 'assertion' in this.client_credentials) { + body.append('grant_type', 'client_credentials'); + body.append('client_assertion_type', this.client_credentials.assertion_type); + body.append('client_assertion', this.client_credentials.assertion); + body.append('scope', 'ca:gf ca:er ca:dr'); + } else if (this.client_assertion_provider) { + const { assertion, type } = await this.client_assertion_provider.create(resource.client_assertion_aud); + + body.append('grant_type', 'client_credentials'); + body.append('client_assertion_type', type); + body.append('client_assertion', assertion); + body.append('scope', 'ca:gf ca:er ca:dr'); + } else { + if (resource.resource_metadata.resource_documentation) { + throw new TypeError('Client authentication not configured\n\n' + + 'See resource documentation at ' + resource.resource_metadata.resource_documentation); + } + + throw new TypeError('Client authentication not configured'); + } + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(resource.token_endpoint, { + method: 'POST', + headers, + body, + signal, + }).finally(cancel); + + if (response.status !== 200) { + const err = await ErrorResponse.fromResponse(response, '[znca-api] Non-200 status code'); + + if (refresh_token && err.data?.error === 'invalid_grant') { + this.refresh_token = null; + + return this.getAccessToken(); + } + + throw err; + } + + const data = await response.json() as TokenResponse; + + const result = defineResponse(data, response); + + debugZncaApi('token', result); + + return result; + } + + async getProtectedResource() { + const resource = new URL(this.url).origin; + + const resource_metadata = await this.getProtectedResourceMetadata(resource); + + if (!resource_metadata.authorization_servers?.length) { + throw new TypeError('Unable to find authorisation server'); + } + if (resource_metadata.authorization_servers.length > 1) { + debugZncaApi('multiple authorisation servers returned for %s, using first', resource); + } + + const authorisation_server = resource_metadata.authorization_servers[0]; + + const authorisation_server_metadata = await this.getAuthorisationServerMetadata(authorisation_server); + + if (!authorisation_server_metadata.token_endpoint) { + throw new TypeError('Unable to find authorisation server token endpoint'); + } + + return { + token_endpoint: authorisation_server_metadata.token_endpoint, + client_assertion_aud: authorisation_server_metadata.issuer, + + resource_metadata, + authorisation_server_metadata, + }; + } + + async getProtectedResourceMetadata(resource: URL | string) { + if (typeof resource === 'string') resource = new URL(resource); + + if (resource.search) debugZncaApi('resource identifier contains search parameters'); + if (resource.hash) throw new TypeError('Resource identifier contains fragment'); + + debugZncaApi('fetching protected resource metadata for %s', resource.href); + + const metadata_url = new URL(resource); + + metadata_url.pathname = '/.well-known/oauth-protected-resource' + + (metadata_url.pathname === '/' ? '' : metadata_url.pathname); + + const headers = new Headers(this.headers); + headers.set('Accept', 'application/json'); + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(metadata_url, { + headers, + signal, + }).finally(cancel); + + const data = await response.json() as ProtectedResourceMetadata; + + const result = defineResponse(data, response); + + return result; + } + + async getAuthorisationServerMetadata(issuer: URL | string) { + if (typeof issuer === 'string') issuer = new URL(issuer); + + if (issuer.search) debugZncaApi('issuer identifier contains search parameters'); + if (issuer.hash) throw new TypeError('Issuer identifier contains fragment'); + + debugZncaApi('fetching authorisation server metadata for %s', issuer.href); + + const metadata_url = new URL(issuer); + + metadata_url.pathname = '/.well-known/oauth-authorization-server' + + (metadata_url.pathname === '/' ? '' : metadata_url.pathname); + + const headers = new Headers(this.headers); + headers.set('Accept', 'application/json'); + + const [signal, cancel] = timeoutSignal(); + const response = await fetch(metadata_url, { + headers, + signal, + }).finally(cancel); + + const data = await response.json() as AuthorisationServerMetadata; + + const result = defineResponse(data, response); + + return result; + } } export type FResult = { @@ -357,7 +759,7 @@ export function getPreferredZncaApiFromEnvironment(options?: ZncaApiOptions | st } if (process.env.ZNCA_API_URL) { - return new ZncaApiNxapi(process.env.ZNCA_API_URL, options, options?.useragent); + return ZncaApiNxapi.create(process.env.ZNCA_API_URL, options, options?.useragent); } return null; @@ -378,7 +780,7 @@ export async function getDefaultZncaApi(options?: ZncaApiOptions | string) { } if (provider[0] === 'nxapi') { - return new ZncaApiNxapi(provider[1], options, options?.useragent); + return ZncaApiNxapi.create(provider[1], options, options?.useragent); } throw new Error('Invalid znca API provider'); diff --git a/src/app/main/index.ts b/src/app/main/index.ts index 20fbb87..95ef37a 100644 --- a/src/app/main/index.ts +++ b/src/app/main/index.ts @@ -320,7 +320,7 @@ class StatusUpdateNotificationSubscriber implements StatusUpdateSubscriber { constructor(readonly app: App) { // } - + _cache: StatusUpdateNotificationsCache | null = null; _cache_updated = false; _load_cache: Promise | null = null; @@ -346,7 +346,7 @@ class StatusUpdateNotificationSubscriber implements StatusUpdateSubscriber { async onNewStatusUpdate(status_update: StatusUpdateResult[0]) { if (status_update.notify === StatusUpdateNotify.NO) return; - + const cache = await this.getNotificationsCache(); const id = status_update[StatusUpdateIdentifierSymbol]; diff --git a/src/common/status.ts b/src/common/status.ts index 7a8ebbd..422e6fb 100644 --- a/src/common/status.ts +++ b/src/common/status.ts @@ -81,7 +81,7 @@ export class StatusUpdateMonitor { start(now = true) { if (this._timeout) return; - + if (now) { this._timeout = setTimeout(() => {}, 0); this._checkStatusUpdatesInterval(); @@ -120,7 +120,7 @@ export class StatusUpdateMonitor { this.subscribers.splice(index, 1); removed++; } - + if (!removed) { debug('attempted to unsubscribe but already not subscribed'); return; @@ -146,7 +146,7 @@ export class StatusUpdateMonitor { const updated = result.map(s => { const existing = this.cache!.find(a => s[StatusUpdateIdentifierSymbol] === a[StatusUpdateIdentifierSymbol]); - + return existing && JSON.stringify(s) === JSON.stringify(existing) ? [s, existing] as const : null as never; }).filter(s => s); @@ -202,7 +202,7 @@ export class StatusUpdateMonitor { const subscribers = [...this.subscribers]; debug('notifying %d subscribers of result', subscribers.length); - + for (const subscriber of subscribers) { Promise.resolve() .then(() => subscriber.onUpdate.call(subscriber, result)) diff --git a/src/util/nxapi-auth.ts b/src/util/nxapi-auth.ts new file mode 100644 index 0000000..6387298 --- /dev/null +++ b/src/util/nxapi-auth.ts @@ -0,0 +1,16 @@ +export const NXAPI_AUTH_CLI_CLIENT_ID = 'CKtknJ6HiH2AZIMw-x8ljw'; +export const NXAPI_AUTH_APP_CLIENT_ID = 'GlR_qsPZpNcxqMwnbsSjMA'; + +export let client_assertion_provider: ClientAssertionProviderInterface | null = null; + +export function setClientAssertionProvider(provider: ClientAssertionProviderInterface) { + client_assertion_provider = provider; +} + +export interface ClientAssertionProviderInterface { + create(aud: string, exp?: number): Promise; +} +export interface OAuthClientAssertion { + assertion: string; + type: string; +}