No description
  • TypeScript 93.8%
  • Nix 3.5%
  • CSS 2.7%
Find a file
2026-07-19 13:05:58 -04:00
apps feat: huge scripting improvements 2026-07-19 13:05:58 -04:00
nix feat: init 2026-07-14 18:53:03 -04:00
packages feat: huge scripting improvements 2026-07-19 13:05:58 -04:00
scripts style: lint & format everything 2026-07-15 17:22:16 -04:00
tests feat: huge scripting improvements 2026-07-19 13:05:58 -04:00
.envrc feat: direnv 2026-07-14 18:55:38 -04:00
.gitignore feat: init 2026-07-14 18:53:03 -04:00
bun.lock style: lint & format everything 2026-07-15 17:22:16 -04:00
default.nix feat: init 2026-07-14 18:53:03 -04:00
flake.lock feat: init 2026-07-14 18:53:03 -04:00
flake.nix feat: features. plenty of them. 2026-07-15 01:50:28 -04:00
LICENSE feat: init 2026-07-14 18:53:03 -04:00
oxfmt.config.ts fix: unformat protocol 2026-07-17 15:58:33 -04:00
oxlint.config.ts fix: unformat protocol 2026-07-17 15:58:33 -04:00
package.json style: lint & format everything 2026-07-15 17:22:16 -04:00
README.md style: lint & format everything 2026-07-15 17:22:16 -04:00
tsconfig.base.json feat: init 2026-07-14 18:53:03 -04:00

Raphael

Raphael is a local, browser-based MITM debugger for Minecraft Java protocol 47 (Minecraft 1.81.8.9) and protocol 774 (Minecraft 1.21.11). It terminates a local client connection, opens a separate connection to the target server, and records the decoded and exact raw packet stream. The protocol-47 listener supports opt-in online-mode client verification; otherwise listener connections are offline-mode.

The current implementation includes:

  • Bun + Elysia local service and Minecraft proxy.
  • SolidJS/Vite debugger with a virtualized timeline, structured filters, decoded/hex/effective inspectors, entity tracking, replay, and capture export.
  • SQLite sessions, packet outcomes, raw input/output, scripts, profiles, and annotations.
  • Protocol types generated from minecraft-data; 112 protocol-47 and 252 protocol-774 packet types are generated at build time without unknown or any payload type nodes.
  • Locally bundled Monaco TypeScript editor with browser and server diagnostics.
  • Trusted TypeScript interceptor subprocesses with ordered execution, a 100 ms fail-open timeout, rewrite/drop/injection decisions, and atomic activation.
  • Managed Microsoft device-code accounts with local credential caching, sign-out, per-target account switching, and preauthentication before the proxy starts listening.
  • Portable .raphael capture archives.
  • Reproducible bun2nix packaging and self-contained release archives with a Bun executable and bundled offline web assets.

Run from source

Requires Bun 1.3 or newer.

bun install --frozen-lockfile
bun run build
bun apps/server/src/index.ts

Raphael prints a one-time pairing URL and opens it in the default browser. Sign in to any Microsoft accounts you need from Accounts, create a target, choose Minecraft 1.8.9 or 1.21.11 and its upstream account, then start the listener. For Minecraft 1.8.9 targets, Client authentication can optionally verify the connecting client in online mode. This is separate from upstream authentication. Raphael refreshes the selected upstream account before opening the listener; add 127.0.0.1:25566 as a server in the matching client version.

For development, run the API without local pairing enforcement and start Vite separately:

RAPHAEL_DEV_AUTH=0 bun run dev:server
bun run dev:web

The Vite UI is at http://127.0.0.1:5173 and proxies API/WebSocket requests to port 3411.

Build a host-native standalone release archive (compiled executable, offline web assets, and bundled script type declarations) with:

bun run build:binary
./dist/release/raphael --no-open

Runtime flags include --no-open, --ui-port, --minecraft-port, --data-dir, and --bind.

Entity tracking

Raphael reads the logged-in player's entity ID from the clientbound play login packet for both supported protocol versions. Once observed, every session returned by GET /api/v1/sessions or GET /api/v1/sessions/:id includes localPlayerEntityId (or null before login). The bottom capture status shows the live entity count and the local player's YOU #… ID; selecting it opens the Entity Explorer.

The explorer live-updates as packets arrive and indexes numeric entity references in decoded packet fields, including nested entityId/entityIds, vehicle IDs, and passenger lists. Spawn packet type IDs are translated through the matching Minecraft version's entity registry when available. It exposes latest observed fields, field changes, every touching packet, and an inline decoded-payload inspector. The same data is available through:

  • GET /api/v1/sessions/:id/entities
  • GET /api/v1/sessions/:id/entities/:entityId
  • GET /api/v1/sessions/:id/entities/:entityId/packets

The entity filter autocompletes type:, status:, id:, and player: expressions and supports - exclusions. Lifecycle status is present after an observed login/spawn, removed when the latest lifecycle packet is entity_destroy, or unknown when neither was captured; for example, type:boat status:present and status:removed. Bare there is an alias for status:present.

Nix

nix develop --accept-flake-config
nix run --accept-flake-config

Regenerate nix/bun.nix after dependency changes:

nix run github:nix-community/bun2nix -- --lock-file bun.lock --output-file nix/bun.nix

Script model

Scripts are trusted local TypeScript, not a security sandbox. They run in a killable subprocess so a timeout or crash does not take down capture. A failing interceptor is disabled and the original packet is forwarded unchanged.

import { defineInterceptor, packet } from '@raphael/script-sdk';

export default defineInterceptor<'java/47'>({
  packets: [{ direction: 'clientbound', name: 'chat' }],
  onPacket(ctx) {
    if (packet(ctx, 'clientbound', 'chat')) {
      const seen = ctx.storage.get<number>('chat-count') ?? 0;
      ctx.storage.set('chat-count', seen + 1);
      ctx.log(ctx.packet.data.message);
      ctx.hide(); // Forward normally, but hide it unless “Show hidden” is enabled.
    }
    return ctx.forward();
  },
});

ctx.storage is shared by all enabled interceptors for one capture and supports get, set, has, delete, clear, and keys. It is cleared when the capture ends. packet(ctx, direction, name) narrows ctx.packet to the matching generated payload type.

Declare packets when an interceptor only needs specific packet types. Unmatched packets bypass the script subprocess entirely, avoiding decoded payload serialization and script round trips. When packets is omitted, Raphael intercepts every packet except the high-volume clientbound chunk payloads: map_chunk and map_chunk_bulk on Java 1.8, and map_chunk on Java 1.21.11. Set packets: "all" to intercept those payloads too.

ctx.state reports the current packet's protocol state. ctx.entityId reports the connected client's player entity ID once the play login packet has been observed, and is null before then. ctx.inject.clientbound(name, data) and ctx.inject.serverbound(name, data) queue typed play-state packets toward the client or upstream server.

The guard also accepts a raw packet: packet(ctx.packet, "clientbound", "chat") narrows that packet directly.

For 1.21.11, select the version at runtime as part of the interceptor value; this makes packet names and payloads version-specific in the editor:

export default defineInterceptor({
  version: 'java/774',
  packets: [{ direction: 'clientbound', name: 'system_chat' }],
  onPacket(ctx) {
    if (packet(ctx, 'clientbound', 'system_chat')) ctx.log(ctx.packet.data.content);
  },
});

Security model

The UI and Minecraft listener bind to loopback by default. The browser exchanges a one-time token for an HttpOnly, SameSite-strict cookie. Raphael never asks for a Microsoft password. Microsoft refresh and access tokens are stored only in the local Raphael data directory and removed when that account is signed out. Raphael does not include telemetry and never activates scripts imported from a capture.

Bedrock, protocol translation, and interactive packet breakpoints are intentionally outside the v1 scope.

Checks

bun run typecheck
bun test
bun run build

The loopback protocol integration test is enabled with RAPHAEL_INTEGRATION=1 bun test.

Licensed under MIT.