lightstream-sdk
v1.13.1
Published
Official TypeScript/JavaScript SDK for LightStream Gateway (Unified TikTok, Kick, and Twitch Real-time Streaming, REST APIs, and Moderation)
Downloads
3,231
Maintainers
Readme
lightstream-sdk
SDK oficial para consumir el Gateway de LightStream con TikTok LIVE, Kick y Twitch.
En 1.10.2, GiftEvent.giftType admite el tipo numérico de TikTok y el nombre
de tipo que publica Kick; no cambia el transporte ni la contabilidad.
En 1.10.1, la identidad de regalos se alinea con el journal del gateway:
un eventId reutilizado conserva cambios de msgId, groupId, progreso,
cierre y totalValue; una copia con otro cursor no se entrega dos veces.
GiftEvent declara también los campos de racha y las imágenes del gateway.
Para contar regalos del gateway unificado usa monetization.quantity y
monetization.amount, no interpretes comboCount como monedas. Para una
proyección incremental, agrupa por sesión, usuario, regalo y groupId solo
cuando giftType === 1 && isCombo !== false, acredita diferencias respecto
al máximo observado y no vuelvas a sumar el cierre. Sin grupo, espera el
final. Los regalos individuales se identifican por mensaje. El SDK entrega
payloads y marcas de replay; cada consumidor conserva su propia contabilidad.
En 1.8.0, stream.connect(platform, channel, options) conserva silent,
events, enableEnrichedAnalytics, enableGiftRadar y enableBattleAnalytics
en cada reconexión. La sesión y el dispositivo viajan en auth, nunca en query
strings. stream.disconnect() envía la salida explícita de la plataforma
antes de cerrar el transporte. Los cierres por red conservan el replay.
Contrato de autenticación
El Gateway separa dos autoridades:
- La API key identifica la integración de la aplicación. Debe permanecer en un servidor o BFF y solo se usa para crear una sesión.
- La sesión opaca identifica al usuario y al dispositivo. Se crea con
deviceIdy, opcionalmente,externalUserId. En navegador viaja como la cookie HttpOnlyls_gateway_session; en Node/Bun puede usarse elsessionTokenopaco devuelto contransport: 'token'.
El SDK no acepta JWT de Supabase, tokens de proveedor, tokens efímeros, credenciales en query string ni cookies separadas por plataforma.
Instalación
npm install lightstream-sdk
# o
bun add lightstream-sdkSesión y tiempo real en Node/Bun
import { createLightStreamClient } from 'lightstream-sdk'
const client = createLightStreamClient({
gatewayUrl: 'https://gateway.lightstream.lat',
apiKey: process.env.LIGHTSTREAM_API_KEY, // solo servidor
deviceId: 'bot-installation-01',
})
const session = await client.session.create({
deviceId: 'bot-installation-01',
externalUserId: 'creator-123',
platform: 'kick',
transport: 'token',
})
const authenticatedClient = createLightStreamClient({
gatewayUrl: 'https://gateway.lightstream.lat',
sessionToken: session.sessionToken,
deviceId: 'bot-installation-01',
})
authenticatedClient.stream.on('chat', (message) => {
console.log(message.text)
})
await authenticatedClient.stream.connect('kick', 'xqc')Para una app web, crea la sesión desde una ruta server-side con la API key y
deja que la respuesta establezca ls_gateway_session. El cliente del
navegador se crea solo con gatewayUrl; Socket.IO envía la cookie con
withCredentials.
OAuth de Kick y Twitch
await client.session.create({
deviceId: 'web-installation-01',
externalUserId: 'creator-123',
platform: 'twitch',
transport: 'cookie',
})
const { url } = await client.oauth.authorize('twitch', {
returnTo: 'https://your-app.example/oauth/complete',
})El Gateway conserva los tokens de Kick/Twitch y solo devuelve la URL, el estado y los metadatos de cuenta. Para desconectar una cuenta:
await client.oauth.disconnect('twitch')REST
Los endpoints REST de la integración reciben x-api-key desde un entorno
confiable. Las operaciones de sesión/OAuth además requieren la sesión opaca.
const live = await client.rest.bulkCheckStreams([
{ platform: 'tiktok', channel: 'charlidamelio' },
{ platform: 'kick', channel: 'xqc' },
{ platform: 'twitch', channel: 'shroud' },
])
await client.rest.moderation.mute('kick', 'xqc', 'spammer', 60)getUserEarnings() conserva el nombre histórico, pero devuelve actividad y
monetización en unidades nativas (nativeAmount, nativeQuantity, nativeUnit),
sin estimaciones ni conversiones de moneda. Los rankings comparan únicamente
scores nativos dentro de la misma plataforma. La moderación mute/ban filtra
eventos entregados por el gateway; borrar mensajes o cambiar comentarios upstream
no está disponible y responde HTTP 501.
getLiveStreams() devuelve un snapshot tipado de las conexiones creadas con la
misma API key. Para mantener un BFF sincronizado sin polling:
for await (const snapshot of client.rest.watchLiveStreams({}, { signal })) {
console.log(snapshot.total, snapshot.totalViewers, snapshot.streams)
}El feed usa SSE y permanece aislado por integración. Consúmalo solo en servidor; un BFF puede proyectar el resultado al navegador sin exponer la API key.
Eventos y errores
El cliente emite connected únicamente cuando el upstream confirma que el
canal está en directo. Cuando no lo está emite stream:offline con
STREAMER_OFFLINE. También expone status, error, chat, gift,
follow, like, subscribe, viewers y los eventos extendidos del Gateway.
Los errores de autenticación (AUTHENTICATION_FAILED) requieren crear o
renovar la sesión del dispositivo. La API key no debe enviarse al cliente del
navegador ni por URL.
Desarrollo local
bun install
bun run typecheck
bun test
bun run buildMIT (c) 2026 LightStream
Batallas y Power-Ups (1.13.1)
Todos viajan en el evento público battle, con BattleEvent, BattleCard, BoostCard, participantes, equipos, resultados y tareas tipados. Active enableBattleAnalytics: true al conectar; si filtra eventos incluya battle.
participants[].points es el total acumulado actual de cada participante, no un
incremento que deba sumarse. roundKey es la identidad estable de la ronda que
calcula el gateway; no use battleId por sí solo porque TikTok puede reutilizarlo
o enviar "0" como valor protobuf por defecto. Para consumidores que mantienen
su propio estado, el SDK incluye un reductor que conserva los totales, ignora
actualizaciones de otra ronda y cierra la batalla una sola vez.
El reductor espera un start; un update o end aislado no crea un marcador.
Desde 1.13.1 conserva la identidad de ronda, equipos, duración y cierre cancelado
sin atribuir una victoria a una ronda reemplazada. La deduplicación distingue
progresión de puntuación y cierre aunque compartan un identificador upstream.
El consumidor que no desea restaurar rondas previas debe excluir los eventos
_replay antes de llamar al reductor:
import { createBattleRoundState, reduceBattleRoundState } from 'lightstream-sdk'
let battle = createBattleRoundState()
client.stream.on('battle', event => {
battle = reduceBattleRoundState(battle, event)
if (battle.phase === 'active') {
console.log(battle.participants.map(({ id, points }) => ({ id, points })))
}
})También están disponibles normalizeBattleId(), normalizeBattleScore() y
createBattleRoundKey() para adaptadores propios. Un evento end conserva los
últimos totales y sus resultados; no se debe inferir el final únicamente por la
ausencia temporal de mensajes.
status: card:cardPhase === 'activated',card.action === 'use'yisEffectCardconfirman que TikTok lanzó/activó la carta. Los tipos son critical-strike, smoke, extra-time, special-effect, potion, wave, top2, top3 y vault-glove.cardNameKeyeimageUrlexponen el identificador visual y el asset HTTPS que acompaña la activación. obtain/award/notice/unknown no son activaciones.status: boost:boostPhase === 'available'yboostCardscontienen inventario (cardId/taskId/taskSource); no demuestran activación. El protocolo no incluye tipo de efecto ni battleId propio en este mensaje, aunque el gateway puede correlacionarlo con la batalla actualmente abierta.status: task:started/updateddescriben la tarea y su ventana prevista (scheduledAt); solotype === 'active'después deTASK_SETTLEexitoso confirma x2/x3.activeAt/activeUntildelimitan la activación real.status: update: triggerCriticalStrike, giftId, giftCount, diamonds y fromUserId permiten correlacionar puntuaciones sin disparar otra alerta de activación.status: punish-endtermina el castigo, no inicia ni termina otra ronda.- Epochs en milisegundos: timestamp, startedAt, sentAt y activeUntil. duration/extraDuration están en segundos. No inventar duración ni multiplicador si faltan.
- No ejecutar efectos sobre
_replay; descartar cartas expiradas y deduplicar por eventId dentro de la sesión. El SDK conserva la marca de replay y distingue progresión/cierre de regalos que reutilizan eventId.
stream:replay_start contiene afterCursor; stream:replay_complete contiene lastCursor y count, tal como los emite el gateway. La API key sigue exclusivamente en el servidor; Socket.IO requiere la sesión opaca.
El cliente REST incluye getLiveStreams, watchLiveStreams, bulkCheckStreams, getRoomStreamInfo, getUserEarnings, getGiftsCatalog, getLeaderboard, searchRankings y moderation; session y oauth mantienen sus clientes propios.
Regalos y animaciones (1.10.0)
El catálogo crece en el gateway: consultar rest.getGiftsCatalog() para el índice
compacto, o rest.getCombinedGiftCatalog() / rest.getRegionalGiftCatalog('US')
para conservar regiones. Los métodos regionales aceptan { signal }.
// Solo servidor/BFF, después de comprobar sesión y permisos del usuario.
import { LightStreamAnimationClient } from 'lightstream-sdk/server'
const animations = new LightStreamAnimationClient({
gatewayUrl: process.env.GATEWAY_INTERNAL_URL!,
internalToken: process.env.GATEWAY_AUTH_TOKEN!,
})
const catalog = await animations.getCatalog()
const asset = catalog.find(item => item.giftId === requestedGiftId)
if (!asset) throw new Error('Animation unavailable')
const response = await animations.playAsset(authorizedUserId, asset)
// El BFF transmite el cuerpo y X-Animation-Key / X-Animation-Id, con private,no-store.
// Nunca transmite GATEWAY_AUTH_TOKEN ni la clave maestra del gateway.// Navegador: la respuesta procede del BFF de tu aplicación.
import { decryptGiftAnimation } from 'lightstream-sdk'
const blob = await decryptGiftAnimation(responseFromYourBff, { giftId, signal })
const url = URL.createObjectURL(blob)
// Usar en <video>; revocar url al terminar, reemplazar o desmontar el reproductor.El subpath /server no está disponible en bundles de navegador. Tickets de 30 s,
de un uso y ligados a usuario/recurso; no se reintenta su consumo automáticamente.
La app debe validar entitlement y la presencia autorizada de overlays antes de
reproducir. El cifrado protege el transporte de aplicación, no impide la captura
por un consumidor autorizado. Solo el catálogo animado representa vídeos listos;
no todos los regalos ni todos los originales 3D tienen un vídeo reproducible.
Version 1.10.3 preserves typed badge params (level/rank/months) and chat emote
id/placeInComment. Render original CDN URLs without rewriting signatures.
superFanJoin with kind: entrance means an existing fan entered the live;
superFan with kind: upgrade means conversion. Do not label entrances as new fans.
Recuperación y normalización (1.13.0)
Mantén una instancia del cliente por conexión; el SDK reusa la sesión, deviceId,
canal y streamSessionId al reconectar. Desde 1.12.1 los errores transitorios tienen
un solo reintento automático por defecto. El presupuesto solo se restaura después
de 60 segundos conectado (reconnectStabilityMs), evitando bucles de conexiones
breves. Puedes ajustar maxReconnectAttempts o usar autoReconnect=false. No
recrees la sesión ni llames connect por cada status=reconnecting. Auth inválida,
canal inexistente/prohibido, rate limit fuera de drenado y offline confirmado no
se reintentan. retryExhausted distingue un fallo transitorio cuyo presupuesto se
agotó. standbyWhenOffline: true sigue siendo una excepción explícita para sondeo
continuo en directos prolongados.
server:restarting abre una ventana de recuperación independiente de 120 segundos
(plannedRestartTimeoutMs). Durante ella, los rechazos transitorios del contenedor
entrante no consumen el presupuesto normal. El SDK conserva sesión, deviceId,
streamSessionId y el cursor confirmado; al reconectar solicita resumeAfter y no
avanza el checkpoint hasta completar el replay. client.stream.resumeAfter permite
persistir el checkpoint fuera del proceso. La ventana no convierte errores de auth,
offline o canal prohibido en reintentables y no promete conservar el socket físico.
Todo evento de datos cruza normalizeGatewayEvent: valida plataforma, tipo, fechas
y cursor, rechaza crosstalk y produce el envelope público 1.0. Los gateways
anteriores siguen siendo compatibles porque los campos ausentes se completan en el
SDK; message se canoniza como chat y objetivos inválidos no se publican.
Errores iniciales y HTTP (1.13.0)
await client.stream.connect(...) rechaza si falla el handshake inicial de
Socket.IO. El evento error se conserva para consumidores reactivos y los
reintentos posteriores siguen la política configurada. Los clientes REST,
sesión y OAuth comparten el mismo transporte, timeouts y normalización de errores.
requestTimeoutMs configura el timeout HTTP común sin alterar el transporte realtime.
Redis conserva hasta 30 minutos / aproximadamente 100000 eventos por ámbito. Deduplicación del SDK acotada a 1000 identidades: efectos durables de negocio requieren idempotencia propia. El checkpoint en memoria no sobrevive a reiniciar el proceso; resumeAfter permite restaurar un cursor persistido por el integrador. No hay confirmación transaccional de efectos de handlers asíncronos. Este contrato recupera eventos confirmados en Redis, no eventos que la plataforma nunca entregó mientras su conexión upstream estuvo cerrada. Ningún orquestador transfiere sockets vivos entre procesos.
