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

mls-ts

v0.1.0

Published

End-to-end encrypted group messaging for Node.js and the browser, using the MLS protocol (RFC 9420).

Readme

mls-ts

End-to-end encrypted group messaging for JavaScript and TypeScript.

mls-ts adds secure group chat to your Node.js and web apps. It implements MLS — Messaging Layer Security, the IETF standard (RFC 9420) for end-to-end encrypted group messaging, with forward secrecy and post-compromise security — behind a small, typed API: create an MlsClient, form a Group, and send and receive encrypted messages. The cryptography runs on the endpoints, so a server only ever sees ciphertext.

Same audited cryptography everywhere: mls-ts wraps OpenMLS — the Rust reference implementation of RFC 9420 — compiled to WebAssembly. No cryptography is reimplemented in JavaScript.

Status: functional prototype. The API is still evolving and it isn't hardened for production yet — see Limitations. Developed with support from Prototype Fund.

Goals

  • Provide a TypeScript-friendly interface for MLS group messaging primitives.
  • Support web, Node.js, and React Native integration paths.
  • Keep the project understandable for contributors who are new to MLS.
  • Maintain the project as free and open-source software.

Installation

npm install mls-ts

The compiled WebAssembly is bundled with the package, so no Rust toolchain is required to use it.

Usage

Each participant is an MlsClient that owns its own in-memory key material. Create or join a Group, then send and receive end-to-end encrypted messages.

import { MlsClient } from "mls-ts";

// Two participants (each models a separate device).
const alice = new MlsClient("alice");
const bob = new MlsClient("bob");

// Alice founds a group and adds Bob from his published key package.
const group = alice.createGroup("chess-club");
const add = group.add(bob.keyPackage());

// Bob joins from the welcome + ratchet tree Alice produced.
const bobGroup = bob.joinGroup(add.welcome, add.ratchetTree);

// End-to-end encrypted application messages.
const ciphertext = group.send("hello bob");
console.log(bobGroup.receiveText(ciphertext)); // => "hello bob"

Adding members and distributing messages

Group.add() commits the change locally and returns everything you need to distribute:

| Field | Give it to | Via | | --- | --- | --- | | welcome + ratchetTree | the new member | client.joinGroup(welcome, ratchetTree) | | proposal then commit | other existing members | group.receive(proposal) then group.receive(commit) |

Transporting these between processes/devices is up to your application — every value is a plain Uint8Array.

API summary

MlsClient

  • new MlsClient(name) — create an in-memory participant with its own provider + identity.
  • MlsClient.open(id, adapter): Promise<MlsClient> — open a persistent client, restoring a prior snapshot or starting fresh.
  • save(): Promise<void> — persist the current state (persistent clients only).
  • keyPackage(): Uint8Array — a serialized key package to publish so others can add you.
  • createGroup(groupId): Group — found a new group.
  • joinGroup(welcome, ratchetTree, groupId?): Group — join from an add's output; pass groupId to make it reloadable after a restart.
  • group(groupId): Group | undefined — get a group handle by id (e.g. after open restores it).

Group

  • add(keyPackage): { welcome, ratchetTree, proposal, commit } — add a member (commits + applies locally).
  • send(message: string | Uint8Array): Uint8Array — encrypt an application message.
  • receive(bytes): Uint8Array — process a message; returns plaintext for application messages, empty for handshakes.
  • receiveText(bytes): string | null — like receive, decoded as UTF-8; null for handshakes.
  • exportRatchetTree(): Uint8Array — serialize the ratchet tree (for joiners).
  • exportKey(label, context, length): Uint8Array — derive an exported group secret.

Helpers: encodeUtf8, decodeUtf8, toHex, fromHex. The raw generated bindings are also re-exported as raw for advanced use.

Persistence

By default a client is in-memory and forgets everything when the process exits. For persistence across restarts, open a client with a StorageAdapter and save() after operations you want to keep:

import { MlsClient, FileStorageAdapter } from "mls-ts";

const adapter = new FileStorageAdapter("./data");

// session 1
const alice = await MlsClient.open("alice", adapter);
const group = alice.createGroup("room");
// ... add members, send / receive ...
await alice.save();

// session 2 (after a restart): the same id + adapter restores the session
const alice2 = await MlsClient.open("alice", adapter);
const group2 = alice2.group("room"); // resumes where it left off

The StorageAdapter interface is a tiny async load/save key–value store, so you can target other platforms (IndexedDB on web, AsyncStorage on React Native) by supplying your own. Only the Node FileStorageAdapter ships today.

Demo

A runnable, narrated three-party demo (alice, bob, charlie forming a group and exchanging messages):

npm run demo            # smoke demo (three parties form a group, exchange messages)
npm run demo:present    # narrated encrypted group chat, with restart persistence
npm run demo:files      # encrypted file sharing (per-file key derived from the group)

The browser and React Native demos live under examples/web and examples/react-native. For a full demo runbook covering all three targets — talking points, exact commands, and a Q&A cheat sheet — see docs/presentation.md.

Development

The wrapper depends only on the prebuilt WebAssembly committed under wasm/. The OpenMLS Rust source is vendored under openmls/ as committed build input, so the wasm can be rebuilt from a clean clone — see openmls/VENDOR.md. Only npm run build:wasm uses it, and only that step needs a Rust toolchain.

npm install        # install dev dependencies + link the local wasm package
npm run typecheck  # type-check the source
npm test           # run the vitest suite
npm run build      # emit dist/ (JS + type declarations)
npm run build:wasm # regenerate wasm/ from the vendored Rust source (needs Rust + wasm-pack)

Repository structure

.
├── src/               # TypeScript facade (MlsClient, Group, helpers) + demo
├── tests/             # vitest end-to-end tests
├── wasm/              # committed openmls-wasm build artifact (bundled on publish)
├── scripts/           # build-wasm.sh (regenerates wasm/)
├── openmls/           # vendored OpenMLS Rust source (committed build input; see VENDOR.md)
├── docs/              # project notes and documentation
├── CONTRIBUTING.md    # contribution guidelines
├── CODE_OF_CONDUCT.md # community expectations
├── SECURITY.md        # security reporting policy
├── LICENSE            # MIT License
└── README.md          # project overview

Limitations

This is a prototype. Current known constraints:

  • Async on web and React Native — the Node target is synchronous; the browser target loads WebAssembly via init(), and React Native additionally round-trips through a hidden WebView bridge (Hermes has no WebAssembly runtime). See mls4rn for its specific constraints.
  • Persistence is opt-in and coarse — without an adapter, clients are in-memory and forget everything on exit. With a StorageAdapter, save() writes a full snapshot of the client's storage each time (simple, not incremental), and snapshots hold private keys unencrypted at rest unless your adapter encrypts them. Only a Node file adapter ships.
  • Single fixed ciphersuite — MLS_128_DHKEMX25519_CHACHA20POLY1305_SHA256_Ed25519.
  • Rough edges — malformed input to receive() currently surfaces as a thrown wasm error rather than a clean typed error.

Contributing

Contributions, questions, and feedback are welcome. Please read CONTRIBUTING.md before opening issues or pull requests.

Security

Please do not report security issues in public GitHub issues. See SECURITY.md for the current reporting process.

License

This project is licensed under the MIT License. See LICENSE.

It builds on OpenMLS (also MIT), whose compiled WebAssembly is bundled with this package. Redistributed third-party licenses are reproduced in THIRD_PARTY_NOTICES.md.