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

ncc-05-js

v1.2.0

Published

Nostr Community Convention 05 - Identity-Bound Service Locator Resolution

Readme

ncc-05-js

Nostr Community Convention 05 (NCC-05) implementation for JavaScript/TypeScript.

This library provides a standard way to publish and resolve Identity-Bound Service Locators on the Nostr network. It allows Nostr identities to dynamically publish endpoints (IPs, domains, Tor .onion addresses) that can be resolved by others, effectively functioning as a decentralized, identity-based DNS.

Features

  • Identity-Bound: Records are signed by Nostr identities (npub), ensuring authenticity.
  • Privacy-Focused: Supports NIP-44 encryption (public, self-encrypted, targeted, and multi-recipient).
  • Dynamic: Updates propagate instantly across relays (Kind 30058 replaceable events).
  • Resilient: Supports NIP-65 gossip for decentralized discovery, finding the relays where the target user actually publishes.
  • Flexible: Works with any endpoint type (TCP, UDP, HTTP, Onion, etc.).
  • Efficient: Supports sharing a SimplePool instance for connection management.
  • Typed: Written in TypeScript with full type definitions.

Installation

npm install ncc-05-js

Quick Start

1. Publishing a Service Locator

import { NCC05Publisher, NCC05Payload } from 'ncc-05-js';

const publisher = new NCC05Publisher();
const relays = ['wss://relay.damus.io', 'wss://npub1base64userpubkey...'];
const mySecretKey = '...'; // Hex string or Uint8Array

const payload: NCC05Payload = {
    v: 1,
    ttl: 3600,
    updated_at: Math.floor(Date.now() / 1000),
    endpoints: [
        { type: 'https', uri: '192.168.1.42:443', priority: 1, family: 'ipv4' }
    ]
};

// Publish a public record
try {
    await publisher.publish(relays, mySecretKey, payload, { public: true });
    console.log('Service published!');
} catch (error) {
    console.error('Publishing failed:', error);
}

publisher.close(relays);

2. Resolving a Service Locator

import { NCC05Resolver } from 'ncc-05-js';

const resolver = new NCC05Resolver();
const targetPubkey = 'npub1...'; // or hex

try {
    const record = await resolver.resolve(targetPubkey);
    
    if (record) {
        console.log('Found endpoints:', record.endpoints);
    } else {
        console.log('No service record found.');
    }
} catch (error) {
    console.error('Resolution failed:', error);
}

resolver.close();

3. Resolving the Freshest Record

When a single identity publishes multiple Kind 30058 records with different d tags, resolveLatest treats the pubkey as a DNS name and automatically returns the freshest valid locator across all of them. It scans every published record, decrypts the ones you are allowed to read, and prefers the entry with the most recent updated_at.

const latest = await resolver.resolveLatest(targetPubkey);
if (latest) {
    console.log('Freshest endpoints:', latest.endpoints);
}

Detailed Usage

Configuration

Both NCC05Resolver and NCC05Publisher accept configuration objects.

Shared Connection Pool

For efficiency, especially in long-running applications or when using other Nostr libraries, you should share a single SimplePool instance.

import { SimplePool } from 'nostr-tools';
import { NCC05Resolver, NCC05Publisher } from 'ncc-05-js';

const pool = new SimplePool();

const resolver = new NCC05Resolver({ pool });
const publisher = new NCC05Publisher({ pool });

// ... usage ...

// You are responsible for closing the pool if you passed it in
// pool.close(usedRelays); 

Custom Relays & Timeouts

const resolver = new NCC05Resolver({
    // Relays to start looking at (Bootstrap relays)
    bootstrapRelays: ['wss://relay.custom.com'], 
    // Timeout for resolution in milliseconds (Default: 10000)
    timeout: 5000 
});

Asynchronous Signers

NCC05Publisher and NCC05Resolver accept SignerInput, which can be a raw secret (hex/Uint8Array) or a NostrSigner bridge (NIP-07, NIP-46, etc.). A signer must expose the identity's pubkey, sign events, and derive conversation keys for encrypted payloads.

const extensionBridge = window.nostr;

const signer: NostrSigner = {
    getPublicKey: async () => await extensionBridge.getPublicKey(),
    signEvent: async (event) => await extensionBridge.signEvent(event),
    getConversationKey: async (peer) => await extensionBridge.getSharedSecret(peer)
};

await publisher.publish(relays, signer, payload);

This keeps private keys inside the signer implementation while still allowing NCC-05 to encrypt, decrypt, and sign on-demand.

Transport-aware Resolution

NCC05Resolver accepts an optional urlTransformer hook that adjusts each endpoint before it is returned to you. This is ideal for automatically wrapping .onion targets in a locally configured bridge URL so browser-based clients are transport-aware.

const resolver = new NCC05Resolver({
    bootstrapRelays: ['wss://relay.damus.io'],
    urlTransformer: endpoint => {
        if (endpoint.family === 'onion' || endpoint.url.includes('.onion')) {
            return { 
                ...endpoint, 
                url: `https://local-tor-proxy/?target=${encodeURIComponent(endpoint.url)}` 
            };
        }
        return endpoint;
    }
});

Publishing Records

The NCC05Publisher supports different privacy levels using NIP-44 encryption.

Public Records (Unencrypted)

Readable by anyone.

await publisher.publish(relays, secretKey, payload, { 
    identifier: 'my-service', // 'd' tag
    public: true 
});

Private Records (Self-Encrypted)

Only readable by you (the publisher). Useful for personal device syncing or private configuration.

await publisher.publish(relays, secretKey, payload, { 
    identifier: 'my-device' 
    // public: false is default
    // recipient defaults to self if omitted
});

Targeted Records

Readable only by a specific recipient.

await publisher.publish(relays, secretKey, payload, { 
    identifier: 'for-alice',
    recipientPubkey: 'alice_hex_pubkey' 
});

Wrapped Records (Multi-Recipient)

Readable by a group of users. Uses a "wrapping" pattern where the payload is encrypted with a random session key, and that session key is encrypted individually for each recipient.

const recipients = ['hex_pubkey_1', 'hex_pubkey_2', 'hex_pubkey_3'];

await publisher.publishWrapped(
    relays, 
    secretKey, 
    recipients, 
    payload, 
    'team-service'
);

Resolving Records

The NCC05Resolver finds the latest valid record for a given user and identifier.

const payload = await resolver.resolve(
    targetPubkey, // npub or hex
    mySecretKey,  // Required if the record is encrypted for you (can be null/undefined if public)
    'my-service', // The 'd' tag identifier (default: 'addr')
    { 
        gossip: true, // Enable NIP-65 relay discovery (Highly Recommended)
        strict: false // If true, returns null for expired records instead of just logging a warning
    }
);

Note that resolve now only returns a payload when the identity's latest event still advertises the requested d tag; otherwise it returns null, enforcing the identity's newest action as the single source of truth. If you only care about the freshest record regardless of metadata, call resolveLatest.

Note on Keys: All methods accept SignerInput, meaning a hex string, Uint8Array, or an asynchronous NostrSigner implementation.

API Reference

NCC05Payload

The core data structure representing the service locator.

interface NCC05Payload {
    v: number;                 // Version (always 1)
    ttl: number;               // Time-to-live (seconds)
    updated_at: number;        // Unix timestamp
    endpoints: NCC05Endpoint[];
    caps?: string[];           // Optional capabilities (e.g. ['upload', 'stream'])
    notes?: string;            // Optional human-readable notes
}

interface NCC05Endpoint {
    type: string;     // e.g., 'tcp', 'http', 'ipfs', 'hyper'
    uri: string;      // e.g., '10.0.0.1:80', '[2001:db8::1]:443', 'onion_address:80'
    priority: number; // Lower number = higher priority
    family: string;   // 'ipv4', 'ipv6', 'onion', 'unknown'
}

NCC05Resolver

  • constructor(options?)
    • bootstrapRelays: string[] (Default: ['wss://relay.damus.io', 'wss://npub1...'])
    • timeout: number (ms) (Default: 10000)
    • pool: SimplePool (optional)
  • resolve(targetPubkey, secretKey?, identifier?, options?): Promise<NCC05Payload | null>
    • gossip: boolean - Fetch target's relay list (NIP-65) to find where they publish.
    • strict: boolean - Enforce TTL expiration strictly.
  • close(): Closes connections (only if pool was created internally).

NCC05Publisher

  • constructor(options?)
    • pool: SimplePool (optional)
    • timeout: number (ms) (Default: 5000)
  • publish(relays, secretKey, payload, options?): Promise<Event>
    • identifier: string (Default: 'addr')
    • public: boolean (Default: false)
    • recipientPubkey: string (Default: self)
    • privateLocator: boolean (Default: false) - Adds ["private", "true"] tag.
  • publishWrapped(relays, secretKey, recipients, payload, options?): Promise<Event>
    • options: { identifier?: string, privateLocator?: boolean } or string (identifier)
  • close(relays): Closes connections to specific relays (only if pool was created internally).

Error Handling

Errors are typed for granular handling:

  • NCC05TimeoutError: Relay operations took too long.
  • NCC05RelayError: Failed to publish or query relays.
  • NCC05DecryptionError: Bad key or invalid ciphertext.
  • NCC05ArgumentError: Invalid inputs (e.g. malformed keys).

Utilities

  • TAG_PRIVATE: Constant string 'private'.
  • isPrivateLocator(event: Event): boolean: Helper to check if an event has the ["private", "true"] tag.

Protocol Details

This library implements NCC-05, which uses Nostr Kind 30058 (Parametrized Replaceable Event) to store service locators.

It leverages:

  • NIP-01: Basic protocol flow.
  • NIP-19: bech32-encoded entities (npub, nsec).
  • NIP-44: Encryption (XChaCha20-Poly1305).
  • NIP-65: Relay discovery (Gossip).

License

CC0-1.0