import type {EnsureForward, ForwardAck, ForwardErrorCode, JoinReady, RelayRequest, WorkflowPayloads, WorldPayloads} from '../types/forward.ts'; import {assert_json} from '../bot/workflow/runtime/value.ts'; const errors: Record = { INVALID_REQUEST: ['Invalid request', false], UNAUTHORIZED: ['Authorization required', false], FORBIDDEN: ['Operation forbidden', false], NODE_OFFLINE: ['Node unavailable', true], RELAY_UNAVAILABLE: ['Relay unavailable', true], SESSION_STALE: ['Session is no longer current', false], TOKEN_EXPIRED: ['Authorization expired', false], TIMEOUT: ['Request deadline exceeded', true], CANCELLED: ['Request cancelled', false], NO_CONTEXT: ['Bot context unavailable', false], BOT_NOT_ONLINE: ['Bot is not online', false], RATE_LIMITED: ['Request limit exceeded', true], RESYNC_REQUIRED: ['Synchronization required', true], INTERNAL_ERROR: ['Operation failed', false], }; export class ForwardFailure extends Error { constructor(public readonly code: ForwardErrorCode) { super(errors[code][0]); } } export function error_ack(code: ForwardErrorCode): ForwardAck { const [message, retryable] = errors[code]; return {status: 'error', data: {code, message, retryable}}; } export function failure_code(error: unknown): ForwardErrorCode { if (error !== null && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && Object.hasOwn(errors, error.code)) { return error.code as ForwardErrorCode; } return 'INTERNAL_ERROR'; } export function object(value: unknown): value is Record { if (value === null || typeof value !== 'object' || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === null || Object.getPrototypeOf(prototype) === null; } export function exact(value: unknown, required: readonly string[], optional: readonly string[] = []): value is Record { return object(value) && required.every(key => Object.hasOwn(value, key)) && Reflect.ownKeys(value).every(key => { if (typeof key !== 'string' || (!required.includes(key) && !optional.includes(key))) return false; const descriptor = Object.getOwnPropertyDescriptor(value, key)!; return descriptor.enumerable && 'value' in descriptor; }); } export const uuid = (value: unknown): value is string => typeof value === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value); export const integer = (value: unknown, min = 0): value is number => Number.isSafeInteger(value) && (value as number) >= min; const text = (value: unknown, max: number): value is string => typeof value === 'string' && value.length > 0 && value.length <= max; export function validate_command(value: unknown): EnsureForward { if (!exact(value, ['forward_id', 'forward_session_id', 'node_session_id', 'node_id', 'bot_id', 'url', 'path', 'namespace', 'token', 'expires_at', 'deadline_at']) || !integer(value.forward_id) || !integer(value.node_id, 1) || !integer(value.bot_id, 1) || !uuid(value.forward_session_id) || !text(value.node_session_id, 256) || !value.node_session_id.trim() || Buffer.byteLength(value.node_session_id) > 256 || !text(value.token, 8192) || Buffer.byteLength(value.token) > 8192 || !integer(value.expires_at, 1) || !integer(value.deadline_at, 1) || !text(value.url, 2048) || !text(value.path, 256) || !text(value.namespace, 256) || !/^\/[a-zA-Z0-9_./-]*$/.test(value.path) || !/^\/[a-zA-Z0-9_/-]*$/.test(value.namespace)) { throw new ForwardFailure('INVALID_REQUEST'); } try { const url = new URL(value.url); if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash || url.search || (url.pathname !== '' && url.pathname !== '/')) throw new Error(); } catch { throw new ForwardFailure('INVALID_REQUEST'); } return value as unknown as EnsureForward; } export function success_data(value: unknown): unknown { if (!exact(value, ['status', 'data'])) throw new ForwardFailure('RELAY_UNAVAILABLE'); if (value.status === 'error') { if (exact(value.data, ['code', 'message', 'retryable']) && typeof value.data.code === 'string' && Object.hasOwn(errors, value.data.code) && typeof value.data.message === 'string' && typeof value.data.retryable === 'boolean') { throw new ForwardFailure(value.data.code as ForwardErrorCode); } throw new ForwardFailure('RELAY_UNAVAILABLE'); } if (value.status !== 'success') throw new ForwardFailure('RELAY_UNAVAILABLE'); return value.data; } export function validate_join(value: unknown, command: EnsureForward, joining: boolean): JoinReady { const data = success_data(value); if (!exact(data, ['forward_id', 'forward_session_id', 'node_session_id', 'node_id', 'bot_id', 'role', 'node_ready', 'expires_at']) || data.forward_id !== command.forward_id || data.forward_session_id !== command.forward_session_id || data.node_session_id !== command.node_session_id || data.node_id !== command.node_id || data.bot_id !== command.bot_id || data.role !== 'node' || typeof data.node_ready !== 'boolean' || (joining && data.node_ready !== false) || data.expires_at !== command.expires_at || (data.expires_at as number) <= Date.now()) { throw new ForwardFailure('SESSION_STALE'); } return data as unknown as JoinReady; } export function validate_rpc(value: unknown): RelayRequest { if (!exact(value, ['request_id', 'method', 'payload', 'correlation_id', 'forward_session_id', 'deadline_at']) || !uuid(value.request_id) || !uuid(value.correlation_id) || !uuid(value.forward_session_id) || !integer(value.deadline_at, 1)) throw new ForwardFailure('INVALID_REQUEST'); switch (value.method) { case 'world.snapshot': case 'world.chunk': case 'world.unwatch': validate_world_payload(value.method, value.payload); break; case 'workflow.start': case 'workflow.stop': case 'workflow.snapshot': case 'workflow.catalog': case 'workflow.runtime': case 'workflow.control': case 'workflow.command': validate_workflow_payload(value.method, value.payload); break; case 'execute_function': if (!exact(value.payload, ['function', 'name', 'args']) || typeof value.payload.function !== 'string' || !/^[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(value.payload.function) || typeof value.payload.name !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(value.payload.name) || !Array.isArray(value.payload.args)) throw new ForwardFailure('INVALID_REQUEST'); break; case 'bot.snapshot': case 'get_log_history': case 'get_context_info': case 'get_data_file_list': if (!exact(value.payload, [])) throw new ForwardFailure('INVALID_REQUEST'); break; case 'get_file_data': { const payload = value.payload; if (!exact(payload, ['id'], ['offset', 'length']) || !uuid(payload.id) || (Object.hasOwn(payload, 'offset') && !integer(payload.offset)) || (Object.hasOwn(payload, 'length') && (!integer(payload.length, 1) || payload.length > 262_144))) { throw new ForwardFailure('INVALID_REQUEST'); } break; } case 'bot.logs': { const payload = value.payload; if (!exact(payload, ['limit'], ['stream_id', 'after_seq']) || !integer(payload.limit, 1) || payload.limit > 200 || (Object.hasOwn(payload, 'stream_id') && !uuid(payload.stream_id)) || (Object.hasOwn(payload, 'after_seq') && (!uuid(payload.stream_id) || !integer(payload.after_seq)))) { throw new ForwardFailure('INVALID_REQUEST'); } break; } case 'bot.chat': if (!exact(value.payload, ['message']) || !text(value.payload.message, 256) || !value.payload.message.trim() || /[\r\n\u0000]/.test(value.payload.message)) throw new ForwardFailure('INVALID_REQUEST'); break; case 'tab_complete': if (!exact(value.payload, ['text']) || typeof value.payload.text !== 'string' || value.payload.text.length > 256 || /[\r\n\u0000]/.test(value.payload.text)) throw new ForwardFailure('INVALID_REQUEST'); break; default: throw new ForwardFailure('INVALID_REQUEST'); } return value as unknown as RelayRequest; } export function validate_workflow_payload(method: M, payload: unknown): WorkflowPayloads[M] { if (method === 'workflow.start') { if (!exact(payload, ['workflow_id'], ['trigger']) || typeof payload.workflow_id !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(payload.workflow_id) || ['__proto__', 'prototype', 'constructor'].includes(payload.workflow_id)) { throw new ForwardFailure('INVALID_REQUEST'); } if (Object.hasOwn(payload, 'trigger')) { try { if (!object(payload.trigger)) throw new Error(); assert_json(payload.trigger); } catch { throw new ForwardFailure('INVALID_REQUEST'); } } } else if (method === 'workflow.catalog' || method === 'workflow.runtime') { if (!exact(payload, [])) throw new ForwardFailure('INVALID_REQUEST'); } else if (method === 'workflow.command') { if (!exact(payload, ['command_id', 'context_generation']) || !uuid(payload.command_id) || !uuid(payload.context_generation)) throw new ForwardFailure('INVALID_REQUEST'); } else if (method === 'workflow.control') { try {assert_json(payload);} catch {throw new ForwardFailure('INVALID_REQUEST');} if (!object(payload) || !uuid(payload.command_id) || !uuid(payload.context_generation) || !['run_once', 'trigger_start', 'trigger_stop', 'stop', 'stop_run', 'reload'].includes(payload.action as string)) { throw new ForwardFailure('INVALID_REQUEST'); } const run = payload.action === 'stop_run'; if (!exact(payload, ['command_id', 'context_generation', 'action', run ? 'run_id' : 'workflow_id'], [...(!run ? ['expected_generation'] : []), ...(payload.action === 'run_once' ? ['input'] : [])]) || run && !uuid(payload.run_id) || !run && (typeof payload.workflow_id !== 'string' || !/^[a-zA-Z0-9_-]{1,100}$/.test(payload.workflow_id) || ['__proto__', 'prototype', 'constructor'].includes(payload.workflow_id)) || Object.hasOwn(payload, 'expected_generation') && !integer(payload.expected_generation, 1)) { throw new ForwardFailure('INVALID_REQUEST'); } if (Object.hasOwn(payload, 'input')) { try { if (!object(payload.input)) throw new Error(); assert_json(payload.input); if (Buffer.byteLength(JSON.stringify(payload.input)) > 65_536) throw new Error(); } catch {throw new ForwardFailure('INVALID_REQUEST');} } } else if (method === 'workflow.stop' || method === 'workflow.snapshot') { if (!exact(payload, method === 'workflow.stop' ? ['run_id'] : [], method === 'workflow.snapshot' ? ['run_id', 'include_branches'] : []) || Object.hasOwn(payload, 'run_id') && !uuid(payload.run_id) || Object.hasOwn(payload, 'include_branches') && (!Object.hasOwn(payload, 'run_id') || typeof payload.include_branches !== 'boolean')) { throw new ForwardFailure('INVALID_REQUEST'); } } else { throw new ForwardFailure('INVALID_REQUEST'); } return payload as WorkflowPayloads[M]; } export function validate_world_payload(method: M, payload: unknown): WorldPayloads[M] { if (method === 'world.unwatch') { if (!exact(payload, ['watch_id']) || !uuid(payload.watch_id)) throw new ForwardFailure('INVALID_REQUEST'); } else if (method === 'world.chunk') { if (!exact(payload, ['x', 'z', 'world_generation', 'watch_id']) || !Number.isSafeInteger(payload.x) || !Number.isSafeInteger(payload.z) || !uuid(payload.world_generation) || !uuid(payload.watch_id)) throw new ForwardFailure('INVALID_REQUEST'); } else if (method === 'world.snapshot') { if (!exact(payload, ['radius', 'known'], ['world_generation', 'watch_id']) || !integer(payload.radius, 1) || payload.radius > 3 || Object.hasOwn(payload, 'world_generation') && !uuid(payload.world_generation) || Object.hasOwn(payload, 'watch_id') && !uuid(payload.watch_id) || !Array.isArray(payload.known) || payload.known.length > 49 || Reflect.ownKeys(payload.known).length !== payload.known.length + 1) throw new ForwardFailure('INVALID_REQUEST'); const seen = new Set(); for (let index = 0; index < payload.known.length; index++) { const descriptor = Object.getOwnPropertyDescriptor(payload.known, String(index)); const item = descriptor && 'value' in descriptor && descriptor.enumerable ? descriptor.value : undefined; if (!exact(item, ['x', 'z', 'revision']) || !Number.isSafeInteger(item.x) || !Number.isSafeInteger(item.z) || !uuid(item.revision)) throw new ForwardFailure('INVALID_REQUEST'); const key = `${item.x},${item.z}`; if (seen.has(key)) throw new ForwardFailure('INVALID_REQUEST'); seen.add(key); } } else throw new ForwardFailure('INVALID_REQUEST'); return payload as WorldPayloads[M]; }