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.
- ✅ 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
Allowquando método não é permitido - ✅ Base path configurável com normalização inteligente
- ✅ 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/onerrornão são sobrescritos pelo router - ✅ Graceful shutdown com
closeAllWebSockets()
- ✅ 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)
- ✅ 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()
- ✅ Servir arquivos de diretório local
- ✅ Fallback automático para
index.htmleindex.htm - ✅ Redirect 301 para diretórios sem barra final
- ✅ MIME types modernos (
.webp,.avif,.webmanifest, etc.) - ✅ Suporte a diretórios embutidos (embedded)
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";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");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));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));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));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);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>;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 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" };
});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 vazioQuando 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, POSTMiddlewares executam antes do handler final e podem modificar a requisição, abortar o fluxo ou passar controle adiante.
app.use(async (req, params, next) => {
console.log(`📝 ${req.method} ${req.url}`);
return await next();
});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();
});app.use(async (req, _params, next) => {
const auth = req.headers.get("authorization");
if (!auth) {
return new Response("Unauthorized", { status: 401 });
}
return await next();
});app.use(async (_req, _params, next) => {
const res = await next();
res.headers.set("X-Custom-Header", "value");
return res;
});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();
});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;
});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;
});const app = createDenoRouter({
basePath: "/api",
staticDir: "./public", // Diretório de arquivos estáticos
});const app = createDenoRouter({
basePath: "/api",
staticDir: "./public",
embeddedDir: "./dist", // Tenta embedded primeiro, depois static
});- Serve arquivos de
staticDirquando 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
GETeHEADsão servidos - Headers completos:
Content-Type,Content-Length,Last-Modified,ETag,Cache-Control
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
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");
};
});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);
};
});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
);
};
});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 grupo específico
app.closeGroupByPath("/chat/:room/:user");
// Fechar todos os WebSockets (graceful shutdown)
app.closeAllWebSockets();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.
- Broadcaster entra e publica stream: envia
{ type: "broadcaster_started", broadcasterId, broadcasterName, streamTitle }. - Espectadores requisitam transmissão: enviam
{ type: "request_stream", viewerId, viewerName }. - Oferta e Resposta SDP: O Broadcaster cria um
RTCPeerConnectionpara cada espectador e enviawebrtc_offer; o espectador responde comwebrtc_answer. - ICE Candidates: Ambas as pontas trocam candidatos via
{ type: "webrtc_candidate", to: targetPeerId, candidate }. - 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);
};
});// 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.
O motor nativo PresenceTracker gerencia o ciclo de vida de usuários online, snapshots de estado e atualizações de status.
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.
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.1e[::1](IPv6) - Adiciona header
Strict-Transport-Security(HSTS)
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.
trustProxy se o servidor estiver exposto diretamente à internet.
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
});O router sanitiza caminhos e verifica containment real:
// Requisição maliciosa
GET /../../etc/passwd
GET /..%2F..%2Fetc%2Fpasswd
// Resultado: 404 (caminho sanitizado e verificado)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)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;
});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.)
O core é runtime-agnostic. Adaptadores para outros runtimes estão em desenvolvimento:
// 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
// Futuro
import { createBunRouter } from "@vanaware/wsrouter/bun";Vantagens:
- Compatível com APIs Node.js
- Suporte nativo a WebSocket via
Bun.serve - Performance excelente
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.
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,
});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)
}// 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>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)HttpRoute: Encapsula métodos HTTP (GET,POST, etc.), compilação deURLPattern, correspondência e execução de handlers.WsRoute: Encapsula endpoints WebSocket, seu respectivoWebSocketGroup,URLPatterne 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), provendoRequestContextestate.WorkerRoute: Encapsula handlers de fallback e workers executados antes de arquivos estáticos.
### `PermissionFn`
```typescript
type PermissionFn = (
receiverParams: RouteParams,
senderParams: RouteParams,
message: string,
) => boolean;
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
);
};
});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;
});Execute a suíte completa de testes:
deno task testsOu separadamente:
# Type checking
deno task check
# Testes unitários
deno task test
# Formatação
deno task fmt
# Linting
deno task lint- Guia WebRTC Live Streaming
- Rastreamento de Presença Online
- Guia de Segurança
- Roadmap de Adaptadores
- Roadmap de Rate Limiting
- Arquitetura Runtime-Agnostic
- Middlewares
- Permissões WebSocket
- Retorno de Handlers
- ✅ Core agnóstico estável
- ✅ Adaptador Deno completo
- ✅ Sistema de permissões Dual Params
- ✅ Segurança reforçada (HSTS, trustProxy, dotfiles, symlinks)
- ⏳ Adaptador Node.js oficial
- ⏳ Adaptador Bun oficial
- ⏳ Suporte a Range Requests para arquivos grandes
- ⏳ Validação de Origin em WebSockets
- ⏳ Rate limiting nativo no core
- ⏳ Integração com OpenTelemetry
- ⏳ Suporte a HTTP/2 Server Push
- ⏳ Compressão automática (gzip, brotli)
Contribuições são bem-vindas! Por favor:
- Faça fork do repositório
- Crie uma branch para sua feature (
git checkout -b feature/AmazingFeature) - Commit suas mudanças (
git commit -m 'Add some AmazingFeature') - Push para a branch (
git push origin feature/AmazingFeature) - Abra um Pull Request
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 testCaso 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 devEste projeto está licenciado sob a Licença MIT - veja o arquivo LICENSE para detalhes.