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

@sovit.xyz/keytr

v0.8.0

Published

Passkey login for Nostr. Encrypt your nsec with a WebAuthn PRF passkey, publish to relays, decrypt on any device. Implements NIP-K1.

Readme

keytr

Passkey login for Nostr. Encrypt your nsec with a WebAuthn passkey, publish to relays, decrypt on any device. Implements NIP-K1.

How it works

Register a passkey, encrypt your nsec, publish the ciphertext to Nostr relays. On any device with the synced passkey, tap to decrypt — no npub input needed, no localStorage, no manual key copying.

keytr derives the encryption key from the authenticator's WebAuthn PRF extension — a deterministic 32-byte secret that never leaves the hardware. The raw 32-byte Nostr public key is stored in the passkey's user.id, which enables discoverable login: the browser returns the pubkey via userHandle when the user picks a passkey, so keytr can fetch and decrypt the matching event with no prior state.

Passkey PRF → HKDF-SHA256 → AES-256-GCM → kind:31777 (v=1) → relay

PRF is required — there is no fallback. An authenticator without the PRF extension (Firefox on Android, some older security keys, and password-manager extensions that don't expose PRF to keytr) cannot register: setup() throws PrfNotSupportedError. keytr ships no password or local-only fallback, so this is a deliberate compatibility trade-off — see Compatibility.

Legacy Key-in-Handle (KiH): earlier versions supported a KiH mode that stored a random encryption key in user.id. KiH registration was removed in v0.8.0 — the key was interceptable in JS memory. Existing KiH credentials (v=3 events) can still log in via discover()/loginWithKeytr() and should upgrade with migrateFromKih(). KiH decryption will be removed in a future release.

Cross-client login works via a federated gateway model — any domain can authorize a set of Nostr clients to share passkey access using WebAuthn Related Origin Requests. The two official gateways (keytr.org on Cloudflare, nostkey.org on Hostinger) trust each other bidirectionally, so a passkey registered under either rpId works on both sites and all authorized client origins.

Documentation

  • Architecture & System Design — detailed walkthrough of every layer: crypto, WebAuthn, Nostr integration, federated gateways, security model
  • Integration Guide — how to wire keytr into a Nostr client's auth flow, session restore, and credential management
  • Roadmap — current state and future direction
  • NIP-K1 Specification — the canonical protocol spec (PRF v=1; KiH v=3 retained decrypt-only, deprecated for new registrations)

Install

npm install @sovit.xyz/keytr

Quick start

Setup (new user)

import { setup, publishKeytrEvent } from '@sovit.xyz/keytr'
import { finalizeEvent } from 'nostr-tools/pure'

// Registers a PRF passkey. Throws PrfNotSupportedError if the authenticator
// lacks the PRF extension — there is no fallback.
const { credential, encryptedBlob, eventTemplate, nsecBytes, npub } = await setup({
  userName: 'alice',
  userDisplayName: 'Alice',
})

// Sign and publish
const signedEvent = finalizeEvent(eventTemplate, nsecBytes)
await publishKeytrEvent(signedEvent, ['wss://relay.damus.io'])

The setup() result has no mode field — PRF is the only registration mode.

Login (discoverable — no npub needed)

import { discover } from '@sovit.xyz/keytr'

// Browser shows passkeys, user picks one. keytr recovers the pubkey from the
// userHandle, fetches the event, and decrypts. On Safari a PRF login may show
// up to two prompts (discovery + a targeted PRF assertion); legacy KiH
// credentials are auto-detected and complete in one prompt.
const { nsecBytes, npub, pubkey } = await discover(
  ['wss://relay.damus.io'],
  { rpId: 'keytr.org' }
)

Login (known pubkey)

import { loginWithKeytr, fetchKeytrEvents } from '@sovit.xyz/keytr'

const events = await fetchKeytrEvents(pubkey, ['wss://relay.damus.io'])
const { nsecBytes, npub } = await loginWithKeytr(events)

loginWithKeytr() dispatches each event by its v tag — v=1 authenticates with PRF, v=3 with the legacy KiH path — so it can log in existing KiH credentials too.

Migration (legacy KiH → PRF)

Users with an old KiH passkey (a v=3 event) should upgrade to PRF:

import { migrateFromKih } from '@sovit.xyz/keytr'

const { npub, pubkey, newEvent, deletionPublished } = await migrateFromKih({
  relays: ['wss://relay.damus.io'],
  userName: 'alice',
  userDisplayName: 'Alice',
})

migrateFromKih() logs in with the old KiH passkey, re-encrypts the nsec under a new PRF passkey, publishes the new v=1 event, then publishes a NIP-09 kind:5 deletion (a tag 31777:<pubkey>:<old-d-tag>) to retire the old KiH event. The new event is published before anything is deleted; the deletion is best-effort (deletionPublished reports whether it landed).

The lower-level setupKeytr() and addBackupGateway() helpers are also available; both use the PRF strategy by default.

Module exports

keytr provides four entry points for tree-shaking and selective imports:

| Export | Path | Contents | |--------|------|----------| | Main | @sovit.xyz/keytr | High-level API + re-exports from all modules | | Crypto | @sovit.xyz/keytr/crypto | encryptNsec, decryptNsec, deriveKey, serializeBlob, deserializeBlob, buildAad | | WebAuthn | @sovit.xyz/keytr/webauthn | registerPasskey, authenticatePasskey, kihAuthenticatePasskey, discoverPasskey, checkPrfSupport, checkCapabilities, ensureBrowser, PRF helpers, KiH decrypt helpers (extractKey, detectMode), Signal API, backup flags | | Nostr | @sovit.xyz/keytr/nostr | Key utilities, event building/parsing, relay operations |

Compatibility

keytr requires discoverable credentials and the PRF extension. PRF is the only registration mode and there is no fallback — any browser or authenticator that can't provide PRF output to keytr simply cannot register (setup() throws PrfNotSupportedError). Rows below marked Not supported are the compatibility cost of dropping the old KiH fallback.

Browsers

| Browser | Min Version | Registration (PRF) | Discoverable Login | Notes | |---------|-------------|--------------------|--------------------|-------| | Chrome (Desktop) | 116+ | Yes | Yes | | | Chrome (Android) | 116+ | Yes | Yes | | | Edge | 116+ | Yes | Yes | Chromium-based | | Safari | 18+ | Yes | Yes | PRF login may show up to two prompts (discovery + targeted PRF assertion) | | Firefox | 122+ | Yes | Yes | | | Firefox (Android) | — | Not supported | — | No PRF extension; no fallback |

Authenticators

| Authenticator | Registration (PRF) | Notes | |---------------|--------------------|-------| | iCloud Keychain | Yes | macOS 15+ / iOS 18+ | | Google Password Manager | Yes | Android 14+ / Chrome 116+ | | Windows Hello | Yes | Windows 11 25H2+ (Feb 2026 update) | | YubiKey 5 (firmware 5.7+) | Yes | PRF via hmac-secret bridge | | YubiKey 5 (firmware < 5.7) | Not supported | No PRF; no fallback | | 1Password | Caveat | PRF supported in Chromium browsers, but the extension's WebAuthn interception can still block keytr's cross-origin gateway registration | | Bitwarden | Caveat | PRF since 2026.1.1 (Chromium), same cross-origin interception caveat | | Dashlane | Not supported | PRF still in beta; treat as unavailable | | Older security keys (no PRF) | Not supported | No PRF; no fallback |

Password-manager browser extensions (Bitwarden, 1Password, Dashlane) are the most common cause of registration failure. Even where they support PRF, they intercept navigator.credentials.* and frequently reject keytr's gateway rpId (Related Origin Requests) or don't expose PRF output — in which case registration is impossible with no fallback. See Architecture: Password Manager Extensions.

Federated gateways

Cross-client login via Related Origin Requests requires additional browser support:

| Browser | Related Origins | Min Version | |---------|-----------------|-------------| | Chrome | Yes | 128+ | | Edge | Yes | 128+ | | Safari | Yes | 18+ | | Firefox | No | Positive standards position (March 2026); no implementation timeline |

Capability detection

Use checkPrfSupport() to gate the passkey UI before calling setup() (which throws PrfNotSupportedError when PRF is unavailable), or checkCapabilities() for a comprehensive report including PRF, conditional mediation, Related Origins, and Signal API support. Because there is no fallback, checking PRF support up front lets you show a clear "your device can't use passkey login" message instead of surfacing a thrown error.

import { checkCapabilities } from '@sovit.xyz/keytr'

const caps = await checkCapabilities()
// caps.prf              — true/false/null (null = unknown, requires credential creation to confirm)
// caps.conditionalMediation — passkey autofill support
// caps.relatedOrigins   — cross-domain passkey use (federated gateways)
// caps.signalApi        — credential lifecycle management

Uses PublicKeyCredential.getClientCapabilities() (Chrome 132+) when available, falls back to feature detection.

Conditional UI (passkey autofill)

Pass mediation: 'conditional' to discover() or discoverPasskey() for passkey autofill instead of the modal picker. Requires <input autocomplete="webauthn"> in the DOM:

const { nsecBytes, pubkey } = await discover(relays, {
  mediation: 'conditional',  // passkey suggestions appear in the input field
})

Credential lifecycle (Signal API)

Tell authenticators to clean up revoked or stale passkeys (Chrome 132+). No-ops on unsupported browsers:

import { signalUnknownCredential, signalAllAcceptedCredentialIds } from '@sovit.xyz/keytr'

// User deleted their passkey — tell authenticators to remove it
await signalUnknownCredential('keytr.org', credentialId)

// Sync the full set of valid credentials for a user
await signalAllAcceptedCredentialIds('keytr.org', userId, [credId1, credId2])

Backup eligibility

After registration, KeytrCredential includes backup flags from authenticatorData:

const { credential } = await setup({ ... })
if (credential.backupEligible === false) {
  console.warn('This passkey is device-bound and will not sync across devices')
}

SSR safety

All WebAuthn functions throw WebAuthnError immediately in non-browser environments (Node.js, SSR). Use ensureBrowser() for explicit pre-checks in SSR frameworks:

import { ensureBrowser } from '@sovit.xyz/keytr'

try { ensureBrowser() } catch { /* render fallback UI */ }

Security properties

| Property | PRF (the only registration mode) | |----------|----------------------------------| | Hardware-bound key | PRF output requires the authenticator + user verification; the secret never leaves the hardware and is not stored in user.id | | Origin-bound | Different domains get different PRF output; the passkey is bound to its rpId | | AAD-bound | "keytr" \|\| 0x01 \|\| credentialId — binds ciphertext to the credential and version | | Version isolation | The AAD version byte prevents a legacy KiH (0x03) blob decrypting as PRF and vice-versa | | No server trust | The relay is a dumb store; encryption is end-to-end | | Memory hygiene | Key material and derived keys are zeroed via safeZero in finally blocks after use | | Interception hardening | All WebAuthn calls route through native references cached at import time (natives.ts), and buffer ops use cached prototype methods (builtins.ts), resisting monkey-patching / prototype-pollution attacks |

Honest limitation. PRF keeps the encryption key hardware-bound, but the decrypted nsec is still returned to your JavaScript so it can sign events. keytr zeros its own copies, but it cannot protect a key it has handed back: an attacker who has already compromised the app's runtime (malicious dependency, XSS, hostile extension) can read the nsec while it is in memory. PRF raises the bar over the removed KiH mode (the key is no longer sitting in user.id), but it is not a substitute for a hardware signer that never releases the private key.

The legacy KiH mode stored the encryption key as a random 256-bit value inside user.id, which was extractable in JS memory — that is why KiH registration was removed in v0.8.0. KiH decryption remains only so existing credentials can log in and migrate, and will be removed in a future release.

License

AGPL-3.0-or-later