@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
@@ -0,0 +1,169 @@
1
+ ---
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`.'
4
+ ---
5
+
6
+ # Manage tokens
7
+
8
+ 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.
9
+
10
+ ## Token shapes
11
+
12
+ ```ts
13
+ type AccessTokenOutput = { token: string; expiresAt: string };
14
+ type RefreshTokenOutput = { token: string; expiresAt: string };
15
+
16
+ type TokenPair = {
17
+ accessToken: AccessTokenOutput;
18
+ refreshToken?: RefreshTokenOutput; // omitted if config.auth.jwt.refresh.enabled = false
19
+ };
20
+ ```
21
+
22
+ ## Issuing tokens
23
+
24
+ ```ts
25
+ import { authService } from "@warlock.js/auth";
26
+
27
+ // Just an access token (rare — usually use createTokenPair)
28
+ const access = await authService.generateAccessToken(user);
29
+
30
+ // Just a refresh token
31
+ const refresh = await authService.createRefreshToken(user, deviceInfo);
32
+
33
+ // Both — the everyday case
34
+ const pair = await authService.createTokenPair(user, deviceInfo);
35
+ ```
36
+
37
+ `createTokenPair` is the typical issuance path. It respects `config.auth.jwt.refresh.enabled` — if disabled, returns only `accessToken`.
38
+
39
+ ## Refresh with rotation — `refreshTokens`
40
+
41
+ ```ts
42
+ const next = await authService.refreshTokens(oldRefreshToken, deviceInfo);
43
+ // next: TokenPair | null
44
+ ```
45
+
46
+ What happens internally:
47
+
48
+ 1. Verify the JWT signature on the old refresh token.
49
+ 2. Find the row in `RefreshToken` — must exist + not be revoked.
50
+ 3. Look up the user via `config.auth.userType[token.userType]`.
51
+ 4. **Rotation** (default — `config.auth.jwt.refresh.rotation = true`): revoke the old refresh token, create a new pair from the same `family_id`.
52
+ 5. **No rotation**: mark the old as "used" but keep it valid.
53
+
54
+ **Replay detection.** If the old refresh token is presented again after rotation (already revoked but still in the DB):
55
+
56
+ ```ts
57
+ // Inside refreshTokens, on a revoked-token presentation:
58
+ await authService.revokeTokenFamily(refreshToken.get("family_id"));
59
+ ```
60
+
61
+ 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.
62
+
63
+ ## Family — the rotation chain
64
+
65
+ ```
66
+ login → creates family X — refresh token A in family X
67
+ refresh (A) → revokes A; creates B in family X
68
+ refresh (B) → revokes B; creates C in family X
69
+ refresh (A again)→ A is revoked → revoke family X entirely
70
+ ```
71
+
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.
73
+
74
+ ## Listing active sessions
75
+
76
+ ```ts
77
+ const sessions = await authService.getActiveSessions(user);
78
+
79
+ for (const session of sessions) {
80
+ session.get("device_info"); // { userAgent, ip, deviceId? } if provided at login
81
+ session.get("created_at");
82
+ session.get("expires_at");
83
+ }
84
+ ```
85
+
86
+ Use this for "active sessions" UIs. Revoke a specific session by calling `.revoke()` on the `RefreshToken` instance.
87
+
88
+ ## Removing tokens
89
+
90
+ ```ts
91
+ // Specific access token
92
+ await authService.removeAccessToken(user, accessTokenString);
93
+
94
+ // Specific refresh token (via the RefreshToken instance)
95
+ const rt = await RefreshToken.first({ token: refreshString });
96
+ await rt?.revoke();
97
+
98
+ // All access tokens for a user
99
+ await authService.removeAllAccessTokens(user);
100
+
101
+ // Everything — access + refresh + family
102
+ await authService.revokeAllTokens(user);
103
+
104
+ // A specific family
105
+ await authService.revokeTokenFamily(familyId);
106
+ ```
107
+
108
+ ## Max refresh tokens per user
109
+
110
+ ```ts
111
+ // In config.auth.jwt.refresh:
112
+ {
113
+ maxPerUser: 5, // default
114
+ }
115
+ ```
116
+
117
+ 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.
118
+
119
+ ## Expired-token cleanup
120
+
121
+ ```ts
122
+ const cleaned = await authService.cleanupExpiredTokens();
123
+ // Returns: number of expired refresh tokens removed.
124
+ // Fires "token.expired" event per token + "cleanup.completed" with the count.
125
+ ```
126
+
127
+ Run this periodically via the scheduler:
128
+
129
+ ```ts
130
+ import { scheduler, job } from "@warlock.js/scheduler";
131
+ import { authService } from "@warlock.js/auth";
132
+
133
+ scheduler.addJob(
134
+ job("auth-cleanup", () => authService.cleanupExpiredTokens())
135
+ .daily()
136
+ .at("03:00"),
137
+ );
138
+ ```
139
+
140
+ Or use the bundled CLI command — see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md).
141
+
142
+ ## JWT helpers
143
+
144
+ For low-level JWT signing/verification (outside the authService flow):
145
+
146
+ ```ts
147
+ import { jwt } from "@warlock.js/auth";
148
+
149
+ const token = await jwt.generate(payload, { expiresIn: 3600 });
150
+ const decoded = await jwt.verify(token);
151
+
152
+ const refreshToken = await jwt.generateRefreshToken(payload, { expiresIn });
153
+ const decodedRefresh = await jwt.verifyRefreshToken(refreshToken);
154
+ ```
155
+
156
+ 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.
157
+
158
+ ## Things NOT to do
159
+
160
+ - Don't use raw JWT libraries directly. The package handles signing, verification, secret loading, and the access/refresh split.
161
+ - Don't disable rotation (`config.auth.jwt.refresh.rotation = false`) unless you genuinely understand the tradeoff — you lose replay detection.
162
+ - 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.
163
+ - 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.
164
+
165
+ ## See also
166
+
167
+ - [`@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
168
+ - [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — the bundled cleanup command
169
+ - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — scheduling cleanup
@@ -0,0 +1,66 @@
1
+ ---
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).'
4
+ ---
5
+
6
+ # `@warlock.js/auth` — overview
7
+
8
+ 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.
9
+
10
+ Coupled to `@warlock.js/core` — you're inside a Warlock project before this package makes sense.
11
+
12
+ ## When to reach for it
13
+
14
+ - You're building a Warlock app that needs login, protected routes, and token-based sessions.
15
+ - You need **multiple user types** (admins + regular users, or client/vendor/staff personas) gated separately on the same auth system.
16
+ - You want refresh-token **rotation + replay detection** out of the box rather than hand-rolling token security.
17
+
18
+ Skip if you're not on `@warlock.js/core` (the package depends on it), or if you need session-cookie auth rather than JWTs.
19
+
20
+ ## The mental model in one paragraph
21
+
22
+ 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.
23
+
24
+ ## Skills index
25
+
26
+ Seven task skills. Most apps need `auth-basics` + `protect-routes` + `handle-login-and-logout`.
27
+
28
+ ### Foundations
29
+
30
+ #### [`auth-basics`](@warlock.js/auth/auth-basics/SKILL.md)
31
+ Start here. The `Auth` base model, `authMiddleware` gate, `authService` (login/logout/refresh), AccessToken + RefreshToken persistence, multi-user-type support.
32
+
33
+ ### The flows
34
+
35
+ #### [`handle-login-and-logout`](@warlock.js/auth/handle-login-and-logout/SKILL.md)
36
+ `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.
37
+
38
+ #### [`register-user`](@warlock.js/auth/register-user/SKILL.md)
39
+ Sign up a new user and issue the first token pair — `User.create({ ...password: await hashPassword(plain) })` then `authService.createTokenPair(user)`. For `POST /register`.
40
+
41
+ #### [`protect-routes`](@warlock.js/auth/protect-routes/SKILL.md)
42
+ `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.
43
+
44
+ ### Going deeper
45
+
46
+ #### [`manage-tokens`](@warlock.js/auth/manage-tokens/SKILL.md)
47
+ 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.
48
+
49
+ #### [`customize-user-type`](@warlock.js/auth/customize-user-type/SKILL.md)
50
+ 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.
51
+
52
+ #### [`run-auth-commands`](@warlock.js/auth/run-auth-commands/SKILL.md)
53
+ Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `warlock auth.cleanup` (remove expired refresh tokens). Register via `registerJWTSecretGeneratorCommand()` and `registerAuthCleanupCommand()`.
54
+
55
+ ## What this package deliberately doesn't do
56
+
57
+ - **Session-cookie auth.** It's JWT/token-based. If you need server-side sessions, this isn't it.
58
+ - **OAuth / social login / SSO.** No provider adapters here — wire those at the controller layer and create the user through this package's models.
59
+ - **Authorization / roles / permissions (RBAC).** It authenticates (who you are) and gates by user *type*, not fine-grained permissions. Build RBAC on top.
60
+ - **Standalone use.** It depends on `@warlock.js/core` for routing, models (Cascade), and config.
61
+
62
+ ## See also
63
+
64
+ - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the framework auth runs inside (routing, middleware, config).
65
+ - [`@warlock.js/cascade/cascade-basics/SKILL.md`](@warlock.js/cascade/cascade-basics/SKILL.md) — the ORM behind the `Auth`, `AccessToken`, and `RefreshToken` models.
66
+ - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes `.claude/skills/warlock-js-auth-overview/`.
@@ -0,0 +1,105 @@
1
+ ---
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.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.'
4
+ ---
5
+
6
+ # Gate routes with `authMiddleware`
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.
9
+
10
+ ## Two modes
11
+
12
+ Middleware is attached via the route's `options.middleware` array (the third argument) — never as a positional argument.
13
+
14
+ ```ts
15
+ import { authMiddleware } from "@warlock.js/auth";
16
+
17
+ // Mode 1 — required, any user type
18
+ // Rejects with 401 if no valid token; any authenticated user passes.
19
+ router.get("/account", accountController, {
20
+ middleware: [authMiddleware([])], // empty array = "any logged-in user"
21
+ });
22
+
23
+ // Mode 2 — required, specific user type(s)
24
+ // Rejects with 401 if no token OR if token's userType isn't allowed.
25
+ router.get("/admin", adminController, {
26
+ middleware: [authMiddleware("admin")],
27
+ });
28
+
29
+ router.get("/staff", staffController, {
30
+ middleware: [authMiddleware(["admin", "staff"])],
31
+ });
32
+ ```
33
+
34
+ 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).
35
+
36
+ ## What the middleware does
37
+
38
+ On success, before your controller runs:
39
+
40
+ ```ts
41
+ request.user = <hydrated user model instance>;
42
+ request.decodedAccessToken = <decoded JWT payload>;
43
+ ```
44
+
45
+ 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.
46
+
47
+ On failure, the middleware returns one of these 401 responses:
48
+
49
+ | Error code | When |
50
+ | --- | --- |
51
+ | `MissingAccessToken` | No `Authorization` header |
52
+ | `InvalidAccessToken` | Token doesn't verify (signature, expired, doesn't match DB) |
53
+ | `Unauthorized` | Token valid but user-type isn't in the allowed list |
54
+
55
+ ## Reading the user in a controller
56
+
57
+ ```ts
58
+ async function accountController(request: Request, response: Response) {
59
+ const user = request.user!; // typed via your Auth subclass
60
+ return response.success({
61
+ id: user.id,
62
+ email: user.get("email"),
63
+ });
64
+ }
65
+ ```
66
+
67
+ 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.
68
+
69
+ ## Route-group protection
70
+
71
+ ```ts
72
+ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () => {
73
+ router.get("/users", listUsersController);
74
+ router.post("/users", createUserController);
75
+ });
76
+ ```
77
+
78
+ 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.
79
+
80
+ ## No optional / fallthrough auth
81
+
82
+ 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:
83
+
84
+ ```ts
85
+ async function feedController(request: Request, response: Response) {
86
+ const token = request.authorizationValue;
87
+ // optionally decode/hydrate manually when a token is present
88
+ }
89
+ ```
90
+
91
+ ## Custom error responses
92
+
93
+ The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
94
+
95
+ ## Things NOT to do
96
+
97
+ - 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).
98
+ - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
99
+ - 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.
100
+ - 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.
101
+
102
+ ## See also
103
+
104
+ - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
105
+ - [`@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
@@ -0,0 +1,135 @@
1
+ ---
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`.'
4
+ ---
5
+
6
+ # Register-and-issue-tokens flow
7
+
8
+ Two-step on the server: create the user (with hashed password), then issue tokens. Cascade handles the persistence; `authService` handles the tokens.
9
+
10
+ ## The minimal shape
11
+
12
+ ```ts
13
+ import { authService } from "@warlock.js/auth";
14
+ import { hashPassword } from "@warlock.js/core";
15
+ import { User } from "@/app/users/models/user.model";
16
+
17
+ async function registerController(request: Request, response: Response) {
18
+ const { email, password, name } = request.all();
19
+
20
+ // 1. Check duplicates
21
+ const existing = await User.first({ email });
22
+ if (existing) {
23
+ return response.conflict({ error: "Email already registered" });
24
+ }
25
+
26
+ // 2. Create the user with hashed password
27
+ const user = await User.create({
28
+ email,
29
+ name,
30
+ password: await hashPassword(password),
31
+ });
32
+
33
+ // 3. Issue tokens
34
+ const tokens = await authService.createTokenPair(user, {
35
+ userAgent: request.header("user-agent"),
36
+ ip: request.ip,
37
+ });
38
+
39
+ // 4. Respond
40
+ return response.successCreate({
41
+ user, // shape via static toJsonColumns / static resource
42
+ tokens,
43
+ });
44
+ }
45
+ ```
46
+
47
+ 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.
48
+
49
+ ## Hash the password on the way in
50
+
51
+ Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
52
+
53
+ ```ts
54
+ import { hashPassword, verifyPassword } from "@warlock.js/core";
55
+
56
+ const hash = await hashPassword("plaintext"); // store this
57
+ const ok = await verifyPassword("plaintext", hash); // compare on login
58
+ ```
59
+
60
+ `authService.attemptLogin` already calls `verifyPassword` against the stored hash — you don't compare passwords manually.
61
+
62
+ ## Schema enforcement
63
+
64
+ Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
65
+
66
+ ```ts
67
+ const userSchema = v.object({
68
+ email: v.string().email(),
69
+ name: v.string().min(2).max(120),
70
+ password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
71
+ // status, role, etc.
72
+ });
73
+ ```
74
+
75
+ But **don't return the password in the public output**:
76
+
77
+ ```ts
78
+ @RegisterModel()
79
+ export class User extends Model<UserSchema> {
80
+ public static table = "users";
81
+ public static schema = userSchema;
82
+ public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
83
+ }
84
+ ```
85
+
86
+ Without this, `JSON.stringify(user)` in your response leaks the hash.
87
+
88
+ ## Email verification flow (extending registration)
89
+
90
+ 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:
91
+
92
+ ```ts
93
+ const user = await User.create({
94
+ ...data,
95
+ email_verified: false,
96
+ verification_token: Random.string(64),
97
+ });
98
+
99
+ await mailer.sendVerificationEmail(user.get("email"), user.get("verification_token"));
100
+
101
+ const tokens = await authService.createTokenPair(user);
102
+ return response.successCreate({ user, tokens });
103
+ ```
104
+
105
+ 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`.
106
+
107
+ ## Side effects via auth events
108
+
109
+ Hook post-registration logic:
110
+
111
+ ```ts
112
+ import { authEvents } from "@warlock.js/auth";
113
+
114
+ authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
115
+ if (user.get("created_at") > new Date(Date.now() - 5000)) {
116
+ // freshly created in the last 5s — treat as registration
117
+ await sendWelcomeEmail(user);
118
+ }
119
+ });
120
+ ```
121
+
122
+ Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
123
+
124
+ ## Things NOT to do
125
+
126
+ - Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
127
+ - Don't return the user without `toJsonColumns` / `resource` shaping — the password hash will leak otherwise.
128
+ - Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
129
+ - 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).
130
+
131
+ ## See also
132
+
133
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
134
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
135
+ - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
@@ -0,0 +1,125 @@
1
+ ---
2
+ name: run-auth-commands
3
+ 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`.'
4
+ ---
5
+
6
+ # Run auth commands
7
+
8
+ The package ships two CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
9
+
10
+ ## Register
11
+
12
+ ```ts title="warlock.config.ts"
13
+ import {
14
+ registerAuthCleanupCommand,
15
+ registerJWTSecretGeneratorCommand,
16
+ } from "@warlock.js/auth";
17
+ import { defineConfig } from "@warlock.js/core";
18
+
19
+ export default defineConfig({
20
+ cli: {
21
+ commands: [
22
+ registerJWTSecretGeneratorCommand(),
23
+ registerAuthCleanupCommand(),
24
+ ],
25
+ },
26
+ });
27
+ ```
28
+
29
+ ## `warlock jwt.generate` — JWT secret bootstrap
30
+
31
+ ```bash
32
+ yarn warlock jwt.generate
33
+ ```
34
+
35
+ Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
36
+
37
+ 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.
38
+
39
+ **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.
40
+
41
+ ## `warlock auth.cleanup` — expired token sweep
42
+
43
+ ```bash
44
+ yarn warlock auth.cleanup
45
+ ```
46
+
47
+ Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed. Fires `token.expired` per token and `cleanup.completed` once.
48
+
49
+ Schedule it periodically. Two common shapes:
50
+
51
+ ### Via the scheduler
52
+
53
+ ```ts
54
+ import { scheduler, job } from "@warlock.js/scheduler";
55
+ import { authService } from "@warlock.js/auth";
56
+
57
+ scheduler.addJob(
58
+ job("auth-cleanup", () => authService.cleanupExpiredTokens())
59
+ .daily()
60
+ .at("03:00")
61
+ .preventOverlap(),
62
+ );
63
+
64
+ scheduler.start();
65
+ ```
66
+
67
+ In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
68
+
69
+ ### Via system cron
70
+
71
+ ```cron
72
+ 0 3 * * * cd /path/to/app && /usr/local/bin/yarn warlock auth.cleanup
73
+ ```
74
+
75
+ Out-of-process — works when you don't want the scheduler subsystem running in this service.
76
+
77
+ ## How often?
78
+
79
+ 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.
80
+
81
+ If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
82
+
83
+ ## Custom commands
84
+
85
+ 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:
86
+
87
+ ```ts
88
+ import { command } from "@warlock.js/core";
89
+ import { authService } from "@warlock.js/auth";
90
+ import { User } from "@/app/users/models/user.model";
91
+
92
+ export function registerDeepCleanupCommand() {
93
+ return command({
94
+ name: "auth.deep-cleanup",
95
+ description: "Expire stale tokens AND revoke tokens for inactive users",
96
+ preload: {
97
+ env: true,
98
+ config: ["auth", "database"],
99
+ connectors: ["database"],
100
+ },
101
+ action: async () => {
102
+ await authService.cleanupExpiredTokens();
103
+
104
+ const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
105
+
106
+ for (const user of stale) {
107
+ await authService.revokeAllTokens(user);
108
+ }
109
+ },
110
+ });
111
+ }
112
+ ```
113
+
114
+ Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
115
+
116
+ ## Things NOT to do
117
+
118
+ - Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
119
+ - 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.
120
+ - 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.
121
+
122
+ ## See also
123
+
124
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
125
+ - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
@@ -1,10 +0,0 @@
1
- /**
2
- * Register the auth:cleanup CLI command
3
- *
4
- * @example
5
- * ```bash
6
- * warlock auth:cleanup
7
- * ```
8
- */
9
- export declare function registerAuthCleanupCommand(): any;
10
- //# sourceMappingURL=auth-cleanup-command.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-cleanup-command.d.ts","sourceRoot":"","sources":["../../src/commands/auth-cleanup-command.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,QAqBzC"}
@@ -1,29 +0,0 @@
1
- import {colors}from'@mongez/copper';import {command}from'@warlock.js/core';import {authService}from'../services/auth.service.js';/**
2
- * Register the auth:cleanup CLI command
3
- *
4
- * @example
5
- * ```bash
6
- * warlock auth:cleanup
7
- * ```
8
- */
9
- function registerAuthCleanupCommand() {
10
- return command({
11
- name: "auth.cleanup",
12
- description: "Remove expired refresh tokens from the database",
13
- preload: {
14
- env: true,
15
- config: ["auth", "database"],
16
- connectors: ["database"],
17
- },
18
- action: async () => {
19
- console.log(colors.cyan("🧹 Cleaning up expired tokens..."));
20
- const count = await authService.cleanupExpiredTokens();
21
- if (count === 0) {
22
- console.log(colors.green("✅ No expired tokens found."));
23
- }
24
- else {
25
- console.log(colors.green(`✅ Removed ${count} expired token(s).`));
26
- }
27
- },
28
- });
29
- }export{registerAuthCleanupCommand};//# sourceMappingURL=auth-cleanup-command.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-cleanup-command.js","sources":["../../src/commands/auth-cleanup-command.ts"],"sourcesContent":[null],"names":[],"mappings":"iIAIA;;;;;;;AAOG;SACa,0BAA0B,GAAA;AACxC,IAAA,OAAO,OAAO,CAAC;AACb,QAAA,IAAI,EAAE,cAAc;AACpB,QAAA,WAAW,EAAE,iDAAiD;AAC9D,QAAA,OAAO,EAAE;AACP,YAAA,GAAG,EAAE,IAAI;AACT,YAAA,MAAM,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC;YAC5B,UAAU,EAAE,CAAC,UAAU,CAAC;AACzB,SAAA;QACD,MAAM,EAAE,YAAW;YACjB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC,CAAC;AAE7D,YAAA,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,oBAAoB,EAAE,CAAC;YAEvD,IAAI,KAAK,KAAK,CAAC,EAAE;gBACf,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAC;AACzD,aAAA;AAAM,iBAAA;AACL,gBAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,UAAA,EAAa,KAAK,CAAA,kBAAA,CAAoB,CAAC,CAAC,CAAC;AACnE,aAAA;SACF;AACF,KAAA,CAAC,CAAC;AACL"}
@@ -1,2 +0,0 @@
1
- export declare function registerJWTSecretGeneratorCommand(): any;
2
- //# sourceMappingURL=jwt-secret-generator-command.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"jwt-secret-generator-command.d.ts","sourceRoot":"","sources":["../../src/commands/jwt-secret-generator-command.ts"],"names":[],"mappings":"AAGA,wBAAgB,iCAAiC,QAMhD"}