refactor(web-panel): update architecture + cheat i18n

Reorganize the panel around product capabilities and integrate the
remote i18n feature from origin/master into the new structure.

- Restructure src into capability features (app, trainer, library,
  remote-session, appearance, shared); move the bridge to bridge/src.
- Add lingui-based UI localization and wrap remaining user-facing
  strings (Trans / msg macros); fix the 'END В·' mojibake.
- Port WeMod cheat-metadata i18n: capture the access token from the
  snapshot's renderer in the bridge and fetch localized trainer_meta
  in remote-session.i18n; guarantee snapshot sync on token failure.
- Wire vitest to the app's lingui/preact pipeline (mergeConfig).
This commit is contained in:
kitbyte
2026-06-15 00:10:49 +03:00
parent 8756e41fb9
commit a0b3968d33
106 changed files with 5526 additions and 1421 deletions
+142
View File
@@ -0,0 +1,142 @@
const {
buildInstalledAppsDebugPayload,
gameStatusSignature,
installedAppsSignature,
normalizeGameStatusSnapshot,
normalizeInstalledAppsSnapshot,
normalizeSnapshot,
normalizeTrainerValue,
summarizeInstalledAppsSource,
} = require('./normalizers');
const { cloneValue, isRecord, safeString } = require('./utils');
const { sendJson } = require('./websocket-codec');
function createBridgeState({ clients, log, getServerInfo }) {
let currentSnapshot: any = null;
let currentInstalledApps: any = null;
let currentInstalledAppsSignature: string | null = null;
let currentGameStatus: any = null;
let currentGameStatusSignature: string | null = null;
function broadcast(type, payload, requestId = null) {
for (const client of clients) {
sendJson(client, type, payload, requestId);
}
}
function sendSnapshot(client) {
if (!currentSnapshot) {
sendJson(client, 'trainer_changed', { previousTrainerId: null, trainerId: '' });
} else {
sendJson(client, 'trainer_meta', currentSnapshot.trainerMeta);
sendJson(client, 'trainer_values', currentSnapshot.trainerValues);
}
if (currentGameStatus) sendJson(client, 'game_status', currentGameStatus);
if (currentInstalledApps) sendJson(client, 'installed_apps', currentInstalledApps);
}
function sync(rawSnapshot) {
const nextSnapshot = rawSnapshot ? normalizeSnapshot(rawSnapshot) : null;
const previousTrainerId = currentSnapshot?.trainerMeta?.trainer?.trainerId ?? null;
const nextTrainerId = nextSnapshot?.trainerMeta?.trainer?.trainerId ?? null;
currentSnapshot = nextSnapshot;
if (previousTrainerId !== nextTrainerId) {
broadcast('trainer_changed', { previousTrainerId, trainerId: nextTrainerId || '' });
}
if (currentSnapshot) {
broadcast('trainer_meta', currentSnapshot.trainerMeta);
broadcast('trainer_values', currentSnapshot.trainerValues);
}
}
function valueChanged(change) {
if (!currentSnapshot || !isRecord(change)) return;
const target = safeString(change.target);
if (!target) return;
const value = normalizeTrainerValue(currentSnapshot, target, change.value);
currentSnapshot.trainerValues.values[target] = value;
broadcast('value_changed', {
trainerId: safeString(change.trainerId, currentSnapshot.trainerMeta.trainer.trainerId),
target,
value,
oldValue: cloneValue(change.oldValue),
source: safeString(change.source, 'desktop'),
cheatId: typeof change.cheatId === 'string' ? change.cheatId : undefined,
});
}
function syncInstalledApps(rawInstalledApps) {
const sourceSummary = summarizeInstalledAppsSource(rawInstalledApps);
const nextInstalledApps = normalizeInstalledAppsSnapshot(rawInstalledApps);
if (!nextInstalledApps) {
log('warn', `Ignored invalid installed apps snapshot.${sourceSummary ? ` ${sourceSummary}` : ''}`);
return;
}
const nextSignature = installedAppsSignature(nextInstalledApps);
if (nextSignature === currentInstalledAppsSignature) return;
currentInstalledApps = nextInstalledApps;
currentInstalledAppsSignature = nextSignature;
log('info', `Installed apps snapshot accepted (${currentInstalledApps.apps.length} app(s)).${sourceSummary ? ` ${sourceSummary}` : ''}`);
broadcast('installed_apps', currentInstalledApps);
}
function syncGameStatus(rawGameStatus) {
const nextGameStatus = normalizeGameStatusSnapshot(rawGameStatus);
if (!nextGameStatus) {
log('warn', 'Ignored invalid game status snapshot.');
return;
}
const nextSignature = gameStatusSignature(nextGameStatus);
if (nextSignature === currentGameStatusSignature) return;
currentGameStatus = nextGameStatus;
currentGameStatusSignature = nextSignature;
log('info', `Game status snapshot accepted (${currentGameStatus.session.state}/${currentGameStatus.session.event}).`);
broadcast('game_status', currentGameStatus);
}
function buildHealthPayload() {
const installedAppsDebug = buildInstalledAppsDebugPayload(currentInstalledApps);
const serverInfo = getServerInfo();
return {
ok: serverInfo.listening,
trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || null,
gameSessionState: currentGameStatus?.session?.state || 'idle',
gameSessionEvent: currentGameStatus?.session?.event || 'snapshot',
runningTrainerId: currentGameStatus?.trainer?.trainerId || null,
installedAppsCount: installedAppsDebug.counts.myGamesEntries,
installedRawAppsCount: installedAppsDebug.counts.rawInstallEntries,
installedTitlesCount: installedAppsDebug.counts.groupedTitles,
installedUniqueTitleIdsCount: installedAppsDebug.counts.uniqueTitleIds,
installedUniqueGameIdsCount: installedAppsDebug.counts.uniqueGameIds,
installedAppsApiPath: serverInfo.installedAppsApiPath,
remoteUrl: serverInfo.remoteUrl,
advertisedUrls: serverInfo.advertisedUrls,
};
}
function clear() {
currentSnapshot = null;
currentInstalledApps = null;
currentInstalledAppsSignature = null;
currentGameStatus = null;
currentGameStatusSignature = null;
}
return {
get snapshot() { return currentSnapshot; },
buildHealthPayload,
buildInstalledAppsDebugPayload: () => buildInstalledAppsDebugPayload(currentInstalledApps),
clear,
sendSnapshot,
sync,
syncGameStatus,
syncInstalledApps,
valueChanged,
};
}
module.exports = {
createBridgeState,
};
+47
View File
@@ -0,0 +1,47 @@
const KNOWN_CHEAT_TYPES = new Set(['slider', 'number', 'toggle', 'button', 'selection', 'scalar', 'incremental']);
const WEB_CONTRACT = require('../../protocol/web-contract.json');
const WS_OPCODE = Object.freeze({
TEXT: 1,
BINARY: 2,
CLOSE: 8,
PING: 9,
PONG: 10,
});
const IPC_CHANNEL = Object.freeze({
BIND_HANDLER: 'wand-remote-set-handler-bind',
COMMAND_REQUEST: 'wand-remote-command',
COMMAND_RESPONSE: 'wand-remote-command-response',
GAME_STATUS: 'wand-remote-game-status',
INSTALLED_APPS: 'wand-remote-installed-apps',
REMOTE_URL: 'wand-remote-url',
SET_VALUE: 'wand-remote-set-value',
TRAINER_SNAPSHOT: 'wand-remote-sync',
VALUE_CHANGED: 'wand-remote-value-changed',
});
module.exports = {
BRIDGE_LOG_FILE_NAME: 'wand-remote-bridge.log',
BRIDGE_PROTOCOL_VERSION: WEB_CONTRACT.protocolVersion,
BRIDGE_SERVER_VERSION: WEB_CONTRACT.serverVersion,
DEFAULT_REMOTE_HOST: WEB_CONTRACT.defaultRemoteHost,
DEFAULT_REMOTE_PORT: WEB_CONTRACT.defaultRemotePort,
IPC_CHANNEL,
KNOWN_CHEAT_TYPES,
PORT_SCAN_RANGE: WEB_CONTRACT.portScanRange,
REMOTE_ASSETS_PREFIX: WEB_CONTRACT.assetsPath,
REMOTE_BASE_PATH: WEB_CONTRACT.basePath,
REMOTE_COMMAND_REQUEST_CHANNEL: IPC_CHANNEL.COMMAND_REQUEST,
REMOTE_COMMAND_RESPONSE_CHANNEL: IPC_CHANNEL.COMMAND_RESPONSE,
REMOTE_COMMAND_RESPONSE_TIMEOUT_MS: 15000,
REMOTE_GAME_STATUS_CHANNEL: IPC_CHANNEL.GAME_STATUS,
REMOTE_HEALTH_PATH: WEB_CONTRACT.healthPath,
REMOTE_INSTALLED_APPS_API_PATH: WEB_CONTRACT.installedAppsPath,
REMOTE_INSTALLED_APPS_CHANNEL: IPC_CHANNEL.INSTALLED_APPS,
REMOTE_WS_PATH: WEB_CONTRACT.webSocketPath,
RENDERER_INJECTION_DELAYS_MS: Object.freeze([500, 2000]),
RENDERER_SCRIPT_API_VERSION: 1,
RENDERER_SCRIPTS_DIR: 'renderer-scripts',
WS_OPCODE,
};
+32
View File
@@ -0,0 +1,32 @@
const { createBridgeRuntime: createRuntime, ensureBridge: ensureRuntime } = require('./runtime');
const { installWandRuntime: installRuntime } = require('./wand/runtime');
import type { BridgeOptions, ElectronPort } from './types';
function withDefaultPanelRoot(options: BridgeOptions = {}): BridgeOptions {
if (options.panelRoot) {
return options;
}
return {
...options,
panelRoot: __dirname,
};
}
function createBridgeRuntime(options: BridgeOptions = {}) {
return createRuntime(withDefaultPanelRoot(options));
}
function ensureBridge(options: BridgeOptions = {}) {
return ensureRuntime(withDefaultPanelRoot(options));
}
function installWandRuntime(electron: ElectronPort, options: BridgeOptions = {}) {
return installRuntime(electron, withDefaultPanelRoot(options));
}
module.exports = {
createBridgeRuntime,
ensureBridge,
installWandRuntime,
};
+36
View File
@@ -0,0 +1,36 @@
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { BRIDGE_LOG_FILE_NAME } = require('./constants');
import type { BridgeOptions } from './types';
function writeLogLine(logFile, level, message, error) {
const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'info';
const tag = `[wand-remote-bridge] ${message}`;
try {
console[method](tag, error || '');
} catch { }
try {
const detail = error ? ` :: ${error && error.stack ? error.stack : String(error)}` : '';
fs.appendFileSync(logFile, `[${new Date().toISOString()}] [${level}] ${message}${detail}\n`);
} catch { }
}
function createBridgeLogger(options: BridgeOptions = {}) {
const logFile = options.logFile || path.join(os.tmpdir(), BRIDGE_LOG_FILE_NAME);
const log = (level, message, error) => writeLogLine(logFile, level, message, error);
log.file = logFile;
return log;
}
function writeInstallLog(level, message, error) {
writeLogLine(path.join(os.tmpdir(), BRIDGE_LOG_FILE_NAME), level, message, error);
}
module.exports = {
createBridgeLogger,
writeInstallLog,
};
@@ -0,0 +1,32 @@
const { isRecord, safeString, toStringId } = require('../utils');
function normalizeRemoteCommandAction(value) {
return value === 'launch' || value === 'stop' ? value : null;
}
function normalizeRemoteCommandResult(rawResult, fallback) {
const action = normalizeRemoteCommandAction(isRecord(rawResult) ? rawResult.action : null) || fallback.action;
const gameId = isRecord(rawResult) ? toStringId(rawResult.gameId) || fallback.gameId || null : fallback.gameId || null;
const titleId = isRecord(rawResult) ? toStringId(rawResult.titleId) || fallback.titleId || null : fallback.titleId || null;
const ok = rawResult === true || Boolean(isRecord(rawResult) && rawResult.ok === true);
const payload = { ok, action, gameId, titleId };
if (ok) return payload;
if (!isRecord(rawResult) || !isRecord(rawResult.error)) {
return {
...payload,
error: { code: 'command_rejected', message: 'The renderer rejected the remote command.' },
};
}
return {
...payload,
error: {
code: safeString(rawResult.error.code, 'command_rejected'),
message: safeString(rawResult.error.message, 'The renderer rejected the remote command.'),
},
};
}
module.exports = {
normalizeRemoteCommandAction,
normalizeRemoteCommandResult,
};
@@ -0,0 +1,55 @@
const { isRecord, safeString, toStringId } = require('../utils');
function normalizeGameStatusSnapshot(rawSnapshot) {
if (!isRecord(rawSnapshot)) return null;
const rawSession = isRecord(rawSnapshot.session) ? rawSnapshot.session : {};
const rawTrainer = isRecord(rawSnapshot.trainer) ? rawSnapshot.trainer : {};
return {
instanceId: safeString(rawSnapshot.instanceId, 'wand-game-status'),
updatedAt: typeof rawSnapshot.updatedAt === 'string' ? rawSnapshot.updatedAt : new Date().toISOString(),
session: {
state: rawSession.state === 'running' ? 'running' : 'idle',
event: safeString(rawSession.event, 'snapshot'),
processId: typeof rawSession.processId === 'number' ? rawSession.processId : null,
gameId: toStringId(rawSession.gameId),
titleId: toStringId(rawSession.titleId),
titleName: typeof rawSession.titleName === 'string' ? rawSession.titleName : null,
sessionDurationSeconds: typeof rawSession.sessionDurationSeconds === 'number' ? rawSession.sessionDurationSeconds : null,
startedAt: typeof rawSession.startedAt === 'string' ? rawSession.startedAt : null,
endedAt: typeof rawSession.endedAt === 'string' ? rawSession.endedAt : null,
},
trainer: {
state: rawTrainer.state === 'running' ? 'running' : 'idle',
event: safeString(rawTrainer.event, 'snapshot'),
trainerId: toStringId(rawTrainer.trainerId),
displayName: typeof rawTrainer.displayName === 'string' ? rawTrainer.displayName : null,
gameId: toStringId(rawTrainer.gameId),
titleId: toStringId(rawTrainer.titleId),
},
};
}
function gameStatusSignature(snapshot) {
return [
snapshot.session.state,
snapshot.session.event,
snapshot.session.processId || '',
snapshot.session.gameId || '',
snapshot.session.titleId || '',
snapshot.session.titleName || '',
snapshot.session.sessionDurationSeconds || '',
snapshot.session.startedAt || '',
snapshot.session.endedAt || '',
snapshot.trainer.state,
snapshot.trainer.event,
snapshot.trainer.trainerId || '',
snapshot.trainer.displayName || '',
snapshot.trainer.gameId || '',
snapshot.trainer.titleId || '',
].join('|');
}
module.exports = {
gameStatusSignature,
normalizeGameStatusSnapshot,
};
+398
View File
@@ -0,0 +1,398 @@
const { KNOWN_CHEAT_TYPES } = require('../constants');
const { cloneValue, firstString, isRecord, safeString, toStringId } = require('../utils');
const { normalizeRemoteCommandAction, normalizeRemoteCommandResult } = require('./command-results');
const { gameStatusSignature, normalizeGameStatusSnapshot } = require('./game-status');
const { normalizeTrainerValue } = require('./trainer');
function normalizeOption(option) {
if (typeof option === 'string' || typeof option === 'number') {
return {
label: String(option),
value: option,
};
}
if (!isRecord(option)) {
return null;
}
const value = option.value;
if (typeof value !== 'string' && typeof value !== 'number') {
return null;
}
return {
label: safeString(option.label, String(value)),
value,
};
}
function normalizeArgs(args) {
if (!isRecord(args)) {
return {};
}
const next: Record<string, unknown> = {};
if (typeof args.min === 'number') next.min = args.min;
if (typeof args.max === 'number') next.max = args.max;
if (typeof args.step === 'number') next.step = args.step;
if (typeof args.postfix === 'string') next.postfix = args.postfix;
if (typeof args.default === 'string' || typeof args.default === 'number' || typeof args.default === 'boolean') {
next.default = args.default;
}
if (Array.isArray(args.options)) {
next.options = args.options.map(normalizeOption).filter(Boolean);
}
if (typeof args.button === 'string' || typeof args.button === 'boolean') {
next.button = args.button;
}
return next;
}
function normalizeCheat(cheat, index) {
if (!isRecord(cheat)) {
return null;
}
const target = safeString(cheat.target);
const type = safeString(cheat.type);
if (!target || !KNOWN_CHEAT_TYPES.has(type)) {
return null;
}
const normalized: Record<string, unknown> = {
uuid: safeString(cheat.uuid, `${target}-${index}`),
target,
type,
name: safeString(cheat.name, target),
description: typeof cheat.description === 'string' ? cheat.description : null,
instructions: typeof cheat.instructions === 'string' ? cheat.instructions : null,
category: safeString(cheat.category, 'general'),
parent: typeof cheat.parent === 'string' ? cheat.parent : null,
args: normalizeArgs(cheat.args),
};
if (typeof cheat.flags === 'number') {
normalized.flags = cheat.flags;
}
if (Array.isArray(cheat.hotkeys)) {
normalized.hotkeys = cheat.hotkeys.filter(Array.isArray).map((group) => group.map((item) => String(item)));
}
return normalized;
}
function normalizeImageUrl(...values) {
const value = firstString(...values);
return value || null;
}
function getRawInstalledApps(rawSnapshot) {
if (Array.isArray(rawSnapshot)) {
return rawSnapshot;
}
if (isRecord(rawSnapshot) && Array.isArray(rawSnapshot.apps)) {
return rawSnapshot.apps;
}
if (isRecord(rawSnapshot) && Array.isArray(rawSnapshot.installedApps)) {
return rawSnapshot.installedApps;
}
return null;
}
function normalizeInstalledApp(app) {
if (!isRecord(app)) {
return null;
}
const platform = safeString(app.platform);
const sku = safeString(app.sku);
if (!platform || !sku) {
return null;
}
const location = typeof app.location === 'string' ? app.location : '';
const alternateLocations = Array.isArray(app.alternateLocations)
? app.alternateLocations.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim())
: [];
return {
platform,
sku,
correlationId: `${platform}:${sku}`,
displayName: firstString(
app.displayName,
app.titleName,
app.gameName,
app.name,
location.replaceAll('\\', '/').split('/').filter(Boolean).pop() || '',
`${platform}:${sku}`
),
gameId: toStringId(app.gameId),
titleId: toStringId(app.titleId),
location,
alternateLocations,
imageUrl: normalizeImageUrl(app.imageUrl, app.iconUrl, app.coverUrl, app.thumbnailUrl, app.logoUrl, app.headerImageUrl),
platformLastPlayedTimestamp: typeof app.platformLastPlayedTimestamp === 'number' ? app.platformLastPlayedTimestamp : null,
platformTotalPlaytimeMinutes: typeof app.platformTotalPlaytimeMinutes === 'number' ? app.platformTotalPlaytimeMinutes : null,
};
}
function normalizeInstalledAppsSnapshot(rawSnapshot) {
const rawApps = getRawInstalledApps(rawSnapshot);
if (!rawApps) {
return null;
}
const apps = rawApps.map(normalizeInstalledApp).filter(Boolean).sort(compareInstalledApps);
const diagnostics = isRecord(rawSnapshot) && isRecord(rawSnapshot.diagnostics)
? cloneValue(rawSnapshot.diagnostics)
: null;
return {
instanceId: isRecord(rawSnapshot) ? safeString(rawSnapshot.instanceId, 'wand-installed-apps') : 'wand-installed-apps',
updatedAt: isRecord(rawSnapshot) && typeof rawSnapshot.updatedAt === 'string' ? rawSnapshot.updatedAt : new Date().toISOString(),
apps,
diagnostics,
};
}
function summarizeInstalledAppsSource(rawSnapshot) {
if (!isRecord(rawSnapshot) || !isRecord(rawSnapshot.diagnostics)) {
return '';
}
const parts: string[] = [];
for (const key of ['rawInstalledApps', 'catalogGames', 'catalogTitles']) {
const value = rawSnapshot.diagnostics[key];
if (typeof value === 'number') {
parts.push(`${key}=${value}`);
}
}
return parts.join(', ');
}
function installedAppsSignature(snapshot) {
return snapshot.apps
.map((app) => [
app.platform,
app.sku,
app.displayName,
app.gameId || '',
app.titleId || '',
app.location,
app.imageUrl || '',
app.platformLastPlayedTimestamp || '',
app.platformTotalPlaytimeMinutes || '',
].join('|'))
.join('\n');
}
function buildInstalledAppsDebugPayload(snapshot) {
if (!snapshot) {
return {
ok: false,
instanceId: null,
updatedAt: null,
counts: {
myGamesEntries: 0,
rawInstallEntries: 0,
groupedTitles: 0,
uniqueTitleIds: 0,
uniqueGameIds: 0,
},
diagnostics: null,
byPlatform: {},
titles: [],
apps: [],
};
}
const diagnostics = isRecord(snapshot.diagnostics) ? snapshot.diagnostics : null;
const byPlatform = {};
const uniqueTitleIds = new Set();
const uniqueGameIds = new Set();
const titleGroups = new Map();
for (const app of snapshot.apps) {
byPlatform[app.platform] = (byPlatform[app.platform] || 0) + 1;
if (app.titleId) {
uniqueTitleIds.add(app.titleId);
}
if (app.gameId) {
uniqueGameIds.add(app.gameId);
}
const groupKey = resolveInstalledAppGroupKey(app);
let group = titleGroups.get(groupKey);
if (!group) {
group = {
key: groupKey,
titleId: app.titleId,
displayName: app.displayName,
gameIds: new Set(),
platforms: new Set(),
apps: [],
};
titleGroups.set(groupKey, group);
}
if (app.gameId) {
group.gameIds.add(app.gameId);
}
group.platforms.add(app.platform);
group.apps.push(app);
}
const titles = Array.from(titleGroups.values())
.map((group) => ({
key: group.key,
titleId: group.titleId,
displayName: group.displayName,
gameIds: Array.from(group.gameIds).sort(),
platforms: Array.from(group.platforms).sort(),
appEntries: group.apps.length,
apps: group.apps,
}))
.sort((left, right) => left.displayName.localeCompare(right.displayName));
return {
ok: true,
instanceId: snapshot.instanceId,
updatedAt: snapshot.updatedAt,
counts: {
myGamesEntries: snapshot.apps.length,
rawInstallEntries: typeof diagnostics?.rawInstalledApps === 'number' ? diagnostics.rawInstalledApps : snapshot.apps.length,
groupedTitles: titles.length,
uniqueTitleIds: uniqueTitleIds.size,
uniqueGameIds: uniqueGameIds.size,
},
diagnostics,
byPlatform,
titles,
apps: snapshot.apps,
};
}
function normalizeSnapshot(rawSnapshot) {
if (!isRecord(rawSnapshot) || !isRecord(rawSnapshot.metadata) || !isRecord(rawSnapshot.metadata.info)) {
return null;
}
const info = rawSnapshot.metadata.info;
const blueprint = isRecord(info.blueprint) ? info.blueprint : {};
const rawCheats = Array.isArray(blueprint.cheats) ? blueprint.cheats : [];
const cheats = rawCheats.map(normalizeCheat).filter(Boolean);
const categories = Array.from(new Set(cheats.map((entry) => entry.category)));
const trainerId = safeString(rawSnapshot.trainerId || rawSnapshot.trainerInfo?.trainerId);
const displayName = firstString(
rawSnapshot.trainerInfo?.displayName,
rawSnapshot.trainerInfo?.gameName,
rawSnapshot.trainerInfo?.titleName,
rawSnapshot.trainerInfo?.title,
rawSnapshot.trainerInfo?.name,
info.displayName,
info.gameName,
info.titleName,
info.title,
info.name,
info.game?.displayName,
info.game?.name,
info.game?.title
);
if (!trainerId) {
return null;
}
const trainerMeta = {
session: {
instanceId: safeString(rawSnapshot.instanceId, 'wand-session'),
accessToken: safeString(rawSnapshot.accessToken),
},
trainer: {
trainerId,
gameId: safeString(rawSnapshot.trainerInfo?.gameId || info.gameId),
displayName: displayName || safeString(rawSnapshot.trainerInfo?.gameId || info.gameId, trainerId),
titleId: typeof info.titleId === 'string' ? info.titleId : null,
gameVersion: typeof rawSnapshot.gameVersion === 'string' ? rawSnapshot.gameVersion : null,
trainerLoading: rawSnapshot.trainerLoading === true,
gameInstalled: rawSnapshot.gameInstalled !== false,
needsCompatibilityWarning: rawSnapshot.needsCompatibilityWarning === true,
language: safeString(rawSnapshot.language, 'en-US'),
themeId: safeString(rawSnapshot.themeId, 'default'),
isTimeLimitExpired: rawSnapshot.isTimeLimitExpired === true,
notesReadHash: typeof rawSnapshot.notesReadHash === 'string' ? rawSnapshot.notesReadHash : null,
},
schema: {
categories,
cheats,
},
};
const trainerValues = {
trainerId,
values: isRecord(rawSnapshot.values) ? cloneValue(rawSnapshot.values) : {},
};
for (const cheat of cheats) {
if (cheat.target in trainerValues.values) {
trainerValues.values[cheat.target] = normalizeTrainerValue({ trainerMeta }, cheat.target, trainerValues.values[cheat.target]);
}
}
return {
trainerMeta,
trainerValues,
};
}
function compareInstalledApps(left, right) {
const displayNameDiff = left.displayName.localeCompare(right.displayName);
if (displayNameDiff !== 0) {
return displayNameDiff;
}
const platformDiff = left.platform.localeCompare(right.platform);
if (platformDiff !== 0) {
return platformDiff;
}
return left.sku.localeCompare(right.sku);
}
function resolveInstalledAppGroupKey(app) {
if (app.titleId) {
return `title:${app.titleId}`;
}
if (app.gameId) {
return `game:${app.gameId}`;
}
return `app:${app.correlationId}`;
}
module.exports = {
buildInstalledAppsDebugPayload,
gameStatusSignature,
installedAppsSignature,
normalizeGameStatusSnapshot,
normalizeInstalledAppsSnapshot,
normalizeRemoteCommandAction,
normalizeRemoteCommandResult,
normalizeSnapshot,
normalizeTrainerValue,
summarizeInstalledAppsSource,
};
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { normalizeTrainerValue } from './trainer';
describe('trainer normalization', () => {
it('normalizes toggle values before they reach clients or Wand', () => {
const snapshot = {
trainerMeta: {
schema: { cheats: [{ target: 'god', type: 'toggle' }] },
},
};
expect(normalizeTrainerValue(snapshot, 'god', 1)).toBe(true);
expect(normalizeTrainerValue(snapshot, 'god', 0)).toBe(false);
expect(normalizeTrainerValue(snapshot, 'speed', 2)).toBe(2);
});
});
@@ -0,0 +1,10 @@
export function normalizeTrainerValue(snapshot, target, value) {
const cheat = snapshot?.trainerMeta?.schema?.cheats?.find((entry) => entry.target === target);
return cheat?.type === 'toggle' ? Boolean(value) : cloneValue(value);
}
function cloneValue(value) {
if (Array.isArray(value)) return value.map(cloneValue);
if (typeof value !== 'object' || value === null) return value;
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, cloneValue(entry)]));
}
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest';
import { validateClientMessage, validateSetValueTarget } from './protocol-router';
const snapshot = {
trainerMeta: {
trainer: { trainerId: 'active' },
schema: { cheats: [{ target: 'god', type: 'toggle' }] },
},
trainerValues: { values: { god: false } },
};
describe('bridge protocol router', () => {
it('requires a compatible hello before commands', () => {
const command = {
type: 'set_value',
version: 1,
requestId: 'set',
payload: { trainerId: 'active', target: 'god', value: true },
};
expect(validateClientMessage(command, false)).toMatchObject({
ok: false,
error: { code: 'handshake_required' },
});
expect(validateClientMessage({ ...command, version: 2 }, true)).toMatchObject({
ok: false,
error: { code: 'protocol_mismatch' },
});
});
it('validates trainer and target while normalizing toggle values', () => {
expect(validateSetValueTarget({
payload: { trainerId: 'other', target: 'god', value: 1 },
}, snapshot)).toMatchObject({ ok: false, error: { code: 'trainer_mismatch' } });
expect(validateSetValueTarget({
payload: { trainerId: 'active', target: 'god', value: 1 },
}, snapshot)).toMatchObject({ ok: true, value: true });
});
});
+78
View File
@@ -0,0 +1,78 @@
import webContract from '../../protocol/web-contract.json';
const BRIDGE_PROTOCOL_VERSION = webContract.protocolVersion;
export function validateClientMessage(message, handshaken) {
if (!isRecord(message) || typeof message.type !== 'string' || !isRecord(message.payload)) {
return invalid('invalid_message', 'Expected a protocol envelope with an object payload.');
}
if (message.version !== BRIDGE_PROTOCOL_VERSION) {
return invalid('protocol_mismatch', `Unsupported protocol version ${String(message.version)}.`);
}
if (message.requestId !== null && typeof message.requestId !== 'string') {
return invalid('invalid_request_id', 'requestId must be a string or null.');
}
if (message.type === 'hello') {
if (message.payload.client !== 'mobile-web' || typeof message.payload.clientVersion !== 'string' || !isRecord(message.payload.capabilities)) {
return invalid('invalid_hello', 'The hello payload is incomplete.');
}
return { ok: true };
}
if (!handshaken) {
return invalid('handshake_required', 'Send a compatible hello message before commands.');
}
if (message.type === 'set_value') {
if (!safeString(message.payload.trainerId) || !safeString(message.payload.target) || !('value' in message.payload)) {
return invalid('invalid_set_value', 'trainerId, target and value are required.');
}
return { ok: true };
}
if (message.type === 'remote_command') {
if (message.payload.action !== 'launch' && message.payload.action !== 'stop') {
return invalid('invalid_command', 'Unknown remote command.');
}
return { ok: true };
}
return invalid('unknown_message', 'Unknown protocol message type.');
}
export function validateSetValueTarget(message, snapshot) {
const target = safeString(message.payload?.target);
const requestedTrainerId = safeString(message.payload?.trainerId);
const activeTrainerId = snapshot?.trainerMeta?.trainer?.trainerId || '';
if (!snapshot || requestedTrainerId !== activeTrainerId) {
return invalid('trainer_mismatch', 'The requested trainer is not active.');
}
const cheat = snapshot.trainerMeta.schema.cheats.find((entry) => entry.target === target);
if (!target || !cheat || !(target in snapshot.trainerValues.values)) {
return invalid('invalid_target', 'Unknown cheat target.');
}
return {
ok: true,
trainerId: activeTrainerId,
target,
cheat,
value: cheat.type === 'toggle' ? Boolean(message.payload.value) : message.payload.value,
};
}
function invalid(code, message) {
return { ok: false, error: { code, message } };
}
function isRecord(value) {
return typeof value === 'object' && value !== null;
}
function safeString(value) {
return typeof value === 'string' && value.length > 0 ? value : '';
}
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest';
import {
findSteamAppId,
getSteamClientIconUrl,
normalizeImageUrl,
} from '../scripts/default/installed-apps-sync/artwork.js';
describe('installed-apps renderer script models', () => {
it('normalizes captured artwork shapes without a Wand runtime', () => {
expect(normalizeImageUrl({ cover: { imageUrl: '//cdn.example/game.webp' } }))
.toBe('https://cdn.example/game.webp');
expect(normalizeImageUrl('file:///local/image.png')).toBeNull();
});
it('finds nested Steam metadata and builds the Wand client icon URL', () => {
const fixture = {
game: {
metadata: {
steam: {
appId: 1245620,
},
},
},
};
expect(findSteamAppId(fixture)).toBe('1245620');
expect(getSteamClientIconUrl(findSteamAppId(fixture)))
.toBe('https://api-cdn.wemod.com/steam_community/1245620/client_icon/96.webp');
});
});
@@ -0,0 +1,91 @@
import { createServer } from 'node:net';
import { describe, expect, it } from 'vitest';
import { WebSocket as NodeWebSocket } from 'ws';
describe('production bridge runtime', () => {
it('preserves the public API and sends cached snapshots after hello', async () => {
const bridge = require('../../dist/bridge.cjs');
expect(Object.keys(bridge).sort()).toEqual(['createBridgeRuntime', 'ensureBridge', 'installWandRuntime']);
const port = await getFreePort();
const runtime = bridge.createBridgeRuntime({ host: '127.0.0.1', port, maxPort: port });
runtime.sync(rawTrainerSnapshot());
try {
await waitUntil(() => runtime.listening);
const messages = await connectAndCollect(port, 3);
expect(messages.map((message) => message.type)).toEqual(['hello_ack', 'trainer_meta', 'trainer_values']);
expect(messages[2].payload.values.god).toBe(true);
} finally {
runtime.close();
}
});
});
async function getFreePort(): Promise<number> {
return await new Promise((resolve, reject) => {
const server = createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
const port = typeof address === 'object' && address ? address.port : 0;
server.close((error) => error ? reject(error) : resolve(port));
});
});
}
async function connectAndCollect(port: number, count: number): Promise<any[]> {
return await new Promise((resolve, reject) => {
const messages: any[] = [];
const socket = new NodeWebSocket(`ws://127.0.0.1:${port}/remote/ws`);
socket.once('error', reject);
socket.once('open', () => socket.send(JSON.stringify({
type: 'hello',
version: 1,
requestId: 'hello',
payload: {
client: 'mobile-web',
clientVersion: 'test',
capabilities: { supportsDeltaValues: true, supportsTrainerSwitch: true },
},
})));
socket.on('message', (raw) => {
messages.push(JSON.parse(String(raw)));
if (messages.length === count) {
socket.close();
resolve(messages);
}
});
});
}
async function waitUntil(predicate: () => boolean): Promise<void> {
const deadline = Date.now() + 3000;
while (!predicate()) {
if (Date.now() > deadline) throw new Error('Bridge did not start listening.');
await new Promise((resolve) => setTimeout(resolve, 10));
}
}
function rawTrainerSnapshot() {
return {
instanceId: 'instance',
trainerId: 'trainer',
trainerInfo: { gameId: 'game', displayName: 'Game' },
metadata: {
info: {
blueprint: {
cheats: [{
uuid: 'god',
target: 'god',
type: 'toggle',
name: 'God mode',
category: 'player',
args: {},
}],
},
},
},
values: { god: 1 },
};
}
+19
View File
@@ -0,0 +1,19 @@
const { createBridgeServer } = require('./server');
import type { BridgeOptions } from './types';
function createBridgeRuntime(options: BridgeOptions = {}) {
return createBridgeServer(options);
}
function ensureBridge(options: BridgeOptions = {}) {
if (!globalThis.__wandRemoteBridgeRuntime) {
globalThis.__wandRemoteBridgeRuntime = createBridgeRuntime(options);
}
return globalThis.__wandRemoteBridgeRuntime;
}
module.exports = {
createBridgeRuntime,
ensureBridge,
};
+166
View File
@@ -0,0 +1,166 @@
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { REMOTE_BASE_PATH } = require('./constants');
const IPV4_OCTET_PATTERN = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
const PHYSICAL_INTERFACE_NAME_PATTERN = /(?:ethernet|wi-?fi|wireless|wlan|lan|local area)/i;
const VIRTUAL_INTERFACE_NAME_PATTERN = /(?:bluetooth|container|docker|hamachi|hyper-v|loopback|npcap|pseudo|tap|tailscale|teredo|tunnel|tun|virtual|vmware|vbox|virtualbox|vpn|wireguard|wsl|zerotier)/i;
const VIRTUAL_MAC_PREFIXES = new Set([
'00:05:69',
'00:0c:29',
'00:15:5d',
'00:16:3e',
'00:1c:14',
'00:50:56',
'08:00:27',
'52:54:00',
]);
function contentTypeFor(filePath) {
const extension = path.extname(filePath).toLowerCase();
switch (extension) {
case '.html':
return 'text/html; charset=utf-8';
case '.js':
case '.cjs':
return 'application/javascript; charset=utf-8';
case '.css':
return 'text/css; charset=utf-8';
case '.json':
return 'application/json; charset=utf-8';
case '.svg':
return 'image/svg+xml';
default:
return 'application/octet-stream';
}
}
function getAdvertisedUrls(port) {
const candidates: any[] = [];
const interfaces = os.networkInterfaces();
let index = 0;
for (const [name, entries] of Object.entries(interfaces) as [string, any[] | undefined][]) {
if (!entries) {
continue;
}
for (const entry of entries) {
if (!isUsableIpv4Entry(entry)) {
continue;
}
candidates.push({
index,
score: scoreIpv4Entry(name, entry),
url: `http://${entry.address}:${port}${REMOTE_BASE_PATH}`,
});
index += 1;
}
}
const urls = candidates
.sort((left, right) => right.score - left.score || left.index - right.index)
.map((candidate) => candidate.url);
urls.unshift(`http://localhost:${port}${REMOTE_BASE_PATH}`);
return Array.from(new Set(urls));
}
function isUsableIpv4Entry(entry) {
return Boolean(entry && !entry.internal && isIpv4Family(entry.family) && parseIpv4(entry.address));
}
function isIpv4Family(family) {
return family === 'IPv4' || family === 4;
}
function scoreIpv4Entry(name, entry) {
const octets = parseIpv4(entry.address) as number[];
let score = 0;
if (isPrivateIpv4(octets)) {
score += 1000;
}
if (octets[0] === 192 && octets[1] === 168) {
score += 40;
} else if (octets[0] === 10) {
score += 30;
} else if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
score += 20;
}
if (PHYSICAL_INTERFACE_NAME_PATTERN.test(name)) {
score += 120;
}
if (VIRTUAL_INTERFACE_NAME_PATTERN.test(name)) {
score -= 700;
}
if (isVirtualMac(entry.mac)) {
score -= 450;
}
if (isLinkLocalIpv4(octets)) {
score -= 1200;
}
if (octets[3] === 1 || octets[3] === 254) {
score -= 25;
}
return score;
}
function parseIpv4(address): number[] | null {
if (typeof address !== 'string') {
return null;
}
const match = address.match(IPV4_OCTET_PATTERN);
if (!match) {
return null;
}
const octets = match.slice(1).map((part) => Number(part));
return octets.every((octet) => Number.isInteger(octet) && octet >= 0 && octet <= 255) ? octets : null;
}
function isPrivateIpv4(octets) {
return octets[0] === 10 || (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) || (octets[0] === 192 && octets[1] === 168);
}
function isLinkLocalIpv4(octets) {
return octets[0] === 169 && octets[1] === 254;
}
function isVirtualMac(mac) {
if (typeof mac !== 'string') {
return false;
}
return VIRTUAL_MAC_PREFIXES.has(mac.toLowerCase().slice(0, 8));
}
function serveFile(response, filePath) {
try {
const content = fs.readFileSync(filePath);
response.writeHead(200, {
'Content-Type': contentTypeFor(filePath),
'Cache-Control': 'no-store',
});
response.end(content);
} catch {
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
}
}
module.exports = {
getAdvertisedUrls,
serveFile,
};
+411
View File
@@ -0,0 +1,411 @@
const http = require('node:http');
const path = require('node:path');
const {
BRIDGE_PROTOCOL_VERSION,
BRIDGE_SERVER_VERSION,
DEFAULT_REMOTE_HOST,
DEFAULT_REMOTE_PORT,
PORT_SCAN_RANGE,
REMOTE_ASSETS_PREFIX,
REMOTE_BASE_PATH,
REMOTE_HEALTH_PATH,
REMOTE_INSTALLED_APPS_API_PATH,
REMOTE_WS_PATH,
WS_OPCODE,
} = require('./constants');
const { createBridgeLogger } = require('./logger');
const {
normalizeRemoteCommandAction,
normalizeRemoteCommandResult,
} = require('./normalizers');
const { createBridgeState } = require('./bridge-state');
const { validateClientMessage, validateSetValueTarget } = require('./protocol-router');
const { getAdvertisedUrls, serveFile } = require('./server-files');
const { cloneValue, isValidPort, safeString } = require('./utils');
const { closeClient, createAcceptKey, makeFrame, parseFrame, sendJson } = require('./websocket-codec');
import type { BridgeOptions } from './types';
function createBridgeServer(options: BridgeOptions = {}) {
const preferredPort = Number(options.port || process.env.WAND_REMOTE_PORT || DEFAULT_REMOTE_PORT);
let port = isValidPort(preferredPort) ? preferredPort : DEFAULT_REMOTE_PORT;
const maxPort = Number(options.maxPort || process.env.WAND_REMOTE_MAX_PORT || port + PORT_SCAN_RANGE);
const host = options.host || process.env.WAND_REMOTE_HOST || DEFAULT_REMOTE_HOST;
const panelRoot = options.panelRoot || path.dirname(__dirname);
const clients = new Set<any>();
const log = createBridgeLogger(options);
let advertisedUrls: string[] = [];
let setValueHandler: any = null;
let commandHandler: any = null;
let listening = false;
const bridgeState = createBridgeState({
clients,
log,
getServerInfo: () => ({
advertisedUrls,
installedAppsApiPath: REMOTE_INSTALLED_APPS_API_PATH,
listening,
remoteUrl: globalThis.__wandRemoteBridgeUrl,
}),
});
function setAdvertisedPort(nextPort) {
port = nextPort;
advertisedUrls = getAdvertisedUrls(port);
globalThis.__wandRemoteBridgeUrl = advertisedUrls.find((entry) => !entry.includes('localhost')) || advertisedUrls[0];
}
function setHandler(handler) {
setValueHandler = typeof handler === 'function' ? handler : null;
}
function setCommandHandler(handler) {
commandHandler = typeof handler === 'function' ? handler : null;
}
function handleRequest(request, response) {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (url.pathname === '/' || url.pathname === '') {
response.writeHead(302, { Location: '/remote/' });
response.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH.slice(0, -1)) {
response.writeHead(302, { Location: REMOTE_BASE_PATH });
response.end();
return;
}
if (url.pathname === REMOTE_BASE_PATH) {
serveFile(response, path.join(panelRoot, 'index.html'));
return;
}
if (url.pathname === REMOTE_HEALTH_PATH) {
response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
response.end(JSON.stringify(bridgeState.buildHealthPayload()));
return;
}
if (url.pathname === REMOTE_INSTALLED_APPS_API_PATH) {
response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
response.end(JSON.stringify(bridgeState.buildInstalledAppsDebugPayload(), null, 2));
return;
}
if (url.pathname.startsWith(REMOTE_ASSETS_PREFIX)) {
serveFile(response, path.join(panelRoot, url.pathname.replace(REMOTE_BASE_PATH, '')));
return;
}
response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
response.end('Not found');
}
async function handleRemoteCommandMessage(client, message) {
const action = normalizeRemoteCommandAction(message.payload?.action);
const gameId = typeof message.payload?.gameId === 'string' || typeof message.payload?.gameId === 'number'
? String(message.payload.gameId)
: null;
const titleId = typeof message.payload?.titleId === 'string' || typeof message.payload?.titleId === 'number'
? String(message.payload.titleId)
: null;
if (!action) {
sendJson(client, 'error', {
code: 'invalid_command',
message: 'Unknown remote command.',
}, message.requestId ?? null);
return;
}
const fallback = { action, gameId, titleId };
if (action === 'launch' && !gameId) {
sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
ok: false,
error: {
code: 'invalid_game',
message: 'A game id is required to launch a trainer.',
},
}, fallback), message.requestId ?? null);
return;
}
if (!commandHandler) {
sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
ok: false,
error: {
code: 'bridge_not_ready',
message: 'The local bridge is not ready to execute remote game commands yet.',
},
}, fallback), message.requestId ?? null);
return;
}
try {
const result = await Promise.resolve(commandHandler({ action, gameId, titleId }));
sendJson(client, 'remote_command_result', normalizeRemoteCommandResult(result, fallback), message.requestId ?? null);
} catch (error) {
sendJson(client, 'remote_command_result', normalizeRemoteCommandResult({
ok: false,
error: {
code: 'command_failed',
message: error instanceof Error ? error.message : 'Failed to execute the remote command.',
},
}, fallback), message.requestId ?? null);
}
}
async function handleSetValueMessage(client, message) {
const currentSnapshot = bridgeState.snapshot;
const validation = validateSetValueTarget(message, currentSnapshot);
if (!validation.ok) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot?.trainerMeta?.trainer?.trainerId || '',
target: safeString(message.payload?.target),
error: validation.error,
}, message.requestId ?? null);
return;
}
const { target } = validation;
if (!setValueHandler) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'bridge_not_ready',
message: 'The local bridge is not ready to write trainer values yet.',
},
}, message.requestId ?? null);
return;
}
let result = false;
try {
result = await Promise.resolve(setValueHandler({
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
value: cloneValue(validation.value),
cheatId: typeof message.payload?.cheatId === 'string' ? message.payload.cheatId : undefined,
}));
} catch (error) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'set_failed',
message: error instanceof Error ? error.message : 'Failed to set trainer value.',
},
}, message.requestId ?? null);
return;
}
if (!result) {
sendJson(client, 'set_value_result', {
ok: false,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
error: {
code: 'set_rejected',
message: 'The trainer rejected the requested value.',
},
}, message.requestId ?? null);
return;
}
sendJson(client, 'set_value_result', {
ok: true,
trainerId: currentSnapshot.trainerMeta.trainer.trainerId,
target,
}, message.requestId ?? null);
}
async function handleClientMessage(client, message) {
const validation = validateClientMessage(message, client.handshaken);
if (!validation.ok) {
sendJson(client, 'error', validation.error, message?.requestId ?? null);
return;
}
if (message?.type === 'hello') {
client.handshaken = true;
sendJson(client, 'hello_ack', {
sessionId: `sess_${Date.now()}`,
accepted: true,
serverVersion: BRIDGE_SERVER_VERSION,
protocolVersion: BRIDGE_PROTOCOL_VERSION,
remoteUrl: globalThis.__wandRemoteBridgeUrl,
advertisedUrls,
}, message.requestId ?? null);
bridgeState.sendSnapshot(client);
return;
}
if (message?.type === 'remote_command') {
await handleRemoteCommandMessage(client, message);
return;
}
if (message?.type === 'set_value') {
await handleSetValueMessage(client, message);
}
}
function bindSocket(socket) {
const client = {
socket,
buffer: Buffer.alloc(0),
closed: false,
handshaken: false,
};
clients.add(client);
socket.on('data', async (chunk) => {
try {
client.buffer = Buffer.concat([client.buffer, chunk]);
while (client.buffer.length > 0) {
const frame = parseFrame(client.buffer);
if (!frame) {
return;
}
client.buffer = client.buffer.subarray(frame.bytesConsumed);
if (!frame.fin) {
closeClient(client, 1003, 'Fragmented frames are not supported.');
return;
}
if (frame.opcode === WS_OPCODE.CLOSE) {
closeClient(client, 1000, 'Closing');
return;
}
if (frame.opcode === WS_OPCODE.PING) {
client.socket.write(makeFrame(WS_OPCODE.PONG, frame.payload));
continue;
}
if (frame.opcode !== WS_OPCODE.TEXT) {
continue;
}
await handleClientMessage(client, JSON.parse(frame.payload.toString('utf8')));
}
} catch (error) {
sendJson(client, 'error', {
code: 'invalid_message',
message: error instanceof Error ? error.message : 'Failed to process client message.',
});
}
});
socket.on('close', () => {
client.closed = true;
clients.delete(client);
});
socket.on('end', () => {
client.closed = true;
clients.delete(client);
});
socket.on('error', (error) => {
client.closed = true;
clients.delete(client);
log('warn', 'WebSocket client error.', error);
});
}
function handleUpgrade(request, socket) {
const url = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`);
if (url.pathname !== REMOTE_WS_PATH) {
socket.destroy();
return;
}
const key = request.headers['sec-websocket-key'];
if (typeof key !== 'string' || !key) {
socket.destroy();
return;
}
socket.write([
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${createAcceptKey(key)}`,
'',
'',
].join('\r\n'));
bindSocket(socket);
}
function listen(nextPort) {
setAdvertisedPort(nextPort);
server.listen(port, host);
}
setAdvertisedPort(port);
log('info', `Bridge starting (pid=${process.pid}, panelRoot=${panelRoot}, preferredPort=${port}, host=${host})`);
globalThis.__wandRemoteBridgeLogFile = log.file;
const server = http.createServer(handleRequest);
server.on('upgrade', handleUpgrade);
server.on('error', (error) => {
if (!listening && error && error.code === 'EADDRINUSE' && port < maxPort) {
const nextPort = port + 1;
log('warn', `Port ${port} is busy, trying ${nextPort}.`);
listen(nextPort);
return;
}
log('warn', `Bridge server error on ${host}:${port}.`, error);
});
server.on('listening', () => {
listening = true;
log('info', `Listening on ${globalThis.__wandRemoteBridgeUrl}`);
});
listen(port);
return {
get advertisedUrls() {
return advertisedUrls.slice();
},
get listening() {
return listening;
},
get remoteUrl() {
return globalThis.__wandRemoteBridgeUrl;
},
close() {
for (const client of clients) {
closeClient(client);
}
clients.clear();
bridgeState.clear();
listening = false;
server.close();
},
setCommandHandler,
setHandler,
sync: bridgeState.sync,
syncGameStatus: bridgeState.syncGameStatus,
syncInstalledApps: bridgeState.syncInstalledApps,
valueChanged: bridgeState.valueChanged,
};
}
module.exports = {
createBridgeServer,
};
+24
View File
@@ -0,0 +1,24 @@
export type BridgeOptions = {
host?: string;
logFile?: string;
maxPort?: number | string;
panelRoot?: string;
port?: number | string;
scriptsRoot?: string;
};
export type WebContentsPort = {
executeJavaScript(source: string, userGesture?: boolean): Promise<unknown>;
isDestroyed(): boolean;
on(event: string, listener: () => void): void;
send(channel: string, payload: unknown): void;
};
export type ElectronPort = {
app: {
on(event: 'web-contents-created', listener: (event: unknown, contents: WebContentsPort) => void): void;
};
ipcMain: {
handle(channel: string, handler: (event: { sender?: WebContentsPort }, payload?: unknown) => unknown): void;
};
};
+64
View File
@@ -0,0 +1,64 @@
function isRecord(value) {
return typeof value === 'object' && value !== null;
}
function safeString(value, fallback = '') {
return typeof value === 'string' && value.length ? value : fallback;
}
function firstString(...values) {
for (const value of values) {
if (typeof value !== 'string') {
continue;
}
const trimmed = value.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return '';
}
function cloneValue(value) {
if (Array.isArray(value)) {
return value.map(cloneValue);
}
if (!isRecord(value)) {
return value;
}
const result = {};
for (const [key, entry] of Object.entries(value)) {
result[key] = cloneValue(entry);
}
return result;
}
function isValidPort(value) {
return Number.isFinite(value) && value > 0 && value < 65536;
}
function toStringId(value) {
if (typeof value === 'string' && value.trim()) {
return value.trim();
}
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value);
}
return null;
}
module.exports = {
cloneValue,
firstString,
isRecord,
isValidPort,
safeString,
toStringId,
};
@@ -0,0 +1,91 @@
const fs = require('node:fs');
const path = require('node:path');
const { RENDERER_INJECTION_DELAYS_MS, RENDERER_SCRIPT_API_VERSION, RENDERER_SCRIPTS_DIR } = require('../constants');
const { writeInstallLog } = require('../logger');
import type { BridgeOptions, ElectronPort } from '../types';
function loadRendererScripts(panelRoot, scriptsRoot) {
const root = scriptsRoot || path.join(panelRoot, RENDERER_SCRIPTS_DIR);
if (!fs.existsSync(root)) {
return [];
}
return fs.readdirSync(root)
.filter((name) => name.endsWith('.js'))
.sort((left, right) => left.localeCompare(right))
.map((name) => ({
name,
source: fs.readFileSync(path.join(root, name), 'utf8'),
}));
}
function buildRendererBootstrap(remoteUrl, scripts) {
const header = `
globalThis.__wandRemoteBridgeUrl = ${JSON.stringify(remoteUrl)};
if (!globalThis.WandEnhancer) {
globalThis.WandEnhancer = Object.freeze({
apiVersion: ${RENDERER_SCRIPT_API_VERSION},
remoteUrl: ${JSON.stringify(remoteUrl)},
log: function () { try { console.info.apply(console, ["[wand-enhancer-script]"].concat(Array.from(arguments))); } catch (_) {} },
});
} else {
try { globalThis.__wandRemoteBridgeUrl = ${JSON.stringify(remoteUrl)}; } catch (_) {}
}
console.info("[wand-remote-bridge] Renderer bootstrap (" + ${scripts.length} + " script(s)).");
`;
const body = scripts.map((script) => {
const tag = JSON.stringify(`wand-enhancer-script-${script.name}`);
return `
;(function (WandEnhancer) {
try {
${script.source}
} catch (error) {
try { console.warn("[wand-remote-bridge] Renderer script failed", ${JSON.stringify(script.name)}, error); } catch (_) {}
}
})(globalThis.WandEnhancer);
//# sourceURL=${tag.slice(1, -1)}
`;
}).join('\n');
return `(() => {\n${header}\n${body}\n})();`;
}
function installRendererScripts(electron: ElectronPort, runtime, options: BridgeOptions = {}) {
if (globalThis.__wandRemoteBridgeRendererScriptsInstalled) {
return;
}
globalThis.__wandRemoteBridgeRendererScriptsInstalled = true;
const scripts = loadRendererScripts(options.panelRoot || path.dirname(__dirname), options.scriptsRoot);
if (scripts.length === 0) {
writeInstallLog('info', 'No renderer scripts found.');
return;
}
electron.app.on('web-contents-created', (_event, contents) => {
const inject = () => {
if (!contents || contents.isDestroyed()) {
return;
}
contents.executeJavaScript(buildRendererBootstrap(runtime.remoteUrl, scripts), true)
.catch((error) => writeInstallLog('warn', 'Failed to inject renderer scripts.', error));
};
contents.on('dom-ready', inject);
contents.on('did-finish-load', inject);
for (const delayMs of RENDERER_INJECTION_DELAYS_MS) {
setTimeout(inject, delayMs);
}
});
writeInstallLog('info', `Renderer script injection installed (${scripts.map((script) => script.name).join(', ')}).`);
}
module.exports = {
buildRendererBootstrap,
installRendererScripts,
loadRendererScripts,
};
+194
View File
@@ -0,0 +1,194 @@
const crypto = require('node:crypto');
const path = require('node:path');
const {
IPC_CHANNEL,
REMOTE_COMMAND_REQUEST_CHANNEL,
REMOTE_COMMAND_RESPONSE_CHANNEL,
REMOTE_COMMAND_RESPONSE_TIMEOUT_MS,
REMOTE_GAME_STATUS_CHANNEL,
REMOTE_INSTALLED_APPS_CHANNEL,
} = require('../constants');
const { writeInstallLog } = require('../logger');
const { ensureBridge } = require('../runtime');
const { installRendererScripts } = require('./renderer-scripts');
const { safeString } = require('../utils');
import type { BridgeOptions, ElectronPort, WebContentsPort } from '../types';
// Reads the signed-in WeMod access token from the renderer's localStorage so the
// panel can request localized cheat metadata from the WeMod API.
const WEMOD_ACCESS_TOKEN_SCRIPT =
'JSON.parse(localStorage.getItem("infinity:globalStore") || "{}")?.token?.accessToken ?? null';
function installWandRuntime(electron: ElectronPort, options: BridgeOptions = {}) {
const runtime = ensureBridge(options);
if (!electron || !electron.ipcMain || !electron.app) {
throw new Error('Electron main-process API is required to install Wand runtime hooks.');
}
const boundRenderers: Set<WebContentsPort> = globalThis.__wandRemoteBridgeBoundRenderers || new Set();
const pendingCommandResponses = globalThis.__wandRemoteBridgePendingCommandResponses || new Map();
globalThis.__wandRemoteBridgeBoundRenderers = boundRenderers;
globalThis.__wandRemoteBridgePendingCommandResponses = pendingCommandResponses;
runtime.setHandler((request) => {
let delivered = false;
for (const sender of Array.from(boundRenderers)) {
try {
if (!sender || sender.isDestroyed()) {
boundRenderers.delete(sender);
continue;
}
sender.send(IPC_CHANNEL.SET_VALUE, request);
delivered = true;
} catch (error) {
boundRenderers.delete(sender);
writeInstallLog('warn', 'Failed to forward set_value to renderer.', error);
}
}
return delivered;
});
runtime.setCommandHandler(async (request) => {
for (const sender of Array.from(boundRenderers)) {
try {
if (!sender || sender.isDestroyed()) {
boundRenderers.delete(sender);
continue;
}
return await dispatchRemoteCommandToRenderer(sender, request, pendingCommandResponses);
} catch (error) {
writeInstallLog('warn', 'Failed to execute remote command in renderer.', error);
}
}
return buildRendererBridgeMissingResponse(request);
});
installIpcHandlers(electron, runtime, boundRenderers, pendingCommandResponses);
installRendererScripts(electron, runtime, {
...options,
panelRoot: options.panelRoot || path.dirname(__dirname),
});
writeInstallLog('info', 'Wand runtime hooks installed.');
return runtime;
}
function installIpcHandlers(electron, runtime, boundRenderers, pendingCommandResponses) {
if (globalThis.__wandRemoteBridgeIpcInstalled) {
return;
}
globalThis.__wandRemoteBridgeIpcInstalled = true;
electron.ipcMain.handle(IPC_CHANNEL.TRAINER_SNAPSHOT, (event, snapshot) => {
void syncSnapshotWithAccessToken(runtime, event?.sender, snapshot);
return true;
});
electron.ipcMain.handle(REMOTE_INSTALLED_APPS_CHANNEL, (_event, snapshot) => {
runtime.syncInstalledApps(snapshot);
return true;
});
electron.ipcMain.handle(REMOTE_GAME_STATUS_CHANNEL, (_event, snapshot) => {
runtime.syncGameStatus(snapshot);
return true;
});
electron.ipcMain.handle(REMOTE_COMMAND_RESPONSE_CHANNEL, (_event, response) => {
const requestId = safeString(response?.requestId);
const pending = requestId ? pendingCommandResponses.get(requestId) : null;
if (!pending) {
return false;
}
pending.resolve(response);
return true;
});
electron.ipcMain.handle(IPC_CHANNEL.VALUE_CHANGED, (_event, change) => {
runtime.valueChanged(change);
return true;
});
electron.ipcMain.handle(IPC_CHANNEL.BIND_HANDLER, (event) => {
if (event && event.sender) {
boundRenderers.add(event.sender);
}
return true;
});
electron.ipcMain.handle(IPC_CHANNEL.REMOTE_URL, () => runtime.remoteUrl);
}
async function syncSnapshotWithAccessToken(runtime, sender, snapshot) {
const accessToken = await readWemodAccessToken(sender);
if (accessToken && snapshot && typeof snapshot === 'object') {
snapshot.accessToken = accessToken;
}
runtime.sync(snapshot);
}
async function readWemodAccessToken(sender) {
if (!sender || typeof sender.executeJavaScript !== 'function' || sender.isDestroyed?.()) {
return null;
}
try {
const token = await sender.executeJavaScript(WEMOD_ACCESS_TOKEN_SCRIPT);
return typeof token === 'string' && token ? token : null;
} catch (error) {
writeInstallLog('warn', 'Failed to read WeMod access token from renderer.', error);
return null;
}
}
function dispatchRemoteCommandToRenderer(sender, request, pendingCommandResponses) {
return new Promise((resolve, reject) => {
const requestId = `remote_command_${typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : Date.now().toString(36)}`;
const timer = setTimeout(() => {
pendingCommandResponses.delete(requestId);
reject(new Error('Renderer remote command timed out.'));
}, REMOTE_COMMAND_RESPONSE_TIMEOUT_MS);
pendingCommandResponses.set(requestId, {
resolve: (response) => {
clearTimeout(timer);
pendingCommandResponses.delete(requestId);
resolve(response);
},
reject: (error) => {
clearTimeout(timer);
pendingCommandResponses.delete(requestId);
reject(error instanceof Error ? error : new Error(String(error)));
},
});
try {
sender.send(REMOTE_COMMAND_REQUEST_CHANNEL, {
...request,
requestId,
});
} catch (error) {
clearTimeout(timer);
pendingCommandResponses.delete(requestId);
reject(error);
}
});
}
function buildRendererBridgeMissingResponse(request) {
return {
ok: false,
action: request?.action === 'stop' ? 'stop' : 'launch',
gameId: typeof request?.gameId === 'string' ? request.gameId : null,
titleId: typeof request?.titleId === 'string' ? request.titleId : null,
error: {
code: 'bridge_not_ready',
message: 'The renderer command bridge is not ready yet.',
},
};
}
module.exports = {
installWandRuntime,
};
+139
View File
@@ -0,0 +1,139 @@
const crypto = require('node:crypto');
const { BRIDGE_PROTOCOL_VERSION, WS_OPCODE } = require('./constants');
const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function jsonMessage(type, payload, requestId = null) {
return JSON.stringify({
type,
version: BRIDGE_PROTOCOL_VERSION,
requestId,
payload,
});
}
function makeFrame(opcode, payload) {
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
const header: number[] = [];
header.push(0x80 | (opcode & 0x0f));
if (source.length < 126) {
header.push(source.length);
return Buffer.concat([Buffer.from(header), source]);
}
if (source.length < 65536) {
const prefix = Buffer.from([header[0], 126, (source.length >> 8) & 0xff, source.length & 0xff]);
return Buffer.concat([prefix, source]);
}
const prefix = Buffer.alloc(10);
prefix[0] = header[0];
prefix[1] = 127;
prefix.writeUInt32BE(0, 2);
prefix.writeUInt32BE(source.length, 6);
return Buffer.concat([prefix, source]);
}
function sendText(client, text) {
if (!client.closed) {
client.socket.write(makeFrame(WS_OPCODE.TEXT, Buffer.from(text, 'utf8')));
}
}
function sendJson(client, type, payload, requestId = null) {
sendText(client, jsonMessage(type, payload, requestId));
}
function closeClient(client, code = 1000, reason = 'Closing') {
if (client.closed) {
return;
}
client.closed = true;
const reasonBuffer = Buffer.from(reason, 'utf8');
const payload = Buffer.alloc(2 + reasonBuffer.length);
payload.writeUInt16BE(code, 0);
reasonBuffer.copy(payload, 2);
client.socket.write(makeFrame(WS_OPCODE.CLOSE, payload));
client.socket.end();
}
function parseFrame(buffer) {
if (buffer.length < 2) {
return null;
}
const first = buffer[0];
const second = buffer[1];
const fin = (first & 0x80) !== 0;
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let length = second & 0x7f;
let offset = 2;
if (length === 126) {
if (buffer.length < offset + 2) {
return null;
}
length = buffer.readUInt16BE(offset);
offset += 2;
} else if (length === 127) {
if (buffer.length < offset + 8) {
return null;
}
const high = buffer.readUInt32BE(offset);
const low = buffer.readUInt32BE(offset + 4);
if (high !== 0) {
throw new Error('Large websocket frames are not supported.');
}
length = low;
offset += 8;
}
let mask = null;
if (masked) {
if (buffer.length < offset + 4) {
return null;
}
mask = buffer.subarray(offset, offset + 4);
offset += 4;
}
if (buffer.length < offset + length) {
return null;
}
const payload = Buffer.from(buffer.subarray(offset, offset + length));
if (masked && mask) {
for (let index = 0; index < payload.length; index += 1) {
payload[index] ^= mask[index % 4];
}
}
return {
bytesConsumed: offset + length,
fin,
opcode,
payload,
};
}
function createAcceptKey(key) {
return crypto.createHash('sha1').update(key + WS_GUID).digest('base64');
}
module.exports = {
closeClient,
createAcceptKey,
jsonMessage,
makeFrame,
parseFrame,
sendJson,
sendText,
};