import { Transform, type TransformCallback } from 'node:stream'; import { TextDecoder } from 'node:util'; import { jsonPieces } from './json.js'; import { LIMITS, type ErrorShape, type BridgeBaseMessage } from './types.js'; export function isValidId(id: unknown): id is string { return typeof id === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(id); } const messages: Record = { invalid_input: 'Invalid runtime protocol or input.', connectivity_failed: 'The backend connection was lost.', query_timeout: 'The runtime deadline expired.', model_context_exceeded: 'The configured model context limit was exceeded.', model_output_limit: 'The model output is incomplete or exceeds its configured limit.', upstream_failed: 'The model request failed.', upstream_response_too_large: 'The upstream response exceeds the supported limit.', tls_failed: 'The upstream TLS connection failed.', confluence_auth_failed: 'Confluence authentication failed.', destination_denied: 'The requested Confluence destination is denied.', cleanup_failed: 'Runtime cleanup failed.', execution_failed: 'The runtime could not complete execution.', }; // Never reflect arbitrary exceptions or peer content into terminal diagnostics. export function sanitizeError(code: string, _raw?: unknown): ErrorShape { const safeCode = Object.hasOwn(messages, code) ? code : 'execution_failed'; return { code: safeCode, message: messages[safeCode] }; } export class NDJsonFrameParser extends Transform { private parts: Buffer[] = []; private bytes = 0; private slab = Buffer.alloc(65536); private used = 0; constructor(private maxFrameBytes: number = LIMITS.ORDINARY_FRAME_MAX_BYTES) { super({ readableObjectMode: true }); } setMaxFrameBytes(max: number): void { this.maxFrameBytes = max; } _transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void { try { let offset = 0; while (offset < chunk.length) { const end = chunk.indexOf(10, offset); const stop = end < 0 ? chunk.length : end; const part = chunk.subarray(offset, stop); if (this.bytes + part.length > this.maxFrameBytes) throw new Error('Frame limit exceeded'); // Copy slices so a small unfinished line cannot retain a large coalesced chunk. for (let at = 0; at < part.length;) { const n = Math.min(this.slab.length - this.used, part.length - at); part.copy(this.slab, this.used, at, at + n); this.used += n; at += n; this.bytes += n; if (this.used === this.slab.length) { this.parts.push(this.slab); this.slab = Buffer.alloc(65536); this.used = 0; } } if (end < 0) break; if (this.used) this.parts.push(this.slab.subarray(0, this.used)); const line = Buffer.concat(this.parts, this.bytes); this.slab = Buffer.alloc(65536); this.used = 0; this.parts = []; this.bytes = 0; if (!line.length) throw new Error('Empty frame'); const parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(line)); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Invalid frame'); if (line.length > (parsed.type === 'start' ? LIMITS.START_FRAME_MAX_BYTES : LIMITS.ORDINARY_FRAME_MAX_BYTES)) throw new Error('Frame limit exceeded'); this.push(parsed); offset = end + 1; } callback(); } catch { callback(new Error('Invalid protocol frame')); } } _flush(callback: TransformCallback): void { callback(this.bytes ? new Error('Truncated protocol frame') : undefined); } } type QueuedWrite = { message: BridgeBaseMessage; onStart?: () => void; resolve: () => void; reject: (error: Error) => void }; export class StreamMessageWriter { private queue: QueuedWrite[] = []; private active?: QueuedWrite; private closed = false; private failure?: Error; private idle: Array<() => void> = []; constructor(private stream: NodeJS.WritableStream) { stream.on('error', () => { this.failure = new Error('Output stream failed'); }); } write(message: BridgeBaseMessage, onStart?: () => void): Promise { if (this.closed) return Promise.reject(new Error('Writer closed')); return this.enqueue(message, onStart); } /** Drop frames not yet started; never splice a terminal error into an active line. */ writeTerminal(message: BridgeBaseMessage): Promise { if (this.closed) return Promise.reject(new Error('Writer closed')); this.closed = true; for (const queued of this.queue.splice(0)) queued.reject(new Error('Write cancelled by terminal failure')); // An already-started complete/error is itself terminal. A failed/truncated // write must be reported by stream closure, not a second terminal frame. if (this.active && ['complete', 'error'].includes(this.active.message.type)) { return Promise.reject(new Error('Terminal frame already started')); } return this.enqueue(message); } private enqueue(message: BridgeBaseMessage, onStart?: () => void): Promise { const result = new Promise((resolve, reject) => this.queue.push({ message, onStart, resolve, reject })); if (!this.active) void this.pump(); return result; } private async pump(): Promise { while (this.queue.length) { const current = this.queue.shift()!; this.active = current; try { if (this.failure) throw this.failure; current.onStart?.(); let batch = ''; const flush = async () => { const data = batch; batch = ''; await new Promise((resolve, reject) => { this.stream.write(data, 'utf8', (error?: Error | null) => error ? reject(new Error('Output stream failed')) : resolve()); }); }; for (const part of jsonPieces(current.message)) { batch += part; if (batch.length >= 65536) await flush(); } batch += '\n'; await flush(); current.resolve(); } catch { this.failure = new Error('Output stream failed'); current.reject(this.failure); for (const queued of this.queue.splice(0)) queued.reject(this.failure); this.closed = true; } this.active = undefined; } for (const resolve of this.idle.splice(0)) resolve(); } async close(): Promise { this.closed = true; if (this.active || this.queue.length) await new Promise(resolve => this.idle.push(resolve)); if (this.failure) throw this.failure; } } export function logDiagnostic(message: string): void { process.stderr.write(message.slice(0, 1024) + '\n'); }