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

matrix-gleam-sdk

v0.1.0

Published

A lean Matrix client SDK with from-scratch E2EE, written in Gleam and usable from TypeScript/JavaScript.

Readme

matrix-gleam-sdk

A lean Matrix client SDK for Gleam, targeting the JavaScript backend (Node, Deno, Bun, browsers — anywhere fetch exists).

It wraps the everyday parts of the Client-Server API: logging in, syncing, reading history, sending messages, managing rooms, and end-to-end encryption (Olm + Megolm) — with zero runtime dependencies beyond the Gleam basics (gleam_stdlib, gleam_json for encoding, gleam_javascript for promises). HTTP is a 20-line fetch FFI shim and the crypto is a thin shim over the runtime's built-in node:crypto; there is nothing else underneath. No libolm, no vodozemac, no WASM.

Design

  • A Client is just an immutable record — homeserver URL, access token, user ID, device ID. No hidden state, no event emitters, no caches. Copy it, store it, serialise it.
  • Everything returns Promise(Result(a, matrix/error.Error)). One error type for the whole SDK: Network, Server(status, code, message), RateLimited, Unexpected.
  • Events keep their content as Dynamic. Unknown event types flow through untouched; typed helpers (event.message, event.text) and ordinary gleam/dynamic/decode decoders read out what you need.
  • State aggregation is a pure fold. matrix/room turns lists of state events into Room snapshots with no I/O, so it is trivially testable and reusable.
  • E2EE from scratch, not from libolm. The Olm double ratchet and Megolm group ratchet are implemented directly from the spec (see below). The only crypto primitives used are the platform's own — X25519, Ed25519, HKDF, HMAC, AES-CBC, all from node:crypto. The ratchets and the entire account/session state are pure, immutable Gleam values.

For how this surface lines up with a real client's needs, see COVERAGE.md — a map of the matrix-js-sdk API that Cinny exercises onto this SDK.

Modules

| Module | Contents | |---------------------------|----------| | matrix | The facade: sessions, rooms, sending, reactions/replies/edits, history, relations & context, search, account data, profiles, media, spaces, devices, push | | matrix/sync | /sync long-polling: once and the auto-retrying stream loop | | matrix/room | Pure folding of state events into Room snapshots | | matrix/event | The Event type, its decoder, and content helpers | | matrix/api | Client, raw request/expect — call any endpoint the facade doesn't wrap | | matrix/error | The single Error type | | matrix/crypto | The E2EE engine: the Vault holding account + sessions, and all Matrix crypto JSON | | matrix/e2ee | HTTP wiring for E2EE: keys, to-device, share-key, send-encrypted, cross-signing, key backup | | matrix/crypto/olm | The Olm double ratchet (m.olm.v1.curve25519-aes-sha2) | | matrix/crypto/megolm | The Megolm group ratchet (m.megolm.v1.aes-sha2) | | matrix/crypto/cross_signing | The master / self-signing / user-signing key hierarchy | | matrix/crypto/verification | SAS device verification (emoji/decimal/MAC) | | matrix/crypto/secret_storage| Secret storage (SSSS): AES-CTR+HMAC, PBKDF2, recovery keys | | matrix/crypto/key_backup | Server-side key backup (m.megolm_backup.v1) | | matrix/crypto/primitives| Crypto bindings + unpadded base64 + canonical JSON |

Using it from TypeScript / JavaScript

The SDK ships a prebuilt, dependency-free ESM bundle with TypeScript types in dist/, so it can be consumed from any TS/JS project without a Gleam toolchain. The public surface is an idiomatic MatrixClient class — async methods that resolve on success and reject with a typed MatrixError (no Gleam Result/Option leaks; mxc media is Uint8Array).

import { MatrixClient, MatrixError } from "matrix-gleam-sdk";

const client = await MatrixClient.login("https://matrix.org", "alice", "pw");
const room = await client.createRoom({ name: "Hi", preset: "private" });
const eventId = await client.sendText(room, "Hello from TypeScript!");
await client.sendReaction(room, eventId, "👋");

const page = await client.messages(room, { limit: 20 });
const batch = await client.sync({ timeoutMs: 0 });        // typed SyncResult

try {
  await client.login(/* ... */);
} catch (e) {
  if (e instanceof MatrixError) console.error(e.kind, e.status, e.message);
}

MatrixClient covers the everyday surface: sessions, sending (text / message / event / state / reactions / replies / edits / redactions), rooms (create / join / leave / invite / kick / ban / knock / name / topic / avatar), reading (sync, history, state, context, relations, search), account data, profiles, media (upload / download / file / image), devices, and a request() escape hatch. End-to-end encryption is exposed under the raw namespace (the Gleam-shaped API) — see ts/example.ts and the E2EE section below.

npm run build      # gleam build -> esbuild bundle -> tsc declarations -> dist/

The bundle inlines the compiled Gleam; the only external is the built-in node:crypto, so it runs on Node / Bun / Deno. (See the runtime note under End-to-end encryption about browsers.)

An echo bot

import gleam/dict
import gleam/javascript/promise
import gleam/list
import gleam/option.{None}
import matrix
import matrix/event
import matrix/sync

pub fn main() {
  use client <- promise.try_await(matrix.login(
    homeserver: "https://matrix.org",
    username: "echobot",
    password: "secret",
  ))

  // Skip everything that happened before we logged in.
  use first <- promise.try_await(sync.once(client, since: None, timeout_ms: 0))

  sync.stream(client, since: option.Some(first.next_batch), handler: fn(batch) {
    dict.each(batch.joined, fn(room_id, room) {
      list.each(room.timeline.events, fn(ev) {
        case event.text(ev), ev.sender == option.Some(client.user_id) {
          Ok(body), False -> {
            matrix.send_text(client, room_id, "you said: " <> body)
            Nil
          }
          _, _ -> Nil
        }
      })
    })
    sync.Continue
  })
}

The stream loop retries network failures with exponential backoff and waits out rate limits on its own; it only stops when the handler returns sync.Stop or the server rejects us outright (e.g. the token was revoked).

Building a room snapshot

use events <- promise.try_await(matrix.state(client, room_id))
let snapshot = room.new(room_id) |> room.apply_all(events)
// snapshot.name, snapshot.topic, room.joined_members(snapshot), ...

During a sync, apply each batch's state then timeline events to keep the snapshot current — room state in Matrix is just a left fold over events.

Calling unwrapped endpoints

The facade covers the common 90%. For anything else, matrix/api speaks raw:

api.expect(
  client,
  api.Get,
  "/_matrix/client/v3/rooms/" <> api.encode(room_id) <> "/joined_members",
  [],
  None,
  decode.at(["joined"], decode.dict(decode.string, decode.dynamic)),
)

End-to-end encryption

E2EE lives behind a single stateful object, the crypto.Vault, which holds a device's Olm account and all of its Olm and Megolm sessions. Everything the Vault produces and consumes is plain JSON, so it stays decoupled from the network; matrix/e2ee wires it to the homeserver.

The two algorithms are implemented directly from the Olm and Megolm specifications:

  • Olm (m.olm.v1.curve25519-aes-sha2) — the Signal double ratchet over Curve25519, used for the secure device-to-device channel that distributes room keys. Triple-DH handshake, root/chain key ratchets, pre-key and normal message wire formats, skipped-key handling for out-of-order delivery.
  • Megolm (m.megolm.v1.aes-sha2) — the four-part hash ratchet for efficient group messaging, with Ed25519-signed messages and the session sharing/export formats.

Both are byte-compatible with libolm, the reference C implementation. The interop/ directory contains a cross-implementation test that encrypts with libolm and decrypts with this SDK (and vice versa) for both ratchets:

npm install @matrix-org/olm
node interop/interop.mjs        # 16 checks vs libolm: Olm, Megolm, SAS, key backup

The flow

import matrix/crypto
import matrix/e2ee

// 1. Create a device vault and publish its keys.
let vault = crypto.new(user_id: client.user_id, device_id: client.device_id)
use vault <- promise.try_await(e2ee.publish_keys(client, vault, one_time_key_count: 50))

// 2. Discover the recipients' devices and share a room key with them.
use devices <- promise.try_await(e2ee.query_keys(client, ["@bob:example.org"]))
use #(vault, _session_id) <- promise.try_await(
  e2ee.share_room_key(client, vault, room_id, devices),
)

// 3. Send an encrypted message — Megolm under the hood.
use #(vault, _event_id) <- promise.try_await(e2ee.send_encrypted(
  client, vault, room_id, "m.room.message",
  primitives.JsonObject([
    #("msgtype", primitives.JsonString("m.text")),
    #("body", primitives.JsonString("encrypted hello")),
  ]),
))

Incoming events are handled with the pure Vault functions: crypto.decrypt_olm for to-device messages (auto-establishing inbound sessions from pre-key messages), crypto.add_inbound_session to store a received m.room_key, and crypto.decrypt_room_event for m.room.encrypted timeline events (with replay rejection built in). Persist the whole Vault between runs with crypto.to_json_string / crypto.from_json_string.

Scope

Implemented: device identity & one-time keys (signed), Olm 1:1 sessions, Megolm room sessions, key sharing over to-device, room message encrypt/decrypt, replay protection, and full session persistence — plus the trust infrastructure: cross-signing (crypto/cross_signing), SAS device verification (crypto/verification), secret storage / SSSS (crypto/secret_storage), and server-side key backup (crypto/key_backup). The SAS derivation and the key-backup encryption are byte-checked against libolm.

What's left to the application: driving the m.key.verification.* to-device state machine (the module supplies the crypto for each step), QR-code verification, and key requests for missing sessions. See COVERAGE.md for the full map.

Runtime note. E2EE uses the synchronous primitives in node:crypto, so it runs on Node, Bun, and Deno. The non-encrypted client (matrix, matrix/sync, …) only needs fetch and runs anywhere, browsers included.

Examples

Runnable examples live in src/examples/. They read secrets from a git-ignored .env file — copy .env.example to .env and fill it in.

gleam run -m examples/register     # register one account via a registration token
gleam run -m examples/showcase     # profile, room management, attachments, history
gleam run -m examples/group_chat   # the full E2EE story (below)
gleam run -m examples/trust        # search, cross-signing, key backup, SSSS, SAS

examples/trust exercises the newer features live against a homeserver: full-text search of a plaintext room; cross-signing bootstrap (over UIA) with a self-signature and chain verification; a complete key-backup round-trip (create a version, back up a Megolm session, fetch it back, restore it, and decrypt a message); a secret-storage round-trip through account data; and a locally-derived SAS emoji/decimal code. Its actual output:

2. Cross-signing ------------------------
   ✓ uploaded master/self-signing/user-signing keys
   ✓ self-signed this device
   ✓ trust chain verifies (master -> SSK): yes
3. Key backup ---------------------------
   ✓ created backup version 3077
   ✓ uploaded the session key to the backup
   ✓ restored the session and decrypted: "a backed-up message"

examples/showcase tours the everyday (non-encryption) surface against a real homeserver: it uploads an avatar and sets the profile, creates a room and sets its name/topic/avatar, invites and kicks a user, sends a file (m.file) and an image (m.image), downloads the image back and verifies the bytes, then sends several messages and paginates the room history.

examples/group_chat is an end-to-end demo against a real homeserver:

  1. Creates (or logs in to) three accounts — Alice, Bob, Carol.
  2. Alice creates a private room and invites Bob and Carol, who join.
  3. Alice sends a plaintext message; Bob reads it from his timeline.
  4. All three publish device keys; Alice enables encryption (m.room.encryption).
  5. Alice discovers the recipients' devices, establishes Olm sessions, shares a Megolm room key over to-device, and sends an end-to-end encrypted message.
  6. Bob and Carol sync, decrypt the room key (Olm) then the message (Megolm), and print the plaintext.

Its actual output:

3. Alice sends a PLAINTEXT message…
   Bob reads: "Hello room — this message is NOT encrypted."
...
6. Bob and Carol sync, decrypt the key (Olm) and the message (Megolm)…
   Bob decrypted: "Hello room — this message IS end-to-end encrypted!"
   Carol decrypted: "Hello room — this message IS end-to-end encrypted!"

Development

gleam test               # pure unit tests: ratchets, crypto, decoders, room folding
gleam run -m smoke       # live connectivity check against matrix.org
node interop/interop.mjs # cross-check Olm, Megolm, SAS, key backup against libolm

The suite is ~100 tests across thirteen files:

  • Primitives — known-answer tests against the published RFC/NIST vectors (SHA-256, HMAC RFC 4231, HKDF RFC 5869, PBKDF2-SHA-512), plus base64/base58 known values, AES-CTR self-inversion, and canonical-JSON edge cases.
  • Wire format — varint and tagged-field encoding, including multi-byte varints and truncation handling.
  • Events & rooms — decoding across message/state/ephemeral/media shapes, and folding state into a Room (last-write-wins, membership transitions).
  • Ratchets — Olm (handshake, bidirectional, out-of-order) and Megolm (including a reseed past the 2^8 boundary).
  • Persistence — Olm account/session and Megolm/Vault save-and-reload that still encrypt and decrypt after the round-trip.
  • Engine — the type-1 Olm path, Megolm replay across many messages, and error handling for unknown sessions / corrupt state.
  • Trust — cross-signing chain verification (incl. verifying another user), SAS symmetry/commitment/MAC, SSSS and recovery-key round-trips, the SSSS key check, and key-backup of a real Megolm session.

The interop script additionally checks the SAS codes/MAC and the key-backup encryption byte-for-byte against libolm.