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

@xkore/triple-ratchet

v0.1.4

Published

Transport-agnostic bounded triple ratchet for quantum-resistant encrypted P2P communication.

Readme

@xkore/triple-ratchet

Transport-agnostic bounded triple ratchet for quantum-resistant encrypted P2P communication.

Overview

@xkore/triple-ratchet provides end-to-end encrypted sessions between peers using a bounded triple ratchet protocol combining:

  • ML-KEM-1024 (NIST FIPS 203) - Quantum-resistant key encapsulation
  • X25519 - Classical ECDH for defense-in-depth
  • XChaCha20-Poly1305 - Authenticated encryption
  • Bounded rotation - Enforced message/time limits to prevent stale quantum-resistant keys

Inspired by Signal's SPQR protocol, adapted for P2P environments where communication can be one-sided.

Features

  • Transport-agnostic - No I/O, no network, pure crypto
  • Event-based API - Emit send events, handle receive calls
  • Out-of-band key exchange - User controls key distribution
  • Automatic ratcheting - Forward & backward secrecy
  • Bounded rotation - Time & message-based ML-KEM rotation
  • Out-of-order handling - Skipped message keys (up to 1000 gap)
  • Serializable state - Persist sessions with getState()
  • Browser compatible - No Node.js dependencies

Installation

npm install @xkore/triple-ratchet

Quick Start

import { Session, Keys, RatchetKeys } from "@xkore/triple-ratchet";

// Setup local keys
const localKeys = new Keys();
const localInitiationKeys = new RatchetKeys();

// Get remote peer's public initiation keys (out-of-band exchange)
const remoteInitiationKeys = remoteInitiationKeysFromSomewhere;

// Create session
const session = new Session({
	localKeys,
	localInitiationKeys,
	remoteNodeId: remoteKeys.nodeId,
	remoteInitiationKeys,
});

// Handle outgoing buffers
session.on("send", (buffer) => {
	myTransport.send(remoteAddress, buffer);
});

// Handle incoming decrypted messages
session.on("message", (data) => {
	console.log("Received:", data);
});

// Handle state changes (for persistence)
session.on("stateChanged", () => {
	db.put(remoteNodeId, session.getState());
});

// Send encrypted data
await session.send(new TextEncoder().encode("hello"));

// Receive from transport
myTransport.on("message", (buffer) => {
	session.receive(buffer);
});

After initial key exchange, the session handles all cryptographic state updates automatically. The stateChanged event fires after each send() or receive() so you can persist the updated state.

Key Exchange

The initiator must fetch the responder's initiation keys before the first message. This is a one-time, one-way exchange—once the session is established, key rotation happens automatically via the ratchet protocol.

import { RatchetPublicKeys } from "@xkore/triple-ratchet";

// Publish your keys (responder)
app.get("/initiation-keys", (req, res) => {
	res.json(localInitiationKeys.publicKeys.toJson());
});

// Fetch remote peer's keys (initiator)
const response = await fetch(`https://peer.example.com/initiation-keys`);
const remoteKeys = RatchetPublicKeys.fromJson(await response.json());

const session = new Session({
	localKeys,
	localInitiationKeys,
	remotePublicKey,
	remoteInitiationKeys: remoteKeys,
});

Session Persistence

Sessions can be serialized and restored using either JSON or binary formats:

Using JSON

import { RatchetState } from "@xkore/triple-ratchet";

// Save state as JSON
session.events.on("stateChanged", async () => {
	const state = session.ratchetState;

	if (state) {
		await db.put(remoteNodeId, JSON.stringify(state.toJson()));
	}
});

// Restore from JSON
const savedJson = await db.get(remoteNodeId);
const ratchetState = savedJson ? RatchetState.fromJson(JSON.parse(savedJson)) : undefined;

const session = new Session({ localKeys, localInitiationKeys, remotePublicKey, ratchetState });

Using Binary

// Save state as binary (more compact)
session.events.on("stateChanged", async () => {
	const state = session.ratchetState;

	if (state) {
		await db.put(remoteNodeId, state.buffer);
	}
});

// Restore from binary
const savedBuffer = await db.get(remoteNodeId);
const ratchetState = savedBuffer ? RatchetState.fromBuffer(savedBuffer) : undefined;

const session = new Session({ localKeys, localInitiationKeys, remotePublicKey, ratchetState });

Transport Integration

WebSocket Example

const ws = new WebSocket("wss://peer.example.com");

session.events.on("send", (buffer) => {
	ws.send(buffer);
});

ws.onmessage = (event) => {
	session.receive(new Uint8Array(event.data));
};

// Send a message
await session.send(new TextEncoder().encode("Hello over WebSocket!"));

UDP Example

import dgram from "dgram";

const socket = dgram.createSocket("udp4");

session.events.on("send", (buffer) => {
	socket.send(buffer, remotePort, remoteHost);
});

socket.on("message", (buffer) => {
	session.receive(buffer);
});

// Send a message
await session.send(new TextEncoder().encode("Hello over UDP!"));

Security Properties

  • Forward secrecy: Compromised state doesn't reveal past messages
  • Backward secrecy: Compromised state doesn't reveal future messages after next ratchet
  • Post-quantum security: ML-KEM-1024 protects against quantum computers
  • Bounded rotation: Keys rotate every 100 messages or 1 hour (configurable)
  • Out-of-order tolerance: Up to 1000 message gap before rejection (DoS protection)

Configuration

Session accepts optional configuration for ratchet bounds and limits:

const session = new Session(
	{
		localKeys,
		localInitiationKeys,
		remotePublicKey,
	},
	{
		// ML-KEM rotation triggers
		messageBound: 100, // Rotate after 100 messages (default)
		timeBound: 3600000, // Rotate after 1 hour in ms (default)

		// Out-of-order message handling
		maxMessageSkip: 1000, // Max gap before rejection (default)
		maxStoredSkippedKeys: 2000, // Max stored skipped keys (default)
		skippedKeyMaxAge: 86400000, // Prune skipped keys after 24h (default)
	},
);

API Reference

Session

class Session {
  constructor(options: SessionOptions)

  send(data: Uint8Array): Promise<void>
  receive(buffer: Uint8Array): void
  getState(): RatchetState | undefined
  setRemoteInitiationKeys(keys: RatchetPublicKeys): void

  // Events
  on('send', (buffer: Uint8Array) => void)
  on('message', (data: Uint8Array) => void)
  on('stateChanged', () => void)
  on('error', (error: Error) => void)
}

Keys

class Keys {
	constructor(properties?: { secretKey?: Uint8Array });

	readonly secretKey: Uint8Array;
	readonly publicKey: Uint8Array;
	readonly nodeId: Uint8Array;

	rSign(message: Uint8Array): RSignature;
	static recover(signature: RSignature, message: Uint8Array): Uint8Array;
}

RatchetKeys

class RatchetKeys {
	constructor(properties?: { dhSecretKey?: Uint8Array; mlKemSeed?: Uint8Array });

	readonly keyId: Uint8Array;
	readonly encryptionKey: Uint8Array;
	readonly decryptionKey: Uint8Array;
	readonly dhPublicKey: Uint8Array;

	get publicKeys(): RatchetPublicKeys;
	toPublicBuffer(): Uint8Array;
	static fromPublicBuffer(buffer: Uint8Array): RatchetPublicKeys;
}

Architecture

Session (event-based communication)
  ├─ Keys (secp256k1 identity)
  ├─ RatchetKeys (ML-KEM-1024 + X25519 initiation keys)
  └─ RatchetState (per-peer triple ratchet state)
      ├─ RootChain (root key + DH ratchet)
      │   ├─ KeyChain (symmetric sending chain)
      │   └─ KeyChain (symmetric receiving chain)
      └─ Envelope (wire format with XChaCha20-Poly1305)

Comparison to Signal Protocol

| Feature | Signal | @xkore/triple-ratchet | | ------------------ | ------------------ | --------------------- | | Quantum resistance | SPQR (optional) | ML-KEM-1024 (always) | | Transport | Centralized server | Any transport | | Key exchange | X3DH | Out-of-band | | Rotation bounds | None | Enforced (100 msg/1h) | | Use case | Mobile messaging | P2P applications |

License

MIT

Credits

Built with @noble/post-quantum, @noble/curves, and @noble/ciphers.

Inspired by Signal's SPQR protocol.