@ezstart/auth-sdk
v2.1.1
Published
EZAuth React SDK for authentication
Readme
@ezstart/auth-sdk
Drop-in authentication SDK for React + Express apps with email/password, OAuth, 2FA, sessions, RBAC, and API key management.
Install
npm install @ezstart/auth-sdk
# Peer deps for the components entry: react, react-dom, sonner, @ezstart/uiArchitecture note
auth-sdk is independent from pay-sdk: neither package imports the other, and each service keeps its own key registry. ezauth exposes RS256 tokens verifiable through JWKS/OIDC (planned, Phase R); shared API-key code moves to the neutral @ezstart/keys package (planned, Phase R). See .claude/design/decoupled-ezauth-ezpay.md in the monorepo.
Quickstart — React with components
The fastest path: wrap your app in <AuthProvider> and drop in pre-built UI.
'use client'
import { AuthProvider } from '@ezstart/auth-sdk'
import { LoginButton, UserMenu } from '@ezstart/auth-sdk/components'
export default function App({ children }: { children: React.ReactNode }) {
return (
<AuthProvider apiUrl="https://api.example.com" appName="myapp" authMode="httpOnly">
<header>
<UserMenu fallback={<LoginButton>Sign in</LoginButton>} />
</header>
{children}
</AuthProvider>
)
}For a Next.js App Router setup with zero login flash on first paint, resolve
the user server-side via getServerAuth() and pass it as initialUser:
// app/[locale]/layout.tsx — Server Component
import { headers } from 'next/headers'
import { getServerAuth } from '@ezstart/auth-sdk/server'
import { Providers } from '@/components/providers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const cookieHeader = (await headers()).get('cookie') ?? undefined
const initialUser = await getServerAuth({
apiUrl: process.env.NEXT_PUBLIC_AUTH_API_URL!,
cookieHeader,
})
return <Providers initialUser={initialUser}>{children}</Providers>
}// components/providers.tsx — Client Component
'use client'
import { AuthProvider, type AuthUser } from '@ezstart/auth-sdk'
export function Providers({
children,
initialUser,
}: {
children: React.ReactNode
initialUser: AuthUser | null
}) {
return (
<AuthProvider
apiUrl={process.env.NEXT_PUBLIC_AUTH_API_URL!}
appName="myapp"
authMode="httpOnly"
initialUser={initialUser}
>
{children}
</AuthProvider>
)
}See examples/nextjs-minimal for a complete starter.
Quickstart — React hooks only
If you want full UI control with the auth state hook (no @ezstart/ui peer dep):
'use client'
import { AuthProvider, useAuth } from '@ezstart/auth-sdk/react'
function Header() {
const { user, isAuthenticated, login, logout } = useAuth()
if (!isAuthenticated) {
return <button onClick={() => login({ email, password })}>Sign in</button>
}
return (
<>
<span>Hello, {user.username}</span>
<button onClick={logout}>Sign out</button>
</>
)
}
export default function App({ children }: { children: React.ReactNode }) {
return (
<AuthProvider apiUrl="https://api.example.com" appName="myapp" authMode="httpOnly">
<Header />
{children}
</AuthProvider>
)
}Keeping the session in sync
'use client'
import { useAuth, useSessionRetry } from '@ezstart/auth-sdk/react'
import { apiCall } from '@ezstart/api-sdk'
function EnableTwoFactorDone() {
const { revalidateSession } = useAuth()
// After a change YOU made server-side, re-sync the store (and the other tabs).
return <button onClick={() => revalidateSession({ fresh: true })}>Refresh my session</button>
}
function useAdminStats() {
const withSession = useSessionRetry()
return () =>
withSession(() =>
apiCall('/admin/stats', { appName: 'myapp', baseUrl: 'https://api.example.com' })
)
}Automatic triggers need no code: tab focus, guards before they deny, and the SDK 2FA / email flows. fresh: true also posts a signal-only USER_UPDATED on the existing BroadcastChannel so other tabs re-fetch (they never trust the payload).
Quickstart — Core only
For Vue, Svelte, vanilla JS, Node, or React Native — zero framework deps:
import { createCoreAuthClient } from '@ezstart/auth-sdk/core'
const client = createCoreAuthClient({
apiUrl: 'https://api.example.com',
appName: 'myapp',
})
const user = await client.loginWithCookie('[email protected]', 'password')
const me = await client.getCurrentUser()
await client.logout()See examples/vanilla-standalone for a complete browser starter.
API
Core (@ezstart/auth-sdk/core)
createCoreAuthClient(config)— Factory for the framework-agnostic client. Returnslogin,logout,refresh,getCurrentUser,updateProfile, etc.AuthError— Typed error class withcodeandstatus. The client now threads the server'serror.codeintoAuthError.codeso consumers canswitchon it instead of matching error strings.EMAIL_VERIFICATION_REQUIRED/isEmailVerificationRequiredError(err)— Constant + type guard for the403returned by therequireEmailVerifiedserver gate on privileged routes. Use the guard to surface a "verify your email" prompt (<EmailVerificationBanner>/ resend CTA) instead of a generic error.TWO_FACTOR_REQUIRED/isTwoFactorRequiredError(err)— Constant + duck-typed guard ({ status: 403, code }, matchesAuthErrorand@ezstart/api-sdkerrors) for the403returned by therequireTwoFactorserver gate when an admin has no enrolled 2FA. Use it to prompt enrollment.try { await apiCall('/admin/stats', { appName: 'myapp' }) } catch (err) { if (isTwoFactorRequiredError(err)) promptEnrollment() }TokenManager— Pluggable token persistence interface.createLocalStorage()/createMemoryStorage()— Built-in storage backends.
React (@ezstart/auth-sdk/react)
<AuthProvider>— Context provider with auto-refresh. Owns the per-tree Zustand store. AcceptsinitialUserfor SSR bootstrap (no login flash).useAuth()— Main hook returning{ user, isAuthenticated, login, logout, ... }.useAuthStore(selector?)— Bound hook reading the per-Provider Zustand store.useAuthStoreApi()— Returns the store instance for imperativegetState()/setState()access inside the React tree.useAuthStoreGetSnapshot()— Stable() => statereader for closures passed to non-React APIs.useAuth().revalidateSession(options?)— Re-reads the session from the server (GET /me) and updates the store so rights changed server-side (2FA enabled/disabled, role granted/revoked, email verified) reach the client without a re-login, in both directions. Deduplicated, throttled viamaxAgeMs, never rejects (returns{ status, changed, previous, user }); a network/5xx failure keeps local state, a dead session (401 + failed silent refresh) signs out locally. Runs automatically on tab focus (>= 30 s apart), before<RequireTwoFactor>/<RequireEmailVerified>/<RequireRole>deny access, and after the SDK's own 2FA / verify-email / email-change flows. Call it with{ fresh: true }after your own server-side change.withSessionRetry(revalidate, request)/useSessionRetry()— wrap an API call: on403 TWO_FACTOR_REQUIREDthe session is revalidated once and the call retried once if the server confirms the user is now enrolled. Wired into the admin analytics / error-log hooks; wrap your own calls the same way.useRevalidateSession()— Returns the Provider'srevalidateSessionfunction (same one asuseAuth().revalidateSession), for components that only need to revalidate. Throws outside<AuthProvider>.const revalidateSession = useRevalidateSession() const { status, user } = await revalidateSession({ fresh: true })useRevalidateBeforeDeny(shouldDeny)— Guard helper: whileshouldDenyis true, revalidates the session once per mount and returnstrueuntil it settled, so a custom guard shows a loader instead of flashing "denied" on a stale flag.const checking = useRevalidateBeforeDeny(!hasAccess) // hasAccess: your own, possibly stale, flag if (checking) return <Loader />createSessionRevalidator({ store, client, logger })/DEFAULT_REVALIDATE_MAX_AGE_MS(30 000 ms) — Advanced, non-React factory behindrevalidateSession(deduplicated, throttled, never rejects).<AuthProvider>already creates one; use the factory only for a custom store. Pass{ maxAgeMs: DEFAULT_REVALIDATE_MAX_AGE_MS }to throttle your own automatic triggers.const revalidate = createSessionRevalidator({ store, client, logger }) await revalidate({ maxAgeMs: DEFAULT_REVALIDATE_MAX_AGE_MS })<RequireAuth>— Route protection wrapper. Auto-redirects to/{locale}/login?redirect_uri=...by default; acceptsloginPath,redirectTo, orfallbackComponent.<SignedIn>/<SignedOut>— Conditional rendering for partial UI (no redirect).<AccessDenied>— Default fallback component for unauthorized access.
Components (@ezstart/auth-sdk/components)
Auth flows
<SignInForm>,<SignUpForm>,<QuickSignUpForm>— Login and registration forms with OAuth + 2FA support.<ForgotPasswordForm>,<ResetPasswordForm>— Password recovery.<TwoFactorPrompt>,<TwoFactorSettings>— TOTP enrollment and challenge.<VerifyEmailFlow>,<EmailVerificationStatus>— Email verification flow + inline status.<MagicLinkButton>— Passwordless email login.<EmailChangeForm>— Change email with verification + cooldown.<OAuthButtons>,<OAuthProvidersSection>— OAuth sign-in buttons + connected providers manager.<LoginButton>,<RegisterButton>— Header CTAs with theme handoff (?theme=propagation).<AuthCallbackPage>— OAuth/SSO callback handler page.
User account
<AccountModal>— Clerk-like account management modal.<UserMenu>,<UserAvatar>— Header dropdown + avatar with initials fallback.<UserSettings>,<UserDashboard>— Settings and dashboard pages.<SessionsManager>— Active sessions list with revoke.<DeleteAccountSection>— Soft-delete with confirmation + grace period.<AuditLogSection>— User activity log.
Admin
<AuthAdminDashboard>— All-in-one admin console (Overview / Users / Applications / Settings tabs). Auto-scoped server-side via JWT (req.derivedScope): superadmin sees all tenants, app admin sees owned Applications, regular user sees own account.<MaintenanceBanner>— Maintenance window status banner.
Developer (API keys)
<DeveloperPortal>— Full developer dashboard (keys CRUD + usage).<ApiKeysTable>,<CreateKeyModal>,<KeyCreatedModal>— Key management primitives.<UsageDetailsModal>,<UsageBadge>— Per-key usage drill-down.
Applications (multi-tenant)
<ApplicationsList>,<ApplicationCard>— Owner's Applications list.<CreateApplicationModal>— New Application wizard.<ApplicationDetailView>— Application settings + theme + keys.
Server (@ezstart/auth-sdk/server)
Server-only helpers (import 'server-only' guards). Safe to call from any Server Component or Route Handler.
getServerAuth({ apiUrl, cookieHeader })— Resolve current user from the session cookie. Returnsnullfor anonymous requests.getServerApiKeys({ apiUrl, cookieHeader, applicationId? })— List API keys for the current user, or for an Application (?applicationId=, owner / superadmin). PassapplicationIdwhenever the result seeds a scoped<DeveloperPortal applicationId>/<ApplicationDetailView>; a seed loaded for another scope shows the wrong list.getServerApplication({ apiUrl, cookieHeader, id })— Fetch a single Application.getServerApplications({ apiUrl, cookieHeader })— List Applications owned by the current user.getServerAuditLog({ apiUrl, cookieHeader })— Fetch audit log entries.hasFeature(user, flag)— Server-side feature flag check.
Middleware (@ezstart/auth-sdk/middleware)
createAuthMiddleware(config)— Express middleware that accepts JWT cookie + Bearer +X-API-Keyon the same routes. Bundles JWT verification, API-key lookup, monthly quota cache, scope policy, and best-effort tracking.
import { createAuthMiddleware } from '@ezstart/auth-sdk/server'
const authJwtOrKey = createAuthMiddleware({
appName: 'myapp',
jwtSecret: process.env.JWT_SECRET!,
cookieName: 'access_token',
getApiKeyModel,
getApiKeyUsageModel,
getAuthUserModel,
})
router.get('/me', authJwtOrKey(), controller)
router.get('/admin/users', authJwtOrKey({ requireKeyScope: 'admin' }), controller)Create
createAuthMiddlewareonce per application (module-level export). Quota reservations and the monthly usage cache live in the factory closure; one instance per request or route gives each its own counters and a monthly quota can be exceeded.
API keys (moved to @ezstart/keys)
Publish order:
@ezstart/keysis a runtime dependency of this SDK and must be published first. Legacyezk_*keys are no longer accepted (401).
Key-format helpers (generateRawApiKey, hashApiKey, detectKeyFormat, extractKeyPrefix), createApiKeyMiddleware, createApiKeySchema and createApiKeyUsageSchema now live in @ezstart/keys. The previous exports from @ezstart/auth-sdk, /core and /server still work but are deprecated and will be removed in the next major version:
// before
import { createApiKeyMiddleware } from '@ezstart/auth-sdk/server'
// after
import { createApiKeyMiddleware } from '@ezstart/keys/server'RBAC (@ezstart/auth-sdk/rbac)
hasRole(user, role)/hasGlobalRole(user, role)— Role checks.requireRole(role),requireAdmin(),requireSuperadmin()— Express middleware factories.<RequireRole>— React guard component.
i18n — texts prop pattern
The components layer is agnostic of any i18n library. All user-facing strings are accepted via a texts?: Partial<...Texts> prop with English defaults baked in. Locale is auto-detected from the URL pathname (/fr/login → 'fr').
import { SignInForm } from '@ezstart/auth-sdk/components'
;<SignInForm
appName="myapp"
texts={{
emailOrUsername: 'Email or username',
password: 'Password',
submit: 'Continue',
}}
/>Bundled defaults: en, fr, vi. Pass locale="fr" to force a specific language without overriding texts.
Hosted vs Embedded auth UX
Two patterns supported:
Pattern A — Hosted login (redirect to a hosted EZAuth-style page): drop <LoginButton> + <AuthCallbackPage>. Zero auth UI code, central security, cross-app SSO trivial.
Pattern B — Embedded forms (stay on your domain): build your own /login, /register, etc. pages and drop in <SignInForm>, <SignUpForm>, etc. 100% your branding, no redirect.
You can mix both in the same app.
Configuration safety
<AuthProvider> fails fast with AuthError({ code: 'CONFIG_ERROR' }) instead of silently falling back to localhost when run off-localhost. Throws when:
- No URL signals (off-localhost, no
apiUrl, nopublishableKey, nofirstParty: true). publishableKeywithoutapiUrloff-localhost.firstParty: truewithout an explicitappNameoff-localhost.webUrlresolves tolocalhostoff-localhost (usually meansNEXT_PUBLIC_AUTH_WEB_URLenv is missing).
Localhost (localhost, *.localhost, 127.0.0.1, 0.0.0.0, ::1) keeps permissive defaults so zero-config dev still works.
Roadmap
@ezstart/auth-sdk ships a complete MVP for email/password + OAuth Google + 2FA + sessions + RBAC + API keys management.
- Magic Link (passwordless email login) — shipped.
- Passkey / WebAuthn — planned.
- Additional OAuth providers (GitHub, Discord, Microsoft, Apple) — planned.
- GDPR data export — planned.
Migration from 1.x
2.0.0 removes the Application webhook UI (WebhookSecretSection, useRegenerateWebhookSecret, the "Webhooks" tab of ApplicationDetailView, tabWebhooks and the webhook* text keys). Webhooks are an ezpay feature. Remove those keys from any texts override; nothing else changes. Publish order: @ezstart/api-contracts 2.0.0, then @ezstart/auth-sdk 2.0.0.
Related
@ezstart/keys— API key format, hashing, schemas and middleware (extracted from this package).@ezstart/api-sdk— HTTP client primitives consumed internally.@ezstart/api-contracts— wire-level request/response schemas.@ezstart/ui— UI primitives consumed by thecomponents/entry.@ezstart/pay-sdk— Companion payments SDK with the same 3-layer architecture.
