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

rfb2

v1.1.0

Published

RFB (VNC) protocol client and server for Node.js: every common encoding and security type, typed, promise-based

Readme

rfb2

CI npm

An RFB (VNC) client and server for Node.js, in TypeScript with no runtime dependencies. It speaks to TigerVNC, TightVNC, TurboVNC, x11vnc/libvncserver, QEMU, macOS Screen Sharing, RealVNC and UltraVNC, and decodes every common encoding to pixels in the format you choose.

import { connect, Framebuffer, buttons, keysyms } from 'rfb2'

const client = await connect({ host: '127.0.0.1', port: 5900, password: 'secret' })
console.log(`${client.name}: ${client.width}×${client.height}`)

const screen = Framebuffer.attach(client) // a copy of the remote screen, kept up to date

// Updates with back-pressure: the next one is requested when you ask for it.
for await (const update of client.updates()) {
  console.log(`${update.rects.length} rects`)
  const rgba = screen.toRgba() // for a canvas, a PNG encoder, a template matcher...
  break
}

client.pointerEvent(100, 100, buttons.left) // button down
client.pointerEvent(100, 100, 0) // and up
client.keyEvent(keysyms.Return, true)
client.keyEvent(keysyms.Return, false)
client.updateClipboard('héllo ☃') // UTF-8 when client.supports.unicodeClipboard

await client.close()

Node.js 22.12 or later. The package is an ES module and also loads with require('rfb2').

What's supported

| | | |---|---| | Protocol versions | 3.3, 3.7, 3.8 (Apple's 3.889 and RealVNC's 4.x get 3.8) | | Security types | None, VNC password, Apple Remote Desktop (macOS user login), VeNCrypt (Plain, TLSNone/Vnc/Plain, X509None/Vnc/Plain), RSA-AES (RA2, RA2ne, RA2_256, RA2ne_256), Tight, MS Logon II (UltraVNC); UltraVNC repeaters | | Encodings | Raw, CopyRect, RRE, CoRRE, Hextile, Zlib, Tight (lossless, and JPEG with a decoder you plug in), TightPNG, TRLE, ZRLE | | Pseudo-encodings | DesktopSize, ExtendedDesktopSize (+ SetDesktopSize), LastRect, DesktopName, Cursor, X Cursor, Cursor With Alpha, VMware cursor, cursor position (PointerPos, VMware), QEMU and VMware LED state, QEMU extended key events, QEMU audio, QEMU pointer motion, ExtendedMouseButtons, Fence, ContinuousUpdates, Extended Clipboard (UTF-8), xvp, compression and JPEG quality levels | | Pixel formats | any true-colour or colour-map format of 8, 16 or 32 bits, either byte order |

Every decoder is tested against encoders written from the specification, and every encoding against real TigerVNC, x11vnc, TightVNC, TurboVNC and QEMU servers in Docker (see Development).

Connecting

connect(options) resolves with a connected RfbClient (or rejects with an RfbError). new RfbClient(options) starts connecting at once and emits connect; createConnection(options) is the 0.x name for it.

| Option | Default | | |---|---|---| | host, port | 127.0.0.1, 5900 | | | path | | a Unix socket instead | | stream | | any duplex stream instead: a proxy, a WebSocket stream, a reverse connection, an FBS replay | | password, username | | username is for the types that log in a user (ARD, VeNCrypt Plain, RSA-AES, MS Logon II) | | credentials | | async ({ securityType, needs }) => ({ username, password }), asked for what's missing | | security | all, by the credentials given | security types (security.*) in order of preference; VeNCrypt subtypes such as security.X509Vnc select VeNCrypt | | tls | | tls.connect() options for the VeNCrypt X509 subtypes, e.g. { ca, servername } | | encodings | defaultEncodings | encodings and pseudo-encodings to offer, best first | | pixelFormat | 'bgrx' | 'bgrx', 'rgbx', 'rgb565', 'bgr233', a PixelFormat, or 'server' for the server's own | | shared | true | false asks the server to disconnect other clients | | initialUpdate | true | request the whole screen right after connecting | | autoUpdate | false | request the next incremental update after each update | | decodeJpeg | | turns Tight JPEG rects into pixels (see JPEG) | | maxClipboardBytes | 10 MiB | the longest clipboard text taken from the server; longer text is dropped (never buffered or inflated past this) with a CLIPBOARD_TOO_LARGE warning. With the Extended Clipboard it's also the size the client announces it takes unasked | | timeout | none | fail with TIMEOUT when the handshake takes longer (ms) | | signal | | an AbortSignal that closes the connection | | repeaterId | | the ID to send to an UltraVNC repeater, e.g. 'ID:1234' | | maxVersion | '3.8' | the highest protocol version to agree to |

After connect: width, height, name, pixelFormat, serverPixelFormat, protocolVersion, serverVersion, securityType, screens, colorMap, cursor, and supports (what the server has confirmed: extendedDesktopSize, continuousUpdates, fence, extendedClipboard, unicodeClipboard, qemuExtendedKeyEvent, qemuAudio, extendedMouseButtons, xvp). unicodeClipboard means updateClipboard sends any text as UTF-8 (the server's Extended Clipboard takes text with the notify action); without it, text may go as Latin-1 with ? for other characters, so check it before sending text that Latin-1 can't hold.

Security

Without a security option the client takes the first type the server offers from: None, the VNC password types, RSA-AES, then the user-login types (ARD, VeNCrypt Plain, MS Logon II) — or the user-login types first when a username is given, which is what macOS Screen Sharing wants:

// macOS: System Settings → Sharing → Screen Sharing; log in as a user of the Mac.
const mac = await connect({ host: 'my-mac.local', username: 'me', password: '…' })

// TigerVNC with a certificate:
const tls = await connect({ host, security: [security.X509Vnc], password, tls: { ca: readFileSync('ca.pem') } })

The anonymous TLS subtypes (TLSNone, TLSVnc, TLSPlain) encrypt but can't authenticate the server, and RSA-AES doesn't verify the server's key either.

Updates and rects

Each FramebufferUpdate emits a rect event per rect, then an update event with all of them. Every rect has type, x, y, width, height and the wire encoding:

| type | | | |---|---|---| | pixels | data | width * height pixels in client.pixelFormat, whatever the encoding (Raw, Hextile, ZRLE, Tight, ...) | | copy | src | CopyRect: copy from src.x, src.y (in the framebuffer as it is at that point) | | jpeg | jpeg | Tight JPEG data, when there's no decodeJpeg | | cursor | rgba | a new cursor shape (x, y is the hotspot); also the cursor event | | desktopSize, extendedDesktopSize | screens, reason, status | the framebuffer changed size; also resize | | desktopName | name | also the desktopName event | | cursorPosition, ledState, pointerMotion, cursorState, capability | | server notifications |

Apply rects in order. Framebuffer does it for you, CopyRect and resizes included:

const fb = Framebuffer.attach(client) // or new Framebuffer(w, h, pixelFormat) and fb.apply(rect)
fb.data // the screen in fb.pixelFormat; fb.toRgba() converts it

Ways to ask for updates:

  • requestUpdate(incremental = true, x, y, width, height) (whole screen by default) and the update event; nextUpdate() resolves with the next update.
  • for await (const update of client.updates()): requests the next update only when you take the previous one, so a slow consumer never falls behind (the first request is non-incremental).
  • autoUpdate: true: a new incremental request after every update.
  • enableContinuousUpdates() when supports.continuousUpdates: the server sends updates unasked.

JPEG

Tight JPEG is lossy and only used when you offer a quality level (jpegQuality(0..9) or jpegFineQuality(1..100) in encodings). Bring a decoder that returns RGB or RGBA pixels:

import jpeg from 'jpeg-js'

const client = await connect({
  encodings: [encodings.tight, jpegQuality(7), ...defaultEncodings],
  decodeJpeg: (data) => jpeg.decode(data, { useTArray: true }).data,
})

Input and other messages

| | | |---|---| | pointerEvent(x, y, buttons) | buttons.left \| buttons.right ...; the wheel is a press and release of buttons.wheelUp / wheelDown | | keyEvent(keysym, down) | X11 keysyms: keysyms.Return, keysymFromChar('é'), ... | | qemuKeyEvent(keysym, scancode, down) | QEMU: XT scancodes, independent of the server's keymap | | updateClipboard(text) | UTF-8 when supports.unicodeClipboard, else Latin-1 | | setDesktopSize(width, height, screens?) | resolves when the server has resized | | setEncodings(list) | change the encodings mid-session | | fence(flags, payload), xvp('reboot'), enableAudio(format) / disableAudio() | | | close(), end(), destroy() | close() resolves once closed |

Events: connect, rect, update, resize, cursor, cursorPosition, desktopName, ledState, colormap, bell, clipboard, clipboardEmpty, fence, xvp, audio, endOfContinuousUpdates, warning, error, close.

The server's clipboard arrives as clipboard with its text ('' when the server sends empty text). With the Extended Clipboard, clipboardEmpty says that it changed and holds no text: it was emptied, or holds only other formats, such as an image. To wait for what a copy on the server produced, listen for both. TigerVNC and TurboVNC also tell every client "no text" when a client's updateClipboard replaces text of the server's own; the client that made the change gets no clipboardEmpty for it. After updateClipboard, the client hands its text to the server on request until the server reports a newer clipboard of its own.

Errors

Errors are RfbErrors with a code you can switch on, and messages that say what to do next. reason holds the server's own explanation when it sent one.

CONNECTION_FAILED, CONNECTION_CLOSED, TIMEOUT, ABORTED, NOT_RFB, UNSUPPORTED_VERSION, SERVER_REFUSED, UNSUPPORTED_SECURITY, AUTH_REQUIRED, AUTH_FAILED, TLS_FAILED, UNSUPPORTED_ENCODING, UNSUPPORTED_MESSAGE, UNSUPPORTED_BY_SERVER, PROTOCOL_ERROR, INVALID_ARGUMENT.

During the handshake they reject connect() (or are emitted as error). Afterwards they're error events, followed by close. A server that closes the connection between messages just causes close.

A warning event is an RfbError the connection survives. Its one code is CLIPBOARD_TOO_LARGE: the server's clipboard text was over maxClipboardBytes and was dropped.

Sizes from the wire are checked before they cost memory or time: zlib data (Zlib, ZRLE, Tight, TightPNG, the Extended Clipboard) stops inflating at the most the rect or clipboard text can hold, a JPEG must be the size of its rect before it reaches decodeJpeg, and malformed data fails the connection with PROTOCOL_ERROR.

Recording

recordFbs(socket, file) records what a server sends in the FBS format (rfbproxy, vncrec); replayFbs(recording, { speed }) plays it back as a stream for connect({ stream }):

const socket = net.connect(5900, host)
recordFbs(socket, fs.createWriteStream('session.fbs'))
const client = await connect({ stream: socket })
// later
const replay = await connect({ stream: replayFbs(fs.readFileSync('session.fbs'), { speed: 1 }) })

Server

createServer(options, onConnection) is a TCP server; RfbServerConnection the server side of one connection over any stream. It does the handshake (None, or VNC authentication with password), parses the client's messages into events (setPixelFormat, setEncodings, updateRequest, key, pointer, clipboard) and sends updates converted to the client's pixel format:

createServer({ width: 640, height: 480, name: 'node', password: 'secret' }, (conn) => {
  conn.on('updateRequest', ({ incremental }) => {
    if (!incremental) conn.sendUpdate([{ type: 'pixels', x: 0, y: 0, width: 640, height: 480, data: frame }])
  })
  conn.on('key', ({ keysym, down }) => {})
}).listen(5900)

Rects go out Raw (or CopyRect, DesktopSize, Cursor, or pre-encoded with { type: 'encoded' }).

Upgrading from 0.x

The 0.x API still works: createConnection(), the connect, rect, resize, clipboard, bell and error events, requestUpdate(incremental, x, y, w, h), pointerEvent, keyEvent, updateClipboard, end(), title, rect.data for Raw and rect.src for CopyRect, and the pixel format fields (bpp, redShift, ...). What changed:

  • Pixel format: rects come in bgrx (32-bit little-endian 0x00RRGGBB) unless you set pixelFormat; 0.x echoed the server's own format (pixelFormat: 'server' does that).
  • Every encoding is decoded: Hextile, ZRLE, Tight, ... arrive as type: 'pixels' rects with data. The default encodings now include them; 0.x offered Raw and CopyRect only.
  • Errors are RfbError objects (0.x emitted strings for server errors) and are never thrown from socket callbacks.
  • params.in / params.out (FBS replay and recording) became replayFbs() / recordFbs().
  • The rfbclient.js, unpackstream.js and d3des.js files are gone; import from rfb2.
  • Node.js 22.12 or later.

Development

npm install                # also builds dist/ (the prepare script)
npm run check              # lint (Biome), typecheck (tsc), unit tests (node:test)
npm run build              # rebuild dist/ after changing src/
npm run servers:up         # VNC servers in Docker on 127.0.0.1:159xx (see docker/README.md)
npm run test:integration   # every encoding, pixel format and security type against them
npm run servers:down

The sources run directly on Node (type stripping): node examples/screenshot.ts host 5900 > screen.png. To try an unreleased branch in another project, npm install github:sidorares/node-rfb2#<branch>; npm runs prepare there too, so the install has dist/.

Integration tests skip servers that aren't running; CI starts them all (.github/workflows/ci.yml). Commits on master use Conventional Commits. release-please opens a release PR from them; merging it tags the release and publishes to npm (.github/workflows/release-please.yml).

See also

License

MIT