codexmobile-relay / server /relay-protocol.js
Codex
deploy: CodexMobile Relay
90f0300
Raw
History Blame Contribute Delete
6.64 kB
import crypto from 'node:crypto';
export const RELAY_PROTOCOL_VERSION = 1;
export const DEFAULT_RELAY_REQUEST_TIMEOUT_MS = 120000;
export const DEFAULT_RELAY_HEARTBEAT_MS = 15000;
export const DEFAULT_RELAY_IDLE_HEARTBEAT_MS = 300000;
export const DEFAULT_RELAY_SMALL_BODY_LIMIT = 2 * 1024 * 1024;
export const DEFAULT_RELAY_STREAM_CHUNK_BYTES = 256 * 1024;
export const DEFAULT_RELAY_WS_BUFFERED_BYTES = 4 * 1024 * 1024;
export const DEFAULT_RELAY_WS_MAX_PAYLOAD_BYTES = 512 * 1024;
export const DEFAULT_RELAY_REALTIME_FRAME_BYTES = 256 * 1024;
const HOP_BY_HOP_HEADERS = new Set([
'connection',
'upgrade',
'transfer-encoding',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailer'
]);
const REQUEST_HEADER_ALLOWLIST = new Set([
'authorization',
'content-type',
'accept',
'user-agent'
]);
const RESPONSE_HEADER_ALLOWLIST = new Set([
'content-type',
'content-length',
'cache-control'
]);
export function createRequestId() {
return crypto.randomUUID();
}
export function createConnectorInstanceId() {
return crypto.randomUUID();
}
export function isStrongRelaySecret(value) {
return String(value || '').trim().length >= 32;
}
export function timingSafeTextEqual(left, right) {
const leftValue = String(left || '');
const rightValue = String(right || '');
if (!leftValue || !rightValue) {
return false;
}
const leftHash = crypto.createHash('sha256').update(leftValue).digest();
const rightHash = crypto.createHash('sha256').update(rightValue).digest();
return crypto.timingSafeEqual(leftHash, rightHash);
}
export function parsePositiveInt(value, fallback) {
const next = Number(value);
return Number.isFinite(next) && next > 0 ? Math.floor(next) : fallback;
}
export function safeJsonParse(value) {
try {
return value ? JSON.parse(value) : null;
} catch {
return null;
}
}
export function jsonMessage(type, patch = {}) {
return {
type,
protocolVersion: RELAY_PROTOCOL_VERSION,
...patch
};
}
export function sendWsJson(ws, payload) {
if (ws.readyState !== ws.OPEN) {
return false;
}
try {
ws.send(JSON.stringify(payload));
return true;
} catch {
return false;
}
}
export function safePathWithQuery(pathname, search = '') {
const path = String(pathname || '/');
if (!path.startsWith('/')) {
return `/${path}`;
}
return `${path}${search || ''}`;
}
export function buildLocalTargetUrl(forwardPath, localBaseUrl) {
const pathWithQuery = String(forwardPath || '/');
if (!pathWithQuery.startsWith('/') || pathWithQuery.startsWith('//') || /[\x00-\x1f\x7f]/.test(pathWithQuery)) {
throw Object.assign(new Error('relay_invalid_forward_path'), { status: 400 });
}
const base = new URL(localBaseUrl);
const parsedPath = new URL(pathWithQuery, 'http://codexmobile.local');
base.pathname = parsedPath.pathname;
base.search = parsedPath.search;
base.hash = '';
return base.toString();
}
export function isRelayUnsupportedPath(pathname, contentType = '') {
const path = String(pathname || '');
if (path === '/ws/realtime') {
return 'relay_realtime_http_upgrade_required';
}
if (path.startsWith('/generated/')) {
return 'relay_streaming_required';
}
return '';
}
export function isRelayStreamingRequest(method, pathname, contentType = '') {
const normalizedMethod = String(method || 'GET').toUpperCase();
if (['GET', 'HEAD'].includes(normalizedMethod)) {
return false;
}
const path = String(pathname || '');
if (path === '/api/uploads' || path === '/api/voice/transcribe') {
return true;
}
return /multipart\/form-data|audio\/|image\//i.test(String(contentType || ''));
}
export function isRelayStreamingResponseRequest(method, pathname) {
const normalizedMethod = String(method || 'GET').toUpperCase();
const path = String(pathname || '');
if (normalizedMethod === 'GET' && path.startsWith('/generated/')) {
return true;
}
return normalizedMethod === 'POST' && path === '/api/voice/speech';
}
export function filterRequestHeaders(headers = {}) {
const next = {};
for (const [key, value] of Object.entries(headers)) {
const normalized = key.toLowerCase();
if (HOP_BY_HOP_HEADERS.has(normalized)) {
continue;
}
if (REQUEST_HEADER_ALLOWLIST.has(normalized) || normalized.startsWith('x-codexmobile-')) {
next[normalized] = Array.isArray(value) ? value.join(', ') : String(value);
}
}
return next;
}
export function filterResponseHeaders(headers = {}) {
const next = {};
const entries = typeof headers.entries === 'function' ? headers.entries() : Object.entries(headers);
for (const [key, value] of entries) {
const normalized = key.toLowerCase();
if (HOP_BY_HOP_HEADERS.has(normalized) || normalized === 'set-cookie') {
continue;
}
if (RESPONSE_HEADER_ALLOWLIST.has(normalized) || normalized.startsWith('x-codexmobile-')) {
next[normalized] = Array.isArray(value) ? value.join(', ') : String(value);
}
}
return next;
}
export function redactHeaders(headers = {}) {
const next = {};
for (const [key, value] of Object.entries(headers)) {
const normalized = key.toLowerCase();
if (
normalized.includes('authorization') ||
normalized.includes('cookie') ||
normalized.includes('token') ||
normalized.includes('secret') ||
normalized.includes('key')
) {
next[key] = '[redacted]';
} else {
next[key] = value;
}
}
return next;
}
export function browserTokenFromHeaders(headers = {}) {
const header = headers.authorization || headers.Authorization || '';
if (String(header).toLowerCase().startsWith('bearer ')) {
return String(header).slice(7).trim();
}
const fallback = headers['x-codexmobile-token'] || headers['X-Codexmobile-Token'];
return typeof fallback === 'string' ? fallback.trim() : '';
}
export function bearerSecretFromRequest(req) {
const header = req.headers.authorization || '';
if (!String(header).toLowerCase().startsWith('bearer ')) {
return '';
}
return String(header).slice(7).trim();
}
export function normalizeErrorCode(error, fallback = 'relay_error') {
return String(error || fallback)
.trim()
.replace(/[^a-zA-Z0-9_-]+/g, '_')
.toLowerCase() || fallback;
}
export function logRelayEvent(event, fields = {}, level = 'log') {
const payload = {
event,
ts: new Date().toISOString(),
...fields
};
const line = JSON.stringify(payload);
if (level === 'warn') {
console.warn(line);
return;
}
if (level === 'error') {
console.error(line);
return;
}
console.log(line);
}