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

zcap-zod

v0.1.0

Published

Zod schemas for W3C CCG Authorization Capabilities (zcaps): root zcaps, delegated zcaps, and capability invocations.

Readme

zcap-zod

Zod schemas for the W3C CCG Authorization Capabilities (zcap) data model.

Parse and validate root zcaps, delegated zcaps, and capability invocations. Every constraint in the schemas is annotated with the normative statement from the spec that it encodes.

Paste this into any .html file. It needs no install and no build:

<script type="module">
  import { RootZcap } from "https://gobengo.github.io/zcap-zod/zcap-zod/index.js"

  const root = RootZcap.parse({
    "@context": "https://w3id.org/zcap/v1",
    id: "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo",
    controller: "did:key:z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9",
    invocationTarget: "https://example.com/foo",
  })
  console.log(root)
</script>

That URL serves the build of the latest commit on main, with zod included. The demo page runs smoke tests against the same build.

With Node.js, a bundler, or an import map, import from "zcap-zod" instead:

import { RootZcap } from "zcap-zod"

In Node.js or a bundler, install the package first. In a browser, map the name with an import map:

<script type="importmap">
  { "imports": { "zcap-zod": "https://gobengo.github.io/zcap-zod/zcap-zod/index.js" } }
</script>
<script type="module">
  import { RootZcap } from "zcap-zod"
</script>

What it does and does not check

zcap-zod checks the zcap data model: which fields must be present, their types, and the structural rules the spec states normatively (a root zcap has exactly four fields; a capability chain starts with the root zcap's id by reference; a delegated zcap carries a capabilityDelegation proof; and so on).

It does not verify signatures, and it does not constrain the Data Integrity shape of a proof — type, cryptosuite, verificationMethod, created, proofValue are all left unconstrained. That is a DI implementation's job. Pass the proof to one.

It also does not enforce the spec's SHOULDs. Those are exported as values you can apply yourself: rootZcapIdFor, ROOT_ZCAP_URN_PREFIX, RECOMMENDED_MAX_CAPABILITY_CHAIN_LENGTH.

Install

Not yet published to npm. Install from the repository:

npm install github:gobengo/zcap-zod

zod v4 is the only runtime dependency.

The published package contains compiled JavaScript and type declarations in dist/, plus the original TypeScript in src/. import { RootZcap } from "zcap-zod" resolves to dist/index.js and dist/index.d.ts; import { RootZcap } from "zcap-zod/src/index.ts" reaches the source, which is what the CDN below uses and what Node 22.6+ and Deno can run directly.

API

Schemas

| Export | Parses | | --- | --- | | RootZcap | A root zcap: @context, id (a URN), invocationTarget, controller, and nothing else. | | DelegatedZcap | A delegated zcap, including at least one conforming capabilityDelegation proof. | | ZcapInvocation | An invocation: any linked data object carrying a conforming capabilityInvocation proof. | | Zcap | A union of the three above — any conforming zcap document. | | CapabilityChain | The capabilityChain array of a delegation proof. | | CapabilityDelegationProof | A single capabilityDelegation proof. | | CapabilityInvocationProof | A single capabilityInvocation proof. |

Each schema is a Zod schema, so the whole Zod API is available: .parse(), .safeParse(), .refine(), .and(), and so on.

TypeScript types are exported under the same names: RootZcap, DelegatedZcap, ZcapInvocation, Zcap.

Helpers

| Export | | | --- | --- | | rootZcapIdFor(invocationTarget) | Builds the RECOMMENDED root zcap id: urn:zcap:root: + encodeURIComponent(invocationTarget). | | ROOT_ZCAP_URN_PREFIX | "urn:zcap:root:" | | ZCAP_V1_JSONLD_CONTEXT | "https://w3id.org/zcap/v1" | | RECOMMENDED_MAX_CAPABILITY_CHAIN_LENGTH | 10. A verifier concern, so it is not enforced by CapabilityChain. |

Usage

Validate a root zcap

import { RootZcap, rootZcapIdFor } from "zcap-zod"

const invocationTarget = "https://example.com/foo"

const root = RootZcap.parse({
  "@context": "https://w3id.org/zcap/v1",
  id: rootZcapIdFor(invocationTarget),
  controller: "did:key:z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9",
  invocationTarget,
})

A root zcap MUST NOT have any other fields, so this throws:

RootZcap.parse({ ...root, allowedAction: ["read"] }) // ZodError: Unrecognized key

Validate a delegated zcap

import { DelegatedZcap } from "zcap-zod"

const result = DelegatedZcap.safeParse({
  "@context": [
    "https://w3id.org/zcap/v1",
    "https://w3id.org/security/suites/ed25519-2020/v1",
  ],
  id: "urn:uuid:cdc77118-6bfa-11ec-aceb-10bf48838a41",
  parentCapability: "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo",
  controller: "did:key:z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9",
  invocationTarget: "https://example.com/foo",
  expires: "2021-11-03T18:33:51Z",
  allowedAction: ["write", "read"],
  proof: {
    type: "Ed25519Signature2020",
    created: "2021-10-27T18:33:51Z",
    verificationMethod: "did:key:z6Mkf...#z6Mkf...",
    proofPurpose: "capabilityDelegation",
    capabilityChain: ["urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo"],
    proofValue: "z3t9BCQyF21MDVYmLKc9zbLreqx4wBtQ...",
  },
})

if (!result.success) {
  console.error(result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`))
}

Unrecognized properties on a delegated zcap are preserved, not rejected — it is a JSON-LD document whose @context may define additional vocabulary.

Validate an invocation

import { ZcapInvocation } from "zcap-zod"

ZcapInvocation.parse({
  "@context": "https://w3id.org/zcap/v1",
  id: "urn:uuid:ad86cb2c-e9db-434a-beae-71b82120a8a4",
  proof: {
    type: "DataIntegrityProof",
    cryptosuite: "eddsa-jcs-2022",
    proofPurpose: "capabilityInvocation",
    capability: "urn:zcap:root:https%3A%2F%2Fexample.com%2Ffoo",
    invocationTarget: "https://example.com/foo",
    capabilityAction: "read",
    verificationMethod: "did:key:z6Mkf...#z6Mkf...",
    proofValue: "z3t9BCQyF21MDVYmLKc9zbLreqx4wBtQ...",
  },
})

Because a delegated zcap can only be invoked by submitting the entire zcap, proof.capability may also be a fully embedded DelegatedZcap rather than a URI.

Accept any zcap document

import { Zcap } from "zcap-zod"

const parsed = Zcap.parse(await request.json())

Zcap is a union, so a document is accepted if it conforms as a root zcap, a delegated zcap, or an invocation.

Parse an incoming request and report why it failed

Zod's errors carry the normative statement that was violated, so a 400 can say something more useful than "invalid".

Which schema to use matters here. Zcap is a union, and a union failure collapses to a single "Invalid input" at the root — it cannot tell you which branch you nearly matched. For an endpoint that returns error messages to a caller, pick the schema the document is trying to be, and the issues come back naming real fields:

import * as z from "zod/v4"
import { RootZcap, DelegatedZcap, ZcapInvocation } from "zcap-zod"

/** Which kind of zcap document is this trying to be? */
function classify(doc) {
  if (doc && typeof doc === "object") {
    if ("parentCapability" in doc) return { kind: "delegated zcap", schema: DelegatedZcap }
    if ("proof" in doc) return { kind: "invocation", schema: ZcapInvocation }
  }
  return { kind: "root zcap", schema: RootZcap }
}

export async function handleRequest(request) {
  let body
  try {
    body = await request.json()
  } catch {
    return Response.json({ error: "request body is not valid JSON" }, { status: 400 })
  }

  const { kind, schema } = classify(body)
  const result = schema.safeParse(body)

  if (!result.success) {
    return Response.json(
      {
        error: `request body is not a conforming ${kind}`,
        // One entry per violated constraint, machine-readable.
        issues: result.error.issues.map((issue) => ({
          path: issue.path.join(".") || "(document)",
          message: issue.message,
        })),
        // The same thing, formatted for a human reading logs or a terminal.
        detail: z.prettifyError(result.error),
      },
      { status: 400 },
    )
  }

  const zcap = result.data
  // ... verify the proof with a Data Integrity implementation, then act on it
  return Response.json({ ok: true, id: zcap.id })
}

A root zcap missing its invocationTarget and using an https: id comes back as:

{
  "error": "request body is not a conforming root zcap",
  "issues": [
    { "path": "id", "message": "Invalid Uniform Resource Name (URN)" },
    { "path": "invocationTarget", "message": "Invalid input: expected string, received undefined" }
  ],
  "detail": "\u2716 Invalid Uniform Resource Name (URN)\n  \u2192 at id\n\u2716 Invalid input: expected string, received undefined\n  \u2192 at invocationTarget"
}

and that detail, printed, reads:

✖ Invalid Uniform Resource Name (URN)
  → at id
✖ Invalid input: expected string, received undefined
  → at invocationTarget

A delegated zcap whose expires is not a date-time and whose proof has the wrong purpose gets the spec's own wording back:

✖ Invalid ISO datetime
  → at expires
✖ At least one proof MUST have a proofPurpose of "capabilityDelegation".
  → at proof

Zod v4 also offers z.treeifyError (issues nested to mirror the document) and z.flattenError (one flat level) if either shape suits your API better. z.treeifyError is also the one way to get detail out of a Zcap union failure — it merges what every branch complained about, which is thorough but noisy.

Enforcing the SHOULDs

import { RECOMMENDED_MAX_CAPABILITY_CHAIN_LENGTH, CapabilityChain } from "zcap-zod"

const VerifierChain = CapabilityChain.refine(
  (chain) => chain.length <= RECOMMENDED_MAX_CAPABILITY_CHAIN_LENGTH,
  { message: "capability chain is longer than a verifier SHOULD accept" },
)

Use it in a browser, with no bundler

From GitHub Pages

Every push to main deploys a browser-ready build to GitHub Pages:

import { Zcap } from "https://gobengo.github.io/zcap-zod/zcap-zod/index.js"

This build is the tsc output from dist/, plus a copy of zod next to it, so the one import is all a page needs. It always tracks main. For a pinned version, use esm.sh (below). Why it's built this way is recorded in ADR-0001.

From esm.sh

esm.sh transpiles the TypeScript source straight from GitHub, so a plain HTML page can import the schemas over a CDN — no install and no build of your own:

import { Zcap } from "https://esm.sh/gh/gobengo/zcap-zod/src/index.ts"

Pin a tag or a commit for anything you care about, rather than tracking the default branch:

import { Zcap } from "https://esm.sh/gh/gobengo/[email protected]/src/index.ts"

esm.sh resolves zod from this package's package.json automatically — you don't need to load it yourself. The ./src/* entry in this package's "exports" is what makes the deep path above resolvable; keep it if you change the package layout.

The repository has to be public on GitHub for this to work, and esm.sh caches aggressively per ref — pinning a tag is also how you get a predictable cache key.

Copy-paste snippet

Paste this into jsbin, CodePen, or any .html file and open it. To pin a version, swap the import URL for an esm.sh one:

<!doctype html>
<meta charset="utf-8">
<title>zcap-zod in the browser</title>
<pre id="out">running…</pre>
<script type="module">
  import { RootZcap, Zcap, rootZcapIdFor } from "https://gobengo.github.io/zcap-zod/zcap-zod/index.js"

  const out = document.getElementById("out")
  const log = (...args) => { out.textContent += args.join(" ") + "\n" }
  out.textContent = ""

  const invocationTarget = "https://example.com/foo"

  // A conforming root zcap.
  const root = {
    "@context": "https://w3id.org/zcap/v1",
    id: rootZcapIdFor(invocationTarget),
    controller: "did:key:z6MkfWKcvBiKCfNgz5UUGseNt37t4dguEvFgJ9XvX2UV6zB9",
    invocationTarget,
  }

  log("root id:", root.id)
  log("valid root zcap:", RootZcap.safeParse(root).success)
  log("valid via the Zcap union:", Zcap.safeParse(root).success)

  // A root zcap MUST NOT have any other fields.
  const bad = RootZcap.safeParse({ ...root, allowedAction: ["read"] })
  log("root with an extra field:", bad.success)
  log(bad.error.issues.map((i) => `  ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n"))

  // A root zcap id MUST be a URN.
  const notAUrn = RootZcap.safeParse({ ...root, id: "https://example.com/root" })
  log("root with an https: id:", notAUrn.success)
  log(notAUrn.error.issues.map((i) => `  ${i.path.join(".") || "(root)"}: ${i.message}`).join("\n"))
</script>

Sharing zod with the rest of your page

By default esm.sh bundles its own copy of zod. If your page already imports zod and you want one shared instance, mark it external and resolve it with an import map:

<script type="importmap">
  {
    "imports": {
      "zod/": "https://esm.sh/zod@4/"
    }
  }
</script>
<script type="module">
  import { Zcap } from "https://esm.sh/gh/gobengo/zcap-zod/src/index.ts?external=zod"
  import * as z from "zod/v4"
</script>

Development

npm install
npm test     # node --test, against the TypeScript source
npm run build  # tsc -> dist/*.js + dist/*.d.ts
npm run dev    # tsc --watch + the demo page at http://localhost:8080/
npm run build:site  # build + assemble _site/, what GitHub Pages deploys

Tests run on node --test against the TypeScript source directly (Node 22.6+), so most work needs no build.

npm run build compiles src/ to dist/ with tsc. Two tsconfig.json settings are load-bearing:

  • rewriteRelativeImportExtensions — the source imports siblings as ./zcap-zod.ts so Node can run it directly. This rewrites those to ./zcap-zod.js on the way out, so the emitted JavaScript is valid ESM.
  • "lib": ["es2023", "dom"] with "types": [] — the only platform global the library uses is URL. Building without @types/node means a node-only API cannot slip in unnoticed, which matters because this ships to browsers too.

prepack runs the build, so npm pack and npm publish always ship a freshly compiled dist/. dist/ is gitignored.

  • test/zcap-zod.test.ts — one test per normative statement in the spec.
  • test/zcap-spec-examples-schema.test.ts — every example in the zcap-spec, and whether it conforms to the v0.4 data model. Examples 1–4 come from the informative "Zcap by Example" narrative, predate the v0.4 normative data model, and are expected not to parse; if the spec is updated to bring them in line, this is the test that tells you.

The zcap-spec/ directory is a vendored subtree of the spec repository.

References

License

MIT