mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
f-generation/request encryption API
This commit is contained in:
parent
e6c97f2924
commit
4889b0d93e
|
|
@ -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<T = unknown>(
|
||||
url: URL | string, method = 'GET', body?: string | Uint8Array | EncryptedRequestBody, _headers?: HeadersInit,
|
||||
flags: Partial<RequestFlags> = {},
|
||||
/** @internal */ _autoRenewToken = true,
|
||||
): Promise<Result<T>> {
|
||||
if (!this[ZncaApiSymbol]) await this.initZncaApi();
|
||||
|
||||
return (new CoralApiRequest<T>(this, url, method, body, _headers, flags, _autoRenewToken)).fetch();
|
||||
return (new CoralApiRequest<T>(this, url, method, body, _headers, flags)).fetch();
|
||||
}
|
||||
|
||||
async call<T extends {}, R extends {}>(
|
||||
url: string, parameter: R & Partial<RequestFlags> = {} 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<T>(url, 'POST', JSON.stringify(body), {}, parameter, _autoRenewToken);
|
||||
return this.fetch<T>(url, 'POST', JSON.stringify(body), {}, parameter);
|
||||
}
|
||||
|
||||
async getCurrentUser() {
|
||||
|
|
@ -509,7 +509,8 @@ export default class CoralApi extends AbstractCoralApi implements CoralApiInterf
|
|||
return await this.fetch<WebServiceToken>('/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<T = unknown> {
|
|||
readonly body: string | Uint8Array | EncryptedRequestBody | undefined,
|
||||
readonly headers: HeadersInit | undefined,
|
||||
readonly flags: Partial<RequestFlags>,
|
||||
readonly auto_renew_token: boolean,
|
||||
) {}
|
||||
|
||||
async fetch(_attempt = 0): Promise<Result<T>> {
|
||||
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<T = unknown> {
|
|||
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 => {
|
||||
|
|
|
|||
538
src/api/f.ts
538
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<FResult>;
|
||||
|
||||
encryptRequest?(url: string, token: string | null, data: string): Promise<EncryptRequestResult>;
|
||||
decryptResponse?(data: Uint8Array): Promise<DecryptResponseResult>;
|
||||
|
||||
supportsEncryption(): this is RequestEncryptionProvider {
|
||||
return !!this.encryptRequest && !!this.decryptResponse;
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestEncryptionProvider {
|
||||
encryptRequest(url: string, token: string | null, data: string): Promise<EncryptRequestResult>;
|
||||
decryptResponse(data: Uint8Array): Promise<DecryptResponseResult>;
|
||||
}
|
||||
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<AndroidZncaFError>('[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<FResult> {
|
||||
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<AndroidZncaFError>(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<AndroidZncaFError>('[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<FResult>;
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, useragent?: string): Promise<FResult>;
|
||||
export async function f(token: string, hash_method: HashMethod | `${HashMethod}`, options?: ZncaApiOptions | string): Promise<FResult> {
|
||||
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<EncryptRequestResult> {
|
||||
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<AndroidZncaFError>(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<DecryptResponseResult> {
|
||||
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<AndroidZncaFError>(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<void> | 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<TokenResponse> {
|
||||
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<AndroidZncaFError>(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');
|
||||
|
|
|
|||
|
|
@ -320,7 +320,7 @@ class StatusUpdateNotificationSubscriber implements StatusUpdateSubscriber {
|
|||
constructor(readonly app: App) {
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
_cache: StatusUpdateNotificationsCache | null = null;
|
||||
_cache_updated = false;
|
||||
_load_cache: Promise<StatusUpdateNotificationsCache> | 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];
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
16
src/util/nxapi-auth.ts
Normal file
16
src/util/nxapi-auth.ts
Normal file
|
|
@ -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<OAuthClientAssertion>;
|
||||
}
|
||||
export interface OAuthClientAssertion {
|
||||
assertion: string;
|
||||
type: string;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user