/** The channel's stream API (/api/broadcast//…), authorized by its agent token. */ export interface Destination { id: string; platform: string; label: string; url: string; } export interface StreamConfig { live: boolean; name?: string; format?: string; width: number; height: number; fps: number; bitrateKbps: number; overlayUrl: string; record: boolean; destinations: Destination[]; } export class VashApiError extends Error { constructor( message: string, readonly status: number ) { super(message); } } export class VashApi { constructor( readonly server: string, readonly channel: string, private token: string, /** Extra headers, e.g. Cloudflare Access service-token headers. */ private headers: Record = {} ) { this.server = server.replace(/\/$/, ''); } async call(path: string, init: RequestInit = {}): Promise { const response = await fetch(`${this.server}/api/broadcast/${this.channel}${path}`, { ...init, redirect: 'manual', headers: { ...this.headers, authorization: `Bearer ${this.token}`, 'content-type': 'application/json', ...init.headers }, signal: AbortSignal.timeout(15_000) }); if (response.status >= 300 && response.status < 400) throw new VashApiError( `${path}: redirected to ${response.headers.get('location') ?? 'a login page'}; the site may need a Cloudflare Access service token`, response.status ); if (!response.ok) throw new VashApiError( `${path}: ${response.status} ${await response.text().catch(() => '')}`.slice(0, 300), response.status ); return response.json() as Promise; } stream() { return this.call('/stream'); } report(body: Record) { return this.call('/stream', { method: 'POST', body: JSON.stringify(body) }); } uploadUrls(sessionId: string, files: string[]) { return this.call<{ urls: Record }>('/uploads', { method: 'POST', body: JSON.stringify({ sessionId, files }) }); } }