{
  "$schema": "https://hyperjs.ai/schema/registry-item.json",
  "name": "auth-jwt",
  "version": "0.1.0",
  "description": "JWT auth middleware + .auth() route builder sugar for Hyper.",
  "readme": "# @hyper/auth-jwt\n\nJWT auth plugin for Hyper — bearer-token verification, typed `ctx.user`, role/scope guards.\n\n## Install\n\nComponents are installed as source into your repo, not pulled from npm:\n\n```bash\nbunx hyper add auth-jwt\n```\n\nWires the alias `@hyper/auth-jwt` to `src/hyper/auth-jwt/` (configurable in `hyper.config.json`). See [hyperjs.ai](https://hyperjs.ai) for the full registry.\n\n## Usage\n\n```ts\nimport { Hyper, ok } from \"@hyper/core\"\nimport { authJwtPlugin } from \"@hyper/auth-jwt\"\n\nexport default new Hyper()\n  .use(authJwtPlugin({ secretEnv: \"JWT_SECRET\" }))\n  .get(\"/me\", ({ ctx }) => ok({ user: ctx.user }))\n  .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
  "registryDeps": [
    "core"
  ],
  "peerDeps": {},
  "optionalPeerDeps": {},
  "files": [
    {
      "path": "auth-jwt/index.ts",
      "contents": "/**\n * @hyper/auth-jwt — JWT authentication middleware + `.auth()` route sugar.\n *\n *   app({ plugins: [authJwtPlugin({ secret: env.JWT_SECRET })] })\n *   route.get(\"/me\").auth().handle((c) => ok({ user: c.ctx.user }))\n *\n * `route.auth()` is a thin wrapper around `route.meta({ auth: true })`\n * plus a pre-chained middleware that enforces presence of `ctx.user`.\n */\n\nimport { RouteBuilder } from \"@hyper/core\"\nimport type { HyperPlugin, Middleware } from \"@hyper/core\"\nimport { JwtError, type JwtPayload, type VerifyOptions, verifyJwt } from \"./jwt.ts\"\n\nexport { JwtError, verifyJwt } from \"./jwt.ts\"\nexport type { JwtAlgorithm, JwtHeader, JwtPayload, VerifyOptions } from \"./jwt.ts\"\n\n/**\n * Default `ctx.user` shape when no `loadUser` is supplied. Mirrors the\n * populated fields in the middleware below.\n */\nexport interface AuthUser {\n  readonly sub: string\n  readonly scope?: string | readonly string[]\n}\n\nexport interface AuthJwtConfig extends VerifyOptions {\n  /** Optional: map verified payload → ctx.user. */\n  readonly loadUser?: (payload: JwtPayload) => unknown | Promise<unknown>\n  /** Optional: where to look for the token. Default: Authorization: Bearer … */\n  readonly extract?: (req: Request) => string | null\n  /**\n   * Opt out of the 32-byte secret length check. Off by default — a secret\n   * shorter than 32 bytes is a bootable footgun and Hyper fails fast.\n   */\n  readonly allowShortSecret?: boolean\n}\n\n/** Minimum secret length we enforce at boot. 32 bytes = 256 bits. */\nexport const MIN_JWT_SECRET_BYTES = 32\n\n/** Validates a JWT secret against the minimum-length rule. Throws with why/fix. */\nexport function validateJwtSecret(\n  secret: string,\n  opts: { readonly allowShort?: boolean } = {},\n): void {\n  if (opts.allowShort) return\n  const bytes = new TextEncoder().encode(secret).byteLength\n  if (bytes < MIN_JWT_SECRET_BYTES) {\n    throw new Error(\n      `@hyper/auth-jwt: secret is ${bytes} bytes; minimum is ${MIN_JWT_SECRET_BYTES}. Why: short HS256 secrets are brute-forceable in hours on commodity hardware. Fix: generate a 32+ byte secret (e.g., \\`openssl rand -base64 48\\`) or pass \\`allowShortSecret: true\\` at your own risk.`,\n    )\n  }\n}\n\ndeclare module \"@hyper/core\" {\n  interface AppContext {\n    /**\n     * The authenticated user. Defaults to the `AuthUser` shape\n     * populated by the middleware when no `loadUser` is supplied.\n     *\n     * To type a custom shape, augment this interface in your app:\n     *   declare module \"@hyper/core\" {\n     *     interface AppContext { user?: MyUser }\n     *   }\n     */\n    readonly user?: AuthUser\n    readonly jwt?: JwtPayload\n  }\n}\n\nexport function authJwt(config: AuthJwtConfig): Middleware {\n  validateJwtSecret(config.secret, { allowShort: config.allowShortSecret ?? false })\n  const extract = config.extract ?? defaultExtract\n  return async ({ ctx, req, next }) => {\n    const token = extract(req)\n    if (!token) return unauthorized(\"missing_token\")\n    try {\n      const { payload } = await verifyJwt(token, config)\n      ;(ctx as { jwt?: JwtPayload }).jwt = payload\n      if (config.loadUser) {\n        ;(ctx as { user?: unknown }).user = await config.loadUser(payload)\n      } else {\n        ;(ctx as { user?: unknown }).user = { sub: payload.sub, scope: payload.scope }\n      }\n      return next()\n    } catch (e) {\n      if (e instanceof JwtError) return unauthorized(e.code)\n      throw e\n    }\n  }\n}\n\nfunction defaultExtract(req: Request): string | null {\n  const h = req.headers.get(\"authorization\")\n  if (!h) return null\n  const [type, value] = h.split(\" \")\n  return type?.toLowerCase() === \"bearer\" ? (value ?? null) : null\n}\n\nfunction unauthorized(code: string) {\n  return new Response(JSON.stringify({ error: \"unauthorized\", code }), {\n    status: 401,\n    headers: {\n      \"content-type\": \"application/json\",\n      \"www-authenticate\": 'Bearer realm=\"hyper\", error=\"invalid_token\"',\n    },\n  })\n}\n\n/**\n * Plugin — installs the `.auth()` method on the route builder prototype.\n * Consumers only need to chain `use(authJwt(...))` on protected routes,\n * or call `.auth()` as sugar.\n */\nexport function authJwtPlugin(config: AuthJwtConfig): HyperPlugin {\n  validateJwtSecret(config.secret, { allowShort: config.allowShortSecret ?? false })\n  installAuthMethod(authJwt(config))\n  return {\n    name: \"@hyper/auth-jwt\",\n  }\n}\n\n/**\n * Install `.auth()` on the RouteBuilder prototype. Safe to call many times —\n * idempotent. Exported so tests and userland can pre-install without a plugin.\n */\nexport function installAuthMethod(mw: Middleware): void {\n  const proto = (\n    RouteBuilder as unknown as {\n      prototype: {\n        auth?: () => unknown\n        use: (m: Middleware) => unknown\n        meta: (m: Record<string, unknown>) => { use: (m: Middleware) => unknown }\n      }\n    }\n  ).prototype\n  if (proto.auth) return\n  proto.auth = function auth(this: {\n    meta: (m: Record<string, unknown>) => { use: (m: Middleware) => unknown }\n  }) {\n    return this.meta({ auth: true }).use(mw)\n  }\n}\n",
      "sha256": "96b122083a7cd37615963b08e411eadfee44b8cd3b8ca971015b92e6a7d98628"
    },
    {
      "path": "auth-jwt/jwt.ts",
      "contents": "/**\n * Minimal HS256/HS384/HS512 verify-only JWT implementation.\n *\n * We explicitly do NOT sign — in 2026 all first-party auth flows issue\n * tokens through the OAuth provider; server-side we just verify.\n *\n * For RS/ES we expose a verify hook — consumers plug in their JWKS\n * resolver via `jwks(url)` utility.\n */\n\nimport { timingSafeEqual } from \"node:crypto\"\n\nexport type JwtAlgorithm = \"HS256\" | \"HS384\" | \"HS512\"\n\nexport interface JwtHeader {\n  readonly alg: string\n  readonly kid?: string\n  readonly typ?: string\n}\n\nexport interface JwtPayload {\n  readonly iss?: string\n  readonly aud?: string | readonly string[]\n  readonly sub?: string\n  readonly exp?: number\n  readonly nbf?: number\n  readonly iat?: number\n  readonly scope?: string\n  readonly [k: string]: unknown\n}\n\nexport interface VerifyOptions {\n  readonly secret?: string | Uint8Array\n  readonly algorithms?: readonly JwtAlgorithm[]\n  readonly issuer?: string\n  readonly audience?: string\n  readonly clockToleranceSec?: number\n}\n\nexport class JwtError extends Error {\n  constructor(\n    readonly code: string,\n    msg: string,\n  ) {\n    super(msg)\n  }\n}\n\nconst SUPPORTED: Record<JwtAlgorithm, \"SHA-256\" | \"SHA-384\" | \"SHA-512\"> = {\n  HS256: \"SHA-256\",\n  HS384: \"SHA-384\",\n  HS512: \"SHA-512\",\n}\n\nexport async function verifyJwt(\n  token: string,\n  options: VerifyOptions,\n): Promise<{ header: JwtHeader; payload: JwtPayload }> {\n  const parts = token.split(\".\")\n  if (parts.length !== 3) throw new JwtError(\"invalid_token\", \"malformed jwt\")\n  const [h, p, sig] = parts\n  const header = JSON.parse(b64urlToUtf8(h!)) as JwtHeader\n  const payload = JSON.parse(b64urlToUtf8(p!)) as JwtPayload\n  const alg = header.alg as JwtAlgorithm\n  const allowed = new Set(options.algorithms ?? [\"HS256\"])\n  if (!allowed.has(alg)) throw new JwtError(\"alg_not_allowed\", `disallowed alg: ${alg}`)\n\n  if (alg.startsWith(\"HS\")) {\n    const digest = SUPPORTED[alg]\n    if (!digest) throw new JwtError(\"alg_unsupported\", `unsupported alg: ${alg}`)\n    if (!options.secret) throw new JwtError(\"no_secret\", \"secret required for HMAC\")\n    const key = await crypto.subtle.importKey(\n      \"raw\",\n      typeof options.secret === \"string\"\n        ? new TextEncoder().encode(options.secret)\n        : options.secret,\n      { name: \"HMAC\", hash: digest },\n      false,\n      [\"sign\"],\n    )\n    const signed = await crypto.subtle.sign(\"HMAC\", key, new TextEncoder().encode(`${h}.${p}`))\n    const expected = new Uint8Array(signed)\n    const actual = b64urlToBytes(sig!)\n    if (\n      expected.length !== actual.length ||\n      !timingSafeEqual(Buffer.from(expected), Buffer.from(actual))\n    ) {\n      throw new JwtError(\"bad_signature\", \"jwt signature mismatch\")\n    }\n  } else {\n    throw new JwtError(\"alg_unsupported\", `unsupported alg: ${alg}`)\n  }\n\n  const now = Math.floor(Date.now() / 1000)\n  const skew = options.clockToleranceSec ?? 30\n  if (typeof payload.exp === \"number\" && now > payload.exp + skew) {\n    throw new JwtError(\"expired\", \"jwt expired\")\n  }\n  if (typeof payload.nbf === \"number\" && now + skew < payload.nbf) {\n    throw new JwtError(\"not_yet_valid\", \"jwt not yet valid\")\n  }\n  if (options.issuer && payload.iss !== options.issuer) {\n    throw new JwtError(\"bad_issuer\", `issuer ${payload.iss ?? \"\"} != ${options.issuer}`)\n  }\n  if (options.audience) {\n    const aud = payload.aud\n    const ok = Array.isArray(aud) ? aud.includes(options.audience) : aud === options.audience\n    if (!ok) throw new JwtError(\"bad_audience\", \"aud mismatch\")\n  }\n  return { header, payload }\n}\n\nfunction b64urlToUtf8(s: string): string {\n  return new TextDecoder().decode(b64urlToBytes(s))\n}\nfunction b64urlToBytes(s: string): Uint8Array {\n  const pad = s.length % 4 === 0 ? 0 : 4 - (s.length % 4)\n  const b64 = (s + \"====\".slice(0, pad)).replace(/-/g, \"+\").replace(/_/g, \"/\")\n  const bin = atob(b64)\n  const out = new Uint8Array(bin.length)\n  for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i)\n  return out\n}\n",
      "sha256": "4b6868d1eeea3479e60a49ed1487ad9b6eee3b5d07491c375c4d9f0b44615360"
    }
  ],
  "subpaths": {}
}