import type { CapabilityBillingReceiptV1 } from '../../shared/data-service'; import { WORKS_SQUARE_CONFIG } from '../api/works-config'; import { proxyAwareFetch } from '../utils/proxy-fetch'; import { getValidWorksSquareAccessToken, getWorksSquareAccountBinding } from './works-square-session'; const ROOT = '/api/plugins/v1/hosted/game-audio/generations'; const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u; const DECIMAL = /^(?:0|[1-9]\d*)\.\d{2}$/u; const AUDIO_EXTENSIONS: Readonly> = { 'audio/mpeg': 'mp3', 'audio/wav': 'wav', 'audio/x-wav': 'wav', 'audio/ogg': 'ogg', 'audio/flac': 'flac', 'audio/mp4': 'm4a', }; export type AudioBilling = Extract; export interface AudioGeneration { schema_version: 1; plugin_id: 'makelore.game-audio'; execution_id: string; release_id: string; project_id: string; logical_operation_id: string; kind: 'music' | 'sound'; provider_status: 'accepted' | 'running' | 'succeeded' | 'failed' | 'cancelled' | 'submission_unknown'; output_ready: boolean; outputs: { index: number; name: string }[]; progress_percent: number | null; poll_interval_seconds: number; error_code: string | null; billing: AudioBilling; } interface AudioIdentity { release_id: string; release_admission_id: string; project_id: string; logical_operation_id: string; pricing_version_id: string; prompt: string; confirmed: true; } export type AudioCommand = AudioIdentity & ({ kind: 'music'; mode: 'full' | 'demo'; reference_files?: { name: string; mime_type: string; data_base64: string }[]; } | { kind: 'sound'; mode: 'single' | 'pack' | 'variants'; duration: number; count: number; loop: boolean; }); export class GameAudioClientError extends Error { constructor(readonly code: string, readonly status: number, message: string) { super(message); this.name = 'GameAudioClientError'; } } export function audioRecord(value: unknown): value is Record { return value !== null && typeof value === 'object' && !Array.isArray(value); } function invalid(): never { throw new GameAudioClientError('plugin_backend_invalid', 502, 'Audio response is invalid'); } function bounded(value: unknown, max: number): value is string { return typeof value === 'string' && value.length > 0 && value.length <= max; } function integer(value: unknown, min: number, max: number): value is number { return typeof value === 'number' && Number.isSafeInteger(value) && value >= min && value <= max; } export function parseAudioGeneration(value: unknown): AudioGeneration { if (!audioRecord(value) || value.schema_version !== 1 || value.plugin_id !== 'makelore.game-audio' || !bounded(value.execution_id, 36) || !UUID.test(value.execution_id) || !bounded(value.release_id, 36) || !UUID.test(value.release_id) || !bounded(value.project_id, 36) || !UUID.test(value.project_id) || !bounded(value.logical_operation_id, 128) || !['music', 'sound'].includes(String(value.kind)) || !['accepted', 'running', 'succeeded', 'failed', 'cancelled', 'submission_unknown'].includes(String(value.provider_status)) || typeof value.output_ready !== 'boolean' || !Array.isArray(value.outputs) || value.outputs.length > 10 || !value.outputs.every((item, index) => audioRecord(item) && Object.keys(item).every((key) => ['index', 'name'].includes(key)) && item.index === index && bounded(item.name, 160)) || value.output_ready !== (value.outputs.length > 0) || (value.output_ready && value.provider_status !== 'succeeded') || !integer(value.poll_interval_seconds, 1, 300) || (value.progress_percent !== null && !integer(value.progress_percent, 0, 100)) || (value.error_code !== null && !bounded(value.error_code, 100))) invalid(); const bill = value.billing; if (!audioRecord(bill) || bill.mode !== 'platform_metered' || !['reserved', 'dispatched', 'settled', 'released', 'expired', 'pending_review', 'refunded'].includes(String(bill.status)) || typeof bill.reserved_points !== 'string' || !DECIMAL.test(bill.reserved_points) || (bill.actual_points != null && (typeof bill.actual_points !== 'string' || !DECIMAL.test(bill.actual_points))) || !integer(bill.usage_amount, 0, 200) || bill.unit !== (value.kind === 'sound' ? 'half_second' : 'generation') || (['settled', 'refunded'].includes(String(bill.status)) && bill.actual_points == null) || (['accepted', 'running'].includes(String(value.provider_status)) && !['reserved', 'dispatched'].includes(String(bill.status))) || (value.provider_status === 'succeeded' && !['settled', 'pending_review', 'refunded'].includes(String(bill.status))) || (value.provider_status === 'submission_unknown' && bill.status !== 'pending_review') || (['failed', 'cancelled'].includes(String(value.provider_status)) && !['released', 'expired', 'refunded', 'pending_review'].includes(String(bill.status)))) invalid(); const fields = ['schema_version', 'plugin_id', 'execution_id', 'release_id', 'project_id', 'logical_operation_id', 'kind', 'provider_status', 'output_ready', 'outputs', 'progress_percent', 'poll_interval_seconds', 'error_code', 'billing']; if (Object.keys(value).some((key) => !fields.includes(key)) || Object.keys(bill).some((key) => !['mode', 'status', 'reserved_points', 'actual_points', 'usage_amount', 'unit'].includes(key))) invalid(); // Hosted JSON uses null; shared capability receipts use an absent optional amount. return { ...value, billing: Object.fromEntries(Object.entries(bill).filter(([, field]) => field !== null)) } as unknown as AudioGeneration; } async function bytes(response: Response, limit: number): Promise { if (Number(response.headers.get('content-length')) > limit) { await response.body?.cancel(); invalid(); } const reader = response.body?.getReader(); if (!reader) invalid(); const chunks: Uint8Array[] = []; let total = 0; try { while (true) { const next = await reader.read(); if (next.done) break; total += next.value.byteLength; if (total > limit) invalid(); chunks.push(next.value); } } finally { await reader.cancel().catch(() => undefined); reader.releaseLock(); } const result = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.byteLength; } return result; } export interface GameAudioClientOptions { fetchImpl?: typeof fetch; getAccessToken?: typeof getValidWorksSquareAccessToken; getAccountBinding?: typeof getWorksSquareAccountBinding; apiBaseUrl?: string; } export class GameAudioClient { constructor(private readonly options: GameAudioClientOptions = {}) {} async generate(command: AudioCommand, account: string): Promise { const result = await this.json(ROOT, account, 'POST', command); if (result.project_id !== command.project_id || result.release_id !== command.release_id || result.logical_operation_id !== command.logical_operation_id || result.kind !== command.kind) invalid(); return result; } async get(id: string, account: string): Promise { if (!UUID.test(id)) invalid(); const result = await this.json(ROOT + '/' + id, account); if (result.execution_id !== id) invalid(); return result; } async byOperation(id: string, account: string): Promise { if (!bounded(id, 128)) invalid(); const result = await this.json(ROOT + '/by-operation/' + encodeURIComponent(id), account); if (result.logical_operation_id !== id) invalid(); return result; } async cancel(id: string, account: string): Promise { if (!UUID.test(id)) invalid(); const result = await this.json(ROOT + '/' + id + '/cancel', account, 'POST'); if (result.execution_id !== id) invalid(); return result; } async download(id: string, index: number, account: string) { if (!UUID.test(id) || !integer(index, 0, 9)) invalid(); return await this.request(ROOT + '/' + id + '/content?output_index=' + index, account, 'GET', undefined, async (response) => { const mime = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase() ?? ''; const extension = AUDIO_EXTENSIONS[mime]; if (!extension) { await response.body?.cancel().catch(() => undefined); throw new GameAudioClientError('game_audio_content_invalid', 502, 'Unsupported audio media'); } const content = await bytes(response, 32 * 1024 * 1024); if (content.length === 0) invalid(); return { bytes: content, extension, mimeType: mime }; }); } private async json(route: string, account: string, method = 'GET', command?: AudioCommand) { return await this.request(route, account, method, command, async (response) => { const content = await bytes(response, 1024 * 1024); let value: unknown; try { value = JSON.parse(new TextDecoder().decode(content)); } catch { invalid(); } return parseAudioGeneration(value); }); } private async request( route: string, account: string, method: string, command: AudioCommand | undefined, consume: (response: Response) => Promise, ): Promise { const binding = this.options.getAccountBinding ?? getWorksSquareAccountBinding; const original = binding(); const assertAccount = () => { const current = binding(); if (!original || original.accountKey !== account || current?.accountKey !== account || current.epoch !== original.epoch) { throw new GameAudioClientError('plugin_account_changed', 409, 'Original signed-in account is required'); } }; assertAccount(); const fetchImpl = this.options.fetchImpl ?? proxyAwareFetch; const getToken = this.options.getAccessToken ?? getValidWorksSquareAccessToken; const body = command ? JSON.stringify(command) : undefined; if (body && Buffer.byteLength(body) > 24 * 1024 * 1024) invalid(); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), method === 'POST' ? 90_000 : 40_000); try { for (let attempt = 0; attempt < 2; attempt++) { const token = await getToken({ fetchImpl, ...(attempt ? { forceRefresh: true } : {}) }); assertAccount(); if (!token) throw new GameAudioClientError('authentication_required', 401, 'Sign-in is required'); const response = await fetchImpl((this.options.apiBaseUrl ?? WORKS_SQUARE_CONFIG.apiBaseUrl).replace(/\/$/u, '') + route, { method, redirect: 'manual', signal: controller.signal, headers: { Accept: 'application/json', Authorization: 'Bearer ' + token, ...(body ? { 'Content-Type': 'application/json' } : {}) }, ...(body ? { body } : {}), }); assertAccount(); if (response.status === 401 && attempt === 0) { await response.body?.cancel(); continue; } if (!response.ok) { let code = response.status === 429 ? 'plugin_rate_limited' : 'plugin_backend_unavailable'; try { const error: unknown = JSON.parse(new TextDecoder().decode(await bytes(response, 1024 * 1024))); if (response.status !== 429 && audioRecord(error) && audioRecord(error.detail) && bounded(error.detail.error_code, 100)) code = error.detail.error_code; } catch { /* HTTP status still owns the failure even if the error body is invalid. */ } throw new GameAudioClientError(code, response.status, 'Audio request failed (' + response.status + ')'); } const value = await consume(response); assertAccount(); return value; } throw new GameAudioClientError('authentication_required', 401, 'Sign-in is required'); } finally { clearTimeout(timeout); } } }