@warlock.js/auth 5.17.1 → 5.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/esm/contracts/index.d.mts +1 -1
  3. package/esm/contracts/types.d.mts +23 -2
  4. package/esm/contracts/types.d.mts.map +1 -1
  5. package/esm/contracts/types.mjs.map +1 -1
  6. package/esm/index.d.mts +3 -2
  7. package/esm/index.mjs +2 -1
  8. package/esm/middleware/auth.middleware.d.mts +9 -6
  9. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  10. package/esm/middleware/auth.middleware.mjs +101 -80
  11. package/esm/middleware/auth.middleware.mjs.map +1 -1
  12. package/esm/models/access-token/access-token.model.d.mts +31 -7
  13. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  14. package/esm/models/access-token/access-token.model.mjs +21 -1
  15. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  16. package/esm/models/access-token/family-association.migration.mjs +18 -0
  17. package/esm/models/access-token/family-association.migration.mjs.map +1 -0
  18. package/esm/models/auth-token-family/auth-token-family.model.d.mts +90 -0
  19. package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -0
  20. package/esm/models/auth-token-family/auth-token-family.model.mjs +80 -0
  21. package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -0
  22. package/esm/models/auth-token-family/index.d.mts +1 -0
  23. package/esm/models/auth-token-family/index.mjs +3 -0
  24. package/esm/models/auth-token-family/migration.mjs +25 -0
  25. package/esm/models/auth-token-family/migration.mjs.map +1 -0
  26. package/esm/models/index.d.mts +1 -0
  27. package/esm/models/index.d.mts.map +1 -1
  28. package/esm/models/index.mjs +8 -0
  29. package/esm/models/index.mjs.map +1 -1
  30. package/esm/models/refresh-token/refresh-token.model.d.mts +43 -0
  31. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  32. package/esm/models/refresh-token/refresh-token.model.mjs +17 -0
  33. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  34. package/esm/models/refresh-token/successor-association.migration.mjs +22 -0
  35. package/esm/models/refresh-token/successor-association.migration.mjs.map +1 -0
  36. package/esm/services/auth.service.d.mts +22 -1
  37. package/esm/services/auth.service.d.mts.map +1 -1
  38. package/esm/services/auth.service.mjs +216 -32
  39. package/esm/services/auth.service.mjs.map +1 -1
  40. package/esm/services/token-family-operation.mjs +74 -0
  41. package/esm/services/token-family-operation.mjs.map +1 -0
  42. package/llms-full.txt +63 -58
  43. package/llms.txt +9 -9
  44. package/package.json +8 -8
  45. package/skills/auth-basics/SKILL.md +3 -2
  46. package/skills/customize-token-storage/SKILL.md +1 -1
  47. package/skills/customize-user-type/SKILL.md +1 -1
  48. package/skills/handle-login-and-logout/SKILL.md +1 -1
  49. package/skills/login-with-providers/SKILL.md +1 -1
  50. package/skills/manage-tokens/SKILL.md +19 -6
  51. package/skills/overview/SKILL.md +3 -3
  52. package/skills/protect-routes/SKILL.md +30 -39
  53. package/skills/register-user/SKILL.md +1 -1
  54. package/skills/run-auth-commands/SKILL.md +1 -1
  55. package/skills/throttle-login-attempts/SKILL.md +1 -1
  56. package/skills/verify-email-and-reset-password/SKILL.md +1 -1
package/llms.txt CHANGED
@@ -6,15 +6,15 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [auth-basics](@warlock.js/auth/auth-basics/SKILL.md): Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.
10
- - [customize-token-storage](@warlock.js/auth/customize-token-storage/SKILL.md): Override the persisted AccessToken / RefreshToken models to add columns (multi-tenant `organization_id`, device metadata), rename, or change storage — without forking the package. Extend the model + `schema.extend(...)`, register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`, override `issue()` to populate the new column, and add a migration. Triggers: `accessToken.model`, `refreshToken.model`, `AccessToken.issue`, `RefreshToken.issue`, `accessTokenSchema`, `refreshTokenSchema`, "add a column to the token table", "multi-tenant tokens", "organization_id on access token", "override the token model", "custom token storage"; typical import `import { AccessToken, accessTokenSchema } from "@warlock.js/auth"`. Skip: multiple user TYPES (not token storage) — `@warlock.js/auth/customize-user-type/SKILL.md`; the token lifecycle API — `@warlock.js/auth/manage-tokens/SKILL.md`; the config blocks themselves — `@warlock.js/auth/auth-basics/SKILL.md`.
11
- - [customize-user-type](@warlock.js/auth/customize-user-type/SKILL.md): Support multiple user types (user / admin / client / staff) in one auth system — each Auth subclass overrides userType, config.auth.userType.<slug> maps slug to model class, authMiddleware('admin') gates per type. Triggers: `Auth`, `userType`, `config.auth.userType`, `Authenticable`, `@RegisterModel`, `confirmPassword`; "add admins and users", "multiple user types", "separate client and vendor personas", "per-type login"; typical import `import { Auth } from "@warlock.js/auth"`. Skip: `authMiddleware` semantics — `@warlock.js/auth/protect-routes/SKILL.md`; login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; RBAC libs `casl`, `accesscontrol`, `rbac`.
12
- - [handle-login-and-logout](@warlock.js/auth/handle-login-and-logout/SKILL.md): Run the full login flow via authService.login(Model, credentials, deviceInfo?) — verify password, create access + refresh token pair, fire events. Logout via authService.logout(user, accessToken?, refreshToken?) revokes tokens. Triggers: `authService.login`, `authService.logout`, `authService.attemptLogin`, `authService.refreshTokens`, `authService.revokeAllTokens`, `authEvents`; "build a login endpoint", "POST /login controller", "logout from all devices", "verify credentials and issue tokens"; typical import `import { authService, authEvents } from "@warlock.js/auth"`. Skip: token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; sign-up — `@warlock.js/auth/register-user/SKILL.md`; competing libs `passport-local`, `next-auth` credentials.
13
- - [login-with-providers](@warlock.js/auth/login-with-providers/SKILL.md): Log in without a password — Google (OIDC code + PKCE + state + nonce, id_token verified with `jose`), GitHub, Discord, LinkedIn, Apple (`form_post` callback), Facebook and X, passkeys (WebAuthn via `@simplewebauthn/server`) and phone one-time codes (delivered through a `@warlock.js/notifications` channel). Every method ends in `authService.completeLogin(user)`, the same tokens, rows and events as password login. `startProviderLogin`, `completeProviderLogin`, `generatePasskeyRegistrationOptions`, `verifyPasskeyRegistration`, `generatePasskeyAuthenticationOptions`, `verifyPasskeyAuthentication`, `requestOtp`, `verifyOtp`, `otpRequestThrottleMiddleware`, `otpVerifyThrottleMiddleware`, `AuthProvider`. Triggers: "login with Google", "sign in with Google", "login with GitHub", "Sign in with Apple", "login with Discord", "login with LinkedIn", "login with Facebook", "login with X", `auth.providers.github`, "OAuth", "OIDC", "passkeys", "WebAuthn", "passwordless", "SMS code login", "WhatsApp OTP", "phone login", `warlock add auth-google`, `warlock add auth-passkeys`, `AuthProviderSdkMissingError`, `InvalidProviderCallbackError`, `ProviderEmailNotVerifiedError`, `InvalidPasskeyError`, `EC009`, `EC010`, `EC011`, `auth.providers`, `auth.passkeys`, `auth.otp`; typical import `import { completeProviderLogin, verifyOtp } from "@warlock.js/auth"`. Skip: password login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; email tokens — `@warlock.js/auth/verify-email-and-reset-password/SKILL.md`; defining an SMS channel — `@warlock.js/notifications/define-channel/SKILL.md`.
9
+ - [auth-basics](@warlock.js/auth/auth-basics/SKILL.md): Start here for @warlock.js/auth configuration, user models, credentials, and application session flows.
10
+ - [customize-token-storage](@warlock.js/auth/customize-token-storage/SKILL.md): Customize token storage in @warlock.js/auth; use when you need to customize token storage.
11
+ - [customize-user-type](@warlock.js/auth/customize-user-type/SKILL.md): Customize user type (multi-user-type auth) in @warlock.js/auth; use when you need to customize user type.
12
+ - [handle-login-and-logout](@warlock.js/auth/handle-login-and-logout/SKILL.md): Login + logout in @warlock.js/auth; use when you need to handle login and logout.
13
+ - [login-with-providers](@warlock.js/auth/login-with-providers/SKILL.md): Login with an OAuth provider, passkeys, or a phone code in @warlock.js/auth; use when you need to login with providers.
14
14
  - [manage-tokens](@warlock.js/auth/manage-tokens/SKILL.md): (no description)
15
15
  - [overview](@warlock.js/auth/overview/SKILL.md): (no description)
16
- - [protect-routes](@warlock.js/auth/protect-routes/SKILL.md): Gate HTTP routes via authMiddleware(allowedUserType) — the argument is required and a valid token is always required: [] allows any authenticated user, a user-type restricts to those types. Sets request.locals.user + request.decodedAccessToken on success, 401 on failure. Triggers: `authMiddleware`, `request.locals.user`, `request.decodedAccessToken`, `AuthErrorCodes`, `MissingAccessToken`, `InvalidAccessToken`; "how do I protect a route", "restrict route by user type", "require any logged-in user"; typical import `import { authMiddleware } from "@warlock.js/auth"`. Skip: multi-user-type config — `@warlock.js/auth/customize-user-type/SKILL.md`; issuing the token — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `express-jwt`, `next-auth` middleware.
17
- - [register-user](@warlock.js/auth/register-user/SKILL.md): Sign up a new user and issue the initial token pair — User.create({...password: await hashPassword(plain)}) then authService.createTokenPair(user). Triggers: `User.create`, `hashPassword`, `verifyPassword`, `authService.createTokenPair`, `toJsonColumns`, `strongPassword`, `authEvents`; "build a register endpoint", "POST /register controller", "sign up a new user", "hash password on signup", "email verification flow"; typical import `import { authService } from "@warlock.js/auth"; import { hashPassword } from "@warlock.js/core"`. Skip: login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; competing libs `bcrypt`, `bcryptjs`, `argon2`.
18
- - [run-auth-commands](@warlock.js/auth/run-auth-commands/SKILL.md): Three bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env), warlock auth.cleanup (removes expired refresh tokens), and warlock auth.purge-never-expiring (finds and revokes token rows that can never expire). Register via registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand() and registerAuthPurgeNeverExpiringCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `registerAuthPurgeNeverExpiringCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `warlock auth.purge-never-expiring`, `cleanupExpiredTokens`, `purgeNeverExpiringTokens`, `command`; "token never expires", "no exp claim", "Invalid Date expires_at"; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.
16
+ - [protect-routes](@warlock.js/auth/protect-routes/SKILL.md): Gate routes with authMiddleware in @warlock.js/auth; use when you need to protect routes.
17
+ - [register-user](@warlock.js/auth/register-user/SKILL.md): Register-and-issue-tokens flow in @warlock.js/auth; use when you need to register user.
18
+ - [run-auth-commands](@warlock.js/auth/run-auth-commands/SKILL.md): Run auth commands in @warlock.js/auth; use when you need to run auth commands.
19
19
  - [throttle-login-attempts](@warlock.js/auth/throttle-login-attempts/SKILL.md): (no description)
20
- - [verify-email-and-reset-password](@warlock.js/auth/verify-email-and-reset-password/SKILL.md): Email verification and password reset with hashed, single-use, expiring, purpose-bound tokens delivered through `@warlock.js/notifications` — `sendEmailVerification(user)`, `verifyEmail(token)`, `requestPasswordReset(Model, email)`, `resetPassword(token, newPassword)`, the `requireVerifiedEmail()` guard, and the `tokenIssueThrottleMiddleware` / `tokenConsumeThrottleMiddleware` presets. Reset revokes every access token, refresh token and cookie session; a reset request answers the same for unknown emails. Triggers: `sendEmailVerification`, `verifyEmail`, `requestPasswordReset`, `resetPassword`, `requireVerifiedEmail`, `isEmailVerified`, `OneTimeToken`, `InvalidOneTimeTokenError`, `EmailNotVerifiedError`, `NotificationsUnavailableError`, `EC007`, `EC008`, `auth.verification`, `auth.passwordReset`; "verify email", "confirm email address", "forgot password", "reset password link", "block unverified users", "resend verification email"; typical import `import { requestPasswordReset, resetPassword } from "@warlock.js/auth"`. Skip: sign-up itself — `@warlock.js/auth/register-user/SKILL.md`; login throttling — `@warlock.js/auth/throttle-login-attempts/SKILL.md`; defining notifications — `@warlock.js/notifications/define-notification/SKILL.md`.
20
+ - [verify-email-and-reset-password](@warlock.js/auth/verify-email-and-reset-password/SKILL.md): Verify email + reset password in @warlock.js/auth; use when you need to verify email and reset password.
package/package.json CHANGED
@@ -12,13 +12,13 @@
12
12
  "ms": "^2.1.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "@warlock.js/fs": "5.17.1",
16
- "@warlock.js/cache": "5.17.1",
17
- "@warlock.js/cascade": "5.17.1",
18
- "@warlock.js/core": "5.17.1",
19
- "@warlock.js/logger": "5.17.1",
20
- "@warlock.js/seal": "5.17.1",
21
- "@warlock.js/notifications": "5.17.1",
15
+ "@warlock.js/fs": "5.19.0",
16
+ "@warlock.js/cache": "5.19.0",
17
+ "@warlock.js/cascade": "5.19.0",
18
+ "@warlock.js/core": "5.19.0",
19
+ "@warlock.js/logger": "5.19.0",
20
+ "@warlock.js/seal": "5.19.0",
21
+ "@warlock.js/notifications": "5.19.0",
22
22
  "@simplewebauthn/server": "^13.1.0",
23
23
  "jose": "^6.1.0"
24
24
  },
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "author": "hassanzohdy",
49
49
  "license": "MIT",
50
- "version": "5.17.1",
50
+ "version": "5.19.0",
51
51
  "type": "module",
52
52
  "main": "./esm/index.mjs",
53
53
  "module": "./esm/index.mjs",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: auth-basics
3
- description: 'Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.'
3
+ description: "Start here for @warlock.js/auth configuration, user models, credentials, and application session flows."
4
4
  ---
5
5
 
6
6
  # Auth basics
@@ -20,7 +20,7 @@ npm install @warlock.js/auth
20
20
  1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
21
21
  2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
22
22
  3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
23
- 4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
23
+ 4. **`authMiddleware` gates or resolves routes.** Use `authMiddleware("user")` for a typed hard gate, `authMiddleware()` for the configured/default user type, or `authMiddleware({ source: "cookie", key: "token", optional: true })` for a public route that receives a valid user when present. The object form can set a page-local login redirect. Legacy `authMiddleware(userType, "cookie:name")` remains valid.
24
24
  5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
25
25
  6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
26
26
  7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
@@ -71,6 +71,7 @@ export default {
71
71
  user: User,
72
72
  // admin: Admin, // for multi-user-type
73
73
  },
74
+ defaultUserType: "user", // needed for authMiddleware() when multiple types exist
74
75
  accessToken: {
75
76
  secret: env("JWT_SECRET"),
76
77
  expiresIn: "1h",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: customize-token-storage
3
- description: 'Override the persisted AccessToken / RefreshToken models to add columns (multi-tenant `organization_id`, device metadata), rename, or change storage — without forking the package. Extend the model + `schema.extend(...)`, register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`, override `issue()` to populate the new column, and add a migration. Triggers: `accessToken.model`, `refreshToken.model`, `AccessToken.issue`, `RefreshToken.issue`, `accessTokenSchema`, `refreshTokenSchema`, "add a column to the token table", "multi-tenant tokens", "organization_id on access token", "override the token model", "custom token storage"; typical import `import { AccessToken, accessTokenSchema } from "@warlock.js/auth"`. Skip: multiple user TYPES (not token storage) — `@warlock.js/auth/customize-user-type/SKILL.md`; the token lifecycle API — `@warlock.js/auth/manage-tokens/SKILL.md`; the config blocks themselves — `@warlock.js/auth/auth-basics/SKILL.md`.'
3
+ description: "Customize token storage in @warlock.js/auth; use when you need to customize token storage."
4
4
  ---
5
5
 
6
6
  # Customize token storage
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: customize-user-type
3
- description: 'Support multiple user types (user / admin / client / staff) in one auth system — each Auth subclass overrides userType, config.auth.userType.<slug> maps slug to model class, authMiddleware(''admin'') gates per type. Triggers: `Auth`, `userType`, `config.auth.userType`, `Authenticable`, `@RegisterModel`, `confirmPassword`; "add admins and users", "multiple user types", "separate client and vendor personas", "per-type login"; typical import `import { Auth } from "@warlock.js/auth"`. Skip: `authMiddleware` semantics — `@warlock.js/auth/protect-routes/SKILL.md`; login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; RBAC libs `casl`, `accesscontrol`, `rbac`.'
3
+ description: "Customize user type (multi-user-type auth) in @warlock.js/auth; use when you need to customize user type."
4
4
  ---
5
5
 
6
6
  # Customize user type (multi-user-type auth)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: handle-login-and-logout
3
- description: 'Run the full login flow via authService.login(Model, credentials, deviceInfo?) — verify password, create access + refresh token pair, fire events. Logout via authService.logout(user, accessToken?, refreshToken?) revokes tokens. Triggers: `authService.login`, `authService.logout`, `authService.attemptLogin`, `authService.refreshTokens`, `authService.revokeAllTokens`, `authEvents`; "build a login endpoint", "POST /login controller", "logout from all devices", "verify credentials and issue tokens"; typical import `import { authService, authEvents } from "@warlock.js/auth"`. Skip: token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; sign-up — `@warlock.js/auth/register-user/SKILL.md`; competing libs `passport-local`, `next-auth` credentials.'
3
+ description: "Login + logout in @warlock.js/auth; use when you need to handle login and logout."
4
4
  ---
5
5
 
6
6
  # Login + logout
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: login-with-providers
3
- description: 'Log in without a password — Google (OIDC code + PKCE + state + nonce, id_token verified with `jose`), GitHub, Discord, LinkedIn, Apple (`form_post` callback), Facebook and X, passkeys (WebAuthn via `@simplewebauthn/server`) and phone one-time codes (delivered through a `@warlock.js/notifications` channel). Every method ends in `authService.completeLogin(user)`, the same tokens, rows and events as password login. `startProviderLogin`, `completeProviderLogin`, `generatePasskeyRegistrationOptions`, `verifyPasskeyRegistration`, `generatePasskeyAuthenticationOptions`, `verifyPasskeyAuthentication`, `requestOtp`, `verifyOtp`, `otpRequestThrottleMiddleware`, `otpVerifyThrottleMiddleware`, `AuthProvider`. Triggers: "login with Google", "sign in with Google", "login with GitHub", "Sign in with Apple", "login with Discord", "login with LinkedIn", "login with Facebook", "login with X", `auth.providers.github`, "OAuth", "OIDC", "passkeys", "WebAuthn", "passwordless", "SMS code login", "WhatsApp OTP", "phone login", `warlock add auth-google`, `warlock add auth-passkeys`, `AuthProviderSdkMissingError`, `InvalidProviderCallbackError`, `ProviderEmailNotVerifiedError`, `InvalidPasskeyError`, `EC009`, `EC010`, `EC011`, `auth.providers`, `auth.passkeys`, `auth.otp`; typical import `import { completeProviderLogin, verifyOtp } from "@warlock.js/auth"`. Skip: password login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; email tokens — `@warlock.js/auth/verify-email-and-reset-password/SKILL.md`; defining an SMS channel — `@warlock.js/notifications/define-channel/SKILL.md`.'
3
+ description: "Login with an OAuth provider, passkeys, or a phone code in @warlock.js/auth; use when you need to login with providers."
4
4
  ---
5
5
 
6
6
  # Login with an OAuth provider, passkeys, or a phone code
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: manage-tokens
3
- description: 'Token lifecycle — generateAccessToken, createRefreshToken, createTokenPair, refreshTokens (with rotation + replay detection), revokeAllTokens, revokeTokenFamily, cleanupExpiredTokens, getActiveSessions. Triggers: `createTokenPair`, `refreshTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`, `jwt.generate`, `jwt.verify`, `AccessToken`, `RefreshToken`; "rotate refresh tokens", "detect token replay", "logout from all devices", "list active sessions", "clean up expired tokens"; typical import `import { authService, jwt } from "@warlock.js/auth"`. Skip: login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; CLI cleanup — `@warlock.js/auth/run-auth-commands/SKILL.md`; competing libs `jsonwebtoken`, `jose`, `fast-jwt`.'
3
+ description: "Manage tokens in @warlock.js/auth; use when you need to manage tokens."
4
4
  ---
5
5
 
6
6
  # Manage tokens
@@ -36,7 +36,20 @@ const pair = await authService.createTokenPair(user, deviceInfo);
36
36
 
37
37
  `createTokenPair` is the typical issuance path. It respects `config.auth.refreshToken.enabled` — if disabled, returns only `accessToken`.
38
38
 
39
- ## Refresh with rotation — `refreshTokens`
39
+ ## Refresh with rotation — `refreshTokens`
40
+
41
+ Refresh and family revocation use durable family revision checks inside a
42
+ serializable Cascade transaction. Run the additive `authMigrations` before
43
+ using the upgraded APIs. Transaction support is required: the service fails
44
+ rather than falling back to uncoordinated writes. MongoDB deployments therefore
45
+ need a transaction-capable topology.
46
+
47
+ An operation that owns its transaction retries serialization conflicts at most
48
+ three times. Inside an application-owned transaction it joins once and leaves
49
+ retry/rollback to the caller. Auth notifications from owned attempts are emitted
50
+ only after successful commit; notifications inside an application-owned outer
51
+ transaction retain their existing timing before that outer commit. Keep external
52
+ side effects outside such a transaction.
40
53
 
41
54
  ```ts
42
55
  const next = await authService.refreshTokens(oldRefreshToken, deviceInfo);
@@ -69,7 +82,7 @@ refresh (B) → revokes B; creates C in family X
69
82
  refresh (A again)→ A is revoked → revoke family X entirely
70
83
  ```
71
84
 
72
- The family ties together "successive rotations of one session." Logout of one device kills only that device's family — other devices keep their own families.
85
+ The family ties together "successive rotations of one session." In 5.19 it is also a durable `AuthTokenFamily` row: family revoke marks that family, revokes its refresh rows, and deletes its associated access rows atomically. Access rows from before family association are conservatively deleted only for the same user and user type; they are never guessed into a family.
73
86
 
74
87
  ## Listing active sessions
75
88
 
@@ -91,9 +104,8 @@ Use this for "active sessions" UIs. Revoke a specific session by calling `.revok
91
104
  // Specific access token
92
105
  await authService.removeAccessToken(user, accessTokenString);
93
106
 
94
- // Specific refresh token (via the RefreshToken instance)
95
- const rt = await RefreshToken.findByToken(refreshString);
96
- await rt?.revoke();
107
+ // Specific session/family
108
+ await authService.revokeTokenFamily(familyId);
97
109
 
98
110
  // All access tokens for a user
99
111
  await authService.removeAllAccessTokens(user);
@@ -175,6 +187,7 @@ The package signs access and refresh tokens with independent secrets — `config
175
187
  - Don't disable rotation (`config.auth.refreshToken.rotation = false`) unless you genuinely understand the tradeoff — you lose replay detection.
176
188
  - Don't increase `maxPerUser` to a huge number "to be safe." Each active refresh token is a revocation surface; fewer simultaneous tokens means less attack surface.
177
189
  - Don't manually delete `AccessToken` rows in a service. The user might be hitting a request mid-revoke and get an inconsistent state. Use the `authService` helpers.
190
+ - Run the additive `authMigrations` on upgrade. Do not alter or backfill token rows yourself; legacy access tokens are handled conservatively on family revoke.
178
191
 
179
192
  ## See also
180
193
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: overview
3
- description: 'Front-door orientation for `@warlock.js/auth` — JWT authentication for Warlock apps: the `Auth` base model, `authMiddleware` route gate, `authService` (login / logout / refresh with token rotation + replay detection), persisted AccessToken + RefreshToken, multi-user-type support, auth lifecycle events, and two CLI commands. Coupled to `@warlock.js/core`. TRIGGER when: code imports anything from `@warlock.js/auth`; user asks "what does @warlock.js/auth do", "how do I add login to my Warlock app", "JWT auth in Warlock", "protect a route", "multiple user types / admin + user", "refresh token rotation"; package.json adds `@warlock.js/auth`. Skip: specific task already known — load the matching task skill directly (`auth-basics`, `protect-routes`, `handle-login-and-logout`, `register-user`, `manage-tokens`, `customize-user-type`, `run-auth-commands`); non-Warlock apps (this package depends on core); session-cookie auth (this is JWT/token-based).'
3
+ description: "@warlock.js/auth overview for orientation and choosing the focused skill for the task."
4
4
  ---
5
5
 
6
6
  # `@warlock.js/auth` — overview
@@ -21,7 +21,7 @@ Skip if you're not on `@warlock.js/core` (the package depends on it), or if you
21
21
 
22
22
  ## The mental model in one paragraph
23
23
 
24
- Your user model extends the `Auth` base model and declares its `userType`. A login flows through `authService.login(Model, credentials, deviceInfo?)`: it verifies the password, issues an access + refresh token pair (persisted as `AccessToken` / `RefreshToken` records), and fires events. `authMiddleware(allowedUserType)` gates routes — the argument is required and always requires a valid token: `[]` allows any authenticated user, a user-type argument restricts to those types (401 otherwise). There is no anonymous mode; public routes simply omit the middleware. Refresh rotates the refresh token and detects replay by revoking the whole token family. CLI commands generate the JWT secret and clean up expired tokens.
24
+ Your user model extends the `Auth` base model and declares its `userType`. A login flows through `authService.login(Model, credentials, deviceInfo?)`: it verifies the password, issues a persisted access + refresh pair, and fires events. `authMiddleware("user")` is a typed hard gate; `authMiddleware()` resolves the configured default type; the object form selects header/cookie credentials, `optional`, and a page-local redirect. Refresh rotation uses a durable token family: family revocation invalidates its refresh and associated access rows. CLI commands generate the JWT secret and clean up expired tokens.
25
25
 
26
26
  ## Skills index
27
27
 
@@ -41,7 +41,7 @@ Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/l
41
41
  Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
42
42
 
43
43
  #### [`protect-routes`](@warlock.js/auth/protect-routes/SKILL.md)
44
- `authMiddleware(allowedUserType)` — the argument is required and always requires a valid token: `[]` allows any authenticated user, a user-type argument restricts to those types. Sets `request.locals.user` + `request.decodedAccessToken`, responds 401 on failure.
44
+ `authMiddleware` object and legacy overloads: typed/default hard gates, optional resolution, credential source, and page-local redirects. Sets `request.locals.user` + `request.decodedAccessToken` when a user resolves.
45
45
 
46
46
  ### Going deeper
47
47
 
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: protect-routes
3
- description: 'Gate HTTP routes via authMiddleware(allowedUserType) — the argument is required and a valid token is always required: [] allows any authenticated user, a user-type restricts to those types. Sets request.locals.user + request.decodedAccessToken on success, 401 on failure. Triggers: `authMiddleware`, `request.locals.user`, `request.decodedAccessToken`, `AuthErrorCodes`, `MissingAccessToken`, `InvalidAccessToken`; "how do I protect a route", "restrict route by user type", "require any logged-in user"; typical import `import { authMiddleware } from "@warlock.js/auth"`. Skip: multi-user-type config — `@warlock.js/auth/customize-user-type/SKILL.md`; issuing the token — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `express-jwt`, `next-auth` middleware.'
3
+ description: "Gate routes with authMiddleware in @warlock.js/auth; use when you need to protect routes."
4
4
  ---
5
5
 
6
6
  # Gate routes with `authMiddleware`
7
7
 
8
- `authMiddleware(allowedUserType: string | string[])` returns a Warlock middleware. Attach it to routes or route groups. The argument is **required** — there is no anonymous/optional mode. A request without a valid access token is always rejected with `401`; public routes simply omit the middleware.
8
+ `authMiddleware` returns a Warlock middleware. Attach it to routes or route groups. The legacy string overload is preserved; the object overload makes credential source, optional resolution, and a page-local redirect explicit.
9
9
 
10
10
  ```ts title="src/app/users/models/user/user.model.ts"
11
11
  import { Auth } from "@warlock.js/auth";
@@ -33,14 +33,13 @@ const accountController: RequestHandler = async ({ response }) => response.succe
33
33
  const adminController: RequestHandler = async ({ response }) => response.success({});
34
34
  const staffController: RequestHandler = async ({ response }) => response.success({});
35
35
 
36
- // Mode 1 — required, any user type
37
- // Rejects with 401 if no valid token; any authenticated user passes.
38
- router.get("/account", accountController, {
39
- middleware: [authMiddleware([])], // empty array = "any logged-in user"
36
+ // Required default type (`auth.defaultUserType`, or the sole configured type)
37
+ router.get("/account", accountController, {
38
+ middleware: [authMiddleware()],
40
39
  });
41
40
 
42
41
  // Mode 2 — required, specific user type(s)
43
- // Rejects with 401 if no token OR if token's userType isn't allowed.
42
+ // Rejects missing/invalid credentials with 401 and a disallowed user type with 403.
44
43
  router.get("/admin", adminController, {
45
44
  middleware: [authMiddleware("admin")],
46
45
  });
@@ -69,13 +68,13 @@ A token passes three separate checks, in order — a token can fail any one of t
69
68
  2. **The access-token row still exists** — deleting it (logout) invalidates the token immediately, before its JWT expiry.
70
69
  3. **The row's own `expires_at` has not passed** (since 4.12.0). The database is the authority on the session: a row whose expiry has elapsed is rejected and deleted, even if the JWT itself is still within its lifetime. A row with a missing or unparseable `expires_at` is treated as expired, not as never-expiring.
71
70
 
72
- On failure, the middleware returns one of these 401 responses:
71
+ Missing or invalid credentials return 401; an authenticated disallowed user type returns 403:
73
72
 
74
73
  | Error code | When |
75
74
  | --- | --- |
76
- | `MissingAccessToken` | No `Authorization` header |
75
+ | `MissingAccessToken` | No credential at the configured source |
77
76
  | `InvalidAccessToken` | Token doesn't verify (signature, missing/passed `exp`, wrong token type), has no DB row, or the row's `expires_at` has passed |
78
- | `Unauthorized` | Token valid but user-type isn't in the allowed list |
77
+ | `Unauthorized` | Token valid but user-type isn't in the allowed list (403) |
79
78
 
80
79
  ## Page routes: redirect to login instead of the JSON 401
81
80
 
@@ -103,7 +102,7 @@ Behavior, precisely:
103
102
 
104
103
  The page-vs-API signal is `request.route.isPage`. Config is typed as `PageAuthConfig` on `AuthConfigurations` (`loginPath?: string`, `returnUrlParam?: string`, default `"returnUrl"`); read at runtime through `authConfig.pageAuth`.
105
104
 
106
- > `auth.pageAuth` is **not a second auth mechanism** — it is a convenience *adapter* over the same "not authenticated" outcome the gate already produces. It only changes the **representation** of that outcome on a page route (a browser-friendly login redirect) instead of the JSON `401`; the decision that the request is unauthenticated is unchanged. The sanctioned *generic* way to make authentication soft is the app-owned optional-auth middleware below — `pageAuth` is the built-in adapter for the common "redirect a logged-out human to /login" case, so most apps never need to hand-roll it.
105
+ > A local `redirect` object is preferred for a page that needs a different login destination; absent it, `auth.pageAuth` remains the compatibility fallback.
107
106
 
108
107
  ## Reading the user in a controller
109
108
 
@@ -142,31 +141,23 @@ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () =>
142
141
 
143
142
  Every route inside the group is gated — the group's `middleware` array applies to each route in the callback. Cleaner than repeating the middleware on each route.
144
143
 
145
- ## Optional auth is an app-owned middleware (the sanctioned pattern)
146
-
147
- `authMiddleware` is a **hard gate** — it always requires a valid token and rejects when one is absent. There is deliberately no built-in "hydrate `request.locals.user` if a token is present, otherwise continue" mode, because the *sanctioned generic pattern* for soft/optional auth is a small **app-owned optional-auth middleware**: resolve the user when a valid token is present, and otherwise leave the route to decide. The app owns it because the "what to do when absent" policy is the app's, not the framework's.
148
-
149
- ```ts title="src/app/middleware/optional-auth.middleware.ts"
150
- import { type Middleware } from "@warlock.js/core";
151
- import { jwt } from "@warlock.js/auth";
152
-
153
- // The one sanctioned optional-auth shape: resolve if present, never reject.
154
- // Middleware receives the context object ({ request, response }), like a handler.
155
- export const optionalAuth: Middleware = async ({ request }) => {
156
- const token = request.authorizationValue;
157
- if (!token) return; // absent → continue anonymously; the route decides
158
- try {
159
- const decoded = await jwt.verify(token);
160
- // hydrate request.locals.user from your token/user model when valid
161
- } catch {
162
- // invalid token on an optional route → treat as anonymous, don't reject
163
- }
164
- // returning nothing (undefined) === continue to the handler
165
- };
166
- ```
167
-
168
- Wire it like any middleware (`{ middleware: [optionalAuth] }`), then branch on `request.locals.user` in the controller. Use `authMiddleware` when the route must be gated; use this when the route is public but personalizes for a signed-in user. `auth.pageAuth` (above) is the built-in **adapter** over this same "unauthenticated" outcome for the common page-redirect case — it is not a competing mechanism.
169
-
144
+ ## Optional auth
145
+
146
+ Use `optional: true` in the object form when a public route should receive a verified user when one is present. Missing or invalid credentials continue anonymously; a hard gate remains the default.
147
+
148
+ ```ts
149
+ middleware: [authMiddleware({ source: "header", optional: true })]
150
+ ```
151
+
152
+ ## Automatic cookie renewal
153
+
154
+ Add `refresh: { source: "cookie", key: "refresh_token", overlapMs?: 5000 }`
155
+ to a typed cookie middleware to renew an absent, invalid, or expired access
156
+ cookie before the handler. It writes the successor access and refresh cookies
157
+ through `authService.setAuthCookie`; it never replays a handler. `overlapMs` is
158
+ clamped to 0–10,000 ms (default 5,000). Only the exact immediate successor is
159
+ tolerated during that window. The legacy `refreshTokens` API stays strict; a
160
+ later old-token presentation revokes its family.
170
161
  ## Custom error responses
171
162
 
172
163
  The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
@@ -208,13 +199,13 @@ router.post("/articles", articlesController, { middleware: [authMiddleware([]),
208
199
 
209
200
  Use `authMiddleware` to establish *who the caller is*, and `access` to decide *what they may do*. They compose — `access` reads the user `authMiddleware` put on the request.
210
201
 
211
- ## Cookie sessions: `authMiddleware([], "cookie:<name>")`
202
+ ## Cookie credentials
212
203
 
213
- `authMiddleware`'s second argument, `tokenFrom`, selects the credential source and defaults to `"header"`. Pass `` `cookie:<name>` `` to read the credential from a named cookie instead of the `Authorization` header — the same gate, the same three checks, the same `request.locals.user` / `request.decodedAccessToken` outcome, just a different place to find the token:
204
+ Use `{ source: "cookie", key: "token" }` in the object form. The same checks and `request.locals.user` outcome apply; legacy `authMiddleware("user", "cookie:token")` is also supported.
214
205
 
215
206
  ```ts
216
207
  router.get("/browser-account", accountController, {
217
- middleware: [authMiddleware([], "cookie:token")],
208
+ middleware: [authMiddleware({ source: "cookie", key: "token" })],
218
209
  });
219
210
  ```
220
211
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: register-user
3
- description: 'Sign up a new user and issue the initial token pair — User.create({...password: await hashPassword(plain)}) then authService.createTokenPair(user). Triggers: `User.create`, `hashPassword`, `verifyPassword`, `authService.createTokenPair`, `toJsonColumns`, `strongPassword`, `authEvents`; "build a register endpoint", "POST /register controller", "sign up a new user", "hash password on signup", "email verification flow"; typical import `import { authService } from "@warlock.js/auth"; import { hashPassword } from "@warlock.js/core"`. Skip: login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; competing libs `bcrypt`, `bcryptjs`, `argon2`.'
3
+ description: "Register-and-issue-tokens flow in @warlock.js/auth; use when you need to register user."
4
4
  ---
5
5
 
6
6
  # Register-and-issue-tokens flow
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: run-auth-commands
3
- description: 'Three bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env), warlock auth.cleanup (removes expired refresh tokens), and warlock auth.purge-never-expiring (finds and revokes token rows that can never expire). Register via registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand() and registerAuthPurgeNeverExpiringCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `registerAuthPurgeNeverExpiringCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `warlock auth.purge-never-expiring`, `cleanupExpiredTokens`, `purgeNeverExpiringTokens`, `command`; "token never expires", "no exp claim", "Invalid Date expires_at"; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.'
3
+ description: "Run auth commands in @warlock.js/auth; use when you need to run auth commands."
4
4
  ---
5
5
 
6
6
  # Run auth commands
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: throttle-login-attempts
3
- description: 'Brute-force / credential-stuffing protection via `loginThrottleMiddleware` — a failure-aware route gate that counts only failed logins (resets on success), locks per-account and per-source after a threshold, and rejects pre-controller with 429 so the DB lookup and bcrypt verify are skipped. Cache-backed (shared across replicas), fixed-window, fails open on a cache outage. Triggers: `loginThrottleMiddleware`, `AuthErrorCodes.TooManyAttempts`, `EC004`, "rate limit login", "brute force protection", "lock account after failed logins", "throttle login attempts", "too many login attempts 429"; typical import `import { loginThrottleMiddleware } from "@warlock.js/auth"`. Skip: generic per-route request rate limiting that counts every request (use core `middleware.rateLimit`); gating a route by auth — `@warlock.js/auth/protect-routes/SKILL.md`; issuing tokens — `@warlock.js/auth/handle-login-and-logout/SKILL.md`.'
3
+ description: "Throttle login attempts with loginThrottleMiddleware in @warlock.js/auth; use when you need to throttle login attempts."
4
4
  ---
5
5
 
6
6
  # Throttle login attempts with `loginThrottleMiddleware`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: verify-email-and-reset-password
3
- description: 'Email verification and password reset with hashed, single-use, expiring, purpose-bound tokens delivered through `@warlock.js/notifications` — `sendEmailVerification(user)`, `verifyEmail(token)`, `requestPasswordReset(Model, email)`, `resetPassword(token, newPassword)`, the `requireVerifiedEmail()` guard, and the `tokenIssueThrottleMiddleware` / `tokenConsumeThrottleMiddleware` presets. Reset revokes every access token, refresh token and cookie session; a reset request answers the same for unknown emails. Triggers: `sendEmailVerification`, `verifyEmail`, `requestPasswordReset`, `resetPassword`, `requireVerifiedEmail`, `isEmailVerified`, `OneTimeToken`, `InvalidOneTimeTokenError`, `EmailNotVerifiedError`, `NotificationsUnavailableError`, `EC007`, `EC008`, `auth.verification`, `auth.passwordReset`; "verify email", "confirm email address", "forgot password", "reset password link", "block unverified users", "resend verification email"; typical import `import { requestPasswordReset, resetPassword } from "@warlock.js/auth"`. Skip: sign-up itself — `@warlock.js/auth/register-user/SKILL.md`; login throttling — `@warlock.js/auth/throttle-login-attempts/SKILL.md`; defining notifications — `@warlock.js/notifications/define-notification/SKILL.md`.'
3
+ description: "Verify email + reset password in @warlock.js/auth; use when you need to verify email and reset password."
4
4
  ---
5
5
 
6
6
  # Verify email + reset password