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

@flarecart/passkeys

v1.0.0

Published

Passkey sign-in for an admin page — server flows over a store you choose (in-memory and Cloudflare D1 included), single-use challenges, provider names, session-cookie helpers, the browser calls, and a software passkey for tests.

Readme

@flarecart/passkeys

Passkey sign-in for an admin page: Face ID, Touch ID or a security key, and nothing else. Server flows over a store you choose (in-memory and Cloudflare D1 included), the browser calls, session-cookie helpers, and a software passkey for your tests.

Why

An admin page needs a way in that a stolen password, a leaked database or a phishing page can't open. Passkeys are that: a key pair the device keeps, bound to your site's address, used only after the device checks the person. But the WebAuthn ceremony has sharp edges every app relearns:

  • A challenge must work once. Keep it in memory and two instances disagree; keep it forever and a captured response can be replayed. Here every challenge is stored, expires after five minutes, and is used up by one atomic delete.
  • "Add a passkey" needs a way in for the first one. A one-time link, an invite, a setup token: whatever proves it, it must be used up only once the passkey verifies, or a failed attempt burns the link. verifyRegistration takes an authorise hook that runs exactly there.
  • The same device shouldn't be added twice, and nobody should type a username. Passkeys here are discoverable and listed in excludeCredentials.
  • People want to know which device a passkey is. The AAGUID the authenticator reports names it: iCloud Keychain, Google Password Manager, 1Password, a YubiKey.
  • The session after sign-in is where apps go wrong. The cookie helpers make the safe version the easy one: HttpOnly, Secure, SameSite=Strict, path-scoped, and an origin check for every change.
  • Testing needs a device. @flarecart/passkeys/testing has a software passkey that makes real P-256 signatures and real CBOR attestations, so your tests run the genuine checks.

WebAuthn itself is @simplewebauthn/server, the one dependency: well audited, and not something to reimplement.

Features

  • 🔑 Discoverable passkeys, user verification required: no usernames, no passwords, no codes.
  • ♻️ Single-use, expiring challenges, kept in your store and claimed atomically.
  • 🗄️ Bring your own storage: a five-method interface, with an in-memory store and a ready Cloudflare D1 store (plain prepared statements, schema included).
  • 🚪 An authorise hook between "verified" and "kept", for one-time enrolment links and invites.
  • 🏷️ Provider names from the AAGUID: iCloud Keychain, Google Password Manager, Windows Hello, 1Password, Bitwarden, YubiKey…
  • 👥 One admin or many: passkeys can belong to a user id, or to the site.
  • 🍪 Session-cookie helpers that make the safe defaults the only ones.
  • 🌐 Browser helpers: native JSON parsing where the browser has it, a hand conversion where it doesn't, and refusals in plain words.
  • 🧪 A software passkey for tests: real keys, real attestation, real signatures.

Install

npm install @flarecart/passkeys

Node 22+, Cloudflare Workers, Deno and Bun: anything with Web Crypto.

Usage

A single-admin page on Cloudflare Workers with D1: the first passkey comes from a one-time link, and every sign-in after that is a passkey.

Migration (once):

import { d1Schema } from '@flarecart/passkeys';
console.log(d1Schema()); // paste into migrations/0001_passkeys.sql

Server:

import { clearCookie, createPasskeys, d1Store, isSameOrigin, PasskeyError, readCookie, sessionCookie } from '@flarecart/passkeys';

const COOKIE = '__Secure-shop_admin';
const PATH = '/admin';
const SESSION_MS = 12 * 3_600_000;

export default {
  async fetch(request, env) {
    const passkeys = createPasskeys({ store: d1Store(env.DB), rpName: 'Wave Shop admin' });
    const url = new URL(request.url);
    const json = (data, init) => Response.json(data, init);

    // Every change must come from the admin page itself.
    if (request.method !== 'GET' && !isSameOrigin(request, url.origin)) return json({ error: 'Wrong origin' }, { status: 403 });

    try {
      switch (`${request.method} ${url.pathname}`) {
        // Add a passkey (the first with a one-time link; later ones signed in).
        case 'POST /admin/passkeys/options':
          return json(await passkeys.registrationOptions(url));
        case 'POST /admin/passkeys': {
          const { response, name, link } = await request.json();
          const passkey = await passkeys.verifyRegistration(url, response, {
            name,
            authorise: async () => {
              // Used up only now that the passkey has verified.
              if (!(await useEnrolLink(env, link))) throw new Error('That link has expired.');
            },
          });
          return json({ id: passkey.id }, { status: 201, headers: { 'Set-Cookie': sessionCookie(COOKIE, await startSession(env, passkey.id), { path: PATH, maxAgeMs: SESSION_MS }) } });
        }

        // Sign in.
        case 'POST /admin/session/options':
          return json(await passkeys.authenticationOptions(url));
        case 'POST /admin/session': {
          const { response } = await request.json();
          const passkey = await passkeys.verifyAuthentication(url, response);
          return json({ signedIn: true }, { headers: { 'Set-Cookie': sessionCookie(COOKIE, await startSession(env, passkey.id), { path: PATH, maxAgeMs: SESSION_MS }) } });
        }

        // Sign out.
        case 'DELETE /admin/session':
          await endSession(env, readCookie(request, COOKIE));
          return json({ signedIn: false }, { headers: { 'Set-Cookie': clearCookie(COOKIE, { path: PATH }) } });
      }
    } catch (error) {
      if (error instanceof PasskeyError) return json({ error: error.message, code: error.code }, { status: 401 });
      throw error;
    }
    return json({ error: 'Not found' }, { status: 404 });
  },
};

startSession, endSession and useEnrolLink are yours: a random token in a table, its hash stored, an expiry. The cookie only carries the token.

Browser:

import { createPasskey, describeError, passkeysSupported, usePasskey } from '@flarecart/passkeys/browser';

/** POST JSON to the admin API. */
const post = async (path, body = {}) => {
  const res = await fetch(path, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
  if (!res.ok) throw new Error((await res.json()).error);
  return res.json();
};

async function signIn() {
  try {
    const options = await post('/admin/session/options');
    await post('/admin/session', { response: await usePasskey(options) });
    location.reload();
  } catch (error) {
    show(describeError(error)); // "Cancelled, or the passkey prompt timed out."
  }
}

async function addPasskey(link) {
  const options = await post('/admin/passkeys/options');
  await post('/admin/passkeys', { response: await createPasskey(options), name: 'MacBook', link });
}

if (!passkeysSupported()) show('Open this page in a current browser.');

Tests:

import { createPasskeys, memoryStore } from '@flarecart/passkeys';
import { SoftPasskey } from '@flarecart/passkeys/testing';

const passkeys = createPasskeys({ store: memoryStore(), rpName: 'Test' });
const device = await SoftPasskey.create({ aaguid: 'fbfc3007-154e-4ecc-8c0b-6e020557d7bd' });

const url = 'https://shop.example/admin/';
await passkeys.verifyRegistration(url, await device.register(await passkeys.registrationOptions(url), 'https://shop.example'));
const signedIn = await passkeys.verifyAuthentication(url, await device.assert(await passkeys.authenticationOptions(url), 'https://shop.example'));
signedIn.provider; // "iCloud Keychain"

API

createPasskeys({ store, rpName, maxPasskeys?, challengeTtlMs?, now? })

| Method | What it does | |---|---| | registrationOptions(url, { userId?, userName?, userDisplayName? }) | Options for navigator.credentials.create(). Discoverable, user verification required, the existing passkeys excluded. The challenge is kept. | | verifyRegistration(url, response, { name?, userId?, authorise? }) | Checks the new passkey (origin, RP ID, challenge, user verification), runs authorise, keeps it. Returns the Passkey. | | authenticationOptions(url) | Options for navigator.credentials.get(). Names no passkeys: the browser offers the ones it has. | | verifyAuthentication(url, response) | Checks a sign-in, updates the counter and lastUsedAt, returns the Passkey. | | list(userId?), remove(id) | Straight to the store. |

url is the page's address (or the request's URL): its host is the RP ID and its origin is the only one accepted. Failures throw PasskeyError with a code (NOT_VERIFIED, UNKNOWN_PASSKEY, TOO_MANY_PASSKEYS, ALREADY_ADDED) and a message safe to show.

Stores

  • memoryStore(): for tests.
  • d1Store(db, { passkeysTable?, challengesTable? }) and d1Schema(options?): Cloudflare D1, or anything with D1's prepare().bind().run()/first()/all().
  • Your own: implement PasskeyStore (list, get, add, update, remove, keepChallenge, useChallenge, and optionally prune). useChallenge must claim the challenge atomically.

Cookies

sessionCookie(name, value, { path?, maxAgeMs }), clearCookie(name, { path? }), readCookie(request, name), isSameOrigin(request, origin).

Other

providerOf(aaguid) and PROVIDERS, relyingParty(url), toBase64Url / fromBase64Url.

@flarecart/passkeys/browser

passkeysSupported(), createPasskey(options), usePasskey(options), describeError(thrown).

@flarecart/passkeys/testing

SoftPasskey.create({ aaguid?, backedUp? }), then .register(options, origin, { userVerified? }) and .assert(options, origin, { rpId?, userVerified? }).

Security notes

  • A passkey works only on the host it was made on. shop.example and shop.workers.dev are different relying parties: add a passkey on each address you use.
  • Only public keys are stored. A leaked database holds nothing that signs in.
  • Keep the session short and narrow. A cookie a browser holds should open what the admin page does and no more; leave anything more powerful (API keys, refunds, settings) to stronger proof.
  • Check Origin on every change a cookie authorises (isSameOrigin), even with SameSite=Strict.
  • Recovery is your design decision. Losing every passkey shouldn't lock the owner out for good, but the way back in must be at least as hard to reach as the passkeys: a command run with the server's own credentials, not an email.
  • Prune expired challenges from a cron (store.prune(Date.now())) so the table stays small.
  • The software passkey is for tests only. It holds its private key in memory and signs whatever it's asked.

License

MIT