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

@tetrac/p2p-sdk

v0.2.0

Published

Reusable, transport-agnostic peer-to-peer end-to-end encrypted messaging SDK for Next.js / React frontends. NaCl Box (X25519-XSalsa20-Poly1305) crypto with a pluggable transport interface and ready-made React hooks.

Readme

@tetrac/p2p-sdk

npm version license: MIT types: TypeScript

Transport-agnostic peer-to-peer end-to-end encrypted messaging for Next.js / React frontends.

NaCl Box (X25519-XSalsa20-Poly1305 — the same authenticated public-key crypto family as Signal), a small pluggable transport interface so you bring your own backend (Solana, WebSocket, REST, …), and ready-made React hooks.

v0.1.0 — audited baseline. This release ports the crypto from a security-audited implementation as-is. It is sound for confidentiality against eavesdroppers but does not yet provide forward secrecy or key rotation. Read Security & known limitations before shipping.


Install

npm install @tetrac/p2p-sdk
# react + react-dom are optional peers, only needed for @tetrac/p2p-sdk/react

How it works

 your wallet ──signMessage──▶ deriveEncryptionKeypair ──▶ X25519 keypair (in memory only)
                                                              │
 P2PChatClient ── encrypt/decrypt + key exchange ─────────────┤
                                                              ▼
 Transport (you implement) ── moves opaque strings ──▶ Solana / WebSocket / REST / …
  • The SDK owns all crypto and the wire format (PUBKEY: / ENC: / PLAIN:).
  • A Transport only moves opaque content strings — it never sees plaintext or keys.
  • chatId is just a string; you decide how to derive it (e.g. from two wallet addresses).

Quick start (core, framework-neutral)

import { deriveEncryptionKeypair, P2PChatClient, InMemoryTransport } from "@tetrac/p2p-sdk";

// 1. Derive the encryption keypair from a wallet signature (deterministic, no persistence).
const address = wallet.publicKey.toBase58();
const keypair = await deriveEncryptionKeypair(address, wallet.signMessage);

// 2. Build a client over your transport (InMemoryTransport shown; see below for a real one).
const transport = new InMemoryTransport(address);
const client = new P2PChatClient({ transport, identity: { address, keypair } });

// 3. Open the chat (publishes your public key), send, and read.
const chatId = "alice-bob";
await client.ensureChat(chatId, peerAddress);
await client.send(chatId, "gm 🔒");

const messages = await client.getMessages(chatId);
// each: { sender, decrypted, isMe, isEncrypted, isKeyExchange, isPlaintext, ... }

Quick start (React / Next.js)

"use client";
import { useEncryptionKeys, useE2EChat } from "@tetrac/p2p-sdk/react";

function Chat({ transport, chatId, wallet, peer }) {
  const { identity, ready } = useEncryptionKeys({
    address: wallet.publicKey?.toBase58(),
    signMessage: wallet.signMessage,
  });

  const { messages, send, peerKeyFound, loading } = useE2EChat({
    transport,
    identity,
    chatId,
    peer,
    pollIntervalMs: 30_000, // default
  });

  if (!ready) return <p>Unlock your wallet to start an encrypted chat.</p>;
  return (
    <div>
      {!peerKeyFound && <p>⚠️ Peer hasn't shared a key yet — messages send as plaintext.</p>}
      {messages.map((m) => (
        <p key={m.messageIndex}>{m.isMe ? "You" : "Them"}: {m.decrypted ?? "🔒 unable to decrypt"}</p>
      ))}
      <button onClick={() => send("hello")} disabled={loading}>Send</button>
    </div>
  );
}

The Transport interface

Implement this to plug in any backend:

interface Transport {
  sendMessage(args: { chatId: string; messageIndex: number; content: string }): Promise<{ id: string }>;
  getMessages(chatId: string): Promise<RawMessage[]>;            // ascending by messageIndex
  createChat?(args: { chatId: string; peer: string }): Promise<{ id: string }>; // optional
  getNextMessageIndex?(chatId: string): Promise<number>;        // optional; default max(idx)+1
}
  • InMemoryTransport ships with the package for tests, demos, and prototyping.
  • A reference Solana adapter (wrapping an Anchor program) is in examples/solana-transport.ts. It is documentation only and is not part of the published package — the SDK has zero Solana/Anchor dependencies.

API surface

Core (@tetrac/p2p-sdk):

| Export | Purpose | |---|---| | deriveEncryptionKeypair(address, signMessage, context?) | Wallet signature → deterministic X25519 keypair | | encryptMessage / decryptMessage | NaCl Box encrypt → ENC:… / decrypt (null on tamper) | | P2PChatClient | Key exchange + send + fetch/decrypt orchestration | | InMemoryTransport | In-memory Transport for tests/demos | | formatPubkeyMessage, parsePubkeyMessage, isEncryptedMessage, formatPlaintextMessage, … | Wire-format helpers | | toBase64 / fromBase64 | Universal base64 (browser + Node) | | Types: Transport, RawMessage, P2PIdentity, DecodedMessage, BoxKeyPair, SignMessage | |

React (@tetrac/p2p-sdk/react):

| Export | Purpose | |---|---| | useEncryptionKeys({ address, signMessage }) | Derive + hold the keypair / identity client-side | | useE2EChat({ transport, identity, chatId, … }) | Managed chat: auto key-publish, 30s polling, send |


Security & known limitations

The crypto is sound: NaCl Box gives X25519 key agreement, XSalsa20 encryption, and a Poly1305 MAC verified on every decrypt (tampering → null). Nonces are 24 random bytes. Secret keys are derived on demand and held only in memory. An eavesdropper with full transport access and both public keys cannot read messages (verified by test/eve-spy.test.ts).

This v0.1.0 baseline inherits these known weaknesses from the audit (PRD-p2p-audit.md), deferred to a hardening release:

| ID | Severity | Limitation | |---|---|---| | W-1 | P0 | No forward secrecy. One static keypair per wallet; a key leak decrypts all past/future messages. | | W-2 | P0 | No key rotation. Same keypair for the life of the wallet. | | W-3 | P1 | Plaintext fallback. Messages sent before the peer's key is known go out as readable PLAIN:. Disable with allowPlaintextFallback: false. | | W-4 | P1 | No memory zeroing. Secret keys are not wiped on unmount/lock. | | W-5 | P2 | SHA-256 KDF, not HKDF — no domain separation/salt. | | W-6 | P2 | Peer-key cache has no TTL (cleared only on new P2PChatClient). |

Roadmap (v0.2 hardening): per-chat ephemeral session keys for PFS (H-1), memory zeroing (H-2), HKDF (H-3), cache TTL (H-4), nonce counters / replay detection (H-5), per-message ratcheting (H-6).

If your threat model requires forward secrecy today, do not rely on this version.

Transport trust boundary (key-exchange / MITM)

Key exchange trusts your transport. getPeerKey adopts the most recent PUBKEY: from a sender other than you. If the transport does not guarantee that only the two intended participants can post (a 2-party on-chain PDA does; a permissive relay/REST backend may not), a third party could publish a PUBKEY: and be adopted as the peer — a man-in-the-middle.

Mitigation — pin the expected peer so only that sender's key is trusted:

new P2PChatClient({ transport, identity, peerAddress: peerWalletAddress });
// React: useE2EChat({ transport, identity, chatId, peer: peerWalletAddress })

Pinning is strongly recommended unless your transport already enforces participants. Verified by the pins the peer key to the expected sender test in test/client.test.ts.


Local development with a Next.js app

Consume the SDK from a sibling app before publishing:

// app/package.json
{ "dependencies": { "@tetrac/p2p-sdk": "file:../tetrac-p2p-sdk" } }

Build the SDK (npm run build, or npm run dev to watch), then npm install in the app.

Turbopack note (Next.js 16+): a file: dependency may be symlinked outside the app's filesystem root, surfacing as a misleading Module not found: Can't resolve '@tetrac/p2p-sdk/react' or Symlink … points out of the filesystem root. Fix by setting turbopack.root / outputFileTracingRoot to the common parent dir, or prefer a pnpm/npm workspace over file:.


Scripts

npm run build      # tsup → dist/ (ESM + CJS + d.ts)
npm run dev        # tsup --watch
npm run typecheck  # tsc --noEmit
npm test           # vitest run

License

MIT © Tetrac