feat(realtime): monitoramento em tempo real via WebSocket multi-tenant
Fecha agente.md secao 54-55 (infraestrutura) e 161 (WebSocket multi-tenant).
Entrega o pipeline de push em tempo real completo — o consumo visual
("Monitoramento -> Filas/Ramais") fica pra fase Frontend.
Requisito central da secao 161 ("nao transmitir tudo e filtrar so no
browser"): RealtimeGateway tem um unico ponto de emissao,
broadcastToTenant(), sempre server.to(`tenant:<id>`), nunca broadcast
global. Cada socket entra na room do proprio tenant no handshake, nunca
escolhe a room.
Autenticacao na conexao (handshake.auth.token, nao Authorization header):
valida o JWT (mesmo verifyAccessToken do JwtAuthGuard), exige tenantId no
token e a permission monitoring.view (ja existia desde RBAC, sem
consumidor ate agora) — mesmo principio de nunca confiar em tenant_id do
client, so do JWT ja emitido por /auth/select-tenant.
Origem dos eventos: canal Redis unico b2bcall:events (o mesmo desde Event
Socket). Dois produtores: b2bcall-fs-events (eventos do FreeSWITCH,
resolvendo tenantId por fan-out quando nao ha channel variable, ver
tenant-resolve.ts) e apps/api (mudancas no nosso Agent.state via
agents-me.controller, tenantId direto do JWT, sem fan-out).
Bug real achado e corrigido ao construir esta fase: nenhum evento CUSTOM do
ESL (sofia::register, sofia::gateway_state, callcenter::info) jamais
chegava em b2bcall-fs-events nesta sessao inteira. Causa: event_json(...)
mandava "CUSTOM" como ultimo token do comando `event json`, sem subclass
depois — mod_event_socket exige os subclasses logo depois do token CUSTOM
no mesmo comando pra serem entregues. Corrigido separando PLAIN_EVENTS
(viram listener .on()) de CUSTOM_SUBCLASSES (so compoem o comando de
assinatura). Resolve as lacunas ja documentadas em docs/TRUNKS.md e
docs/AGENTS.md. De quebra, corrigido um bug de nome de campo
(CC-Agent-Status, que nao existe -> CC-Agent-State) e um segundo bug real
em trunk-sync.ts (rescan nunca descarregava gateway removido -> agora roda
`killgw` antes do rescan).
Novos tipos normalizados a partir de callcenter::info, com nomes de campo
confirmados contra uma fila real: AGENT_OFFERED_CALL, AGENT_BRIDGE_FAILED,
QUEUE_MEMBER_COUNT (chamadas esperando, secao 54), QUEUE_MEMBER_LEFT (com
cause/cancelReason e timestamps — base pra Service Level/Abandon Rate
quando CDR existir).
Verificado ponta a ponta com um client socket.io real: login/pause/resume/
logout emitindo AGENT_STATE_CHANGED; chamada de teste numa fila com agente
logado emitindo QUEUE_MEMBER_COUNT/LEFT, AGENT_OFFERED_CALL,
AGENT_BRIDGE_FAILED, AGENT_STATUS_CHANGED (CC-Agent-State correto); token
ausente/invalido desconectado na hora, sem vazar nenhum evento.
Achado sistemico durante o teste (documentado, nao corrigido nesta fase):
@@unique combinado com soft delete, sem excluir deletedAt, em
Agent/Extension/Trunk/Queue/PauseReason — nao da pra reusar numero/nome/
codigo depois de apagar. Precisa de indice unico parcial em cada um, fora
do escopo desta fase.
typecheck do workspace inteiro limpo. ~144MB de memoria total nos
containers (fs-events 44MB, fs-config 45MB, freeswitch 26MB, postgres
21MB, redis 8MB).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
48
TODO.md
48
TODO.md
@@ -228,14 +228,52 @@
|
|||||||
resume→Available, logout→Logged Out — todos confirmados batendo
|
resume→Available, logout→Logged Out — todos confirmados batendo
|
||||||
entre `Agent.state` (banco) e `callcenter_config agent list`
|
entre `Agent.state` (banco) e `callcenter_config agent list`
|
||||||
(FreeSWITCH)
|
(FreeSWITCH)
|
||||||
- [ ] Estados derivados de chamada (RINGING/IN_CALL/WRAP_UP/RESERVED) —
|
- [x] Estados derivados de chamada (RINGING/IN_CALL/WRAP_UP/RESERVED) —
|
||||||
dependem de `callcenter::info` (CUSTOM event), ainda não provado
|
mecanismo de entrega do `callcenter::info` corrigido e confirmado
|
||||||
funcionando (mesma lacuna do `sofia::gateway_state`, ver docs/TRUNKS.md)
|
(ver PHASE 13); persistir em `Agent.state` ainda não implementado
|
||||||
- [ ] `PauseReason.maxDuration` não é aplicado automaticamente
|
- [ ] `PauseReason.maxDuration` não é aplicado automaticamente
|
||||||
- [ ] Quota de agentes — depende de Plans/Entitlements
|
- [ ] Quota de agentes — depende de Plans/Entitlements
|
||||||
|
- [ ] Achado sistêmico: `@@unique` combinado com soft delete (sem excluir
|
||||||
|
`deletedAt`) em `Agent`/`Extension`/`Trunk`/`Queue`/`PauseReason` —
|
||||||
|
não dá pra reusar número/nome/código depois de apagar. Precisa de
|
||||||
|
índice único parcial em cada um, migration própria (ver docs/AGENTS.md)
|
||||||
|
|
||||||
## PHASE 13+ — ver `agente.md` seções 54 em diante (Monitoramento em tempo real,
|
## PHASE 13 — Realtime Monitoring / WebSocket multi-tenant (agente.md secao 54-55, 161)
|
||||||
Predictive Dialer, Recordings, AI, Billing, Frontend, Reports, Security, Tests)
|
- [x] **Bug real, achado nesta fase**: nenhum evento CUSTOM do ESL
|
||||||
|
(`sofia::register`, `sofia::gateway_state`, `callcenter::info`) jamais
|
||||||
|
chegava em `b2bcall-fs-events` — `event_json(...)` mandava `"CUSTOM"`
|
||||||
|
como último token do comando `event json`, sem subclass depois
|
||||||
|
(mod_event_socket exige os subclasses logo depois do token CUSTOM no
|
||||||
|
mesmo comando). Corrigido separando `PLAIN_EVENTS`/`CUSTOM_SUBCLASSES`.
|
||||||
|
Resolve as lacunas documentadas em PHASE 09/12 e docs/TRUNKS.md/AGENTS.md.
|
||||||
|
- [x] Corrigido de quebra: `CC-Agent-Status` (não existe) → `CC-Agent-State`
|
||||||
|
(campo real); novos tipos normalizados `AGENT_OFFERED_CALL`,
|
||||||
|
`AGENT_BRIDGE_FAILED`, `QUEUE_MEMBER_COUNT`, `QUEUE_MEMBER_LEFT`
|
||||||
|
- [x] Corrigido de quebra: `sofia profile external rescan` nunca descarregava
|
||||||
|
um gateway cujo arquivo foi apagado (fantasma na memória do Sofia) —
|
||||||
|
`trunk-sync.ts` agora roda `killgw <nome>` pra cada gateway removido
|
||||||
|
- [x] `tenant-resolve.ts` (fs-events): resolve tenantId por fan-out
|
||||||
|
(agente/fila não carregam `b2bcall_tenant_id` — só existe a partir do
|
||||||
|
Predictive Engine), cacheado por id
|
||||||
|
- [x] `apps/api`: `RealtimeGateway` (socket.io sobre o Fastify HTTP server),
|
||||||
|
auth via JWT no handshake (`monitoring.view`), uma room por tenant
|
||||||
|
(`tenant:<id>`) — nunca broadcast global, sempre `server.to(room)`
|
||||||
|
(secao 161: tenant-scoped no servidor, nunca filtrar só no browser)
|
||||||
|
- [x] `RealtimeRedisBridge`: assina `b2bcall:events` (canal único, mesmo
|
||||||
|
usado desde Event Socket), reencaminha pro tenant certo
|
||||||
|
- [x] `AGENT_STATE_CHANGED` publicado direto de `agents-me.controller.ts`
|
||||||
|
(login/logout/pause/resume) — tenantId já vem do JWT, sem fan-out
|
||||||
|
- [x] Testado ponta a ponta: login/pause/resume/logout via WS, chamada de
|
||||||
|
teste numa fila real (QUEUE_MEMBER_COUNT/LEFT, AGENT_OFFERED_CALL,
|
||||||
|
AGENT_BRIDGE_FAILED, AGENT_STATUS_CHANGED com CC-Agent-State correto),
|
||||||
|
token inválido desconectado na hora
|
||||||
|
- [ ] Ramais/extensões (secao 55 completa: busy/registro) — precisa de SIP
|
||||||
|
real pra testar, e extrair ramal dos headers de canal (não feito)
|
||||||
|
- [ ] TME/TMA/Service Level/Abandon Rate — dependem de CDR (fase futura)
|
||||||
|
- [ ] Snapshot/reconciliação ao reconectar o WebSocket
|
||||||
|
|
||||||
|
## PHASE 14+ — ver `agente.md` seções 56 em diante (Predictive Dialer,
|
||||||
|
Recordings, AI, Billing, Frontend, Reports, Security, Tests)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,16 @@
|
|||||||
"@nestjs/common": "^12.0.1",
|
"@nestjs/common": "^12.0.1",
|
||||||
"@nestjs/core": "^12.0.1",
|
"@nestjs/core": "^12.0.1",
|
||||||
"@nestjs/platform-fastify": "^12.0.1",
|
"@nestjs/platform-fastify": "^12.0.1",
|
||||||
|
"@nestjs/platform-socket.io": "12.0.1",
|
||||||
|
"@nestjs/websockets": "12.0.1",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.15.1",
|
"class-validator": "^0.15.1",
|
||||||
"fast-xml-parser": "5.11.1",
|
"fast-xml-parser": "5.11.1",
|
||||||
"fastify": "5.12.1",
|
"fastify": "5.12.1",
|
||||||
"ioredis": "^6.0.0",
|
"ioredis": "^6.0.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2"
|
"rxjs": "^7.8.2",
|
||||||
|
"socket.io": "4.8.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { JwtAuthGuard } from "../common/guards/jwt-auth.guard";
|
|||||||
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
import { CurrentUser } from "../common/decorators/current-user.decorator";
|
||||||
import { PauseDto } from "./dto/pause.dto";
|
import { PauseDto } from "./dto/pause.dto";
|
||||||
import { notifyAgentChanged, notifyTierChanged } from "./agent-sync.helper";
|
import { notifyAgentChanged, notifyTierChanged } from "./agent-sync.helper";
|
||||||
|
import { publishAgentStateChanged } from "../realtime/realtime-publish.helper";
|
||||||
|
|
||||||
async function findMyAgent(tx: Prisma.TransactionClient, tenantId: string, userId: string) {
|
async function findMyAgent(tx: Prisma.TransactionClient, tenantId: string, userId: string) {
|
||||||
const agent = await tx.agent.findFirst({
|
const agent = await tx.agent.findFirst({
|
||||||
@@ -69,6 +70,7 @@ export class AgentsMeController {
|
|||||||
for (const tier of agent.tiers) {
|
for (const tier of agent.tiers) {
|
||||||
await notifyTierChanged(tenantId, tier.queueId, agent.id, "upsert", tier.level, tier.position);
|
await notifyTierChanged(tenantId, tier.queueId, agent.id, "upsert", tier.level, tier.position);
|
||||||
}
|
}
|
||||||
|
await publishAgentStateChanged(tenantId, agent.id, "AVAILABLE");
|
||||||
|
|
||||||
return { state: "AVAILABLE" };
|
return { state: "AVAILABLE" };
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,7 @@ export class AgentsMeController {
|
|||||||
await recordAuditEvent(prisma, { action: "AGENT_LOGOUT", tenantId, userId: user.sub, entityType: "agent", entityId: agent.id });
|
await recordAuditEvent(prisma, { action: "AGENT_LOGOUT", tenantId, userId: user.sub, entityType: "agent", entityId: agent.id });
|
||||||
|
|
||||||
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
||||||
|
await publishAgentStateChanged(tenantId, agent.id, "OFFLINE");
|
||||||
|
|
||||||
return { state: "OFFLINE" };
|
return { state: "OFFLINE" };
|
||||||
}
|
}
|
||||||
@@ -141,6 +144,7 @@ export class AgentsMeController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
||||||
|
await publishAgentStateChanged(tenantId, agent.id, "PAUSED");
|
||||||
|
|
||||||
return { state: "PAUSED" };
|
return { state: "PAUSED" };
|
||||||
}
|
}
|
||||||
@@ -167,6 +171,7 @@ export class AgentsMeController {
|
|||||||
await recordAuditEvent(prisma, { action: "AGENT_RESUME", tenantId, userId: user.sub, entityType: "agent", entityId: agent.id });
|
await recordAuditEvent(prisma, { action: "AGENT_RESUME", tenantId, userId: user.sub, entityType: "agent", entityId: agent.id });
|
||||||
|
|
||||||
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
await notifyAgentChanged(tenantId, agent.id, "upsert");
|
||||||
|
await publishAgentStateChanged(tenantId, agent.id, "AVAILABLE");
|
||||||
|
|
||||||
return { state: "AVAILABLE" };
|
return { state: "AVAILABLE" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { DialplanModule } from "./dialplan/dialplan.module";
|
|||||||
import { QueuesModule } from "./queues/queues.module";
|
import { QueuesModule } from "./queues/queues.module";
|
||||||
import { AgentsModule } from "./agents/agents.module";
|
import { AgentsModule } from "./agents/agents.module";
|
||||||
import { PauseReasonsModule } from "./pause-reasons/pause-reasons.module";
|
import { PauseReasonsModule } from "./pause-reasons/pause-reasons.module";
|
||||||
|
import { RealtimeModule } from "./realtime/realtime.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,6 +19,7 @@ import { PauseReasonsModule } from "./pause-reasons/pause-reasons.module";
|
|||||||
QueuesModule,
|
QueuesModule,
|
||||||
AgentsModule,
|
AgentsModule,
|
||||||
PauseReasonsModule,
|
PauseReasonsModule,
|
||||||
|
RealtimeModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { NestFactory } from "@nestjs/core";
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { FastifyAdapter, type NestFastifyApplication } from "@nestjs/platform-fastify";
|
import { FastifyAdapter, type NestFastifyApplication } from "@nestjs/platform-fastify";
|
||||||
|
import { IoAdapter } from "@nestjs/platform-socket.io";
|
||||||
import { ValidationPipe } from "@nestjs/common";
|
import { ValidationPipe } from "@nestjs/common";
|
||||||
import helmet from "@fastify/helmet";
|
import helmet from "@fastify/helmet";
|
||||||
import cors from "@fastify/cors";
|
import cors from "@fastify/cors";
|
||||||
@@ -42,6 +43,11 @@ async function bootstrap() {
|
|||||||
|
|
||||||
app.useGlobalFilters(new DomainExceptionFilter());
|
app.useGlobalFilters(new DomainExceptionFilter());
|
||||||
|
|
||||||
|
// Monitoramento em tempo real (agente.md secao 54-55, 161) — socket.io
|
||||||
|
// sobre o mesmo servidor HTTP do Fastify, path proprio pra nao colidir
|
||||||
|
// com as rotas REST.
|
||||||
|
app.useWebSocketAdapter(new IoAdapter(app));
|
||||||
|
|
||||||
const port = Number(process.env.API_PORT ?? 3000);
|
const port = Number(process.env.API_PORT ?? 3000);
|
||||||
await app.listen(port, "127.0.0.1");
|
await app.listen(port, "127.0.0.1");
|
||||||
console.log(`b2bcall-api ouvindo em http://127.0.0.1:${port}`);
|
console.log(`b2bcall-api ouvindo em http://127.0.0.1:${port}`);
|
||||||
|
|||||||
25
apps/api/src/realtime/realtime-publish.helper.ts
Normal file
25
apps/api/src/realtime/realtime-publish.helper.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { NormalizedEvent } from "@b2bcall/telephony";
|
||||||
|
import { getRedisClient } from "../common/redis";
|
||||||
|
|
||||||
|
const EVENTS_CHANNEL = "b2bcall:events";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publica no mesmo canal que b2bcall-fs-events usa pros eventos vindos do
|
||||||
|
* FreeSWITCH — o RealtimeRedisBridge (apps/api) não distingue a origem, só
|
||||||
|
* reencaminha pro tenant certo via WebSocket (docs/REALTIME.md). Diferente
|
||||||
|
* dos eventos vindos do ESL, aqui o tenantId já é conhecido de cara (vem do
|
||||||
|
* JWT da requisição HTTP) — não precisa do fan-out de tenant-resolve.ts.
|
||||||
|
*/
|
||||||
|
export async function publishAgentStateChanged(
|
||||||
|
tenantId: string,
|
||||||
|
agentId: string,
|
||||||
|
state: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const event: NormalizedEvent = {
|
||||||
|
type: "AGENT_STATE_CHANGED",
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
tenantId,
|
||||||
|
data: { agentId, state },
|
||||||
|
};
|
||||||
|
await getRedisClient().publish(EVENTS_CHANNEL, JSON.stringify(event));
|
||||||
|
}
|
||||||
52
apps/api/src/realtime/realtime-redis-bridge.service.ts
Normal file
52
apps/api/src/realtime/realtime-redis-bridge.service.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
|
||||||
|
import Redis from "ioredis";
|
||||||
|
import type { NormalizedEvent } from "@b2bcall/telephony";
|
||||||
|
import { RealtimeGateway } from "./realtime.gateway";
|
||||||
|
|
||||||
|
const EVENTS_CHANNEL = "b2bcall:events";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pub/sub exige uma conexão Redis dedicada em modo subscriber (não pode
|
||||||
|
* compartilhar com o client usado pra comandos normais) — mesmo padrão já
|
||||||
|
* usado em fs-config/fs-events.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RealtimeRedisBridge implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private readonly logger = new Logger(RealtimeRedisBridge.name);
|
||||||
|
private subscriber?: Redis;
|
||||||
|
|
||||||
|
constructor(private readonly gateway: RealtimeGateway) {}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<void> {
|
||||||
|
const url = process.env.REDIS_URL;
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("REDIS_URL nao configurado");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subscriber = new Redis(url);
|
||||||
|
this.subscriber.on("error", (err) => this.logger.error(`erro na conexao Redis (subscriber): ${String(err)}`));
|
||||||
|
await this.subscriber.subscribe(EVENTS_CHANNEL);
|
||||||
|
|
||||||
|
this.subscriber.on("message", (_channel, raw) => {
|
||||||
|
let event: NormalizedEvent;
|
||||||
|
try {
|
||||||
|
event = JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`evento invalido no canal ${EVENTS_CHANNEL}: ${String(err)}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!event.tenantId) {
|
||||||
|
// Sem tenant resolvido (ex.: chamada sintética sem agente/fila real
|
||||||
|
// por trás) — nada pra emitir, nenhuma room saberia receber.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.gateway.broadcastToTenant(event.tenantId, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log("bridge Redis -> WebSocket conectado");
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy(): Promise<void> {
|
||||||
|
await this.subscriber?.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
64
apps/api/src/realtime/realtime.gateway.ts
Normal file
64
apps/api/src/realtime/realtime.gateway.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { OnGatewayConnection, OnGatewayDisconnect, WebSocketGateway, WebSocketServer } from "@nestjs/websockets";
|
||||||
|
import type { Server, Socket } from "socket.io";
|
||||||
|
import { verifyAccessToken, userHasPermission } from "@b2bcall/auth";
|
||||||
|
import type { NormalizedEvent } from "@b2bcall/telephony";
|
||||||
|
|
||||||
|
function tenantRoom(tenantId: string): string {
|
||||||
|
return `tenant:${tenantId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monitoramento em tempo real (agente.md secao 54-55, 161: "eventos
|
||||||
|
* WebSocket devem ser tenant-scoped no servidor, nunca transmitir tudo e
|
||||||
|
* filtrar só no browser"). `broadcastToTenant` é o único ponto de emissão —
|
||||||
|
* sempre `server.to(room)`, nunca `server.emit()` global.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
@WebSocketGateway({
|
||||||
|
cors: {
|
||||||
|
origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(",") : false,
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
path: "/realtime",
|
||||||
|
})
|
||||||
|
export class RealtimeGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
|
private readonly logger = new Logger(RealtimeGateway.name);
|
||||||
|
|
||||||
|
@WebSocketServer()
|
||||||
|
server!: Server;
|
||||||
|
|
||||||
|
async handleConnection(client: Socket): Promise<void> {
|
||||||
|
const token = client.handshake.auth?.token as string | undefined;
|
||||||
|
if (!token) {
|
||||||
|
client.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const claims = await verifyAccessToken(token);
|
||||||
|
if (!claims.tenantId) {
|
||||||
|
client.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const allowed = await userHasPermission(claims.sub, "monitoring.view", claims.tenantId);
|
||||||
|
if (!allowed) {
|
||||||
|
client.disconnect(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.join(tenantRoom(claims.tenantId));
|
||||||
|
this.logger.log(`cliente conectado ao monitoramento (tenant ${claims.tenantId})`);
|
||||||
|
} catch {
|
||||||
|
client.disconnect(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDisconnect(): void {
|
||||||
|
// socket.io já limpa as rooms sozinho na desconexão — nada a fazer aqui.
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcastToTenant(tenantId: string, event: NormalizedEvent): void {
|
||||||
|
this.server.to(tenantRoom(tenantId)).emit("event", event);
|
||||||
|
}
|
||||||
|
}
|
||||||
8
apps/api/src/realtime/realtime.module.ts
Normal file
8
apps/api/src/realtime/realtime.module.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { RealtimeGateway } from "./realtime.gateway";
|
||||||
|
import { RealtimeRedisBridge } from "./realtime-redis-bridge.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [RealtimeGateway, RealtimeRedisBridge],
|
||||||
|
})
|
||||||
|
export class RealtimeModule {}
|
||||||
@@ -3,6 +3,7 @@ import type { FreeSwitchEventData } from "esl";
|
|||||||
import { FreeSwitchTelephonyProvider, normalizeEslEvent } from "@b2bcall/telephony";
|
import { FreeSwitchTelephonyProvider, normalizeEslEvent } from "@b2bcall/telephony";
|
||||||
import { createLogger } from "@b2bcall/shared";
|
import { createLogger } from "@b2bcall/shared";
|
||||||
import { updateTrunkStatusFromGatewayEvent } from "./trunk-status";
|
import { updateTrunkStatusFromGatewayEvent } from "./trunk-status";
|
||||||
|
import { resolveTenantIdForAgent, resolveTenantIdForQueue } from "./tenant-resolve";
|
||||||
|
|
||||||
const logger = createLogger("b2bcall-fs-events");
|
const logger = createLogger("b2bcall-fs-events");
|
||||||
|
|
||||||
@@ -97,7 +98,27 @@ async function main() {
|
|||||||
logger.error("erro no client ESL", { error: String(err) });
|
logger.error("erro no client ESL", { error: String(err) });
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleEvent(eventName: string, raw: FreeSwitchEventData) {
|
// callcenter::info/sofia::gateway_state não carregam b2bcall_tenant_id
|
||||||
|
// (só existe como channel variable a partir do Predictive Engine) — pra
|
||||||
|
// esses tipos, o tenant é resolvido pelo id do agente/fila embutido no
|
||||||
|
// nome FreeSWITCH (tenant-resolve.ts). Sem isso, o WebSocket multi-tenant
|
||||||
|
// (docs/REALTIME.md) não teria como saber pra qual tenant emitir.
|
||||||
|
async function resolveTenantId(normalized: ReturnType<typeof normalizeEslEvent>): Promise<string | undefined> {
|
||||||
|
if (!normalized || normalized.tenantId) return normalized?.tenantId;
|
||||||
|
switch (normalized.type) {
|
||||||
|
case "AGENT_STATUS_CHANGED":
|
||||||
|
case "AGENT_OFFERED_CALL":
|
||||||
|
case "AGENT_BRIDGE_FAILED":
|
||||||
|
return resolveTenantIdForAgent(normalized.data.agent as string | undefined);
|
||||||
|
case "QUEUE_MEMBER_COUNT":
|
||||||
|
case "QUEUE_MEMBER_LEFT":
|
||||||
|
return resolveTenantIdForQueue(normalized.data.queue as string | undefined);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleEvent(eventName: string, raw: FreeSwitchEventData) {
|
||||||
// Para eventos JSON (event_json), os campos reais do evento FreeSWITCH
|
// Para eventos JSON (event_json), os campos reais do evento FreeSWITCH
|
||||||
// (Event-Name, Unique-ID, Event-Subclass, variable_*, ...) vem em
|
// (Event-Name, Unique-ID, Event-Subclass, variable_*, ...) vem em
|
||||||
// `raw.body`; `raw.headers` são só os headers do protocolo ESL.
|
// `raw.body`; `raw.headers` são só os headers do protocolo ESL.
|
||||||
@@ -107,6 +128,12 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
normalized.tenantId = await resolveTenantId(normalized);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("falha ao resolver tenant do evento", { error: String(err), type: normalized.type });
|
||||||
|
}
|
||||||
|
|
||||||
redis.publish(REDIS_CHANNEL, JSON.stringify(normalized)).catch((err) => {
|
redis.publish(REDIS_CHANNEL, JSON.stringify(normalized)).catch((err) => {
|
||||||
logger.error("falha ao publicar evento normalizado no Redis", { error: String(err) });
|
logger.error("falha ao publicar evento normalizado no Redis", { error: String(err) });
|
||||||
});
|
});
|
||||||
|
|||||||
49
apps/freeswitch-events/src/tenant-resolve.ts
Normal file
49
apps/freeswitch-events/src/tenant-resolve.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { getPrismaClient, withTenantContext, type Prisma } from "@b2bcall/database";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eventos de callcenter/gateway não carregam tenant_id (só existe como
|
||||||
|
* channel variable a partir do Predictive Engine, ver normalize-event.ts) —
|
||||||
|
* mas o id do FreeSWITCH (`<uuid>@dominio`) É o id primário da nossa linha
|
||||||
|
* (Agent.id/Queue.id), então dá pra achar o tenant dono procurando em cada
|
||||||
|
* tenant ativo (mesmo padrão de fan-out de trunk-status.ts). Cacheado por id
|
||||||
|
* — um agente/fila nunca muda de tenant, então uma entrada no cache nunca
|
||||||
|
* fica desatualizada (só inútil, se o registro for apagado depois).
|
||||||
|
*/
|
||||||
|
const cache = new Map<string, string>();
|
||||||
|
|
||||||
|
function extractId(fsName: string | undefined): string | undefined {
|
||||||
|
if (!fsName) return undefined;
|
||||||
|
return fsName.split("@")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveViaFanOut(
|
||||||
|
cacheKey: string,
|
||||||
|
lookup: (tx: Prisma.TransactionClient, tenantId: string) => Promise<unknown>,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const cached = cache.get(cacheKey);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
const tenants = await prisma.tenant.findMany({ where: { status: "ACTIVE" }, select: { id: true } });
|
||||||
|
|
||||||
|
for (const tenant of tenants) {
|
||||||
|
const found = await withTenantContext(prisma, tenant.id, (tx) => lookup(tx, tenant.id));
|
||||||
|
if (found) {
|
||||||
|
cache.set(cacheKey, tenant.id);
|
||||||
|
return tenant.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTenantIdForAgent(agentFsName: string | undefined): Promise<string | undefined> {
|
||||||
|
const id = extractId(agentFsName);
|
||||||
|
if (!id) return undefined;
|
||||||
|
return resolveViaFanOut(`agent:${id}`, (tx) => tx.agent.findUnique({ where: { id } }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveTenantIdForQueue(queueFsName: string | undefined): Promise<string | undefined> {
|
||||||
|
const id = extractId(queueFsName);
|
||||||
|
if (!id) return undefined;
|
||||||
|
return resolveViaFanOut(`queue:${id}`, (tx) => tx.queue.findUnique({ where: { id } }));
|
||||||
|
}
|
||||||
@@ -108,6 +108,20 @@ FreeSWITCH (`agent list`).
|
|||||||
- Quota de agentes (`max_agents`) — depende de Plans/Entitlements.
|
- Quota de agentes (`max_agents`) — depende de Plans/Entitlements.
|
||||||
- `PauseReason.maxDuration` existe no modelo mas não é aplicado
|
- `PauseReason.maxDuration` existe no modelo mas não é aplicado
|
||||||
automaticamente ainda (ninguém força o fim da pausa ao expirar).
|
automaticamente ainda (ninguém força o fim da pausa ao expirar).
|
||||||
|
- **Achado ao testar a fase Realtime Monitoring, sistêmico (não é só
|
||||||
|
Agent)**: `@@unique([tenantId, userId])` em `Agent` não exclui
|
||||||
|
`deletedAt` — apagar um agente (soft delete) e tentar criar outro pro
|
||||||
|
mesmo usuário no mesmo tenant falha com `Unique constraint failed`,
|
||||||
|
porque a linha apagada continua ocupando o slot único indefinidamente.
|
||||||
|
Confirmado ao vivo durante o teste desta fase. O mesmo padrão
|
||||||
|
(`@@unique` combinado com soft delete, sem excluir `deletedAt`) existe
|
||||||
|
em pelo menos mais 4 models: `Extension` (`tenantId, number`), `Trunk`
|
||||||
|
(`tenantId, name`), `Queue` (`tenantId, name`), `PauseReason`
|
||||||
|
(`tenantId, code`) — todos vão ter o mesmo problema (não dá pra reusar
|
||||||
|
um número/nome/código depois de apagar). Precisa de um índice único
|
||||||
|
parcial (`WHERE deleted_at IS NULL`) em cada um — não corrigido nesta
|
||||||
|
fase (é uma migration própria tocando 5 tabelas, fora do escopo de
|
||||||
|
Realtime Monitoring); fica registrado aqui pra não se perder.
|
||||||
|
|
||||||
## Correção: eventos CUSTOM (`callcenter::info`) nunca chegavam
|
## Correção: eventos CUSTOM (`callcenter::info`) nunca chegavam
|
||||||
|
|
||||||
|
|||||||
129
docs/REALTIME.md
Normal file
129
docs/REALTIME.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Monitoramento em tempo real (WebSocket)
|
||||||
|
|
||||||
|
Agente.md secao 54-55 (filas/ramais) e 161 (WebSocket multi-tenant). Esta
|
||||||
|
fase entrega a infraestrutura de push em tempo real — o consumo visual
|
||||||
|
("Monitoramento → Filas/Ramais", cards coloridos) fica pra fase Frontend.
|
||||||
|
|
||||||
|
## Requisito central: tenant-scoped no servidor
|
||||||
|
|
||||||
|
Secao 161: "Não transmitir tudo e filtrar somente no browser." O gateway
|
||||||
|
(`apps/api/src/realtime/realtime.gateway.ts`) só tem **um** ponto de
|
||||||
|
emissão, `broadcastToTenant()`, e ele sempre usa `server.to(room)` — nunca
|
||||||
|
`server.emit()` global. Cada socket só entra na room do próprio tenant
|
||||||
|
(`tenant:<tenantId>`) na conexão, nunca escolhe a room ele mesmo.
|
||||||
|
|
||||||
|
## Autenticação na conexão
|
||||||
|
|
||||||
|
Socket.io não usa headers HTTP como uma request REST — o client manda o
|
||||||
|
access token em `socket.handshake.auth.token` (não em `Authorization`).
|
||||||
|
`RealtimeGateway.handleConnection`:
|
||||||
|
|
||||||
|
1. Rejeita (`disconnect(true)`) se não tiver token.
|
||||||
|
2. Valida o JWT (`verifyAccessToken`, mesmo helper usado no `JwtAuthGuard`).
|
||||||
|
3. Exige `tenantId` no token (mesmo princípio do `PermissionGuard`: nunca
|
||||||
|
um tenant vindo do client, só do JWT já emitido por `/auth/select-tenant`).
|
||||||
|
4. Exige a permission `monitoring.view` (`userHasPermission`, já existia
|
||||||
|
desde a fase RBAC — só não tinha nenhum consumidor ainda).
|
||||||
|
5. Só então entra na room do tenant.
|
||||||
|
|
||||||
|
Testado: token ausente/inválido → desconectado na hora, nenhum evento
|
||||||
|
vaza. Token válido → entra na room certa, recebe eventos do próprio tenant.
|
||||||
|
|
||||||
|
## Origem dos eventos
|
||||||
|
|
||||||
|
Um único canal Redis, `b2bcall:events` (o mesmo que já existia desde a fase
|
||||||
|
Event Socket) — o `RealtimeRedisBridge` (`apps/api`) assina esse canal com
|
||||||
|
uma conexão dedicada (pub/sub exige conexão própria, não pode compartilhar
|
||||||
|
com a usada pra comandos) e reencaminha pra `broadcastToTenant()` quando o
|
||||||
|
evento já tem `tenantId` resolvido; sem tenant resolvido, o evento é
|
||||||
|
descartado (nenhuma room saberia receber).
|
||||||
|
|
||||||
|
Dois produtores publicam nesse canal:
|
||||||
|
|
||||||
|
1. **`b2bcall-fs-events`** (eventos do FreeSWITCH, normalizados — ver
|
||||||
|
`packages/telephony/src/normalize-event.ts`). A maioria desses eventos
|
||||||
|
(`callcenter::info`, `sofia::gateway_state`) não carrega
|
||||||
|
`b2bcall_tenant_id` como channel variable (só existe a partir do
|
||||||
|
Predictive Engine) — o tenant é resolvido pelo id do agente/fila embutido
|
||||||
|
no nome FreeSWITCH (`<uuid>@dominio`), com fan-out sobre os tenants
|
||||||
|
ativos (mesmo padrão de `trunk-status.ts`), cacheado por id em
|
||||||
|
`tenant-resolve.ts` (agente/fila nunca troca de tenant, cache nunca fica
|
||||||
|
desatualizado). Isso só passou a funcionar depois da correção do bug de
|
||||||
|
subscrição de eventos CUSTOM — ver docs/AGENTS.md e docs/EVENT_SOCKET.md.
|
||||||
|
2. **`apps/api`** (mudanças no nosso próprio `Agent.state`, via
|
||||||
|
`realtime-publish.helper.ts`, chamado de dentro de
|
||||||
|
`agents-me.controller.ts` em login/logout/pause/resume). Aqui o
|
||||||
|
`tenantId` já vem direto do JWT da requisição HTTP — sem fan-out.
|
||||||
|
|
||||||
|
## Tipos de evento emitidos
|
||||||
|
|
||||||
|
- `AGENT_STATE_CHANGED`: nosso enum próprio (`OFFLINE`/`AVAILABLE`/
|
||||||
|
`PAUSED`/...), só muda via login/logout/pause/resume.
|
||||||
|
- `AGENT_STATUS_CHANGED`: `CC-Agent-State` bruto do mod_callcenter
|
||||||
|
(`Waiting`/`Receiving`/...) — vocabulário diferente do de cima, não dá
|
||||||
|
pra misturar (mesmo agente pode estar "AVAILABLE" no nosso enum e
|
||||||
|
"Receiving" no mod_callcenter simultaneamente, description de momentos
|
||||||
|
diferentes do ciclo de uma chamada).
|
||||||
|
- `QUEUE_MEMBER_COUNT`: contagem ao vivo de chamadas esperando por fila
|
||||||
|
(`CC-Count`) — a peça central da secao 54 ("Chamadas esperando").
|
||||||
|
- `QUEUE_MEMBER_LEFT`: uma chamada saiu da fila, com `cause`/`cancelReason`
|
||||||
|
e timestamps de entrada/saída — atendida vs. abandonada, base pro cálculo
|
||||||
|
futuro de Service Level/Abandon Rate.
|
||||||
|
- `AGENT_OFFERED_CALL` / `AGENT_BRIDGE_FAILED`: uma chamada foi ofertada a
|
||||||
|
um agente / falhou ao bridgear (ex.: `USER_NOT_REGISTERED`).
|
||||||
|
- `CALL_CREATED`/`CALL_ANSWERED`/`CALL_ENDED`: já existiam desde a fase
|
||||||
|
Event Socket, mas só carregam `tenantId` quando o `b2bcall_tenant_id`
|
||||||
|
channel variable existir (chamadas puramente sintéticas de teste, como as
|
||||||
|
usadas pra verificar esta fase, não têm — não chegam no WebSocket).
|
||||||
|
|
||||||
|
## Verificado ponta a ponta
|
||||||
|
|
||||||
|
Client de teste com `socket.io-client`, autenticado com o JWT de um tenant
|
||||||
|
de teste, na room `tenant:<id>`:
|
||||||
|
|
||||||
|
```
|
||||||
|
login do agente → AGENT_STATE_CHANGED {state: "AVAILABLE"}
|
||||||
|
pause → AGENT_STATE_CHANGED {state: "PAUSED"}
|
||||||
|
resume → AGENT_STATE_CHANGED {state: "AVAILABLE"}
|
||||||
|
logout → AGENT_STATE_CHANGED {state: "OFFLINE"}
|
||||||
|
|
||||||
|
originate null/dummy &callcenter(fila@dominio), com agente logado numa fila:
|
||||||
|
→ QUEUE_MEMBER_COUNT {count: 3}
|
||||||
|
→ AGENT_STATUS_CHANGED {state: "Receiving"}
|
||||||
|
→ AGENT_OFFERED_CALL
|
||||||
|
→ AGENT_BRIDGE_FAILED {hangupCause: "USER_NOT_REGISTERED"} (ramal sem SIP real registrado)
|
||||||
|
→ QUEUE_MEMBER_LEFT {cause: "Cancel", cancelReason: "TIMEOUT"}
|
||||||
|
→ QUEUE_MEMBER_COUNT {count: 2}
|
||||||
|
|
||||||
|
token ausente/inválido → socket desconectado na hora, nenhum evento recebido.
|
||||||
|
```
|
||||||
|
|
||||||
|
Todos chegaram só na room do tenant certo, com o `tenantId` batendo.
|
||||||
|
|
||||||
|
## O que falta
|
||||||
|
|
||||||
|
- **Ramais/extensões (secao 55 completa)**: as cores dependem de "ocupado"
|
||||||
|
(busy — CHANNEL_ANSWER/HANGUP tied a um ramal específico) e "offline"
|
||||||
|
(registro SIP — `sofia::register`/`unregister`, cujo mecanismo de entrega
|
||||||
|
agora funciona, mas nunca foi exercitado com um client SIP real nesta
|
||||||
|
sessão). Além disso, resolver **qual ramal** um evento de canal pertence
|
||||||
|
exige extrair o número de discagem dos headers (`Caller-Destination-Number`/
|
||||||
|
`Channel-Name`), não implementado ainda — e o mesmo limite de domínio
|
||||||
|
compartilhado entre tenants já documentado em `docs/EXTENSIONS.md` afeta
|
||||||
|
esse mapeamento. Não implementado; fica pra quando houver um client SIP de
|
||||||
|
verdade pra testar.
|
||||||
|
- **Persistir `AGENT_STATUS_CHANGED`/`QUEUE_MEMBER_COUNT` em `Agent.state`
|
||||||
|
ou numa tabela de snapshot de fila**: hoje só passam pelo WebSocket, não
|
||||||
|
gravam nada — suficiente pro requisito "tempo real", mas sem histórico
|
||||||
|
consultável fora da tabela `agent_state_events` (que só reflete
|
||||||
|
login/logout/pause/resume, não os estados derivados de chamada).
|
||||||
|
- TME/TMA/Service Level/Abandon Rate (secao 54): dependem de CDR
|
||||||
|
(fase futura, depois do Predictive Engine na ordem do agente.md secao
|
||||||
|
232) — `QUEUE_MEMBER_LEFT` já traz os dados brutos (`cause`,
|
||||||
|
`cancelReason`, timestamps) que vão alimentar esse cálculo quando CDR
|
||||||
|
existir.
|
||||||
|
- Dashboard Tenant/Platform (secao 162-163) — fase Frontend.
|
||||||
|
- Reconexão/reconciliação de estado ao reconectar o WebSocket (perder um
|
||||||
|
evento por queda de rede momentânea não é recuperável hoje — o client
|
||||||
|
precisa buscar o snapshot atual via REST depois de reconectar; não existe
|
||||||
|
endpoint de snapshot ainda).
|
||||||
@@ -53,6 +53,10 @@ export type NormalizedEventType =
|
|||||||
| "EXTENSION_REGISTERED"
|
| "EXTENSION_REGISTERED"
|
||||||
| "EXTENSION_UNREGISTERED"
|
| "EXTENSION_UNREGISTERED"
|
||||||
| "AGENT_STATUS_CHANGED"
|
| "AGENT_STATUS_CHANGED"
|
||||||
|
/** Mudança no nosso próprio enum AgentState (login/logout/pause/resume) —
|
||||||
|
* diferente de AGENT_STATUS_CHANGED, que é o `CC-Agent-State` bruto do
|
||||||
|
* mod_callcenter (Waiting/Receiving/...), vocabulário diferente. */
|
||||||
|
| "AGENT_STATE_CHANGED"
|
||||||
| "AGENT_OFFERED_CALL"
|
| "AGENT_OFFERED_CALL"
|
||||||
| "AGENT_BRIDGE_FAILED"
|
| "AGENT_BRIDGE_FAILED"
|
||||||
| "QUEUE_MEMBER_COUNT"
|
| "QUEUE_MEMBER_COUNT"
|
||||||
|
|||||||
233
pnpm-lock.yaml
generated
233
pnpm-lock.yaml
generated
@@ -40,10 +40,16 @@ importers:
|
|||||||
version: 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core':
|
'@nestjs/core':
|
||||||
specifier: ^12.0.1
|
specifier: ^12.0.1
|
||||||
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/platform-fastify':
|
'@nestjs/platform-fastify':
|
||||||
specifier: ^12.0.1
|
specifier: ^12.0.1
|
||||||
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)
|
||||||
|
'@nestjs/platform-socket.io':
|
||||||
|
specifier: 12.0.1
|
||||||
|
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(rxjs@7.8.2)
|
||||||
|
'@nestjs/websockets':
|
||||||
|
specifier: 12.0.1
|
||||||
|
version: 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-socket.io@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
class-transformer:
|
class-transformer:
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
version: 0.5.1
|
version: 0.5.1
|
||||||
@@ -65,6 +71,9 @@ importers:
|
|||||||
rxjs:
|
rxjs:
|
||||||
specifier: ^7.8.2
|
specifier: ^7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
|
socket.io:
|
||||||
|
specifier: 4.8.3
|
||||||
|
version: 4.8.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.0
|
specifier: ^22.0.0
|
||||||
@@ -485,6 +494,25 @@ packages:
|
|||||||
'@fastify/view':
|
'@fastify/view':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/platform-socket.io@12.0.1':
|
||||||
|
resolution: {integrity: sha512-1lNTp0tjlRp+5rMX5b3G0l9bt1CH4ejH4ZMn7Qi7dJc536UpweNVWmL5zm8EHFN71Q3CxP1PbqL6aw+QrtS+8w==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^12.0.0
|
||||||
|
'@nestjs/websockets': ^12.0.0
|
||||||
|
rxjs: ^7.1.0
|
||||||
|
|
||||||
|
'@nestjs/websockets@12.0.1':
|
||||||
|
resolution: {integrity: sha512-TxyfPg726saaaRihjBc5izQjQ7bRV1VJrZiEH9KO1LztJQyFbMDVztUI8ZXRgMYDuv8ZY0ENUaDb2JkpCkny0Q==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^12.0.0
|
||||||
|
'@nestjs/core': ^12.0.0
|
||||||
|
'@nestjs/platform-socket.io': ^12.0.0
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
rxjs: ^7.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@nestjs/platform-socket.io':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@nodable/entities@3.0.0':
|
'@nodable/entities@3.0.0':
|
||||||
resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==}
|
resolution: {integrity: sha512-8L9xFeTYKhm49xfIypoe2W5wV1m/3Z58kT+7kR9A8OyFxcPduI4VmxaUMQyKYrRjUoLLSXv6EKKID5Tvj9cUVw==}
|
||||||
|
|
||||||
@@ -714,6 +742,9 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2':
|
||||||
|
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0':
|
'@standard-schema/spec@1.1.0':
|
||||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||||
|
|
||||||
@@ -724,6 +755,9 @@ packages:
|
|||||||
'@tokenizer/token@0.3.0':
|
'@tokenizer/token@0.3.0':
|
||||||
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
|
||||||
|
|
||||||
'@types/d3-array@3.0.3':
|
'@types/d3-array@3.0.3':
|
||||||
resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==}
|
resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==}
|
||||||
|
|
||||||
@@ -775,6 +809,9 @@ packages:
|
|||||||
'@types/validator@13.15.10':
|
'@types/validator@13.15.10':
|
||||||
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
'@visx/curve@4.0.1-alpha.0':
|
'@visx/curve@4.0.1-alpha.0':
|
||||||
resolution: {integrity: sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==}
|
resolution: {integrity: sha512-jRu61Uz274pV1zyioXmboyrLutYbnKsgjj4njSGCnhdXj5GkZvZbg+ThDb6oOzoAnJOBRLz4rzPlWvNJOzuVMg==}
|
||||||
|
|
||||||
@@ -813,6 +850,10 @@ packages:
|
|||||||
abstract-logging@2.0.1:
|
abstract-logging@2.0.1:
|
||||||
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ajv-formats@3.0.1:
|
ajv-formats@3.0.1:
|
||||||
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -838,6 +879,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
|
resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
|
base64id@2.0.0:
|
||||||
|
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||||
|
engines: {node: ^4.5.0 || >= 5.9}
|
||||||
|
|
||||||
better-result@2.10.0:
|
better-result@2.10.0:
|
||||||
resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==}
|
resolution: {integrity: sha512-oQhh0y1qo2/ZKdAAEvHZAqKKiHOFU5k/bW96fE2ScgQOVkJRiHwB+nOS1SgFsYqRlxMDWvefXi9Q3px7QvgNDw==}
|
||||||
|
|
||||||
@@ -869,10 +914,18 @@ packages:
|
|||||||
confbox@0.2.4:
|
confbox@0.2.4:
|
||||||
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==}
|
||||||
|
|
||||||
|
cookie@0.7.2:
|
||||||
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
cookie@1.1.1:
|
cookie@1.1.1:
|
||||||
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
cors@2.8.6:
|
||||||
|
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -972,6 +1025,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3:
|
||||||
|
resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
engine.io@6.6.9:
|
||||||
|
resolution: {integrity: sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
|
||||||
env-paths@3.0.0:
|
env-paths@3.0.0:
|
||||||
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -1147,6 +1208,14 @@ packages:
|
|||||||
magicast@0.5.4:
|
magicast@0.5.4:
|
||||||
resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
|
resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1158,6 +1227,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
|
resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
negotiator@0.6.3:
|
||||||
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
object-assign@4.1.1:
|
||||||
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
object-hash@3.0.0:
|
||||||
|
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
ohash@2.0.12:
|
ohash@2.0.12:
|
||||||
resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
|
resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
|
||||||
|
|
||||||
@@ -1382,6 +1463,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
socket.io-adapter@2.5.8:
|
||||||
|
resolution: {integrity: sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==}
|
||||||
|
|
||||||
|
socket.io-parser@4.2.7:
|
||||||
|
resolution: {integrity: sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
|
socket.io@4.8.3:
|
||||||
|
resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==}
|
||||||
|
engines: {node: '>=10.2.0'}
|
||||||
|
|
||||||
sonic-boom@4.2.1:
|
sonic-boom@4.2.1:
|
||||||
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
|
||||||
|
|
||||||
@@ -1458,11 +1550,27 @@ packages:
|
|||||||
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
|
resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
vary@1.1.2:
|
||||||
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ws@8.21.3:
|
||||||
|
resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
xml-naming@0.3.0:
|
xml-naming@0.3.0:
|
||||||
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
|
resolution: {integrity: sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -1655,7 +1763,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
'@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
@@ -1665,13 +1773,15 @@ snapshots:
|
|||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@nestjs/websockets': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-socket.io@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
|
||||||
'@nestjs/platform-fastify@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2))':
|
'@nestjs/platform-fastify@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@fastify/cors': 11.3.0
|
'@fastify/cors': 11.3.0
|
||||||
'@fastify/formbody': 9.0.0
|
'@fastify/formbody': 9.0.0
|
||||||
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
fast-querystring: 1.1.2
|
fast-querystring: 1.1.2
|
||||||
fastify: 5.12.1
|
fastify: 5.12.1
|
||||||
fastify-plugin: 6.0.0
|
fastify-plugin: 6.0.0
|
||||||
@@ -1681,6 +1791,30 @@ snapshots:
|
|||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@nestjs/platform-socket.io@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/websockets': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-socket.io@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
rxjs: 7.8.2
|
||||||
|
socket.io: 4.8.3
|
||||||
|
tslib: 2.8.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
'@nestjs/websockets@12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@12.0.1)(@nestjs/platform-socket.io@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
iterare: 1.2.1
|
||||||
|
object-hash: 3.0.0
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
optionalDependencies:
|
||||||
|
'@nestjs/platform-socket.io': 12.0.1(@nestjs/common@12.0.1(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/websockets@12.0.1)(rxjs@7.8.2)
|
||||||
|
|
||||||
'@nodable/entities@3.0.0': {}
|
'@nodable/entities@3.0.0': {}
|
||||||
|
|
||||||
'@node-rs/argon2-android-arm-eabi@2.1.0':
|
'@node-rs/argon2-android-arm-eabi@2.1.0':
|
||||||
@@ -1900,6 +2034,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.2.18
|
'@types/react': 19.2.18
|
||||||
|
|
||||||
|
'@socket.io/component-emitter@3.1.2': {}
|
||||||
|
|
||||||
'@standard-schema/spec@1.1.0': {}
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
@@ -1911,6 +2047,10 @@ snapshots:
|
|||||||
|
|
||||||
'@tokenizer/token@0.3.0': {}
|
'@tokenizer/token@0.3.0': {}
|
||||||
|
|
||||||
|
'@types/cors@2.8.19':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.20.1
|
||||||
|
|
||||||
'@types/d3-array@3.0.3': {}
|
'@types/d3-array@3.0.3': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.0': {}
|
'@types/d3-color@3.1.0': {}
|
||||||
@@ -1961,6 +2101,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/validator@13.15.10': {}
|
'@types/validator@13.15.10': {}
|
||||||
|
|
||||||
|
'@types/ws@8.18.1':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.20.1
|
||||||
|
|
||||||
'@visx/curve@4.0.1-alpha.0':
|
'@visx/curve@4.0.1-alpha.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@visx/vendor': 4.0.0-alpha.0
|
'@visx/vendor': 4.0.0-alpha.0
|
||||||
@@ -2040,6 +2184,11 @@ snapshots:
|
|||||||
|
|
||||||
abstract-logging@2.0.1: {}
|
abstract-logging@2.0.1: {}
|
||||||
|
|
||||||
|
accepts@1.3.8:
|
||||||
|
dependencies:
|
||||||
|
mime-types: 2.1.35
|
||||||
|
negotiator: 0.6.3
|
||||||
|
|
||||||
ajv-formats@3.0.1(ajv@8.20.0):
|
ajv-formats@3.0.1(ajv@8.20.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
ajv: 8.20.0
|
ajv: 8.20.0
|
||||||
@@ -2062,6 +2211,8 @@ snapshots:
|
|||||||
|
|
||||||
aws-ssl-profiles@1.1.2: {}
|
aws-ssl-profiles@1.1.2: {}
|
||||||
|
|
||||||
|
base64id@2.0.0: {}
|
||||||
|
|
||||||
better-result@2.10.0: {}
|
better-result@2.10.0: {}
|
||||||
|
|
||||||
c12@3.3.4(magicast@0.5.4):
|
c12@3.3.4(magicast@0.5.4):
|
||||||
@@ -2099,8 +2250,15 @@ snapshots:
|
|||||||
|
|
||||||
confbox@0.2.4: {}
|
confbox@0.2.4: {}
|
||||||
|
|
||||||
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
cookie@1.1.1: {}
|
cookie@1.1.1: {}
|
||||||
|
|
||||||
|
cors@2.8.6:
|
||||||
|
dependencies:
|
||||||
|
object-assign: 4.1.1
|
||||||
|
vary: 1.1.2
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -2184,6 +2342,25 @@ snapshots:
|
|||||||
|
|
||||||
empathic@2.0.0: {}
|
empathic@2.0.0: {}
|
||||||
|
|
||||||
|
engine.io-parser@5.2.3: {}
|
||||||
|
|
||||||
|
engine.io@6.6.9:
|
||||||
|
dependencies:
|
||||||
|
'@types/cors': 2.8.19
|
||||||
|
'@types/node': 22.20.1
|
||||||
|
'@types/ws': 8.18.1
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cookie: 0.7.2
|
||||||
|
cors: 2.8.6
|
||||||
|
debug: 4.4.3
|
||||||
|
engine.io-parser: 5.2.3
|
||||||
|
ws: 8.21.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
env-paths@3.0.0: {}
|
env-paths@3.0.0: {}
|
||||||
|
|
||||||
esbuild@0.28.2:
|
esbuild@0.28.2:
|
||||||
@@ -2395,6 +2572,12 @@ snapshots:
|
|||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
mysql2@3.15.3:
|
mysql2@3.15.3:
|
||||||
@@ -2413,6 +2596,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
lru.min: 1.1.4
|
lru.min: 1.1.4
|
||||||
|
|
||||||
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
|
object-hash@3.0.0: {}
|
||||||
|
|
||||||
ohash@2.0.12: {}
|
ohash@2.0.12: {}
|
||||||
|
|
||||||
on-exit-leak-free@2.1.2: {}
|
on-exit-leak-free@2.1.2: {}
|
||||||
@@ -2601,6 +2790,36 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
socket.io-adapter@2.5.8:
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
ws: 8.21.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
|
socket.io-parser@4.2.7:
|
||||||
|
dependencies:
|
||||||
|
'@socket.io/component-emitter': 3.1.2
|
||||||
|
debug: 4.4.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
socket.io@4.8.3:
|
||||||
|
dependencies:
|
||||||
|
accepts: 1.3.8
|
||||||
|
base64id: 2.0.0
|
||||||
|
cors: 2.8.6
|
||||||
|
debug: 4.4.3
|
||||||
|
engine.io: 6.6.9
|
||||||
|
socket.io-adapter: 2.5.8
|
||||||
|
socket.io-parser: 4.2.7
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- bufferutil
|
||||||
|
- supports-color
|
||||||
|
- utf-8-validate
|
||||||
|
|
||||||
sonic-boom@4.2.1:
|
sonic-boom@4.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
atomic-sleep: 1.0.0
|
atomic-sleep: 1.0.0
|
||||||
@@ -2660,10 +2879,14 @@ snapshots:
|
|||||||
|
|
||||||
validator@13.15.35: {}
|
validator@13.15.35: {}
|
||||||
|
|
||||||
|
vary@1.1.2: {}
|
||||||
|
|
||||||
which@2.0.2:
|
which@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe: 2.0.0
|
isexe: 2.0.0
|
||||||
|
|
||||||
|
ws@8.21.3: {}
|
||||||
|
|
||||||
xml-naming@0.3.0: {}
|
xml-naming@0.3.0: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user