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

nostr-cs

v0.0.5

Published

Embeddable customer-support SDK over Nostr — tickets, replies, DMs, CSAT, end-to-end encrypted via NIP-44 + NIP-17.

Readme

nostr-cs

[!WARNING] This project is in early development. Protocol details, APIs, and package layout can change without notice. Not ready for production use.

Customer support over Nostr. Reach a support agent by their pubkey, not a vendor account. End-to-end encrypted via NIP-44 + NIP-17; no SaaS ticket DB, no DNS, no vendor lock-in. Tickets, replies, status changes, internal notes, CSAT — all plain Nostr events on relays you choose.

   your customer app           public relay (ws)         your agent app
 ┌────────────────────┐                                ┌────────────────────┐
 │ ┌──── SDK ───────┐ │   NIP-44 body / NIP-17 wrap    │ ┌──── SDK ───────┐ │
 │ │ createTicket   │─┼─── 7700 ticket ──────────────▶ │ │ onTicket       │ │
 │ │ sendMessage    │─┼─── 1059 DM ──────────────────▶ │ │ replyTicket    │ │
 │ │ on* handlers   │◀┼─── 7702 reply / 7701 status ── │ │ updateStatus   │ │
 │ └────────────────┘ │                                │ └────────────────┘ │
 │ + your UI          │                                │ + your UI          │
 └────────────────────┘                                └────────────────────┘

  The SDK handles encryption, gift wrap, relay routing, subscription
  filters, and persistence. You write only the UI — PWA, React Native,
  Tauri, CLI, or embedded inside a product you already ship. Customer
  support becomes an embeddable feature, not a separate service.

Both sides are apps you build with the same SDK. There's no central service to deploy — every ticket interaction is a Nostr event on a relay you (or your counterpart) pick:

 ┌────────────┐
 │  web PWA   │   NIP-44 body / NIP-17 wrap     ┌────────┐
 │ mobile app │ ─── ticket / reply / DM ──────▶ │ public │
 │  desktop   │                                 │ relay  │
 │    CLI     │ ◀── status / reply / DM ─────── │  (wss) │
 └────────────┘                                 └────────┘
   identity = npub you control                  no DB, no auth provider,
   relays = your choice (multiple)              no agent platform service

The same intuition behind running a service as a Telegram bot — get a callable address without exposing your real backend — applied to customer support: every ticket is a signed event delivered by a relay, identity is a keypair, and "switching providers" is a relay URL change.

Why

 vendor SaaS stack:                       with nostr-cs:
 ──────────────────                       ──────────────

 customer ──HTTPS──▶ Zendesk    (vendor)  customer ──HTTPS──▶ CDN  (rented)
                                                              │
 agent    ──HTTPS──▶ Zendesk    (vendor)  customer ──NIP-44──▶ relay
                                                              │
 vendor owns:                                                 ▼
   · ticket DB                                            agent PWA
   · authentication                                       ─────────
   · audit trail                                           · npub identity
   · search index                                          · no inbound port
   · pricing model                                         · multi-relay
                                                           · BYO storage

Traditional CS couples ticket identity to a vendor account: you log into tenant.zendesk.com, agents are users in their RBAC, the thread lives in their database, and migration means CSV exports.

nostr-cs decouples the two:

  • Identity = Nostr pubkey (self-certifying, no vendor)
  • Storage = a Nostr relay you pick (swappable, multiple at once)
  • Threads = signed events you can fan out to your own relay

A customer needs only an agent's pubkey + one relay URL to open a ticket. Agents authenticate by signing events. If a relay disappears or starts charging, point the client at another — pubkey and old threads (replicated to other relays you trust) keep working.

Events use plaintext #p tags for routing — both pubkeys are visible to relays. Bodies are NIP-44 encrypted; NIP-17 DMs hide the sender. See docs/MESSAGE-ENVELOPE.md for the wire format and metadata exposure details.

What you write

A normal SDK call. Wrap/unwrap, NIP-44 body encryption, gift-wrap, and relay routing live behind the facade.

// customer side
const ticket = await client.createTicket({
  title: 'Login broken',
  body: 'gets stuck after OTP',
  agentPubkey: AGENT_PUBKEY,
  priority: 'high',
  category: 'technical',
})
client.onReply((r) => render(r))

// agent side
client.onTicket(async (t) => {
  await client.replyTicket({
    ticketId: t.id, threadRoot: t.eventId,
    body: 'Looking into it.',
    customerPubkey: t.customerPubkey,
  })
})
  • Domain code only. Handlers receive Ticket / Message / TicketReply domain objects. NIP-17 gift wrap, NIP-44 conversation keys, relay subscription filters — all behind ports.
  • Two channels, one API. Public ticket thread (kind 7702 reply, encrypted to customer) and private NIP-17 DM (kind 1059, sender hidden) on the same handler.
  • Reachable on any client implementing the kinds. Other clients speaking the same kinds interop without coordination.

Positioning

| | Zendesk / Intercom | Discord / Telegram bot | Email + SMTP | nostr-cs | |---|---|---|---|---| | Identity | vendor account | vendor account | mailbox + SPF/DKIM | Nostr pubkey | | Storage owner | vendor | vendor | mail server operator | relay you pick | | End-to-end encryption | no (vendor sees plaintext) | no | optional (PGP, rare) | yes (NIP-44 / NIP-17) | | Vendor lock-in | high (CSV export) | medium (export tools) | low (mbox/IMAP) | none | | Multi-agent | built in | manual | shared mailbox | roadmap | | Custom domain / branding | paid tier | bot username | yes | client-owned UI | | Attachment privacy | TLS to vendor only | TLS to vendor only | sender's mail server | client-side AES-256-GCM + Blossom | | Pricing | per-seat | bot platform fees | server cost | relay cost (typically free / sats) |

nostr-cs is not a Zendesk replacement for orgs that need RBAC, SLA dashboards, and integration marketplaces today. It's the "TLS + vendor account replaced by pubkey + relay" rebuild — useful when keeping the thread on infra you own matters more than features you'd buy.

Protocol at a glance

| kind | role | encryption | who emits | |---|---|---|---| | 7700 | ticket creation | NIP-44 body to agent | customer | | 7701 | status change | plaintext status tag | agent | | 7702 | public thread reply | NIP-44 body to customer | agent | | 7703 | internal note | NIP-44 body, per recipient | agent | | 7704 | CSAT response | NIP-44 body to agent (rating + comment private) | customer | | 14 / 13 / 1059 | NIP-17 rumor / seal / gift wrap (DMs) | gift-wrap, sender hidden | both | | 0 / 10002 / 10050 / 10166 | profile / NIP-65 / NIP-17 relay list / NIP-66 monitor | standard | both | | 30078 | NIP-78 app data (e.g. ephemeral keyring) | NIP-44 self | client-side state |

Wire envelope schema (text + encrypted attachments) is documented in docs/MESSAGE-ENVELOPE.md.

Architecture

Hexagonal. The domain core (CSClient facade + use cases + entities) depends only on port interfaces. Concrete Nostr libraries plug in at the composition root.

                    ports (contracts)
                          ▲
              ┌───────────┴───────────┐
              │   CSClient (facade)   │
              │   + 9 use cases       │
              │   + domain entities   │
              └───────────────────────┘
                          ▲
        ┌─────────────────┼──────────────────┐
        ▼                 ▼                  ▼
 ┌────────────┐   ┌─────────────┐   ┌────────────────┐
 │  inbound   │   │  outbound   │   │ infrastructure │
 │  ports     │   │  ports      │   │ adapters       │
 │ ─────────  │   │ ──────────  │   │ ─────────────  │
 │ Customer   │   │ NostrEvent  │   │ SimplePool *   │
 │ Agent      │   │ Crypto      │   │ Nip59Crypto    │
 │            │   │ KeyProvider │   │ PrivateKey *   │
 │            │   │ Profile     │   │ NIP07 / NIP46  │
 │            │   │ RelayIndex  │   │ InMemoryBus    │
 │            │   │ EventBus    │   │                │
 └────────────┘   └─────────────┘   └────────────────┘
                          ▼
                   Nostr relay (WebSocket)

Default adapter set is nostr-tools SimplePool for relay traffic and nip44.v2 + nip59 for crypto. The port layout makes NDK or other implementations a drop-in swap at the composition root.

Stack

  • Bun ≥ 1.1 (runtime, test, package manager)
  • TypeScript strict (noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax)
  • nostr-tools ≥ 2.23
  • SvelteKit + Tailwind (example PWA only)

Try it

Run the example PWA — no local relay required, uses public Nostr relays by default.

bun install
cd example/pwa
cp .env.example .env
bun run dev
# → http://localhost:5173

Open in two browser windows (or two devices):

  1. First window — Setup → "Generate New Key" → pick role: agent → connect. Copy the agent npub from Settings.
  2. Second window — Setup → "Generate New Key" → pick role: customer → connect. Open "New Ticket", paste the agent npub.

Replies / DMs / status changes / CSAT round-trip live over relays.

Usage

import { CSClient } from 'nostr-cs'

const client = new CSClient({
  key: { type: 'nsec', value: 'nsec1...' },           // or NIP-07 / NIP-46 signer
  relays: { bootstrap: ['wss://relay.damus.io'] },
  profile: { name: 'Alice', csRole: 'customer' },     // or 'agent'
})
await client.connect()

Advanced relay infrastructure

CSClient creates its own SimplePool and public relay discovery adapter by default. Host apps that already manage Nostr relay connections can inject those dependencies instead:

import { CSClient, type RelayIndexPort } from 'nostr-cs'

const relayIndex: RelayIndexPort = {
  fetchPublicRelays: async () => ['wss://relay.example'],
}

const client = new CSClient({
  key: { type: 'signer', value: keyProvider },
  relays: { bootstrap: ['wss://relay.example'] },
  infrastructure: {
    pool: sharedSimplePool,
    relayIndex,
  },
})

When a pool is injected, disconnect() stops SDK subscriptions but does not destroy the external pool. This lets apps share WebSocket connections and control which discovery sources are queried while preserving the default SDK behavior for simple integrations.

An external pool must be paired with an explicit relayIndex. This prevents a host app from accidentally sending public relay discovery queries to the SDK's default monitor relays through a shared pool. If you only need custom public relay discovery and do not need pool sharing, inject relayIndex without pool.

Customer

await client.createTicket({
  title, body, agentPubkey,
  priority: 'high', category: 'technical',
})
await client.sendMessage({                            // private NIP-17 DM
  ticketId, threadRoot, content,
  recipientPubkey: agentPubkey,
})
await client.submitCsat({
  ticketId, threadRoot, agentPubkey,
  rating: 5, comment: 'Fast resolution',
})

Agent

await client.replyTicket({                            // public thread reply
  ticketId, threadRoot, body,
  customerPubkey,
})
await client.updateStatus({
  ticketId, threadRoot,
  newStatus: 'resolved',
  customerPubkey,
})
await client.addInternalNote({                        // encrypted to other agents
  ticketId, threadRoot, body,
  otherAgentPubkeys: [...],
})

Subscriptions (both roles)

client.onTicket((t) => ...)
client.onReply((r) => ...)
client.onNote((n) => ...)
client.onStatusChange((u) => ...)
client.onMessage((m) => ...)
client.onCsat((c) => ...)

Persistence + cross-device sync

// Generic NIP-78 (kind 30078) — encrypted to self, syncs via relays.
await client.appDataSave('drafts', JSON.stringify({...}))
const drafts = await client.appDataLoad('drafts')

// Back-fill self-authored events the role's #p:[me] subscription misses
// (e.g. a customer's own kind-7700 tickets on a fresh device).
await client.pullOwnHistory()

Develop

bun install
bun run typecheck         # tsc --noEmit
bun test                  # unit tests (61)
bun run e2e:smoke         # round-trip against local docker relay
bun run e2e:full          # 6-step §17 scenario (customer + agent + peer agent)

The E2E harness spawns scsibug/nostr-rs-relay via Docker on localhost:17777 and tears it down on exit.

Environment configuration (PWA only):

| var | purpose | |---|---| | PUBLIC_BOOTSTRAP_RELAYS | comma-separated wss URLs for kind 0 / 10002 / 10050 lookup | | PUBLIC_WRITE_RELAYS | publish targets (default: bootstrap) | | PUBLIC_READ_RELAYS | subscription targets (default: bootstrap) | | PUBLIC_DM_RELAYS | NIP-17 DM relays (default: read) | | PUBLIC_BLOSSOM_SERVERS | Blossom servers for encrypted attachments |

Examples

  • example/pwa — dual-role customer + agent PWA. SvelteKit + Tailwind, Industrial Sage Material 3 tokens, encrypted Blossom attachments with NIP-78 ephemeral keyring, optimistic publish, localStorage cache + cross-device pull.
  • example/design_example — original UI mockups (HTML + screenshots) the PWA was built against.

Docs

  • docs/MESSAGE-ENVELOPE.md — wire envelope (v1) spec for the body field of 7700 / 7702 / 7703 and NIP-17 rumor content. Defines the encrypted-attachment schema.
  • cs-nostr-hexagonal-v7.md — original protocol + architecture spec the framework was built from.

Roadmap

  • Multi-agent: roster events, claim / handoff, derived current-assignee
  • Wire format flatten (envelope v2): drop the inner JSON-string layer
  • Decoy traffic / padding for #p metadata reduction
  • Public registry of agent pubkeys (NIP-89 / custom directory)

License

MIT.