import { createHash, randomUUID } from 'node:crypto'; const baseUrl = process.env.D3RO_E2E_SUPABASE_URL; const anonKey = process.env.D3RO_E2E_ANON_KEY; const serviceKey = process.env.D3RO_E2E_SERVICE_KEY; if (!baseUrl || !anonKey || !serviceKey) { throw new Error('D3RO_E2E_SUPABASE_URL, D3RO_E2E_ANON_KEY and D3RO_E2E_SERVICE_KEY are required'); } let assertions = 0; function assert(condition, message) { if (!condition) throw new Error(`assertion_failed: ${message}`); assertions += 1; } async function jsonRequest(path, init = {}, expected = [200]) { const response = await fetch(`${baseUrl}${path}`, init); const text = await response.text(); if (!expected.includes(response.status)) { throw new Error(`${init.method ?? 'GET'} ${path} -> ${response.status}: ${text.slice(0, 500)}`); } return text === '' ? null : JSON.parse(text); } function authHeaders(token, extra = {}) { return { apikey: anonKey, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...extra, }; } async function createUser(email, password) { return jsonRequest('/auth/v1/admin/users', { method: 'POST', headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ email, password, email_confirm: true }), }); } async function login(email, password) { return jsonRequest('/auth/v1/token?grant_type=password', { method: 'POST', headers: { apikey: anonKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }), }); } function wavFixture(dataBytes) { const output = Buffer.alloc(44 + dataBytes); output.write('RIFF', 0); output.writeUInt32LE(36 + dataBytes, 4); output.write('WAVEfmt ', 8); output.writeUInt32LE(16, 16); output.writeUInt16LE(1, 20); output.writeUInt16LE(1, 22); output.writeUInt32LE(16_000, 24); output.writeUInt32LE(32_000, 28); output.writeUInt16LE(2, 32); output.writeUInt16LE(16, 34); output.write('data', 36); output.writeUInt32LE(dataBytes, 40); for (let index = 44; index < output.length; index += 2) { output.writeInt16LE(Math.round(Math.sin(index / 30) * 1_500), index); } return output; } function metadata(name, value) { return `${name} ${Buffer.from(value, 'utf8').toString('base64')}`; } const password = `Local-${randomUUID()}-A1!`; const ownerEmail = `recording-owner-${randomUUID()}@example.invalid`; const otherEmail = `recording-other-${randomUUID()}@example.invalid`; let owner = null; let other = null; let ownerToken = null; let storageKey = null; try { owner = await createUser(ownerEmail, password); other = await createUser(otherEmail, password); const ownerSession = await login(ownerEmail, password); const otherSession = await login(otherEmail, password); ownerToken = ownerSession.access_token; assert(ownerSession.user.id === owner.id, 'owner login returns the created account'); const [meeting] = await jsonRequest('/rest/v1/meetings', { method: 'POST', headers: authHeaders(ownerToken, { Prefer: 'return=representation' }), body: JSON.stringify({ user_id: owner.id, title: 'Resumable mobile meeting', status: 'recording' }), }, [201]); assert(meeting.status === 'recording', 'meeting begins in recording state'); const anonymous = await fetch(`${baseUrl}/rest/v1/rpc/mobile_begin_meeting_recording`, { method: 'POST', headers: { apikey: anonKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ p_meeting_id: meeting.id }), }); assert(anonymous.status === 401 || anonymous.status === 403, 'anonymous recording mutation fails closed'); const forbidden = await fetch(`${baseUrl}/rest/v1/rpc/mobile_begin_meeting_recording`, { method: 'POST', headers: authHeaders(otherSession.access_token), body: JSON.stringify({ p_meeting_id: meeting.id }), }); assert(!forbidden.ok, 'another authenticated user cannot start the owner meeting'); const begun = await jsonRequest('/rest/v1/rpc/mobile_begin_meeting_recording', { method: 'POST', headers: authHeaders(ownerToken), body: JSON.stringify({ p_meeting_id: meeting.id }), }); assert(begun.id === meeting.id && begun.status === 'recording', 'owner recording RPC is confirmed'); const audio = wavFixture(6 * 1024 * 1024 + 32_000); const sha256 = createHash('sha256').update(audio).digest('hex'); storageKey = `${owner.id}/imports/${sha256}/resumable.wav`; const [audioRow] = await jsonRequest('/rest/v1/audio_files', { method: 'POST', headers: authHeaders(ownerToken, { Prefer: 'return=representation' }), body: JSON.stringify({ user_id: owner.id, meeting_id: meeting.id, source: 'recording', original_name: 'resumable.wav', storage_key: storageKey, mime_type: 'audio/wav', size_bytes: audio.length, duration_ms: Math.floor((audio.length - 44) / 32), sha256, upload_status: 'uploading', }), }, [201]); const createUpload = await fetch(`${baseUrl}/storage/v1/upload/resumable`, { method: 'POST', headers: { apikey: anonKey, Authorization: `Bearer ${ownerToken}`, 'Tus-Resumable': '1.0.0', 'Upload-Length': String(audio.length), 'Upload-Metadata': [ metadata('bucketName', 'audio'), metadata('objectName', storageKey), metadata('contentType', 'audio/wav'), metadata('cacheControl', '3600'), ].join(','), 'x-upsert': 'true', }, }); assert(createUpload.status === 201, `TUS upload is created (${createUpload.status})`); const location = createUpload.headers.get('location'); assert(typeof location === 'string' && location.includes('/storage/v1/upload/resumable/'), 'TUS location is returned'); const uploadUrl = new URL(location, baseUrl).toString(); const firstChunkSize = 6 * 1024 * 1024; const firstPatch = await fetch(uploadUrl, { method: 'PATCH', headers: { apikey: anonKey, Authorization: `Bearer ${ownerToken}`, 'Tus-Resumable': '1.0.0', 'Upload-Offset': '0', 'Content-Type': 'application/offset+octet-stream', }, body: audio.subarray(0, firstChunkSize), }); assert(firstPatch.status === 204, `first TUS chunk succeeds (${firstPatch.status})`); const head = await fetch(uploadUrl, { method: 'HEAD', headers: { apikey: anonKey, Authorization: `Bearer ${ownerToken}`, 'Tus-Resumable': '1.0.0', }, }); assert(head.status === 200 && Number(head.headers.get('upload-offset')) === firstChunkSize, 'HEAD restores the exact upload offset'); const finalPatch = await fetch(uploadUrl, { method: 'PATCH', headers: { apikey: anonKey, Authorization: `Bearer ${ownerToken}`, 'Tus-Resumable': '1.0.0', 'Upload-Offset': String(firstChunkSize), 'Content-Type': 'application/offset+octet-stream', }, body: audio.subarray(firstChunkSize), }); assert(finalPatch.status === 204, `resumed final TUS chunk succeeds (${finalPatch.status})`); assert(Number(finalPatch.headers.get('upload-offset')) === audio.length, 'final TUS offset equals file size'); const [uploadedAudio] = await jsonRequest(`/rest/v1/audio_files?id=eq.${audioRow.id}`, { method: 'PATCH', headers: authHeaders(ownerToken, { Prefer: 'return=representation' }), body: JSON.stringify({ upload_status: 'uploaded' }), }); assert(uploadedAudio.upload_status === 'uploaded', 'audio metadata confirms upload only after TUS completion'); const queuedMeeting = await jsonRequest('/rest/v1/rpc/mobile_queue_meeting_recording', { method: 'POST', headers: authHeaders(ownerToken), body: JSON.stringify({ p_meeting_id: meeting.id, p_duration_ms: 197_608 }), }); assert(queuedMeeting.status === 'processing', 'meeting becomes processing while the durable job runs'); const idempotencyKey = `mobile-meeting:${meeting.id}:${sha256}`; const processingJob = await jsonRequest('/rest/v1/rpc/mobile_begin_meeting_processing', { method: 'POST', headers: authHeaders(ownerToken), body: JSON.stringify({ p_meeting_id: meeting.id, p_audio_file_id: audioRow.id, p_idempotency_key: idempotencyKey, }), }); assert(processingJob.status === 'running' && processingJob.audio_file_id === audioRow.id, 'server-owned processing job is linked'); const completed = await jsonRequest('/rest/v1/rpc/mobile_complete_meeting_processing', { method: 'POST', headers: authHeaders(ownerToken), body: JSON.stringify({ p_meeting_id: meeting.id, p_audio_file_id: audioRow.id, p_idempotency_key: idempotencyKey, p_transcript: 'local resumable lifecycle verified', p_language: 'en', p_provider: 'integration-fixture', p_duration_ms: 197_608, p_stt_latency_ms: 50, }), }); assert(completed.status === 'completed' && completed.raw_transcript === 'local resumable lifecycle verified', 'atomic meeting completion is returned'); assert(completed.audio_storage_key === storageKey, 'meeting links the uploaded audio key'); const jobs = await jsonRequest(`/rest/v1/processing_jobs?id=eq.${processingJob.id}&select=*`, { headers: authHeaders(ownerToken), }); assert(jobs.length === 1 && jobs[0].status === 'succeeded' && jobs[0].progress === 100, 'job readback is succeeded at 100 percent'); const transcripts = await jsonRequest(`/rest/v1/transcripts?meeting_id=eq.${meeting.id}&select=*`, { headers: authHeaders(ownerToken), }); assert(transcripts.length === 1 && transcripts[0].text === 'local resumable lifecycle verified', 'transcript readback is linked to the meeting'); } finally { if (storageKey && ownerToken) { await fetch(`${baseUrl}/storage/v1/object/audio/${storageKey.split('/').map(encodeURIComponent).join('/')}`, { method: 'DELETE', headers: { apikey: anonKey, Authorization: `Bearer ${ownerToken}` }, }).catch(() => undefined); } for (const user of [owner, other]) { if (!user?.id) continue; await fetch(`${baseUrl}/auth/v1/admin/users/${user.id}`, { method: 'DELETE', headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}` }, }).catch(() => undefined); } } console.log(`recording lifecycle local integration: ${assertions} assertions passed`);