chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 6ccb102d38
20 changed files with 5949 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {createServer} from 'node:http';
import {EventEmitter, once} from 'node:events';
import {randomUUID} from 'node:crypto';
import {connect as connect_tcp} from 'node:net';
import {Server} from 'socket.io';
import {io as client} from 'socket.io-client';
import type {Socket as ClientSocket} from 'socket.io-client';
import {start_forward_server, stop_forward_server} from '../index.ts';
import {DEFAULT_LIMITS, resolve_options} from '../utils/relay_config.ts';
import {encode_token} from '../utils/token_utils.ts';
import type {BotLogRecord, ForwardAck, RelayClaims, SessionScope} from '../types.ts';
const key = 'lifecycle-test-secret-no-production-value';
const options = () => ({forward_id: 0, key, port: 0, allowed_origins: ['https://console.example']});
const address = (server: ReturnType<typeof createServer>) => {
const value = server.address();
assert.ok(value && typeof value === 'object');
return `http://127.0.0.1:${value.port}`;
};
async function connect(socket: ClientSocket) {
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('connect_error', reject);
socket.connect();
});
}
function control_request(control: EventEmitter, event: string, value: unknown): ForwardAck<unknown> {
let result: ForwardAck<unknown> | undefined;
control.emit(event, value, (ack: ForwardAck<unknown>) => { result = ack; });
assert.ok(result, `Missing control ACK for ${event}`);
return result;
}
test('configuration validates explicit origins, IDs, limits and required secret', () => {
assert.throws(() => resolve_options({...options(), key: ''}));
assert.throws(() => resolve_options({...options(), forward_id: -1}));
assert.throws(() => resolve_options({...options(), allowed_origins: ['*']}));
assert.throws(() => resolve_options({...options(), limits: {rpc_timeout_ms: 0}}));
assert.equal(DEFAULT_LIMITS.rpc_timeout_ms, 5_000);
});
test('concurrent startup shares one listening service, stop is idempotent and restart works', async t => {
t.after(stop_forward_server);
const input = options();
const [a, b] = await Promise.all([start_forward_server(input), start_forward_server(input)]);
assert.equal(a, b);
assert.ok(a.http_server.listening);
await assert.rejects(start_forward_server({...input, forward_id: 2}), /different options/);
await Promise.all([a.stop(), a.stop()]);
assert.equal(a.http_server.listening, false);
const restarted = await start_forward_server(options());
assert.notEqual(restarted, a);
assert.ok(restarted.http_server.listening);
});
test('port collision rejects startup and resets initialization for retry', async t => {
const occupied = createServer();
occupied.listen(0, '127.0.0.1');
await once(occupied, 'listening');
t.after(async () => {
await stop_forward_server();
await new Promise<void>(resolve => occupied.close(() => resolve()));
});
const bound = occupied.address();
assert.ok(bound && typeof bound === 'object');
await assert.rejects(start_forward_server({...options(), port: bound.port, host: '127.0.0.1'}), /EADDRINUSE/);
const relay = await start_forward_server(options());
assert.ok(relay.http_server.listening);
});
test('shutdown deadline terminates owned stalled HTTP connections and allows restart', async t => {
t.after(stop_forward_server);
const relay = await start_forward_server({...options(), limits: {close_timeout_ms: 30}});
const bound = relay.http_server.address();
assert.ok(bound && typeof bound === 'object');
const stalled = connect_tcp({host: '127.0.0.1', port: bound.port});
t.after(() => stalled.destroy());
await once(stalled, 'connect');
stalled.write('GET / HTTP/1.1\r\nHost: localhost\r\n');
const closed = once(stalled, 'close');
await assert.rejects(relay.stop(), /shutdown timed out/);
await closed;
const restarted = await start_forward_server(options());
assert.ok(restarted.http_server.listening);
});
test('borrowed host and unrelated Socket.IO stay alive through relay stop and restart', async t => {
const host = createServer((_request, response) => response.end('main-alive'));
const control = new EventEmitter();
const input = {forward_id: 0, key, http_server: host, allowed_origins: [] as string[], control};
let relay = await start_forward_server(input);
// Main installs its own Socket.IO service after the embedded relay.
const main_io = new Server(host, {path: '/api/node', serveClient: false});
host.listen(0, '127.0.0.1');
await once(host, 'listening');
const main_client = client(address(host), {path: '/api/node', autoConnect: false, reconnection: false});
t.after(async () => {
main_client.disconnect();
await stop_forward_server();
await main_io.close();
});
await connect(main_client);
for (let count = 0; count < 3; count++) {
await relay.stop();
assert.equal(host.listening, true);
assert.equal(main_client.connected, true);
assert.equal(await (await fetch(`${address(host)}/health`)).text(), 'main-alive');
assert.equal((await fetch(`${address(host)}/api/forward_server/?EIO=4&transport=polling`)).status, 503);
relay = await start_forward_server(input);
const response = await fetch(`${address(host)}/api/forward_server/?EIO=4&transport=polling`);
assert.equal(response.status, 200);
assert.match(await response.text(), /^0\{/);
assert.equal(control.listenerCount('forward_open_session'), 1);
}
assert.ok(host.listenerCount('request') <= 3);
});
test('polling and websocket enforce origins and use authenticated control scope', async t => {
const control = new EventEmitter();
const relay = await start_forward_server({...options(), control});
const sockets: ClientSocket[] = [];
t.after(async () => {
for (const socket of sockets) socket.disconnect();
await stop_forward_server();
});
const scope: SessionScope = {
forward_id: 0, forward_session_id: randomUUID(), node_session_id: 'authenticated-main-node-session',
node_id: 10, bot_id: 20,
};
const now = Date.now();
assert.equal(control_request(control, 'forward_open_session', {...scope, lease_expires_at: now + 60_000}).status, 'success');
const grant_id = randomUUID();
assert.equal(control_request(control, 'forward_authorize_grant', {
forward_session_id: scope.forward_session_id, grant_id, user_id: 30,
permissions: ['bot.snapshot'], expires_at: now + 60_000,
}).status, 'success');
const claims: RelayClaims = {
...scope, v: 1, role: 'web', user_id: 30, grant_id, permissions: ['bot.snapshot'],
issued_at: now, join_before: now + 30_000, expires_at: now + 60_000, jti: randomUUID(),
};
for (const transport of ['polling', 'websocket'] as const) {
const socket = client(address(relay.http_server), {
path: relay.path, transports: [transport], autoConnect: false, reconnection: false,
extraHeaders: {Origin: 'https://console.example'},
});
sockets.push(socket);
await connect(socket);
const ack = await socket.timeout(2_000).emitWithAck('join_room', {token: encode_token(key, claims)});
assert.equal(ack.status, 'success');
assert.equal(ack.data.node_ready, false);
socket.disconnect();
const forbidden = client(address(relay.http_server), {
path: relay.path, transports: [transport], autoConnect: false, reconnection: false,
extraHeaders: {Origin: 'https://attacker.example'},
});
sockets.push(forbidden);
await assert.rejects(connect(forbidden));
}
});
test('default transport and validators relay large log history over polling and websocket', async t => {
const control = new EventEmitter();
const relay = await start_forward_server({...options(), control});
const sockets: ClientSocket[] = [];
t.after(async () => {
for (const socket of sockets) socket.disconnect();
await stop_forward_server();
});
const scope: SessionScope = {forward_id: 0, forward_session_id: randomUUID(),
node_session_id: 'large-history-node-session', node_id: 10, bot_id: 20};
const now = Date.now();
assert.equal(control_request(control, 'forward_open_session', {...scope,
lease_expires_at: now + 60_000}).status, 'success');
const records: BotLogRecord[] = Array.from({length: 20}, (_, index): BotLogRecord => ({id: randomUUID(),
timestamp: now, context_generation: null, seq: index + 1,
segments: Array.from({length: 32}, () => ({message: '\u754c'.repeat(1024),
font_color: '#ffffff', font_style: ['normal']}))}));
const result = {status: 'success', data: {stream_id: randomUUID(), seq: 20, records, truncated: false}};
assert.ok(Buffer.byteLength(JSON.stringify(result)) > 1024 * 1024);
for (const transport of ['polling', 'websocket'] as const) {
const grant_id = randomUUID();
assert.equal(control_request(control, 'forward_authorize_grant', {
forward_session_id: scope.forward_session_id, grant_id, user_id: 30,
permissions: ['bot.logs'], expires_at: now + 60_000,
}).status, 'success');
const base: RelayClaims = {...scope, v: 1, role: 'node', permissions: ['bot.logs'],
issued_at: now, join_before: now + 30_000, expires_at: now + 60_000, jti: randomUUID()};
const node = client(address(relay.http_server), {path: relay.path, transports: [transport],
autoConnect: false, reconnection: false});
sockets.push(node);
node.on('relay_request', (_request, reply) => reply(result));
await connect(node);
assert.equal((await node.timeout(2_000).emitWithAck('join_room', {token: encode_token(key, base)})).status, 'success');
assert.equal((await node.timeout(2_000).emitWithAck('forward_node_ready', {
forward_session_id: scope.forward_session_id})).status, 'success');
const web = client(address(relay.http_server), {path: relay.path, transports: [transport],
autoConnect: false, reconnection: false});
sockets.push(web);
await connect(web);
assert.equal((await web.timeout(2_000).emitWithAck('join_room', {token: encode_token(key, {
...base, role: 'web', grant_id, user_id: 30, jti: randomUUID(),
})})).status, 'success');
const history = await web.timeout(5_000).emitWithAck('relay_request', {
request_id: randomUUID(), method: 'get_log_history', payload: {},
});
assert.deepEqual(history, result);
assert.equal(node.connected, true);
assert.equal(web.connected, true);
web.disconnect();
node.disconnect();
}
});
test('control rebinding removes only owned listeners and stale disconnect cannot disable new control', async t => {
const first = new EventEmitter();
const second = new EventEmitter();
let unrelated = 0;
first.on('forward_open_session', () => { unrelated++; });
const relay = await start_forward_server({...options(), control: first});
t.after(stop_forward_server);
const dispose = relay.bind_control(second);
assert.equal(first.listenerCount('forward_open_session'), 1);
first.emit('disconnect');
assert.equal(relay.store.control_available, true);
first.emit('forward_open_session');
assert.equal(unrelated, 1);
second.emit('disconnect');
assert.equal(relay.store.control_available, false);
dispose();
assert.equal(second.listenerCount('forward_open_session'), 0);
});
test('borrowed host dispatches custom namespace WebSocket without closing an existing Main connection', async t => {
const host = createServer((_request, response) => response.end('main'));
const main_io = new Server(host, {path: '/api/node', serveClient: false});
host.listen(0, '127.0.0.1');
await once(host, 'listening');
const main_client = client(address(host), {path: '/api/node', autoConnect: false, reconnection: false});
await connect(main_client);
const relay = await start_forward_server({forward_id: 0, key, http_server: host,
allowed_origins: [], namespace: '/relay'});
const web = client(`${address(host)}/relay`, {path: relay.path,
transports: ['websocket'], autoConnect: false, reconnection: false});
t.after(async () => {
web.disconnect();
main_client.disconnect();
await stop_forward_server();
await main_io.close();
});
await connect(web);
const result = await web.timeout(1_000).emitWithAck('forward_open_session', {});
assert.equal(result.status, 'error');
assert.equal(result.data.code, 'FORBIDDEN');
await relay.stop();
assert.equal(main_client.connected, true);
assert.equal(host.listening, true);
});
test('closing and relistening the borrowed host reuses its relay dispatcher', async t => {
const host = createServer((_request, response) => response.end('main'));
const input = {forward_id: 0, key, http_server: host, allowed_origins: [] as string[]};
let relay = await start_forward_server(input);
host.listen(0, '127.0.0.1');
await once(host, 'listening');
t.after(async () => {
await stop_forward_server();
if (host.listening) await new Promise<void>(resolve => host.close(() => resolve()));
});
const listener_counts = {request: host.listenerCount('request'), upgrade: host.listenerCount('upgrade')};
await new Promise<void>(resolve => host.close(() => resolve()));
await relay.stop();
relay = await start_forward_server(input);
host.listen(0, '127.0.0.1');
await once(host, 'listening');
const web = client(address(host), {path: relay.path, transports: ['websocket'],
autoConnect: false, reconnection: false});
t.after(() => web.disconnect());
await connect(web);
assert.equal(host.listenerCount('request'), listener_counts.request);
assert.equal(host.listenerCount('upgrade'), listener_counts.upgrade);
assert.equal(await (await fetch(`${address(host)}/`)).text(), 'main');
});
+791
View File
@@ -0,0 +1,791 @@
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {createServer} from 'node:http';
import {once} from 'node:events';
import {setTimeout as delay} from 'node:timers/promises';
import test from 'node:test';
import {Server} from 'socket.io';
import {io, type Socket as Client} from 'socket.io-client';
import {create_session_store} from '../runtime/session_store.ts';
import {install_relay_routes} from '../runtime/relay_routes.ts';
import {DEFAULT_LIMITS} from '../utils/relay_config.ts';
import {encode_token} from '../utils/token_utils.ts';
import type {BotLogRecord, ForwardAck, RelayClaims, RelayLimits, RelayMethod, RelayRequest, SessionScope} from '../types.ts';
const key = 'relay-integration-test-secret-only';
const permissions: RelayMethod[] = ['bot.snapshot', 'bot.logs', 'bot.subscribe', 'bot.unsubscribe', 'bot.chat', 'execute_function', 'bot.files'];
const scope = (): SessionScope => ({forward_id: 1, forward_session_id: randomUUID(), node_session_id: randomUUID(), node_id: 2, bot_id: 3});
function token(s: SessionScope, role: 'node' | 'web', grant_id?: string, expires_at = Date.now() + 10_000, user_id = 4,
granted = permissions): string {
const now = Date.now();
const claims: RelayClaims = {...s, role, v: 1, permissions: granted, issued_at: now, join_before: Math.min(now + 1000, expires_at),
expires_at, jti: randomUUID(), ...(role === 'web' ? {grant_id, user_id} : {})};
return encode_token(key, claims);
}
const request = (method: RelayRequest['method'], payload: unknown = {}, request_id = randomUUID()) => ({method, payload, request_id});
function received(socket: Client, event: string): Promise<any[]> {return new Promise(resolve => socket.once(event, (...args) => resolve(args)));}
async function ack(socket: Client, name: string, value: unknown): Promise<ForwardAck<any>> {
return socket.timeout(1500).emitWithAck(name, value);
}
function code(result: ForwardAck<unknown>, expected: string): void {
assert.equal(result.status, 'error');
if (result.status === 'error') assert.equal(result.data.code, expected);
}
async function fixture(overrides: Partial<RelayLimits> = {}) {
const http = createServer();
const server = new Server(http);
const namespace = server.of('/relay');
const statuses: unknown[] = [];
const store = create_session_store({forward_id: 1, key, limits: {...DEFAULT_LIMITS, rpc_timeout_ms: 120, ...overrides},
on_status: value => statuses.push(value)});
const dispose = install_relay_routes(namespace, store);
http.listen(0, '127.0.0.1');
await once(http, 'listening');
const port = (http.address() as {port: number}).port;
const clients: Client[] = [];
const s = scope();
store.open({...s, lease_expires_at: Date.now() + 20_000});
async function connect() {
const socket = io(`http://127.0.0.1:${port}/relay`, {transports: ['websocket'], reconnection: false, forceNew: true});
clients.push(socket);
await received(socket, 'connect');
return socket;
}
async function node(granted = permissions, ready = true) {
const socket = await connect();
assert.equal((await ack(socket, 'join_room', {token: token(s, 'node', undefined, Date.now() + 10_000, 4, granted)})).status, 'success');
if (ready) assert.equal((await ack(socket, 'forward_node_ready', {forward_session_id: s.forward_session_id})).status, 'success');
return socket;
}
async function web(grant_id = randomUUID(), granted = permissions, expires_at = Date.now() + 10_000) {
store.authorize({forward_session_id: s.forward_session_id, grant_id, user_id: 4, permissions: granted, expires_at});
const socket = await connect();
assert.equal((await ack(socket, 'join_room', {token: token(s, 'web', grant_id, expires_at, 4, granted)})).status, 'success');
return {socket, grant_id};
}
async function close() {
store.close(); dispose();
for (const client of clients) client.close();
await server.close();
}
return {store, namespace, connect, node, web, s, close, statuses, dispose, http};
}
function snapshot(bot_id: number, seq = 0) {
return {status: 'success', data: {bot_id, bot_status: 'offline', context_exists: false,
context_generation: null as string | null, stream_id: randomUUID(), seq, captured_at: Date.now()}};
}
test('file RPCs require bot.files, match requested ranges and redispatch without caching ACK bodies', async () => {
const f = await fixture({requests_per_second: 100});
try {
const node = await f.node(); const web = await f.web(randomUUID(), ['bot.files']);
const limited = await f.web(randomUUID(), ['bot.snapshot']);
const file_id = randomUUID();
const listing = request('get_data_file_list');
const read = request('get_file_data', {id: file_id});
let calls = 0;
let response: unknown = {status: 'success', data: {listing_id: randomUUID(), expires_at: Date.now() + 1000,
partial: false, children: []}};
let held: ((value: unknown) => void) | undefined;
let hold = false;
node.on('relay_request', (_value, reply) => {calls++; if (hold) held = reply; else reply(response);});
for (const command of [listing, read]) code(await ack(limited.socket, 'relay_request', command), 'FORBIDDEN');
code(await ack(node, 'relay_request', listing), 'FORBIDDEN');
code(await ack(web.socket, 'relay_request', request('bot.files')), 'INVALID_REQUEST');
assert.deepEqual(await ack(web.socket, 'relay_request', listing), response);
assert.deepEqual(await ack(web.socket, 'relay_request', listing), response);
assert.equal(calls, 2);
const sessions = (f.store as any).sessions as Map<string, any>;
const entries = sessions.get(f.s.forward_session_id).grants.get(web.grant_id).dedupe as Map<string, any>;
assert.equal(entries.get(listing.request_id).result, undefined);
assert.equal(entries.get(listing.request_id).completed, true);
response = {status: 'success', data: {id: file_id, name: 'data.bin', encoding: 'base64',
offset: 0, next_offset: 1, total_bytes: 1, eof: true, data: 'YQ=='}};
assert.deepEqual(await ack(web.socket, 'relay_request', read), response);
assert.deepEqual(await ack(web.socket, 'relay_request', read), response);
assert.equal(entries.get(read.request_id).result, undefined);
code(await ack(web.socket, 'relay_request', {...read, payload: {id: randomUUID()}}), 'INVALID_REQUEST');
code(await ack(web.socket, 'relay_request', {...listing, method: 'get_file_data', payload: {id: file_id}}), 'INVALID_REQUEST');
for (const changes of [{id: randomUUID()}, {offset: 1, next_offset: 2, total_bytes: 2},
{data: 'YWI=', next_offset: 2, total_bytes: 2}]) {
const original = response as {status: string; data: Record<string, unknown>};
response = {...original, data: {...original.data, ...changes}};
const command = request('get_file_data', {id: file_id, length: 1});
code(await ack(web.socket, 'relay_request', command), 'INVALID_REQUEST');
assert.equal(entries.get(command.request_id).result, undefined);
response = original;
}
response = {status: 'success', data: {id: file_id, name: 'data.bin', encoding: 'base64',
offset: 7, next_offset: 7, total_bytes: 7, eof: true, data: ''}};
assert.deepEqual(await ack(web.socket, 'relay_request', request('get_file_data', {id: file_id, offset: 7, length: 1})), response);
hold = true;
const pending = ack(web.socket, 'relay_request', read);
while (!held) await delay(1);
code(await ack(web.socket, 'relay_request', read), 'RATE_LIMITED');
held({status: 'error', data: {code: 'NO_CONTEXT', message: 'No context', retryable: false}});
code(await pending, 'NO_CONTEXT');
assert.equal(entries.get(read.request_id).result, undefined);
hold = false;
response = {status: 'error', data: {code: 'BOT_NOT_ONLINE', message: 'Offline', retryable: false}};
const before = calls;
code(await ack(web.socket, 'relay_request', read), 'BOT_NOT_ONLINE');
assert.equal(calls, before + 1);
f.store.set_control_available(false);
code(await ack(web.socket, 'relay_request', listing), 'BOT_NOT_ONLINE');
assert.equal(calls, before + 2);
} finally {await f.close();}
});
test('file RPC redispatch rechecks Node permission and readiness', async () => {
const f = await fixture();
try {
const node = await f.node(['bot.snapshot']);
const web = await f.web(randomUUID(), ['bot.files']);
const command = request('get_data_file_list');
code(await ack(web.socket, 'relay_request', command), 'FORBIDDEN');
const replacement = await f.node(['bot.files'], false);
code(await ack(web.socket, 'relay_request', command), 'NODE_OFFLINE');
await ack(replacement, 'forward_node_ready', {forward_session_id: f.s.forward_session_id});
replacement.on('relay_request', (_value, reply) => reply({status: 'success', data: {
listing_id: randomUUID(), expires_at: 1, partial: true, children: []}}));
assert.equal((await ack(web.socket, 'relay_request', command)).status, 'success');
await f.node(['bot.snapshot']);
code(await ack(web.socket, 'relay_request', command), 'FORBIDDEN');
node.close();
} finally {await f.close();}
});
test('RPC responses stay private, request IDs are grant-scoped, dedupe conflicts and chat replay are rejected', async () => {
const f = await fixture();
try {
const node = await f.node();
let calls = 0;
node.on('relay_request', (value, reply) => {calls++; reply(value.method === 'bot.chat' ? {status: 'success', data: {accepted: true}} : snapshot(f.s.bot_id, calls));});
const a = await f.web(); const b = await f.web();
let leaked = 0;
b.socket.on('relay_request', () => leaked++);
const id = randomUUID();
const [ra, rb] = await Promise.all([ack(a.socket, 'relay_request', request('bot.snapshot', {}, id)), ack(b.socket, 'relay_request', request('bot.snapshot', {}, id))]);
assert.equal(ra.status, 'success'); assert.equal(rb.status, 'success');
assert.equal(calls, 2); assert.equal(leaked, 0);
assert.deepEqual(await ack(a.socket, 'relay_request', request('bot.snapshot', {}, id)), ra);
assert.equal(calls, 2);
code(await ack(a.socket, 'relay_request', request('bot.logs', {limit: 1}, id)), 'INVALID_REQUEST');
const chat = request('bot.chat', {message: 'hello'});
assert.equal((await ack(a.socket, 'relay_request', chat)).status, 'success');
code(await ack(a.socket, 'relay_request', chat), 'CANCELLED');
assert.equal(calls, 3);
} finally {await f.close();}
});
test('execute_function forwards exact payloads, validates ACKs and rejects pending and completed replay', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web(randomUUID(), ['execute_function']);
const payload = {function: 'movement/navigation', name: 'go_to', args: [{x: 1, y: 2}, true, null]};
const command = request('execute_function', payload);
let calls = 0;
let reply!: (value: unknown) => void;
const dispatched = new Promise<void>(resolve => node.on('relay_request', (value, callback) => {
calls++;
assert.equal(value.method, command.method);
assert.deepEqual(value.payload, payload);
assert.equal(value.forward_session_id, f.s.forward_session_id);
reply = callback; resolve();
}));
const pending = ack(web.socket, 'relay_request', command);
await dispatched;
code(await ack(web.socket, 'relay_request', command), 'RATE_LIMITED');
reply({status: 'success', data: true});
assert.deepEqual(await pending, {status: 'success', data: true});
code(await ack(web.socket, 'relay_request', command), 'CANCELLED');
code(await ack(web.socket, 'relay_request', {...command, payload: {...payload, args: []}}), 'INVALID_REQUEST');
assert.equal(calls, 1);
node.removeAllListeners('relay_request');
let response: unknown = {status: 'success', data: false};
node.on('relay_request', (_value, callback) => {calls++; callback(response);});
assert.deepEqual(await ack(web.socket, 'relay_request', request('execute_function', payload)), response);
response = {status: 'success', data: {accepted: true}};
const invalid_ack = request('execute_function', payload);
code(await ack(web.socket, 'relay_request', invalid_ack), 'INVALID_REQUEST');
code(await ack(web.socket, 'relay_request', invalid_ack), 'CANCELLED');
f.store.set_control_available(false);
code(await ack(web.socket, 'relay_request', request('execute_function', payload)), 'RELAY_UNAVAILABLE');
assert.equal(calls, 3);
} finally {await f.close();}
});
test('execute_function requires permission in Web token, grant and Node token', async () => {
const f = await fixture();
try {
const node = await f.node(['bot.chat']);
let calls = 0; node.on('relay_request', () => calls++);
const grant_id = randomUUID();
f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id, user_id: 4,
permissions, expires_at: Date.now() + 10_000});
const limited = await f.connect();
assert.equal((await ack(limited, 'join_room', {token: token(f.s, 'web', grant_id,
Date.now() + 10_000, 4, ['bot.chat'])})).status, 'success');
const command = request('execute_function', {function: 'movement', name: 'stop', args: []});
code(await ack(limited, 'relay_request', command), 'FORBIDDEN');
const web = await f.web(randomUUID(), ['execute_function']);
code(await ack(web.socket, 'relay_request', command), 'FORBIDDEN');
const restricted_grant = randomUUID();
f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: restricted_grant, user_id: 4,
permissions: ['bot.chat'], expires_at: Date.now() + 10_000});
const excessive = await f.connect();
code(await ack(excessive, 'join_room', {token: token(f.s, 'web', restricted_grant,
Date.now() + 10_000, 4, ['execute_function'])}), 'FORBIDDEN');
assert.equal(calls, 0);
} finally {await f.close();}
});
test('Node replacement cancels pending, ignores late ACK, resets readiness, and old disconnect cannot clear new Node', async () => {
const f = await fixture();
try {
const old = await f.node(); const web = await f.web();
let late: (value: unknown) => void;
const received = new Promise<void>(resolve => old.on('relay_request', (_value, reply) => {late = reply; resolve();}));
const response = ack(web.socket, 'relay_request', request('bot.snapshot'));
await received;
const replacement = await f.connect();
assert.equal((await ack(replacement, 'join_room', {token: token(f.s, 'node')})).status, 'success');
code(await response, 'NODE_OFFLINE');
late!(snapshot(f.s.bot_id, 999));
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'NODE_OFFLINE');
replacement.on('relay_request', (_value, reply) => reply(snapshot(f.s.bot_id, 2)));
await ack(replacement, 'forward_node_ready', {forward_session_id: f.s.forward_session_id});
old.disconnect();
assert.equal((await ack(web.socket, 'relay_request', request('bot.snapshot'))).data.seq, 2);
} finally {await f.close();}
});
test('tab_complete dispatch rejects ranges beyond request text and accepts exact boundaries', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web();
let range = {start: 0, length: 0};
node.on('relay_request', (_value, reply) => reply({status: 'success', data: {matches: ['/help'], ...range}}));
for (const [text, start, length] of [['', 0, 0], ['/h', 0, 2], ['/h', 2, 0], ['界'.repeat(256), 128, 128]] as const) {
range = {start, length};
assert.deepEqual(await ack(web.socket, 'relay_request', {method: 'tab_complete', payload: {text}, request_id: randomUUID()}),
{status: 'success', data: {matches: ['/help'], start, length}});
}
for (const [start, length] of [[2, 0], [1, 1], [0, 2]]) {
range = {start, length};
code(await ack(web.socket, 'relay_request', {method: 'tab_complete', payload: {text: '/'}, request_id: randomUUID()}), 'INVALID_REQUEST');
}
} finally {await f.close();}
});
test('bot_event is rejected while bot_log checks permission, scope and sequence', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web();
const limited = await f.web(randomUUID(), ['bot.subscribe']);
await ack(web.socket, 'relay_request', request('bot.subscribe', {events: ['status_update', 'log']}));
await ack(limited.socket, 'relay_request', request('bot.subscribe', {events: ['status_update']}));
let limited_events = 0; limited.socket.on('bot_event', () => limited_events++);
const stream = randomUUID();
const event = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id, context_generation: null as string | null,
stream_id: stream, seq: 1, emitted_at: Date.now(), name: 'status_update', payload: {status: 'offline', context_exists: false}};
let web_events = 0; web.socket.on('bot_event', () => web_events++);
code(await ack(node, 'bot_event', event), 'FORBIDDEN');
code(await ack(web.socket, 'bot_event', {...event, seq: 2}), 'FORBIDDEN');
code(await ack(web.socket, 'forward_open_session', {}), 'FORBIDDEN');
node.emit('bot_event', {...event, seq: 3});
node.on('relay_request', (_value, reply) => reply(snapshot(f.s.bot_id)));
assert.equal((await ack(web.socket, 'relay_request', request('bot.snapshot'))).status, 'success');
assert.equal(web_events, 0);
const log = {...event, name: 'log', payload: {records: [] as never[]}};
let limited_logs = 0; limited.socket.on('bot_log', () => limited_logs++);
const delivered = received(web.socket, 'bot_log');
assert.equal((await ack(node, 'bot_log', log)).status, 'success');
assert.deepEqual((await delivered)[0], log);
code(await ack(node, 'bot_log', log), 'INVALID_REQUEST');
code(await ack(web.socket, 'bot_log', {...log, seq: 2}), 'FORBIDDEN');
code(await ack(node, 'bot_log', {...log, seq: 2, bot_id: f.s.bot_id + 1}), 'FORBIDDEN');
code(await ack(node, 'bot_log', {...event, seq: 2}), 'INVALID_REQUEST');
assert.equal(limited_logs, 0);
assert.equal(limited_events, 0);
} finally {await f.close();}
});
test('get_context_info maps to snapshot permissions and validates Node ACKs', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web(randomUUID(), ['bot.snapshot']);
const limited = await f.web(randomUUID(), ['bot.logs']);
let calls = 0;
let response: unknown = {status: 'success', data: [{name: 'health',
i18n: {lang: 'en', name: 'Health', description: 'Current HP'}, value: {current: 20}}]};
node.on('relay_request', (value, reply) => {
calls++;
assert.equal(value.method, 'get_context_info');
assert.deepEqual(value.payload, {});
assert.equal(value.forward_session_id, f.s.forward_session_id);
reply(response);
});
assert.deepEqual(await ack(web.socket, 'relay_request', request('get_context_info')), response);
code(await ack(limited.socket, 'relay_request', request('get_context_info')), 'FORBIDDEN');
code(await ack(node, 'relay_request', request('get_context_info')), 'FORBIDDEN');
code(await ack(web.socket, 'relay_request', request('get_context_info', {name: 'health'})), 'INVALID_REQUEST');
assert.equal(calls, 1);
response = {status: 'success', data: [{name: 'health', value: 20}]};
code(await ack(web.socket, 'relay_request', request('get_context_info')), 'INVALID_REQUEST');
await f.node(['bot.logs']);
code(await ack(web.socket, 'relay_request', request('get_context_info')), 'FORBIDDEN');
} finally {await f.close();}
});
test('completed get_context_info IDs recheck Node online state, readiness and permissions', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web();
let calls = 0;
node.on('relay_request', (_value, reply) => {
calls++;
reply(calls === 1 ? {status: 'success', data: []}
: {status: 'error', data: {code: 'BOT_NOT_ONLINE', message: 'Offline', retryable: false}});
});
const read = request('get_context_info');
assert.deepEqual(await ack(web.socket, 'relay_request', read), {status: 'success', data: []});
code(await ack(web.socket, 'relay_request', read), 'BOT_NOT_ONLINE');
assert.equal(calls, 2);
const replacement = await f.node(permissions, false);
code(await ack(web.socket, 'relay_request', read), 'NODE_OFFLINE');
replacement.on('relay_request', (_value, reply) => reply({status: 'success', data: [{name: 'health',
i18n: {lang: '', name: '', description: ''}, value: 10}]}));
await ack(replacement, 'forward_node_ready', {forward_session_id: f.s.forward_session_id});
assert.equal((await ack(web.socket, 'relay_request', read)).data[0].value, 10);
code(await ack(web.socket, 'relay_request', request('bot.snapshot', {}, read.request_id)), 'INVALID_REQUEST');
const restricted = await f.node(['bot.logs']);
code(await ack(web.socket, 'relay_request', read), 'FORBIDDEN');
const server_node = f.namespace.sockets.get(restricted.id!)!;
const disconnected = once(server_node, 'disconnect'); restricted.disconnect(); await disconnected;
code(await ack(web.socket, 'relay_request', read), 'NODE_OFFLINE');
} finally {await f.close();}
});
test('get_context_info duplicate pending and reused payload checks survive fresh dispatch', async () => {
const f = await fixture({max_dedupe_entries: 1});
try {
const node = await f.node(); const web = await f.web();
let calls = 0;
let reply!: (value: unknown) => void;
const incoming = received(node, 'relay_request');
node.on('relay_request', (_value, callback) => {calls++; reply = callback;});
const read = request('get_context_info');
const first = ack(web.socket, 'relay_request', read);
await incoming;
code(await ack(web.socket, 'relay_request', read), 'RATE_LIMITED');
code(await ack(web.socket, 'relay_request', request('bot.snapshot', {}, read.request_id)), 'INVALID_REQUEST');
reply({status: 'success', data: []}); await first;
const repeated = received(node, 'relay_request');
const second = ack(web.socket, 'relay_request', read); await repeated;
code(await ack(web.socket, 'relay_request', read), 'RATE_LIMITED');
reply({status: 'success', data: []}); await second;
assert.equal(calls, 2);
const cached = request('bot.snapshot');
node.removeAllListeners('relay_request');
node.on('relay_request', (_value, callback) => callback(snapshot(f.s.bot_id)));
await ack(web.socket, 'relay_request', cached);
code(await ack(web.socket, 'relay_request', request('get_context_info', {}, cached.request_id)), 'INVALID_REQUEST');
} finally {await f.close();}
});
test('info_update auto-pushes exact envelopes and shares log stream and sequence state', async () => {
const f = await fixture();
try {
const node = await f.node(); const web = await f.web(randomUUID(), ['bot.snapshot']);
const logs = await f.web(randomUUID(), ['bot.logs']);
let info_leaks = 0; let log_leaks = 0;
logs.socket.on('info_update', () => info_leaks++);
web.socket.on('bot_log', () => log_leaks++);
const update = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: randomUUID(), stream_id: randomUUID(), seq: 1, emitted_at: Date.now(),
data: {name: 'health', value: {current: 20}}};
const log = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id, context_generation: null as string | null,
stream_id: update.stream_id, seq: 2, emitted_at: Date.now(), name: 'log', payload: {records: [] as BotLogRecord[]}};
const first = received(web.socket, 'info_update');
assert.equal((await ack(node, 'info_update', update)).status, 'success');
assert.deepEqual((await first)[0], update);
const next = received(logs.socket, 'bot_log');
assert.equal((await ack(node, 'bot_log', log)).status, 'success');
assert.deepEqual((await next)[0], log);
code(await ack(node, 'info_update', {...update, seq: 2}), 'INVALID_REQUEST');
const third = received(web.socket, 'info_update');
node.emit('info_update', {...update, seq: 3});
assert.equal((await third)[0].seq, 3);
code(await ack(node, 'bot_log', {...log, seq: 3}), 'INVALID_REQUEST');
code(await ack(web.socket, 'info_update', {...update, seq: 4}), 'FORBIDDEN');
for (const invalid of [{...update, seq: 4, context_generation: null as string | null}, {...update, seq: 4, name: 'info_update'},
{...update, seq: 4, payload: update.data}]) {
code(await ack(node, 'info_update', invalid), 'INVALID_REQUEST');
}
for (const invalid of [{...update, seq: 4, bot_id: f.s.bot_id + 1},
{...update, seq: 4, forward_session_id: randomUUID()}]) {
code(await ack(node, 'info_update', invalid), 'FORBIDDEN');
}
const replacement = {...update, stream_id: randomUUID(), seq: 0};
assert.equal((await ack(node, 'info_update', replacement)).status, 'success');
code(await ack(node, 'bot_log', {...log, seq: 4}), 'RESYNC_REQUIRED');
assert.equal(info_leaks, 0); assert.equal(log_leaks, 0);
} finally {await f.close();}
});
test('info_update enforces readiness and Node and Web token permissions independently of subscriptions', async () => {
const f = await fixture();
try {
const node = await f.node(permissions, false);
const web = await f.web();
const update = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: randomUUID(), stream_id: randomUUID(), seq: 1, emitted_at: Date.now(),
data: {name: 'health', value: 20}};
code(await ack(node, 'info_update', update), 'NODE_OFFLINE');
const restricted = await f.node(['bot.logs']);
code(await ack(restricted, 'info_update', update), 'FORBIDDEN');
const full = await f.node();
const narrow = await f.connect();
assert.equal((await ack(narrow, 'join_room', {token: token(f.s, 'web', web.grant_id,
Date.now() + 10_000, 4, ['bot.logs'])})).status, 'success');
let leaked = 0; narrow.on('info_update', () => leaked++);
code(await ack(narrow, 'relay_request', request('get_context_info')), 'FORBIDDEN');
assert.equal((await ack(full, 'info_update', update)).status, 'success');
const allowed = await f.web(randomUUID(), ['bot.snapshot']);
const pushed = received(allowed.socket, 'info_update');
await ack(full, 'info_update', {...update, seq: 2}); await pushed;
assert.equal(leaked, 0);
} finally {await f.close();}
});
test('logs and info updates share the Node event rate and slow-Web buffer protection', async () => {
for (const first_info of [false, true]) {
const f = await fixture({events_per_second: 2, max_buffered_packets: 2});
try {
const node = await f.node(); const web = await f.web();
const info = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: randomUUID(), stream_id: randomUUID(), seq: 1, emitted_at: Date.now(),
data: {name: 'health', value: 20}};
const log = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: null as string | null, stream_id: info.stream_id, seq: 2, emitted_at: Date.now(),
name: 'log', payload: {records: [] as BotLogRecord[]}};
const server_web = f.namespace.sockets.get(web.socket.id!)!;
server_web.conn.transport.writable = false;
assert.equal((await ack(node, first_info ? 'info_update' : 'bot_log', first_info ? info : log)).status, 'success');
assert.equal((await ack(node, first_info ? 'bot_log' : 'info_update',
first_info ? log : {...info, seq: 3})).status, 'success');
const closed = received(node, 'forward_session_closed');
node.emit('info_update', {...info, seq: 4});
assert.equal((await closed)[0].code, 'RATE_LIMITED');
} finally {await f.close();}
}
const f = await fixture({max_buffered_packets: 2});
try {
const node = await f.node(); const web = await f.web(randomUUID(), ['bot.snapshot']);
const server_web = f.namespace.sockets.get(web.socket.id!)!;
server_web.conn.transport.writable = false;
const info = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: randomUUID(), stream_id: randomUUID(), seq: 1, emitted_at: Date.now(),
data: {name: 'health', value: 20}};
for (let seq = 1; seq <= 3; seq++) assert.equal((await ack(node, 'info_update', {...info, seq})).status, 'success');
assert.equal(server_web.connected, false);
assert.ok((server_web.conn as unknown as {writeBuffer: unknown[]}).writeBuffer.length <= 4);
} finally {await f.close();}
});
test('async join awaits the adapter and rechecks revoked grant before binding', async () => {
const f = await fixture();
try {
const grant_id = randomUUID();
f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id, user_id: 4, permissions, expires_at: Date.now() + 10_000});
const client = await f.connect();
const socket = f.namespace.sockets.get(client.id!)!;
const original = socket.join.bind(socket);
let release!: () => void;
let entered!: () => void;
const joining = new Promise<void>(resolve => {entered = resolve;});
socket.join = async room => {entered(); await new Promise<void>(resolve => {release = resolve;}); await original(room);};
let completed = false;
const joined = ack(client, 'join_room', {token: token(f.s, 'web', grant_id)}).then(value => {completed = true; return value;});
await joining;
assert.equal(completed, false);
code(await ack(client, 'reauth', {token: token(f.s, 'web', grant_id)}), 'INVALID_REQUEST');
f.store.revoke_grant({forward_session_id: f.s.forward_session_id, grant_id, reason: 'revoked_during_join'});
release();
await joined.catch((): void => {});
await delay(0);
assert.equal(socket.rooms.size, 0);
} finally {await f.close();}
});
test('grant connection replacement and immutable identity, control disconnect allows reads but blocks joins, reauth, chat', async () => {
const f = await fixture();
try {
const node = await f.node(); node.on('relay_request', (_value, reply) => reply(snapshot(f.s.bot_id)));
const web = await f.web();
const old_closed = received(web.socket, 'forward_session_closed');
const replacement = await f.connect();
const credential = token(f.s, 'web', web.grant_id);
const joined = await ack(replacement, 'join_room', {token: credential});
assert.equal(joined.status, 'success'); assert.equal((await old_closed)[0].reason, 'grant_connection_replaced');
assert.deepEqual(await ack(replacement, 'join_room', {token: credential}), joined);
code(await ack(replacement, 'reauth', {token: token(f.s, 'web', web.grant_id, Date.now() + 10_000, 5)}), 'FORBIDDEN');
f.store.set_control_available(false);
assert.equal((await ack(replacement, 'relay_request', request('bot.snapshot'))).status, 'success');
code(await ack(replacement, 'relay_request', request('bot.chat', {message: 'no'})), 'RELAY_UNAVAILABLE');
code(await ack(replacement, 'reauth', {token: credential}), 'RELAY_UNAVAILABLE');
const another = await f.connect(); code(await ack(another, 'join_room', {token: credential}), 'RELAY_UNAVAILABLE');
} finally {await f.close();}
});
test('timeouts, pending caps and invalid Node responses return one strict ACK', async () => {
const f = await fixture({max_pending_per_web: 1});
try {
const node = await f.node(); const web = await f.web();
let started!: () => void;
const received = new Promise<void>(resolve => {started = resolve;});
node.on('relay_request', () => started());
const pending = ack(web.socket, 'relay_request', request('bot.snapshot'));
await received;
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'RATE_LIMITED');
code(await pending, 'TIMEOUT');
node.removeAllListeners('relay_request');
node.on('relay_request', (_value, reply) => reply({status: 'success', data: {bot_id: 999}}));
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'INVALID_REQUEST');
node.removeAllListeners('relay_request');
node.on('relay_request', (_value, reply) => reply({status: 'success', data: {stream_id: randomUUID(), seq: 2,
truncated: false, records: [1, 2].map((seq): BotLogRecord => ({id: randomUUID(), timestamp: Date.now(),
context_generation: null, seq, segments: []}))}}));
code(await ack(web.socket, 'relay_request', request('bot.logs', {limit: 1})), 'INVALID_REQUEST');
assert.equal((f.namespace.sockets.get(node.id!)! as unknown as {acks: Map<number, unknown>}).acks.size, 0);
} finally {await f.close();}
});
test('grant renewal does not silently extend socket lease and revoke releases pending transport timers', async () => {
const f = await fixture();
try {
const node = await f.node(); const expires_at = Date.now() + 180;
const web = await f.web(randomUUID(), permissions, expires_at);
f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: web.grant_id, user_id: 4, permissions, expires_at: Date.now() + 10_000});
const closed = received(web.socket, 'forward_session_closed');
assert.equal((await closed)[0].code, 'TOKEN_EXPIRED');
const renewed = await f.web();
let started!: () => void; const request_received = new Promise<void>(resolve => {started = resolve;});
node.on('relay_request', () => started());
const response = ack(renewed.socket, 'relay_request', request('bot.snapshot'));
await request_received;
f.store.revoke_session({forward_session_id: f.s.forward_session_id, reason: 'ownership_changed'});
// The close notification may terminate the client before its pending ACK arrives.
await response.catch((): void => {});
assert.equal(f.store.size, 0);
assert.equal((f.namespace.sockets.get(node.id!) as unknown as {acks?: Map<number, unknown>})?.acks?.size ?? 0, 0);
} finally {await f.close();}
});
test('initial-ready unused and final-Web idle reclamation release sessions with explicit reason', async () => {
const unused = await fixture({unused_timeout_ms: 45});
try {
const node = await unused.node();
const closed = await received(node, 'forward_session_closed');
assert.equal(closed[0].reason, 'unused_session'); assert.equal(unused.store.size, 0);
} finally {await unused.close();}
const idle = await fixture({idle_timeout_ms: 45});
try {
const node = await idle.node(); const web = await idle.web();
const closed = received(node, 'forward_session_closed'); web.socket.disconnect();
assert.equal((await closed)[0].reason, 'last_web_left'); assert.equal(idle.store.size, 0);
} finally {await idle.close();}
});
test('slow-client status buffer overflow and forbidden event floods require explicit recovery', async () => {
const f = await fixture({requests_per_second: 6, max_buffered_packets: 2});
try {
const node = await f.node(); const web = await f.web();
await ack(web.socket, 'relay_request', request('bot.subscribe', {events: ['status_update']}));
const server_web = f.namespace.sockets.get(web.socket.id!)!;
const transport = server_web.conn as unknown as {writeBuffer: unknown[]};
const actual = transport.writeBuffer;
let checks = 0;
Object.defineProperty(transport, 'writeBuffer', {configurable: true, get: () => ++checks === 1 ? [null, null] : actual, set: () => {}});
const closed = received(web.socket, 'forward_session_closed');
const event = {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id, context_generation: null as string | null,
stream_id: randomUUID(), seq: 1, emitted_at: Date.now(), name: 'status_update', payload: {status: 'offline', context_exists: false}};
const replacement = await f.connect();
const first = ack(replacement, 'join_room', {token: token(f.s, 'node')});
await delay(10);
Object.defineProperty(transport, 'writeBuffer', {configurable: true, writable: true, value: actual});
await first;
// Notification is emitted before disconnect; packet delivery depends on Engine.IO's buffer flush.
assert.equal(server_web.connected, false);
await Promise.race([closed, delay(30)]);
code(await ack(replacement, 'bot_event', event), 'FORBIDDEN');
const node_closed = received(replacement, 'forward_session_closed');
for (let seq = 2; seq <= 10; seq++) replacement.emit('bot_event', {...event, seq});
assert.equal((await node_closed)[0].code, 'RATE_LIMITED');
} finally {await f.close();}
});
test('scope conflicts and route disposer preserve HTTP ownership', async () => {
const f = await fixture();
try {
assert.throws(() => f.store.open({...f.s, forward_session_id: randomUUID(), node_session_id: randomUUID(), lease_expires_at: Date.now() + 10_000}), {code: 'SESSION_STALE'});
assert.throws(() => f.store.revoke_session({forward_session_id: f.s.forward_session_id, reason: 'x'.repeat(257)}), {code: 'INVALID_REQUEST'});
const node = await f.node();
const extra = install_relay_routes(f.namespace, f.store);
assert.equal(f.namespace.listenerCount('connection'), 2);
extra(); assert.equal(f.namespace.listenerCount('connection'), 1);
assert.equal(node.connected, true);
const closed = received(node, 'forward_session_closed');
f.dispose();
assert.equal((await closed)[0].reason, 'relay_routes_closed');
assert.equal(f.namespace.listenerCount('connection'), 0);
assert.equal(f.http.listening, true);
} finally {await f.close();}
});
test('reauth extends only the same identity, reconnect restores subscriptions, revoked IDs cannot revive', async () => {
const f = await fixture();
try {
const node = await f.node();
const web = await f.web(randomUUID(), permissions, Date.now() + 180);
const subscribe = request('bot.subscribe', {events: ['status_update']});
await ack(web.socket, 'relay_request', subscribe);
const expiry = Date.now() + 3000;
f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: web.grant_id, user_id: 4, permissions, expires_at: expiry});
const renewed = await ack(web.socket, 'reauth', {token: token(f.s, 'web', web.grant_id, expiry)});
assert.equal(renewed.data.expires_at, expiry);
await delay(200);
assert.equal(web.socket.connected, true);
const replacement = await f.connect();
await ack(replacement, 'join_room', {token: token(f.s, 'web', web.grant_id, expiry)});
await ack(replacement, 'relay_request', subscribe);
code(await ack(node, 'bot_event', {forward_session_id: f.s.forward_session_id, bot_id: f.s.bot_id,
context_generation: null, stream_id: randomUUID(), seq: 1, emitted_at: Date.now(),
name: 'status_update', payload: {status: 'offline', context_exists: false}}), 'FORBIDDEN');
node.on('relay_request', (_value, reply) => reply(snapshot(f.s.bot_id)));
assert.equal((await ack(replacement, 'relay_request', request('bot.snapshot'))).status, 'success');
f.store.revoke_grant({forward_session_id: f.s.forward_session_id, grant_id: web.grant_id, reason: 'permission_removed'});
assert.throws(() => f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: web.grant_id,
user_id: 4, permissions, expires_at: expiry}), {code: 'FORBIDDEN'});
f.store.revoke_session({forward_session_id: f.s.forward_session_id, reason: 'closed'});
assert.throws(() => f.store.open({...f.s, lease_expires_at: expiry}), {code: 'SESSION_STALE'});
} finally {await f.close();}
});
test('async join cannot commit after session revocation or disconnect, successful join ACK follows room membership', async () => {
for (const action of ['revoke', 'disconnect', 'success']) {
const f = await fixture();
try {
const client = await f.connect();
const socket = f.namespace.sockets.get(client.id!)!;
const join = socket.join.bind(socket);
let release!: () => void;
let entered!: () => void;
const started = new Promise<void>(resolve => {entered = resolve;});
socket.join = async room => {entered(); await new Promise<void>(resolve => {release = resolve;}); await join(room);};
const joining = f.store.join(socket, {token: token(f.s, 'node')});
await started;
if (action === 'revoke') f.store.revoke_session({forward_session_id: f.s.forward_session_id, reason: 'revoked_while_joining'});
if (action === 'disconnect') {client.disconnect(); await once(socket, 'disconnect');}
release();
if (action === 'success') {
await joining;
assert.equal(socket.rooms.size, 2);
} else {
await assert.rejects(joining, {code: action === 'revoke' ? 'SESSION_STALE' : 'CANCELLED'});
await delay(0);
assert.equal(socket.rooms.size, 0);
}
} finally {await f.close();}
}
});
test('stalled async adapters are cancelled without waiting for join and late membership is removed', async () => {
for (const action of ['disconnect', 'timeout']) {
const f = await fixture({auth_timeout_ms: 45});
try {
const client = await f.connect();
const socket = f.namespace.sockets.get(client.id!)!;
const original = socket.join.bind(socket);
let release!: () => void;
let entered!: () => void;
const started = new Promise<void>(resolve => {entered = resolve;});
socket.join = async room => {entered(); await new Promise<void>(resolve => {release = resolve;}); await original(room);};
const joining = f.store.join(socket, {token: token(f.s, 'node')});
const rejected = assert.rejects(joining, {code: action === 'disconnect' ? 'CANCELLED' : 'UNAUTHORIZED'});
await started;
if (action === 'disconnect') client.disconnect();
await rejected;
release();
await delay(0);
assert.equal(socket.rooms.size, 0);
} finally {await f.close();}
}
});
test('capacity bounds reject new sessions, grants and sockets, including retained revocation records', async () => {
const f = await fixture({max_sessions: 1, max_grants_per_session: 1, max_connections: 1});
try {
assert.throws(() => f.store.open({...scope(), bot_id: 9, lease_expires_at: Date.now() + 10_000}), {code: 'RATE_LIMITED'});
const web = await f.web();
assert.throws(() => f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: randomUUID(),
user_id: 4, permissions, expires_at: Date.now() + 10_000}), {code: 'RATE_LIMITED'});
const closed = received(web.socket, 'forward_session_closed');
f.store.revoke_grant({forward_session_id: f.s.forward_session_id, grant_id: web.grant_id, reason: 'revoked'});
await closed;
assert.throws(() => f.store.authorize({forward_session_id: f.s.forward_session_id, grant_id: randomUUID(),
user_id: 4, permissions, expires_at: Date.now() + 10_000}), {code: 'RATE_LIMITED'});
const first = await f.connect();
const rejected = await f.connect();
assert.equal((await Promise.race([received(rejected, 'disconnect').then(() => true), delay(30).then(() => !rejected.connected)])), true);
assert.equal(first.connected, true);
} finally {await f.close();}
});
test('unauthenticated sockets and session leases expire, authentication is rate limited', async () => {
const f = await fixture({auth_timeout_ms: 40, requests_per_second: 2});
try {
const idle = await f.connect();
assert.equal((await received(idle, 'forward_session_closed'))[0].reason, 'authentication_timeout');
const node = await f.connect();
const credential = token(f.s, 'node');
await ack(node, 'join_room', {token: credential});
await ack(node, 'join_room', {token: credential});
code(await ack(node, 'join_room', {token: credential}), 'RATE_LIMITED');
assert.equal(f.namespace.sockets.get(node.id!)!.rooms.size, 2);
} finally {await f.close();}
const lease = await fixture();
try {
const short = {...scope(), node_id: 10};
lease.store.open({...short, lease_expires_at: Date.now() + 70});
const node = await lease.connect();
const joined = await ack(node, 'join_room', {token: token(short, 'node')});
assert.equal(joined.status, 'success');
assert.ok(joined.data.expires_at <= Date.now() + 70);
assert.equal((await received(node, 'forward_session_closed'))[0].code, 'TOKEN_EXPIRED');
await delay(10);
assert.equal(lease.store.size, 1);
} finally {await lease.close();}
});
test('slow Node RPC queues and unauthenticated unknown-event ACK queues stay bounded', async () => {
const f = await fixture({max_buffered_packets: 2, rpc_timeout_ms: 20});
try {
const node = await f.node(); const web = await f.web();
const server_node = f.namespace.sockets.get(node.id!)!;
server_node.conn.transport.writable = false;
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'TIMEOUT');
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'TIMEOUT');
code(await ack(web.socket, 'relay_request', request('bot.snapshot')), 'RESYNC_REQUIRED');
assert.equal(server_node.connected, false);
const raw = await f.connect();
const server_raw = f.namespace.sockets.get(raw.id!)!;
server_raw.conn.transport.writable = false;
for (let index = 0; index < 20; index++) raw.emit('unknown', {}, () => {});
await delay(20);
assert.equal(server_raw.connected, false);
// Two bounded replies plus the explicit close notification and disconnect packet.
assert.ok((server_raw.conn as unknown as {writeBuffer: unknown[]}).writeBuffer.length <= 4);
} finally {await f.close();}
});
+295
View File
@@ -0,0 +1,295 @@
import assert from 'node:assert/strict';
import {EventEmitter} from 'node:events';
import {randomUUID} from 'node:crypto';
import test from 'node:test';
import type {Socket} from 'socket.io';
import {RELAY_METHODS, WORKFLOW_ACTIONS} from '../types.ts';
import type {ForwardAck, JsonValue, NodeRelayRequest, RelayClaims, RelayLimits, RelayMethod, RelayRequest, WorkflowRunSnapshot} from '../types.ts';
import {create_session_store} from '../runtime/session_store.ts';
import {DEFAULT_LIMITS} from '../utils/relay_config.ts';
import {encode_token} from '../utils/token_utils.ts';
import {error_ack, success} from '../utils/relay_validation.ts';
class MockSocket extends EventEmitter {
connected = true;
conn = {writeBuffer: [] as unknown[]};
acks = new Map<number, (value: unknown) => void>();
rooms = new Set<string>();
requests: NodeRelayRequest[] = [];
respond: (request: NodeRelayRequest) => unknown = () => success(null);
private next_ack = 0;
join(room: string) {this.rooms.add(room);}
leave(room: string) {this.rooms.delete(room);}
disconnect() {this.connected = false; this.emit('disconnect');}
timeout(_ms: number) {return this;}
emitWithAck(event: string, request: NodeRelayRequest) {
assert.equal(event, 'relay_request');
this.requests.push(request);
return new Promise<unknown>((resolve, reject) => {
const id = this.next_ack++;
this.acks.set(id, resolve);
Promise.resolve().then(() => this.respond(request)).then(value => {
if (this.acks.delete(id)) resolve(value);
}, reject);
});
}
get socket(): Socket {return this as unknown as Socket;}
}
async function fixture(t: {after: (fn: () => void) => unknown}, overrides: Partial<RelayLimits> = {},
web_permissions: RelayMethod[] = [...RELAY_METHODS], node_permissions: RelayMethod[] = [...RELAY_METHODS]) {
const key = 'mock-session-test-secret-only';
const scope = {forward_id: 1, forward_session_id: randomUUID(), node_session_id: 'main-session', node_id: 2, bot_id: 3};
const store = create_session_store({forward_id: 1, key, limits: {...DEFAULT_LIMITS, ...overrides}, on_status() {}});
t.after(() => store.close());
store.open({...scope, lease_expires_at: Date.now() + 60_000});
const grant_id = randomUUID();
const expires_at = Date.now() + 50_000;
store.authorize({forward_session_id: scope.forward_session_id, grant_id, user_id: 4, permissions: web_permissions, expires_at});
const join = async (socket: MockSocket, role: 'node' | 'web', permissions: RelayMethod[]) => {
const now = Date.now();
const claims: RelayClaims = {...scope, v: 1, role, permissions, issued_at: now, join_before: now + 10_000,
expires_at, jti: randomUUID(), ...(role === 'web' ? {grant_id, user_id: 4} : {})};
store.attach(socket.socket);
await store.join(socket.socket, {token: encode_token(key, claims)});
};
const node = new MockSocket(); const web = new MockSocket();
await join(node, 'node', node_permissions);
store.node_ready(node.socket, {forward_session_id: scope.forward_session_id});
await join(web, 'web', web_permissions);
const request = async (method: RelayRequest['method'], payload: unknown = {}, request_id = randomUUID()): Promise<ForwardAck<unknown>> => {
try {return await store.request(web.socket, {method, payload, request_id});}
catch (error) {return error_ack(error);}
};
return {store, node, web, scope, grant_id, join, request};
}
function code(value: ForwardAck<unknown>, expected: string) {
assert.equal(value.status, 'error');
if (value.status === 'error') assert.equal(value.data.code, expected);
}
const control = () => ({command_id: randomUUID(), context_generation: randomUUID(), action: 'run_once', workflow_id: 'a'});
const command = (input: ReturnType<typeof control>) => ({command_id: input.command_id, action: input.action, workflow_id: input.workflow_id, state: 'accepted'});
test('mock relay forwards all workflow RPCs with private correlated deadlines and safe ACKs', async t => {
const f = await fixture(t);
const input = control(); const run_id = randomUUID();
const summary: WorkflowRunSnapshot = {run_id, workflow_id: 'a', status: 'completed', steps: 1, active_nodes: [],
started_at: 0, ended_at: 1, stop_reason: null, error: null, trigger_available: true};
const detail: WorkflowRunSnapshot = {...summary, trigger: {disabled: false, count: 0, empty: null, nested: {values: [0, false, null]}},
branches: [{trigger: {}, vars: {}, outputs: {}, inputs: {}, retry_counts: {}, last_result: null}]};
const trigger_detail = {...detail};
delete trigger_detail.branches;
const legacy = {...summary};
delete legacy.trigger_available;
const runtime = {source_config_revision: 0, workflows: [] as JsonValue[], commands: [command(input)]};
const catalog = {catalog_version: 1, nodes: [] as JsonValue[], start_nodes: [] as JsonValue[], events: [] as JsonValue[], queries: [] as JsonValue[],
common: {node_fields: [] as JsonValue[], policy_fields: [] as JsonValue[], result: {}, edge_input: {}, references: [] as JsonValue[]}};
const requests: Array<[RelayRequest['method'], unknown, unknown]> = [
['workflow.start', {workflow_id: 'a', trigger: {x: 1}}, {status: 'started', run_id}],
['workflow.stop', {run_id}, {stopped: true}], ['workflow.snapshot', {}, []],
['workflow.snapshot', {}, [summary]], ['workflow.snapshot', {run_id}, detail],
['workflow.snapshot', {run_id, include_branches: false}, trigger_detail],
['workflow.snapshot', {run_id, include_branches: true}, detail],
['workflow.snapshot', {run_id}, legacy],
['workflow.snapshot', {run_id}, null], ['workflow.catalog', {}, catalog],
['workflow.control', input, command(input)],
['workflow.command', {command_id: input.command_id, context_generation: input.context_generation}, command(input)],
['workflow.runtime', {}, runtime],
];
let received = 0;
f.web.on('relay_request', () => received++);
for (const [method, payload, data] of requests) {
f.node.respond = () => success(data);
const before = Date.now();
assert.deepEqual(await f.request(method, payload), success(data));
const forwarded = f.node.requests.at(-1)!;
assert.equal(forwarded.method, method);
assert.deepEqual(forwarded.payload, payload);
assert.equal(forwarded.forward_session_id, f.scope.forward_session_id);
assert.notEqual(forwarded.correlation_id, forwarded.request_id);
assert.ok(forwarded.deadline_at >= before && forwarded.deadline_at <= Date.now() + DEFAULT_LIMITS.rpc_timeout_ms);
}
for (const action of WORKFLOW_ACTIONS) {
const payload = action === 'stop_run' ? {command_id: randomUUID(), context_generation: randomUUID(), action, run_id}
: {...control(), action, expected_generation: 1, ...(action === 'run_once' ? {input: {value: true}} : {})};
const data = {command_id: payload.command_id, action, workflow_id: action === 'stop_run' ? null : 'a', state: 'accepted'};
f.node.respond = () => success(data);
assert.deepEqual(await f.request('workflow.control', payload), success(data));
assert.deepEqual(f.node.requests.at(-1)!.payload, payload);
}
assert.equal(received, 0);
assert.equal(f.node.acks.size, 0);
});
test('mock workflow read/write permissions apply to web grants and Node tokens', async t => {
for (const [web, node] of [
[['bot.snapshot'], [...RELAY_METHODS]], [['workflow.control'], [...RELAY_METHODS]],
[[...RELAY_METHODS], ['bot.snapshot']], [[...RELAY_METHODS], ['workflow.control']],
] as RelayMethod[][][]) {
const f = await fixture(t, {}, web, node);
for (const [method, payload] of [['workflow.start', {workflow_id: 'a'}], ['workflow.stop', {run_id: randomUUID()}],
['workflow.control', control()]] as const) {
f.node.respond = request => success(method === 'workflow.start' ? {status: 'skipped'} : method === 'workflow.stop' ? {stopped: true}
: command(request.payload as ReturnType<typeof control>));
const result = await f.request(method, payload);
if (web.includes('workflow.control') && node.includes('workflow.control')) assert.equal(result.status, 'success');
else code(result, 'FORBIDDEN');
}
for (const method of ['workflow.catalog', 'workflow.snapshot', 'workflow.command', 'workflow.runtime'] as const) {
const before = f.node.requests.length;
const payload = method === 'workflow.command' ? {command_id: randomUUID(), context_generation: randomUUID()} : {};
await f.request(method, payload);
assert.equal(f.node.requests.length - before, web.includes('bot.snapshot') && node.includes('bot.snapshot') ? 1 : 0);
}
}
});
test('mock execute_function workflow category requires both capabilities including on Node', async t => {
for (const [web, node] of [
[['execute_function'], [...RELAY_METHODS]], [['workflow.control'], [...RELAY_METHODS]],
[[...RELAY_METHODS], ['execute_function']], [[...RELAY_METHODS], ['workflow.control']],
[['execute_function', 'workflow.control'], ['execute_function', 'workflow.control']],
] as RelayMethod[][][]) {
const f = await fixture(t, {}, web, node);
f.node.respond = () => success(true);
for (const category of ['workflow', 'workflow/nested']) {
const result = await f.request('execute_function', {function: category, name: 'run_once', args: []});
if ([web, node].every(permissions => permissions.includes('execute_function') && permissions.includes('workflow.control')))
assert.deepEqual(result, success(true));
else code(result, 'FORBIDDEN');
}
const result = await f.request('execute_function', {function: 'movement', name: 'go', args: []});
if (web.includes('execute_function') && node.includes('execute_function')) assert.deepEqual(result, success(true));
else code(result, 'FORBIDDEN');
}
});
test('mock workflow mutations cannot replay and reads refresh with current Node permission', async t => {
const f = await fixture(t);
for (const [method, payload, data] of [
['workflow.start', {workflow_id: 'a'}, {status: 'skipped'}], ['workflow.stop', {run_id: randomUUID()}, {stopped: true}],
['workflow.control', control(), null],
] as const) {
f.node.respond = request => success(method === 'workflow.control' ? command(request.payload as ReturnType<typeof control>) : data);
const id = randomUUID();
assert.equal((await f.request(method, payload, id)).status, 'success');
const before = f.node.requests.length;
code(await f.request(method, payload, id), 'CANCELLED');
assert.equal(f.node.requests.length, before);
}
const id = randomUUID(); let revision = 0;
f.node.respond = () => success({source_config_revision: revision++, workflows: [], commands: []});
assert.deepEqual(await f.request('workflow.runtime', {}, id), success({source_config_revision: 0, workflows: [], commands: []}));
assert.deepEqual(await f.request('workflow.runtime', {}, id), success({source_config_revision: 1, workflows: [], commands: []}));
const replacement = new MockSocket();
await f.join(replacement, 'node', ['workflow.control']);
f.store.node_ready(replacement.socket, {forward_session_id: f.scope.forward_session_id});
code(await f.request('workflow.runtime', {}, id), 'FORBIDDEN');
assert.equal(replacement.requests.length, 0);
});
test('mock control availability, method rate limits and malformed context prevent dispatch', async t => {
const f = await fixture(t);
f.store.set_control_available(false);
for (const [method, payload] of [['workflow.start', {workflow_id: 'a'}], ['workflow.stop', {run_id: randomUUID()}],
['workflow.control', control()]] as const) code(await f.request(method, payload), 'RELAY_UNAVAILABLE');
f.node.respond = () => success({source_config_revision: 0, workflows: [], commands: []});
assert.equal((await f.request('workflow.runtime')).status, 'success');
f.store.set_control_available(true);
const before = f.node.requests.length;
code(await f.request('workflow.control', {...control(), context_generation: 'bad'}), 'INVALID_REQUEST');
code(await f.request('workflow.command', {command_id: randomUUID()}), 'INVALID_REQUEST');
for (const include_branches of [false, true]) {
code(await f.request('workflow.snapshot', {include_branches}), 'INVALID_REQUEST');
}
for (const include_branches of [null, 0, 1, 'false', 'true', [], {}]) {
code(await f.request('workflow.snapshot', {run_id: randomUUID(), include_branches}), 'INVALID_REQUEST');
}
for (const action of WORKFLOW_ACTIONS) {
const payload = action === 'stop_run' ? {command_id: randomUUID(), context_generation: randomUUID(), action, run_id: randomUUID()}
: {...control(), action};
for (const [field, value] of Object.entries({workflow_id: 'a', run_id: randomUUID(), expected_generation: 1, input: {value: true}})) {
const allowed = field === 'workflow_id' || field === 'expected_generation' ? action !== 'stop_run'
: field === 'run_id' ? action === 'stop_run' : action === 'run_once';
if (!allowed) code(await f.request('workflow.control', {...payload, [field]: value}), 'INVALID_REQUEST');
}
}
assert.equal(f.node.requests.length, before);
const limited = await fixture(t, {requests_per_second: 1});
limited.node.respond = f.node.respond;
// Authentication consumes this connection's first request in the same rate window.
code(await limited.request('workflow.runtime'), 'RATE_LIMITED');
assert.equal(limited.node.requests.length, 0);
});
test('mock relay rejects malformed and mismatched workflow ACKs without admitting domain envelope errors', async t => {
const f = await fixture(t); const input = control();
for (const data of [{...command(input), command_id: randomUUID()}, {...command(input), action: 'stop'},
{...command(input), workflow_id: 'other'}, {...command(input), state: 'invalid'}]) {
f.node.respond = () => success(data);
code(await f.request('workflow.control', input), 'INVALID_REQUEST');
}
f.node.respond = () => success({...command(input), command_id: randomUUID()});
code(await f.request('workflow.command', {command_id: input.command_id, context_generation: input.context_generation}), 'INVALID_REQUEST');
f.node.respond = () => success(null);
code(await f.request('workflow.snapshot'), 'INVALID_REQUEST');
f.node.respond = () => success([]);
code(await f.request('workflow.snapshot', {run_id: randomUUID()}), 'INVALID_REQUEST');
const run: WorkflowRunSnapshot = {run_id: randomUUID(), workflow_id: 'a', status: 'running', steps: 0, active_nodes: [],
started_at: 0, ended_at: null, stop_reason: null, error: null, trigger_available: true, trigger: {value: false}, branches: []};
f.node.respond = () => success(run);
code(await f.request('workflow.snapshot', {run_id: randomUUID()}), 'INVALID_REQUEST');
code(await f.request('workflow.snapshot'), 'INVALID_REQUEST');
f.node.respond = () => success([run]);
code(await f.request('workflow.snapshot', {run_id: run.run_id}), 'INVALID_REQUEST');
for (const data of [{...run, trigger: {value: 'x'.repeat(65_536)}}, {...run, trigger: []},
{...run, trigger: {value: NaN}}, {...run, trigger: JSON.parse('{"nested":{"constructor":1}}')},
{...run, trigger_available: false}]) {
f.node.respond = () => success(data);
code(await f.request('workflow.snapshot', {run_id: run.run_id}), 'INVALID_REQUEST');
}
f.node.respond = () => ({status: 'error', data: {code: 'WORKFLOW_NOT_FOUND', message: 'missing', retryable: false}});
code(await f.request('workflow.runtime'), 'INVALID_REQUEST');
f.node.respond = () => ({status: 'error', data: {code: 'INVALID_REQUEST', message: 'missing', retryable: false}});
code(await f.request('workflow.runtime'), 'INVALID_REQUEST');
});
test('mock deadlines, grant revocation and Node replacement release pending workflow ACKs', async t => {
for (const mode of ['timeout', 'revoke', 'replace', 'close'] as const) {
const f = await fixture(t);
t.mock.timers.enable({apis: ['setTimeout']});
let release!: (value: unknown) => void;
f.node.respond = () => new Promise(resolve => {release = resolve;});
const input = control(); const id = randomUUID();
const result = f.request('workflow.control', input, id);
await Promise.resolve();
if (mode === 'timeout') t.mock.timers.tick(DEFAULT_LIMITS.rpc_timeout_ms);
else if (mode === 'revoke') f.store.revoke_grant({forward_session_id: f.scope.forward_session_id, grant_id: f.grant_id, reason: 'revoked'});
else if (mode === 'replace') await f.join(new MockSocket(), 'node', [...RELAY_METHODS]);
else f.store.close();
code(await result, mode === 'timeout' ? 'TIMEOUT' : mode === 'revoke' ? 'CANCELLED' : mode === 'replace' ? 'NODE_OFFLINE' : 'RELAY_UNAVAILABLE');
assert.equal(f.node.acks.size, 0);
release(success(command(input)));
await Promise.resolve();
if (mode === 'timeout') code(await f.request('workflow.control', input, id), 'CANCELLED');
t.mock.timers.reset();
}
});
test('mock workflow_states info updates fan out only to snapshot readers', async t => {
const f = await fixture(t, {}, ['bot.snapshot']);
const received: unknown[] = [];
f.web.on('info_update', value => received.push(value));
const summary: WorkflowRunSnapshot = {run_id: randomUUID(), workflow_id: 'a', status: 'completed', steps: 1, active_nodes: [],
started_at: 0, ended_at: 1, stop_reason: null, error: null, trigger_available: true};
const update = {forward_session_id: f.scope.forward_session_id, bot_id: f.scope.bot_id, context_generation: randomUUID(),
stream_id: randomUUID(), seq: 1, emitted_at: Date.now(), data: {name: 'workflow_states', value: {available: true,
runs: [summary]}}};
assert.deepEqual(f.store.info(f.node.socket, update), {accepted: true});
assert.deepEqual(received, [update]);
const writer = await fixture(t, {}, ['workflow.control']);
let leaked = false;
writer.web.on('info_update', () => leaked = true);
writer.store.info(writer.node.socket, {...update, forward_session_id: writer.scope.forward_session_id});
assert.equal(leaked, false);
});
+756
View File
@@ -0,0 +1,756 @@
import assert from 'node:assert/strict';
import {createCipheriv, createHash} from 'node:crypto';
import {test} from 'node:test';
import {RELAY_METHODS, WORKFLOW_ACTIONS, WORKFLOW_METHODS} from '../types.ts';
import type {BotEvent, JsonValue, RelayClaims, RelayLimits, WorkflowRunSnapshot, WorkflowRuntimeState} from '../types.ts';
import {decode_token, encode_token, verify_token} from '../utils/token_utils.ts';
import {DEFAULT_LIMITS} from '../utils/relay_config.ts';
import {
RelayError, check_permissions, check_size, error_ack, fail, is_record, is_uuid,
success, validate_event, validate_grant, validate_info_update, validate_node_ack, validate_open, validate_request, validate_scope,
} from '../utils/relay_validation.ts';
const secret = 'relay-test-key-not-a-production-secret';
const uuid = 'a89acd9a-0ab3-44ac-812e-e221a5b915e0';
const other_uuid = '45264c09-7529-478f-b3ef-6a84bec8c1a7';
const now = 1_800_000_000_000;
const limits: RelayLimits = {
auth_timeout_ms: 10_000, close_timeout_ms: 5_000, rpc_timeout_ms: 5_000, unused_timeout_ms: 60_000,
idle_timeout_ms: 30_000, max_token_age_ms: 300_000, join_window_ms: 60_000,
clock_skew_ms: 1_000, max_lease_ms: 300_000, max_token_bytes: 4096,
max_payload_bytes: 256 * 1024, max_chat_length: 256, max_connections: 100,
max_sessions: 100, max_grants_per_session: 100, max_pending_per_web: 8,
max_pending_total: 100, requests_per_second: 20, events_per_second: 100,
max_dedupe_entries: 100, dedupe_ttl_ms: 30_000, max_buffered_packets: 100,
};
function claims(overrides: Record<string, unknown> = {}): RelayClaims {
return {
v: 1, role: 'web', forward_id: 0, forward_session_id: uuid,
node_session_id: 'mZAb09_-SocketIOmainID', node_id: 1, bot_id: 2,
user_id: 3, grant_id: other_uuid, permissions: ['bot.snapshot', 'bot.logs'],
issued_at: now, join_before: now + 60_000, expires_at: now + 300_000,
jti: other_uuid, ...overrides,
} as RelayClaims;
}
function rejected(action: () => unknown, code = 'INVALID_REQUEST'): void {
assert.throws(action, (error: unknown) => error instanceof RelayError && error.code === code);
}
function verify(value: unknown, at = now, settings = limits): RelayClaims {
return verify_token(secret, encode_token(secret, value), 0, settings, at);
}
function request(method: string, payload: unknown): unknown {
return {request_id: uuid, method, payload};
}
function record(): Record<string, unknown> {
return {id: uuid, timestamp: now, context_generation: null, seq: 1,
segments: [{message: 'hello', font_color: '#ffffff', font_style: ['normal', 'bold']}]};
}
function event(name: string, payload: unknown): unknown {
return {forward_session_id: uuid, bot_id: 2, context_generation: null,
stream_id: other_uuid, seq: 1, emitted_at: now, name, payload};
}
test('AES-256-GCM wire format remains IV[12] + tag[16] + ciphertext', () => {
const value = claims();
const iv = Buffer.from('000102030405060708090a0b', 'hex');
const cipher = createCipheriv('aes-256-gcm', createHash('sha256').update(secret).digest(), iv);
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(value)), cipher.final()]);
const token = Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString('base64url');
assert.equal(token, 'AAECAwQFBgcICQoLKReCzFKlEY6g42nAQLV9_x0ByGStIbnU7jVnJBA372eKB2zodLxLE9mBUS1agWpMmTc7swZYqNfSWy53I7FUGk6-ik0it6dgU_Sr5B4xex5F89Dl89k2hg2wkenWZpeIksBqjx2kPX2vj1o05iSueFxOXLopUmDFt7SWUFLzsQUdE5Q1nyTH7cWi71k5S5Fmc-Fo9qetCUp3QDyMPaD5Bn4sTrmUoSztp1AHREowEAMFMNAhjQ5Y5-aVDg08biMmw9X2KbBMukqomk5t-cHk0-l6r2XudPdPxH6B4YFhfE8iqIPnekUXz-GyCwuOODvdPajUKQs48s13OCVyhW5WzvxsoimN1KrM0ac0S1ybB9aRvkXSbkDHuEfy9Q-NCcddJacIKjIpFqeIrRV0EJAdCaMirNPIer_5F4YvmJB9H5UKNGSg4mZ-DYkvmXsvYAleGpSJzIpwjWG_pXb8dbjltryrQQ5HbXSIvs_8GBVGZdROTmwXCOrI_ZS8azkFA29zrHRBHiz-H3Rs0bv0GUgfZPoz_eJWxyA');
assert.deepEqual(decode_token(secret, token), value);
assert.deepEqual(verify_token(secret, token, 0, limits, now), value);
assert.deepEqual(decode_token(secret, encode_token(secret, value)), value);
assert.notEqual(encode_token(secret, value), encode_token(secret, value));
});
test('tokens enforce canonical base64url, byte limit, authentication and audience', () => {
const token = encode_token(secret, claims());
for (const input of ['', token + '=', token + '\n', ' ' + token, token + '+', 'A', '_'.repeat(40)]) {
rejected(() => verify_token(secret, input, 0, limits, now), 'UNAUTHORIZED');
}
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
// GCM token ends in unused base64 bits: accepting an alias would allow multiple wire IDs.
let short_token = encode_token(secret, {a: 1});
if (short_token.length % 4 === 0) short_token = encode_token(secret, {a: 12});
const last_index = alphabet.indexOf(short_token.at(-1)!);
const alias = short_token.slice(0, -1) + alphabet[last_index + 1];
assert.deepEqual(Buffer.from(alias, 'base64url'), Buffer.from(short_token, 'base64url'));
assert.throws(() => decode_token(secret, alias));
const raw = Buffer.from(token, 'base64url');
for (const offset of [0, 12, 28, raw.length - 1]) {
const modified = Buffer.from(raw);
modified[offset] ^= 1;
rejected(() => verify_token(secret, modified.toString('base64url'), 0, limits, now), 'UNAUTHORIZED');
}
rejected(() => verify_token('wrong-secret', token, 0, limits, now), 'UNAUTHORIZED');
rejected(() => verify_token(secret, token, 1, limits, now), 'UNAUTHORIZED');
rejected(() => verify_token(secret, token, 0, {...limits, max_token_bytes: token.length - 1}, now), 'UNAUTHORIZED');
assert.deepEqual(verify_token(secret, token, 0, {...limits, max_token_bytes: token.length}, now), claims());
});
test('authenticated plaintext must be valid UTF-8 JSON of exactly typed claims', () => {
for (const value of [null, [], 'claims', 1, {}, {...claims(), v: '1'}, {...claims(), password: 'sensitive'},
{...claims(), role: 'admin'}, {...claims(), permissions: ['*']},
{...claims(), permissions: ['bot.chat', 'bot.chat']}, {...claims(), permissions: null}]) {
rejected(() => verify(value), 'UNAUTHORIZED');
}
for (const key of Object.keys(claims())) {
const missing = {...claims()} as Record<string, unknown>;
delete missing[key];
rejected(() => verify(missing), 'UNAUTHORIZED');
}
const cipher = createCipheriv('aes-256-gcm', createHash('sha256').update(secret).digest(), Buffer.alloc(12));
const ciphertext = Buffer.concat([cipher.update(Buffer.from([0x22, 0xff, 0x22])), cipher.final()]);
const token = Buffer.concat([Buffer.alloc(12), cipher.getAuthTag(), ciphertext]).toString('base64url');
rejected(() => verify_token(secret, token, 0, limits, now), 'UNAUTHORIZED');
});
test('IDs are safe integers, protocol IDs are UUIDs, and Main socket session IDs are bounded strings', () => {
for (const field of ['forward_id', 'node_id', 'bot_id', 'user_id']) {
for (const value of ['1', -1, 1.5, Number.MAX_SAFE_INTEGER + 1, null]) {
rejected(() => verify(claims({[field]: value})), 'UNAUTHORIZED');
}
}
for (const field of ['forward_session_id', 'grant_id', 'jti']) {
rejected(() => verify(claims({[field]: 'not-a-uuid'})), 'UNAUTHORIZED');
}
for (const value of ['', ' ', 'x'.repeat(257), '\u754c'.repeat(86), null, 123]) {
rejected(() => verify(claims({node_session_id: value})), 'UNAUTHORIZED');
}
assert.equal(verify(claims({node_session_id: 'x'.repeat(256)})).node_session_id.length, 256);
assert.equal(verify(claims({node_id: Number.MAX_SAFE_INTEGER})).node_id, Number.MAX_SAFE_INTEGER);
});
test('node tokens exclude web identity; web tokens require both grant and positive user ID', () => {
const node = {...claims(), role: 'node'} as Record<string, unknown>;
delete node.user_id;
delete node.grant_id;
assert.equal(verify(node).role, 'node');
for (const extras of [{user_id: 3}, {grant_id: uuid}, {user_id: null}, {grant_id: null}]) {
rejected(() => verify({...node, ...extras}), 'UNAUTHORIZED');
}
for (const extras of [{user_id: 0}, {user_id: null}, {grant_id: null}, {grant_id: ''}]) {
rejected(() => verify(claims(extras)), 'UNAUTHORIZED');
}
assert.deepEqual(verify(claims({permissions: []})).permissions, []);
});
test('time ordering, TTL, join window and finite clock skew are enforced at boundaries', () => {
for (const times of [
{issued_at: '1800000000000'}, {issued_at: -1}, {issued_at: now + 0.5},
{join_before: now}, {join_before: now - 1}, {join_before: now + 60_001},
{expires_at: now + 300_001}, {expires_at: now + 10},
{issued_at: now + 1001, join_before: now + 2000, expires_at: now + 3000},
]) rejected(() => verify(claims(times)), 'UNAUTHORIZED');
assert.equal(verify(claims(), now + 60_999).role, 'web');
rejected(() => verify(claims(), now + 61_000), 'TOKEN_EXPIRED');
const short = claims({join_before: now + 1000, expires_at: now + 1000});
assert.equal(verify(short, now + 1999).role, 'web');
rejected(() => verify(short, now + 2000), 'TOKEN_EXPIRED');
rejected(() => verify(claims(), now + 60_000, {...limits, clock_skew_ms: 0}), 'TOKEN_EXPIRED');
assert.equal(verify(claims(), now - 1000).role, 'web');
rejected(() => verify(claims(), now - 1001), 'UNAUTHORIZED');
for (const clock of [NaN, Infinity, -1, 1.5]) {
rejected(() => verify(claims(), clock), 'UNAUTHORIZED');
rejected(() => verify(claims(), now, {...limits, clock_skew_ms: clock}), 'UNAUTHORIZED');
}
for (const option of ['max_token_bytes', 'max_token_age_ms', 'join_window_ms'] as const) {
rejected(() => verify(claims(), now, {...limits, [option]: 0}), 'UNAUTHORIZED');
}
});
test('errors are standard ACKs and unknown errors do not expose internal secrets', () => {
assert.throws(() => fail('TIMEOUT', 'RPC timed out', true), RelayError);
assert.deepEqual(error_ack(new RelayError('TIMEOUT', 'RPC timed out', true)), {
status: 'error', data: {code: 'TIMEOUT', message: 'RPC timed out', retryable: true},
});
for (const error of [new Error('password=secret'), {code: 'TIMEOUT', message: 'secret'}, null]) {
assert.deepEqual(error_ack(error), {
status: 'error', data: {code: 'INTERNAL_ERROR', message: 'Internal relay error', retryable: false},
});
}
assert.deepEqual(success({accepted: true}), {status: 'success', data: {accepted: true}});
assert.equal(is_record([]), false);
assert.equal(is_record(new Date()), false);
assert.equal(is_record(Object.create(null)), true);
assert.equal(is_uuid(uuid), true);
assert.equal(is_uuid('not-a-uuid'), false);
});
test('payload size uses UTF-8 JSON bytes and limits traversal without invoking accessors', () => {
const value = {text: '\u754c\n'};
const size = Buffer.byteLength(JSON.stringify(value));
check_size(value, size);
rejected(() => check_size(value, size - 1));
const cyclic: Record<string, unknown> = {};
cyclic.self = cyclic;
let nested: unknown = 'leaf';
for (let i = 0; i < 17; i++) nested = {nested};
for (const invalid of [cyclic, nested, Array(4097).fill(0), {value: undefined}, {value: NaN},
{value: Infinity}, {value: 1n}, {value: () => 1}, Array(2), new Date()]) {
rejected(() => check_size(invalid, limits.max_payload_bytes));
}
let calls = 0;
rejected(() => check_size({get secret() { calls++; return 'secret'; }}, 4096));
assert.equal(calls, 0);
for (const bad_limit of [0, -1, Infinity, NaN]) rejected(() => check_size({}, bad_limit));
});
test('large segmented history fits default limits while bytes and complexity remain bounded', () => {
const records = Array.from({length: 20}, (_, index) => ({...record(),
id: `a89acd9a-0ab3-44ac-812e-${index.toString(16).padStart(12, '0')}`, seq: index + 1,
segments: Array.from({length: 32}, () => ({message: '\u754c'.repeat(1024),
font_color: '#ffffff', font_style: ['normal']}))}));
const result = success({stream_id: uuid, seq: 20, records, truncated: false});
const size = Buffer.byteLength(JSON.stringify(result));
assert.ok(size > 1024 * 1024);
assert.ok(size < DEFAULT_LIMITS.max_payload_bytes);
validate_node_ack('get_log_history', result, 2, DEFAULT_LIMITS);
validate_node_ack('bot.logs', result, 2, DEFAULT_LIMITS);
rejected(() => validate_node_ack('get_log_history', result, 2,
{...DEFAULT_LIMITS, max_payload_bytes: 262_144}));
check_size('x'.repeat(DEFAULT_LIMITS.max_payload_bytes - 2), DEFAULT_LIMITS.max_payload_bytes);
rejected(() => check_size('x'.repeat(DEFAULT_LIMITS.max_payload_bytes - 1), DEFAULT_LIMITS.max_payload_bytes));
const many_nodes = Array.from({length: 1024}, () => Array(64).fill(0));
rejected(() => check_size(many_nodes, DEFAULT_LIMITS.max_payload_bytes));
});
test('requests whitelist every method and reject identity or sensitive payload fields', () => {
for (const [method, payload] of [
['bot.snapshot', {}], ['bot.logs', {limit: 1}],
['bot.logs', {limit: 200, stream_id: uuid, after_seq: Number.MAX_SAFE_INTEGER}],
['bot.subscribe', {events: ['status_update', 'message']}], ['bot.unsubscribe', {events: []}],
['bot.chat', {message: 'hello'}],
] as const) validate_request(request(method, payload), limits);
for (const [method, payload] of [
['bot.delete', {}], ['bot.snapshot', null], ['bot.snapshot', {token: 'secret'}],
['bot.logs', {limit: 0}], ['bot.logs', {limit: 201}], ['bot.logs', {limit: '1'}],
['bot.logs', {limit: 1, after_seq: 0}], ['bot.logs', {limit: 1, stream_id: 'stream'}],
['bot.logs', {limit: 1, stream_id: uuid, after_seq: Number.MAX_SAFE_INTEGER + 1}],
['bot.subscribe', {events: ['log', 'log']}], ['bot.subscribe', {events: ['error']}],
['bot.chat', {message: ' '}], ['bot.chat', {message: 'x'.repeat(257)}],
['bot.chat', {message: 'hello', password: 'secret'}],
] as Array<[string, unknown]>) rejected(() => validate_request(request(method, payload), limits));
rejected(() => validate_request({...request('bot.snapshot', {}) as object, bot_id: 99}, limits));
rejected(() => validate_request({request_id: 'id', method: 'bot.snapshot', payload: {}}, limits));
rejected(() => validate_request(request('bot.chat', {message: '\u754c'.repeat(20)}), {...limits, max_payload_bytes: 100}));
check_permissions(['bot.chat']);
rejected(() => check_permissions(['bot.chat', 'bot.chat']));
rejected(() => check_permissions(['unknown']));
rejected(() => check_permissions(Array(1)));
});
test('all six bot statuses and typed event DTOs are accepted, unexpected fields are rejected', () => {
for (const status of ['offline', 'ready', 'connecting', 'online', 'disconnecting', 'reconnecting']) {
validate_event({...event('status_update', {status, context_exists: true}) as BotEvent,
context_generation: uuid}, limits);
}
validate_event(event('status_update', {status: 'offline', context_exists: false}), limits);
rejected(() => validate_event(event('status_update', {status: 'online', context_exists: false}), limits));
rejected(() => validate_event(event('status_update', {status: 'offline', context_exists: true}), limits));
for (const [name, payload] of [
['log', {records: [record()]}], ['message', {text: 'hello', position: 'chat', json: {extra: [{text: 'hello'}]}}],
['bot_error', {code: 'OFFLINE', message: 'not online'}], ['bot_error', {message: 'error'}],
] as Array<[string, unknown]>) validate_event(event(name, payload), limits);
for (const [name, payload] of [
['error', {message: 'error'}], ['status_update', {status: 'unknown', context_exists: true}],
['status_update', {status: 'online', context_exists: 'true'}],
['message', {text: 'hello', position: 'chat', token: 'secret'}],
['bot_error', {message: 'error', stack: 'sensitive stack'}],
['log', {records: [{...record(), password: 'secret'}]}],
['log', {records: [{...record(), segments: [{message: 'hi', font_color: '#fff', font_style: ['script']}]}]}],
] as Array<[string, unknown]>) rejected(() => validate_event(event(name, payload), limits));
for (const seq of [-1, 0.5, Number.MAX_SAFE_INTEGER + 1]) {
rejected(() => validate_event({...event('log', {records: []}) as BotEvent, seq}, limits));
}
rejected(() => validate_event({...event('log', {records: []}) as BotEvent, key: 'secret'}, limits));
});
test('execute_function validates exact payloads, bounded JSON args, boolean ACKs and permissions', () => {
const payload = {function: 'movement/navigation', name: 'go_to', args: [1, 'target', true, null, {position: [1, 2, 3]}]};
validate_request(request('execute_function', payload), limits);
validate_request(request('execute_function', {function: '', name: '', args: []}), limits);
for (const invalid of [null, [], {}, {...payload, category: 'movement'}, {...payload, function: []},
{...payload, name: 1}, {...payload, args: {}}, {name: 'go_to', args: []},
{function: 'movement', args: []}, {function: 'movement', name: 'go_to'}]) {
rejected(() => validate_request(request('execute_function', invalid), limits));
}
const cyclic: unknown[] = []; cyclic.push(cyclic);
let deep: unknown = 0;
for (let i = 0; i < 17; i++) deep = [deep];
for (const args of [[undefined], [NaN], [Infinity], [1n], [() => true], [new Date()],
Array(1), cyclic, [deep], Array(4097).fill(0)]) {
rejected(() => validate_request(request('execute_function', {...payload, args}), limits));
}
const value = request('execute_function', payload);
const bytes = Buffer.byteLength(JSON.stringify(value));
validate_request(value, {...limits, max_payload_bytes: bytes});
rejected(() => validate_request(value, {...limits, max_payload_bytes: bytes - 1}));
for (const data of [true, false]) validate_node_ack('execute_function', success(data), 2, limits);
for (const data of [null, 0, 1, 'true', [], {}, {accepted: true}]) {
rejected(() => validate_node_ack('execute_function', success(data), 2, limits));
}
validate_node_ack('execute_function', error_ack(new RelayError('NO_CONTEXT', 'No context')), 2, limits);
rejected(() => validate_node_ack('execute_function', {status: 'success', data: true, extra: true}, 2, limits));
check_permissions(['execute_function']);
rejected(() => check_permissions(['execute_function', 'execute_function']));
assert.deepEqual(verify(claims({permissions: ['execute_function']})).permissions, ['execute_function']);
});
test('tab_complete validates exact bounded completion DTOs without adding token permissions', () => {
for (const text of ['', ' ', '/help ', 'x'.repeat(256), '\u754c'.repeat(256)]) {
validate_request(request('tab_complete', {text}), limits);
}
for (const payload of [null, {}, {text: 1}, {text: 'x'.repeat(257)}, {text: 'a\rb'},
{text: 'a\nb'}, {text: 'a\0b'}, {text: '', extra: true}, {message: '/help'}]) {
rejected(() => validate_request(request('tab_complete', payload), limits));
}
for (const matches of [[], [' '], ['x'.repeat(256), '\u754c'.repeat(256)],
Array.from({length: 100}, (_, index) => String(index))]) {
validate_node_ack('tab_complete', success({matches, start: 0, length: 0}), 2, limits);
}
for (const data of [null, {}, {matches: 'help'}, {matches: [1]}, {matches: ['']},
{matches: ['help', 'help']}, {matches: ['x'.repeat(257)]},
{matches: Array.from({length: 101}, (_, index) => String(index))}, {matches: [], extra: true}]) {
rejected(() => validate_node_ack('tab_complete', success(data === null ? null : {start: 0, length: 0, ...data}), 2, limits));
}
for (const range of [{start: 0, length: 256}, {start: 256, length: 0}, {start: 128, length: 128}]) {
validate_node_ack('tab_complete', success({matches: [], ...range}), 2, limits);
}
for (const data of [{matches: []}, {matches: [], start: 0}, {matches: [], length: 0}] as unknown[]) {
rejected(() => validate_node_ack('tab_complete', success(data), 2, limits));
}
for (const value of [-1, 0.5, 257, Number.MAX_SAFE_INTEGER + 1, NaN, Infinity, '0', null]) {
for (const field of ['start', 'length']) {
rejected(() => validate_node_ack('tab_complete', success({matches: [], start: 0, length: 0, [field]: value}), 2, limits));
}
}
rejected(() => validate_node_ack('tab_complete', success({matches: [], start: 256, length: 1}), 2, limits));
validate_node_ack('tab_complete', error_ack(new RelayError('BOT_NOT_ONLINE', 'Offline')), 2, limits);
check_permissions(['bot.chat']);
rejected(() => check_permissions(['tab_complete']));
rejected(() => verify(claims({permissions: ['tab_complete']})), 'UNAUTHORIZED');
});
test('log metadata is accepted for events and history with exact bounded payloads', () => {
const base = record();
const segment = {message: 'hello', font_color: '#ffffff', font_style: ['normal'], hoverEvent: {action: 'show_text', value: 'tooltip', segments: [{message: 'tooltip', font_color: '#55ffff', font_style: ['bold']}]},
clickEvent: {action: 'suggest_command', value: '/help'}, insertion: 'Alex'};
const records = [{...base, segments: [segment]}];
validate_event(event('log', {records}), limits);
validate_node_ack('bot.logs', success({stream_id: uuid, seq: 1, records, truncated: false}), 2, limits);
for (const metadata of [
{hoverEvent: {action: 'unknown', value: 'text'}},
{clickEvent: {action: 'run_command', value: '/help', secret: 'extra'}},
{clickEvent: {action: 'run_command', value: 42}},
{insertion: 'x'.repeat(4097)},
]) rejected(() => validate_event(event('log', {records: [{...base, segments: [{...segment, ...metadata}]}]}), limits));
});
test('get_context_info validates empty requests and exact bounded info arrays without new permissions', () => {
validate_request(request('get_context_info', {}), limits);
for (const payload of [null, [], {name: 'health'}, {token: 'secret'}] as unknown[]) {
rejected(() => validate_request(request('get_context_info', payload), limits));
}
const info = {name: 'health_0-HP', i18n: {lang: 'en', name: 'Health', description: ''}, value: {hp: 20}};
for (const data of [[], [info], Array(256).fill(info), [{...info, name: 'a'.repeat(128),
i18n: {lang: 'a'.repeat(128), name: 'a'.repeat(256), description: 'a'.repeat(4096)}}]]) {
validate_node_ack('get_context_info', success(data), 2, limits);
}
for (const data of [null, {}, Array(257).fill(info), [{...info, extra: true}],
[{...info, i18n: {...info.i18n, extra: true}}], [{name: 'health', value: 1}],
[{...info, i18n: {...info.i18n, fields: {x: {lang: 'en', name: 'X'}}}}],
[{...info, i18n: {...info.i18n, values: []}}],
[{...info, i18n: {lang: 'en', name: 'Health'}}]]) {
rejected(() => validate_node_ack('get_context_info', success(data), 2, limits));
}
for (const name of ['', 'a.b', 'a b', '\u754c', 'a'.repeat(129), null, 1]) {
rejected(() => validate_node_ack('get_context_info', success([{...info, name}]), 2, limits));
}
for (const [field, length] of [['lang', 128], ['name', 256], ['description', 4096]] as const) {
for (const value of ['a'.repeat(length + 1), '\u754c'.repeat(Math.floor(length / 3) + 1), null, 1]) {
rejected(() => validate_node_ack('get_context_info', success([{...info, i18n: {...info.i18n, [field]: value}}]), 2, limits));
}
}
validate_node_ack('get_context_info', error_ack(new RelayError('BOT_NOT_ONLINE', 'Offline')), 2, limits);
rejected(() => check_permissions(['get_context_info']));
rejected(() => verify(claims({permissions: ['get_context_info']})), 'UNAUTHORIZED');
});
test('info_update uses an exact envelope with non-null context and JSON-safe bounded values', () => {
const update = {forward_session_id: uuid, bot_id: 2, context_generation: uuid, stream_id: other_uuid,
seq: 1, emitted_at: now, data: {name: 'health', value: null as unknown}};
const info = {name: 'health', i18n: {lang: '', name: '', description: ''}, value: null as unknown};
for (const value of [null, true, 0, -1.5, '', [1, {nested: ['ok']}], {health: 20}, Object.create(null)]) {
validate_info_update({...update, data: {...update.data, value}}, limits);
validate_node_ack('get_context_info', success([{...info, value}]), 2, limits);
}
for (const invalid of [{...update, context_generation: null}, {...update, context_generation: ''},
{...update, name: 'info_update'}, {...update, payload: update.data}, {...update, seq: -1},
{...update, bot_id: 0}, {...update, emitted_at: 0.5}, {...update, stream_id: 'stream'},
{...update, forward_session_id: 'session'}, {...update, data: {name: 'health'}},
{...update, data: {...update.data, i18n: {}}}, {...update, data: {name: 'bad.name', value: 1}},
{...update, data: {name: 'a'.repeat(129), value: 1}}]) {
rejected(() => validate_info_update(invalid, limits));
}
const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic;
let nested: unknown = 0;
for (let index = 0; index < 69; index++) nested = {nested};
let accessor_calls = 0;
for (const value of [undefined, NaN, Infinity, 1n, () => 1, new Date(), Array(1), cyclic, nested,
{get secret() {accessor_calls++; return 1;}}, Array(65_537).fill(0)]) {
rejected(() => validate_info_update({...update, data: {...update.data, value}}, limits));
rejected(() => validate_node_ack('get_context_info', success([{...info, value}]), 2, limits));
}
assert.equal(accessor_calls, 0);
const size = Buffer.byteLength(JSON.stringify(update));
validate_info_update(update, {...limits, max_payload_bytes: size});
rejected(() => validate_info_update(update, {...limits, max_payload_bytes: size - 1}));
const result = success([info]);
const ack_size = Buffer.byteLength(JSON.stringify(result));
validate_node_ack('get_context_info', result, 2, {...limits, max_payload_bytes: ack_size});
rejected(() => validate_node_ack('get_context_info', result, 2, {...limits, max_payload_bytes: ack_size - 1}));
});
test('inventory info alone accepts deep NBT and large JSON within dedicated complexity limits', () => {
const info = {name: 'inventory', i18n: {lang: 'en', name: 'Inventory', description: ''}, value: null as unknown};
const update = {forward_session_id: uuid, bot_id: 2, context_generation: uuid, stream_id: other_uuid,
seq: 1, emitted_at: now, data: {name: 'inventory', value: null as unknown}};
const validate = (value: unknown) => {
validate_info_update({...update, data: {...update.data, value}}, DEFAULT_LIMITS);
validate_node_ack('get_context_info', success([{...info, value}]), 2, DEFAULT_LIMITS);
};
let nbt: unknown = {text: 'deep'};
for (let index = 0; index < 40; index++) nbt = {type: 'compound', value: nbt};
const inventory = {slots: Array.from({length: 46}, (_, slot) => ({slot, nbt, components: '\u754c'.repeat(2048)}))};
assert.ok(Buffer.byteLength(JSON.stringify(inventory)) > 192 * 1024);
validate(inventory);
for (const value of [Array(65_536).fill(0), Object.fromEntries(Array.from({length: 65_536}, (_, index) => [String(index), 0])),
Array.from({length: 4}, () => Array(60_000).fill(0))]) validate(value);
for (const value of [Array.from({length: 4}, () => Array(65_000).fill(0)),
Object.fromEntries(Array.from({length: 65_537}, (_, index) => [String(index), 0]))]) {
rejected(() => validate(value));
}
let nested: unknown = 0;
for (let index = 0; index < 65; index++) nested = {nested};
validate(nested); // ACK value starts at depth 3, reaching the depth-68 boundary.
rejected(() => validate_node_ack('get_context_info', success([{...info, value: {nested}}]), 2, DEFAULT_LIMITS));
validate_info_update({...update, data: {...update.data, value: {nested}}}, DEFAULT_LIMITS);
rejected(() => validate_info_update({...update, data: {...update.data, value: {nested: {nested}}}}, DEFAULT_LIMITS));
rejected(() => check_size(inventory, DEFAULT_LIMITS.max_payload_bytes));
rejected(() => validate_event(event('message', {text: '', position: 'chat', json: nbt}), DEFAULT_LIMITS));
rejected(() => validate_node_ack('bot.chat', success({accepted: true, nested: nbt}), 2, DEFAULT_LIMITS));
rejected(() => validate_request(request('get_context_info', {nested: nbt}), DEFAULT_LIMITS));
});
test('Node success ACKs enforce method-specific exact DTOs and expected bot', () => {
const snapshot = {bot_id: 2, bot_status: 'offline', context_exists: false, context_generation: null as string | null,
stream_id: uuid, seq: 0, captured_at: now};
validate_node_ack('bot.snapshot', success(snapshot), 2, limits);
validate_node_ack('bot.logs', success({stream_id: uuid, seq: 1, records: [record()], truncated: false}), 2, limits);
validate_node_ack('bot.chat', success({accepted: true}), 2, limits);
validate_node_ack('bot.subscribe', success({events: ['log']}), 2, limits);
validate_node_ack('bot.chat', error_ack(new RelayError('NO_CONTEXT', 'No context')), 2, limits);
rejected(() => validate_node_ack('bot.snapshot', success(snapshot), 3, limits));
for (const data of [true, {}, {accepted: false}, {accepted: true, token: 'secret'}, snapshot]) {
rejected(() => validate_node_ack('bot.chat', success(data), 2, limits));
}
rejected(() => validate_node_ack('bot.snapshot', success({...snapshot, config: {password: 'secret'}}), 2, limits));
rejected(() => validate_node_ack('bot.snapshot', success({...snapshot, seq: Number.MAX_SAFE_INTEGER + 1}), 2, limits));
rejected(() => validate_node_ack('bot.snapshot', success({...snapshot, bot_status: 'online'}), 2, limits));
rejected(() => validate_node_ack('bot.snapshot', success({...snapshot, context_generation: uuid}), 2, limits));
rejected(() => validate_node_ack('bot.chat', {status: 'error', data: {code: 'NO_CONTEXT', message: 'error', retryable: false, stack: 'secret'}}, 2, limits));
rejected(() => validate_node_ack('bot.chat', {status: 'error', data: {code: 'UNKNOWN', message: 'error', retryable: false}}, 2, limits));
});
test('logs result record count cannot exceed 200 even inside the byte budget', () => {
const records = Array.from({length: 200}, (_, index) => ({...record(),
id: `a89acd9a-0ab3-44ac-812e-${index.toString(16).padStart(12, '0')}`, seq: index + 1}));
const result = {stream_id: uuid, seq: 200, records, truncated: false};
validate_node_ack('bot.logs', success(result), 2, limits);
rejected(() => validate_node_ack('bot.logs', success({...result, records: [...records, record()]}), 2, limits));
rejected(() => validate_event(event('log', {records: [...records, record()]}), limits));
rejected(() => validate_node_ack('bot.logs', success({...result, seq: 199}), 2, limits));
rejected(() => validate_node_ack('bot.logs', success({...result, records: [records[1], records[0]]}), 2, limits));
rejected(() => validate_node_ack('bot.logs', success({...result, records: [records[0], records[0]]}), 2, limits));
});
test('control DTOs validate scope, permissions, audience and bounded leases', () => {
const {forward_id, forward_session_id, node_session_id, node_id, bot_id} = claims();
const scope = {forward_id, forward_session_id, node_session_id, node_id, bot_id};
validate_scope(scope);
const open = {...scope, lease_expires_at: Date.now() + 60_000};
validate_open(open, 0, limits);
rejected(() => validate_open(open, 1, limits));
rejected(() => validate_open({...open, key: 'secret'}, 0, limits));
rejected(() => validate_open({...open, lease_expires_at: Date.now() + limits.max_lease_ms + 5000}, 0, limits));
rejected(() => validate_open({...open, lease_expires_at: Date.now() - 5000}, 0, limits));
const grant = {forward_session_id, grant_id: uuid, user_id: 3, permissions: ['bot.snapshot'], expires_at: Date.now() + 60_000};
validate_grant(grant, limits);
rejected(() => validate_grant({...grant, user_id: 1.5}, limits));
rejected(() => validate_grant({...grant, permissions: ['bot.snapshot', 'bot.snapshot']}, limits));
rejected(() => validate_grant({...grant, key: 'secret'}, limits));
});
test('file requests have exact payloads and a dedicated bot.files permission', () => {
check_permissions(['bot.files']);
assert.deepEqual(verify(claims({permissions: ['bot.files']})).permissions, ['bot.files']);
validate_request(request('get_data_file_list', {}), limits);
for (const payload of [{id: uuid}, {id: uuid, offset: 0, length: 1},
{id: uuid, offset: Number.MAX_SAFE_INTEGER, length: 262_144}]) {
validate_request(request('get_file_data', payload), limits);
}
for (const payload of [null, [], {id: uuid}] as unknown[]) rejected(() => validate_request(request('get_data_file_list', payload), limits));
for (const payload of [{}, {id: 'file'}, {id: uuid, path: 'secret'}, {id: uuid, offset: undefined},
...[-1, 0.5, Number.MAX_SAFE_INTEGER + 1, '0', null].map(offset => ({id: uuid, offset})),
...[0, -1, 0.5, 262_145, '1', null].map(length => ({id: uuid, length}))]) {
rejected(() => validate_request(request('get_file_data', payload), limits));
}
rejected(() => validate_request(request('bot.files', {}), limits));
rejected(() => check_permissions(['get_file_data']));
rejected(() => check_permissions(['get_data_file_list']));
});
test('file listings bound exact recursive trees, JSON bytes, depth and total nodes', () => {
const file = {type: 'file', name: 'data.bin', id: uuid, size: Number.MAX_SAFE_INTEGER, modified_at: 0};
const listing = {listing_id: uuid, expires_at: 1, partial: false, children: [file] as unknown[]};
const validate = (data: unknown) => validate_node_ack('get_data_file_list', success(data), 2, DEFAULT_LIMITS);
validate(listing);
validate({...listing, children: []});
validate({...listing, children: Array(2000).fill(file)});
let deep: unknown = file;
for (let index = 0; index < 31; index++) deep = {type: 'directory', name: 'dir', children: [deep]};
validate({...listing, children: [deep]});
rejected(() => check_size(success({...listing, children: [deep]}), DEFAULT_LIMITS.max_payload_bytes));
rejected(() => validate({...listing, children: [{type: 'directory', name: 'dir', children: [deep]}]}));
for (const data of [{...listing, children: Array(2001).fill(file)}, {...listing, listing_id: 'id'},
{...listing, expires_at: 0}, {...listing, partial: 1}, {...listing, extra: true},
{...listing, children: {}}, {...listing, children: [{...file, size: -1}]},
{...listing, children: [{...file, modified_at: Number.MAX_SAFE_INTEGER + 1}]},
{...listing, children: [{...file, path: 'secret'}]},
{...listing, children: [{type: 'directory', name: 'dir'}]},
{...listing, children: [{type: 'directory', name: 1, children: []}]}]) rejected(() => validate(data));
const small_tree = [{...file, name: ''}];
const overhead = Buffer.byteLength(JSON.stringify(small_tree));
const children = [{...file, name: 'x'.repeat(512 * 1024 - overhead)}];
validate({...listing, children});
rejected(() => validate({...listing, children: [{...children[0], name: children[0].name + 'x'}]}));
const cyclic: unknown[] = [];
cyclic.push({type: 'directory', name: 'dir', children: cyclic});
let calls = 0;
for (const children of [cyclic, Array(1), [{...file, get name() {calls++; return 'secret';}}]]) {
rejected(() => validate({...listing, children}));
}
assert.equal(calls, 0);
const bytes = Buffer.byteLength(JSON.stringify(success(listing)));
validate_node_ack('get_data_file_list', success(listing), 2, {...limits, max_payload_bytes: bytes});
rejected(() => validate_node_ack('get_data_file_list', success(listing), 2, {...limits, max_payload_bytes: bytes - 1}));
});
test('file ACKs enforce canonical base64, safe offsets, byte counts and EOF equivalence', () => {
const chunk = {id: uuid, name: 'data.bin', encoding: 'base64', offset: 0, next_offset: 1,
total_bytes: 2, eof: false, data: 'YQ=='};
const validate = (data: unknown) => validate_node_ack('get_file_data', success(data), 2, DEFAULT_LIMITS);
validate(chunk);
validate({...chunk, total_bytes: 1, eof: true});
validate({...chunk, offset: 2, next_offset: 2, total_bytes: 2, eof: true, data: ''});
validate({...chunk, next_offset: 0, total_bytes: 0, eof: true, data: ''});
const data = Buffer.alloc(262_144).toString('base64');
validate({...chunk, data, next_offset: 262_144, total_bytes: 262_144, eof: true});
for (const invalid of [{...chunk, id: 'id'}, {...chunk, name: 1}, {...chunk, encoding: 'utf8'},
{...chunk, offset: -1}, {...chunk, next_offset: 0}, {...chunk, total_bytes: 0},
{...chunk, total_bytes: Number.MAX_SAFE_INTEGER + 1}, {...chunk, next_offset: 0.5},
{...chunk, eof: true}, {...chunk, total_bytes: 1}, {...chunk, extra: true},
...['YQ', 'YR==', 'YQ==\n', 'YQ===', ' YQ==', '_w==', '!!!!'].map(data => ({...chunk, data})),
{...chunk, data: Buffer.alloc(262_145).toString('base64'), next_offset: 262_145, total_bytes: 262_145, eof: true}]) {
rejected(() => validate(invalid));
}
});
test('workflow permissions are capabilities distinct from RPC names for tokens and grants', () => {
check_permissions([...RELAY_METHODS]);
assert.deepEqual(verify(claims({permissions: ['workflow.control', 'bot.snapshot']})).permissions, ['workflow.control', 'bot.snapshot']);
const grant = {forward_session_id: uuid, grant_id: other_uuid, user_id: 3,
permissions: ['workflow.control'], expires_at: Date.now() + 60_000};
validate_grant(grant, limits);
rejected(() => check_permissions(['workflow.control', 'workflow.control']));
for (const method of WORKFLOW_METHODS.filter(method => method !== 'workflow.control')) {
rejected(() => check_permissions([method]));
rejected(() => verify(claims({permissions: [method]})), 'UNAUTHORIZED');
rejected(() => validate_grant({...grant, permissions: [method]}, limits));
}
});
test('workflow requests enforce exact targets, UUID context, positive generation and safe 64 KiB inputs', () => {
const control = {command_id: uuid, context_generation: other_uuid, workflow_id: 'workflow-1', action: 'run_once'};
for (const action of WORKFLOW_ACTIONS) {
const payload = action === 'stop_run' ? {command_id: uuid, context_generation: other_uuid, action, run_id: uuid}
: {...control, action};
validate_request(request('workflow.control', payload), limits);
if (action !== 'stop_run') validate_request(request('workflow.control', {...payload, expected_generation: 1}), limits);
if (action === 'run_once') {
validate_request(request('workflow.control', {...payload, input: {value: [1, null, true]}}), limits);
validate_request(request('workflow.control', {...payload, expected_generation: 1, input: {value: [1, null, true]}}), limits);
}
for (const [field, value] of Object.entries({workflow_id: 'workflow-1', run_id: uuid, expected_generation: 1, input: {value: true}})) {
const allowed = field === 'workflow_id' || field === 'expected_generation' ? action !== 'stop_run'
: field === 'run_id' ? action === 'stop_run' : action === 'run_once';
if (!allowed) rejected(() => validate_request(request('workflow.control', {...payload, [field]: value}), limits));
}
}
for (const [method, payload] of [['workflow.start', {workflow_id: 'a'}], ['workflow.start', {workflow_id: 'a', trigger: {}}],
['workflow.stop', {run_id: uuid}], ['workflow.snapshot', {}], ['workflow.snapshot', {run_id: uuid}],
['workflow.snapshot', {run_id: uuid, include_branches: false}], ['workflow.snapshot', {run_id: uuid, include_branches: true}],
['workflow.catalog', {}], ['workflow.runtime', {}], ['workflow.command', {command_id: uuid, context_generation: other_uuid}]]) {
validate_request(request(method as string, payload), limits);
rejected(() => validate_request(request(method as string, {...payload as object, extra: true}), limits));
}
for (const payload of [{...control, command_id: 'bad'}, {...control, context_generation: null},
{command_id: uuid, workflow_id: 'a', action: 'run_once'}, {...control, action: 'unknown'},
{...control, workflow_id: undefined}, {...control, action: 'stop_run'}, {...control, run_id: 'bad'},
...[0, -1, 1.5, Number.MAX_SAFE_INTEGER + 1, null, '1'].map(expected_generation => ({...control, expected_generation})),
...['', 'a/b', 'a'.repeat(101), '__proto__', 'constructor', 'prototype'].map(workflow_id => ({...control, workflow_id}))]) {
rejected(() => validate_request(request('workflow.control', payload), limits));
}
for (const method of ['workflow.stop', 'workflow.snapshot']) {
rejected(() => validate_request(request(method, {run_id: 'bad'}), limits));
}
for (const include_branches of [false, true]) {
rejected(() => validate_request(request('workflow.snapshot', {include_branches}), limits));
rejected(() => validate_request(request('workflow.snapshot', {run_id: 'bad', include_branches}), limits));
rejected(() => validate_request(request('workflow.stop', {run_id: uuid, include_branches}), limits));
}
for (const include_branches of [null, undefined, 0, 1, 'false', 'true', [], {}]) {
rejected(() => validate_request(request('workflow.snapshot', {run_id: uuid, include_branches}), limits));
}
rejected(() => validate_request(request('workflow.stop', {}), limits));
rejected(() => validate_request(request('workflow.command', {command_id: uuid}), limits));
const input = {value: 'x'.repeat(64 * 1024 - Buffer.byteLength(JSON.stringify({value: ''})))};
validate_request(request('workflow.control', {...control, input}), limits);
validate_request(request('workflow.start', {workflow_id: 'a', trigger: input}), limits);
rejected(() => validate_request(request('workflow.control', {...control, input: {value: input.value + 'x'}}), limits));
const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic;
let calls = 0;
for (const input of [null, [], {value: 'x'.repeat(64 * 1024)}, {value: undefined}, {value: NaN},
cyclic, {get value() {calls++; return 1;}}, JSON.parse('{"nested":{"__proto__":1}}'),
JSON.parse('{"constructor":1}'), {value: new Date()}, {value: Array(1)}]) {
rejected(() => validate_request(request('workflow.control', {...control, input}), limits));
rejected(() => validate_request(request('workflow.start', {workflow_id: 'a', trigger: input}), limits));
}
assert.equal(calls, 0);
for (const field of ['method', 'payload']) {
const value = Object.defineProperty(request('workflow.control', control), field, {enumerable: true, get() {calls++; return control;}});
rejected(() => validate_request(value, limits));
}
assert.equal(calls, 0);
const value = request('workflow.control', control);
const bytes = Buffer.byteLength(JSON.stringify(value));
validate_request(value, {...limits, max_payload_bytes: bytes});
rejected(() => validate_request(value, {...limits, max_payload_bytes: bytes - 1}));
});
test('workflow ACKs validate start/stop, opaque bounded catalog, run output and runtime commands', () => {
const command = {command_id: uuid, action: 'run_once', workflow_id: 'a', state: 'completed'};
const state: WorkflowRuntimeState = {workflow_id: 'a', generation: 1, enabled: true, start_type: 'timer', trigger_state: 'scheduled',
accepted_count: 0, max_runs: null, next_fire_at: null, active_run_ids: [uuid], queued_count: 0, last_error: null};
const runtime = {source_config_revision: 0, workflows: [state], commands: [command]};
const run: WorkflowRunSnapshot = {run_id: uuid, workflow_id: 'a', status: 'running', steps: 0, active_nodes: ['n'], started_at: 0,
ended_at: null, stop_reason: null, error: null};
const full = {...run, branches: [{trigger: {}, vars: {}, inputs: {}, retry_counts: {}, last_result: null as JsonValue,
outputs: {n: {execution: 1, status: 'success', output: {value: [1, 'output']}}}}]};
const catalog = {catalog_version: 1, nodes: [{name: 'code', config: [] as JsonValue[], output: {type: 'json'}}], start_nodes: [] as JsonValue[],
events: [] as JsonValue[], queries: [] as JsonValue[], common: {node_fields: [] as JsonValue[], policy_fields: [] as JsonValue[],
result: {}, edge_input: {}, references: [] as JsonValue[]}};
for (const data of [{status: 'started', run_id: uuid}, {status: 'queued', request_id: uuid}, {status: 'skipped'}]) {
validate_node_ack('workflow.start', success(data), 2, limits);
validate_node_ack('workflow.control', success({...command, result: data}), 2, limits);
}
validate_node_ack('workflow.stop', success({stopped: true}), 2, limits);
validate_node_ack('workflow.catalog', success(catalog), 2, limits);
for (const data of [null, [], [run, full], run, full]) validate_node_ack('workflow.snapshot', success(data), 2, limits);
const trigger: Record<string, JsonValue> = {disabled: false, count: 0, empty: null, nested: {values: [false, 0, null, 'text']}};
const summary: WorkflowRunSnapshot = {...run, trigger_available: true};
const detail: WorkflowRunSnapshot = {...run, trigger, trigger_available: true, branches: []};
for (const data of [summary, [summary], detail, {...run, trigger}, {...full, trigger}, {...run, trigger: {}},
{...run, trigger: Object.create(null)}, {...run, trigger_available: false}]) {
validate_node_ack('workflow.snapshot', success(data), 2, limits);
}
assert.equal(Object.hasOwn(summary, 'trigger'), false);
assert.equal(Object.hasOwn(summary, 'branches'), false);
assert.deepEqual(detail.trigger, trigger);
const overhead = Buffer.byteLength(JSON.stringify({value: ''}));
const bounded_trigger = {value: '\u754c'.repeat(Math.floor((65_536 - overhead) / 3))
+ 'x'.repeat((65_536 - overhead) % 3)};
assert.equal(Buffer.byteLength(JSON.stringify(bounded_trigger), 'utf8'), 65_536);
validate_node_ack('workflow.snapshot', success({...detail, trigger: bounded_trigger}), 2, limits);
rejected(() => validate_node_ack('workflow.snapshot', success({...detail,
trigger: {value: bounded_trigger.value + 'x'}}), 2, limits));
const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic;
let calls = 0;
for (const trigger of [null, [], 'trigger', false, 0, undefined, {value: NaN}, {value: Infinity}, {value: undefined},
{value: 1n}, {value: () => 1}, new Date(), Object.create({inherited: true}), cyclic,
{nested: new Date()}, {nested: Object.create({inherited: true})}, {nested: Array(1)},
{nested: Object.setPrototypeOf([], null)}, JSON.parse('{"nested":{"__proto__":1}}'),
JSON.parse('{"constructor":1}'), JSON.parse('{"nested":{"prototype":1}}'),
{get value() {calls++; return 1;}}, Object.defineProperty({}, 'hidden', {value: true}), {[Symbol('key')]: true}]) {
for (const data of [{...run, trigger}, [{...run, trigger}]]) {
rejected(() => validate_node_ack('workflow.snapshot', success(data), 2, limits));
}
}
for (const trigger_available of [null, undefined, 0, 1, 'true', {}, []]) {
rejected(() => validate_node_ack('workflow.snapshot', success({...run, trigger_available}), 2, limits));
}
rejected(() => validate_node_ack('workflow.snapshot', success({...detail, trigger_available: false}), 2, limits));
for (const field of ['trigger', 'trigger_available']) {
const data = Object.defineProperty({...run}, field, {enumerable: true, get() {calls++; return true;}});
rejected(() => validate_node_ack('workflow.snapshot', success(data), 2, limits));
}
assert.equal(calls, 0);
for (const data of [command, {...command, state: 'accepted'}, {...command, result: {stopped: true}},
{...command, result: {generation: 1}}, {...command, state: 'failed', error: {code: 'WORKFLOW_NOT_FOUND', message: 'Missing'}}]) {
validate_node_ack('workflow.control', success(data), 2, limits);
validate_node_ack('workflow.command', success(data), 2, limits);
}
validate_node_ack('workflow.command', success(null), 2, limits);
validate_node_ack('workflow.runtime', success(runtime), 2, limits);
const bytes = Buffer.byteLength(JSON.stringify(success(runtime)));
validate_node_ack('workflow.runtime', success(runtime), 2, {...limits, max_payload_bytes: bytes});
rejected(() => validate_node_ack('workflow.runtime', success(runtime), 2, {...limits, max_payload_bytes: bytes - 1}));
const catalog_bytes = Buffer.byteLength(JSON.stringify(catalog));
const bounded_catalog = {...catalog, nodes: [{...catalog.nodes[0], description: 'x'.repeat(100 * 1024 - catalog_bytes - 17)}]};
assert.equal(Buffer.byteLength(JSON.stringify(bounded_catalog)), 100 * 1024);
validate_node_ack('workflow.catalog', success(bounded_catalog), 2, limits);
rejected(() => validate_node_ack('workflow.catalog', success({...bounded_catalog,
nodes: [{...bounded_catalog.nodes[0], description: bounded_catalog.nodes[0].description + 'x'}]}), 2, limits));
const full_bytes = Buffer.byteLength(JSON.stringify(success(full)));
validate_node_ack('workflow.snapshot', success(full), 2, {...limits, max_payload_bytes: full_bytes});
rejected(() => validate_node_ack('workflow.snapshot', success(full), 2, {...limits, max_payload_bytes: full_bytes - 1}));
for (const [method, data] of [
['workflow.start', {status: 'started'}], ['workflow.start', {status: 'queued', request_id: 'bad'}],
['workflow.start', {status: 'other'}], ['workflow.stop', {stopped: false}], ['workflow.stop', {stopped: true, extra: true}],
['workflow.catalog', {...catalog, catalog_version: 2}], ['workflow.catalog', {...catalog, nodes: {}}],
['workflow.catalog', {...catalog, common: {}}], ['workflow.catalog', {...catalog, nodes: [{description: 'x'.repeat(100 * 1024)}]}],
['workflow.catalog', {...catalog, nodes: [JSON.parse('{"constructor":1}')]}],
['workflow.snapshot', {...run, run_id: 'bad'}], ['workflow.snapshot', {...run, status: 'unknown'}],
['workflow.snapshot', {...full, branches: [{}]}], ['workflow.snapshot', {...full, branches: [{...full.branches[0], vars: {bad: Infinity}}]}],
['workflow.snapshot', {...full, branches: [{...full.branches[0], outputs: {n: {status: 'invalid', output: null as JsonValue, execution: 1}}}]}],
['workflow.snapshot', {...full, branches: [{...full.branches[0], retry_counts: {n: -1}}]}],
['workflow.snapshot', {...full, branches: [{...full.branches[0], last_result: {status: 'success', output: true, node_id: 'n', extra: true}}]}],
['workflow.control', null], ['workflow.control', {...command, command_id: 'bad'}],
['workflow.control', {...command, action: 'invalid'}], ['workflow.control', {...command, result: {generation: 0}}],
['workflow.control', {...command, error: {code: 'bad', message: 'x'.repeat(1025)}}],
['workflow.runtime', {...runtime, source_config_revision: -1}], ['workflow.runtime', {...runtime, commands: Array(257).fill(command)}],
['workflow.runtime', {...runtime, workflows: [{...state, generation: 0}]}],
['workflow.runtime', {...runtime, workflows: [{...state, active_run_ids: ['bad']}]}],
] as const) rejected(() => validate_node_ack(method, success(data), 2, limits));
for (const method of WORKFLOW_METHODS) {
validate_node_ack(method, error_ack(new RelayError('INVALID_REQUEST', 'Invalid workflow')), 2, limits);
rejected(() => validate_node_ack(method, {status: 'error', data: {code: 'WORKFLOW_NOT_FOUND', message: 'bad', retryable: false}}, 2, limits));
}
});