# IACL Comms > Vídeo, audio y chat con IA en tiempo real y al cerrar la reunión, para integrarlo en tus propias apps y sistemas con una librería y una API key. Servidores en la UE. Generado con `pnpm docs:llms` desde docs/developers y los tipos de los paquetes. No se edita a mano. --- # IACL Comms: contexto para una IA que programa con él Este documento es la cabecera de `llms-full.txt` y `llms-small.txt`. Está escrito para un modelo que va a escribir código de integración: primero el modelo mental, después las reglas que no se pueden romper y, al final, dónde está cada cosa. Lo demás del fichero son las guías y la referencia, generadas del propio código. ## Qué es IACL Comms es vídeo, audio y chat con IA en tiempo real y al cerrar la reunión, para meterlo dentro de la aplicación de quien integra. Hay una API HTTP (`https://api.comms.iaconsultinglabs.com/v1`), eventos firmados por webhook y cuatro paquetes npm con licencia MIT: | Paquete | Dónde corre | Para qué | |---|---|---| | `@iaconsultinglabs/comms-server` | Backend (Node ≥ 22) | Crear reuniones, dar accesos, configurar, leer el consumo, verificar webhooks. Usa la API key | | `@iaconsultinglabs/comms-core` | Navegador, sin framework | Entrar a la reunión con un acceso, medios, chat, la IA en directo (`meeting.ai`) | | `@iaconsultinglabs/comms-vue` | Navegador, Vue 3 | Los componentes ya hechos (`MeetingRoom`, `AiLivePanel`, `LiveCaptions`…) y composables (`useMeeting`, `useAi`…). También custom elements (``) para usarlo sin Vue | | `@iaconsultinglabs/comms-contracts` | Los dos | Tipos y constantes del contrato (OpenAPI, eventos, errores) | ## El modelo mental - **Tenant**: la cuenta del cliente en Comms. Todo lo que hace una clave es de su tenant. - **API key**: `iacl_test_…` (prueba) o `iacl_live_…` (producción). Vive **solo en el backend**. - El modo lo da la clave, no un parámetro. Una clave de prueba ve y cambia solo lo de prueba (reuniones, eventos, configuración, consumo) y nunca lo de producción, y al revés. - Las claves de prueba: reuniones de hasta 4 personas y 15 minutos, IA **simulada** y determinista (`iacl-sandbox-live`, `iacl-sandbox-closing`), gratis. - Cada clave tiene permisos (`scopes`): `meetings:read`, `meetings:write`, `tokens:create`, `recordings:read`, `recordings:write`, `ai:read`, `ai:configure`, `ai:run`, `events:read`, `webhooks:manage`, `storage:manage`, `usage:read`, `keys:manage`. Sin el permiso, el error es `key_scope_missing`. - **Reunión** (`mtg_…`): la crea el backend. Lleva sus ajustes, también los de la IA (`settings.ai`), y opcionalmente el contexto inicial para la IA (`aiContext`). - **Acceso (token)**: el backend lo pide para cada persona (`issueMemberToken`, `issueGuestToken`) y se lo da al navegador. El navegador entra con ese acceso, nunca con la clave. - **Persona y aparato**: la misma persona puede estar desde varios aparatos; la identidad es reunión + persona + aparato (`currentDevice()` en el navegador). - **Eventos**: lo que pasa en Comms llega al backend por webhook firmado y se puede listar con la API. Hay que verificarlos (`verifyWebhook`) y tratarlos de forma idempotente por su id. - **IA en directo**: transcripción, detectores en lenguaje natural, ayuda privada por turno (sugerencias, respuestas con fuente del contexto, datos, avisos) y salidas del modo. En el navegador se lee con `meeting.ai` (`comms-core`) o con `AiLivePanel`/`LiveCaptions` (`comms-vue`). Solo ve la ayuda privada quien tiene `aiAssist` en su acceso; nunca un invitado. - **IA al cerrar**: resumen, acciones propuestas, comprobaciones y el resultado del modo, por evento (`summary.ready`, `action.proposed`, `ai.closing.completed`). - **Modelos**: se eligen de un catálogo de IACL con precio (`listAiModels`), por cuenta (`setAiModelDefaults`) o por reunión. No se puede traer otro proveedor ni otra clave de IA. ## Reglas que no se rompen 1. **La API key nunca llega al navegador**, ni a un repositorio, ni a un log. Va en una variable de entorno del backend (`IACL_API_KEY`). 2. **El navegador solo recibe accesos**, pedidos por el backend después de decidir quién es la persona y si puede entrar. 3. **Comms propone y nunca ejecuta**: las acciones de la IA (`action.proposed`) las aprueba y ejecuta el sistema del integrador. 4. **Los webhooks se verifican** con su secreto antes de leerlos, y se procesan por id de evento una sola vez. 5. **Los errores se tratan por su `code` estable**, no por el texto (`isCommsApiError(error)` en el backend; `CommsError` en el navegador). Reintentar solo lo que la guía de errores dice que es reintentable, con la espera que pide la respuesta. 6. **Privacidad**: quien tiene `aiExcluded` no es oído ni transcrito; los mensajes privados del chat no pasan nunca por la IA; la grabación se entrega al almacenamiento del integrador y Comms borra su copia al confirmarse. Qué guarda Comms, cuánto y por qué: `privacy.md`. 7. **Si la IA falla, la llamada sigue**: el código del integrador no puede depender de que llegue una ayuda o un evento de IA para que la reunión funcione. 8. **Desarrollar con claves de prueba** y pasar a `iacl_live_…` solo en producción; la configuración de cada modo se prepara aparte. ## Anti-patrones - Crear la reunión o pedir el acceso desde el navegador con la clave. - Guardar el acceso de una persona y reutilizarlo para otra. - Leer el cuerpo de un webhook sin verificar la firma, o con el cuerpo ya parseado (la firma va sobre el cuerpo crudo). - Tratar `action.proposed` como una orden que se ejecuta sola. - Pintar la ayuda privada de la IA a todos los participantes. - Copiar los ids de modelo de la documentación en vez de leerlos de `listAiModels()`. ## Dónde está cada cosa En este mismo fichero, por este orden: el inicio rápido, la integración entera paso a paso, las claves, Node, el navegador, los widgets de Vue, la IA en directo, la IA al cerrar, la grabación, los webhooks y su orden, errores y reintentos, límites, privacidad, el catálogo de eventos, el de errores y, al final, la referencia de cada paquete sacada de sus tipos. --- # De cero a una videollamada con IA en 5 minutos Al acabar tendrás una reunión con vídeo, chat y la IA en directo funcionando en tu ordenador, creada desde tu backend y abierta en el navegador. Con una **clave de prueba**: la IA es simulada, no cuesta nada y no sale nada a ningún proveedor. ``` Navegador ── (1) pide la reunión y el acceso ──▶ TU backend ── clave de API ──▶ API de IACL Comms │ │ └── (2) entra con el acceso (token de corta duración) ──▶ vídeo, chat e IA ◀────┘ ``` La regla que no se rompe: **la clave de API vive solo en tu backend**. Al navegador le llega un acceso de corta duración que vale para una conexión de una reunión. El proyecto entero está en [`examples/quickstart`](../../examples/quickstart): lo de abajo son sus ficheros, tal cual. ## 0. Lo que necesitas - **Node 22 o superior.** - **Una clave de prueba** (`iacl_test_…`). Pídela en el portal de clientes de IACL Comms o al equipo de IACL. Se enseña **una sola vez**: guárdala en tu gestor de secretos. Cómo son las claves, sus permisos y sus límites: [api-keys.md](api-keys.md). Con una clave de prueba, cada reunión admite 4 personas y dura como mucho 15 minutos, y la IA usa los modelos simulados (`iacl-sandbox-live` e `iacl-sandbox-closing`). ## 1. Instala (1 minuto) ```bash npm create vite@latest mi-reunion -- --template vue-ts cd mi-reunion npm install @iaconsultinglabs/comms-server @iaconsultinglabs/comms-core @iaconsultinglabs/comms-vue npm install -D tsx concurrently ``` Pon tu clave en un `.env` (nunca en el repositorio ni en el navegador): ```ini IACL_API_KEY=iacl_test_… IACL_API_URL=https://api.comms.iaconsultinglabs.com/v1 ``` ## 2. El backend (2 minutos) `server.ts` crea la reunión con la IA en directo y da el acceso a quien entra. En tu aplicación, esto va dentro de tus rutas (Express, Fastify…), detrás de tu sesión: ahí decides quién entra y con qué papel. ```ts // El backend del inicio rápido (docs/developers/quickstart.md): crea una reunión con la IA en // directo y da el acceso a quien entra. La clave de API vive aquí y nunca sale al navegador. // // IACL_API_KEY=iacl_test_… IACL_API_URL=https://api.comms.iaconsultinglabs.com/v1 // npm run server import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import { pathToFileURL } from 'node:url' import { createCommsClient, isCommsApiError } from '@iaconsultinglabs/comms-server' import type { Device } from '@iaconsultinglabs/comms-contracts' export interface QuickstartOptions { /** Tu clave: `iacl_test_…` para probar (IA simulada, gratis), `iacl_live_…` en producción. */ apiKey: string /** La API de IACL Comms, con su `/v1`. */ apiUrl: string } async function readJson(req: IncomingMessage): Promise> { const chunks: Buffer[] = [] for await (const chunk of req) chunks.push(chunk as Buffer) return chunks.length > 0 ? (JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record) : {} } function send(res: ServerResponse, status: number, body: unknown): void { res.writeHead(status, { 'content-type': 'application/json' }) res.end(JSON.stringify(body)) } /** El servidor del ejemplo, sin arrancar: `listen(3000)` para usarlo. */ export function createQuickstartServer(options: QuickstartOptions): Server { const comms = createCommsClient({ baseUrl: options.apiUrl, apiKey: options.apiKey }) async function route(req: IncomingMessage, res: ServerResponse): Promise { const url = new URL(req.url ?? '/', 'http://localhost') // 1. Una reunión con la IA en directo. Con una clave de prueba, el modelo es el simulado. // Si la página ya trae una (otra pestaña), se reutiliza. if (req.method === 'POST' && url.pathname === '/api/meeting') { const { meetingId } = (await readJson(req)) as { meetingId?: string | null } if (meetingId && /^mtg_[0-9a-z]+$/.test(meetingId)) { const existing = await comms.getMeeting(meetingId) return send(res, 200, { meetingId: existing.id, apiUrl: options.apiUrl }) } const meeting = await comms.createMeeting({ type: 'meeting', title: 'Mi primera reunión con IA', settings: { ai: { enabled: true, realtime: true, postMeeting: true, profileId: 'aip_tpl_sales' } }, // Lo que la IA sabe al empezar. Tú decides de dónde sale: tu CRM, tu ERP, un documento… aiContext: { brief: 'Llamada comercial con un cliente que pide presupuesto para un proyecto nuevo.', documents: [ { title: 'Propuesta enviada', text: 'Plazo de entrega: 15 de diciembre. Precio: 12.000 € más IVA, instalación aparte. Validez: 30 días.', }, ], }, }) return send(res, 201, { meetingId: meeting.id, apiUrl: options.apiUrl }) } // 2. El acceso de quien entra. En tu app, aquí decides quién es y si puede entrar. const match = /^\/api\/meetings\/(mtg_[0-9a-z]+)\/token$/.exec(url.pathname) if (req.method === 'POST' && match?.[1]) { const { device, name } = (await readJson(req)) as { device: Device; name?: string } const grant = await comms.issueMemberToken(match[1], { person: { id: `demo-${device.key}`, name: name ?? 'Demo' }, device, role: 'host', aiAssist: true, // ve la ayuda privada de la IA }) return send(res, 201, grant) } send(res, 404, { error: 'not found' }) } return createServer((req, res) => { route(req, res).catch((error: unknown) => { if (isCommsApiError(error)) { console.error('comms:', error.code, error.status, error.requestId) return send(res, error.status, { error: error.code }) } console.error(error) send(res, 500, { error: 'internal' }) }) }) } // `npm run server`: arranca con lo que diga el entorno (.env). if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { const apiKey = process.env['IACL_API_KEY'] ?? '' const apiUrl = process.env['IACL_API_URL'] ?? 'https://api.comms.iaconsultinglabs.com/v1' if (!/^iacl_(test|live)_/.test(apiKey)) { console.error('Falta IACL_API_KEY (iacl_test_… para probar). Copia .env.example a .env.') process.exit(1) } createQuickstartServer({ apiKey, apiUrl }).listen(3000, () => { console.log(`Backend en http://localhost:3000 (${apiKey.startsWith('iacl_test_') ? 'prueba: IA simulada' : 'producción'})`) }) } ``` ## 3. La página (1 minuto) `src/App.vue`: un botón que pide la reunión a tu backend y, dentro, `MeetingRoom`, la sala entera: escenario, botones, chat, personas, subtítulos (`captions`) y el panel privado de la IA (`ai-panel`). ```vue ``` Y que la página mande `/api` a tu backend (`vite.config.ts`): ```ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': 'http://localhost:3000' } }, }) ``` ## 4. Arranca (1 minuto) ```bash npx concurrently "npx tsx --env-file=.env server.ts" "npx vite" ``` Abre y pulsa «Empezar la reunión». Ábrela en otra pestaña con el mismo enlace de reunión y verás dos conexiones. - **Subtítulos:** el botón «Subtítulos» de la barra. Con la clave de prueba, el texto sale de un guion de prueba, no de tu voz: así ves cómo se pinta sin gastar nada. - **El panel de la IA:** sugerencias, respuestas con su fuente, datos del contexto y avisos, solo para quien tiene la ayuda (`aiAssist`), nunca para un invitado. - **Al terminar**, si la reunión pidió el cierre (`postMeeting`), te llega el resumen por webhook. ## 5. Y después | Quieres | Lee | |---|---| | Recibir lo que pasa en tu servidor (webhooks firmados, ponerse al día) | [integracion-de-cero.md](integracion-de-cero.md) | | Darle contexto a la IA, detectores propios y las salidas de tu modo | [ia-en-directo.md](ia-en-directo.md) | | Tu propia pantalla en vez de `MeetingRoom` | [widgets-vue.md](widgets-vue.md) | | Grabar | [recording.md](recording.md) | | Pasar a producción | [api-keys.md](api-keys.md): cambia a `iacl_live_…`, y configura otra vez el webhook, el destino de grabaciones y la IA con esa clave (cada modo tiene la suya) | Si no usas Node, todo esto se hace con HTTP a pelo: [curl.md](curl.md). --- # Integrar IACL Comms desde cero Esta guía lleva de una aplicación sin nada a una con reuniones, invitados, grabación e IA en directo, paso a paso y con todo el código. Cada bloque `ts` y `vue` de esta página **se compila en la CI** contra los paquetes publicados (`tools/docs/snippets.mjs`): si uno deja de funcionar, la CI falla. El mismo recorrido, ejecutable, está en [examples/node/backend.mjs](examples/node/backend.mjs) (el backend) y en `apps/example-vue` (la pantalla). ``` Navegador ── (1) pide acceso ──▶ TU backend ── clave de API ──▶ api.comms.iaconsultinglabs.com/v1 │ ▲ │ └── (2) entra con el token ──────┼───────────▶ medios (opaco) │ └──────── (3) webhooks firmados ◀────┘ ``` **La regla que no se rompe: la clave de API vive solo en tu backend.** Al navegador le llega un token de corta duración que vale para una conexión de una reunión. | Paquete | Dónde | Para qué | |---|---|---| | [`@iaconsultinglabs/comms-server`](../../packages/server/README.md) | Tu backend (Node 22+) | Reuniones, accesos, webhooks, grabaciones, IA | | [`@iaconsultinglabs/comms-vue`](../../packages/vue/README.md) | Tu web (Vue 3.5+) | La sala entera, o sus piezas | | [`@iaconsultinglabs/comms-core`](../../packages/core/README.md) | Tu web, sin Vue | La reunión como objeto con eventos | | [`@iaconsultinglabs/comms-contracts`](../../packages/contracts/README.md) | Los dos | Los tipos y las constantes del contrato | ## 0. Lo que necesitas 1. **Una cuenta y sus claves de API.** Date de alta en el portal de clientes de IACL Comms (te crea la cuenta con una clave de prueba) o pídeselas al equipo de IACL. - Empiezas con una clave de prueba (`iacl_test_.`). Las de producción (`iacl_live_…`) llegan con un plan de pago, o te las da el equipo de IACL, y la primera trae `keys:manage` para crear las demás. - Cada clave se enseña **una sola vez**: guárdala en tu gestor de secretos y pásala a tu backend por variable de entorno. - Con la de `keys:manage` creas las demás: una por backend y por entorno, cada una con solo lo que necesita. Lo ves en el apartado 2.5 y en [api-keys.md](api-keys.md). - Empieza con la de **prueba**: sus reuniones duran como mucho 15 minutos y tienen como mucho 4 personas, su IA es simulada (sin coste y sin mandar nada a ningún proveedor) y su consumo no se factura. - **Cada modo tiene su configuración**, como en Stripe: el webhook, el destino de grabaciones, los modos y los detectores que pongas con la clave de prueba son solo de prueba. Al pasar a producción, configúralos otra vez con la clave de producción ([api-keys.md](api-keys.md#la-configuración-por-modo)). 2. **Los paquetes.** Están en npm, con el scope `@iaconsultinglabs`, y se instalan sin token. 3. **Tu dominio dado de alta.** Dile a IACL desde qué orígenes se sirve tu web: la API solo acepta las llamadas del navegador que vengan de ellos. ## 1. Instala los paquetes ```bash # En el backend pnpm add @iaconsultinglabs/comms-server # En la web pnpm add @iaconsultinglabs/comms-core @iaconsultinglabs/comms-vue ``` `comms-contracts` llega como dependencia de los demás; instálalo aparte solo si usas sus constantes en tu código. ## 2. El backend ### 2.1 El cliente Uno por proceso. Reintenta lo que es seguro reintentar, corta a los 30 s y nunca escribe la clave en un log ni en un error. ```ts import { createCommsClient } from '@iaconsultinglabs/comms-server' export const comms = createCommsClient({ baseUrl: 'https://api.comms.iaconsultinglabs.com/v1', apiKey: process.env.COMMS_API_KEY ?? '', }) // La clave vale si la API contesta: una página vacía es un tenant sin reuniones. const page = await comms.listMeetings({ limit: 1 }) console.log(page.items.length) ``` ### 2.2 Crear reuniones y dar acceso Tu backend crea la reunión cuando tu aplicación lo necesita (una cita, un hilo, una llamada) y, cuando alguien quiere entrar, decide si puede y le da su acceso (`TokenGrant`). Este servidor `node:http` hace las dos cosas. Con Express o Fastify es lo mismo dentro de tus rutas. ```ts import { createServer, type IncomingMessage, type ServerResponse } from 'node:http' import { createCommsClient, isCommsApiError } from '@iaconsultinglabs/comms-server' import type { Device } from '@iaconsultinglabs/comms-contracts' const comms = createCommsClient({ baseUrl: 'https://api.comms.iaconsultinglabs.com/v1', apiKey: process.env.COMMS_API_KEY ?? '', }) /** TU APP: quién pide, según tu sesión. Aquí, una cabecera para probar en local. */ function currentUser(req: IncomingMessage): { id: string; name: string } | null { const id = req.headers['x-demo-user'] return typeof id === 'string' ? { id, name: 'Demo' } : null } async function readJson(req: IncomingMessage): Promise> { const chunks: Buffer[] = [] for await (const chunk of req) chunks.push(chunk as Buffer) return chunks.length > 0 ? (JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record) : {} } function send(res: ServerResponse, status: number, body: unknown): void { res.writeHead(status, { 'content-type': 'application/json' }) res.end(JSON.stringify(body)) } async function route(req: IncomingMessage, res: ServerResponse): Promise { const user = currentUser(req) if (!user) return send(res, 401, { error: 'login required' }) const url = new URL(req.url ?? '/', 'http://localhost') // Crear una reunión. `externalRef` es tu id; con él como Idempotency-Key, repetir no crea otra. if (req.method === 'POST' && url.pathname === '/api/meetings') { const { title, ref } = (await readJson(req)) as { title?: string; ref?: string } const meeting = await comms.createMeeting( { type: 'meeting', title: title ?? 'Reunión', ...(ref ? { externalRef: ref } : {}), settings: { guestsAllowed: true } }, ref ? { idempotencyKey: `meeting:${ref}` } : {}, ) return send(res, 201, { meetingId: meeting.id }) } // Dar el acceso. Lo pide el navegador (el `tokenSource` del paso 3) con su aparato. const match = /^\/api\/meetings\/(mtg_[0-9a-z]+)\/token$/.exec(url.pathname) if (req.method === 'POST' && match?.[1]) { const { device } = (await readJson(req)) as { device: Device } // TU APP: decide aquí si este usuario puede entrar en esta reunión y con qué rol. const grant = await comms.issueMemberToken(match[1], { person: { id: user.id, name: user.name }, device, role: 'host', }) return send(res, 201, grant) // el grant entero; nunca la clave de API } send(res, 404, { error: 'not found' }) } createServer((req, res) => { route(req, res).catch((error: unknown) => { // Decide por `code`, que es estable; el mensaje es para personas. if (isCommsApiError(error, 'meeting_ended')) return send(res, 409, { error: 'meeting_ended' }) if (isCommsApiError(error, 'plan_limit_reached')) return send(res, 422, { error: 'plan_limit_reached' }) if (isCommsApiError(error)) console.error('comms:', error.code, error.status, error.requestId) send(res, 500, { error: 'internal' }) }) }).listen(3000) ``` Para alguien de fuera, `issueGuestToken(meetingId, { person, device })`, solo si la reunión admite invitados (`settings.guestsAllowed`) y cuando ya ha pasado **tu** puerta: el enlace, la sala de espera y el consentimiento son tuyos, Comms no los guarda. `GuestLobby` y `KnockCards` (paso 3) son la parte visible de esa puerta. ### 2.3 Recibir los webhooks Comms te avisa de lo que pasa (empieza, entra alguien, termina, hay grabación, la IA detectó algo) con un `POST` firmado. Primero dile dónde, una sola vez; la respuesta trae el secreto de firma, **solo esa vez**: ```ts import { createCommsClient } from '@iaconsultinglabs/comms-server' const comms = createCommsClient({ baseUrl: 'https://api.comms.iaconsultinglabs.com/v1', apiKey: process.env.COMMS_API_KEY ?? '' }) const endpoint = await comms.setWebhookEndpoint({ url: 'https://app.example.org/hooks/comms' }) console.log('Guarda esto como COMMS_WEBHOOK_SECRET:', endpoint.secret) ``` Y monta el receptor. Comprueba la firma y el reloj, deduplica por `id` y solo contesta 2xx si tu código terminó bien, así que lo que falla se vuelve a entregar (durante 72 h): ```ts import { createServer } from 'node:http' import { createWebhookHandler } from '@iaconsultinglabs/comms-server' const webhooks = createWebhookHandler({ // El secreto actual y, durante una rotación, el anterior. secrets: () => [process.env.COMMS_WEBHOOK_SECRET, process.env.COMMS_WEBHOOK_SECRET_PREVIOUS].filter((s): s is string => Boolean(s)), handlers: { 'meeting.ended': async (event) => { console.log('terminó', event.data.meetingId, event.data.durationSeconds, 's') }, 'participant.joined': async (event) => { console.log('entró', event.data.personId, 'desde', event.data.device?.label ?? 'un aparato') }, }, // Jamás la clave ni el cuerpo en el log. onError: (error) => console.error('webhook:', error instanceof Error ? error.message : error), }) // Lee el cuerpo crudo él mismo: la firma cubre los bytes exactos. Con Express, `express.raw()`, // nunca `express.json()`, delante de esta ruta. createServer(webhooks.nodeListener()).listen(3001) ``` Tienes 10 s para contestar: el trabajo largo, fuera (guárdalo y responde). ### 2.4 Ponerse al día Si tu servidor estuvo caído, `reconcile` pide a la API lo que te perdiste desde el último cursor y lo procesa en orden, con la misma deduplicación que el webhook. Ejecútalo al arrancar y cada pocos minutos: ```ts import { MemoryCursorStore, MemoryIdempotencyStore, createCommsClient, reconcile } from '@iaconsultinglabs/comms-server' import type { AnyEvent } from '@iaconsultinglabs/comms-contracts' const comms = createCommsClient({ baseUrl: 'https://api.comms.iaconsultinglabs.com/v1', apiKey: process.env.COMMS_API_KEY ?? '' }) // En producción, los dos sobre tu base de datos, y el de idempotencia compartido con el webhook. const cursorStore = new MemoryCursorStore() const idempotency = new MemoryIdempotencyStore() async function onEvent(event: AnyEvent): Promise { console.log(event.type, event.id) // la MISMA función que usa tu webhook } const result = await reconcile({ client: comms, cursorStore, idempotency, onEvent, onCursorExpired: async () => { // Más de 72 h sin reconciliar: reconstruye tu estado con listMeetings/getMeeting. }, }) console.log(result.processed, 'nuevos,', result.duplicates, 'repetidos') ``` ### 2.5 Tus claves y tu consumo Con una clave con `keys:manage` gestionas las de tu cuenta: creas, cambias permisos y límites, rotas sin cortar y revocas. Con `usage:read`, lees el consumo del mes por clave. ```ts import { createCommsClient } from '@iaconsultinglabs/comms-server' const admin = createCommsClient({ apiKey: process.env.COMMS_ADMIN_KEY ?? '', baseUrl: 'https://api.comms.iaconsultinglabs.com/v1' }) // La clave del backend que crea reuniones: solo lo justo. const created = await admin.createApiKey({ label: 'Backend de reuniones', scopes: ['meetings:read', 'meetings:write', 'tokens:create', 'events:read', 'ai:read', 'ai:run'], }) console.log('Guárdala en el gestor de secretos ahora:', created.apiKey.prefix) // Rotar sin cortar: el secreto anterior sigue valiendo 24 h. await admin.rotateApiKey(created.apiKey.id) // Lo de este mes, por clave; lo de las claves de prueba va aparte, en `test`. const usage = await admin.getUsage({ breakdown: 'apiKey' }) console.log(usage.meetingMinutes, usage.test?.meetingMinutes ?? 0, usage.byApiKey?.length ?? 0) ``` - Sin el permiso de una ruta, la respuesta es `key_scope_missing`. - Al tope del mes de una clave, `key_limit_reached`. - Al pasar el límite de peticiones, `rate_limited` con `Retry-After`. Los permisos y los límites, uno por uno, están en [api-keys.md](api-keys.md). ## 3. La pantalla ### 3.1 La sala entera con `comms-vue` `MeetingRoom` es la reunión completa: escenario, botones, chat, personas, ajustes, grabación e IA. Lo único que necesita de ti es cómo pedir el token a tu backend: ```vue ``` Abre la página en dos pestañas: verás dos conexiones en la misma reunión. Los textos vienen en castellano, catalán, inglés y francés (`locale`), y cualquiera se cambia con `messages`. ### 3.2 Tus piezas con los composables Si quieres tu propia pantalla, los composables dan cada parte, reactiva, y los componentes sueltos (`CallStage`, `ControlsBar`, `ChatPanel`, `LiveCaptions`…) se combinan como quieras: ```vue ``` ### 3.3 Sin Vue: `comms-core` `comms-core` es la reunión como objeto: métodos para hacer y eventos para enterarte. Funciona con cualquier framework (o ninguno): ```ts import { createMeeting, currentDevice, isCommsError } from '@iaconsultinglabs/comms-core' const meeting = createMeeting({ apiUrl: 'https://api.comms.iaconsultinglabs.com/v1', tokenSource: async () => { const response = await fetch('/api/meetings/mtg_0000000000000000/token', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ device: currentDevice() }), }) return response.json() // el TokenGrant que devolvió tu backend }, }) meeting.on('state', (state) => console.log('conexión:', state.status, state.reason ?? '')) meeting.on('participants', (list) => { for (const one of list) { const video = document.querySelector(`video[data-identity="${one.identity}"]`) if (video && video.srcObject !== one.media.camera) video.srcObject = one.media.camera } }) meeting.on('error', (error) => console.warn(error.code)) try { await meeting.join({ microphone: true, camera: false }) } catch (error) { if (isCommsError(error, 'media_permission_denied')) console.log('Sin permiso de micro: entra sin él') else throw error } document.querySelector('#mute')?.addEventListener('click', () => void meeting.toggleMicrophone()) document.querySelector('#leave')?.addEventListener('click', () => void meeting.destroy()) ``` El audio de los demás lo reproduce el motor: no lo pongas en otro `