@playstack/auth
Framework-independent identity and opaque session primitives for Node and Workers.
Free. MIT licensed. Check preview availability before installing. See package access.
@playstack/auth owns framework-independent identities, credentials, and opaque session behavior for Node and Workers. It deliberately does not own accounts, authorization, device identity, or MFA.
Install
npm install @playstack/auth @playstack/core @playstack/crypto @playstack/eventsInstall the optional argon2 peer only in the Node process that hashes passwords.
Compose sessions and credentials
import { createAuthAttemptLimiter, createCredentialService, databaseSessions } from '@playstack/auth'
import { argon2id } from '@playstack/auth/node'
const sessions = databaseSessions({
persistence,
crypto: applicationCrypto,
events,
clock,
ids,
deviceValidator: devices,
requireDevice: true,
reusePolicy: 'all-user-sessions',
})
const credentials = createCredentialService({
persistence,
crypto: applicationCrypto,
passwordHasher: argon2id(),
breachChecker,
limiter: createAuthAttemptLimiter(rateLimiter),
delivery,
stepUp: mfa,
events,
clock,
ids,
dummyPasswordHash: process.env.AUTH_DUMMY_PASSWORD_HASH!,
})The same persistence instance may implement both the session and credential contracts. Transactional event handlers must use the same transactionKind so state and Tier 1 events commit atomically.
Session configuration
DatabaseSessionOptions property | Required | Purpose |
|---|---|---|
persistence | Yes | Transactional session and refresh-token state. |
crypto | Yes | Random tokens, SHA-256 digests, and replacement-token encryption. |
events | Yes | Typed auth event emitter. |
clock, ids | Yes | Application-owned time and ID generation. |
deviceValidator | No | Rejects inactive device-backed sessions. |
requireDevice | No | Requires a device ID on issue; defaults to false. |
accessTtlMs | No | Access lifetime; defaults to 15 minutes. |
refreshTtlMs | No | Refresh lifetime; defaults to 30 days. |
refreshGraceMs | No | Parallel-refresh replacement grace; defaults to 30 seconds. |
reusePolicy | No | Reuse response: session-family by default or all-user-sessions. |
Refresh tokens rotate under a persistence lock. Reuse after the grace window revokes the token family and session.
Session security and maintenance
const active = await sessions.listSessionsForUser(userId)
await sessions.revokeSessionForUser(userId, active[0].id)
const maintenance = createAuthMaintenance({
persistence,
clock,
consumedRetentionMs: 7 * 24 * 60 * 60_000,
})
await maintenance.purgeExpired({ limit: 250 })Session activity exposes IDs, creation and last-use timestamps, expiry, account/client/device references, and bounded IP and user-agent metadata. It never exposes access or refresh credentials. Maintenance is bounded and scheduler-neutral; one call removes at most 1,000 expired or retained consumed records across sessions, refresh records, one-time tokens, authorization codes, email changes, and staff sessions.
Credential configuration
CredentialServiceOptions property | Required | Bridge responsibility |
|---|---|---|
persistence | Yes | Users, identities, one-time credentials, pending email changes, and sessions. |
crypto | Yes | Opaque token generation, hashing, and encryption. |
passwordHasher | Yes | Password-grade hash and verify operations. |
breachChecker | Yes | Checks candidate passwords against the application’s breach source. |
limiter | Yes | Fails closed for identifier-plus-IP auth attempts. |
delivery | Yes | Sends verification, reset, magic-link, and email-change notifications. |
stepUp | Yes | Verifies a recent-auth or MFA proof for sensitive changes. |
events, clock, ids | Yes | Typed events and deterministic primitives. |
dummyPasswordHash | Yes | Valid hash verified for unknown users to reduce timing disclosure. |
tokenTtlMs | No | One-time token lifetime, 15–60 minutes; defaults to 30 minutes. |
Primary bridge contracts
| Contract | Key members |
|---|---|
AuthPersistence | Transaction, session lookup/save/revoke, refresh-family locking and rotation. |
CredentialPersistence | Extends auth persistence with user, identity, one-time token, and email-change records. |
AuthCrypto | randomToken, digestSha256, encrypt, decryptText; structurally compatible with @playstack/crypto. |
PasswordHasher | hash(password) and verify(hash, password). |
PasswordBreachChecker | isBreached(password). |
AuthAttemptLimiter | consume(operation, { identifier, ip }); returns allowed and optional retryAt. |
AuthDelivery | Token delivery plus password- and email-change notifications. |
StepUpVerifier | verify(userId, proof); MfaService implements it structurally. |
DeviceSessionValidator | assertActive(userId, deviceId). |
CredentialService.authenticateProfile(profile, context?) is the entry point
for a provider profile already normalized by Auth.js or another framework.
authenticateProvider(provider, code, context?) performs the provider exchange
and delegates to the same method, so linking and verified-email policy cannot
drift between paths.
The transactional auth.user.registered event lets @playstack/accounts
provision an initial account and owner membership in the same database unit of
work. Delivery and initial session issuance remain post-commit application
orchestration.
registerPassword() also accepts an explicit transaction context. Use createUnitOfWork() from @playstack/events when auth, accounts, profiles, and audit records must share one application-owned database transaction. Persistence adapters must declare the same transaction kind and physical handle; mismatches fail closed.
Website sign-in with IndieAuth
Use the optional server-side IndieAuth client when visitors should sign in with their own website URL. It verifies the website's authorization server and hands the confirmed URL to the existing credential/session system; it does not host an IndieAuth authorization server or request Micropub access tokens.
import {
createIndieAuthClient,
createIndieAuthBrowserBinding,
createMemoryIndieAuthAttemptStore,
} from '@playstack/auth/indieauth'
import { createNodeIndieAuthTransport } from '@playstack/auth/indieauth/node'
const indieauth = createIndieAuthClient({
clientId: 'https://your-site.example/indieauth-client',
redirectUri: 'https://your-site.example/auth/indieauth/callback',
attempts: createMemoryIndieAuthAttemptStore(), // single process only
transport: createNodeIndieAuthTransport(),
})
// After rate limiting and CSRF validation on your sign-in POST:
const browserBinding = createIndieAuthBrowserBinding()
const { authorizationUrl } = await indieauth.begin({ me: websiteUrl, browserBinding })
// Set a Secure/HttpOnly/SameSite=Lax/Path=/ host-only binding cookie;
// then redirect the browser to authorizationUrl.
// In the configured callback, read browserBinding from that cookie:
const profile = await indieauth.complete({ callbackUrl, browserBinding })
const user = await credentials.authenticateProfile(profile)
// Continue required MFA/account checks and your existing session flow.Serve indieauth.clientMetadata as JSON at clientId. Configure the credential
service with providerIdentityPolicy: { linking: 'explicit', email: 'optional' }.
Only the confirmed website URL identifies the visitor. Provider email, profile
attributes and tokens are discarded; first-contact email enrollment and identity
linking require separate verified flows. An existing account is never selected
merely because a provider claims its email address.
Attempts use S256 PKCE, browser binding, expiry and atomic single-use consumption.
Multi-process/serverless deployments must supply a shared IndieAuthAttemptStore
with atomic binding/expiry checks and consumption; a replayable cookie alone or
get-then-delete storage is insufficient. Missing/duplicate binding cookies must
fail. Keep login/callback responses private and uncached, suppress referrers,
and redact callback query strings and credentials from logs. Uncertain exchanges
require fresh sign-in, never an automatic code retry.
The supported profile uses public HTTPS websites and current server-metadata
discovery with S256 and iss. HTTP/private/local endpoints and endpoint-only
discovery are not supported. The Node transport pins public DNS answers and
bounds redirects, headers, bodies and deadlines. Workers require an explicit
equivalent safe transport; ordinary global fetch does not establish SSRF safety.
The installed package's README includes the full setup, storage contract, limits and error policy. Qualify your provider, browser cookies, proxy configuration, shared storage and session/MFA flow before launch. The adapter itself adds no managed schema or application routes.
Realtime composition
Issue a purpose-bound ticket from authenticated HTTP, then redeem it in the WebSocket hello frame:
import { createRealtimeTicketService } from '@playstack/auth'
const tickets = createRealtimeTicketService({ sessions, crypto: applicationCrypto })
const ticket = await tickets.issue(accessToken)
const session = await tickets.redeem(ticket)Tickets default to 30 seconds and may be configured from 1–60 seconds. Single-use safety depends on the atomic replay store configured in @playstack/crypto. Redemption re-resolves the session, so a newly revoked session cannot open a connection.
Web and persistence adapters
sessionCookie,clearSessionCookie, andassertCsrfenforce the v1 cookie/CSRF transport invariants without choosing a framework.@playstack/auth-contractspublishes the JSON-safe session contract independently so a React or Next client can migrate before the server implementation.parseAuthSessionViewvalidates the narrow JSON-safe client projection and rejects unknown or credential-shaped fields.@playstack/auth/prismaprovidesprismaAuthPersistence(prisma)and the managed, non-ejectable Prisma fragment.@playstack/auth/testingprovides in-memory persistence and crypto fakes.
Applications can register a validated extensions envelope with createAuthSessionViewParser() and createAuthSessionProjector(). The same parser must be used by server projection, React transport, account client, and Next helpers so product-specific account summaries or capabilities remain typed end to end. Tokens and provider credentials never belong in this envelope.
Migrate an established application
createMigratingPasswordHasher() verifies an explicitly configured legacy hash such as bcrypt, then rehashes with the current Argon2id policy under a compare-and-swap write. createLegacySessionExchange() provides an absolute, bounded window for exchanging an existing JWT or refresh session into ordinary Playstack opaque credentials.
Existing schemas may satisfy managed artifacts through exact model and field mappings. When physical storage differs, omit the artifact and implement application-owned AuthPersistence or CredentialPersistence; that adapter owns identifier conversion, dual reads, shadow writes, and record migration. The CLI can scaffold typed method stubs and contract-test entry points without declaring a semantically similar schema compatible.
Register sessions.deviceRevocationHandler() for devices.device.revoked and sessions.mfaChangeHandler() for MFA enrollment and disable events when those packages share the same transaction kind.
Boundary
Auth does not load secrets, choose HTTP routes, own account membership or authorization, identify devices, or decide MFA policy. Add React, Next.js, NestJS, accounts, devices, and MFA packages only at application edges that need them.
Explicit identity and replay policies
providerIdentityPolicy: { linking: 'explicit', email: 'optional' } supports email-less provider users without silently linking a matching email. Trusted server normalization and step-up-backed identity linking remain required. The compatible default is verified-email linking with required email. Safe user/session projections and registration-event email may be null; first-contact email enrollment remains an application-owned verified workflow.
New provider users emit the same transactional registration event as password signup. Required account/audit provisioning must join the actual transaction, not a post-login callback.
refreshReplayPolicy: 'strict' rejects every consumed-token replay without returning replacement material. The default bounded-grace policy retains encrypted replacement credentials for a bounded interval. reusePolicy: 'all-user-sessions' separately widens revocation scope. Strict mode rejects refreshGraceMs and requires shared client refresh coordination.
Authorization-code consumption and session issuance can commit together through required transaction-aware issuance/authorization seams with account context. A committed code cannot be replayed to recover lost credentials. Optional device authorization follows the same one-time boundary. See Client authentication.
Optional session lifetime and admission policies
databaseSessions() now accepts idleTtlMs, absoluteTtlMs and maxActiveSessions; omitted options preserve existing defaults.
- Idle expiry is inclusive and measured from successful verify, resolve or refresh. Background requests count as activity and verification adds locked activity writes; this is not automatically human-idle detection.
- Absolute lifetime starts at original issuance, survives refresh/account switching and clips token expiry. It cannot exceed
refreshTtlMs. - Per-user caps reject new issuance with
playstack/auth/session_limit_exceeded, never silently evict devices. Refreshable sessions count even after access expiry; caps require an explicit absolute lifetime.
Prisma supplies transaction-scoped creation locks and locked reads. Application persistence must implement lockSessionCreationForUser for caps and findSessionByIdForUpdate for idle expiry, with real cross-instance correctness. Missing seams fail configuration; no-op locks are not sufficient. All issuers must use the same policy/lock contract.
Map authenticated cap failures to a deliberate host response while retaining enumeration-safe unauthenticated errors. Strict/bounded-grace refresh replay remains independent; cookie transport, SSO, native coordination and deployment policy changes require application acceptance tests.
API entry points and requirements
Reference snapshot: @playstack/auth@0.1.0-beta.1. Import only the entry point your runtime needs. Paths below are relative to the installed package; use Go to Definition in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.
| Public entry point | Declaration file |
|---|---|
@playstack/auth | ./dist/index.d.ts |
@playstack/auth/errors | ./dist/errors.d.ts |
@playstack/auth/indieauth | ./dist/indieauth.d.ts |
@playstack/auth/indieauth/node | ./dist/indieauth-node.d.ts |
@playstack/auth/authorization | ./dist/authorization.d.ts |
@playstack/auth/device | ./dist/device-authorization.d.ts |
@playstack/auth/device-prisma | ./dist/device-prisma.d.ts |
@playstack/auth/device-testing | ./dist/device-testing.d.ts |
@playstack/auth/passwords | ./dist/passwords.d.ts |
@playstack/auth/node | ./dist/node.d.ts |
@playstack/auth/prisma | ./dist/prisma.d.ts |
@playstack/auth/testing | ./dist/testing.d.ts |
@playstack/auth/playstack.artifacts.json | No TypeScript declaration (asset or metadata export). |
@playstack/auth/package.json | No TypeScript declaration (asset or metadata export). |
Node.js engine requirement: >=20. This is not a claim that every entry point works in browsers or Workers.
Peer dependencies
Keep existing framework versions that satisfy these ranges. Install optional peers only when using the corresponding adapter. The package manager resolves ordinary dependencies separately.
| Peer | Compatible range | When needed |
|---|---|---|
argon2 | >=0.44.0 <1 | Optional; only for the entry points that use it. |
For a complete first program, start with Getting started. For API lookup and partial-example conventions, see Reading the reference. Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.