feat: relay world snapshot and voxel payloads

Allow world.* methods through the relay, validate their payloads, and
track per-session world revisions in the session store.
This commit is contained in:
2026-10-23 10:44:42 +08:00
parent 6ccb102d38
commit c82f15e8b6
8 changed files with 1064 additions and 46 deletions
+199 -6
View File
@@ -1,4 +1,4 @@
import {BOT_EVENTS, RELAY_METHODS, WORKFLOW_ACTIONS, WORKFLOW_METHODS} from '../types.ts';
import {BOT_EVENTS, RELAY_METHODS, WORKFLOW_ACTIONS, WORKFLOW_METHODS, WORLD_METHODS} from '../types.ts';
import type {
AuthorizeGrantCommand, BotEvent, ForwardAck, ForwardErrorCode, InfoUpdate, OpenSessionCommand,
RelayLimits, RelayMethod, RelayRequest, SessionScope,
@@ -86,13 +86,14 @@ export function check_size(value: unknown, max: number): void {
check_json_size(value, max, false);
}
function check_json_size(value: unknown, max: number, info: boolean | 'files' | 'workflow'): void {
function check_json_size(value: unknown, max: number, info: boolean | 'files' | 'workflow' | 'world'): void {
require_valid(integer(max, 1), 'Invalid byte limit');
let entries = 0;
let bytes = 0;
const ancestors = new Set<object>();
function visit(item: unknown, depth: number): void {
require_valid(depth <= (info ? 68 : 16) && ++entries <= (info === true ? 260_000 : 65_536), 'JSON complexity limit exceeded');
require_valid(depth <= (info === 'world' ? 16 : info ? 68 : 16)
&& ++entries <= (info === true ? 260_000 : info === 'world' ? 850_000 : 65_536), 'JSON complexity limit exceeded');
if (item === null || typeof item === 'boolean') {
bytes += item === null ? 4 : item ? 4 : 5;
} else if (typeof item === 'string') {
@@ -109,7 +110,7 @@ function check_json_size(value: unknown, max: number, info: boolean | 'files' |
const keys = Reflect.ownKeys(item);
if (Array.isArray(item)) {
if (info === 'workflow') require_valid(Object.getPrototypeOf(item) === Array.prototype, 'Expected a plain JSON array');
require_valid(item.length <= (info === true ? 65_536 : 4096) && keys.length === item.length + 1, 'Expected a dense JSON array');
require_valid(item.length <= (info === true ? 65_536 : info === 'world' ? 8192 : 4096) && keys.length === item.length + 1, 'Expected a dense JSON array');
for (let index = 0; index < item.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(item, String(index));
require_valid(descriptor && 'value' in descriptor && descriptor.enumerable, 'Expected JSON array data');
@@ -173,9 +174,27 @@ export function validate_request(value: unknown, limits: RelayLimits): asserts v
require_valid(value.method === 'get_log_history' || value.method === 'tab_complete' || value.method === 'get_context_info'
|| value.method === 'get_data_file_list' || value.method === 'get_file_data'
|| WORKFLOW_METHODS.includes(value.method as typeof WORKFLOW_METHODS[number])
|| WORLD_METHODS.includes(value.method as typeof WORLD_METHODS[number])
|| (value.method !== 'bot.files' && RELAY_METHODS.includes(value.method as RelayMethod)), 'Invalid relay method');
const payload = value.payload;
switch (value.method) {
case 'world.snapshot':
exact(payload, ['radius', 'known'], ['world_generation', 'watch_id']);
require_valid(integer(payload.radius, 1) && payload.radius <= 3, 'Invalid world radius');
world_revisions(payload.known);
for (const field of ['world_generation', 'watch_id']) {
if (Object.hasOwn(payload, field)) require_valid(is_uuid(payload[field]), 'Invalid world watch scope');
}
break;
case 'world.chunk':
exact(payload, ['x', 'z', 'world_generation', 'watch_id']);
require_valid(world_coordinate(payload.x) && world_coordinate(payload.z)
&& is_uuid(payload.world_generation) && is_uuid(payload.watch_id), 'Invalid world chunk request');
break;
case 'world.unwatch':
exact(payload, ['watch_id']);
require_valid(is_uuid(payload.watch_id), 'Invalid world watch ID');
break;
case 'bot.snapshot':
case 'get_log_history':
case 'get_context_info':
@@ -456,11 +475,175 @@ function workflow_node_result(value: unknown, field: 'execution' | 'node_id'): v
if (Object.hasOwn(value, 'error')) workflow_error(value.error, true);
}
export function validate_node_ack(method: RelayRequest['method'], value: unknown, bot_id: number, limits: RelayLimits): asserts value is ForwardAck<unknown> {
function world_coordinate(value: unknown): value is number {
return integer(value, -1_875_000) && value <= 1_875_000;
}
function world_revisions(value: unknown): asserts value is Array<{x: number; z: number; revision: string}> {
require_valid(Array.isArray(value) && value.length <= 49, 'Invalid world revisions');
const seen = new Set<string>();
for (const entry of value) {
exact(entry, ['x', 'z', 'revision']);
require_valid(world_coordinate(entry.x) && world_coordinate(entry.z) && is_uuid(entry.revision), 'Invalid world chunk revision');
const key = `${entry.x},${entry.z}`;
require_valid(!seen.has(key), 'Duplicate world coordinates');
seen.add(key);
}
}
function world_cells(value: unknown, max: number): Buffer {
require_valid(typeof value === 'string' && value.length === 684, 'Invalid world encoded length');
const decoded = Buffer.from(value, 'base64');
require_valid(decoded.length === 512 && decoded.toString('base64') === value, 'Invalid world encoded cells');
for (let offset = 0; offset < 512; offset += 2) {
require_valid(decoded.readUInt16LE(offset) <= max, 'Invalid world cell value');
}
return decoded;
}
function world_snapshot(value: unknown, payload: unknown): void {
exact(value, ['world_generation', 'context_generation', 'stream_id', 'watch_id', 'version', 'dimension',
'min_y', 'height', 'center', 'position', 'chunks', 'loaded', 'partial']);
require_valid(['world_generation', 'context_generation', 'stream_id', 'watch_id'].every(key => is_uuid(value[key]))
&& text(value.version, 64, true) && text(value.dimension, 256, true)
&& integer(value.min_y, -4096) && value.min_y <= 4096 && integer(value.height, 1) && value.height <= 4096
&& typeof value.partial === 'boolean', 'Invalid world snapshot metadata');
exact(value.center, ['x', 'z']);
require_valid(world_coordinate(value.center.x) && world_coordinate(value.center.z), 'Invalid world center');
exact(value.position, ['x', 'y', 'z', 'yaw']);
require_valid(Object.values(value.position).every(number => typeof number === 'number' && Number.isFinite(number)), 'Invalid world position');
const radius = is_record(payload) ? payload.radius : 3;
require_valid(integer(radius, 1) && radius <= 3, 'Invalid world request radius');
const center = value.center;
const in_radius = (x: number, z: number) => Math.abs(x - (center.x as number)) <= radius && Math.abs(z - (center.z as number)) <= radius;
world_revisions(value.loaded);
const loaded = new Map(value.loaded.map(entry => [`${entry.x},${entry.z}`, entry.revision.toLowerCase()]));
require_valid(value.loaded.every(entry => in_radius(entry.x, entry.z)), 'Loaded world chunk outside radius');
require_valid(Array.isArray(value.chunks) && value.chunks.length <= 49, 'Invalid world chunks');
const seen = new Set<string>();
for (const chunk of value.chunks) {
exact(chunk, ['x', 'z', 'revision', 'palette', 'heights', 'blocks'], ['terrain', 'fluids', 'spikes', 'features']);
require_valid(world_coordinate(chunk.x) && world_coordinate(chunk.z) && is_uuid(chunk.revision)
&& in_radius(chunk.x, chunk.z), 'Invalid world chunk scope');
const key = `${chunk.x},${chunk.z}`;
require_valid(!seen.has(key) && loaded.get(key) === chunk.revision.toLowerCase(), 'Invalid world chunk membership');
seen.add(key);
require_valid(Array.isArray(chunk.palette) && chunk.palette.length >= 1 && chunk.palette.length <= 512, 'Invalid world palette');
for (const block of chunk.palette) {
exact(block, ['name', 'color']);
require_valid(text(block.name, 128) && integer(block.color) && block.color <= 0xffffff, 'Invalid world palette entry');
}
// Heights encode topY - min_y + 1; zero represents an empty column.
const heights = world_cells(chunk.heights, value.height as number);
const blocks = world_cells(chunk.blocks, chunk.palette.length - 1);
require_valid(Object.hasOwn(chunk, 'terrain') === Object.hasOwn(chunk, 'fluids'), 'World terrain and fluids must appear together');
if (Object.hasOwn(chunk, 'fluids')) {
exact(chunk.terrain, ['heights', 'blocks']);
exact(chunk.fluids, ['heights', 'blocks', 'levels']);
const terrain_heights = world_cells(chunk.terrain.heights, value.height as number);
world_cells(chunk.terrain.blocks, chunk.palette.length - 1);
const fluid_heights = world_cells(chunk.fluids.heights, value.height as number);
const fluid_blocks = world_cells(chunk.fluids.blocks, chunk.palette.length - 1);
world_cells(chunk.fluids.levels, 15);
for (let offset = 0; offset < 512; offset += 2) {
const fluid_height = fluid_heights.readUInt16LE(offset);
if (fluid_height === 0) continue;
const name = chunk.palette[fluid_blocks.readUInt16LE(offset)].name;
require_valid(['water', 'lava', 'minecraft:water', 'minecraft:lava'].includes(name), 'Invalid world fluid block');
require_valid(terrain_heights.readUInt16LE(offset) <= fluid_height, 'World terrain exceeds fluid height');
require_valid(heights.readUInt16LE(offset) === fluid_height, 'World fluid differs from visible height');
}
}
if (Object.hasOwn(chunk, 'features')) {
exact(chunk.features, ['heights', 'blocks', 'entries']);
const bases = world_cells(chunk.features.heights, value.height as number);
world_cells(chunk.features.blocks, chunk.palette.length - 1);
require_valid(Array.isArray(chunk.features.entries) && chunk.features.entries.length > 0 && chunk.features.entries.length <= 512, 'Invalid world features');
const pairs = new Set<string>(), counts = new Map<number, number>();
const enums: Record<string, readonly string[]> = {facing: ['north', 'south', 'east', 'west', 'up', 'down'], half: ['top', 'bottom'], type: ['top', 'bottom', 'double', 'single', 'left', 'right'],
shape: ['straight', 'inner_left', 'inner_right', 'outer_left', 'outer_right'], axis: ['x', 'y', 'z'], vertical_direction: ['up', 'down'], thickness: ['tip', 'tip_merge', 'frustum', 'middle', 'base']};
for (const entry of chunk.features.entries) {
exact(entry, ['index', 'height', 'block', 'properties']);
require_valid(integer(entry.index) && entry.index <= 255 && integer(entry.height, 1) && entry.height <= (value.height as number)
&& integer(entry.block) && entry.block < chunk.palette.length, 'Invalid feature position');
exact(entry.properties, [], ['facing', 'rotation', 'half', 'type', 'shape', 'open', 'attached', 'lit', 'waterlogged', 'east', 'west', 'north', 'south', 'axis', 'up', 'vertical_direction', 'thickness']);
for (const [key, item] of Object.entries(entry.properties)) require_valid(key === 'rotation' ? integer(item) && item <= 15
: enums[key] ? typeof item === 'string' && enums[key]!.includes(item)
: ['north', 'south', 'east', 'west', 'up'].includes(key) ? typeof item === 'boolean' || ['none', 'low', 'tall'].includes(item as string)
: typeof item === 'boolean', 'Invalid feature property');
const index = entry.index as number, height = entry.height as number, pair = `${index}:${height}`;
const name = chunk.palette[entry.block as number].name.replace(/^minecraft:/, '');
require_valid(entry.properties.waterlogged === true || /(?:^|_)(?:wall_)?torch$|_(?:sign|slab|stairs|fence|pane|trapdoor|wall)$/.test(name), 'Invalid feature block');
require_valid(!pairs.has(pair) && height <= heights.readUInt16LE(index * 2) && height > bases.readUInt16LE(index * 2), 'Invalid feature column');
pairs.add(pair); counts.set(index, (counts.get(index) ?? 0) + 1);
require_valid(counts.get(index)! <= 16, 'Feature column exceeds limit');
}
}
if (Object.hasOwn(chunk, 'spikes')) {
exact(chunk.spikes, ['heights', 'blocks', 'segments']);
const bases = world_cells(chunk.spikes.heights, value.height as number);
world_cells(chunk.spikes.blocks, chunk.palette.length - 1);
require_valid(Array.isArray(chunk.spikes.segments) && chunk.spikes.segments.length >= 1
&& chunk.spikes.segments.length <= 512, 'Invalid world spike segments');
const visible = Object.hasOwn(chunk, 'terrain') ? world_cells((chunk.terrain as Record<string, unknown>).blocks, chunk.palette.length - 1) : blocks;
const pairs = new Set<string>(), counts = new Map<number, number>();
for (const segment of chunk.spikes.segments) {
exact(segment, ['index', 'height', 'direction', 'thickness']);
require_valid(integer(segment.index) && segment.index <= 255 && integer(segment.height, 1)
&& segment.height <= (value.height as number) && ['up', 'down'].includes(segment.direction as string)
&& ['tip', 'tip_merge', 'frustum', 'middle', 'base'].includes(segment.thickness as string), 'Invalid world spike state');
const index = segment.index as number, height = segment.height as number, pair = `${index}:${height}`;
require_valid(!pairs.has(pair) && height <= heights.readUInt16LE(index * 2) && height > bases.readUInt16LE(index * 2)
&& /^(?:minecraft:)?pointed_dripstone$/.test(chunk.palette[visible.readUInt16LE(index * 2)].name), 'Invalid world spike column');
pairs.add(pair); counts.set(index, (counts.get(index) ?? 0) + 1);
require_valid(counts.get(index)! <= 16, 'World spike column exceeds segment limit');
}
}
}
}
function world_voxel_chunk(value: unknown, payload: unknown): void {
exact(value, ['x', 'z', 'revision', 'world_generation', 'min_y', 'height', 'palette', 'sections']);
require_valid(world_coordinate(value.x) && world_coordinate(value.z) && is_uuid(value.revision)
&& is_uuid(value.world_generation) && integer(value.min_y, -4096) && value.min_y <= 4096
&& integer(value.height, 1) && value.height <= 4096, 'Invalid world voxel metadata');
require_valid(is_record(payload) && value.x === payload.x && value.z === payload.z
&& is_uuid(payload.world_generation) && value.world_generation.toLowerCase() === payload.world_generation.toLowerCase(),
'World chunk does not match the request');
require_valid(Array.isArray(value.palette) && value.palette.length >= 1 && value.palette.length <= 8192, 'Invalid world voxel palette');
for (const block of value.palette) {
exact(block, ['name', 'color', 'properties', 'occluding']);
require_valid(text(block.name, 128, true) && integer(block.color) && block.color <= 0xffffff
&& typeof block.occluding === 'boolean' && is_record(block.properties)
&& Object.keys(block.properties).length <= 32, 'Invalid world voxel palette entry');
for (const [key, item] of Object.entries(block.properties)) {
require_valid(key.length >= 1 && key.length <= 32 && !/[^a-z0-9_]/.test(key) && (typeof item === 'boolean' || text(item, 64)
|| integer(item, -65_536) && item <= 65_536), 'Invalid world voxel property');
}
}
require_valid(Array.isArray(value.sections) && value.sections.length <= 256, 'Invalid world voxel sections');
let previous_y = -1;
for (const section of value.sections) {
exact(section, ['y', 'blocks']);
require_valid(integer(section.y) && section.y < Math.ceil(value.height / 16) && section.y > previous_y,
'Invalid world voxel section order');
previous_y = section.y;
require_valid(typeof section.blocks === 'string' && section.blocks.length === 10_924, 'Invalid world voxel encoded length');
const decoded = Buffer.from(section.blocks, 'base64');
require_valid(decoded.length === 8192 && decoded.toString('base64') === section.blocks, 'Invalid world voxel encoded blocks');
for (let offset = 0; offset < decoded.length; offset += 2) {
require_valid(decoded.readUInt16LE(offset) < value.palette.length, 'Invalid world voxel palette index');
}
}
}
export function validate_node_ack(method: RelayRequest['method'], value: unknown, bot_id: number, limits: RelayLimits, payload?: unknown): asserts value is ForwardAck<unknown> {
const workflow = WORKFLOW_METHODS.includes(method as typeof WORKFLOW_METHODS[number]);
check_json_size(value, limits.max_payload_bytes, workflow ? 'workflow' : method === 'get_data_file_list' ? 'files' : method === 'get_context_info');
check_json_size(value, limits.max_payload_bytes, workflow ? 'workflow' : method === 'world.snapshot' || method === 'world.chunk' ? 'world'
: method === 'get_data_file_list' ? 'files' : method === 'get_context_info');
require_valid((method === 'get_log_history' || method === 'tab_complete' || method === 'get_context_info'
|| method === 'get_data_file_list' || method === 'get_file_data'
|| WORLD_METHODS.includes(method as typeof WORLD_METHODS[number])
|| workflow || (method !== 'bot.files' && RELAY_METHODS.includes(method as RelayMethod))) && integer(bot_id, 1), 'Invalid ACK context');
exact(value, ['status', 'data']);
const data = value.data;
@@ -472,6 +655,16 @@ export function validate_node_ack(method: RelayRequest['method'], value: unknown
}
require_valid(value.status === 'success', 'Invalid ACK status');
switch (method) {
case 'world.snapshot':
world_snapshot(data, payload);
break;
case 'world.chunk':
world_voxel_chunk(data, payload);
break;
case 'world.unwatch':
exact(data, ['released']);
require_valid(data.released === true, 'Invalid world unwatch result');
break;
case 'workflow.start':
workflow_start_result(data);
break;