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

@acpantenna/relay-protocol

v0.4.0

Published

acpantenna's relay protocol: Noise NKpsk0 to pair and KK to reconnect, frames, the pairing link and the SAS words. Runs in browsers and Bun.

Readme

@acpantenna/relay-protocol

The wire protocol acpantenna speaks with a web client through a relay: Noise NKpsk0_25519_ChaChaPoly_BLAKE2s to pair and KK_25519_ChaChaPoly_BLAKE2s to reconnect, the frame codec, the pairing link and the SAS words. The spec is docs/relay-protocol.md.

It uses only Uint8Array, TextEncoder, crypto.getRandomValues and the @noble libraries, so the same code runs in Bun and in a browser.

import { initiator, parseLink, isLinkExpired } from "@acpantenna/relay-protocol";

const link = parseLink(location.href);
const session = initiator({
	link,
	send: (data) => socket.send(data),
	onHandshake: ({ sas }) => showWords(sas),
	onConfirmed: () => session.send(JSON.stringify(initializeRequest)),
	onMessage: (message) => handle(JSON.parse(message)),
	onClose: (event) => socket.close(),
});
// After the relay says {"t":"ready"}:
session.start();
// Every binary WebSocket message:
socket.onmessage = (event) => session.receive(new Uint8Array(event.data));

When message 2 arrives the initiator sends a PING on its own: its first transport frame, which lets the agent side confirm the keys. The agent side is responder({ staticKeyPair, psk, room, webOrigin, acceptEphemeral, send, ... }); it calls onHandshake with the words only once that PING has decrypted, and confirm() once the user has checked them.

Remembering and reconnecting

Right after onConfirmed, a browser that wants to come back sends the public half of a static key it made for this connection: session.remember(publicKey). The agent side gets it in onRemember. In a browser the key can be a non-extractable WebCrypto X25519 key: a StaticKey is only its public half and an asynchronous dh.

const keys = await crypto.subtle.generateKey({ name: "X25519" }, false, ["deriveBits"]);
const staticKey = {
	publicKey: new Uint8Array(await crypto.subtle.exportKey("raw", keys.publicKey)),
	dh: async (publicKey) => {
		const peer = await crypto.subtle.importKey("raw", publicKey, { name: "X25519" }, false, []);
		return new Uint8Array(await crypto.subtle.deriveBits({ name: "X25519", public: peer }, keys.privateKey, 256));
	},
};

To come back, join the room with {"v":1,"role":"browser","resume":true} and, on ready, start a reconnectInitiator({ room, webOrigin, remoteStaticPublicKey, staticKey, resume, buffer, ... }). Pass the buffer of the connection's last session and resume: true to carry on where it stopped. After a reload, pass resume: false; then session.resumed is false, and the client loads the session afresh. The agent side answers with reconnectResponder({ staticKeyPair, room, webOrigin, remoteStaticPublicKey, buffer, ... }), with the key it was given and its own buffer.

Test vectors

test-vectors/ holds what a second implementation should check itself against:

  • noise-nkpsk0.json and noise-kk.json: the vectors for Noise_NKpsk0_25519_ChaChaPoly_BLAKE2s and Noise_KK_25519_ChaChaPoly_BLAKE2s, from cacophony;
  • frames.json: frames and their plaintexts, frames to refuse, padding buckets, chunking, and stream sequences to accept or refuse;
  • sas.json: handshake hashes and their words;
  • session.json: a whole pairing with fixed keys, every relay message in order.

scripts/vectors.ts regenerates the last three; only do that when the protocol changes on purpose.

The BIP-39 English wordlist in src/bip39-english.ts is MIT-licensed; its notice is in the file.