fix(protocol): reject invalid request contract values

This commit is contained in:
2026-09-05 02:16:47 +08:00
parent 0a7951ba32
commit fdc6fc2264
5 changed files with 113 additions and 38 deletions
@@ -2,10 +2,12 @@ export {
hasOwn,
optionalField,
requireArray,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
export {
@@ -5,10 +5,12 @@ import {
import {
hasOwn,
optionalField,
requireBoolean,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import {
@@ -73,18 +75,6 @@ export interface LoginRequestPayload extends Readonly<Record<string, unknown>> {
readonly machineroom: string;
}
function requireStringOrNumber(value: unknown, path: string): string | number {
if (typeof value === 'string') return value;
return requireNumber(value, path);
}
function requireBoolean(value: unknown, path: string): boolean {
if (typeof value !== 'boolean') {
throw new TypeError(`${path}: expected boolean, received ${value === null ? 'null' : typeof value}`);
}
return value;
}
export function buildLoginRequest(
runtimeConfig: FirstSliceRuntimeConfig,
account: LoginAccountIdentity,
@@ -99,18 +89,18 @@ export function buildLoginRequest(
if (location !== null) requireRecord(location, '$.device.location');
const data: Record<string, unknown> = {
agentid: requireStringOrNumber(identity.agentid, '$.runtimeConfig.identity.agentid'),
gameid: requireStringOrNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
agentid: requireStringOrFiniteNumber(identity.agentid, '$.runtimeConfig.identity.agentid'),
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
openid: requireString(accountRecord.openid, '$.account.openid'),
nickname: requireString(accountRecord.nickname, '$.account.nickname'),
avatar: requireString(accountRecord.avatar, '$.account.avatar'),
sex: requireInteger(accountRecord.sex, '$.account.sex'),
province: requireString(accountRecord.province, '$.account.province'),
city: requireString(accountRecord.city, '$.account.city'),
unionid: requireStringOrNumber(accountRecord.unionid, '$.account.unionid'),
unionid: requireStringOrFiniteNumber(accountRecord.unionid, '$.account.unionid'),
version: requireNumber(identity.version, '$.runtimeConfig.identity.version'),
channelid: requireStringOrNumber(identity.channelid, '$.runtimeConfig.identity.channelid'),
marketid: requireStringOrNumber(identity.marketid, '$.runtimeConfig.identity.marketid'),
channelid: requireStringOrFiniteNumber(identity.channelid, '$.runtimeConfig.identity.channelid'),
marketid: requireStringOrFiniteNumber(identity.marketid, '$.runtimeConfig.identity.marketid'),
location,
machineid: requireString(deviceRecord.machineid, '$.device.machineid'),
machineroom: requireString(deviceRecord.machineroom, '$.device.machineroom'),
@@ -127,10 +117,12 @@ export function buildLoginRequest(
const loginPlayerId = requireRecord(deviceRecord.loginPlayerId, '$.device.loginPlayerId');
if (requireBoolean(loginPlayerId.enabled, '$.device.loginPlayerId.enabled')) {
const cached = loginPlayerId.cachedPlayerId;
if (typeof cached === 'number' && Number.isInteger(cached) && cached > 0) {
data.playerid = cached;
const path = '$.device.loginPlayerId.cachedPlayerId';
const cachedPlayerId = requireInteger(loginPlayerId.cachedPlayerId, path);
if (cachedPlayerId <= 0) {
throw new RangeError(`${path}: expected positive integer, received number`);
}
data.playerid = cachedPlayerId;
}
return buildFirstSliceEnvelope('player_login', data as LoginRequestPayload);
@@ -4,10 +4,13 @@ import {
} from '../first-slice-routes.ts';
import {
hasOwn,
optionalField,
requireArray,
requireInteger,
requireNumber,
requireRecord,
requireString,
requireStringOrFiniteNumber,
type JsonRecord,
} from './validation.ts';
import type { FirstSliceRuntimeConfig } from './login-contract.ts';
@@ -63,18 +66,13 @@ function baseRoomAction(
): RoomActionPayload {
const identity = requireRecord(runtimeConfig?.identity, '$.runtimeConfig.identity');
const roomRecord = requireRecord(room, '$.room');
const agentid = identity.agentid;
const gameid = identity.gameid;
if (typeof agentid !== 'string' && typeof agentid !== 'number') {
throw new TypeError(`$.runtimeConfig.identity.agentid: expected string or number, received ${typeof agentid}`);
}
if (typeof gameid !== 'string' && typeof gameid !== 'number') {
throw new TypeError(`$.runtimeConfig.identity.gameid: expected string or number, received ${typeof gameid}`);
}
return {
agentid,
agentid: requireStringOrFiniteNumber(
identity.agentid,
'$.runtimeConfig.identity.agentid',
),
playerid: requireInteger(roomRecord.playerid, '$.room.playerid'),
gameid,
gameid: requireStringOrFiniteNumber(identity.gameid, '$.runtimeConfig.identity.gameid'),
roomcode: requireString(roomRecord.roomcode, '$.room.roomcode'),
};
}
@@ -124,8 +122,27 @@ export function buildJoinRoomRequest(
location: environmentRecord.location,
ip: requireString(environmentRecord.ip, '$.environment.ip'),
};
if (hasOwn(environmentRecord, 'vipMatch')) data.vipMatch = environmentRecord.vipMatch;
if (hasOwn(environmentRecord, 'match_id')) data.match_id = environmentRecord.match_id;
const vipMatch = optionalField(
environmentRecord,
'vipMatch',
'$.environment',
(value, path): 1 => {
const parsed = requireNumber(value, path);
if (parsed !== 1) {
throw new RangeError(`${path}: expected number 1, received number`);
}
return 1;
},
);
if (vipMatch !== undefined) data.vipMatch = vipMatch;
const matchId = optionalField(
environmentRecord,
'match_id',
'$.environment',
requireStringOrFiniteNumber,
);
if (matchId !== undefined) data.match_id = matchId;
return buildFirstSliceEnvelope('self_join_room', data as JoinRoomPayload);
}
@@ -26,6 +26,11 @@ export function requireString(value: unknown, path: string): string {
return value;
}
export function requireBoolean(value: unknown, path: string): boolean {
if (typeof value !== 'boolean') return invalid(path, 'boolean', value);
return value;
}
export function requireNumber(value: unknown, path: string): number {
if (typeof value !== 'number' || !Number.isFinite(value)) {
return invalid(path, 'finite number', value);
@@ -39,6 +44,14 @@ export function requireInteger(value: unknown, path: string): number {
return number;
}
export function requireStringOrFiniteNumber(value: unknown, path: string): string | number {
if (typeof value === 'string') return value;
if (typeof value !== 'number' || !Number.isFinite(value)) {
return invalid(path, 'string or finite number', value);
}
return value;
}
export function requireArray(value: unknown, path: string): readonly unknown[] {
if (!Array.isArray(value)) return invalid(path, 'array', value);
return value;