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

@m-doc/vical

v1.1.0

Published

VICAL (ISO/IEC 18013-5 Annex C) trust lists for TypeScript, with a CLI

Readme

A VICAL — Verified Issuer Certificate Authority List — is the trust list an mDL verifier checks issuer certificates against. ISO/IEC 18013-5:2021 Annex C defines it as a CBOR list of IACA certificates, wrapped in a COSE_Sign1 signed by the VICAL provider.

Part of m-doc. 04-vical.ts runs everything below against a real AAMVA list, or try it in the browser playground.

CLI

Reading a trust list is often a one-off, so the package ships a command:

npx @m-doc/vical vical.cbor
================================================================
VICAL
================================================================
Provider:        AAMVA
Version:         1.0
Date:            2026-08-05T01:39:05.000Z
Issue ID:        344243
Next update:     2026-08-06T01:39:05.000Z
Algorithm:       ES256
Certificates:    20
Signature:       valid
Staleness:       past nextUpdate
...

The signature is checked by default, with Node's own primitives -- node:crypto parses the signer certificate and WebCrypto checks the ECDSA -- so the command needs nothing beyond this package.

| Option | | | --- | --- | | -j, --json | print JSON instead of a table | | -v, --verbose | add sizes, chain length and the full country list | | --pem | print each certificate as PEM, and nothing else | | --no-verify | skip the signature check | | -h, --help | usage | | --version | the version that was built |

The input may be raw CBOR, or hex, base64 or base64url text. Pass - to read stdin.

Exit codes make it scriptable: 0 read and verified, 1 could not be read, 2 read but the signature did not verify.

# Trust anchors, straight out as PEM
npx @m-doc/vical --pem vical.cbor > anchors.pem

# Stop a pipeline on a list that does not verify
npx @m-doc/vical --json vical.cbor > vical.json || exit 1

# Which countries does this list cover?
npx @m-doc/vical --json -v vical.cbor | jq .supportedCountries

Installation

npm i @m-doc/vical @m-doc/context

Usage

import { createMdocContext } from '@m-doc/context'
import { SignedVical } from '@m-doc/vical'

const ctx = createMdocContext()
const signed = SignedVical.decode(bytes)

// The signature is what makes the list worth anything
if (!(await signed.verify({}, ctx))) throw new Error('VICAL signature is invalid')

const vical = signed.vical

vical.vicalProvider          // who published it
vical.date                   // when
vical.nextUpdate             // when to fetch again

vical.forDocType()           // entries listed for org.iso.18013.5.1.mDL
vical.forCountry('NL')       // the entry a country issued
vical.trustAnchors()         // Map<issuing country, CertificateInfo>
vical.certificates()         // raw DER, ready for an X.509 library

Handing a whole trust list to a verifier is the point of all this:

await Verifier.verifyDeviceResponse(
  { deviceResponse, sessionTranscript, trustedCertificates: vical.certificates() },
  ctx
)

SignedVical

SignedVical extends Sign1 from @m-doc/core, so the COSE surface — protectedHeaders, certificateChain, signatureAlgorithmName, toBeSigned — is all available on it.

class SignedVical extends Sign1 {
  static decode(bytes: Uint8Array): SignedVical

  get vical(): Vical      // decoded lazily from the payload

  verify(options: { key?: CoseKey }, ctx: Pick<MdocContext, 'cose' | 'x509'>): Promise<boolean>
}

verify({}, ctx) with no key takes the public key from the leaf of the x5chain header, which establishes only that the list is internally consistent. Pass the provider's known key — or validate the chain against a trust anchor of your own — to learn that it is the list you meant to fetch.

Decoding failures raise VicalError, which carries the underlying cause.

Vical

class Vical extends CborStructure {
  get version(): string                 // '1.0'
  get vicalProvider(): string
  get date(): Date
  get nextUpdate(): Date | undefined
  get vicalIssueID(): number | undefined   // monotonic issue counter
  get certificateInfos(): Array<CertificateInfo>
  get extensions(): Map<string, unknown> | undefined

  forDocType(docType?: string): Array<CertificateInfo>          // defaults to mDL
  forCountry(countryCode: string): CertificateInfo | undefined
  forSubjectKeyIdentifier(ski: Uint8Array): CertificateInfo | undefined
  trustAnchors(docType?: string): Map<string, CertificateInfo>  // keyed by issuing country
  certificates(docType?: string): Array<Uint8Array>
}

trustAnchors keeps the last entry where a country lists more than one certificate for the docType; read forDocType where every entry matters.

A list past its nextUpdate is stale however valid its signature — check it yourself, since nothing here will.

CertificateInfo

for (const info of vical.forDocType()) {
  info.certificate           // DER-encoded IACA certificate
  info.toPem()               // the same, as a PEM block
  info.serialNumber          // bigint
  info.ski                   // Subject Key Identifier, as the provider wrote it
  info.docType               // Array<string> — what this CA may issue
  info.certificateProfile    // e.g. '1.0.18013.5.1.2'
  info.issuingAuthority
  info.issuingCountry
  info.stateOrProvinceName   // where a jurisdiction is a subdivision
  info.issuer, info.subject  // DER-encoded names
  info.notBefore, info.notAfter
  info.extensions
}

Also exported: MDL_DOCTYPE, IACA_CERTIFICATE_PROFILE (1.0.18013.5.1.2) and VICAL_EKU_OID (1.0.18013.5.1.8).

Round-tripping

Members the spec reserves for future use are carried through decoding untouched, so a list from a newer provider still re-encodes to the bytes it arrived as — which is what keeps its signature verifiable after a decode.

SignedVical.decode(bytes).encode()   // byte-identical to bytes

Requirements

Node 20.19 or newer. Ships ESM and CJS, with type declarations for both. The library itself is runtime-agnostic; only the CLI is Node-specific.

License

Apache-2.0.