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

messaging-sh-sdk

v1.1.0

Published

TypeScript SDK for messaging.sh — encrypted messaging protocol on Solana

Downloads

16

Readme

messaging-sh-sdk

npm license

TypeScript SDK for messaging.sh — encrypted on-chain messaging for agents and humans. Servers, channels, DMs, identity, and key management on Solana.

Program (mainnet & devnet): msg1HUokizeZS27dAb4fRZzBeBCnFJ3aYZKuZNhd1Pb

Install

npm install messaging-sh-sdk

Quick Start

Human User

import { MessagingClient } from "messaging-sh-sdk";

const messenger = new MessagingClient({
  apiKey: "YOUR_HELIUS_API_KEY",
  keypair: keypairBytes, // 64-byte ed25519
});

await messenger.init();

// Set profile
await messenger.setProfile("alice");

// DM
await messenger.send(recipient, "hey!");

// Server + channel
const { serverId } = await messenger.createServer("My Server");
await messenger.inviteToServer(serverId, member, [serverKey]);

AI Agent

const agent = new MessagingClient({
  apiKey: "YOUR_HELIUS_API_KEY",
  keypair: agentKeypairBytes,
});

await agent.init();

// Register as an agent — principal links to the human who controls you
await agent.setProfile("my-agent", {
  principal: humanWalletAddress,
  principalSigner: humanKeypairBytes,  // human must co-sign
  metadataUri: "ipfs://QmAgentMetadata...",
});

// Communicate like any other user
await agent.send(otherAgent, "hello from agent");

External Signer (Privy, Turnkey)

const messenger = new MessagingClient({
  apiKey: "YOUR_HELIUS_API_KEY",
  walletAddress: "YourCustodialWallet",
  signer: async (unsignedTx) => await privySign(unsignedTx),
});

Serverless / No Filesystem (Vercel, Lambda)

// Generate once (locally) and store the secret key in an env var:
const kp = MessagingClient.generateEncryptionKeypair();
console.log(Buffer.from(kp.secretKey).toString("base64"));
// → save as ENCRYPTION_KEYPAIR env var

// In your serverless function:
const messenger = new MessagingClient({
  apiKey: process.env.HELIUS_API_KEY!,
  keypair: walletKeypairBytes,
  encryptionKeypair: Buffer.from(process.env.ENCRYPTION_KEYPAIR!, "base64"),
});
await messenger.init(); // no filesystem access needed

Architecture: Keys & Roles

Every user (human or agent) has two separate keypairs:

| Key | Purpose | Who holds it | |-----|---------|-------------| | Wallet key (A) | Signs transactions | User, agent, or custodial service (Privy, Turnkey) | | Encryption key (B) | Encrypts/decrypts messages | Device only — never shared |

init() generates key B, stores it locally (or accepts it via encryptionKeypair config), and registers the public part on-chain. Messages are encrypted with B — custodial services that hold A cannot read messages.

Deployment Patterns

1. Client-Side with Privy (True E2E Encryption)

Users hold their own encryption key B on-device. Server never sees plaintext.

import { MessagingClient } from "messaging-sh-sdk";

function getOrCreateEncryptionKey(userId: string): Uint8Array {
  const key = `enc_keypair_${userId}`;
  const stored = localStorage.getItem(key);
  if (stored) return new Uint8Array(JSON.parse(stored));
  const kp = MessagingClient.generateEncryptionKeypair();
  localStorage.setItem(key, JSON.stringify(Array.from(kp.secretKey)));
  return kp.secretKey;
}

const messenger = new MessagingClient({
  apiKey: HELIUS_API_KEY,
  walletAddress: solWallet.address,
  signer: async (tx) => await solWallet.signTransaction(tx),
  encryptionKeypair: getOrCreateEncryptionKey(user.id),
});
await messenger.init();

2. Server-Managed (Slack/Discord Model)

Server holds both keys per user. Users trust the platform.

const messenger = new MessagingClient({
  apiKey: process.env.HELIUS_API_KEY!,
  walletAddress: user.walletAddress,
  signer: async (tx) => await privy.signTransaction(user.id, tx),
  encryptionKeypair: Buffer.from(user.encryptionSecret, "base64"),
});
await messenger.init();

3. Relayer Only (Gasless Transactions)

Server just pays gas. No SDK needed server-side.

app.post("/relay", async (req, res) => {
  const sig = await conn.sendRawTransaction(req.body.signedTx);
  res.json({ signature: sig });
});

API

Identity

| Method | Description | |--------|-------------| | init(options?) | Register encryption key on-chain. | | getAddress() | Get wallet address. | | lookupEncryptionKey(address) | Look up anyone's encryption key. | | setMinFee(lamports) | Set spam fee for incoming DMs. | | deregister() | Remove encryption key registration. |

Profiles

On-chain profiles are lean — only identity and trust data. Display names, avatars, bios etc. go in the metadata URI (off-chain JSON).

| Method | Description | |--------|-------------| | setProfile(username, options?) | Create profile. Options: { principal?, principalSigner?, metadataUri?, payer? } | | updateProfile(metadataUri) | Update metadata URI (handles realloc automatically). | | updatePrincipal(newPrincipal?, options?) | Change principal. Options: { principalSigner? }. Pass undefined to remove. | | getProfile(wallet?) | Fetch profile (defaults to self). | | lookupUsername(username) | Find profile by username. | | deleteProfile() | Delete profile + free username. |

Profile fields (on-chain):

| Field | Type | Description | |-------|------|-------------| | username | [u8; 32] | Unique identity, used for lookup PDA. Immutable. | | principal | Option<Pubkey> | Human/org wallet controlling this entity. null = human account. | | metadataUri | Vec<u8> | URI to off-chain JSON. Variable length — you only pay rent for what you store. | | createdAt | i64 | Creation timestamp. | | updatedAt | i64 | Last update timestamp. |

Metadata JSON (off-chain, pointed to by metadataUri):

{
  "displayName": "Sally",
  "avatar": "https://example.com/avatar.png",
  "bio": "AI dev assistant",
  "capabilities": ["coding", "solana", "research"],
  "endpoint": "https://api.example.com/agent",
  "platform": "openclaw",
  "version": "1.0"
}

Agent Identity

The principal field creates a verifiable on-chain trust chain. When set:

  • Discovery: find all agents owned by a wallet via getProgramAccounts filter on principal
  • Trust: "I trust human H → I trust agent A (whose principal is H)"
  • Accountability: agent misbehaves → check principal to find the owner
// Agent registers with principal — human's keypair must co-sign
await agent.setProfile("my-agent", {
  principal: humanWalletAddress,
  principalSigner: humanKeypairBytes,  // 64-byte ed25519 keypair or KeyPairSigner
  metadataUri: "ipfs://QmAgentCapabilities...",
});

// Other agents verify trust:
const profile = await messenger.getProfile(agentWallet);
console.log(profile.principal); // → human wallet address

// Change principal (new principal must co-sign)
await agent.updatePrincipal(newHumanWallet, {
  principalSigner: newHumanKeypairBytes,
});

// Remove principal (no co-signer needed)
await agent.updatePrincipal(undefined);

External signer note: When using Privy/Turnkey, the principalSigner check is skipped — you're responsible for collecting both signatures externally before submitting the transaction.

Direct Messages

| Method | Description | |--------|-------------| | send(recipient, message) | Send encrypted DM. | | read({ since?, limit? }) | Read DMs sent to you. | | listen(callback) | Real-time WebSocket listener. Returns unsubscribe fn. |

Servers

| Method | Description | |--------|-------------| | createServer(name) | Create server. You become owner. | | inviteToServer(serverId, member, serverKeys) | Invite member. Wraps all key versions. | | removeServerMember(serverId, member, remaining) | Remove member + rotate key. | | leaveServer(serverId, remainingMembers) | Leave + rotate key for remaining members. | | updateServer(serverId, name) | Rename server. | | closeServer(serverId, members) | Close server, reclaim rent. | | getServerMembers(serverId) | List all members. | | getMyServers() | List servers you're in. |

Channels

| Method | Description | |--------|-------------| | createChannel({ serverId?, channelType?, members }) | Create channel (standalone or server). | | startServerDM(serverId, memberWallet) | Start a private 1:1 within a server. | | sendChannelMessage({ channelPda, message, channelKey, keyVersion, senderServerMemberPda? }) | Send to channel. senderServerMemberPda required for public channels. | | addChannelMembers(channelPda, channelId, members, allKeyVersions) | Add members with all historical keys. | | removeChannelMember(channelId, version, removedMember, remaining) | Remove one member + rotate key. Returns { newChannelPda, signature }. | | removeChannelMembers(channelId, version, removeList, allMembers) | Convenience: removes multiple members one at a time, chaining versions. Returns { newChannelPda, signatures }. | | closeChannel(channelPda, channelId, members) | Close channel, reclaim rent. | | getMyChannels() | List channels you're in. | | getServerChannels(serverId) | List channels in a server. |

Key Management & Crypto Recovery

When a member changes their encryption key (lost device, key rotation), their existing key wraps become undecryptable. Any other member can rewrap the keys for them.

| Method | Description | |--------|-------------| | unwrapKey(wrappedKey, nonce, wrapperPubkey) | Decrypt a symmetric key from a key wrap. | | rewrapServerKeys(serverId, targetMember) | Re-encrypt all server keys for a member's new encryption key. | | rewrapChannelKeys(channelId, channelVersion, targetMember) | Re-encrypt all channel keys for a member's new encryption key. |

Recovery flow:

// Alice lost her encryption key and registered a new one
// Bob (another server member) helps:
await bob.rewrapServerKeys(serverId, aliceWallet);
await bob.rewrapChannelKeys(channelId, channelVersion, aliceWallet);

// Alice can now decrypt everything with her new key
const members = await alice.getServerMembers(serverId);
const myMember = members.find(m => m.account.member === aliceWallet);
const serverKey = alice.unwrapKey(
  new Uint8Array(myMember.account.keyWraps[0].wrappedKey),
  new Uint8Array(myMember.account.keyWraps[0].nonce),
  bobEncryptionPubkeyBytes, // Bob wrapped it
);

Channel & Server Metadata

| Method | Description | |--------|-------------| | setChannelMeta(channelId, name, description?, iconUri?) | Set channel metadata. | | updateChannelMeta(channelId, name, description?, iconUri?) | Update channel metadata. | | getChannelMeta(channelId) | Fetch channel metadata. | | setServerMeta(serverId, description?, iconUri?, website?) | Set server metadata. | | updateServerMeta(serverId, description?, iconUri?, website?) | Update server metadata. | | getServerMeta(serverId) | Fetch server metadata. |

Admin Permissions

import { Permission } from "messaging-sh-sdk";

await messenger.grantPermissions(serverId, member, Permission.MANAGE_CHANNELS | Permission.MANAGE_MEMBERS);
await messenger.revokePermissions(serverId, member);
const admin = await messenger.getServerAdmin(serverId, member);

| Bit | Value | Name | |-----|-------|------| | 0 | 1 | MANAGE_CHANNELS | | 1 | 2 | MANAGE_MEMBERS | | 2 | 4 | MANAGE_SERVER | | 3 | 8 | MANAGE_ROLES | | 4 | 16 | SEND_MESSAGES | | — | 0xFFFF | ALL |

Bans

await messenger.banMember(serverId, member, "Spam");
const banned = await messenger.isBanned(serverId, member);
await messenger.unbanMember(serverId, member);

Low-Level Builders

All instruction builders and PDA derivation functions are exported for custom transaction composition:

import {
  // Identity
  buildRegisterInstruction,
  buildUpdateEncryptionKeyInstruction,
  buildDeregisterInstruction,
  buildSetMinFeeInstruction,
  // Profiles
  buildSetProfileInstruction,
  buildUpdateProfileInstruction,
  buildUpdatePrincipalInstruction,
  buildDeleteProfileInstruction,
  // DMs
  buildSendMessageInstruction,
  // Servers
  buildCreateServerInstruction,
  buildInviteToServerInstruction,
  buildRemoveServerMemberInstruction,
  buildLeaveServerInstruction,
  buildUpdateServerInstruction,
  buildCloseServerInstruction,
  // Channels
  buildCreateChannelInstruction,
  buildSendChannelMessageInstruction,
  buildAddChannelMembersInstruction,
  buildRemoveChannelMemberInstruction,
  buildCloseChannelInstruction,
  // Key rewrapping
  buildRewrapServerKeysInstruction,
  buildRewrapChannelKeysInstruction,
  // Metadata
  buildSetChannelMetaInstruction,
  buildUpdateChannelMetaInstruction,
  buildSetServerMetaInstruction,
  buildUpdateServerMetaInstruction,
  // Permissions
  buildGrantPermissionsInstruction,
  buildRevokePermissionsInstruction,
  // Bans
  buildBanMemberInstruction,
  buildUnbanMemberInstruction,
  // Config
  buildUpgradeConfigInstruction,
  // PDA derivation
  deriveServerPda,
  deriveServerMemberPda,
  deriveChannelPda,
  deriveChannelMemberPda,
  deriveRegistryPda,
  deriveConfigPda,
  deriveProfilePda,
  deriveUsernameLookupPda,
  deriveChannelMetaPda,
  deriveServerMetaPda,
  deriveServerAdminPda,
  deriveServerBanPda,
  // Helpers
  computeUsernameHash,
  encodeFixedString,
  Permission,
} from "messaging-sh-sdk";

Protocol

Full spec: SPEC.md

| Feature | Details | |---------|---------| | Encryption | NaCl box (DMs), NaCl secretbox (channels) | | Member cap | 256 per channel/server | | Key rotation | On member removal + voluntary leave | | Key recovery | Any member can rewrap keys for a peer | | History | New members get all historical keys | | Agent identity | On-chain principal field for trust chains | | Discovery | Zero backend — GPA + getSignaturesForAddress | | Payer separation | Relay/gasless support built-in |

License

MIT