ProvenPin Install

Hono + hono/jwt bearer auth with Zod login: working versions (tested 2026-10-15)

Issue HS256 JWTs from a Zod-validated login route in Hono and guard routes with the built-in jwt middleware.

passed4 passed · 0 failed2026-10-15 11:35 UTC
Status
passed
Last passed
2026-10-15 11:35 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions5
hono@4.13.13zod@4.6.7@hono/zod-validator@0.9.1tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38728226240

The code

Exactly the files that passed. Your agent can pull them with get_recipe("hono-jwt-auth") (install), or see them on GitHub.

package.json
{
  "name": "hono-jwt-auth",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "hono": "4.13.13",
    "zod": "4.6.7",
    "@hono/zod-validator": "0.9.1"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/app.ts
import { Hono } from "hono";
import { jwt, sign } from "hono/jwt";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const Login = z.object({ username: z.string().min(1), password: z.string().min(1) });
const USERS: Record<string, string> = { ada: "lovelace" }; // swap for a real user store

export function createApp(secret: string) {
  const app = new Hono();
  app.post("/login", zValidator("json", Login), async (c) => {
    const { username, password } = c.req.valid("json");
    if (USERS[username] !== password) return c.json({ error: "invalid_credentials" }, 401);
    const token = await sign({ sub: username, exp: Math.floor(Date.now() / 1000) + 3600 }, secret, "HS256");
    return c.json({ token });
  });
  app.use("/me/*", jwt({ secret, alg: "HS256" }));
  app.get("/me/profile", (c) => c.json({ user: c.get("jwtPayload").sub }));
  return app;
}
test/app.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { createApp } from "../src/app.ts";

const app = createApp("test-secret");
const post = (body: unknown) => app.request("/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });

test("login returns a token that unlocks /me", async () => {
  const { token } = await (await post({ username: "ada", password: "lovelace" })).json();
  const res = await app.request("/me/profile", { headers: { authorization: `Bearer ${token}` } });
  assert.equal(res.status, 200);
  assert.deepEqual(await res.json(), { user: "ada" });
});

test("wrong password is 401", async () => {
  assert.equal((await post({ username: "ada", password: "x" })).status, 401);
});

test("invalid body is 400", async () => {
  assert.equal((await post({ username: "" })).status, 400);
});

test("no token is 401", async () => {
  assert.equal((await app.request("/me/profile")).status, 401);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.