@warlock.js/auth 4.0.174 → 4.1.1

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 (164) hide show
  1. package/README.md +37 -0
  2. package/cjs/index.cjs +807 -0
  3. package/cjs/index.cjs.map +1 -0
  4. package/esm/commands/auth-cleanup-command.d.mts +13 -0
  5. package/esm/commands/auth-cleanup-command.d.mts.map +1 -0
  6. package/esm/commands/auth-cleanup-command.mjs +34 -0
  7. package/esm/commands/auth-cleanup-command.mjs.map +1 -0
  8. package/esm/commands/jwt-secret-generator-command.d.mts +5 -0
  9. package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -0
  10. package/esm/commands/jwt-secret-generator-command.mjs +15 -0
  11. package/esm/commands/jwt-secret-generator-command.mjs.map +1 -0
  12. package/esm/contracts/auth-contract.d.mts +40 -0
  13. package/esm/contracts/auth-contract.d.mts.map +1 -0
  14. package/esm/contracts/index.d.mts +2 -0
  15. package/esm/contracts/types.d.mts +170 -0
  16. package/esm/contracts/types.d.mts.map +1 -0
  17. package/esm/contracts/types.mjs +25 -0
  18. package/esm/contracts/types.mjs.map +1 -0
  19. package/esm/index.d.mts +15 -0
  20. package/esm/index.mjs +17 -0
  21. package/esm/middleware/auth.middleware.d.mts +22 -0
  22. package/esm/middleware/auth.middleware.d.mts.map +1 -0
  23. package/esm/middleware/auth.middleware.mjs +72 -0
  24. package/esm/middleware/auth.middleware.mjs.map +1 -0
  25. package/esm/middleware/index.mjs +3 -0
  26. package/esm/models/access-token/access-token.model.d.mts +13 -0
  27. package/esm/models/access-token/access-token.model.d.mts.map +1 -0
  28. package/esm/models/access-token/access-token.model.mjs +23 -0
  29. package/esm/models/access-token/access-token.model.mjs.map +1 -0
  30. package/esm/models/access-token/index.d.mts +1 -0
  31. package/esm/models/access-token/index.mjs +3 -0
  32. package/esm/models/access-token/migration.mjs +24 -0
  33. package/esm/models/access-token/migration.mjs.map +1 -0
  34. package/esm/models/auth.model.d.mts +63 -0
  35. package/esm/models/auth.model.d.mts.map +1 -0
  36. package/esm/models/auth.model.mjs +77 -0
  37. package/esm/models/auth.model.mjs.map +1 -0
  38. package/esm/models/index.d.mts +8 -0
  39. package/esm/models/index.d.mts.map +1 -0
  40. package/esm/models/index.mjs +14 -0
  41. package/esm/models/index.mjs.map +1 -0
  42. package/esm/models/refresh-token/index.d.mts +1 -0
  43. package/esm/models/refresh-token/index.mjs +3 -0
  44. package/esm/models/refresh-token/migration.mjs +27 -0
  45. package/esm/models/refresh-token/migration.mjs.map +1 -0
  46. package/esm/models/refresh-token/refresh-token.model.d.mts +36 -0
  47. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -0
  48. package/esm/models/refresh-token/refresh-token.model.mjs +58 -0
  49. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -0
  50. package/esm/services/auth-events.d.mts +89 -0
  51. package/esm/services/auth-events.d.mts.map +1 -0
  52. package/esm/services/auth-events.mjs +68 -0
  53. package/esm/services/auth-events.mjs.map +1 -0
  54. package/esm/services/auth.service.d.mts +95 -0
  55. package/esm/services/auth.service.d.mts.map +1 -0
  56. package/esm/services/auth.service.mjs +275 -0
  57. package/esm/services/auth.service.mjs.map +1 -0
  58. package/esm/services/generate-jwt-secret.d.mts +5 -0
  59. package/esm/services/generate-jwt-secret.d.mts.map +1 -0
  60. package/esm/services/generate-jwt-secret.mjs +48 -0
  61. package/esm/services/generate-jwt-secret.mjs.map +1 -0
  62. package/esm/services/index.d.mts +4 -0
  63. package/esm/services/index.mjs +6 -0
  64. package/esm/services/jwt.d.mts +52 -0
  65. package/esm/services/jwt.d.mts.map +1 -0
  66. package/esm/services/jwt.mjs +58 -0
  67. package/esm/services/jwt.mjs.map +1 -0
  68. package/esm/utils/auth-error-codes.d.mts +23 -0
  69. package/esm/utils/auth-error-codes.d.mts.map +1 -0
  70. package/esm/utils/auth-error-codes.mjs +23 -0
  71. package/esm/utils/auth-error-codes.mjs.map +1 -0
  72. package/llms-full.txt +1023 -0
  73. package/llms.txt +16 -0
  74. package/package.json +47 -36
  75. package/skills/auth-basics/SKILL.md +88 -0
  76. package/skills/customize-user-type/SKILL.md +137 -0
  77. package/skills/handle-login-and-logout/SKILL.md +160 -0
  78. package/skills/manage-tokens/SKILL.md +169 -0
  79. package/skills/overview/SKILL.md +66 -0
  80. package/skills/protect-routes/SKILL.md +105 -0
  81. package/skills/register-user/SKILL.md +135 -0
  82. package/skills/run-auth-commands/SKILL.md +125 -0
  83. package/esm/commands/auth-cleanup-command.d.ts +0 -10
  84. package/esm/commands/auth-cleanup-command.d.ts.map +0 -1
  85. package/esm/commands/auth-cleanup-command.js +0 -29
  86. package/esm/commands/auth-cleanup-command.js.map +0 -1
  87. package/esm/commands/jwt-secret-generator-command.d.ts +0 -2
  88. package/esm/commands/jwt-secret-generator-command.d.ts.map +0 -1
  89. package/esm/commands/jwt-secret-generator-command.js +0 -7
  90. package/esm/commands/jwt-secret-generator-command.js.map +0 -1
  91. package/esm/contracts/auth-contract.d.ts +0 -23
  92. package/esm/contracts/auth-contract.d.ts.map +0 -1
  93. package/esm/contracts/index.d.ts +0 -3
  94. package/esm/contracts/index.d.ts.map +0 -1
  95. package/esm/contracts/types.d.ts +0 -167
  96. package/esm/contracts/types.d.ts.map +0 -1
  97. package/esm/contracts/types.js +0 -20
  98. package/esm/contracts/types.js.map +0 -1
  99. package/esm/index.d.ts +0 -8
  100. package/esm/index.d.ts.map +0 -1
  101. package/esm/index.js +0 -1
  102. package/esm/index.js.map +0 -1
  103. package/esm/middleware/auth.middleware.d.ts +0 -2
  104. package/esm/middleware/auth.middleware.d.ts.map +0 -1
  105. package/esm/middleware/auth.middleware.js +0 -72
  106. package/esm/middleware/auth.middleware.js.map +0 -1
  107. package/esm/middleware/index.d.ts +0 -2
  108. package/esm/middleware/index.d.ts.map +0 -1
  109. package/esm/models/access-token/access-token.model.d.ts +0 -9
  110. package/esm/models/access-token/access-token.model.d.ts.map +0 -1
  111. package/esm/models/access-token/access-token.model.js +0 -14
  112. package/esm/models/access-token/access-token.model.js.map +0 -1
  113. package/esm/models/access-token/index.d.ts +0 -2
  114. package/esm/models/access-token/index.d.ts.map +0 -1
  115. package/esm/models/access-token/migration.d.ts +0 -2
  116. package/esm/models/access-token/migration.d.ts.map +0 -1
  117. package/esm/models/access-token/migration.js +0 -22
  118. package/esm/models/access-token/migration.js.map +0 -1
  119. package/esm/models/auth.model.d.ts +0 -58
  120. package/esm/models/auth.model.d.ts.map +0 -1
  121. package/esm/models/auth.model.js +0 -68
  122. package/esm/models/auth.model.js.map +0 -1
  123. package/esm/models/index.d.ts +0 -5
  124. package/esm/models/index.d.ts.map +0 -1
  125. package/esm/models/index.js +0 -1
  126. package/esm/models/index.js.map +0 -1
  127. package/esm/models/refresh-token/index.d.ts +0 -2
  128. package/esm/models/refresh-token/index.d.ts.map +0 -1
  129. package/esm/models/refresh-token/migration.d.ts +0 -2
  130. package/esm/models/refresh-token/migration.d.ts.map +0 -1
  131. package/esm/models/refresh-token/migration.js +0 -23
  132. package/esm/models/refresh-token/migration.js.map +0 -1
  133. package/esm/models/refresh-token/refresh-token.model.d.ts +0 -32
  134. package/esm/models/refresh-token/refresh-token.model.d.ts.map +0 -1
  135. package/esm/models/refresh-token/refresh-token.model.js +0 -53
  136. package/esm/models/refresh-token/refresh-token.model.js.map +0 -1
  137. package/esm/services/auth-events.d.ts +0 -85
  138. package/esm/services/auth-events.d.ts.map +0 -1
  139. package/esm/services/auth-events.js +0 -65
  140. package/esm/services/auth-events.js.map +0 -1
  141. package/esm/services/auth.service.d.ts +0 -92
  142. package/esm/services/auth.service.d.ts.map +0 -1
  143. package/esm/services/auth.service.js +0 -322
  144. package/esm/services/auth.service.js.map +0 -1
  145. package/esm/services/generate-jwt-secret.d.ts +0 -2
  146. package/esm/services/generate-jwt-secret.d.ts.map +0 -1
  147. package/esm/services/generate-jwt-secret.js +0 -47
  148. package/esm/services/generate-jwt-secret.js.map +0 -1
  149. package/esm/services/index.d.ts +0 -5
  150. package/esm/services/index.d.ts.map +0 -1
  151. package/esm/services/jwt.d.ts +0 -23
  152. package/esm/services/jwt.d.ts.map +0 -1
  153. package/esm/services/jwt.js +0 -40
  154. package/esm/services/jwt.js.map +0 -1
  155. package/esm/utils/auth-error-codes.d.ts +0 -18
  156. package/esm/utils/auth-error-codes.d.ts.map +0 -1
  157. package/esm/utils/auth-error-codes.js +0 -18
  158. package/esm/utils/auth-error-codes.js.map +0 -1
  159. package/esm/utils/duration.d.ts +0 -45
  160. package/esm/utils/duration.d.ts.map +0 -1
  161. package/esm/utils/duration.js +0 -93
  162. package/esm/utils/duration.js.map +0 -1
  163. package/esm/utils/index.d.ts +0 -3
  164. package/esm/utils/index.d.ts.map +0 -1
package/llms-full.txt ADDED
@@ -0,0 +1,1023 @@
1
+ # Warlock Auth — full skills
2
+
3
+ > Package: `@warlock.js/auth`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/auth/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## auth-basics `@warlock.js/auth/auth-basics/SKILL.md`
8
+
9
+ ---
10
+ name: auth-basics
11
+ 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`.'
12
+ ---
13
+
14
+ # Auth basics
15
+
16
+ JWT-based authentication for Warlock. `Auth` base model + `authMiddleware` gate + `authService` for login/logout/refresh + `AccessToken` / `RefreshToken` persistence + multi-user-type support.
17
+
18
+ > This skill is the auth **map** — read it first, then load the specific skill for the task.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ yarn add @warlock.js/auth
24
+ ```
25
+
26
+ ## Foundations
27
+
28
+ 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)).
29
+ 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.
30
+ 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.
31
+ 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.
32
+ 5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
33
+ 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.
34
+ 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)).
35
+
36
+ ## Minimal wire-up
37
+
38
+ ```ts title="warlock.config.ts"
39
+ import {
40
+ authMigrations,
41
+ registerAuthCleanupCommand,
42
+ registerJWTSecretGeneratorCommand,
43
+ } from "@warlock.js/auth";
44
+ import { defineConfig } from "@warlock.js/core";
45
+
46
+ export default defineConfig({
47
+ cli: {
48
+ commands: [
49
+ registerJWTSecretGeneratorCommand(),
50
+ registerAuthCleanupCommand(),
51
+ ],
52
+ },
53
+ database: {
54
+ migrations: authMigrations,
55
+ },
56
+ });
57
+ ```
58
+
59
+ ```ts title="src/config/auth.ts"
60
+ import { User } from "@/app/users/models/user.model";
61
+
62
+ export default {
63
+ userType: {
64
+ user: User,
65
+ // admin: Admin, // for multi-user-type
66
+ },
67
+ jwt: {
68
+ secret: env("JWT_SECRET"),
69
+ expiresIn: "1h",
70
+ refresh: {
71
+ enabled: true,
72
+ expiresIn: "30d",
73
+ rotation: true,
74
+ maxPerUser: 5,
75
+ },
76
+ },
77
+ };
78
+ ```
79
+
80
+ ## Pick a skill
81
+
82
+ | If the task is about… | Load |
83
+ | --- | --- |
84
+ | Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
85
+ | `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) |
86
+ | Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
87
+ | Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
88
+ | Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) |
89
+ | `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
90
+
91
+ ## Things NOT to do
92
+
93
+ - Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
94
+ - Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
95
+ - Don't return the raw `User` from a login endpoint without shaping output. Configure `static toJsonColumns` or `static resource` (see [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md)).
96
+ - Don't run `auth.cleanup` from app boot. Schedule it (cron, scheduler) as a periodic task — see [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
97
+
98
+
99
+ ## customize-user-type `@warlock.js/auth/customize-user-type/SKILL.md`
100
+
101
+ ---
102
+ name: customize-user-type
103
+ 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`.'
104
+ ---
105
+
106
+ # Customize user type (multi-user-type auth)
107
+
108
+ The `Auth` base class has a `userType` slot. Subclass it once per type, register each class under `config.auth.userType.<slug>`, and the auth flow handles the rest.
109
+
110
+ ## Define a model per user type
111
+
112
+ ```ts title="src/app/users/models/user/user.model.ts"
113
+ import { Auth } from "@warlock.js/auth";
114
+ import { RegisterModel } from "@warlock.js/cascade";
115
+
116
+ @RegisterModel()
117
+ export class User extends Auth<UserSchema> {
118
+ public static table = "users";
119
+ public static schema = userSchema;
120
+
121
+ public get userType(): string {
122
+ return "user";
123
+ }
124
+ }
125
+ ```
126
+
127
+ ```ts title="src/app/admins/models/admin/admin.model.ts"
128
+ @RegisterModel()
129
+ export class Admin extends Auth<AdminSchema> {
130
+ public static table = "admins";
131
+ public static schema = adminSchema;
132
+
133
+ public get userType(): string {
134
+ return "admin";
135
+ }
136
+ }
137
+ ```
138
+
139
+ Each gets its own table, its own schema, its own `userType` slug. They DON'T share table — they're separate models.
140
+
141
+ ## Register them in `config.auth`
142
+
143
+ ```ts title="src/config/auth.ts"
144
+ import { User } from "@/app/users/models/user.model";
145
+ import { Admin } from "@/app/admins/models/admin.model";
146
+
147
+ export default {
148
+ userType: {
149
+ user: User,
150
+ admin: Admin,
151
+ // staff: Staff,
152
+ // client: Client,
153
+ },
154
+ jwt: {
155
+ secret: env("JWT_SECRET"),
156
+ expiresIn: "1h",
157
+ refresh: { enabled: true, expiresIn: "30d", rotation: true },
158
+ },
159
+ };
160
+ ```
161
+
162
+ The keys (`"user"`, `"admin"`) are the **userType slugs** that flow through every token, middleware call, and event payload.
163
+
164
+ ## Gate routes per user type
165
+
166
+ ```ts
167
+ import { authMiddleware } from "@warlock.js/auth";
168
+
169
+ router.get("/account", userAccountController, { middleware: [authMiddleware("user")] });
170
+ router.get("/admin/users", listUsersController, { middleware: [authMiddleware("admin")] });
171
+ router.get("/back-office", backOfficeController, { middleware: [authMiddleware(["admin", "staff"])] });
172
+ router.get("/dashboard", dashboardController, { middleware: [authMiddleware([])] }); // any logged-in
173
+ ```
174
+
175
+ See [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md).
176
+
177
+ ## Login per user type — pass the right Model
178
+
179
+ `authService.login(Model, credentials, deviceInfo?)` is keyed off the model you pass:
180
+
181
+ ```ts
182
+ // User login endpoint
183
+ const result = await authService.login(User, credentials);
184
+ // Issues tokens with userType "user"; middleware will route them to User model.
185
+
186
+ // Admin login endpoint
187
+ const result = await authService.login(Admin, credentials);
188
+ // Issues tokens with userType "admin"; middleware will route them to Admin model.
189
+ ```
190
+
191
+ The middleware then uses `config.auth.userType[token.userType]` to know which model to hydrate.
192
+
193
+ ## Cross-type behavior
194
+
195
+ - **Tokens are scoped to their issuing user-type.** A user-type token doesn't unlock admin-type routes.
196
+ - **AccessToken / RefreshToken rows carry the `user_type` column.** Same model classes, different rows per type.
197
+ - **`authMiddleware(["admin", "user"])`** allows either — useful for endpoints shared between roles.
198
+
199
+ ## When NOT to use multi-user-type
200
+
201
+ If the distinction is **permissions/roles within one user shape**, use a `role` column on a single User model instead. Multi-user-type is right when:
202
+
203
+ - Different tables / schemas (admins have an `admin_level`; users have a `subscription_tier`).
204
+ - Separate registration flows (admins are created via an admin panel; users self-register).
205
+ - Truly separate concepts at the data layer (clients vs vendors in a marketplace).
206
+
207
+ If users and admins differ only in a `role` field, stick with one `User` model + a role check at the controller layer.
208
+
209
+ ## `Auth` base — what your subclass inherits
210
+
211
+ ```ts
212
+ abstract class Auth<TSchema> extends Model<TSchema> implements Authenticable {
213
+ // ...all the Model<> methods
214
+ public abstract get userType(): string;
215
+ public generateAccessToken(payload?: Record<string, unknown>): Promise<AccessTokenOutput>;
216
+ public generateRefreshToken(deviceInfo?: DeviceInfo): Promise<RefreshToken | undefined>;
217
+ public createTokenPair(deviceInfo?: DeviceInfo): Promise<TokenPair>;
218
+ public confirmPassword(password: string): Promise<boolean>;
219
+ }
220
+ ```
221
+
222
+ `userType` is the only required override (an abstract getter — return the type slug). Override `generateAccessToken` if you need a non-default payload.
223
+
224
+ `Auth` implements the `Authenticable` contract — that interface mirrors exactly these methods (`userType`, `generateAccessToken`, `generateRefreshToken`, `createTokenPair`, `confirmPassword`), so the class fails to compile if it drifts from the contract. Use `confirmPassword(plaintext)` to check a password against the stored hash (e.g. a "confirm current password" step).
225
+
226
+ ## Things NOT to do
227
+
228
+ - Don't use multi-user-type for what's really role-based access control. Use a `role` column on a single User model when the data shape is shared.
229
+ - Don't forget the `public get userType(): string` override. It's an abstract getter on `Auth` — a subclass without it won't compile, and middleware lookups key off its return value.
230
+ - Don't reuse the same `userType` slug across two models — the `config.auth.userType` map can only point one slug at one model.
231
+ - Don't put admins and users in the same table differentiated by a flag. Separate tables means migrations don't coupling, queries don't accidentally cross, and audit logs are cleaner.
232
+
233
+ ## See also
234
+
235
+ - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware` semantics
236
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — passing the right Model to `login`
237
+ - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `@RegisterModel`, models in general
238
+
239
+
240
+ ## handle-login-and-logout `@warlock.js/auth/handle-login-and-logout/SKILL.md`
241
+
242
+ ---
243
+ name: handle-login-and-logout
244
+ 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.'
245
+ ---
246
+
247
+ # Login + logout
248
+
249
+ `authService` exposes the full flow. Pass the model class so the service knows which user-type to look up.
250
+
251
+ ## Login — `authService.login(Model, credentials, deviceInfo?)`
252
+
253
+ ```ts
254
+ import { authService } from "@warlock.js/auth";
255
+ import { User } from "@/app/users/models/user.model";
256
+
257
+ async function loginController(request: Request, response: Response) {
258
+ const result = await authService.login(User, {
259
+ email: request.input("email"),
260
+ password: request.input("password"),
261
+ }, {
262
+ userAgent: request.header("user-agent"),
263
+ ip: request.ip,
264
+ });
265
+
266
+ if (!result) {
267
+ return response.unauthorized({ error: "Invalid credentials" });
268
+ }
269
+
270
+ return response.success({
271
+ user: result.user,
272
+ tokens: result.tokens,
273
+ });
274
+ }
275
+ ```
276
+
277
+ The returned shape:
278
+
279
+ ```ts
280
+ {
281
+ user: T, // your User subclass, hydrated
282
+ tokens: {
283
+ accessToken: { token: string, expiresAt: string },
284
+ refreshToken?: { token: string, expiresAt: string }, // omitted if refresh tokens disabled
285
+ },
286
+ }
287
+ ```
288
+
289
+ Returns `null` on failure (wrong password, user not found). The service emits `login.attempt` → `login.success` or `login.failed` events as it goes — subscribe via the auth event bus if you need an audit trail.
290
+
291
+ ## What `credentials` looks like
292
+
293
+ The shape is **arbitrary** — every key except `password` is used as a `where(...)` filter against the model. The password is verified separately via bcrypt.
294
+
295
+ ```ts
296
+ // Email + password
297
+ authService.login(User, { email: "ada@example.com", password: "..." });
298
+
299
+ // Username + password
300
+ authService.login(User, { username: "ada", password: "..." });
301
+
302
+ // Phone-based OTP (where password is the OTP hash)
303
+ authService.login(User, { phone: "+1...", password: hashedOTP });
304
+ ```
305
+
306
+ For lower-level credential verification (just check, don't issue tokens), use `authService.attemptLogin(Model, credentials)` — returns the user or null without creating tokens.
307
+
308
+ ## Device info
309
+
310
+ The optional `deviceInfo` carries metadata into the refresh token row:
311
+
312
+ ```ts
313
+ authService.login(User, credentials, {
314
+ userAgent: request.header("user-agent"),
315
+ ip: request.ip,
316
+ deviceId: "...", // your client-side device fingerprint
317
+ familyId: "...", // pre-existing family for token rotation, usually omitted
318
+ });
319
+ ```
320
+
321
+ Useful for "show active sessions" UIs — see `authService.getActiveSessions(user)`.
322
+
323
+ ## Logout — `authService.logout(user, accessToken?, refreshToken?)`
324
+
325
+ ```ts
326
+ async function logoutController(request: Request, response: Response) {
327
+ await authService.logout(
328
+ request.user!,
329
+ request.authorizationValue, // access token from the Authorization header
330
+ request.input("refreshToken"), // refresh token from the request body
331
+ );
332
+
333
+ return response.success({ message: "Logged out" });
334
+ }
335
+ ```
336
+
337
+ The contract:
338
+ - **Pass the access token** → that specific access-token row is deleted.
339
+ - **Pass the refresh token** → that specific refresh-token row is revoked.
340
+ - **Omit refresh token** → behavior depends on `config.auth.jwt.refresh.logoutWithoutToken`:
341
+ - `"revoke-all"` (default) — every refresh token for this user is revoked. Fail-safe.
342
+ - `"error"` — throws. Force the client to send the refresh token.
343
+
344
+ The `revoke-all` default is the right call for most apps. If a client loses track of the refresh token, logout still works and the user has to log in fresh on every device.
345
+
346
+ ## Logout-everywhere
347
+
348
+ ```ts
349
+ await authService.revokeAllTokens(user);
350
+ // Revokes every refresh token + deletes every access token for this user.
351
+ ```
352
+
353
+ Useful for "logout from all devices" buttons. Fires `token.revoked` per token + `logout.all` once.
354
+
355
+ ## Refresh tokens — `authService.refreshTokens(refreshTokenString, deviceInfo?)`
356
+
357
+ ```ts
358
+ async function refreshController(request: Request, response: Response) {
359
+ const tokens = await authService.refreshTokens(
360
+ request.input("refreshToken"),
361
+ { userAgent: request.header("user-agent"), ip: request.ip },
362
+ );
363
+
364
+ if (!tokens) {
365
+ return response.unauthorized({ error: "Invalid refresh token" });
366
+ }
367
+
368
+ return response.success({ tokens });
369
+ }
370
+ ```
371
+
372
+ Returns a new token pair or `null` (token expired, revoked, or replay-detected). With rotation enabled (default), the old refresh token is consumed; the new pair stays in the same "family." Replay → revoke the whole family. See [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md).
373
+
374
+ ## Auth events
375
+
376
+ `authEvents` is a type-safe event bus (over `@mongez/events`) that fires on every meaningful auth moment. Subscribe with `on` / `subscribe`, unsubscribe with `off` / `unsubscribeAll`:
377
+
378
+ ```ts
379
+ import { authEvents } from "@warlock.js/auth";
380
+
381
+ authEvents.on("login.success", (user, tokens, deviceInfo) => { /* audit */ });
382
+ authEvents.on("login.failed", (credentials, reason) => { /* alert on brute force */ });
383
+ authEvents.on("logout", (user) => { /* clear server-side session, if any */ });
384
+ authEvents.on("token.refreshed", (user, newPair, oldToken) => { /* track rotation */ });
385
+ authEvents.on("cleanup.completed", (count) => { /* metrics */ });
386
+ ```
387
+
388
+ Full event list: `login.attempt`, `login.success`, `login.failed`, `logout`, `logout.all`, `logout.failsafe`, `token.created`, `token.refreshed`, `token.revoked`, `token.expired`, `token.familyRevoked`, `session.created`, `session.destroyed`, `cleanup.completed`.
389
+
390
+ ## Things NOT to do
391
+
392
+ - Don't `authService.login(User, { password })` without other credentials — the password is the secret; the other fields are the lookup. A login with only a password is a logic bug.
393
+ - Don't return the password hash in the response. `static toJsonColumns` on the User model should explicitly exclude it.
394
+ - Don't store the refresh token in localStorage. Use an httpOnly secure cookie for refresh tokens; the access token can sit in memory.
395
+ - Don't issue a new token pair without revoking the old one when rotation is enabled. `refreshTokens` does this for you — don't bypass it.
396
+
397
+ ## See also
398
+
399
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token lifecycle, rotation, family revocation
400
+ - [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — sign-up that issues tokens after creation
401
+ - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — where the access token gets consumed
402
+
403
+
404
+ ## manage-tokens `@warlock.js/auth/manage-tokens/SKILL.md`
405
+
406
+ ---
407
+ name: manage-tokens
408
+ 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`.'
409
+ ---
410
+
411
+ # Manage tokens
412
+
413
+ Tokens are persisted Cascade models. Issuing a token writes a row. Verification checks the row exists. Revocation deletes / marks-revoked. This gives you JWT's stateless verification + statelful revocation.
414
+
415
+ ## Token shapes
416
+
417
+ ```ts
418
+ type AccessTokenOutput = { token: string; expiresAt: string };
419
+ type RefreshTokenOutput = { token: string; expiresAt: string };
420
+
421
+ type TokenPair = {
422
+ accessToken: AccessTokenOutput;
423
+ refreshToken?: RefreshTokenOutput; // omitted if config.auth.jwt.refresh.enabled = false
424
+ };
425
+ ```
426
+
427
+ ## Issuing tokens
428
+
429
+ ```ts
430
+ import { authService } from "@warlock.js/auth";
431
+
432
+ // Just an access token (rare — usually use createTokenPair)
433
+ const access = await authService.generateAccessToken(user);
434
+
435
+ // Just a refresh token
436
+ const refresh = await authService.createRefreshToken(user, deviceInfo);
437
+
438
+ // Both — the everyday case
439
+ const pair = await authService.createTokenPair(user, deviceInfo);
440
+ ```
441
+
442
+ `createTokenPair` is the typical issuance path. It respects `config.auth.jwt.refresh.enabled` — if disabled, returns only `accessToken`.
443
+
444
+ ## Refresh with rotation — `refreshTokens`
445
+
446
+ ```ts
447
+ const next = await authService.refreshTokens(oldRefreshToken, deviceInfo);
448
+ // next: TokenPair | null
449
+ ```
450
+
451
+ What happens internally:
452
+
453
+ 1. Verify the JWT signature on the old refresh token.
454
+ 2. Find the row in `RefreshToken` — must exist + not be revoked.
455
+ 3. Look up the user via `config.auth.userType[token.userType]`.
456
+ 4. **Rotation** (default — `config.auth.jwt.refresh.rotation = true`): revoke the old refresh token, create a new pair from the same `family_id`.
457
+ 5. **No rotation**: mark the old as "used" but keep it valid.
458
+
459
+ **Replay detection.** If the old refresh token is presented again after rotation (already revoked but still in the DB):
460
+
461
+ ```ts
462
+ // Inside refreshTokens, on a revoked-token presentation:
463
+ await authService.revokeTokenFamily(refreshToken.get("family_id"));
464
+ ```
465
+
466
+ Every refresh token in the same family is revoked. Pattern: a leaked refresh token is used by both legitimate user and attacker — the second use triggers the revoke, both sides get kicked.
467
+
468
+ ## Family — the rotation chain
469
+
470
+ ```
471
+ login → creates family X — refresh token A in family X
472
+ refresh (A) → revokes A; creates B in family X
473
+ refresh (B) → revokes B; creates C in family X
474
+ refresh (A again)→ A is revoked → revoke family X entirely
475
+ ```
476
+
477
+ 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.
478
+
479
+ ## Listing active sessions
480
+
481
+ ```ts
482
+ const sessions = await authService.getActiveSessions(user);
483
+
484
+ for (const session of sessions) {
485
+ session.get("device_info"); // { userAgent, ip, deviceId? } if provided at login
486
+ session.get("created_at");
487
+ session.get("expires_at");
488
+ }
489
+ ```
490
+
491
+ Use this for "active sessions" UIs. Revoke a specific session by calling `.revoke()` on the `RefreshToken` instance.
492
+
493
+ ## Removing tokens
494
+
495
+ ```ts
496
+ // Specific access token
497
+ await authService.removeAccessToken(user, accessTokenString);
498
+
499
+ // Specific refresh token (via the RefreshToken instance)
500
+ const rt = await RefreshToken.first({ token: refreshString });
501
+ await rt?.revoke();
502
+
503
+ // All access tokens for a user
504
+ await authService.removeAllAccessTokens(user);
505
+
506
+ // Everything — access + refresh + family
507
+ await authService.revokeAllTokens(user);
508
+
509
+ // A specific family
510
+ await authService.revokeTokenFamily(familyId);
511
+ ```
512
+
513
+ ## Max refresh tokens per user
514
+
515
+ ```ts
516
+ // In config.auth.jwt.refresh:
517
+ {
518
+ maxPerUser: 5, // default
519
+ }
520
+ ```
521
+
522
+ When issuing a new refresh token, the service counts active tokens for the user and revokes the oldest until count < `maxPerUser`. Pattern: limits how many simultaneous sessions a user can hold; prevents an attacker who got a token from gradually accumulating many.
523
+
524
+ ## Expired-token cleanup
525
+
526
+ ```ts
527
+ const cleaned = await authService.cleanupExpiredTokens();
528
+ // Returns: number of expired refresh tokens removed.
529
+ // Fires "token.expired" event per token + "cleanup.completed" with the count.
530
+ ```
531
+
532
+ Run this periodically via the scheduler:
533
+
534
+ ```ts
535
+ import { scheduler, job } from "@warlock.js/scheduler";
536
+ import { authService } from "@warlock.js/auth";
537
+
538
+ scheduler.addJob(
539
+ job("auth-cleanup", () => authService.cleanupExpiredTokens())
540
+ .daily()
541
+ .at("03:00"),
542
+ );
543
+ ```
544
+
545
+ Or use the bundled CLI command — see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md).
546
+
547
+ ## JWT helpers
548
+
549
+ For low-level JWT signing/verification (outside the authService flow):
550
+
551
+ ```ts
552
+ import { jwt } from "@warlock.js/auth";
553
+
554
+ const token = await jwt.generate(payload, { expiresIn: 3600 });
555
+ const decoded = await jwt.verify(token);
556
+
557
+ const refreshToken = await jwt.generateRefreshToken(payload, { expiresIn });
558
+ const decodedRefresh = await jwt.verifyRefreshToken(refreshToken);
559
+ ```
560
+
561
+ The package signs access and refresh tokens with independent secrets — `config.auth.jwt.secret` and `config.auth.jwt.refresh.secret`. Setting a distinct `refresh.secret` is recommended: it prevents an access-token compromise from forging refresh tokens (and vice versa). The refresh secret is **optional** — when `config.auth.jwt.refresh.secret` is unset, refresh tokens fall back to the main `config.auth.jwt.secret`, so refresh works out of the box without a second secret.
562
+
563
+ ## Things NOT to do
564
+
565
+ - Don't use raw JWT libraries directly. The package handles signing, verification, secret loading, and the access/refresh split.
566
+ - Don't disable rotation (`config.auth.jwt.refresh.rotation = false`) unless you genuinely understand the tradeoff — you lose replay detection.
567
+ - 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.
568
+ - 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.
569
+
570
+ ## See also
571
+
572
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — full login/logout flow that uses these primitives
573
+ - [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — the bundled cleanup command
574
+ - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — scheduling cleanup
575
+
576
+
577
+ ## overview `@warlock.js/auth/overview/SKILL.md`
578
+
579
+ ---
580
+ name: overview
581
+ 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).'
582
+ ---
583
+
584
+ # `@warlock.js/auth` — overview
585
+
586
+ JWT authentication for Warlock apps. You get a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` that runs login/logout/refresh (with refresh-token rotation and replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, and two bundled CLI commands.
587
+
588
+ Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
589
+
590
+ ## When to reach for it
591
+
592
+ - You're building a Warlock app that needs login, protected routes, and token-based sessions.
593
+ - You need **multiple user types** (admins + regular users, or client/vendor/staff personas) gated separately on the same auth system.
594
+ - You want refresh-token **rotation + replay detection** out of the box rather than hand-rolling token security.
595
+
596
+ Skip if you're not on `@warlock.js/core` (the package depends on it), or if you need session-cookie auth rather than JWTs.
597
+
598
+ ## The mental model in one paragraph
599
+
600
+ 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.
601
+
602
+ ## Skills index
603
+
604
+ Seven task skills. Most apps need `auth-basics` + `protect-routes` + `handle-login-and-logout`.
605
+
606
+ ### Foundations
607
+
608
+ #### [`auth-basics`](@warlock.js/auth/auth-basics/SKILL.md)
609
+ Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/logout/refresh), AccessToken + RefreshToken persistence, multi-user-type support.
610
+
611
+ ### The flows
612
+
613
+ #### [`handle-login-and-logout`](@warlock.js/auth/handle-login-and-logout/SKILL.md)
614
+ `authService.login(Model, credentials, deviceInfo?)` — verify password, issue the token pair, fire events. `authService.logout(user, accessToken?, refreshToken?)` — revoke tokens. For your `POST /login` and `POST /logout` controllers.
615
+
616
+ #### [`register-user`](@warlock.js/auth/register-user/SKILL.md)
617
+ Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
618
+
619
+ #### [`protect-routes`](@warlock.js/auth/protect-routes/SKILL.md)
620
+ `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.user` + `request.decodedAccessToken`, responds 401 on failure.
621
+
622
+ ### Going deeper
623
+
624
+ #### [`manage-tokens`](@warlock.js/auth/manage-tokens/SKILL.md)
625
+ The token lifecycle — `generateAccessToken`, `createRefreshToken`, `createTokenPair`, `refreshTokens` (rotation + replay detection), `revokeAllTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`. For custom login/registration, token revocation, "logout everywhere", and scheduled cleanup.
626
+
627
+ #### [`customize-user-type`](@warlock.js/auth/customize-user-type/SKILL.md)
628
+ Support multiple user types in one system — each `Auth` subclass overrides `userType`, `config.auth.userType.<slug>` maps the slug to a model class, `authMiddleware("admin")` / `authMiddleware(["admin", "staff"])` gates per type.
629
+
630
+ #### [`run-auth-commands`](@warlock.js/auth/run-auth-commands/SKILL.md)
631
+ Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `warlock auth.cleanup` (remove expired refresh tokens). Register via `registerJWTSecretGeneratorCommand()` and `registerAuthCleanupCommand()`.
632
+
633
+ ## What this package deliberately doesn't do
634
+
635
+ - **Session-cookie auth.** It's JWT/token-based. If you need server-side sessions, this isn't it.
636
+ - **OAuth / social login / SSO.** No provider adapters here — wire those at the controller layer and create the user through this package's models.
637
+ - **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user *type*, not fine-grained permissions. Build RBAC on top.
638
+ - **Standalone use.** It depends on `@warlock.js/core` for routing, models (Cascade), and config.
639
+
640
+ ## See also
641
+
642
+ - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the framework auth runs inside (routing, middleware, config).
643
+ - [`@warlock.js/cascade/cascade-basics/SKILL.md`](@warlock.js/cascade/cascade-basics/SKILL.md) — the ORM behind the `Auth`, `AccessToken`, and `RefreshToken` models.
644
+ - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes `.claude/skills/warlock-js-auth-overview/`.
645
+
646
+
647
+ ## protect-routes `@warlock.js/auth/protect-routes/SKILL.md`
648
+
649
+ ---
650
+ name: protect-routes
651
+ 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.user + request.decodedAccessToken on success, 401 on failure. Triggers: `authMiddleware`, `request.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.'
652
+ ---
653
+
654
+ # Gate routes with `authMiddleware`
655
+
656
+ `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.
657
+
658
+ ## Two modes
659
+
660
+ Middleware is attached via the route's `options.middleware` array (the third argument) — never as a positional argument.
661
+
662
+ ```ts
663
+ import { authMiddleware } from "@warlock.js/auth";
664
+
665
+ // Mode 1 — required, any user type
666
+ // Rejects with 401 if no valid token; any authenticated user passes.
667
+ router.get("/account", accountController, {
668
+ middleware: [authMiddleware([])], // empty array = "any logged-in user"
669
+ });
670
+
671
+ // Mode 2 — required, specific user type(s)
672
+ // Rejects with 401 if no token OR if token's userType isn't allowed.
673
+ router.get("/admin", adminController, {
674
+ middleware: [authMiddleware("admin")],
675
+ });
676
+
677
+ router.get("/staff", staffController, {
678
+ middleware: [authMiddleware(["admin", "staff"])],
679
+ });
680
+ ```
681
+
682
+ The `userType` slug must match a key in `config.auth.userType.<name>` — see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md).
683
+
684
+ ## What the middleware does
685
+
686
+ On success, before your controller runs:
687
+
688
+ ```ts
689
+ request.user = <hydrated user model instance>;
690
+ request.decodedAccessToken = <decoded JWT payload>;
691
+ ```
692
+
693
+ The user is loaded via `Model.find(decodedToken.id)` against the `config.auth.userType[userType]` class. If the user no longer exists (deleted), the access token row is destroyed and the request gets 401.
694
+
695
+ On failure, the middleware returns one of these 401 responses:
696
+
697
+ | Error code | When |
698
+ | --- | --- |
699
+ | `MissingAccessToken` | No `Authorization` header |
700
+ | `InvalidAccessToken` | Token doesn't verify (signature, expired, doesn't match DB) |
701
+ | `Unauthorized` | Token valid but user-type isn't in the allowed list |
702
+
703
+ ## Reading the user in a controller
704
+
705
+ ```ts
706
+ async function accountController(request: Request, response: Response) {
707
+ const user = request.user!; // typed via your Auth subclass
708
+ return response.success({
709
+ id: user.id,
710
+ email: user.get("email"),
711
+ });
712
+ }
713
+ ```
714
+
715
+ Because the middleware always requires a valid token, `request.user` is guaranteed present inside any gated controller (the middleware would have responded 401 otherwise). The `!` is safe here.
716
+
717
+ ## Route-group protection
718
+
719
+ ```ts
720
+ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () => {
721
+ router.get("/users", listUsersController);
722
+ router.post("/users", createUserController);
723
+ });
724
+ ```
725
+
726
+ 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.
727
+
728
+ ## No optional / fallthrough auth
729
+
730
+ There is no "hydrate `request.user` if a token is present, otherwise continue" mode. `authMiddleware` always requires a valid token. If a route should be reachable anonymously, leave the middleware off — and read the token yourself in the controller if you want soft personalization:
731
+
732
+ ```ts
733
+ async function feedController(request: Request, response: Response) {
734
+ const token = request.authorizationValue;
735
+ // optionally decode/hydrate manually when a token is present
736
+ }
737
+ ```
738
+
739
+ ## Custom error responses
740
+
741
+ The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
742
+
743
+ ## Things NOT to do
744
+
745
+ - Don't call `authMiddleware` outside route definition. It returns a function — the function is what runs per-request. Calling it once per request creates a fresh middleware on every hit (wasteful) and a fresh allowed-types Set (correctness if the input changes per call).
746
+ - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
747
+ - Don't trust `request.user` set by client-supplied headers. The middleware is the only place that sets it on the server — client headers can't reach this slot.
748
+ - Don't pass an unknown user-type to `authMiddleware("typo")`. The middleware will reject every request because the lookup fails. Test the wire-up with a real token of each user type.
749
+
750
+ ## See also
751
+
752
+ - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
753
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — where the access token gets issued in the first place
754
+
755
+
756
+ ## register-user `@warlock.js/auth/register-user/SKILL.md`
757
+
758
+ ---
759
+ name: register-user
760
+ 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`.'
761
+ ---
762
+
763
+ # Register-and-issue-tokens flow
764
+
765
+ Two-step on the server: create the user (with hashed password), then issue tokens. Cascade handles the persistence; `authService` handles the tokens.
766
+
767
+ ## The minimal shape
768
+
769
+ ```ts
770
+ import { authService } from "@warlock.js/auth";
771
+ import { hashPassword } from "@warlock.js/core";
772
+ import { User } from "@/app/users/models/user.model";
773
+
774
+ async function registerController(request: Request, response: Response) {
775
+ const { email, password, name } = request.all();
776
+
777
+ // 1. Check duplicates
778
+ const existing = await User.first({ email });
779
+ if (existing) {
780
+ return response.conflict({ error: "Email already registered" });
781
+ }
782
+
783
+ // 2. Create the user with hashed password
784
+ const user = await User.create({
785
+ email,
786
+ name,
787
+ password: await hashPassword(password),
788
+ });
789
+
790
+ // 3. Issue tokens
791
+ const tokens = await authService.createTokenPair(user, {
792
+ userAgent: request.header("user-agent"),
793
+ ip: request.ip,
794
+ });
795
+
796
+ // 4. Respond
797
+ return response.successCreate({
798
+ user, // shape via static toJsonColumns / static resource
799
+ tokens,
800
+ });
801
+ }
802
+ ```
803
+
804
+ That's the whole flow. `User.create({...})` runs the schema validation (including `.email()`, `.min()`, etc. on each field), so you don't need a separate validation pass — see [`@warlock.js/seal/handle-seal-errors/SKILL.md`](@warlock.js/seal/handle-seal-errors/SKILL.md) for catching validation failures.
805
+
806
+ ## Hash the password on the way in
807
+
808
+ Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
809
+
810
+ ```ts
811
+ import { hashPassword, verifyPassword } from "@warlock.js/core";
812
+
813
+ const hash = await hashPassword("plaintext"); // store this
814
+ const ok = await verifyPassword("plaintext", hash); // compare on login
815
+ ```
816
+
817
+ `authService.attemptLogin` already calls `verifyPassword` against the stored hash — you don't compare passwords manually.
818
+
819
+ ## Schema enforcement
820
+
821
+ Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
822
+
823
+ ```ts
824
+ const userSchema = v.object({
825
+ email: v.string().email(),
826
+ name: v.string().min(2).max(120),
827
+ password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
828
+ // status, role, etc.
829
+ });
830
+ ```
831
+
832
+ But **don't return the password in the public output**:
833
+
834
+ ```ts
835
+ @RegisterModel()
836
+ export class User extends Model<UserSchema> {
837
+ public static table = "users";
838
+ public static schema = userSchema;
839
+ public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
840
+ }
841
+ ```
842
+
843
+ Without this, `JSON.stringify(user)` in your response leaks the hash.
844
+
845
+ ## Email verification flow (extending registration)
846
+
847
+ Common pattern: create the user as `email_verified = false`, send a verification email, mark verified on click. The auth package doesn't ship this; build it on top:
848
+
849
+ ```ts
850
+ const user = await User.create({
851
+ ...data,
852
+ email_verified: false,
853
+ verification_token: Random.string(64),
854
+ });
855
+
856
+ await mailer.sendVerificationEmail(user.get("email"), user.get("verification_token"));
857
+
858
+ const tokens = await authService.createTokenPair(user);
859
+ return response.successCreate({ user, tokens });
860
+ ```
861
+
862
+ Optional: pre-verification, restrict the user to a `unverified` user-type and gate routes accordingly via `authMiddleware("user")`. After verification, swap user-type to `user`.
863
+
864
+ ## Side effects via auth events
865
+
866
+ Hook post-registration logic:
867
+
868
+ ```ts
869
+ import { authEvents } from "@warlock.js/auth";
870
+
871
+ authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
872
+ if (user.get("created_at") > new Date(Date.now() - 5000)) {
873
+ // freshly created in the last 5s — treat as registration
874
+ await sendWelcomeEmail(user);
875
+ }
876
+ });
877
+ ```
878
+
879
+ Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
880
+
881
+ ## Things NOT to do
882
+
883
+ - Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
884
+ - Don't return the user without `toJsonColumns` / `resource` shaping — the password hash will leak otherwise.
885
+ - Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
886
+ - Don't run "send welcome email" inline in the controller. Push it to a queue or run it after-commit via the outbox pattern — see [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md).
887
+
888
+ ## See also
889
+
890
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
891
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
892
+ - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
893
+
894
+
895
+ ## run-auth-commands `@warlock.js/auth/run-auth-commands/SKILL.md`
896
+
897
+ ---
898
+ name: run-auth-commands
899
+ description: 'Two bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env) and warlock auth.cleanup (removes expired refresh tokens). Register via registerJWTSecretGeneratorCommand() and registerAuthCleanupCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `cleanupExpiredTokens`, `command`; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand } 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`.'
900
+ ---
901
+
902
+ # Run auth commands
903
+
904
+ The package ships two CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
905
+
906
+ ## Register
907
+
908
+ ```ts title="warlock.config.ts"
909
+ import {
910
+ registerAuthCleanupCommand,
911
+ registerJWTSecretGeneratorCommand,
912
+ } from "@warlock.js/auth";
913
+ import { defineConfig } from "@warlock.js/core";
914
+
915
+ export default defineConfig({
916
+ cli: {
917
+ commands: [
918
+ registerJWTSecretGeneratorCommand(),
919
+ registerAuthCleanupCommand(),
920
+ ],
921
+ },
922
+ });
923
+ ```
924
+
925
+ ## `warlock jwt.generate` — JWT secret bootstrap
926
+
927
+ ```bash
928
+ yarn warlock jwt.generate
929
+ ```
930
+
931
+ Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
932
+
933
+ Run it once when setting up a new project. Each developer typically runs it locally; production secrets come from your secret manager (Vault, AWS Secrets Manager, k8s secrets) and bypass this command.
934
+
935
+ **Don't commit `.env`.** The generated secret should never live in the repo. The command writes to `.env`, which `.gitignore` already excludes in a default Warlock project.
936
+
937
+ ## `warlock auth.cleanup` — expired token sweep
938
+
939
+ ```bash
940
+ yarn warlock auth.cleanup
941
+ ```
942
+
943
+ Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed. Fires `token.expired` per token and `cleanup.completed` once.
944
+
945
+ Schedule it periodically. Two common shapes:
946
+
947
+ ### Via the scheduler
948
+
949
+ ```ts
950
+ import { scheduler, job } from "@warlock.js/scheduler";
951
+ import { authService } from "@warlock.js/auth";
952
+
953
+ scheduler.addJob(
954
+ job("auth-cleanup", () => authService.cleanupExpiredTokens())
955
+ .daily()
956
+ .at("03:00")
957
+ .preventOverlap(),
958
+ );
959
+
960
+ scheduler.start();
961
+ ```
962
+
963
+ In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
964
+
965
+ ### Via system cron
966
+
967
+ ```cron
968
+ 0 3 * * * cd /path/to/app && /usr/local/bin/yarn warlock auth.cleanup
969
+ ```
970
+
971
+ Out-of-process — works when you don't want the scheduler subsystem running in this service.
972
+
973
+ ## How often?
974
+
975
+ Once a day is usually enough. The check is cheap (single indexed DELETE on `expires_at < now()`), and refresh tokens that have already expired don't grant access — cleanup is housekeeping, not security.
976
+
977
+ If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
978
+
979
+ ## Custom commands
980
+
981
+ If `auth.cleanup` doesn't cover everything your app needs (e.g. you also want to revoke tokens for inactive users), write your own command and combine the auth service helpers:
982
+
983
+ ```ts
984
+ import { command } from "@warlock.js/core";
985
+ import { authService } from "@warlock.js/auth";
986
+ import { User } from "@/app/users/models/user.model";
987
+
988
+ export function registerDeepCleanupCommand() {
989
+ return command({
990
+ name: "auth.deep-cleanup",
991
+ description: "Expire stale tokens AND revoke tokens for inactive users",
992
+ preload: {
993
+ env: true,
994
+ config: ["auth", "database"],
995
+ connectors: ["database"],
996
+ },
997
+ action: async () => {
998
+ await authService.cleanupExpiredTokens();
999
+
1000
+ const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
1001
+
1002
+ for (const user of stale) {
1003
+ await authService.revokeAllTokens(user);
1004
+ }
1005
+ },
1006
+ });
1007
+ }
1008
+ ```
1009
+
1010
+ Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
1011
+
1012
+ ## Things NOT to do
1013
+
1014
+ - Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
1015
+ - Don't run `auth.cleanup` from a long-running scheduler at sub-minute intervals. The DELETE itself is cheap, but the per-token `token.expired` event fan-out has cost. Hourly is plenty even at scale.
1016
+ - Don't put the JWT secret in your codebase fallback (`env("JWT_SECRET", "dev-secret")`). A missing secret should fail the boot — not silently degrade to a dev value.
1017
+
1018
+ ## See also
1019
+
1020
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
1021
+ - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
1022
+
1023
+