@khalidadil/auth-manager (0.2.0)
Installation
@khalidadil:registry=npm install @khalidadil/auth-manager@0.2.0"@khalidadil/auth-manager": "0.2.0"About this package
auth-manager
Magic-link customer auth, Google SSO admin auth, sessions, and an address book for the apparel-designer storefront.
Status:
v0.1.0-alpha.0— first release. Tracks the v0.8 sub-plan inProject - Apparel Designer/v08_Auth_v1.md.
Login: codes-only OTP (preferred) vs magic links (legacy)
Use email one-time CODES, not magic links. Email-security scanners (Outlook SafeLinks etc.) pre-open links in messages, which consumes a single-use magic link before the human ever clicks it. Typed 6-digit codes can't be consumed by a scanner. The magic-link routes remain for backward compatibility with existing apparel-designer wiring, but new hosts should use the code flow.
Codes-only endpoints (added 2026-09-02):
| Route | Body | Effect |
|---|---|---|
POST ${apiBase}/auth/customer/request-code |
{ email, next? } |
emails a 6-digit code (10-min TTL, one live code per email); always 200 (no enumeration) |
POST ${apiBase}/auth/customer/verify-code |
{ email, code } |
verifies + mints the session cookie; JSON response { ok, next } — no redirect, no clickable link; wrong codes burn attempts (5 → locked); IP-rate-limited |
Security: codes are stored as SHA-256(email:code) (never plaintext), compared
timing-safe, single-use, attempt-limited, and short-lived. Sessions, cookies,
and rate limiting are the module's existing infrastructure (unchanged).
What's in here
| Layer | Concerns |
|---|---|
| Server (Express middleware) | mountAuth(app, {...}), migration runner, session middleware, route handlers, in-memory rate limiter |
| Database (Postgres) | customers, admins, magic_links, sessions, addresses |
| Client (React) | <AuthProvider>, <LoginForm>, <AccountMenu>, <AccountShell>, <AccountAddressesPage>, <AdminLoginPage>, <RequireAdmin>, plus useCustomer / useAdmin / useAddresses hooks |
Install (from the host)
// apparel-designer/package.json
"dependencies": {
"@khalidadil/auth-manager": "^0.2.0",
"pg": "^8.13.0"
}
Published to the Gitea registry: npm config set @khalidadil:registry https://git.kadil.dev/api/packages/khalidadil/npm/ then a normal npm install. Local development can still use "@khalidadil/auth-manager": "file:../auth-manager" (the key must be the scoped name).
Server wiring
import { Pool } from 'pg';
import { mountAuth, runMigrations } from '@khalidadil/auth-manager/server';
const pgPool = new Pool({ connectionString: process.env.DATABASE_URL });
// Run migrations at boot. Idempotent — safe to invoke on every start.
await runMigrations(pgPool, { logger });
const { authMiddleware, requireCustomer, requireAdmin } = mountAuth(app, {
db: pgPool,
apiBase: '/api',
callbackBaseUrl: process.env.PUBLIC_BASE_URL, // e.g. 'http://localhost:3001'
smtp: process.env.SMTP_HOST ? {
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
from: process.env.SMTP_FROM,
} : undefined,
fromAddress: process.env.SMTP_FROM || 'no-reply@example.com',
appName: 'Pawfectly Yours',
googleClientId: process.env.GOOGLE_CLIENT_ID,
googleClientSecret: process.env.GOOGLE_CLIENT_SECRET,
adminEmails: (process.env.ADMIN_EMAILS || '').split(',').map((s) => s.trim()).filter(Boolean),
cookieDomain: process.env.COOKIE_DOMAIN, // optional
isProduction: process.env.NODE_ENV === 'production',
logger,
});
// Hand requireCustomer / requireAdmin to other modules at their mount time.
// mountCommerce(app, { requireAuth: { customer: requireCustomer, admin: requireAdmin } });
Client wiring
// main.jsx
import { AuthProvider } from '@khalidadil/auth-manager';
import '@khalidadil/auth-manager/styles.css';
createRoot(...).render(
<AuthProvider apiBase="/api">
<App />
</AuthProvider>,
);
// AppRouter.jsx
import {
LoginForm, AccountShell, AccountHomePage, AccountAddressesPage,
AdminLoginPage, RequireAdmin,
} from '@khalidadil/auth-manager';
<Routes>
<Route path="/auth/login" element={<LoginForm next={location.search.get('next')} />} />
<Route path="/admin/login" element={<AdminLoginPage />} />
<Route path="/account" element={<AccountShell />}>
<Route index element={<AccountHomePage />} />
<Route path="addresses" element={<AccountAddressesPage />} />
{/* /account/designs from goods-editor-module, /account/orders from commerce-module */}
</Route>
<Route path="/admin/*" element={<RequireAdmin>{/* admin shell */}</RequireAdmin>} />
</Routes>
// Anywhere you want an account chip
import { AccountMenu } from '@khalidadil/auth-manager';
<AccountMenu />
Required environment variables
| Name | Required | Notes |
|---|---|---|
DATABASE_URL |
Yes | Postgres 13+ connection string. PG17 recommended. |
PUBLIC_BASE_URL |
Yes | Used to build magic-link URLs + the OAuth redirect URI. Must match what's registered in Google Cloud Console (e.g. http://localhost:3001 for dev). |
GOOGLE_CLIENT_ID |
Admin SSO | OAuth 2.0 Web client. |
GOOGLE_CLIENT_SECRET |
Admin SSO | Same client. |
ADMIN_EMAILS |
Admin SSO | Comma-separated allowlist. Plan recommends 2+ entries for break-glass recovery. |
SMTP_HOST |
Optional | If unset, magic links print to console only. |
SMTP_PORT |
If SMTP | e.g. 587 or 1025 (Mailpit). |
SMTP_USER |
If SMTP | If your relay needs auth. |
SMTP_PASS |
If SMTP | |
SMTP_FROM |
If SMTP | From: header. Defaults to SMTP_USER or no-reply@example.com. |
COOKIE_DOMAIN |
Optional | e.g. .yourdomain.com to share sessions with the WP checkout sidecar. Usually leave empty. |
Email delivery in v1
Per the v0.8 sub-plan (updated 2026-05-26): the original plan called for Resend with React Email templates. For the first alpha we're using a simpler approach:
- The magic link is ALWAYS printed to stdout with a loud banner — enough to develop and test without any provider account.
- If
SMTP_HOSTis set, it's also sent via SMTP using nodemailer. Useful for Mailpit / Mailcatcher / Mailtrap during dev, or a real relay for staging. - Resend is deferred to v2. When adopted,
createEmailSendergrows aprovider: 'resend'branch; the interface stays the same.
Cookie names
| Cookie | Purpose |
|---|---|
pawfectly_customer_session |
Customer session token. HttpOnly, Secure in prod, SameSite=Lax, 90-day TTL with rolling renewal. |
pawfectly_admin_session |
Admin session token. Same flags, 24-hour TTL (admins re-auth more often). |
pawfectly_oauth_state |
PKCE state during Google SSO dance. 10-min TTL. |
pawfectly_oauth_verifier |
PKCE code verifier. 10-min TTL. |
Migration
The migrations/ directory contains 001_init.sql, which creates all five tables plus a schema_migrations tracker. The runMigrations(pool) helper reads the directory, skips applied migrations, and applies new ones in lexical order. Idempotent — safe on every boot.
To run migrations manually outside the host (debugging):
psql $DATABASE_URL -f migrations/001_init.sql
Build
npm install
npm run build # client lib + server bundle
v2 deferred (per sub-plan)
Tracked in v08_Auth_v1.md §v2: account merge, 2FA, email change, account deletion, Redis-backed rate limiting, WebAuthn, session list + revocation, admin audit log, Resend integration.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @oslojs/crypto | ^1.0.1 |
| @oslojs/encoding | ^1.1.0 |
| arctic | ^2.3.0 |
| cookie | ^0.7.2 |
| nodemailer | ^6.9.16 |
Development Dependencies
| ID | Version |
|---|---|
| @rollup/plugin-commonjs | ^28.0.0 |
| @rollup/plugin-json | ^6.1.0 |
| @rollup/plugin-node-resolve | ^15.3.0 |
| express | ^4.21.0 |
| pg | ^8.13.0 |
| react | ^19.2.5 |
| react-dom | ^19.2.5 |
| react-router-dom | ^7.1.0 |
| rollup | ^4.30.0 |
| rollup-plugin-esbuild | ^6.1.0 |
| rollup-plugin-postcss | ^4.0.2 |
Peer Dependencies
| ID | Version |
|---|---|
| express | >=4.18.0 |
| pg | >=8.11.0 |
| react | >=18.0.0 |
| react-dom | >=18.0.0 |
| react-router-dom | >=6.0.0 |