Spaces:
Running
Running
| 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); | |
| } | |