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

@aswincloud/auth

v0.5.0

Published

Shared, framework-agnostic auth primitives for Cloudflare Workers sites (Web Crypto sessions, signed tokens, password hashing, OAuth) plus an optional React login UI. Each consuming site brings its own D1 database and secrets; nothing links sites together

Readme

@aswincloud/auth

npm CI license: MIT zero deps

Drop-in auth for Cloudflare Workers sites. Write the login plumbing once, reuse it across every site — each keeping its own database, fully isolated.

Shared, framework-agnostic auth primitives for Cloudflare Workers sites — signed sessions, HMAC tokens, PBKDF2 password hashing, owner allowlists, and OAuth (Google / GitHub / Microsoft) — plus an optional React login UI.

Each consuming site brings its own D1 database and its own secrets. Nothing in this package links sites together. Security rests on your per-site SESSION_SECRET, not on this code being private (it's public on purpose).

Install

npm install @aswincloud/auth

New site? Each gets its OWN database

This package never shares a database between sites — every site you build gets its own isolated D1, so nothing is linked. Setting one up is one command:

# creates the D1, prints the binding to paste into wrangler.jsonc, applies the schema
npx aswincloud-auth-setup-db mysite-db

Or do it by hand:

npx wrangler d1 create mysite-db          # → copy the database_id it prints
# paste into wrangler.jsonc:
#   "d1_databases": [{ "binding": "DB", "database_name": "mysite-db",
#                      "database_id": "<id>" }]
npx wrangler d1 execute mysite-db --remote \
  --file=node_modules/@aswincloud/auth/schema.sql

schema.sql creates users, oauth_identities, and otp_codes — all IF NOT EXISTS, so re-running is safe, and you add your own app tables alongside them in your own migrations. Then set per-site secrets (SESSION_SECRET, OAuth client ids).

Two entry points

// Core — pure Web Crypto, zero deps, runs on any Worker.
import {
  hashPassword, verifyPassword,
  signToken, verifyToken,
  createSessionCookie, readSession, clearSessionCookie,
  isOwner,
} from "@aswincloud/auth";

// Optional React login UI — only multi-user sites need this.
import { LoginPage } from "@aswincloud/auth/react";

Owner-only Worker sites import only the core and never pull React in.

Owner-only site (status / console style)

const session = { secret: env.SESSION_SECRET, cookieName: "sess" };

// after verifying the user (e.g. Google OAuth) and checking the allowlist:
if (!isOwner(env.OWNER_EMAILS, email)) return new Response("forbidden", { status: 403 });
const setCookie = await createSessionCookie(session, email);

// gate a request:
const who = await readSession(session, req);
if (!who) return Response.redirect(new URL("/api/auth/login", req.url), 302);

Multi-user site (shiptrack style)

const hash = await hashPassword(plainPassword);          // on signup -> store in your D1
const ok = await verifyPassword(plainPassword, hash);    // on login
const setCookie = await createSessionCookie(session, userId);

OAuth — Google, GitHub, Microsoft

One config drives all three providers. Configure only the ones you have credentials for; configuredProviders() tells the UI which buttons to show.

import {
  startOAuth, handleOAuthCallback, configuredProviders, isOwner,
  createSessionCookie,
} from "@aswincloud/auth";

const origin = new URL(req.url).origin;
const oauth = {
  clients: {
    google:    { clientId: env.GOOGLE_CLIENT_ID,    clientSecret: env.GOOGLE_CLIENT_SECRET },
    github:    { clientId: env.GITHUB_CLIENT_ID,    clientSecret: env.GITHUB_CLIENT_SECRET },
    microsoft: { clientId: env.MICROSOFT_CLIENT_ID, clientSecret: env.MICROSOFT_CLIENT_SECRET,
                 tenantId: env.MICROSOFT_TENANT_ID }, // optional, defaults to "common"
  },
  stateSecret: env.STATE_SECRET,
  redirectUri: (p) => `${origin}/api/auth/oauth/${p}/callback`,
};

// GET /api/auth/oauth/:provider/start
return startOAuth(oauth, provider);   // 302 to provider + signed CSRF state cookie

// GET /api/auth/oauth/:provider/callback
const r = await handleOAuthCallback(oauth, provider, req);
if (!r.ok) return Response.redirect(`${origin}/login?oauth_error=${r.error}`, 302);
// r.user = { providerUserId, email, emailVerified } — verified by the provider.

// Owner-only site: gate on the allowlist.
if (!isOwner(env.OWNER_EMAILS, r.user.email)) return new Response("forbidden", { status: 403 });
const setCookie = await createSessionCookie(session, r.user.email);

// Multi-user site: look up / create the user in YOUR D1, then issue the session.
// The package never touches a database — each site keeps its own, fully isolated.

Who can sign in — access policy

emailAllowed decides whether a provider-verified email may sign in, in one of three modes a site picks (e.g. from an ACCESS_MODE env):

import { emailAllowed } from "@aswincloud/auth";

// "public"  → any authenticated email
// "domain"  → only emails in ACCESS_DOMAINS (owners, if any, also allowed)
// "owners"  → only emails in the OWNER_EMAILS allowlist (strict "only me")
if (!emailAllowed({
  mode: env.ACCESS_MODE,          // "public" | "domain" | "owners"
  email: r.user.email,
  owners: env.OWNER_EMAILS,       // comma-separated
  domains: env.ACCESS_DOMAINS,    // comma-separated, e.g. "aswincloud.com"
})) return new Response("forbidden", { status: 403 });

parseAccessMode(raw) normalizes the env string (unknown ⇒ "owners", the safe default). Pure and zero-dep.

Central OAuth broker (relay)

For many sites that shouldn't each register their own OAuth client, a central broker authenticates with the provider once and relays the verified email back to each site, signed with a per-site shared secret (RELAY_SECRET):

import { signRelay, verifyRelay } from "@aswincloud/auth";

// On the broker, after handleOAuthCallback succeeds:
const relay = await signRelay(site.relaySecret, { email, provider, nonce, providerUserId });
// → 302 back to the site with ?relay=<relay>
// providerUserId is optional: owner-only sites can omit it (token shape is then
// unchanged); multi-user sites link accounts on (provider, providerUserId).

// On the site's callback:
const claims = await verifyRelay(env.RELAY_SECRET, relayToken); // {email,provider,nonce,providerUserId?} | null
if (!claims || claims.nonce !== expectedNonce) return forbidden();

Short-lived (2 min) and purpose-bound; the nonce (echoed from the site's start request) defends against replay. The broker Worker itself lives outside this package; these are just the shared sign/verify helpers.

Provider notes: Google & Microsoft return verified emails; GitHub falls back to /user/emails for the primary verified address. Microsoft uses your tenant in the URL. CSRF is the HMAC-signed state cookie (checked against the returned state param). Zero-dependency, pure fetch + Web Crypto.

SSO buttons (React)

import { SsoButtons } from "@aswincloud/auth/react";

// providers usually comes from configuredProviders(oauth) on the server
<SsoButtons providers={["google", "github", "microsoft"]} />

Renders <a href="/api/auth/oauth/{provider}/start"> buttons with real provider logos. Drop it into <LoginPage ssoSlot={...} /> to get SSO + password on one screen.

React login UI

import { LoginPage } from "@aswincloud/auth/react";

<LoginPage
  action="/api/auth/login"
  onSuccess={() => location.assign("/dashboard")}
  signupHref="/signup"
  forgotHref="/forgot"
/>;

Framework-agnostic — no next/* imports, so it runs on Next, Vite, or plain React. Self-contained default styling; every element overridable via styles. Navigation is yours via onSuccess. Drop SSO buttons in through ssoSlot.

User-management flows — @aswincloud/auth/d1

A separate, opt-in entry point with DB-backed flows for multi-user sites: signup + OTP verify, password reset, change password/username, self-service email change, and account removal. The core stays zero-dep — only import /d1 where you need it.

Flows are functions, not HTTP handlers: each takes your D1 binding + plain values and returns { ok: true, … } | { ok: false, error: "<code>" } — never throws. You own the routing, the session cookie, and the email provider.

import {
  signup, verifyOtp, requestPasswordReset, resetPassword,
  changePassword, requestEmailChange, confirmEmailChange, removeUser,
  type EmailSender,
} from "@aswincloud/auth/d1";
import { createSessionCookie } from "@aswincloud/auth";

// You inject the email provider — the package never hardcodes one:
const sendEmail: EmailSender = async ({ to, subject, html, text }) => {
  await fetch("https://api.resend.com/emails", {
    method: "POST",
    headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({ from: env.MAIL_FROM, to, subject, html, text }),
  });
};

// POST /api/auth/forgot
await requestPasswordReset(env.DB, {
  email, secret: env.TOKEN_SECRET, sendEmail, appUrl: origin,
}); // always { ok: true } — no account enumeration

// POST /api/auth/reset
const r = await resetPassword(env.DB, { token, newPassword, secret: env.TOKEN_SECRET });
if (!r.ok) return json({ error: r.error }, 400);

// POST /api/auth/verify  → issue the session yourself on success
const v = await verifyOtp(env.DB, { email, code, secret: env.TOKEN_SECRET });
if (v.ok) setCookie(await createSessionCookie(session, v.userId));

Pure email templates ship too (passwordResetEmail, verifyEmail, otpEmail, emailChangeEmail, accountDeletedEmail) returning { subject, html, text } — use them or write your own. Matching React pages: ForgotPasswordPage, ResetPasswordPage, VerifyEmailPage from @aswincloud/auth/react.

Branded emails — to keep your own look instead of the built-in template, pass a render* override to the sending flows; it gets the dynamic bits and returns { subject, html, text }:

await signup(env.DB, { email, password, secret, sendEmail,
  renderOtp: ({ code, ttlMinutes }) => myOtpEmail({ code, ttlMinutes }) });
// likewise: requestPasswordReset → renderReset({ resetUrl, ttlHours }),
//           requestEmailChange   → renderEmailChange({ confirmUrl, newEmail, ttlHours }).

When omitted, the built-in template is used (app name via appName).

Schema (0.2.0): the users table gained name + is_admin. New sites get them from schema.sql. Existing DBs (created before 0.2.0) — run once:

ALTER TABLE users ADD COLUMN name TEXT;
ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0;

Sites using only the core primitives (no /d1) need no migration.

License

MIT