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

@santi020k/auth-cloudflare

v0.5.0

Published

Single-owner and multi-user Better Auth policy for Hono applications on Cloudflare Workers and D1.

Readme

@santi020k/auth-cloudflare

Email-code and passkey authentication policy for Hono applications running on Cloudflare Workers and D1. Choose a single configured owner or let the consuming application approve multiple identities. Better Auth owns the authentication protocol and persistence implementation; this package owns the shared Santiago policy.

The package intentionally does not share sessions, cookies, passkeys, databases, secrets, or relying-party IDs between applications. Every consumer supplies a unique origin, cookie prefix, secret, D1 binding, and email delivery callback.

When the authentication Worker is hosted separately from the browser application, baseURL is the public Worker URL and browserOrigin is the single exact browser origin allowed to make unsafe requests and register passkeys. Omitting browserOrigin keeps the same-origin default.

Current status

This package is already public. Version 0.4 preserves its v0.3 policy options and compatibility subpaths and prepares the seven split packages for public release. Publication does not replace consumer rollout validation: an application must still prove its own D1 migration, compatibility route, session cutover, and browser passkey flow. See the repository's release process.

v0.3 compatibility

Existing consumers may continue using applicationOrigin and authServerURL; they are compatibility aliases for browserOrigin and baseURL. Versioned secrets with an optional legacySecret, configurable emailOtpRateLimit, and the original two-field AuthSessionIdentity assignment contract also remain supported. Newly resolved sessions include optional typed lifecycle fields (authenticatedAt, expiresAt, and sessionId). New code should use browserOrigin and baseURL; providing a legacy and replacement origin option with different values fails closed.

Version 0.4 intentionally expands three pre-1.0 TypeScript contracts. Code that manually constructs an AuthPolicy must use resolveOwnerAuthPolicy or resolveMultiUserAuthPolicy so the new baseURL, origin, and tableNames fields are present. Test doubles typed as OwnerAuthInstance or MultiUserAuthInstance must add the session inventory, revocation, emergency-lockout, and rate-limit pruning methods. Their resolveSession implementation must return the lifecycle fields in ResolvedAuthSessionIdentity. Runtime consumers that call the factory functions require no adapter.

Policy

  • Email OTPs are six digits, expire after ten minutes, allow five verification attempts, and are stored hashed.
  • Rate limits are enabled in every environment and use D1 rather than per-isolate memory.
  • A D1-backed pre-authentication limit also bounds rejected POST requests per Cloudflare client IP and auth path before email admission; Better Auth's stricter endpoint limits still apply to requests that continue into its handler.
  • Only the configured owner or identities approved by the consumer may create or update an identity.
  • Unauthorized email-code requests receive the same success-shaped response without sending mail.
  • Unsafe requests require the exact configured browser Origin header.
  • Passkeys require discoverable credentials and user verification.
  • Optional social providers use the same consumer email-admission policy; provider credentials remain consumer-owned.
  • Optional Cloudflare Turnstile protection defaults to the email-code request endpoint.
  • The WebAuthn relying-party ID is always the exact application hostname.
  • Production cookies are secure and remain scoped to the authentication API hostname.

Example

const ownerAuth = createOwnerAuth({
  appName: "Example owner workspace",
  baseURL: "https://planner.example.com",
  cookiePrefix: "example-owner",
  database: env.DB,
  ownerEmail: env.OWNER_EMAIL,
  secret: env.AUTH_SECRET,
  sendVerificationOTP: ({ email, otp }) => sendLoginCode(env, email, otp),
  waitUntil: (task) => context.waitUntil(task),
});

app.all("/api/auth/*", (context) => ownerAuth.handler(context.req.raw));

For multiple accounts, keep membership and roles in the consuming application and provide a current access decision:

const auth = createMultiUserAuth({
  appName: "Example team workspace",
  authorizeEmail: async (email) => Boolean(await findActiveMember(env.DB, email)),
  baseURL: "https://workspace.example.com",
  cookiePrefix: "example-members",
  database: env.DB,
  secret: env.AUTH_SECRET,
  sendVerificationOTP: ({ email, otp }) => sendLoginCode(env, email, otp),
  waitUntil: (task) => context.executionCtx.waitUntil(task),
});

For a split web/API deployment, keep the two security boundaries explicit:

const ownerAuth = createOwnerAuth({
  appName: "Example control room",
  baseURL: "https://api.example.com",
  browserOrigin: "https://control.example.com",
  cookiePrefix: "example-control-owner",
  database: env.DB,
  ownerEmail: env.OWNER_EMAIL,
  secret: env.AUTH_SECRET,
  sendVerificationOTP: ({ email, otp }) => sendLoginCode(env, email, otp),
  waitUntil: (task) => context.executionCtx.waitUntil(task),
});

The API must still return credentialed CORS headers for that exact browser origin. browserOrigin does not accept an origin list and does not create a shared cookie domain.

waitUntil is required because code delivery runs outside the response path to keep approved and rejected addresses indistinguishable even when the provider fails. On Workers, pass context.executionCtx.waitUntil; do not substitute an untracked promise that the runtime may terminate after sending the response.

Set tablePrefix when the consumer database already contains generic tables such as user or session. Generate its app-owned migration with the same prefix through @santi020k/auth-migrations; a prefix is part of the persistent storage contract and must not be changed after deployment.

authorizeEmail receives a normalized email address and is checked during code requests, identity writes, code delivery, and session resolution. A member rejected after signing in no longer resolves as authenticated. The package does not own roles, invitations, organizations, or cross-application identity state.

Consumers may pass Better Auth socialProviders for approved OAuth providers. The package checks the fresh provider email before provisioning, linking, or OAuth sign-in, so OAuth cannot bypass ownerEmail or authorizeEmail. Account-linking UX, scopes, callback routes, and provider credentials remain consumer-owned.

Public code-request surfaces can enable Turnstile through the same server policy:

const auth = createMultiUserAuth({
  // ...application-owned configuration
  turnstile: {
    allowedHostnames: ["workspace.example.com"],
    expectedAction: "request-login-code",
    secretKey: env.TURNSTILE_SECRET_KEY,
  },
});

The browser supplies Better Auth's x-captcha-response header. Site and secret keys must not be shared across products.

Cloudflare Workers must enable nodejs_compat (or the narrower nodejs_als flag when no other Node compatibility is needed). Each application must generate and review its own Better Auth core and passkey migration; the package does not silently create or mutate production tables.

D1 rate-limit counters are persistent. Call auth.pruneRateLimits() from the consuming application's scheduled maintenance to remove counters older than the default 24-hour retention window. A consumer may provide a positive retentionSeconds override and deterministic now value for testing. Cleanup is explicit and never changes schemas or runs implicitly inside an authentication request.

Session operations and security events

The configured instance provides server-side listSessions(userId), revokeSession(userId, sessionId), revokeAllSessions(userId), and emergencyLockout(userId) operations. Inventory entries contain ISO timestamps and device metadata but never session tokens, and expired sessions are omitted. The explicit userId scope prevents a session ID from revoking another account's session. Consumers must authenticate and authorize their own management routes before calling these methods; this package deliberately does not define roles, recovery, or administrative policy.

Supply onSecurityEvent to forward authentication events into the consumer's established audit or alerting system:

const auth = createMultiUserAuth({
  // ...application-owned configuration
  onSecurityEvent: (event) => auditSecurityEvent(event),
});

Events cover blocked origins or credentials, pre-authentication rate limiting, suppressed and failed email-code delivery, code requests, session creation, scoped revocation, revoke-all, and explicit emergency lockout. Listener failures are isolated from the auth flow. Event payloads intentionally omit codes, cookies, session tokens, secrets, client IPs, and request bodies.

Resolved identities include authenticatedAt and expiresAt ISO timestamps. Use isRecentAuthentication or the Hono recent-authentication middleware for sensitive actions. The application still chooses the step-up method and owns the authorization decision.