npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bonktools

v0.2.7

Published

Headless TypeScript client for bonk.io — connects directly via Socket.IO, no browser required. Create/join rooms, manage rosters, and run 24/7 bots with automatic reconnection.

Readme

bonktools

npm version CI license: MIT node

Cliente TypeScript headless para bonk.io — conecta direto via Socket.IO, sem browser. Crie e gerencie salas, entre em salas existentes, escute o roster e os eventos de partida em tempo real, e rode bots 24h com reconexão automática.

Por que headless? Automatizar o bonk.io com Puppeteer/Playwright exige um browser completo (~300 MB de RAM, headless instável, tela virtual no Linux). bonktools fala o protocolo Socket.IO do jogo diretamente — menos de 15 MB de RAM por sala, sem Chromium, sem Xvfb, sem depender de seletores de UI que quebram a cada atualização do client.


Índice


Arquitetura em camadas

BonkSession
  └── BonkRoom  (1 por sala)
        └── BonkTransport  (socket.io-client v2)
              └── bonk.io WS

| Camada | Responsabilidade | |---|---| | BonkTransport | Conexão Socket.IO v2 (EIO=3), TLS Sectigo, timesync | | BonkRoom | Ciclo de vida da sala, roster, eventos tipados, reconexão | | BonkSession | Pool de salas, auth compartilhado, throttle, reconcile 60s |


Instalação

npm install bonktools
# ou
pnpm add bonktools
# ou
yarn add bonktools

Requisitos: Node.js >= 20.18.1. O pacote usa ESM ("type": "module"), com build CJS disponível para require().


Autenticação

A lib suporta dois modos:

import type { AuthOptions } from 'bonktools';

// Conta registrada (recomendado para bots de longa duração)
const auth: AuthOptions = {
  type: 'registered',
  username: 'meu_usuario',
  password: 'minha_senha',
};

// Convidado (sem HTTP de auth)
const auth: AuthOptions = {
  type: 'guest',
  guestName: 'BonkBot',
};

Com type: 'registered', a lib faz uma chamada HTTP para o endpoint de login do bonk.io e obtém um token de sessão. O token é reusado em todas as salas da mesma BonkSession.


Criando uma sala — createRoom()

A função mais direta. Retorna um BonkRoom já conectado e ativo (aguarda o packet SHARE_LINK do servidor antes de resolver).

import { createRoom } from 'bonktools';

const room = await createRoom({
  auth: { type: 'registered', username: '...', password: '...' },
  desiredState: {
    roomName: 'Minha Sala',
    password: '',         // string vazia = sem senha
    maxPlayers: 6,
    mode: 'b',            // 'b'=classic, 'ar'=arrows, 'ard'=arrows death, 'sp'=grapple, 'v'=vtol, 'f'=football
    rounds: 3,
  },
  hidden: false,          // aparece na lista pública
  timeoutMs: 10_000,      // rejeita com RoomCreationTimeoutError se demorar mais
});

console.log('Link da sala:', room.shareLink);
// => https://bonk.io/123456abcde

Opções de createRoom()

| Campo | Tipo | Default | Descrição | |---|---|---|---| | auth | AuthOptions | — | Estratégia de autenticação (obrigatório) | | desiredState | DesiredRoomState | — | Configuração da sala (obrigatório) | | hidden | boolean | false | Sala oculta na lista pública | | minLevel | number | 0 | Nível mínimo para entrar | | maxLevel | number | 999 | Nível máximo para entrar | | timeoutMs | number | 10000 | Timeout em ms para receber SHARE_LINK | | protocolVersion | number | 49 | Versão do protocolo bonk.io | | reconnectPolicy | ReconnectPolicyOptions | defaults | Política de backoff |

DesiredRoomState

interface DesiredRoomState {
  roomName: string;
  password: string;       // '' = sem senha
  maxPlayers: number;     // 1–8
  mode: string | number;  // 'b', 'ar', 'ard', 'sp', 'v', 'f'...
  engine?: string;        // 'b', 'f'...
  rounds: number;
  map?: string | null;    // blob LZ-String do mapa
}

Entrando em uma sala — joinRoom()

import { joinRoom } from 'bonktools';

// Via URL pública
const room = await joinRoom('https://bonk.io/123456abcde', {
  auth: { type: 'registered', username: '...', password: '...' },
  role: 'host',       // 'host' (time=1) ou 'spectator' (time=0, apenas o time inicial requisitado)
  password: '',       // senha da sala, se houver
});

A lib parseia a URL, resolve o servidor e depois conecta. Resolve após o packet ROOM_JOIN, ou rejeita com RoomJoinTimeoutError.

Também aceita um ResolvedRoomAddress já pronto (sem chamada HTTP):

const room = await joinRoom(
  { server: 'b2seattle1', joinId: '...', bypass: 'abcde' },
  { auth, role: 'spectator' },
);

Nota: role: 'spectator' define apenas o time inicial requisitado no JOIN_ROOM (mesmo comportamento do botão "Spectate" do client oficial) — o servidor ainda trata a conexão como um jogador normal daí em diante. Qualquer lógica de sala que decide quem joga (pick systems, etc.) precisa lidar com isso explicitamente.


BonkRoom — eventos e métodos

BonkRoom estende EventEmitter3<BonkRoomEvents>. Todos os eventos são tipados.

Estado atual da sala

const state = room.state;
// state.myId        — ID numérico do bot nesta sala (null antes de entrar)
// state.hostId      — ID numérico do host atual
// state.players     — Map<id, PlayerData>
// state.inGame      — boolean (partida em andamento)
// state.teamsLocked — boolean

room.currentMap; // blob LZ-String do mapa ativo, ou null (mapa padrão)

Eventos principais

// Jogador entrou
room.on('player-join', (packet) => {
  console.log(packet.userName, packet.id, packet.level);
});

// Jogador saiu
room.on('player-leave', (packet) => {
  console.log('saiu id:', packet.id);
});

// Mensagem de chat (filtra echo do próprio bot automaticamente)
room.on('chat-message', (packet) => {
  const nome = room.state.players.get(packet.id)?.userName;
  console.log(`[${nome}]: ${packet.message}`);
});

// Link da sala disponível
room.on('share-link', (packet) => {
  console.log(`https://bonk.io/${packet.roomId}${packet.bypass}`);
});

// Sala morreu (socket caiu, ban, sala cheia, retries esgotados)
room.on('room-dead', (reason) => {
  // reason.kind: 'socket-disconnect' | 'status-banned' | 'status-room_full' | 'max-retries-exceeded'
});

// Sala foi recriada após reconexão
room.on('room-rebuilt', (shareLink) => {
  console.log('nova URL:', shareLink);
});

// Todos os packets brutos (antes dos reducers)
room.on('raw-packet', (packet) => {
  if (packet.type === 'UNKNOWN') { /* packet não mapeado */ }
});

Tabela completa de eventos

| Evento | Payload | Descrição | |---|---|---| | room-join | RoomJoinPacket | Bot entrou na sala | | room-created | RoomCreatedPacket | Bot criou a sala | | player-join | PlayerJoinPacket | Jogador entrou | | player-leave | PlayerLeavePacket | Jogador saiu | | host-leave | HostLeavePacket | Host saiu (newHostId=-1 = sala fechada) | | team-change | TeamChangePacket | Jogador trocou de time | | ready-change | ReadyChangePacket | Jogador marcou/desmarcou pronto | | tabbed | TabbedPacket | Jogador tabou/voltou | | username-change | UsernameChangePacket | Jogador mudou de nome | | player-pings | PlayerPingsPacket | Ping de todos os jogadores | | game-start | GameStartPacket | Partida iniciada | | game-end | GameEndPacket | Partida encerrada | | all-ready-reset | AllReadyResetPacket | Reset de estado pronto | | chat-message | ChatMessagePacket | Mensagem de chat | | player-kick | PlayerKickPacket | Jogador foi kickado | | countdown | CountdownPacket | Countdown iniciado | | abort-countdown | AbortCountdownPacket | Countdown abortado | | teamlock-toggle | TeamlockTogglePacket | Times bloqueados/desbloqueados | | gamemode-change | GamemodeChangePacket | Modo de jogo alterado | | change-rounds | ChangeRoundsPacket | Número de rounds alterado | | map-switch | MapSwitchPacket | Mapa trocado | | balance-set | BalanceSetPacket | Balance de jogador alterado | | player-level-up | PlayerLevelUpPacket | Jogador subiu de nível | | room-name-update | RoomNameUpdatePacket | Nome da sala alterado | | room-password-update | RoomPasswordUpdatePacket | Senha da sala alterada | | status-message | StatusMessagePacket | Mensagem de status do servidor | | share-link | ShareLinkPacket | Link da sala disponível | | room-dead | RoomDeadReason | Sala morreu | | room-rebuilt | string (shareLink) | Sala reconectada | | raw-packet | IncomingPacket \| UnknownPacket | Todo packet bruto |

Métodos de ação

Sala

room.setRoomName('Novo Nome');
room.setRoomPassword('nova_senha');  // '' = remover senha

Jogo

room.startGame();
room.stopGame();           // volta ao lobby
room.startCountdown(3);    // countdown de 3s
room.abortCountdown();

room.setMode('b', 'b');    // engine, mode
room.setRounds(5);
room.setMap(lzStringBlob); // troca o mapa ativo

Moderação

room.chat('Olá!');
room.kickPlayer(id);       // kick sem ban
room.banPlayer(id);        // ban permanente

Times e host

// time: 0=spec 1=ffa 2=red 3=blue 4=green 5=yellow
room.setTeam(id, 2);
room.setTeamLock(true);
room.setTeamsEnabled(true);
room.giveHost(id);
room.setNoHostSwap(true);  // desativa troca automática de host

Desconectar

room.disconnect();  // idempotente, limpa timers e estado

Reconexão automática

BonkRoom reconecta automaticamente após desconexões transitórias (socket caiu, servidor reiniciou). A política de backoff é configurável:

const room = await createRoom({
  auth,
  desiredState: { /* ... */ },
  reconnectPolicy: {
    maxAttempts: 10,       // default
    initialDelayMs: 1000,  // default: 1s
    maxDelayMs: 30_000,    // default: 30s
    multiplier: 1.5,       // default
    jitter: true,          // full jitter (recomendado)
  },
});

Causas terminais (sem retry): status-banned, status-room_full, max-retries-exceeded. Causas transitórias (com retry): socket-disconnect.


BonkSession — pool de salas

Para rodar múltiplas salas com a mesma conta, use BonkSession. Ela compartilha o AuthClient, o token e aplica throttle de token-bucket entre criações.

import { BonkSession } from 'bonktools';

const session = new BonkSession({
  auth: { type: 'registered', username: '...', password: '...' },
  throttle: {
    capacity: 3,       // burst máximo de criações simultâneas
    refillPerSec: 0.5, // 1 slot reabastecido a cada 2s
  },
});

// Pré-autentica uma vez; token reusado em todas as salas
await session.getToken();

// Ouve eventos do pool
session.on('room-added', (localId) => {
  const { room } = session.rooms.get(localId)!;
  console.log('sala ativa:', room.shareLink);
});

session.on('room-dead-terminal', ({ localId, reason }) => {
  console.error('sala terminal:', localId, reason);
});

startFromConfig() — modo declarativo

Cria todas as salas a partir de um array de configs, com stagger + jitter entre criações. Registra as configs no reconcile loop de 60s (rede de segurança para falhas silenciosas).

await session.startFromConfig({
  rooms: [
    { id: 'sala-1', name: 'ATLAS', maxPlayers: 6, mode: 'b', rounds: 3 },
    { id: 'sala-2', name: 'ZEUS',  maxPlayers: 8, mode: 'ar', rounds: 5 },
  ],
  throttle: {
    maxConcurrentRooms: 10,
    roomCreationDelayMs: 3000,   // espera mínima entre criações
    roomCreationJitterMs: 2000,  // + aleatório de até 2s
  },
});

addRoom() / removeRoom() — modo imperativo

// Adicionar sala avulsa
const localId = await session.addRoom({
  id: 'sala-3',
  name: 'HERMES',
  password: 'segredo',
  maxPlayers: 4,
  mode: 'sp',
  rounds: 3,
});

// Acessar a BonkRoom diretamente
const { room, status } = session.rooms.get(localId)!;
room.chat('Olá!');

// Remover sala
await session.removeRoom(localId);

// Encerrar toda a sessão (idempotente)
await session.destroy();

RoomConfig

interface RoomConfig {
  id: string;          // identificador único (usado no reconcile)
  name: string;
  password?: string;   // default: ''
  maxPlayers?: number; // default: 6
  mode?: string;       // default: 'b'
  rounds?: number;     // default: 3
  hidden?: boolean;    // default: false
  map?: string;        // blob LZ-String
}

Status de uma sala no pool

| Status | Significado | |---|---| | starting | createRoom() ainda não resolveu | | active | sala criada e viva | | dead-transient | morta — será recriada com throttle | | dead-terminal | morta permanentemente (ban, sala cheia, retries esgotados) |


Travar times

room.lockTeams();     // jogadores não conseguem mais trocar de time nem sair para o spec
room.unlockTeams();  // libera de novo
room.state.teamsLocked; // estado atual

Só o host pode chamar (de outro cliente é ignorado com um aviso). Com os times travados só o host move jogadores (room.setTeam(id, team) continua funcionando). Quem entra depois do lock já vê a sala travada, e o lock é reaplicado sozinho se a sala for reconstruída (room-rebuilt). setTeamLock(boolean) continua disponível; lockTeams/unlockTeams são os atalhos.


Placar e vencedor (football, experimental)

O bonk.io não informa placar nem vencedor ao host por pacote nenhum. O ScoreTracker roda a física do football do próprio client do jogo (baixada de bonk.io na primeira execução, não faz parte do pacote) num worker, alimentada com o estado inicial da partida e com os inputs dos jogadores. O resultado é idêntico ao dos clientes, sem navegador e sem ocupar vaga na sala.

import { ScoreTracker } from 'bonktools';

const score = new ScoreTracker(room, { cacheDir: '.cache/bonk-client' });
score.on('score', ({ team, scores }) => room.chat(`ponto do time ${team}: ${scores[3]} x ${scores[2]}`));
score.on('match-winner', ({ team }) => console.log('venceu o time', team)); // 2 vermelho, 3 azul
score.on('error', (e) => console.error('rastreamento desativado', e));
await score.start();

// Estado inicial pelo próprio jogo (qualquer nº de jogadores e ids, sem blobs capturados):
const is = await score.buildInitialState([null, { id: 1, team: 3 }, { id: 2, team: 2 }]);
room.startGame({ is: is ?? undefined });
  • Só football; outros modos são ignorados. Vence quem chega a gs.wl (ou maxScore).
  • Depende de trechos do código ofuscado do client: se o jogo mudar, emite error e para.
  • Também exporta decodeInitialState, encodeInitialState e remapInitialStatePlayers (formato do IS blob documentado em BONK_PROTOCOL.md).

Anti-AFK

room.enableAntiAfk();                          // 12 s sem se mexer nem falar no chat (padrão)
room.on('player-afk',  (id) => room.kickPlayer(id));
room.on('player-back', (id) => room.chat(`jogador ${id} voltou`));
room.isAfk(playerId);                          // consulta pontual

Só vigia jogadores em time durante a partida (espectadores e o bot são ignorados). Opções: enableAntiAfk({ thresholdMs, checkIntervalMs }). O movimento é detectado pelos frames de input via WebRTC (evento peer-input); o chat, pelo Socket.IO.


Tratamento de erros

import { RoomCreationTimeoutError, RoomJoinTimeoutError } from 'bonktools';

try {
  const room = await createRoom({ auth, desiredState: { /* ... */ } });
} catch (err) {
  if (err instanceof RoomCreationTimeoutError) {
    // SHARE_LINK não chegou dentro do timeout
  }
}

try {
  const room = await joinRoom('https://bonk.io/...', { auth });
} catch (err) {
  if (err instanceof RoomJoinTimeoutError) {
    // ROOM_JOIN não chegou dentro do timeout (ex: sala cheia)
  }
}

Registro público de IS blobs

O IS blob (estado inicial da física) depende do mapa ativo na sala — capturar um novo exige um client real do bonk.io iniciando a partida (veja Decisões técnicas). Pra evitar que todo mundo precise capturar os mesmos blobs dos mapas padrão de cada gamemode, registry/blobs.json espelha publicamente os que este projeto já capturou:

const res = await fetch('https://cdn.jsdelivr.net/gh/brenoluizdev/bonktools@main/registry/blobs.json');
const registry = await res.json();

const blob = registry.gamemodeDefaults.vtol.blobs['2']; // vtol, mapa padrão, 1v1

Os mesmos dados já vêm embutidos na lib via GAMEMODE_DEFAULT_BLOBS/FOOTBALL_DEFAULT_BLOBS — o registro é útil pra quem não usa TypeScript/o pacote npm diretamente, ou quer os dados mais recentes sem esperar um novo release. Detalhes do formato e como contribuir: registry/README.md.


Decisões técnicas

Por que socket.io-client@2 e não a versão mais recente? O servidor bonk.io fala o protocolo Engine.IO 3 (EIO=3). O cliente v4 negocia EIO=4 e não tem opção de downgrade — a conexão falha imediatamente no handshake. A versão 2.5.0 é a última da linha v2 e a única compatível.

Por que undici e não fetch nativo? O bonk.io serve uma cadeia TLS Sectigo incompleta. O fetch nativo do Node.js não permite injetar uma CA customizada por requisição sem monkeypatch global. O undici.Agent permite configurar a CA Sectigo por cliente, sem afetar outras requisições HTTPS do processo.

Por que o CA Sectigo está bundlado? Usar o TLS store padrão do Node.js rejeitaria a cadeia incompleta do bonk.io. Ao invés de desativar toda a verificação TLS globalmente (NODE_TLS_REJECT_UNAUTHORIZED=0), o projeto bundla a cadeia completa Sectigo e injeta apenas onde necessário.

Por que EventEmitter3 e não o EventEmitter nativo? EventEmitter3 tem tipagem genérica por evento (EventEmitter<Events>), o que permite room.on('player-join', handler) com o tipo do handler inferido corretamente pelo TypeScript.


Notas de segurança

  • Credenciais (username, password) nunca são logadas — apenas eventos de sucesso/falha.
  • O token de sessão nunca aparece em logs.
  • TLS usa cadeia Sectigo customizada via undici.Agent — sem NODE_TLS_REJECT_UNAUTHORIZED=0 global.
  • Variáveis de ambiente são lidas via process.env — nunca hardcode credenciais no código.

Disclaimer

bonktools é um cliente não-oficial, resultado de engenharia reversa do protocolo Socket.IO público do bonk.io. Não é afiliado, endossado ou mantido pela equipe do bonk.io. Use por sua conta e risco, respeitando os termos de serviço do jogo — o projeto existe para automação, bots e ferramentas legítimas (salas 24h, moderação, integração com outras plataformas), não para lag/DDoS, spam ou qualquer forma de abuso.


Contribuindo

Veja CONTRIBUTING.md.


Licença

MIT