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

@vouchflow/web

v0.2.1

Published

Vouchflow Web SDK — passkey-backed device verification and high-assurance payload signing for browsers.

Readme

@vouchflow/web

npm bundle size types license

Passkey-backed device verification and high-assurance payload signing for browsers. Mirrors the Vouchflow iOS and Android SDKs and adds web-specific extensions for arbitrary payload signing — a primitive the mobile SDKs do not expose.

  • Zero runtime dependencies for the core
  • TypeScript-first; full types ship in the package
  • 6.8 KB gzipped UMD, 9 KB ESM
  • React entry point at @vouchflow/web/react (opt-in)
  • Compatible with strict CSP — no eval, no inline scripts

Install

npm install @vouchflow/web

Or via UMD from jsDelivr — Vouchflow attaches to window:

<script src="https://cdn.jsdelivr.net/npm/@vouchflow/web/dist/umd/vouchflow.min.js"></script>

Quick start

import { Vouchflow } from '@vouchflow/web'

Vouchflow.configure({
  apiKey: 'vsk_sandbox_…',
  environment: 'sandbox',
  rpId: 'app.example.com',  // must match current origin's registrable domain
  rpName: 'Example',
})

const result = await Vouchflow.shared.verify({
  context: 'login',
  userHandle: 'user_abc',
  minConfidence: 'medium',
})

// result.verified      — true
// result.confidence    — 'high' | 'medium' | 'low'
// result.deviceToken   — pass to your server for reputation queries
// result.sessionId     — matches webhook session_id

Enrollment is automatic on first verify(). Call enroll({ userHandle, forceNew: true }) only when you need to add a backup credential.

High-assurance payload signing

signPayload() produces a Vouchflow-attested JWS over any JSON-serializable payload — useful for mandate signing, approval signing, or any flow where you need a server-verifiable signed envelope.

import { Vouchflow, type SignResult } from '@vouchflow/web'

const signed: SignResult = await Vouchflow.shared.signPayload({
  context: 'mandate_signing',
  payload: { v: 1, id: 'mand_abc', scope: 'send', amount: 500 },
  userHandle: 'user_abc',
  minConfidence: 'high',  // default for signPayload
})

await fetch('/api/mandates', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({
    mandate: signed.payload,      // canonicalized JSON the user signed
    assertion: signed.assertion,  // Vouchflow-signed JWS
  }),
})

Server-side verification (Node.js)

Verify the JWS against Vouchflow's published JWKS — no WebAuthn knowledge required.

import { jwtVerify, createRemoteJWKSet } from 'jose'
import crypto from 'node:crypto'

const JWKS = createRemoteJWKSet(
  new URL('https://api.vouchflow.dev/v1/.well-known/jwks.json'),
)

export async function verifyVouchflowAssertion(
  assertion: string,
  canonicalizedPayload: string,
  customerId: string,
) {
  const { payload } = await jwtVerify(assertion, JWKS, {
    issuer: 'https://vouchflow.dev',
    audience: customerId,
  })

  const expected = crypto
    .createHash('sha256')
    .update(canonicalizedPayload, 'utf8')
    .digest('hex')

  if (payload.payload_sha256 !== expected) {
    throw new Error('Payload hash mismatch — client sent different bytes than were signed')
  }

  return {
    confidence: payload.confidence as 'high' | 'medium' | 'low',
    deviceToken: payload.device_token as string,
    signingDeviceId: payload.signing_device_id as string,
    sessionId: payload.session_id as string,
  }
}

React

VouchflowProvider configures the SDK once on mount. useVerify and useSign are thin wrappers around the core that expose loading state and the last result.

import { VouchflowProvider, useVerify, useSign } from '@vouchflow/web/react'

export function App() {
  return (
    <VouchflowProvider config={{
      apiKey: 'vsk_sandbox_…',
      environment: 'sandbox',
      rpId: 'app.example.com',
      rpName: 'Example',
    }}>
      <SignInButton />
    </VouchflowProvider>
  )
}

function SignInButton() {
  const { verify, isVerifying } = useVerify()
  return (
    <button
      disabled={isVerifying}
      onClick={() => verify({ context: 'login', userHandle: 'user_abc' })}
    >
      {isVerifying ? 'Authenticating…' : 'Sign in with passkey'}
    </button>
  )
}

Email fallback

When WebAuthn is unavailable, cancelled, or fails, request an email OTP. The session ID flows through the thrown error.

import { Vouchflow, VouchflowError } from '@vouchflow/web'

try {
  await Vouchflow.shared.verify({ context: 'signup', userHandle: 'user_abc' })
} catch (err) {
  if (
    err instanceof VouchflowError &&
    (err.code === 'biometric_cancelled' ||
     err.code === 'biometric_failed' ||
     err.code === 'webauthn_unavailable')
  ) {
    const fb = await Vouchflow.shared.requestFallback({
      sessionId: err.sessionId!,
      email: '[email protected]',
      reason: 'biometric_failed',
    })
    const code = await promptUserForCode()
    const completed = await Vouchflow.shared.completeFallback({
      sessionId: fb.fallbackSessionId,
      code,
    })
    // completed.confidence === 'low'   ← always 'low' for email fallback
  }
}

Email OTP fallback always returns confidence: 'low'. It proves the user controls an inbox, not that a hardware-backed device is present.

Capability detection

const support = await Vouchflow.shared.checkSupport()

if (!support.webauthn) showEmailOTPOnly()
else if (!support.platformAuthenticator) showSecurityKeyAndEmailOptions()
else showPasskeyButton()

support.webauthn, support.platformAuthenticator, support.userVerifyingAuthenticator, support.conditionalUI, support.attestation ('available' | 'unsupported'), support.recommendedFallback ('email' | 'none').

Error handling

All errors are instances of VouchflowError with a discriminated code field. Switch on err.code, not on subclass.

| Code | Recommended action | | --- | --- | | invalid_config | Fix at init (most often an rpId mismatch with the current origin) | | not_configured | Call Vouchflow.configure() before Vouchflow.shared | | not_in_browser | Don't call verify()/signPayload() from Node / SSR | | webauthn_unavailable | Offer email fallback | | platform_authenticator_unavailable | Offer security-key or email fallback | | biometric_cancelled | Offer retry; err.sessionId is set | | biometric_failed | Offer fallback using err.sessionId | | concurrent_ceremony | Another tab is mid-verify; WebAuthn is exclusive per origin | | enrollment_failed | Usually transient — retry | | invalid_signature | Clear state and re-enroll | | challenge_expired | Retry; SDK normally fires within ms | | challenge_already_used | Atomic guard tripped — retry from scratch | | device_not_found | Local state stale — forget() and re-enroll | | minimum_confidence_unmet | Block or degrade; err.actualConfidence is set | | rate_limit_exceeded | Back off and retry | | unauthorized | Wrong API-key prefix or scope | | invalid_otp | Re-prompt up to the lockout | | fallback_locked | Surface a generic auth failure | | network_error | err.retryable === true — back off | | aborted | Caller asked for this via AbortSignal | | unknown_error | Log err.cause |

Browser support

| Browser | Minimum | Notes | | --- | --- | --- | | Chrome | 109+ | Full WebAuthn + conditional UI | | Edge | 109+ | Chromium engine — same support | | Safari | 16.4+ | iOS 16.4+ and macOS 13.3+ for full passkey support | | Firefox | 119+ | WebAuthn mature; conditional UI lags Chromium |

WebAuthn refuses to run over http:// outside localhost. Make sure staging and preview environments have valid TLS.

In Node.js / SSR, verify() and signPayload() throw not_in_browser. The package is import-safe in SSR — only the active call sites need a real browser.

Bundle size

| Build | Size (gzipped) | | --- | --- | | UMD (dist/umd/vouchflow.min.js) | 6.8 KB | | ESM (dist/index.js) | 9 KB (tree-shakeable) | | React entry (dist/react/index.js) | +1.4 KB |

CI fails if either core bundle exceeds the configured budget.

Spec

The full specification — including architectural rationale, IndexedDB schema, error mapping rules, and the WebAuthn ceremony details — lives at docs/spec.md. Customer-facing docs are at vouchflow.dev/docs/web-sdk.

Contributing

Issues and PRs welcome at github.com/vouchflow/web-sdk. Unit tests use Vitest with fake-indexeddb; integration tests use Playwright's virtual authenticator API. See test/ for examples.

License

Apache-2.0.