Files
forward_server/types.ts
T
2508932142 c82f15e8b6 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.
2026-10-23 10:44:42 +08:00

328 lines
12 KiB
TypeScript

import type {Server as HttpServer} from 'node:http';
// Token permissions are capabilities, not the complete RPC method list.
export const RELAY_METHODS = ['bot.snapshot', 'bot.logs', 'bot.subscribe', 'bot.unsubscribe', 'bot.chat', 'execute_function', 'bot.files', 'workflow.control'] as const;
export const WORKFLOW_ACTIONS = ['run_once', 'trigger_start', 'trigger_stop', 'stop', 'stop_run', 'reload'] as const;
export const WORKFLOW_METHODS = ['workflow.start', 'workflow.stop', 'workflow.snapshot', 'workflow.catalog',
'workflow.control', 'workflow.command', 'workflow.runtime'] as const;
export const WORLD_METHODS = ['world.snapshot', 'world.chunk', 'world.unwatch'] as const;
export const BOT_EVENTS = ['status_update', 'log', 'message', 'bot_error'] as const;
export type RelayMethod = typeof RELAY_METHODS[number];
export type BotEventName = typeof BOT_EVENTS[number];
export type BotStatus = 'offline' | 'ready' | 'connecting' | 'online' | 'disconnecting' | 'reconnecting';
export type ForwardErrorCode = 'INVALID_REQUEST' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'NODE_OFFLINE'
| 'RELAY_UNAVAILABLE' | 'SESSION_STALE' | 'TOKEN_EXPIRED' | 'TIMEOUT' | 'CANCELLED'
| 'NO_CONTEXT' | 'BOT_NOT_ONLINE' | 'RATE_LIMITED' | 'RESYNC_REQUIRED' | 'INTERNAL_ERROR';
export type ForwardAck<T> = {status: 'success'; data: T} | {
status: 'error'; data: {code: ForwardErrorCode; message: string; retryable: boolean};
};
export type AckCallback<T = unknown> = (result: ForwardAck<T>) => void;
export interface SessionScope {
forward_id: number;
forward_session_id: string;
node_session_id: string;
node_id: number;
bot_id: number;
}
export interface RelayClaims extends SessionScope {
v: 1;
role: 'node' | 'web';
user_id?: number;
grant_id?: string;
permissions: RelayMethod[];
issued_at: number;
join_before: number;
expires_at: number;
jti: string;
}
export interface JoinRoomReady extends SessionScope {
role: 'node' | 'web';
node_ready: boolean;
expires_at: number;
}
export interface NodeReadyRequest {forward_session_id: string}
export interface NodeReadyResult {node_ready: true}
export interface SessionClosed {
forward_session_id?: string;
reason: string;
code: ForwardErrorCode;
}
export interface RelayRequest {
request_id: string;
method: RelayMethod | keyof WorkflowPayloads | keyof WorldPayloads | 'get_log_history' | 'tab_complete' | 'get_context_info' | 'get_data_file_list' | 'get_file_data';
payload: unknown;
}
export interface NodeRelayRequest extends RelayRequest {
correlation_id: string;
forward_session_id: string;
deadline_at: number;
}
export interface WorldChunkRevision {x: number; z: number; revision: string}
export interface WorldChunk extends WorldChunkRevision {
palette: Array<{name: string; color: number}>;
// Canonical Base64 UInt16LE[256]; heights/blocks retain the top visible surface.
heights: string;
blocks: string;
// Emitted together for chunks with fluids; omitted by legacy snapshots.
terrain?: {heights: string; blocks: string};
fluids?: {heights: string; blocks: string; levels: string};
spikes?: {heights: string; blocks: string; segments: Array<{
index: number; height: number; direction: 'up' | 'down';
thickness: 'tip' | 'tip_merge' | 'frustum' | 'middle' | 'base';
}>};
features?: {heights: string; blocks: string; entries: Array<{
index: number; height: number; block: number; properties: Record<string, string | number | boolean>;
}>};
}
export interface WorldPayloads {
'world.snapshot': {radius: number; known: WorldChunkRevision[]; world_generation?: string; watch_id?: string};
'world.chunk': {x: number; z: number; world_generation: string; watch_id: string};
'world.unwatch': {watch_id: string};
}
export interface WorldVoxelChunk extends WorldChunkRevision {
world_generation: string;
min_y: number;
height: number;
palette: Array<{name: string; color: number; properties: Record<string, string | number | boolean>; occluding: boolean}>;
// Sparse sections ordered by y relative to min_y; blocks are canonical Base64 UInt16LE[4096].
sections: Array<{y: number; blocks: string}>;
}
export interface WorldSnapshot {
world_generation: string;
context_generation: string;
stream_id: string;
watch_id: string;
version: string;
dimension: string;
min_y: number;
height: number;
center: {x: number; z: number};
position: {x: number; y: number; z: number; yaw: number};
chunks: WorldChunk[];
loaded: WorldChunkRevision[];
partial: boolean;
}
export interface BotSnapshot {
bot_id: number;
bot_status: BotStatus;
context_exists: boolean;
context_generation: string | null;
stream_id: string;
seq: number;
captured_at: number;
}
export interface LogSegment {
message: string;
font_color: string;
font_style: Array<'normal' | 'italic' | 'bold' | 'light' | 'underline' | 'url'>;
hoverEvent?: {action: 'show_text' | 'show_item' | 'show_entity'; value: string; segments?: Array<Pick<LogSegment, 'message' | 'font_color' | 'font_style'>>};
clickEvent?: {action: string; value: string};
insertion?: string;
}
export interface BotLogRecord {
id: string;
timestamp: number;
context_generation: string | null;
seq: number;
segments: LogSegment[];
}
export interface BotLogsResult {
stream_id: string;
seq: number;
records: BotLogRecord[];
truncated: boolean;
}
export interface TabCompleteResult {
matches: string[];
start: number;
length: number;
}
export type DataFileNode = {type: 'directory'; name: string; children: DataFileNode[]}
| {type: 'file'; name: string; id: string; size: number; modified_at: number};
export interface DataFileListResult {
listing_id: string;
expires_at: number;
partial: boolean;
children: DataFileNode[];
}
export interface FileDataResult {
id: string;
name: string;
encoding: 'base64';
offset: number;
next_offset: number;
total_bytes: number;
eof: boolean;
data: string;
}
export type JsonValue = null | boolean | number | string | JsonValue[] | {[key: string]: JsonValue};
export type WorkflowAction = typeof WORKFLOW_ACTIONS[number];
export interface WorkflowPayloads {
'workflow.start': {workflow_id: string; trigger?: Record<string, JsonValue>};
'workflow.stop': {run_id: string};
'workflow.snapshot': {run_id?: string; include_branches?: boolean};
'workflow.catalog': {};
'workflow.control': {command_id: string; context_generation: string; action: WorkflowAction;
workflow_id?: string; run_id?: string; expected_generation?: number; input?: Record<string, JsonValue>};
'workflow.command': {command_id: string; context_generation: string};
'workflow.runtime': {};
}
export interface WorkflowStartResult {status: 'started' | 'queued' | 'skipped'; run_id?: string; request_id?: string}
export interface WorkflowRunSnapshot {
run_id: string;
workflow_id: string;
status: 'running' | 'stopping' | 'completed' | 'failed' | 'cancelled' | 'timed_out';
steps: number;
active_nodes: string[];
started_at: number;
ended_at: number | null;
stop_reason: 'manual' | 'disconnect' | 'timeout' | 'disposed' | 'updated' | 'reloaded' | null;
error: {code: string; message: string; node_id: string} | null;
// Summary responses omit trigger and branches; legacy responses may omit the marker.
trigger?: Record<string, JsonValue>;
trigger_available?: boolean;
branches?: Array<{
trigger: Record<string, JsonValue>;
vars: Record<string, JsonValue>;
outputs: Record<string, {status: 'success' | 'failure'; output: JsonValue; execution: number;
error?: NonNullable<WorkflowRunSnapshot['error']>}>;
last_result: {status: 'success' | 'failure'; output: JsonValue; node_id: string;
error?: NonNullable<WorkflowRunSnapshot['error']>} | null;
inputs: Record<string, JsonValue>;
retry_counts: Record<string, number>;
}>;
}
export interface WorkflowCommand {
command_id: string;
action: WorkflowAction;
workflow_id: string | null;
state: 'accepted' | 'completed' | 'failed';
result?: WorkflowStartResult | {stopped: true} | {generation: number};
error?: {code: string; message: string};
}
export interface WorkflowRuntimeState {
workflow_id: string;
generation: number;
enabled: boolean;
start_type: 'common' | 'event' | 'timer';
trigger_state: 'stopped' | 'listening' | 'scheduled' | 'exhausted' | 'reloading' | 'error';
accepted_count: number;
max_runs: number | null;
next_fire_at: number | null;
active_run_ids: string[];
queued_count: number;
last_error: {code: string; message: string} | null;
}
export interface WorkflowRuntimeSnapshot {
source_config_revision: number;
workflows: WorkflowRuntimeState[];
commands: WorkflowCommand[];
}
export interface ContextInfoI18n {
lang: string; name: string; description: string;
fields?: Record<string, ContextInfoI18n>;
values?: Record<string, ContextInfoI18n>;
}
export interface ContextInfo {
name: string;
i18n: ContextInfoI18n;
value: JsonValue;
}
export interface InfoUpdate {
forward_session_id: string;
bot_id: number;
context_generation: string;
stream_id: string;
seq: number;
emitted_at: number;
data: {name: string; value: JsonValue};
}
export interface BotEventPayloads {
status_update: {status: BotStatus; context_exists: boolean};
log: {records: BotLogRecord[]};
message: {text: string; position: string; json?: unknown};
bot_error: {code?: string; message: string};
}
export type BotEvent = {[K in BotEventName]: {
forward_session_id: string;
bot_id: number;
context_generation: string | null;
stream_id: string;
seq: number;
emitted_at: number;
name: K;
payload: BotEventPayloads[K];
}}[BotEventName];
export interface RelayEventPayloads {
bot_event: Extract<BotEvent, {name: 'status_update'}>;
bot_log: Extract<BotEvent, {name: 'log'}>;
info_update: InfoUpdate;
}
export interface OpenSessionCommand extends SessionScope {lease_expires_at: number}
export interface AuthorizeGrantCommand {
forward_session_id: string;
grant_id: string;
user_id: number;
permissions: RelayMethod[];
expires_at: number;
}
export interface RevokeSessionCommand {forward_session_id: string; reason: string}
export interface RevokeGrantCommand extends RevokeSessionCommand {grant_id: string}
export interface RelayStatus {
forward_session_id: string;
node_ready: boolean;
reason: string;
}
export interface ForwardSessionStatus extends RelayStatus, SessionScope {
state: 'open' | 'ready' | 'unavailable' | 'closed';
reported_at: number;
}
export const CONTROL_EVENTS = ['forward_open_session', 'forward_authorize_grant',
'forward_revoke_session', 'forward_revoke_grant'] as const;
export type ControlEvent = typeof CONTROL_EVENTS[number];
export interface RelayLimits {
auth_timeout_ms: number;
close_timeout_ms: number;
rpc_timeout_ms: number;
unused_timeout_ms: number;
idle_timeout_ms: number;
max_token_age_ms: number;
join_window_ms: number;
clock_skew_ms: number;
max_lease_ms: number;
max_token_bytes: number;
max_payload_bytes: number;
max_chat_length: number;
max_connections: number;
max_sessions: number;
max_grants_per_session: number;
max_pending_per_web: number;
max_pending_total: number;
requests_per_second: number;
events_per_second: number;
max_dedupe_entries: number;
dedupe_ttl_ms: number;
max_buffered_packets: number;
}
export interface RelayLogger {log(input: unknown): void; error(message: unknown): unknown}
export interface ControlTransport {
on(event: string, listener: (...args: any[]) => void): unknown;
off(event: string, listener: (...args: any[]) => void): unknown;
emit(event: string, ...args: any[]): unknown;
}
export interface ForwardServerOptions {
forward_id: number;
key: string;
http_server?: HttpServer;
port?: number;
host?: string;
path?: string;
namespace?: string;
allowed_origins: string[];
allow_no_origin?: boolean;
control?: ControlTransport;
logger?: RelayLogger;
limits?: Partial<RelayLimits>;
}