Skip to content

Repository files navigation

🚂 @vanaware/wsrouter

Deno TypeScript License: MIT

Router HTTP/WebSocket runtime-agnostic para Deno — Rápido, seguro e extensível.

O @vanaware/wsrouter é um router moderno com suporte completo a HTTP e WebSockets, projetado com arquitetura agnóstica que permite execução em múltiplos runtimes JavaScript. Atualmente oferece suporte oficial para Deno, com adaptadores para Node.js, Bun e Cloudflare Workers em desenvolvimento.

✨ Features Principais

🌐 HTTP Routing

  • ✅ Todos os métodos HTTP: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD
  • ✅ Parâmetros de rota nomeados (:id) e catch-all (*)
  • HEAD automático baseado em rotas GET (semântica HTTP correta)
  • 405 Method Not Allowed com header Allow quando método não é permitido
  • ✅ Base path configurável com normalização inteligente

🔌 WebSockets & Real-Time Suite

  • ✅ Upgrade automático com adaptadores por runtime
  • Grupos de WebSocket com broadcast inteligente
  • Dual Params PermissionFn: filtra por receiver, sender e conteúdo da mensagem
  • Last Broadcast automático: novos membros recebem a última mensagem ao conectar
  • 🎥 WebRTC Signaling Engine: streaming P2P de webcam, tela e áudio, ofertas SDP, respostas e candidatos ICE
  • 👥 Online Presence Tracking: contagem e roster de usuários online, detecção multi-aba/multi-dispositivo e status customizados
  • Reações Flutuantes: broadcast e overlay de emojis animados em tempo real
  • ✅ Handlers onclose/onerror não são sobrescritos pelo router
  • ✅ Graceful shutdown com closeAllWebSockets()

🛡️ Segurança

  • Force HTTPS com redirect automático (ignora localhost)
  • HSTS (HTTP Strict Transport Security) em respostas HTTPS
  • Trust Proxy explícito para headers X-Forwarded-*
  • Bloqueio de dotfiles (.env, .git, etc.) por padrão
  • Recusa de symlinks para evitar vazamento de arquivos
  • Path traversal protection com sanitização e containment real
  • ✅ Headers completos em arquivos estáticos (ETag, Last-Modified, Cache-Control)

🎯 Middlewares

  • ✅ Cadeia de middlewares com next()
  • Rewrite de rotas via next(newReq)
  • ✅ Execução mesmo em 404 (útil para logging e CORS)
  • ✅ Proteção contra múltiplas chamadas de next()

📂 Arquivos Estáticos

  • ✅ Servir arquivos de diretório local
  • ✅ Fallback automático para index.html e index.htm
  • Redirect 301 para diretórios sem barra final
  • ✅ MIME types modernos (.webp, .avif, .webmanifest, etc.)
  • ✅ Suporte a diretórios embutidos (embedded)

📦 Instalação

Deno

import { createDenoRouter } from "jsr:@vanaware/wsrouter@0.1.0/deno";

Ou via import map no deno.json:

{
  "imports": {
    "@vanaware/wsrouter": "jsr:@vanaware/wsrouter@0.1.0"
  }
}
import { createDenoRouter } from "@vanaware/wsrouter/deno";

🚀 Quick Start

Servidor HTTP Simples

import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({
  basePath: "/api",
  staticDir: "./public",
});

app.get("/hello", () => ({
  body: JSON.stringify({ message: "Hello, World!" }),
  init: { headers: { "Content-Type": "application/json" } },
}));

app.get("/users/:id", (_req, params) => ({
  body: JSON.stringify({ userId: params.id }),
}));

Deno.serve({ port: 3000 }, app.handleRequest.bind(app));
console.log("🚀 Servidor rodando em http://localhost:3000");

Chat WebSocket com Salas

import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({ basePath: "/api" });

app.ws("/chat/:room/:user", (ws, _req, params) => {
  const room = params.room as string;
  const user = params.user as string;
  const group = app.getWsGroupByPath("/chat/:room/:user");
  
  if (!group) {
    ws.close(1011, "Internal error");
    return;
  }
  
  console.log(`✅ ${user} entrou na sala ${room}`);
  
  ws.onmessage = (event) => {
    // Broadcast apenas para usuários na mesma sala
    group.broadcast(
      `[${user}]: ${event.data}`,
      (receiver, sender, _msg) => receiver.room === sender.room,
      params
    );
  };
  
  ws.onclose = () => {
    console.log(`❌ ${user} saiu da sala ${room}`);
  };
});

Deno.serve({ port: 3000 }, app.handleRequest.bind(app));

🎥 WebRTC Live Streaming & Sinalização P2P

import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({ basePath: "/api" });

// Rota de sinalização WebRTC (transmissão de webcam, ofertas SDP, respostas, ICE e reações)
app.ws("/webrtc/:room", (ws, _req, params) => {
  const group = app.getWsGroupByPath("/webrtc/:room");
  if (!group) return;

  ws.onmessage = (event) => {
    // Encaminha ofertas SDP, ICE candidates e eventos de stream automaticamente
    group.handleSignaling(ws, event.data, params);
  };
});

Deno.serve({ port: 3000 }, app.handleRequest.bind(app));

👥 Rastreamento de Presença Online em Tempo Real

import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({ basePath: "/api" });

app.ws("/presence-chat/:room", (ws, req, params) => {
  const group = app.getWsGroupByPath("/presence-chat/:room");
  if (!group) return;

  const url = new URL(req.url);
  const userId = url.searchParams.get("userId") || "anon";
  const name = url.searchParams.get("name") || "Guest";

  // Registra presença do usuário (emite snapshot e broadcasts de join/leave sem duplicar abas)
  group.track(ws, {
    userId,
    name,
    status: "online",
    statusMessage: "Programando com WsRouter",
  });

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "update_status") {
      group.updatePresence(ws, { status: data.status, statusMessage: data.statusMessage });
    }
  };
});

Deno.serve({ port: 3000 }, app.handleRequest.bind(app));

🌐 Roteamento HTTP

Métodos Suportados

app.get("/path", handler);
app.post("/path", handler);
app.put("/path", handler);
app.delete("/path", handler);
app.patch("/path", handler);
app.options("/path", handler);
app.head("/path", handler);

Formato do Handler

Os handlers podem retornar um objeto com body (e opcionalmente init) ou diretamente uma instância padrão de Response:

type HttpHandler = (
  req: Request,
  params: RouteParams,
  ctx?: RequestContext,
) => 
  | { body: BodyInit; init?: ResponseInit }
  | Response
  | Promise<{ body: BodyInit; init?: ResponseInit } | Response>;

Exemplos

Resposta Direta com Response:

app.get("/health", () => {
  return new Response("OK", { status: 200 });
});

Resposta JSON:

app.get("/api/user/:id", async (_req, params) => {
  const user = await db.getUser(params.id);
  return {
    body: JSON.stringify(user),
    init: {
      status: 200,
      headers: { "Content-Type": "application/json" },
    },
  };
});

Resposta de Texto:

app.get("/hello", () => ({
  body: "Hello, World!",
}));

Status Customizado:

app.post("/users", async (req) => {
  const data = await req.json();
  const user = await db.createUser(data);
  return {
    body: JSON.stringify(user),
    init: {
      status: 201,
      headers: { "Content-Type": "application/json" },
    },
  };
});

No Content (204):

app.delete("/users/:id", async (_req, params) => {
  await db.deleteUser(params.id);
  return {
    body: "",
    init: { status: 204 },
  };
});

Redirect:

app.get("/old-page", () => ({
  body: "",
  init: {
    status: 301,
    headers: { "Location": "/new-page" },
  },
}));

Parâmetros de Rota

// Parâmetros nomeados
app.get("/users/:id/posts/:postId", (_req, params) => {
  console.log(params.id);      // "123"
  console.log(params.postId);  // "456"
  return { body: "OK" };
});

// Catch-all com *
app.get("/files/*", (_req, params) => {
  console.log(params.catch);  // ["path", "to", "file.txt"]
  return { body: JSON.stringify(params.catch) };
});

// Combinação
app.get("/api/:version/*", (_req, params) => {
  console.log(params.version);  // "v1"
  console.log(params.catch);    // ["users", "123"]
  return { body: "OK" };
});

HEAD Automático

O router automaticamente suporta HEAD para rotas GET registradas:

app.get("/resource", () => ({
  body: "data",
  init: { headers: { "X-Custom": "value" } },
}));

// GET /resource → 200 com body "data"
// HEAD /resource → 200 com headers mas body vazio

405 Method Not Allowed

Quando um path existe mas o método não é permitido:

app.get("/resource", () => ({ body: "data" }));
app.post("/resource", () => ({ body: "created", init: { status: 201 } }));

// PUT /resource → 405 Method Not Allowed
// Headers: Allow: GET, POST

🎯 Middlewares

Middlewares executam antes do handler final e podem modificar a requisição, abortar o fluxo ou passar controle adiante.

Básico

app.use(async (req, params, next) => {
  console.log(`📝 ${req.method} ${req.url}`);
  return await next();
});

Escopo por Caminho (Path-scoped) & Estado Compartilhado (State/Meta)

Middlewares podem ser vinculados a caminhos específicos e compartilhar dados entre si ou com os handlers:

app.use("/admin/*", async (req, params, next, ctx) => {
  const token = req.headers.get("Authorization");
  if (!token) {
    return new Response("Unauthorized", { status: 401 });
  }
  if (ctx) {
    ctx.state.authorizedUser = { id: "user-42", role: "admin" };
  }
  return await next();
});

Abortar Fluxo

app.use(async (req, _params, next) => {
  const auth = req.headers.get("authorization");
  if (!auth) {
    return new Response("Unauthorized", { status: 401 });
  }
  return await next();
});

Modificar Resposta

app.use(async (_req, _params, next) => {
  const res = await next();
  res.headers.set("X-Custom-Header", "value");
  return res;
});

Rewrite de Rota

Você pode passar uma nova Request para next() para reescrever a rota:

app.use(async (req, _params, next) => {
  // Reescreve /old-api/* para /api/*
  if (req.url.includes("/old-api/")) {
    const newUrl = req.url.replace("/old-api/", "/api/");
    const newReq = new Request(newUrl, req);
    return next(newReq);
  }
  return next();
});

Logging com Tempo

app.use(async (req, _params, next) => {
  const start = Date.now();
  const res = await next();
  const ms = Date.now() - start;
  console.log(`${req.method} ${req.url}${res.status} (${ms}ms)`);
  return res;
});

CORS

app.use(async (req, _params, next) => {
  if (req.method === "OPTIONS") {
    return new Response(null, {
      status: 204,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
        "Access-Control-Max-Age": "86400",
      },
    });
  }
  const res = await next();
  res.headers.set("Access-Control-Allow-Origin", "*");
  return res;
});

📂 Arquivos Estáticos

Configuração Básica

const app = createDenoRouter({
  basePath: "/api",
  staticDir: "./public",  // Diretório de arquivos estáticos
});

Com Diretório Embutido

const app = createDenoRouter({
  basePath: "/api",
  staticDir: "./public",
  embeddedDir: "./dist",  // Tenta embedded primeiro, depois static
});

Comportamento

  • Serve arquivos de staticDir quando rota HTTP não é encontrada
  • Fallback automático: /docs/docs.html/docs/index.html
  • Redirect 301 para diretórios sem barra final: /docs/docs/
  • Apenas métodos GET e HEAD são servidos
  • Headers completos: Content-Type, Content-Length, Last-Modified, ETag, Cache-Control

MIME Types Modernos

Suporte nativo para:

  • Imagens: .webp, .avif, .png, .jpg, .gif, .svg
  • Web: .html, .css, .js, .mjs, .json, .webmanifest
  • Fontes: .woff, .woff2, .ttf, .otf
  • Mídia: .mp3, .mp4, .webm
  • Outros: .pdf, .xml, .wasm, .ts, .tsx, .jsx

🔌 WebSockets

Upgrade Automático

app.ws("/chat/:room", (ws, req, params) => {
  console.log(`Cliente conectou na sala ${params.room}`);
  
  ws.onmessage = (event) => {
    ws.send(`Echo: ${event.data}`);
  };
  
  ws.onclose = () => {
    console.log("Cliente desconectou");
  };
});

Grupos de WebSocket

Cada rota WS tem seu próprio grupo automaticamente:

app.ws("/chat/:room/:user", (ws, _req, params) => {
  const group = app.getWsGroupByPath("/chat/:room/:user");
  
  ws.onmessage = (event) => {
    // Broadcast para todos na mesma rota
    group.broadcast(event.data);
  };
});

Dual Params PermissionFn

Filtre broadcasts com base em receiver, sender e mensagem:

app.ws("/chat/:room/:user", (ws, _req, params) => {
  const group = app.getWsGroupByPath("/chat/:room/:user");
  
  ws.onmessage = (event) => {
    group.broadcast(
      `[${params.user}]: ${event.data}`,
      (receiver, sender, message) => {
        // Regra 1: Mesma sala
        if (receiver.room !== sender.room) return false;
        
        // Regra 2: Não enviar para o próprio sender
        if (receiver.user === sender.user) return false;
        
        // Regra 3: Bloquear spam
        if (message.toLowerCase().includes("spam")) return false;
        
        return true;
      },
      params  // senderParams
    );
  };
});

Last Broadcast Automático

Novos membros recebem a última mensagem ao conectar:

// 10:00:00 → User A envia: "Olá a todos!"
// Router salva: { message: "Olá...", permissionFn: ..., senderParams: { room: "lobby" } }

// 10:00:05 → User B conecta em /chat/lobby/userB
// Router reavalia: permissionFn({ room: "lobby" }, { room: "lobby" }, "Olá...") → TRUE
// User B recebe "Olá a todos!" automaticamente!

// 10:00:10 → User C conecta em /chat/vip/userC
// Router reavalia: permissionFn({ room: "vip" }, { room: "lobby" }, "Olá...") → FALSE
// User C NÃO recebe (segurança garantida!)

Fechar Grupos

// Fechar grupo específico
app.closeGroupByPath("/chat/:room/:user");

// Fechar todos os WebSockets (graceful shutdown)
app.closeAllWebSockets();

🎥 WebRTC Live Webcam Streaming & Sinalização

O @vanaware/wsrouter inclui um hub de sinalização WebRTC de alta performance integrado ao WebSocketGroup. Permite streaming peer-to-peer de vídeo/áudio, anúncios de transmissão, encaminhamento de ofertas SDP, respostas, candidatos ICE e reações flutuantes com zero dependências externas.

Fluxo de Sinalização Simplificado

  1. Broadcaster entra e publica stream: envia { type: "broadcaster_started", broadcasterId, broadcasterName, streamTitle }.
  2. Espectadores requisitam transmissão: enviam { type: "request_stream", viewerId, viewerName }.
  3. Oferta e Resposta SDP: O Broadcaster cria um RTCPeerConnection para cada espectador e envia webrtc_offer; o espectador responde com webrtc_answer.
  4. ICE Candidates: Ambas as pontas trocam candidatos via { type: "webrtc_candidate", to: targetPeerId, candidate }.
  5. Mídia Direta P2P: O fluxo de áudio e vídeo trafega diretamente entre os navegadores sem onerar a CPU do servidor.
import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({ basePath: "/api" });

app.ws("/webrtc/:room", (ws, _req, params) => {
  const group = app.getWsGroupByPath("/webrtc/:room");
  if (!group) return;

  // Encaminha ofertas/respostas SDP e candidatos ICE diretamente ao destinatário
  ws.onmessage = (event) => {
    group.handleSignaling(ws, event.data, params);
  };
});

Consultas de Streams e Peers via Router e Grupo

// Saber se há live ativa em uma sala
const isLive = app.isBroadcasting("/webrtc/:room", "gaming");

// Obter dados do transmissor atual
const broadcaster = app.getActiveStream("/webrtc/:room", "gaming");

// Listar todas as transmissões ativas
const allLive = app.getAllActiveStreams("/webrtc/:room");

// Enviar reação flutuante programaticamente
group.sendReaction("gaming", {
  from: "user_123",
  fromName: "Dan",
  emoji: "🔥",
});

Veja o guia completo em docs/webrtc.md.


👥 Online Presence Tracking Suite

O motor nativo PresenceTracker gerencia o ciclo de vida de usuários online, snapshots de estado e atualizações de status.

Deduplicação Automática de Múltiplas Abas

Quando um mesmo userId abre múltiplas abas ou conexões simultâneas, o WsRouter incrementa o contador connections sem poluir a sala com múltiplos eventos presence_join. O evento presence_leave só é emitido aos pares quando a última conexão daquele usuário é encerrada.

app.ws("/presence/:room", (ws, req, params) => {
  const group = app.getWsGroupByPath("/presence/:room");
  if (!group) return;

  // Registra presença com metadados customizados
  group.track(ws, {
    userId: "user_42",
    name: "Alice",
    status: "online", // "online" | "away" | "busy" | "meeting"
    statusMessage: "Em reunião de design",
  });

  ws.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === "set_status") {
      // Atualiza status e emite broadcast `presence_update`
      group.updatePresence(ws, {
        status: data.status,
        statusMessage: data.statusMessage,
      });
    }
  };
});

// API REST: inspecionar usuários online em qualquer rota
app.get("/api/online-users", () => {
  const users = app.getPresence("/presence/:room");
  return {
    body: JSON.stringify({ count: users.length, users }),
    init: { headers: { "Content-Type": "application/json" } },
  };
});

Veja o guia completo em docs/presence.md.


🛡️ Segurança

Force HTTPS

Redireciona HTTP → HTTPS automaticamente em produção:

const app = createDenoRouter({
  forceHttps: true,
});

Comportamento:

  • Redireciona com status 301 Moved Permanently
  • Ignora automaticamente localhost, 127.0.0.1 e [::1] (IPv6)
  • Adiciona header Strict-Transport-Security (HSTS)

Trust Proxy

Quando atrás de proxy reverso (nginx, Cloudflare, etc.):

const app = createDenoRouter({
  forceHttps: true,
  trustProxy: true,  // ⚠️ Apenas se estiver atrás de proxy confiável
});

Com trustProxy: true, o router respeita o header X-Forwarded-Proto.

⚠️ AVISO: Nunca ative trustProxy se o servidor estiver exposto diretamente à internet.

Bloqueio de Dotfiles

Por padrão, arquivos que começam com . são bloqueados:

const app = createDenoRouter({
  staticDir: "./public",
  allowDotfiles: false,  // Default: false
});

Bloqueados: .env, .git/config, .DS_Store, .htaccess, etc.

Se precisar servir dotfiles (não recomendado):

const app = createDenoRouter({
  staticDir: "./public",
  allowDotfiles: true,  // ⚠️ Risco de segurança
});

Proteção contra Path Traversal

O router sanitiza caminhos e verifica containment real:

// Requisição maliciosa
GET /../../etc/passwd
GET /..%2F..%2Fetc%2Fpasswd

// Resultado: 404 (caminho sanitizado e verificado)

Recusa de Symlinks

O adaptador Deno recusa symlinks por padrão para evitar vazamento:

# Se existir: public/secret -> /etc/passwd
# Requisição: GET /secret
# Resultado: 404 (symlink recusado)

Headers de Segurança

Adicione via middleware:

app.use(async (_req, _params, next) => {
  const res = await next();
  res.headers.set("X-Frame-Options", "DENY");
  res.headers.set("X-Content-Type-Options", "nosniff");
  res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
  res.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  );
  return res;
});

🔌 Adaptadores

Deno (Oficial)

Suporte completo e testado:

import { createDenoRouter } from "@vanaware/wsrouter/deno";

const app = createDenoRouter({
  basePath: "/api",
  staticDir: "./public",
  forceHttps: true,
  trustProxy: true,
  allowDotfiles: false,
});

Features:

  • ✅ HTTP Routing
  • ✅ WebSockets (via Deno.upgradeWebSocket)
  • ✅ Static Files (via Deno.open + Deno.stat)
  • ✅ Containment real e recusa de symlinks
  • ✅ Headers completos (ETag, Last-Modified, etc.)

Outros Runtimes (Roadmap)

O core é runtime-agnostic. Adaptadores para outros runtimes estão em desenvolvimento:

Node.js (Planejado)

// Futuro
import { createNodeRouter } from "@vanaware/wsrouter/node";

Desafios:

  • URLPattern disponível apenas em Node 18.17+
  • WebSocket requer biblioteca externa (ws, uWebSockets.js)
  • Static files via módulo fs

Bun (Planejado)

// Futuro
import { createBunRouter } from "@vanaware/wsrouter/bun";

Vantagens:

  • Compatível com APIs Node.js
  • Suporte nativo a WebSocket via Bun.serve
  • Performance excelente

Cloudflare Workers (Removido do Core)

Os adaptadores Cloudflare foram removidos do core na versão 1.0 devido a limitações de estado compartilhado. Para WebSockets em Cloudflare, use Durable Objects.

Para static files, use a nova feature Static Assets do Cloudflare Workers.

Veja docs/roadmap-adapters.md para detalhes.

Criando seu Próprio Adaptador

Implemente as interfaces:

interface WebSocketUpgrader {
  upgrade(req: Request): { socket: WebSocket; response: Response };
}

interface StaticFileHandler {
  handle(path: string): Promise<Response | null>;
}

Use o core diretamente:

import { Router } from "@vanaware/wsrouter";

const app = new Router({
  basePath: "/api",
  webSocketUpgrader: myCustomUpgrader,
  staticFileHandler: myCustomStaticHandler,
});

📚 API Reference

createDenoRouter(options)

interface DenoRouterOptions {
  basePath?: string;           // Prefixo para todas as rotas
  staticDir?: string | null;   // Diretório de arquivos estáticos (default: null)
  embeddedDir?: string | null; // Diretório embutido (opcional)
  forceHttps?: boolean;        // Redirecionar HTTP → HTTPS
  trustProxy?: boolean;        // Confiar em X-Forwarded-*
  allowDotfiles?: boolean;     // Permitir arquivos .env, .git, etc.
  lastBroadcastDelay?: number; // Delay antes de enviar last broadcast (default: 0ms)
}

Router Methods

// Registro de rotas
app.get(path, handler)
app.post(path, handler)
app.put(path, handler)
app.delete(path, handler)
app.patch(path, handler)
app.options(path, handler)
app.head(path, handler)
app.ws(path, handler)
app.worker(handlerOrRoute, name?)

// Middlewares
app.use(middleware)
app.use(pathPattern, middleware)

// Sub-routers
app.mount(prefix, subRouter)

// WebSockets
app.broadcast(pathOrPattern, message, permissionFn?, senderParams?): boolean
app.getWsGroupByPath(pattern): WebSocketGroup | undefined
app.closeGroupByPath(pattern): boolean
app.closeAllWebSockets(): void

// WebRTC Signaling & Streaming
app.getSignaling(pathOrPattern): WebRTCSignalingHub | undefined
app.getActiveStream(pathOrPattern, room): ActiveStreamInfo | undefined
app.getAllActiveStreams(pathOrPattern): ActiveStreamInfo[]
app.isBroadcasting(pathOrPattern, room): boolean
app.startBroadcasting(pathOrPattern, broadcasterId, name, room, title?, params?): ActiveStreamInfo | undefined
app.stopBroadcasting(pathOrPattern, broadcasterId, room, params?): boolean
app.sendReaction(pathOrPattern, room, reaction, params?): boolean
app.getPeerCount(pathOrPattern): number
app.getPeers(pathOrPattern): string[]
app.sendToPeer(pathOrPattern, peerId, message): boolean

// Presence Tracking
app.getPresence(pathOrPattern): PresenceUser[]
app.getPresenceUser(pathOrPattern, userId): PresenceUser | undefined
app.updatePresence(pathOrPattern, wsOrUserId, partialData, params?): PresenceUser | undefined

// Inspeção & Rotas Modulares
app.getHttpRoutes(): readonly HttpRoute[]
app.getWsRoutes(): readonly WsRoute[]
app.getMiddlewares(): readonly MiddlewareRoute[]
app.getMiddlewareChain(): MiddlewareChain
app.getWorkers(): readonly WorkerRoute[]
app.getHttpRouteByPath(method, path): HttpRoute | undefined
app.getWsRouteByPath(path): WsRoute | undefined

// Handler principal
app.handleRequest(req: Request): Promise<Response>

WebSocketGroup Methods, Presence & WebRTC

group.addSocket(ws, params)
group.removeSocket(ws)
group.size: number

group.broadcast(message, permissionFn?, senderParams?)
group.sendLastBroadcastTo(ws, receiverParams)
group.closeGroup()

// Online Presence Tracking
group.presence: PresenceTracker
group.track(ws, user)
group.untrack(ws)
group.updatePresence(wsOrUserId, partialData)
group.getPresenceList(): PresenceUser[]
group.getPresenceUser(userId): PresenceUser | undefined
group.presenceSize: number

// WebRTC Live Signaling
group.signaling: WebRTCSignalingHub
group.handleSignaling(ws, rawData, params?)
group.registerPeer(ws, peerId)
group.unregisterPeer(ws)
group.startBroadcasting(broadcasterId, name, room, title?, params?)
group.stopBroadcasting(broadcasterId, room, params?)
group.getActiveStream(room)
group.getAllActiveStreams()
group.isBroadcasting(room)
group.sendReaction(room, reaction, params?)
group.sendToPeer(peerId, message)
group.peerCount: number
group.getPeers(): string[]

// Event Hooks & Lifecycle
group.onConnect((ws, params) => void)
group.onDisconnect((ws, params) => void)
group.on(event, listener)
group.off(event, listener)
group.emit(event, ...args)

Modular Routing Classes

  • HttpRoute: Encapsula métodos HTTP (GET, POST, etc.), compilação de URLPattern, correspondência e execução de handlers.
  • WsRoute: Encapsula endpoints WebSocket, seu respectivo WebSocketGroup, URLPattern e ciclo de conexões.
  • MiddlewareRoute: Encapsula funções de middleware com suporte a escopo de caminho opcional.
  • MiddlewareChain: Executa a pilha em modelo cebola (onion), provendo RequestContext e state.
  • WorkerRoute: Encapsula handlers de fallback e workers executados antes de arquivos estáticos.

### `PermissionFn`

```typescript
type PermissionFn = (
  receiverParams: RouteParams,
  senderParams: RouteParams,
  message: string,
) => boolean;

📖 Exemplos Completos

Autenticação JWT

import { createDenoRouter } from "@vanaware/wsrouter/deno";
import { SignJWT, jwtVerify } from "jose";

const JWT_SECRET = new TextEncoder().encode(Deno.env.get("JWT_SECRET"));

const app = createDenoRouter({
  basePath: "/api",
  staticDir: "./public",
});

// Rota de login
app.post("/login", async (req) => {
  const { username, password } = await req.json();
  
  if (username === "admin" && password === "secret") {
    const token = await new SignJWT({ userId: "1", username })
      .setProtectedHeader({ alg: "HS256" })
      .setExpirationTime("1h")
      .sign(JWT_SECRET);
    
    return {
      body: JSON.stringify({ token }),
      init: { headers: { "Content-Type": "application/json" } },
    };
  }
  
  return {
    body: JSON.stringify({ error: "Invalid credentials" }),
    init: { status: 401 },
  };
});

// Middleware de autenticação WebSocket
app.use(async (req, _params, next) => {
  if (req.headers.get("upgrade")?.toLowerCase() !== "websocket") {
    return await next();
  }
  
  const protocol = req.headers.get("sec-websocket-protocol") ?? "";
  const protocols = protocol.split(",").map(p => p.trim());
  const bearerIndex = protocols.findIndex(p => p === "Bearer");
  const token = bearerIndex !== -1 ? protocols[bearerIndex + 1] : null;
  
  if (!token) {
    return new Response("Token required", { status: 401 });
  }
  
  try {
    await jwtVerify(token, JWT_SECRET, { algorithms: ["HS256"] });
    return await next();
  } catch {
    return new Response("Invalid token", { status: 403 });
  }
});

// Rota WebSocket protegida
app.ws("/chat/:room", (ws, _req, params) => {
  const group = app.getWsGroupByPath("/chat/:room");
  
  ws.onmessage = (event) => {
    group.broadcast(
      `[${params.room}]: ${event.data}`,
      (receiver, sender, _msg) => receiver.room === sender.room,
      params
    );
  };
});

Rate Limiting

const requestCounts = new Map<string, { count: number; resetTime: number }>();

app.use(async (req, _params, next) => {
  const ip = req.headers.get("x-forwarded-for")?.split(",")[0] ?? "unknown";
  const now = Date.now();
  const windowMs = 60000; // 1 minuto
  const maxRequests = 100;
  
  const record = requestCounts.get(ip) ?? { count: 0, resetTime: now + windowMs };
  
  if (now > record.resetTime) {
    record.count = 0;
    record.resetTime = now + windowMs;
  }
  
  record.count++;
  requestCounts.set(ip, record);
  
  if (record.count > maxRequests) {
    return new Response("Too Many Requests", {
      status: 429,
      headers: {
        "Retry-After": Math.ceil((record.resetTime - now) / 1000).toString(),
        "X-RateLimit-Limit": maxRequests.toString(),
        "X-RateLimit-Remaining": "0",
      },
    });
  }
  
  const res = await next();
  res.headers.set("X-RateLimit-Limit", maxRequests.toString());
  res.headers.set("X-RateLimit-Remaining", (maxRequests - record.count).toString());
  return res;
});

🧪 Testes

Execute a suíte completa de testes:

deno task tests

Ou separadamente:

# Type checking
deno task check

# Testes unitários
deno task test

# Formatação
deno task fmt

# Linting
deno task lint

📚 Documentação


🗺️ Roadmap

Versão 1.0 (Atual)

  • ✅ Core agnóstico estável
  • ✅ Adaptador Deno completo
  • ✅ Sistema de permissões Dual Params
  • ✅ Segurança reforçada (HSTS, trustProxy, dotfiles, symlinks)

Versão 1.1 (Planejado)

  • ⏳ Adaptador Node.js oficial
  • ⏳ Adaptador Bun oficial
  • ⏳ Suporte a Range Requests para arquivos grandes
  • ⏳ Validação de Origin em WebSockets

Versão 2.0 (Futuro)

  • ⏳ Rate limiting nativo no core
  • ⏳ Integração com OpenTelemetry
  • ⏳ Suporte a HTTP/2 Server Push
  • ⏳ Compressão automática (gzip, brotli)

🤝 Contribuindo

Contribuições são bem-vindas! Por favor:

  1. Faça fork do repositório
  2. Crie uma branch para sua feature (git checkout -b feature/AmazingFeature)
  3. Commit suas mudanças (git commit -m 'Add some AmazingFeature')
  4. Push para a branch (git push origin feature/AmazingFeature)
  5. Abra um Pull Request

Desenvolvimento Local / AI Studio

Devido às restrições do ambiente AI Studio e para compatibilidade com a plataforma, o projeto utiliza a porta 3000 para o servidor de desenvolvimento. A infraestrutura inclui um arquivo package.json ponte que executa o install-script.sh antes de invocar o deno task. Isso garante que o Deno seja baixado e configurado se não estiver no ambiente.

# Iniciar o servidor de desenvolvimento
npm run dev

# Rodar a checagem completa de testes, tipos e linting
npm run test

Caso esteja rodando localmente (fora do AI Studio) com o Deno já instalado:

# Clone o repositório
git clone https://github.com/vanaware/wsrouter.git
cd wsrouter

# Execute testes
deno task check-all

# Execute exemplo principal
deno task dev

📄 Licença

Este projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.

About

Simple websocket router

Resources

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages