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

@altimist/did-web-client

v0.7.2

Published

Server-side + browser helpers for verifying Altimist users (did:web) and team Verifiable Credentials, tier-2 read helpers for inline identity-status rendering, the SE-signed JWT bridge for MCP, and the F-016 release-vcs client for scoped VC release. F-020

Downloads

779

Readme

@altimist/did-web-client

Drop this library into any app — yours or third-party — to authenticate Altimist users without redirecting them anywhere or running a central session. Verification happens locally; the user signs a challenge with their device, you check the signature against their public DID document, done.

npm install @altimist/did-web-client

What this is

  • A verifier library. Your app uses it to check that someone holds a particular Altimist DID, holds a particular team credential, or hasn't had their credentials revoked.
  • Server + browser, ESM, no runtime dependencies on altimist-id. Server-side functions for verifying signatures and credentials; a small browser-side helper for the WebAuthn ceremony.
  • The protocol implementation of F-010. Counterpart to @altimist/did-publisher (the publisher-side library used by the altimist-com-router Cloudflare Worker that hosts DIDs at *.altimist.com).

What this is not

  • Not an OAuth provider, OIDC client, or session library. No redirect flow. No central session store. No bearer tokens issued by an Altimist server. You generate challenges, the user signs them locally, you verify locally. Period.
  • Not a place to send users to "log in." Users authenticate against your app, not against altimist.id. altimist.id is where users went once, to create their identity — like signing up for a Gmail account. Subsequent sign-ins to your app happen between your app and the user's device, mediated by this library.
  • Not a profile or marketing surface. Identity profiles live at <handle>.altimist.com, owned by the corporate website. This library doesn't render anything.

Trust model

This library is designed so altimist.id is never on the request path between your app and your users. The diagram is short:

your app  ──── WebAuthn challenge ────►  user's browser  ────►  user's Secure Enclave (signs)
   ▲                                         │                          │
   │                                         ▼                          │
   │  ◄──── signed assertion ───────────  ────────────────────────────  │
   │
   └──── fetch did.json ──────────────►  patrick.altimist.com (Cloudflare-cached, public)

altimist-id wrote the user's did.json to patrick.altimist.com once (when the user enrolled their first device) and updates it when the user adds/revokes a device. After that, your app reads it the same way any verifier in the world does — over HTTPS, with no token, no rate limit, no API key.

Implications:

  • Your app's auth latency is dominated by Cloudflare cache. Sub-50ms typical.
  • altimist-id can be down without breaking auth on your app.
  • altimist-id can't see who's signing in to your app — no telemetry pipe.
  • The only "shared secret" between your app and altimist is the user's published public key in their DID doc. That's it.

Prerequisites

Before this library is useful for a given user, the user needs to already have an Altimist DID. They get one by signing up at altimist.id (the operator's onboarding surface), where they pick a handle and enrol a device using their browser's passkey support. After that, did:web:<handle>.altimist.com/.well-known/did.json exists and is publicly resolvable, and your app can authenticate them.

You don't need to know about altimist.id beyond pointing your users at it. If a user lands in your sign-in flow without an Altimist DID yet, send them to https://altimist.id/signup.

Quick start: add Altimist sign-in to a Next.js app

Two server routes plus a browser-side button. Total: ~50 lines of glue.

1. Server: issue the challenge

// app/api/auth/altimist/challenge/route.ts
import { issueChallenge } from "@altimist/did-web-client";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { handle } = await req.json();
  const options = await issueChallenge({
    handle,
    rpID: "yourapp.com",          // your app's domain
  });

  // Persist options.challenge alongside the user's session — you'll
  // re-check it on /verify. iron-session, cookies, or a short-lived
  // server-side cache all work.
  await persistChallenge(handle, options.challenge);

  return NextResponse.json(options);
}

2. Server: verify the assertion

// app/api/auth/altimist/verify/route.ts
import { verifyChallenge } from "@altimist/did-web-client";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const { handle, response } = await req.json();
  const expectedChallenge = await readPersistedChallenge(handle);

  const result = await verifyChallenge({
    handle,
    expectedChallenge,
    expectedOrigin: "https://yourapp.com",
    expectedRPID: "yourapp.com",
    response,                       // AuthenticationResponseJSON from browser
  });

  if (!result.ok) {
    return NextResponse.json({ error: result.reason }, { status: 401 });
  }

  // result.kid = the device key id that signed this challenge.
  // Issue your own session cookie / JWT here.
  return NextResponse.json({ handle, kid: result.kid });
}

3. Browser: drive the WebAuthn ceremony

// app/components/sign-in-button.tsx
"use client";
import { startAuthentication } from "@simplewebauthn/browser";

export function SignInWithAltimist({ handle }: { handle: string }) {
  async function handleClick() {
    const optionsRes = await fetch("/api/auth/altimist/challenge", {
      method: "POST",
      body: JSON.stringify({ handle }),
    });
    const options = await optionsRes.json();

    const response = await startAuthentication({ optionsJSON: options });

    const verifyRes = await fetch("/api/auth/altimist/verify", {
      method: "POST",
      body: JSON.stringify({ handle, response }),
    });
    if (verifyRes.ok) window.location.href = "/dashboard";
  }
  return <button onClick={handleClick}>Sign in with Altimist</button>;
}

That's the whole sign-in flow. The user clicks, their device prompts for biometric (Touch ID, Windows Hello, 1Password), and your app gets their authenticated handle.

You install @simplewebauthn/browser separately (npm install @simplewebauthn/browser); this library doesn't bundle it.

Authorisation: checking team membership

Sign-in proves "this is patrick." Authorisation answers "what is patrick allowed to do?" — answered by checking a Verifiable Credential issued by an Altimist team-hub.

import { fetchTeamIssuer, verifyMembershipVC, isRevoked } from "@altimist/did-web-client";

const { jwk } = await fetchTeamIssuer("altimist");        // public key
const result = await verifyMembershipVC({
  vcJwt: presentedVC,
  issuerPublicJwk: jwk,
  subjectDid: `did:web:${handle}.altimist.com`,
});
if (!result.ok) return forbid("invalid VC");

if (await isRevoked(result.vcHash)) return forbid("revoked");

if (!result.scopes.includes("capital.trader")) return forbid("missing scope");

The VC is presented to your app by the user (typically in a header or query param after sign-in). Your app verifies the signature locally — no network call to altimist-id, just fetchTeamIssuer to get the public key from altimist.com/.well-known/team-issuers/altimist.json, which is heavily cached.

MCP / agent-to-server bridge (v0.2+)

For machine-to-machine calls where the user's authenticated agent needs to authenticate to a downstream server (e.g. an MCP server) without a browser in the loop:

Mint (browser, after the user is signed in):

import { mintBridgeJwt } from "@altimist/did-web-client/browser";

const token = await mintBridgeJwt({
  handle: "patrick",
  kid: "<cred id from did.json>",
  rpID: "yourapp.com",
});
// Use as: Authorization: Bearer <token>

Verify (server-side, on the receiving end of the bridge):

import { verifyBridgeJwt } from "@altimist/did-web-client";

const result = await verifyBridgeJwt({
  token: bearerToken,
  expectedOrigin: "https://yourapp.com",
  expectedRPID: "yourapp.com",
});
if (!result.ok) return res.status(401).json({ reason: result.reason });
// result.handle, result.kid, result.vc?, result.jti, result.iat, result.exp

The bridge JWT carries a 60-second lifetime by default and a jti so the receiver can implement replay protection if needed. It's not a vanilla JWT — the signature uses Secure Enclave WebAuthn rather than a server-side key, so off-the-shelf JWT libraries won't verify it. Always use verifyBridgeJwt.

Full API

| Function | Purpose | |---|---| | resolveDid(handle, opts?) | Fetch + parse <handle>.altimist.com/.well-known/did.json (subdomain form) | | parseDid(did) | Parse a did:web: identifier into { form, host, handle, url }. Recognises both subdomain (did:web:<handle>.<host>) and F-011 path form (did:web:<host>:users:<handle>) | | getDidDocument(did) | Same as resolveDid but takes a full did:web: identifier (either form) — uses parseDid to derive the URL. Validates the round-trip (doc.id === input did) | | getDevices(handle, opts?) | Tier-2 helper: returns the user's active devices as { kid, publicKeyJwk }[] for inline display | | getAlsoKnownAs(handle, opts?) | Tier-2 helper: returns the user's bound alternative DIDs (display-only) | | getMemberships(handle, vcs, opts?) | Tier-2 helper: filters presented JWT-VCs to only those that verify against the user's identity AND aren't revoked | | issueChallenge({ handle, rpID }) | WebAuthn auth options for navigator.credentials.get()allowCredentials is drawn from the user's did.json | | verifyChallenge({ ... }) | Verify a WebAuthn assertion against the JWK published in did.json | | fetchTeamIssuer(team, opts?) | Fetch the team-hub public JWK from altimist.com/.well-known/team-issuers/<team>.json | | verifyMembershipVC({ ... }) | Verify a JWT-VC (W3C VC 2.0) signed by a team-hub. Pure function — no network | | isRevoked(hash, opts?) | Check altimist.com/.well-known/revocations.json | | mintBridgeJwt({ ... }) (browser) | Mint an SE-signed JWT for machine-to-machine auth | | verifyBridgeJwt({ ... }) | Verify a bridge JWT |

All fetchers accept an optional { baseUrl } (template with {handle} / {team} substitution) so staging environments can override the apex (e.g. altimist.dev for testing).

Tier-2 helpers — inline identity-status rendering

For the auth-vs-display split: tier-2 helpers let consumer apps render identity state inline without redirecting users to altimist.id. Use them on your own account/profile pages.

import { getDevices, getMemberships, getAlsoKnownAs } from "@altimist/did-web-client";

// "Patrick · 3 devices" inline
const devices = await getDevices("patrick");

// "Patrick holds: staff.admin, capital.trader"
const memberships = await getMemberships("patrick", presentedVcs);

// "Also known as: did:web:patrickjv.com"
const akas = await getAlsoKnownAs("patrick");

All three are server-only, fail-closed (resolver errors throw ResolverError), and accept the same { baseUrl } template option for staging overrides. getMemberships additionally accepts teamIssuerBaseUrl and revocationsUrl for fully custom staging.

For sensitive identity mutations (add device, revoke device, bind alt-DID, account recovery), don't use this library — those happen at altimist.id via deep-link from your app. See F-012 spec for the tier-3 deep-link pattern.

Common gotchas

  • expectedOrigin and expectedRPID must match what the user's browser saw. WebAuthn binds credentials to (origin, RP_ID) pairs. If your app is yourapp.com, set both. If you have a staging host, keep separate values for that environment. Mismatches surface as result.ok === false with reason: "origin" or reason: "rpid".
  • Persist the challenge between issue and verify. The challenge is single-use, short-lived (~60s), and stateless from this library's perspective. Use a session cookie, iron-session, Redis, or a short-lived DB row.
  • Don't put the kid in your session cookie if you can avoid it. Use it transiently for revocation checks at sign-in time, then issue your app's normal session token. Storing per-device kids in long-lived cookies leaks user-device-fingerprint info if your cookies leak.
  • Revocation is fail-closed by default. If the apex revocations endpoint is unreachable, isRevoked returns true (treats credentials as revoked) rather than passing through. Pass { failOpen: true } if you'd rather degrade open during an outage. Discuss with your security stakeholders before flipping.
  • @simplewebauthn/browser versions matter. This library was tested against @simplewebauthn/browser v10+. Earlier versions have different API shapes.

Versioning

This library follows semver. Pin to a specific version ("@altimist/did-web-client": "0.3.x") until 1.0 — the API surface is stable but internal types may move.

Phase 2a status

  • v0.1 — initial publish. Identity verification (resolveDid, issueChallenge, verifyChallenge), VC verification (fetchTeamIssuer, verifyMembershipVC), revocation check (isRevoked).
  • v0.2 — bridge JWT mint + verify (M8). VC revocation cross-check at verify time (M9).
  • v0.3 (F-012) — tier-2 read helpers (getDevices, getMemberships, getAlsoKnownAs) for inline rendering of identity status in consumer apps without redirects. Foundation for the E-002 consumer-app onboarding epic.
  • v0.6 (F-011) — dual-form did:web parsing. parseDid(did) + getDidDocument(did) recognise both did:web:<handle>.<host> (subdomain) and did:web:<host>:users:<handle> (path) and fetch from the right URL transparently. resolveDid(handle, opts?) is unchanged for callers that already have the handle string.

See F-010 milestones for the broader plan and F-012 spec for the tier-2 + tier-3 design.