feat: implement authentication test command and api utilities

This commit is contained in:
daydreamer-json
2026-01-23 02:40:37 +09:00
parent 62cacda659
commit dea23392d2
13 changed files with 616 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
import ky from 'ky';
import semver from 'semver';
import * as TypesApiAkEndfield from '../types/api/akEndfield/api.js';
import * as TypesApiAkEndfield from '../types/api/akEndfield/Api.js';
import appConfig from './config.js';
const defaultKySettings = {
@@ -248,5 +248,104 @@ export default {
},
},
},
accountService: {
user: {
auth: {
v1: {
tokenByEmailPassword: async (
email: string,
password: string,
from: number = 0,
): Promise<TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail> => {
const rsp = await ky
.post(
`https://${appConfig.network.api.akEndfield.base.accountService}/user/auth/v1/token_by_email_password`,
{ ...defaultKySettings, json: { email, from, password } },
)
.json();
return rsp as TypesApiAkEndfield.AccSrvUserAuthV1TokenByEmail;
},
},
},
oauth2: {
v2: {
grant: async (
appCode: string,
token: string,
type: number = 0,
): Promise<TypesApiAkEndfield.AccSrvUserOAuth2V2Grant> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.accountService}/user/oauth2/v2/grant`, {
...defaultKySettings,
json: { appCode, token, type },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserOAuth2V2Grant;
},
},
},
info: {
v1: {
basic: async (appCode: string, token: string): Promise<TypesApiAkEndfield.AccSrvUserInfoV1Basic> => {
const rsp = await ky
.get(`https://${appConfig.network.api.akEndfield.base.accountService}/user/info/v1/basic`, {
...defaultKySettings,
searchParams: { appCode, token },
})
.json();
return rsp as TypesApiAkEndfield.AccSrvUserInfoV1Basic;
},
},
},
},
},
u8: {
user: {
auth: {
v2: {
tokenByChToken: async (
appCode: string,
channelMasterId: number,
channelToken: string,
platform: number = 2,
type: number = 0,
): Promise<TypesApiAkEndfield.U8UserAuthV2ChToken> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/u8/user/auth/v2/token_by_channel_token`, {
...defaultKySettings,
json: {
appCode,
channelMasterId,
channelToken: JSON.stringify({
code: channelToken,
type: 1,
isSuc: true,
}),
type,
platform,
},
})
.json();
return rsp as TypesApiAkEndfield.U8UserAuthV2ChToken;
},
},
},
},
game: {
server: {
v1: {
serverList: async (token: string): Promise<TypesApiAkEndfield.U8GameServerV1ServerList> => {
const rsp = await ky
.post(`https://${appConfig.network.api.akEndfield.base.u8}/game/server/v1/server_list`, {
...defaultKySettings,
json: { token },
})
.json();
return rsp as TypesApiAkEndfield.U8GameServerV1ServerList;
},
},
},
},
},
},
};

View File

@@ -15,8 +15,12 @@ type ConfigType = AllRequired<
network: {
api: {
akEndfield: {
appCode: { osWinRel: string };
launcherAppCode: { osWinRel: string };
appCode: {
game: { osWinRel: string };
launcher: { osWinRel: string };
accountService: { osWinRel: string };
u8: { osWinRel: string };
};
channel: { osWinRel: number };
base: {
accountService: string;
@@ -55,8 +59,12 @@ const initialConfig: ConfigType = {
network: {
api: {
akEndfield: {
appCode: { osWinRel: 'YDUTE5gscDZ229CW' },
launcherAppCode: { osWinRel: 'TiaytKBUIEdoEwRT' },
appCode: {
game: { osWinRel: 'YDUTE5gscDZ229CW' },
launcher: { osWinRel: 'TiaytKBUIEdoEwRT' },
accountService: { osWinRel: 'd9f6dbb6bbd6bb33' },
u8: { osWinRel: '973bd727dd11cbb6ead8' },
},
channel: { osWinRel: 6 },
base: {
accountService: 'YXMuZ3J5cGhsaW5lLmNvbQ==',

148
src/utils/string.ts Normal file
View File

@@ -0,0 +1,148 @@
// === Generated by Qwen3-Max ===
/**
* 元となるURLと相対URLから絶対URLを解決します。
* ベースURLがファイルパスを含む場合でも、そのファイルのディレクトリを基準に解決します。
*
* @param baseUrl - 元となるURL絶対URL
* @param relativeUrl - 相対URLまたは絶対URL
* @returns 解決された絶対URL
* @throws URLが無効な場合にエラーをスローします
*/
function resolveUrl(baseUrl: string, relativeUrl: string): string {
try {
// 相対URLがすでに絶対URLの場合、そのまま返す
if (isAbsoluteUrl(relativeUrl)) {
return new URL(relativeUrl).href;
}
// baseUrlをURLとしてパース
const base = new URL(baseUrl);
// ベースURLがファイルパス末尾が/でない)の場合、
// ファイル名を除去してディレクトリパスにする
if (!base.pathname.endsWith('/')) {
// 最後のスラッシュ以降を除去(ファイル名を削除)
const lastSlashIndex = base.pathname.lastIndexOf('/');
if (lastSlashIndex !== -1) {
base.pathname = base.pathname.substring(0, lastSlashIndex + 1);
} else {
// パスにスラッシュが含まれていない稀なケース
base.pathname = '/';
}
}
// 相対URLを解決
const resolved = new URL(relativeUrl, base);
return resolved.href;
} catch (error) {
throw new Error(`URLの解決に失敗しました: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* URLが絶対URLかどうかを判定します。
*
* @param url - 判定するURL文字列
* @returns 絶対URLの場合はtrue、それ以外はfalse
*/
function isAbsoluteUrl(url: string): boolean {
try {
return Boolean(new URL(url).protocol);
} catch {
return false;
}
}
/**
* URLフルパスからファイル名を除いた部分を抽出します
* @param url
* @returns
*/
function getBaseUrlWithoutLastSegment(url: string): string {
const u = new URL(url);
const dirPath = u.pathname.split('/').slice(0, -1).join('/');
return `${u.origin}${dirPath}`;
}
// ==============================
function replaceMultiPatterns(replacements: [RegExp, string][], originalString: string): string {
return replacements.reduce((currentString, [pattern, replacement]) => {
return currentString.replace(pattern, replacement);
}, originalString);
}
function sanitizeFilename(filename: string) {
const invalidCharsMap: Record<string, string> = {
'\\': '',
'/': '',
':': '',
'*': '',
'?': '',
'"': '',
'<': '',
'>': '',
'|': '',
'&': '',
};
return Array.from(filename)
.map((char) => invalidCharsMap[char] || char)
.join('');
}
/**
* 文字列が指定された正規表現パターンにマッチするかを判定する関数
*
* @param inputStr - チェック対象の文字列
* @param regexInclude - マッチが必須の正規表現パターン少なくとも1つマッチする必要あり
* @param regexExclude - マッチが禁止の正規表現パターン1つもマッチしてはならない
* @returns
* includeパターンが空の場合はexcludeにマッチしない限りtrue
* includeパターンがある場合は、少なくとも1つのincludeにマッチし、
* かつ全てのexcludeにマッチしない場合のみtrue
*/
function filterByRegex(
inputStr: string,
regexInclude: readonly (RegExp | string)[],
regexExclude: readonly (RegExp | string)[],
): boolean {
// 文字列をRegExpに変換するヘルパー関数グローバルフラグを無効化
const toRegExp = (pattern: RegExp | string): RegExp => {
if (pattern instanceof RegExp) {
// グローバルフラグ/ステッキーフラグを無効化test()の状態問題を回避)
const flags = pattern.flags.replace(/[gy]/g, '');
return new RegExp(pattern.source, flags);
}
return new RegExp(pattern);
};
// 正規表現を変換include/exclude両方
const includePatterns = regexInclude.map(toRegExp);
const excludePatterns = regexExclude.map(toRegExp);
// 1. excludeパターンに1つでもマッチしたら即座にfalse
if (excludePatterns.some((pattern) => pattern.test(inputStr))) {
return false;
}
// 2. includeパターンが空の場合はtrueexcludeを通過した時点でOK
if (includePatterns.length === 0) {
return true;
}
// 3. includeパターンに1つでもマッチすればtrue
return includePatterns.some((pattern) => pattern.test(inputStr));
}
// ==============================
export default {
resolveUrl,
isAbsoluteUrl,
getBaseUrlWithoutLastSegment,
replaceMultiPatterns,
sanitizeFilename,
filterByRegex,
};

46
src/utils/termPretty.ts Normal file
View File

@@ -0,0 +1,46 @@
const cliTableConfig = {
rounded: {
chars: {
top: '─',
'top-mid': '',
'top-left': '╭',
'top-right': '╮',
bottom: '─',
'bottom-mid': '',
'bottom-left': '╰',
'bottom-right': '╯',
left: '│',
'left-mid': '├',
mid: '─',
'mid-mid': '',
right: '│',
'right-mid': '┤',
middle: '',
},
style: { 'padding-left': 1, 'padding-right': 1, head: [''], border: [''], compact: true },
},
noBorder: {
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' ',
},
style: { 'padding-left': 0, 'padding-right': 0 },
},
};
export default {
cliTableConfig,
};