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

@writhq/verify

v0.2.1

Published

Writ platform middleware — requireKYA() for Hono / Express / Next, plus a Verify API client. ~10 lines to gate an endpoint on a verified agent passport. Fails closed.

Downloads

635

Readme

@writhq/verify

Writ — KYA (Know Your Agent). Gate any endpoint on a verified agent passport.

The platform-side middleware. Add ~10 lines and an endpoint accepts requests only from agents presenting a valid, in-scope, in-cap, non-revoked passport. It fails closed: a missing header, a denied decision, an over-cap amount, or an unreachable passport all block. Blocked callers get a 403 "KYA required" block body with an onboarding link — rejected traffic becomes leads.

Adapters ship for Hono, Express, and Next.js (App Router). The agent side is @writhq/sdk.

Install

npm install @writhq/verify

Requires Node 20+. Zero runtime dependencies. ESM only — there is no CommonJS build, so require('@writhq/verify') fails with ERR_PACKAGE_PATH_NOT_EXPORTED. That error reads like a broken package and is not: use import, or await import() from CJS.

Configuration

Both are read from the environment (or pass them per-call):

  • PASSPORT_URL — base URL of the passport service (default https://api.writhq.com; set it to point at a local stack).
  • PLATFORM_API_KEY — your platform's Bearer key for POST /v1/verify (NORTHBANK_API_KEY is also accepted as a fallback).

Hono

import { Hono } from 'hono';
import { requireKYA } from '@writhq/verify';

const app = new Hono();

app.post(
  '/api/refill',
  requireKYA({
    action: 'account.refill',
    amount: async (c) => (await c.req.json()).amount_minor, // MINOR units
  }),
  async (c) => {
    const kya = c.get('kya'); // resolved verify response (chain, receipt, ...)
    return c.json({ ok: true, chain: kya.chain });
  },
);

Express

import express from 'express';
import { requireKYAExpress } from '@writhq/verify';

const app = express();
app.use(express.json());

app.post(
  '/api/refill',
  requireKYAExpress({ action: 'account.refill', amount: (req) => req.body.amount_minor }),
  (req, res) => {
    res.json({ ok: true, chain: req.kya.chain }); // req.kya set on allow
  },
);

Next.js (App Router)

import { guardKYA } from '@writhq/verify';

export async function POST(req: Request) {
  const gate = await guardKYA(req, {
    action: 'account.refill',
    amount: async () => (await req.clone().json()).amount_minor,
  });
  if (gate instanceof Response) return gate; // 403 block page / denial
  // gate is the verify response — gate.chain, gate.receipt, ...
  return Response.json({ ok: true, chain: gate.chain });
}

The block body

Blocked callers receive (HTTP 403, or the passport's status on a denial):

{
  "error": "KYA required",
  "message": "This action requires a verified agent passport.",
  "reason": "missing_passport",
  "onboarding_url": "https://.../onboarding"
}

Set onboardingUrl in the options to control where leads land.

Signature authority (document.sign)

Gate a signing route the way you gate a payment route. A document resolver replaces amount, because a signing call states its number once — as liability_minor:

app.post('/api/sign',
  requireKYA({
    action: 'document.sign',
    // hash · class · counterparty · liability, read from the REAL request
    document: async (c) => (await c.req.json()).document,
  }),
  handler,   // only runs when the agent had the authority; fails closed
);
// the document block — the same object the agent signed for
{
  "document_hash": "<lowercase hex sha256 of the document bytes>",
  "document_class": "nda",          // nda | msa | sow | order_form | dpa | other
  "counterparty": "Northbank Sandbox",
  "liability_minor": 2500000        // $25,000 of exposure
}

The mandate names which classes the agent may sign and reads its caps as liability caps: max_amount_per_tx per document, max_amount_per_period cumulative. Three ways it stops — document_class (wrong kind of paper), per_tx_cap (one document too big), period_cap (too much cumulative exposure). Present a document that differs from the one the agent signed for and the decision is context_mismatch. The passport never receives the document, only its SHA-256.

Writ attests the authority. It never produces the signature — your e-signature step still does that — and none of this is a qualified electronic signature (eIDAS/QES).

Countersigning

After you execute the action, co-sign the verification. A record signed by both sides — Writ's receipt plus your platform key — is the strongest artifact this system produces.

import { countersign, generateKeypair, publicFromPrivate } from '@writhq/verify';

// Once, at setup: mint a countersigning keypair and register the PUBLIC half
// with POST /v1/platforms/self/key using your platform API key.
const { privateJwk, publicJwk } = await generateKeypair();

// Then, after every executed action:
const jws = await countersign(
  {
    verification_id,
    decision: 'allow',
    platform: 'plt_your_platform',
    platform_ref: ledgerTxId,   // your own id for what you just did
    ts: new Date().toISOString(),
  },
  privateJwk,
);

await fetch(`${passportUrl}/v1/verifications/${verification_id}/countersign`, {
  method: 'POST',
  headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' },
  body: JSON.stringify({ countersig: jws }),
});

If a countersignature is rejected 422, the response names the public key actually on file for your platform so you can compare it against the key you signed with — that mismatch is the only way a correct integration fails here.

Lower level

  • KYAClient — thin, never-throws client for POST /v1/verify. Any transport or protocol failure resolves to { ok: false } so you fail closed.
  • evaluateRequest(ctx, opts, headerGetter) — the framework-agnostic core the adapters wrap; use it to build your own adapter.
  • blockBody, PASSPORT_HEADER ("x-passport").
import { KYAClient } from '@writhq/verify';

const client = new KYAClient({ passportUrl: process.env.PASSPORT_URL, apiKey: process.env.PLATFORM_API_KEY });
const outcome = await client.verify({ assertion, action: 'account.refill', amount: 50_000 });
if (!outcome.ok) return deny(outcome.reason); // 'per_tx_cap' | 'mandate_revoked' | ...

API

requireKYA (Hono) · requireKYAExpress (Express) · guardKYA (Next) · evaluateRequest · KYAClient · blockBody · PASSPORT_HEADER. Types: RequireKYAOptions (incl. the document resolver), KYABlockBody, KYAContext, KYAClientConfig, KYAOutcome, VerifyArgs, VerifyResponse.

Home: https://writhq.com · Docs: https://writhq.com/docs/ · API: https://api.writhq.com

License

MIT © Tundra Industries