JSPM

@internet-privacy/marmot-ts

0.5.2-next.20260520185742
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 28
  • Score
    100M100P100Q76589F
  • License MIT

Marmot protocol implementation in TypeScript

Package Exports

  • @internet-privacy/marmot-ts
  • @internet-privacy/marmot-ts/client
  • @internet-privacy/marmot-ts/core
  • @internet-privacy/marmot-ts/extra
  • @internet-privacy/marmot-ts/mls
  • @internet-privacy/marmot-ts/package.json
  • @internet-privacy/marmot-ts/utils

Readme

marmot-ts

TypeScript implementation of the Marmot protocol — end-to-end encrypted group messaging on Nostr using MLS (Messaging Layer Security).

[!WARNING] This library is in Alpha and under heavy development. The API is subject to breaking changes without notice. It relies on ts-mls for MLS cryptographic guarantees. Do not use in production yet.

Features

  • 🔐 End-to-end encrypted group messaging using MLS (RFC 9420)
  • 🌐 Decentralized — groups operate across Nostr relays
  • 🔑 Key package lifecycle — publishing, rotation, deletion
  • 📦 Storage-agnostic — bring any GenericKeyValueStore backend (LocalForage, IndexedDB, in-memory, …)
  • 🔌 Network-agnostic — works with any Nostr client library
  • 📱 Cross-platform — browsers and Node.js (v20+)

Marmot Protocol Compliance

marmot-ts currently supports the following Marmot Improvement Proposals (MIPs):

MIP Description Status
MIP-00 Introduction and Basic Operations ✅ Supported
MIP-01 Network Transport & Relay Communication ✅ Supported
MIP-02 Identities and Keys ✅ Supported
MIP-03 Group State & Memberships ✅ Supported

Installation

npm install @internet-privacy/marmot-ts
# or
pnpm add @internet-privacy/marmot-ts

Concepts

A MarmotClient needs four things to operate:

  1. A signer (EventSigner) — signs Nostr events on behalf of the user.
  2. A network interface (NostrNetworkInterface) — publishes, requests, and subscribes to events on relays.
  3. A group state store — persists serialized MLS group state.
  4. A key package store — persists local key package material.

Both stores share a single interface: GenericKeyValueStore<T>.

Storage

interface GenericKeyValueStore<T> {
  getItem(key: string): Promise<T | null>;
  setItem(key: string, value: T): Promise<T>;
  removeItem(key: string): Promise<void>;
  clear(): Promise<void>;
  keys(): Promise<string[]>;
}

Any backend that matches this shape works. LocalForage instances satisfy it directly:

import localforage from "localforage";

const groupStateStore = localforage.createInstance({ name: "marmot-groups" });
const keyPackageStore = localforage.createInstance({ name: "marmot-keys" });

For tests or short-lived processes, the library ships an in-memory implementation:

import { InMemoryKeyValueStore } from "@internet-privacy/marmot-ts";

const groupStateStore = new InMemoryKeyValueStore();
const keyPackageStore = new InMemoryKeyValueStore();

Quick Start

Create the client

import { MarmotClient } from "@internet-privacy/marmot-ts";

const client = new MarmotClient({
  signer, // your EventSigner (e.g. from applesauce-core)
  network, // your NostrNetworkInterface implementation
  groupStateStore, // GenericKeyValueStore<SerializedClientState>
  keyPackageStore, // GenericKeyValueStore<StoredKeyPackage>
  clientId: "my-app-desktop", // stable d-tag for kind 30443 key packages
});

Publish a key package

Other users invite you by referencing a key package you've published to relays.

await client.keyPackages.create({
  relays: ["wss://relay.example.com"],
});

Create a group

const group = await client.groups.create("My Secret Group", {
  description: "A private discussion",
  relays: ["wss://relay.example.com"],
});

Send a message

await group.sendChatMessage("Hello, world!");

Invite a member

Look up their key package event on a relay, then invite by event:

const [keyPackageEvent] = await client.network.request(
  ["wss://relay.example.com"],
  [{ kinds: [30443], authors: [memberPubkey], limit: 1 }],
);

if (keyPackageEvent) {
  await group.inviteByKeyPackageEvent(keyPackageEvent);
}

Join a group from an invite

When you receive a kind 1059 gift wrap, decrypt it to a kind 444 rumor and pass it to joinGroupFromWelcome:

const { group } = await client.joinGroupFromWelcome({ welcomeRumor });

Receive messages

Subscribe to the group's relays for kind 445 events and feed them to group.ingest:

import { bytesToHex } from "@noble/hashes/utils.js";

const subscription = client.network.subscription(group.relays, [
  { kinds: [445], "#h": [bytesToHex(group.groupData.nostrGroupId)] },
]);

subscription.subscribe({
  next: async (event) => {
    for await (const result of group.ingest([event])) {
      if (result.kind === "applicationMessage") {
        console.log(result.message);
      }
    }
  },
});

Documentation

Full documentation is in docs/ and served via VitePress. Run pnpm docs:dev to browse locally.

  • Getting Started — first-run walkthrough
  • Architecture — component overview and Nostr/MLS mapping
  • Client ModuleMarmotClient, MarmotGroup, storage, network, UI integration
  • Core Module — protocol, credentials, key packages, groups, messages, welcome

Development

pnpm install    # Install dependencies
pnpm build      # Compile TypeScript
pnpm test       # Run tests (watch mode)
pnpm format     # Format code with Prettier
pnpm docs:dev   # Serve documentation locally
pnpm docs:build # Build documentation