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

@suluk/better-auth

v0.3.1

Published

Official Better-Auth-on-Hono support for Suluk: auth methods -> v4 securitySchemes; ingest Better Auth's OpenAPI 3.0 -> v4; session -> principal for per-viewer docs. CANDIDATE tooling.

Downloads

131

Readme


CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.

Install

bun add @suluk/better-auth

better-auth and hono are optional peers — everything here is duck-typed, so you only need the ones you actually use.

What it does

Better Auth is a Contract input (your auth settings). This package reflects it into the Suluk v4 document and closes the per-viewer loop, without you re-typing the auth surface:

  • Auth methods → v4 securitySchemesauthSecuritySchemes({ session, bearer, apiKey, … }) derives the components.securitySchemes block (session-cookie, HTTP bearer, API-key header).
  • Better Auth's OpenAPI 3.0 → v4ingestAuthOpenAPI normalizes Better Auth's own generateOpenAPISchema() output to JSON Schema 2020-12, lifts it to v4 (via @suluk/openapi-compat), and mergeAuth folds the auth routes + schemes into your app's document. /sign-up, /get-session, … get documented for free.
  • Session → { scopes } principalprincipalFromSession maps a Better Auth session (role, granted scopes, 2FA state, org memberships) to the { scopes } shape @suluk/hono's emitV4(routes, { principal }) uses to project the doc each viewer is allowed to see. verifyApiKey produces the same shape for API-key callers, so enforcement is identical for sessions and keys.
  • Thin Hono mount + auth-flow plumbingmountAuth routes /api/auth/* to the Better Auth handler; helpers cover open-redirect-safe redirects, frictionless email verification, a GDPR erasure cascade, and a fail-closed role-preview login route.

When to reach for it

Reach for it when your app uses Better Auth and you want auth reflected in the contract — its schemes and its endpoints in the v4 doc — plus a principal that drives per-viewer projection and @suluk/hono access enforcement. The session→principal shape is the seam many other packages key off; this is where it's produced.

Don't reach for it to render the doc (that's @suluk/reference / @suluk/scalar / @suluk/swagger) or to build routes from entities (that's @suluk/builder / @suluk/drizzle). This package's job ends at handing those layers a contract that already knows about auth, and a principal to scope it.

Usage

Reflect auth into the contract

The common case: derive the schemes, ingest Better Auth's own OpenAPI, and merge both into the app doc.

import { authSecuritySchemes, ingestAuthOpenAPI, mergeAuth } from "@suluk/better-auth";
import { auth } from "./auth"; // your betterAuth({ … }) instance, with the openAPI() plugin enabled

// 1. auth methods → v4 securitySchemes
const { securitySchemes } = authSecuritySchemes({ session: true, bearer: true, apiKey: true });

// 2. ingest Better Auth's own OpenAPI 3.0 surface, lifted to v4 and prefixed under its mount base
const authSchema = await auth.api.generateOpenAPISchema(); // OpenAPI 3.0
const authV4 = ingestAuthOpenAPI(authSchema, { basePath: "/api/auth" });

// 3. fold auth routes + schemes into your app's v4 document
const document = mergeAuth(appDocument, authV4, { securitySchemes });
// → document.paths now has "api/auth/sign-up/email", …; components.securitySchemes has sessionCookie/bearerAuth/apiKey

authSecuritySchemes also reports session-based plugins that add no new wire scheme but gate into the session — { twoFactor, passkey, organization } — via the returned plugins field.

Close the per-viewer loop (session → principal)

import { principalFromSession } from "@suluk/better-auth";
import { emitV4 } from "@suluk/hono";

const session = await auth.api.getSession({ headers: req.headers });
const principal = principalFromSession(session, {
  roleScopes: { admin: ["read:*", "write:*"], user: ["read:self"] },
});

// @suluk/hono projects only the operations this viewer's scopes allow
const { document } = emitV4(routes, { principal });

principalFromSession also encodes plugin state as scopes: a 2FA-cleared session gains mfa:verified (the exported MFA_SCOPE), and each org membership contributes org:<id>:<scope> scopes (build/parse them with orgScope / parseOrgScope) — so 2FA and tenancy gate through the same scope check @suluk/hono already runs.

Scope-aware API-key verification

Give API-key callers the same { scopes } principal as sessions, so @suluk/hono's createGuard / enforceAccess works for keys too:

import { verifyApiKey } from "@suluk/better-auth";

const result = await verifyApiKey(auth.api, key, { requireScopes: ["orders:read"] });
if (!result.ok) {
  // result.reason is "invalid" | "insufficient_scope" | "error"
  return new Response("unauthorized", { status: 401 });
}
result.principal; // { scopes: ["orders:read", …] } — feed this to @suluk/hono
result.key;       // { id, userId, name, metadata } — metadata parsed past Better Auth's double-stringification

scopesToPermissions / permissionsToScopes convert between flat "cart:read" scopes and Better Auth's { cart: ["read"] } permission shape.

Mount the handler on Hono

import { mountAuth } from "@suluk/better-auth";
import { Hono } from "hono";

const app = new Hono();
mountAuth(app, auth); // routes POST/GET /api/auth/* → auth.handler(c.req.raw)

Auth-flow UX: redirects + frictionless verification

import {
  resolveRedirectTo, withRedirectTo, isSafeRelativePath, emailVerificationConfig,
} from "@suluk/better-auth";

resolveRedirectTo(location.search);        // read ?redirectTo, honored only if same-origin-relative (open-redirect-safe)
withRedirectTo("/login", "/dashboard");    // "/login?redirectTo=%2Fdashboard" — return here post-auth

// spread into betterAuth({ emailVerification: … }) — verify-on-sign-up + auto-sign-in after, by default
const emailVerification = emailVerificationConfig({
  sendVerificationEmail: async ({ user, url }) => { await sendVerifyEmail(user.email, url); },
});

GDPR erasure cascade

beforeDeleteCascade builds Better Auth's user.deleteUser.beforeDelete hook from an ordered list of steps. The package orchestrates; you supply the steps and pick the posture (deleteStep hard-deletes, anonymizeStep scrubs PII, step is generic). Fail-closed by default — a failed cleanup aborts the delete so no rows are orphaned.

import { betterAuth } from "better-auth";
import { beforeDeleteCascade, deleteStep } from "@suluk/better-auth";

const steps = [
  deleteStep("orders", (user) => db.delete(orders).where(eq(orders.customerId, user.id)).run()),
  deleteStep("apiTokens", (user) => db.delete(apiToken).where(eq(apiToken.userId, user.id)).run()),
];

betterAuth({
  user: { deleteUser: { enabled: true, beforeDelete: beforeDeleteCascade(steps) } },
});

Role-preview login (the one credentialed surface)

previewLoginHandler mints a role-scoped session for the seeded demo user, for the generated preview Worker — fail-closed behind two independent locks (env.SULUK_PREVIEW === "1" and an env.PREVIEW_DB binding; absence of either ⇒ 404). The role comes from a server-side allow-list, never a client header.

import { previewLoginHandler } from "@suluk/better-auth";

// inside the preview Worker, for GET /preview/login?role=…
return previewLoginHandler(request, env, {
  allowedRoles: ["user", "admin"],          // from the contract's preview roles, NEVER hardcoded loosely
  mintSession: (role) => mintDemoSession(role), // binds to a seeded throwaway demo user for `role`
});

API

| Export | What it does | | --- | --- | | authSecuritySchemes(methods) | Auth methods → v4 securitySchemes + the enabled session-plugins (AuthMethodsAuthSecurity). | | ingestAuthOpenAPI(schema30, opts?) | Normalize Better Auth's OpenAPI 3.0 to 2020-12 and lift it to a v4 document. | | normalizeOas30(node) | Rewrite 3.0 Schema dialect (nullable, boolean exclusiveMin/Max) into JSON Schema 2020-12. | | mergeAuth(app, auth, extra?) | Deep-merge auth paths + schemas + securitySchemes into the app's v4 document. | | principalFromSession(session, opts?) | Better Auth session → { scopes } Principal (role, scopes, MFA, org). | | MFA_SCOPE, orgScope, parseOrgScope | The mfa:verified scope + build/parse org:<id>:<scope> tenancy scopes. | | verifyApiKey(verifier, key, opts?) | Verify an API key (optionally requireScopes) → the same { scopes } Principal. | | scopesToPermissions / permissionsToScopes | Convert between flat "a:b" scopes and Better Auth's { a: ["b"] } permissions. | | parseApiKeyMetadata(raw) | Parse key metadata past Better Auth's double-stringification quirk. | | beforeDeleteCascade(steps, opts?) | Build the beforeDelete hook from an ordered, fail-closed erasure cascade. | | step / anonymizeStep / deleteStep | Cascade-step constructors (generic / scrub-PII / hard-delete). | | isSafeRelativePath / resolveRedirectTo / withRedirectTo | Open-redirect-safe redirectTo preservation. | | emailVerificationConfig(opts) | A Better Auth emailVerification block with frictionless-activation defaults. | | mountAuth(app, auth, opts?) | Mount the Better Auth handler onto a Hono app under basePath/*. | | previewLoginHandler(req, env, opts) / isPreviewRuntime(env) | Fail-closed role-preview login + its two-lock gate check. |

Boundary

This package reflects and reads Better Auth — it never becomes the auth runtime. Better Auth owns sessions, storage, and the credential flows; Suluk turns its configuration into a v4 contract and a scope-bearing principal. The seams are injected, not assumed: mountAuth / verifyApiKey are duck-typed against auth.handler / auth.api (no hard better-auth or hono import); beforeDeleteCascade takes the steps that touch your db; previewLoginHandler takes the mintSession that owns the session lookup. The session→principal shape is a stable contract many other @suluk/* packages depend on — extend the method→scheme and session→scope mappings, but keep that shape steady. Rendering the resulting doc, and hosting the auth itself, stay outside this line.

License

Apache-2.0