@warlock.js/auth 4.0.174 → 4.1.2
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.
- package/README.md +37 -0
- package/cjs/index.cjs +807 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/commands/auth-cleanup-command.d.mts +13 -0
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -0
- package/esm/commands/auth-cleanup-command.mjs +34 -0
- package/esm/commands/auth-cleanup-command.mjs.map +1 -0
- package/esm/commands/jwt-secret-generator-command.d.mts +5 -0
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -0
- package/esm/commands/jwt-secret-generator-command.mjs +15 -0
- package/esm/commands/jwt-secret-generator-command.mjs.map +1 -0
- package/esm/contracts/auth-contract.d.mts +40 -0
- package/esm/contracts/auth-contract.d.mts.map +1 -0
- package/esm/contracts/index.d.mts +2 -0
- package/esm/contracts/types.d.mts +170 -0
- package/esm/contracts/types.d.mts.map +1 -0
- package/esm/contracts/types.mjs +25 -0
- package/esm/contracts/types.mjs.map +1 -0
- package/esm/index.d.mts +15 -0
- package/esm/index.mjs +17 -0
- package/esm/middleware/auth.middleware.d.mts +22 -0
- package/esm/middleware/auth.middleware.d.mts.map +1 -0
- package/esm/middleware/auth.middleware.mjs +72 -0
- package/esm/middleware/auth.middleware.mjs.map +1 -0
- package/esm/middleware/index.mjs +3 -0
- package/esm/models/access-token/access-token.model.d.mts +13 -0
- package/esm/models/access-token/access-token.model.d.mts.map +1 -0
- package/esm/models/access-token/access-token.model.mjs +23 -0
- package/esm/models/access-token/access-token.model.mjs.map +1 -0
- package/esm/models/access-token/index.d.mts +1 -0
- package/esm/models/access-token/index.mjs +3 -0
- package/esm/models/access-token/migration.mjs +24 -0
- package/esm/models/access-token/migration.mjs.map +1 -0
- package/esm/models/auth.model.d.mts +63 -0
- package/esm/models/auth.model.d.mts.map +1 -0
- package/esm/models/auth.model.mjs +77 -0
- package/esm/models/auth.model.mjs.map +1 -0
- package/esm/models/index.d.mts +8 -0
- package/esm/models/index.d.mts.map +1 -0
- package/esm/models/index.mjs +14 -0
- package/esm/models/index.mjs.map +1 -0
- package/esm/models/refresh-token/index.d.mts +1 -0
- package/esm/models/refresh-token/index.mjs +3 -0
- package/esm/models/refresh-token/migration.mjs +27 -0
- package/esm/models/refresh-token/migration.mjs.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts +36 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.mjs +58 -0
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -0
- package/esm/services/auth-events.d.mts +89 -0
- package/esm/services/auth-events.d.mts.map +1 -0
- package/esm/services/auth-events.mjs +68 -0
- package/esm/services/auth-events.mjs.map +1 -0
- package/esm/services/auth.service.d.mts +95 -0
- package/esm/services/auth.service.d.mts.map +1 -0
- package/esm/services/auth.service.mjs +275 -0
- package/esm/services/auth.service.mjs.map +1 -0
- package/esm/services/generate-jwt-secret.d.mts +5 -0
- package/esm/services/generate-jwt-secret.d.mts.map +1 -0
- package/esm/services/generate-jwt-secret.mjs +48 -0
- package/esm/services/generate-jwt-secret.mjs.map +1 -0
- package/esm/services/index.d.mts +4 -0
- package/esm/services/index.mjs +6 -0
- package/esm/services/jwt.d.mts +52 -0
- package/esm/services/jwt.d.mts.map +1 -0
- package/esm/services/jwt.mjs +58 -0
- package/esm/services/jwt.mjs.map +1 -0
- package/esm/utils/auth-error-codes.d.mts +23 -0
- package/esm/utils/auth-error-codes.d.mts.map +1 -0
- package/esm/utils/auth-error-codes.mjs +23 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -0
- package/llms-full.txt +1023 -0
- package/llms.txt +16 -0
- package/package.json +47 -36
- package/skills/auth-basics/SKILL.md +88 -0
- package/skills/customize-user-type/SKILL.md +137 -0
- package/skills/handle-login-and-logout/SKILL.md +160 -0
- package/skills/manage-tokens/SKILL.md +169 -0
- package/skills/overview/SKILL.md +66 -0
- package/skills/protect-routes/SKILL.md +105 -0
- package/skills/register-user/SKILL.md +135 -0
- package/skills/run-auth-commands/SKILL.md +125 -0
- package/esm/commands/auth-cleanup-command.d.ts +0 -10
- package/esm/commands/auth-cleanup-command.d.ts.map +0 -1
- package/esm/commands/auth-cleanup-command.js +0 -29
- package/esm/commands/auth-cleanup-command.js.map +0 -1
- package/esm/commands/jwt-secret-generator-command.d.ts +0 -2
- package/esm/commands/jwt-secret-generator-command.d.ts.map +0 -1
- package/esm/commands/jwt-secret-generator-command.js +0 -7
- package/esm/commands/jwt-secret-generator-command.js.map +0 -1
- package/esm/contracts/auth-contract.d.ts +0 -23
- package/esm/contracts/auth-contract.d.ts.map +0 -1
- package/esm/contracts/index.d.ts +0 -3
- package/esm/contracts/index.d.ts.map +0 -1
- package/esm/contracts/types.d.ts +0 -167
- package/esm/contracts/types.d.ts.map +0 -1
- package/esm/contracts/types.js +0 -20
- package/esm/contracts/types.js.map +0 -1
- package/esm/index.d.ts +0 -8
- package/esm/index.d.ts.map +0 -1
- package/esm/index.js +0 -1
- package/esm/index.js.map +0 -1
- package/esm/middleware/auth.middleware.d.ts +0 -2
- package/esm/middleware/auth.middleware.d.ts.map +0 -1
- package/esm/middleware/auth.middleware.js +0 -72
- package/esm/middleware/auth.middleware.js.map +0 -1
- package/esm/middleware/index.d.ts +0 -2
- package/esm/middleware/index.d.ts.map +0 -1
- package/esm/models/access-token/access-token.model.d.ts +0 -9
- package/esm/models/access-token/access-token.model.d.ts.map +0 -1
- package/esm/models/access-token/access-token.model.js +0 -14
- package/esm/models/access-token/access-token.model.js.map +0 -1
- package/esm/models/access-token/index.d.ts +0 -2
- package/esm/models/access-token/index.d.ts.map +0 -1
- package/esm/models/access-token/migration.d.ts +0 -2
- package/esm/models/access-token/migration.d.ts.map +0 -1
- package/esm/models/access-token/migration.js +0 -22
- package/esm/models/access-token/migration.js.map +0 -1
- package/esm/models/auth.model.d.ts +0 -58
- package/esm/models/auth.model.d.ts.map +0 -1
- package/esm/models/auth.model.js +0 -68
- package/esm/models/auth.model.js.map +0 -1
- package/esm/models/index.d.ts +0 -5
- package/esm/models/index.d.ts.map +0 -1
- package/esm/models/index.js +0 -1
- package/esm/models/index.js.map +0 -1
- package/esm/models/refresh-token/index.d.ts +0 -2
- package/esm/models/refresh-token/index.d.ts.map +0 -1
- package/esm/models/refresh-token/migration.d.ts +0 -2
- package/esm/models/refresh-token/migration.d.ts.map +0 -1
- package/esm/models/refresh-token/migration.js +0 -23
- package/esm/models/refresh-token/migration.js.map +0 -1
- package/esm/models/refresh-token/refresh-token.model.d.ts +0 -32
- package/esm/models/refresh-token/refresh-token.model.d.ts.map +0 -1
- package/esm/models/refresh-token/refresh-token.model.js +0 -53
- package/esm/models/refresh-token/refresh-token.model.js.map +0 -1
- package/esm/services/auth-events.d.ts +0 -85
- package/esm/services/auth-events.d.ts.map +0 -1
- package/esm/services/auth-events.js +0 -65
- package/esm/services/auth-events.js.map +0 -1
- package/esm/services/auth.service.d.ts +0 -92
- package/esm/services/auth.service.d.ts.map +0 -1
- package/esm/services/auth.service.js +0 -322
- package/esm/services/auth.service.js.map +0 -1
- package/esm/services/generate-jwt-secret.d.ts +0 -2
- package/esm/services/generate-jwt-secret.d.ts.map +0 -1
- package/esm/services/generate-jwt-secret.js +0 -47
- package/esm/services/generate-jwt-secret.js.map +0 -1
- package/esm/services/index.d.ts +0 -5
- package/esm/services/index.d.ts.map +0 -1
- package/esm/services/jwt.d.ts +0 -23
- package/esm/services/jwt.d.ts.map +0 -1
- package/esm/services/jwt.js +0 -40
- package/esm/services/jwt.js.map +0 -1
- package/esm/utils/auth-error-codes.d.ts +0 -18
- package/esm/utils/auth-error-codes.d.ts.map +0 -1
- package/esm/utils/auth-error-codes.js +0 -18
- package/esm/utils/auth-error-codes.js.map +0 -1
- package/esm/utils/duration.d.ts +0 -45
- package/esm/utils/duration.d.ts.map +0 -1
- package/esm/utils/duration.js +0 -93
- package/esm/utils/duration.js.map +0 -1
- package/esm/utils/index.d.ts +0 -3
- 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 +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 +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"}
|