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

@activescott/auth-provider-passkey

v0.1.0

Published

Passkey (WebAuthn) provider for @activescott/auth

Readme

@activescott/auth-provider-passkey

npm version License: MIT

Passkey (WebAuthn) provider for @activescott/auth. Users add a passkey while signed in (via email or SMS first), then sign in usernameless with Touch ID, Face ID, Windows Hello, Android, 1Password, iCloud Keychain, or a security key.

Server-side WebAuthn verification uses @simplewebauthn/server (WebCrypto-based). A zero-dependency browser client ships as the @activescott/auth-provider-passkey/browser subpath export.

Usage

Server wiring — no new storage interface; passkeys reuse the IdentityStore you already have:

import { Auth, InMemoryChallengeStore } from "@activescott/auth"
import { PasskeyProvider } from "@activescott/auth-provider-passkey"

const auth = new Auth({
  session: { secret: process.env.JWT_SECRET! /* ... */ },
  userStore,
  identityStore,
  challengeStore: new InMemoryChallengeStore(), // DB-backed in production
  providers: [
    // other providers such as email and/or SMS go here too — users add
    // a passkey while signed in, so another provider handles first sign-in
    new PasskeyProvider({
      rpName: "MyApp",
      challengeSecret: process.env.JWT_SECRET!,
    }),
  ],
})

Browser (all four endpoints are fetch/JSON — WebAuthn ceremonies run in page JavaScript, not form navigations):

import {
  startRegistration,
  startAuthentication,
} from "@activescott/auth-provider-passkey/browser"

// Add a passkey (user must be signed in):
const regOptions = await fetch("/auth/passkey/register-options", {
  method: "POST",
}).then((r) => r.json())
const registration = await startRegistration(regOptions)
await fetch("/auth/passkey/register-verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(registration),
})

// Sign in with a passkey:
const authOptions = await fetch("/auth/passkey/authenticate-options", {
  method: "POST",
}).then((r) => r.json())
const assertion = await startAuthentication(authOptions)
const result = await fetch("/auth/passkey/authenticate-verify", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(assertion),
})
if (result.ok) location.assign("/dashboard") // session cookie is set

For conditional UI (passkey autofill on the login form), add autocomplete="username webauthn" to your username/email input and start a conditional request on page load:

import {
  startAuthentication,
  isConditionalUIAvailable,
} from "@activescott/auth-provider-passkey/browser"

if (await isConditionalUIAvailable()) {
  const options = await fetch("/auth/passkey/authenticate-options", {
    method: "POST",
  }).then((r) => r.json())
  // Resolves when the user picks a passkey from the autofill suggestions
  const assertion = await startAuthentication(options, { conditional: true })
  // POST to /auth/passkey/authenticate-verify as above
}

Endpoints

| Endpoint | Auth required | Purpose | | ----------------------------------------- | ------------- | ------------------------------------------------------------------------------- | | POST /auth/passkey/register-options | session | Registration options for adding a passkey to the signed-in user | | POST /auth/passkey/register-verify | session | Verify the attestation, store the credential, link a passkey identity | | POST /auth/passkey/authenticate-options | none | Authentication options (empty allowCredentials → any discoverable credential) | | POST /auth/passkey/authenticate-verify | none | Verify the assertion and set the session cookie |

Registration model: add-passkey-while-signed-in. Users sign in with another provider (email, SMS) first, then add a passkey from a settings/dashboard page; afterwards they can sign in usernameless. Passkey-first signup is not supported.

Configuration

| Option | Default | Description | | --------------------- | -------------------------- | ----------------------------------------------------------------------- | | rpName | (required) | Relying party name shown in authenticator prompts | | rpID | request hostname | Relying party ID; set explicitly in production (e.g. "myapp.example") | | expectedOrigin | request origin | Expected WebAuthn origin (e.g. "https://myapp.example") | | challengeSecret | (required) | Signs the short-lived challenge cookie | | challengeExpiry | "5m" | Challenge lifetime | | challengeCookieName | "auth_passkey_challenge" | Challenge cookie name |

Storage: passkeys are identities

Each passkey is an ordinary identity row — {provider: "passkey", identifier: <base64url credential ID>} — so your existing IdentityStore is the only storage involved. The credential's verification state (public key, signature counter, transports, device type, ...) lives in the row's provider-owned Identity.metadata. Your store treats that metadata as an opaque JSON blob: persist it unmodified and return it exactly as stored — the provider validates it with a zod schema on every read and writes it back wholesale via IdentityStore.update after each sign-in (counter + last-used). A typical identities table:

CREATE TABLE identities (
  id          TEXT PRIMARY KEY,
  user_id     TEXT NOT NULL REFERENCES users (id),
  provider    TEXT NOT NULL,       -- 'email' | 'sms' | 'passkey' | ...
  identifier  TEXT NOT NULL,       -- email, E.164 phone, or WebAuthn credential ID
  metadata    JSONB NOT NULL DEFAULT '{}', -- provider-owned; opaque to the app
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  verified_at TIMESTAMPTZ,
  UNIQUE (provider, identifier)
);
CREATE INDEX identities_user_id ON identities (user_id);

Metadata may contain sensitive material — treat it like credential data (encryption at rest is a reasonable default). Integrity matters more than secrecy here: anyone who can write this column can register their own key, so guard writes accordingly.

To list a user's passkeys (for a settings page), filter their identities to provider === "passkey" and validate each row's metadata:

import { parsePasskeyCredentialMetadata } from "@activescott/auth-provider-passkey"

const passkeys = (await identityStore.findByUserId(user.id))
  .filter((identity) => identity.provider === "passkey")
  .flatMap((identity) => {
    const credential = parsePasskeyCredentialMetadata(identity.metadata)
    return credential ? [{ identity, credential }] : []
  })

Challenges

The options endpoints set an HttpOnly, SameSite=Lax cookie containing a signed JWT (challengeSecret, 5-minute expiry) that binds the ceremony to the browser, and record the challenge in the core challengeStore. The verify endpoints require both and consume the stored challenge on the first redemption attempt — success or not — so every challenge is strictly single-use.

Cross-platform notes

  • Synced passkeys (iCloud Keychain, Google Password Manager, 1Password) report deviceType: "multiDevice" and usually a signature counter of 0. A counter regression is logged as a warning but does not fail authentication — synced passkeys regress counters legitimately, so blocking would lock out real users.
  • rpID scoping: a passkey is bound to its relying party ID. localhost works for development; production passkeys must be created on the production domain. Subdomains of the rpID can use the credential; a different registrable domain cannot.
  • Authenticator choice is the user's: options are generated with residentKey: "preferred", userVerification: "preferred", and no authenticatorAttachment, so platform authenticators, password managers, and roaming security keys all work.
  • Algorithms: ES256 and RS256 are accepted, covering Apple, Google, Microsoft, and common security keys.