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

signet-login

v0.14.2

Published

Signet Access — drop-in auth and signer-access SDK for Nostr-aware websites. Signet QR/redirect, NIP-07, NIP-46, Amber, and nsec fallback in one API.

Readme

Signet Access

GitHub Sponsors

Published as signet-login.

Signet Access is a drop-in auth and signer-access SDK for Nostr-aware websites. One picker, one session shape, multiple ways to prove identity and, when available, keep a live signer:

  • Local Signet on this device, against hosted or local-dev Signet
  • Remote Signet by cross-device QR, so a phone or second machine can approve
  • Browser extension via NIP-07 (bark, Alby, nos2x, Flamingo, ...)
  • Connect a Nostr signer via app-initiated NIP-46 / NostrConnect
  • Paste or scan bunker URI for Heartwood, nsecBunker, Amber, or compatible signers
  • Sign in with Amber via Android NIP-55
  • Paste private key as an in-memory, advanced fallback only

Returns a unified SignetSigner plus a signed kind-21236 auth proof your server can verify before granting privileges.

Install

npm install signet-login

Or drop it in via CDN:

<script src="https://cdn.signet.forgesworn.dev/signet-login.iife.js"></script>

The IIFE bundle additively extends window.Signet — it coexists with signet-verify on the same page in either load order.

Quick start

<button id="login">Sign in</button>

<script src="https://cdn.signet.forgesworn.dev/signet-login.iife.js"></script>
<script>
  document.getElementById('login').addEventListener('click', async () => {
    const session = await Signet.login({ appName: 'My Game' });
    if (!session) return;  // user cancelled

    // Sign a Nostr event with the user's chosen signer:
    const signed = await session.signer.signEvent({
      kind: 30762,
      content: '',
      tags: [
        ['game', 'my-game'],
        ['score', '12350'],
        ['p', session.pubkey],
      ],
    });
    console.log('signed:', signed);
  });
</script>

API

Signet.login(options)

Show the picker, return a SignetSession on success or null on cancel/timeout.

interface LoginOptions {
  appName: string;                              // shown in modal
  challenge?: string;                           // 64 hex; auto if omitted
  preferredMethod?: LoginPickerMethod;          // skip the picker
  methods?: LoginPickerMethod[];                // picker methods, in order
  advancedMethods?: LoginPickerMethod[];        // grouped behind Advanced; [] = flat list
  relayUrl?: string;                            // default wss://relay.damus.io
  relayUrls?: string[];                         // repeated relay= params for NostrConnect
  nostrConnectPerms?: string[];                 // default sign_event + NIP-44
  onNostrConnectStatus?: NostrConnectStatusHandler;
  theme?: 'light' | 'dark' | 'auto';            // default 'auto'
  timeout?: number;                             // default 120_000ms; clamped to [5k, 600k]
  signetAppOrigin?: string;                     // default https://mysignet.app
  redirectCallback?: string;                    // for same-device redirect / Amber return
  mode?: 'relay' | 'redirect';                  // Signet delivery mode
  storage?: SignetStorage;                      // default localStorage
  persist?: boolean;                            // default true
}

interface SignetStorage {
  getItem(key: string): string | null | Promise<string | null>;
  setItem(key: string, value: string): void | Promise<void>;
  removeItem(key: string): void | Promise<void>;
}

type LoginPickerMethod =
  | 'nip07'
  | 'local-signet'  // same-device Signet, relay delivery
  | 'remote-signet' // cross-device Signet QR
  | 'redirect'      // legacy alias for local-signet
  | 'qr'            // legacy alias for remote-signet
  | 'bunker'        // paste bunker://
  | 'nostrconnect'  // show nostrconnect:// QR
  | 'amber'         // Android NIP-55
  | 'nsec';         // in-memory private key fallback

interface SignetSession {
  pubkey: string;                  // hex
  method: 'nip07' | 'redirect' | 'bunker' | 'nsec' | 'amber';
  signer: SignetSigner;
  authEvent: SignetAuthEvent;      // signed kind-21236 challenge proof
  expiresAt?: number;
  displayName?: string;
}

By default, the picker shows ordinary user-facing methods first and groups bunker, nostrconnect, and nsec behind Advanced. Control the surface per app:

await Signet.login({
  appName: 'My Game',
  methods: ['local-signet', 'remote-signet', 'nip07'],
});

await Signet.login({
  appName: 'My Local Dev Game',
  preferredMethod: 'local-signet',
  signetAppOrigin: 'http://localhost:5174',
  relayUrl: 'ws://localhost:7777',
});

await Signet.login({
  appName: 'Power User Tool',
  methods: ['nip07', 'bunker', 'nostrconnect', 'nsec'],
  advancedMethods: [], // flat picker
  relayUrls: ['wss://relay.nsec.app', 'wss://relay.damus.io'],
});

signetAppOrigin defaults to https://mysignet.app. If your integration should launch Signet Lite specifically, pass signetAppOrigin: 'https://lite.mysignet.app'.

In mode: 'redirect', the current tab navigates to Signet and the session is consumed later by Signet.handleRedirectCallback(). If the user taps Back before approving and the browser restores the original page, login() resolves null so your UI can leave its loading state.

redirect and qr remain supported picker aliases for existing apps, but new integrations should use local-signet and remote-signet.

When camera APIs are available, the bunker URI screen can scan bunker:// QR codes directly. Paste remains the fallback.

Headless/custom UI

Use the exported signer constructors and proof helpers when your app owns the UI:

import {
  createBunkerSigner,
  createLoginAuthEvent,
  createSessionFromSigner,
  createLocalSignerFromNsec,
} from 'signet-login';

const signer = await createBunkerSigner({
  uri: bunkerUri,
  timeoutMs: 30_000,
});

const session = await createSessionFromSigner(signer, {
  appName: 'My App',
  challenge: challengeFromServer,
  origin: 'https://my-app.example',
});

await fetch('/api/login', {
  method: 'POST',
  body: JSON.stringify({ authEvent: session.authEvent }),
});

Headless exports include hasNip07, createNip07Signer, createBunkerSigner, createBunkerSignerFromNostrConnect, buildNostrConnectUri, buildBunkerUriFromNostrConnectUri, isBunkerUri, isNostrConnectUri, isSupportedPairingUri, createLocalSignerFromNsec, createLoginAuthEvent, createSessionFromSigner, and generateSecretKey. The IIFE bundle attaches the same helpers to window.Signet.

NostrConnect and bunker roles

NIP-46 has two URI directions:

| URI | Producer | Consumer | Use | |---|---|---|---| | nostrconnect://... | The app / Signet Access client | Signer app scans or opens it | First pairing, where the app advertises its client pubkey, relays, requested permissions, and one-time secret | | bunker://... | The signer / bunker | App connects to it | Reconnect, paste/scan bunker flows, native clients, and persisted sessions |

createBunkerSignerFromNostrConnect() waits for the signer response and then stores the equivalent bunker:// reconnect URI internally, preserving the relay list and secret. This matters for apps such as Canary, Pallasite, and Axenstax: the user can pair once with NostrConnect, then restoreSession() can reconnect with the same stable client key instead of showing a fresh pairing request.

Signet Access is the app-side session broker. Identity creation, recovery, and derived personas belong in Signet, Heartwood, and nsec-tree; app integrations should consume the returned pubkey and capability flags instead of deriving identities inside the login SDK.

NostrConnect status diagnostics

Use onNostrConnectStatus when your app needs a reliable progress panel, telemetry, or support diagnostics for app-initiated NostrConnect. Do not scrape the built-in modal text.

import type { NostrConnectStatus } from 'signet-login';
import { login, restoreSession } from 'signet-login';

function renderNostrConnectStatus(status: NostrConnectStatus) {
  const relay = status.relay ?? status.relays[0] ?? 'the relay';
  const method = status.method ? ` ${status.method}` : '';

  switch (status.type) {
    case 'uri-created':
      return 'Pairing code ready. Scan it with your signer.';
    case 'relay-connecting':
      return `Connecting to ${relay}...`;
    case 'relay-connected':
      return `Connected to ${relay}. Waiting for signer approval...`;
    case 'signer-seen':
      return 'Signer responded. Waiting for approval payload...';
    case 'request-sent':
      return `Sent${method} request. Waiting for signer response...`;
    case 'response-received':
      return `Signer approved${method}.`;
    case 'timeout':
      return 'Signer did not respond before the timeout.';
    case 'error':
      return status.message ?? 'NostrConnect failed.';
  }
}

const session = await login({
  appName: 'My App',
  preferredMethod: 'nostrconnect',
  relayUrls: ['wss://relay.primal.net', 'wss://relay.damus.io'],
  timeout: 120_000,
  onNostrConnectStatus(status) {
    console.log(status);
    statusText.textContent = renderNostrConnectStatus(status);
  },
});

// The same callback works for persisted bunker reconnects.
await restoreSession({
  onNostrConnectStatus(status) {
    reconnectText.textContent = renderNostrConnectStatus(status);
  },
});

Status events are best-effort diagnostics. Handler errors are ignored so a broken dashboard cannot break login.

| Event | Meaning | Useful fields | |---|---|---| | uri-created | The app created a nostrconnect:// URI for the QR/link. | uri, clientPubkey, relays, timeoutMs | | relay-connecting | The SDK is opening relay/subscription state. | relays, clientPubkey, signerPubkey | | relay-connected | A relay connection succeeded. | relay, relays | | signer-seen | An encrypted response event arrived from a signer pubkey. | signerPubkey, clientPubkey | | request-sent | A NIP-46 request was published, such as connect, sign_event, or nip44_encrypt. | method, requestId, signerPubkey | | response-received | Pairing or a NIP-46 request received a response. | phase, method, requestId | | timeout | Pairing or a request hit its timeout. | phase, method, requestId, timeoutMs, message | | error | Relay, subscription, publish, response, or abort failure. | phase, relay, method, message, error |

For support triage: no relay-connected usually means relay/network trouble; signer-seen without response-received usually means the signer saw the request but did not complete approval; request-sent without a response usually means the saved bunker session exists but the signer device is unavailable or rejecting that method.

Custom storage

By default, Signet Access stores session state in localStorage under signet:login.*. Pass storage when you need encrypted, async, IndexedDB, server-backed, or test storage:

const encryptedStorage = {
  async getItem(key) {
    const value = localStorage.getItem(key);
    return value ? await decrypt(value) : null;
  },
  async setItem(key, value) {
    localStorage.setItem(key, await encrypt(value));
  },
  async removeItem(key) {
    localStorage.removeItem(key);
  },
};

const session = await Signet.login({
  appName: 'My Game',
  storage: encryptedStorage,
});

await Signet.restoreSession({ storage: encryptedStorage });
await Signet.handleRedirectCallback({ storage: encryptedStorage });
await Signet.logout(session, { storage: encryptedStorage });

Use the same storage adapter for login, restoreSession, handleRedirectCallback, and logout.

For the same-tab redirect callback, current Signet deployments return the signed event timestamp and the SDK verifies the returned auth proof before accepting it. Older deployments may omit that timestamp — without it the SDK cannot rebuild the signed event and cannot verify the signature, so the returned pubkey would be unverified. handleRedirectCallback rejects those callbacks by default (reason: 't-required'). Only opt into accepting them if you control the signet-app deployment and knowingly accept an unverified pubkey:

await Signet.handleRedirectCallback({
  storage: encryptedStorage,
  allowLegacyRedirectWithoutTimestamp: true, // ⚠️ disables signature verification — see warning above
});

Breaking change (v0.14.0): prior versions accepted timestamp-less redirect callbacks by default. This is now secure-by-default — such callbacks are rejected unless you explicitly opt in as shown above.

This adapter is deliberately not called "Stash". @forgesworn/stash is the separate encrypted cloud-save vault for app data; Signet Access storage is local session/reconnect state needed before a signer is available.

Signet.restoreSession(opts?)

Restore a session from configured storage. For bunker sessions this attempts to reconnect to the stored bunker. Returns null if no session is stored, the session is malformed, or reconnection fails.

const session = await Signet.restoreSession();
if (session?.signer.capabilities.canSignEvents) {
  // we have ongoing signing capability
}

Signet.logout(currentSession?, opts?)

Clear stored session and close the active signer.

By default, logout keeps the persistent NIP-46 client key so a previously approved bunker can recognize this browser on the next login. Pass clearPersistentClientKey: true when the user explicitly wants to break that pairing:

await Signet.logout(session, { clearPersistentClientKey: true });

Signet.handleCallback(opts?)

Run on your callback page when using the same-device redirect flow. Parses URL params and posts them to window.opener (if popup-opened), then closes the popup.

For popup callbacks, pass targetOrigin so callback params are posted only to the expected opener:

Signet.handleCallback({
  targetOrigin: 'https://my-game.example',
});

Signers and capabilities

All session signers implement SignetSigner:

interface SignetSigner {
  readonly pubkey: string;
  readonly method: 'nip07' | 'redirect' | 'bunker' | 'nsec' | 'amber';
  readonly capabilities: { canSignEvents: boolean; hasNip44: boolean };
  signEvent(template: EventTemplate): Promise<NostrEvent>;
  nip44?: { encrypt, decrypt };
  close(): Promise<void>;
}

| Signer | canSignEvents | Source | |---|---|---| | Nip07Signer | true | window.nostr (any NIP-07 extension) | | BunkerSignerImpl | true | nostr-tools BunkerSigner over NIP-46 relay | | LocalSigner | true | In-memory nsec fallback; never persisted | | EphemeralSigner | false | Auth-only Signet redirect / QR / Amber callback |

EphemeralSigner exists because some redirect-style flows return a signed challenge but no ongoing signing channel. Use signer.capabilities.canSignEvents to gate UI:

if (session.signer.capabilities.canSignEvents) {
  enableLeaderboardPublish();
} else {
  promptUserToInstallExtensionOrPasteBunkerURI();
}

When Signet or a signer app returns a bunker:// handoff, the SDK upgrades the auth-only proof into a live BunkerSignerImpl if the handoff connects and matches the authenticated pubkey.

Amber / NIP-55 is currently auth-only in Signet Access. It can prove identity on Android, but it does not leave the web app with a persistent signer or NIP-44 channel. Signing-required consumers should reject Amber/auth-only sessions unless a future Amber flow returns a verified live signer handoff. See docs/amber-policy.md.

Server-side verification

The client sends session.authEvent to your server. Verify it before granting any privileges or paying out sats:

import { verifyLogin } from 'signet-login/verify';

const result = verifyLogin(authEvent, {
  expectedChallenge: theChallengeYouIssued,
  expectedOrigin: 'https://my-game.example',
  expectedAppName: 'My Game',          // optional
  maxAgeSeconds: 300,                  // default 300
});

if (result.valid) {
  // result.pubkey is the authenticated user
} else {
  // result.error: 'invalid-signature' | 'challenge-mismatch' | 'too-old' | …
}

The verifier checks: schnorr signature, canonical event ID, kind=21236, challenge tag match, origin tag match, optional app tag match, freshness window (5-min default + 60s skew tolerance).

Storage

By default, session data is stored in localStorage under signet:login.*:

| Key | Purpose | |---|---| | signet:login.pubkey | Authenticated pubkey | | signet:login.method | nip07 / redirect / bunker / amber | | signet:login.authEvent | Serialised kind-21236 auth event | | signet:login.bunkerUri | Bunker URI for reconnect (bunker only) | | signet:login.bunkerClientSk | Session client secret key hex (bunker only) | | signet:login.clientSk | Persistent NIP-46 client secret key for bunker auto-approval | | signet:login.expiresAt | Optional expiry | | signet:login.displayName | Optional persona handle |

Storage namespace is signet:login.* so it doesn't collide with signet:verify.*. Signet.logout() clears session keys without touching other Signet SDKs; it clears signet:login.clientSk only when clearPersistentClientKey: true is set. Because bunker reconnect data can authorize future signing requests, apps should pair this SDK with a strict CSP and use a custom encrypted storage adapter when their threat model includes XSS on the app origin.

Coexistence with signet-verify

Both SDKs attach to window.Signet additively — load order doesn't matter:

<script src=".../signet-verify.iife.js"></script>
<script src=".../signet-login.iife.js"></script>

<script>
  // age verification
  const ageResult = await Signet.verifyAge('18+');
  // login
  const session = await Signet.login({ appName: 'My App' });
</script>

Each SDK manages its own slice of window.Signet and localStorage namespaces.

Bundle size

The ESM entry is approx 5.9 KB gzipped before bundling dependencies. The standalone IIFE is approx 114.7 KB gzipped because it includes NIP-46, Signet QR/relay support, and camera QR decoding. A future split-bundle could lazy-load advanced signer paths for smaller first-load pages.

Browser support

ES2020 baseline. Tested on modern Chrome / Firefox / Safari. Requires crypto.subtle, WebSocket, and the native <dialog> element. Session persistence defaults to localStorage, but apps can provide a custom storage adapter.

Development

npm install
npm run build       # dist/signet-login.js (ESM) + dist/signet-login.iife.js (browser)
npm run typecheck
npm test            # vitest in jsdom

Examples in examples/:

  • basic.html — full demo with login / sign / logout / restore
  • headless.html — custom UI demo using signer constructors and proof helpers
  • nostrconnect-status.html — NostrConnect pairing with a diagnostic event log
  • callback.html — redirect-back receiver page

Build the IIFE bundle first, then serve the repo root with any static server and open an example page.

Out of scope

| Excluded | Where it lives | |---|---| | Age verification | signet-verify | | Per-game persona derivation | Heartwood RPC (reserved scope) | | Sign-time policy clauses | Reserved (G34 NLnet Jun) | | Generating bunker URIs | Heartwood / bark | | Lightning, payments | Out of scope |

License

MIT

Related

  • signet — protocol, specs, docs
  • signet-protocol — npm primitives
  • signet-verify — age verification + cross-device auth primitives
  • bark — NIP-07 browser extension that signs via NIP-46 to Heartwood
  • Heartwood — self-hosted signing appliance