@warlock.js/auth 5.23.2 → 5.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +370 -357
- package/LICENSE +21 -21
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/middleware/auth.middleware.mjs +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs +5 -0
- package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +10 -9
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +1927 -1927
- package/package.json +8 -8
- package/skills/auth-basics/SKILL.md +1 -1
- package/skills/customize-token-storage/SKILL.md +160 -160
- package/skills/customize-user-type/SKILL.md +168 -168
- package/skills/handle-login-and-logout/SKILL.md +179 -179
- package/skills/login-with-providers/SKILL.md +283 -283
- package/skills/manage-tokens/SKILL.md +196 -196
- package/skills/overview/SKILL.md +74 -74
- package/skills/protect-routes/SKILL.md +299 -299
- package/skills/register-user/SKILL.md +147 -147
- package/skills/run-auth-commands/SKILL.md +159 -159
- package/skills/throttle-login-attempts/SKILL.md +117 -117
- package/skills/verify-email-and-reset-password/SKILL.md +137 -137
|
@@ -1,147 +1,147 @@
|
|
|
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
|
-
```ts title="src/app/users/models/user/user.model.ts"
|
|
11
|
-
import { Auth } from "@warlock.js/auth";
|
|
12
|
-
import { RegisterModel } from "@warlock.js/cascade";
|
|
13
|
-
|
|
14
|
-
@RegisterModel()
|
|
15
|
-
export class User extends Auth {
|
|
16
|
-
public static table = "users";
|
|
17
|
-
|
|
18
|
-
public get userType(): string {
|
|
19
|
-
return "user";
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## The minimal shape
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
import { authService } from "@warlock.js/auth";
|
|
28
|
-
import { hashPassword, type RequestHandler } from "@warlock.js/core";
|
|
29
|
-
import { User } from "app/users/models/user/user.model";
|
|
30
|
-
|
|
31
|
-
export const registerController: RequestHandler = async ({ request, response }) => {
|
|
32
|
-
const { email, password, name } = request.all();
|
|
33
|
-
|
|
34
|
-
// 1. Check duplicates
|
|
35
|
-
const existing = await User.first({ email });
|
|
36
|
-
if (existing) {
|
|
37
|
-
return response.conflict({ error: "Email already registered" });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 2. Create the user with hashed password
|
|
41
|
-
const user = await User.create({
|
|
42
|
-
email,
|
|
43
|
-
name,
|
|
44
|
-
password: await hashPassword(password),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
// 3. Issue tokens
|
|
48
|
-
const tokens = await authService.createTokenPair(user, {
|
|
49
|
-
userAgent: request.header("user-agent"),
|
|
50
|
-
ip: request.ip,
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// 4. Respond
|
|
54
|
-
return response.successCreate({
|
|
55
|
-
user, // shape via static toJsonColumns / static resource
|
|
56
|
-
tokens,
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
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.
|
|
62
|
-
|
|
63
|
-
## Hash the password on the way in
|
|
64
|
-
|
|
65
|
-
Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { hashPassword, verifyPassword } from "@warlock.js/core";
|
|
69
|
-
|
|
70
|
-
const hash = await hashPassword("plaintext"); // store this
|
|
71
|
-
const ok = await verifyPassword("plaintext", hash); // compare on login
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
`authService.attemptLogin` already calls `verifyPassword` against the stored hash — you don't compare passwords manually.
|
|
75
|
-
|
|
76
|
-
## Schema enforcement
|
|
77
|
-
|
|
78
|
-
Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
|
|
79
|
-
|
|
80
|
-
```ts
|
|
81
|
-
const userSchema = v.object({
|
|
82
|
-
email: v.string().email(),
|
|
83
|
-
name: v.string().min(2).max(120),
|
|
84
|
-
password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
|
|
85
|
-
// status, role, etc.
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
But **don't return the password in the public output**:
|
|
90
|
-
|
|
91
|
-
```ts
|
|
92
|
-
@RegisterModel()
|
|
93
|
-
export class User extends Model<UserSchema> {
|
|
94
|
-
public static table = "users";
|
|
95
|
-
public static schema = userSchema;
|
|
96
|
-
public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Without this, `JSON.stringify(user)` in your response leaks the hash.
|
|
101
|
-
|
|
102
|
-
## Email verification flow (extending registration)
|
|
103
|
-
|
|
104
|
-
Since 5.13 auth ships this. Declare `emailVerifiedAt: v.date().optional()` on the schema, then send the verification after creating the user:
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { authService, sendEmailVerification } from "@warlock.js/auth";
|
|
108
|
-
|
|
109
|
-
const user = await User.create({ ...data, password: await hashPassword(data.password) });
|
|
110
|
-
|
|
111
|
-
await sendEmailVerification(user); // hashed, single-use, 24h token via @warlock.js/notifications
|
|
112
|
-
|
|
113
|
-
const tokens = await authService.createTokenPair(user);
|
|
114
|
-
return response.successCreate({ user, tokens });
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Gate routes that need a confirmed address with `requireVerifiedEmail()` after `authMiddleware`. See [`@warlock.js/auth/verify-email-and-reset-password/SKILL.md`](@warlock.js/auth/verify-email-and-reset-password/SKILL.md).
|
|
118
|
-
|
|
119
|
-
## Side effects via auth events
|
|
120
|
-
|
|
121
|
-
Hook post-registration logic:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
import { authEvents } from "@warlock.js/auth";
|
|
125
|
-
|
|
126
|
-
authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
|
|
127
|
-
if (user.get("created_at") > new Date(Date.now() - 5000)) {
|
|
128
|
-
// freshly created in the last 5s — treat as registration
|
|
129
|
-
await sendWelcomeEmail(user);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
|
|
135
|
-
|
|
136
|
-
## Things NOT to do
|
|
137
|
-
|
|
138
|
-
- Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
|
|
139
|
-
- Don't return the user without `toJsonColumns` / `resource` shaping — the password hash will leak otherwise.
|
|
140
|
-
- Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
|
|
141
|
-
- 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).
|
|
142
|
-
|
|
143
|
-
## See also
|
|
144
|
-
|
|
145
|
-
- [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
|
|
146
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
|
|
147
|
-
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
|
|
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
|
+
```ts title="src/app/users/models/user/user.model.ts"
|
|
11
|
+
import { Auth } from "@warlock.js/auth";
|
|
12
|
+
import { RegisterModel } from "@warlock.js/cascade";
|
|
13
|
+
|
|
14
|
+
@RegisterModel()
|
|
15
|
+
export class User extends Auth {
|
|
16
|
+
public static table = "users";
|
|
17
|
+
|
|
18
|
+
public get userType(): string {
|
|
19
|
+
return "user";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The minimal shape
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { authService } from "@warlock.js/auth";
|
|
28
|
+
import { hashPassword, type RequestHandler } from "@warlock.js/core";
|
|
29
|
+
import { User } from "app/users/models/user/user.model";
|
|
30
|
+
|
|
31
|
+
export const registerController: RequestHandler = async ({ request, response }) => {
|
|
32
|
+
const { email, password, name } = request.all();
|
|
33
|
+
|
|
34
|
+
// 1. Check duplicates
|
|
35
|
+
const existing = await User.first({ email });
|
|
36
|
+
if (existing) {
|
|
37
|
+
return response.conflict({ error: "Email already registered" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. Create the user with hashed password
|
|
41
|
+
const user = await User.create({
|
|
42
|
+
email,
|
|
43
|
+
name,
|
|
44
|
+
password: await hashPassword(password),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 3. Issue tokens
|
|
48
|
+
const tokens = await authService.createTokenPair(user, {
|
|
49
|
+
userAgent: request.header("user-agent"),
|
|
50
|
+
ip: request.ip,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 4. Respond
|
|
54
|
+
return response.successCreate({
|
|
55
|
+
user, // shape via static toJsonColumns / static resource
|
|
56
|
+
tokens,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
## Hash the password on the way in
|
|
64
|
+
|
|
65
|
+
Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { hashPassword, verifyPassword } from "@warlock.js/core";
|
|
69
|
+
|
|
70
|
+
const hash = await hashPassword("plaintext"); // store this
|
|
71
|
+
const ok = await verifyPassword("plaintext", hash); // compare on login
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`authService.attemptLogin` already calls `verifyPassword` against the stored hash — you don't compare passwords manually.
|
|
75
|
+
|
|
76
|
+
## Schema enforcement
|
|
77
|
+
|
|
78
|
+
Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const userSchema = v.object({
|
|
82
|
+
email: v.string().email(),
|
|
83
|
+
name: v.string().min(2).max(120),
|
|
84
|
+
password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
|
|
85
|
+
// status, role, etc.
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
But **don't return the password in the public output**:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
@RegisterModel()
|
|
93
|
+
export class User extends Model<UserSchema> {
|
|
94
|
+
public static table = "users";
|
|
95
|
+
public static schema = userSchema;
|
|
96
|
+
public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Without this, `JSON.stringify(user)` in your response leaks the hash.
|
|
101
|
+
|
|
102
|
+
## Email verification flow (extending registration)
|
|
103
|
+
|
|
104
|
+
Since 5.13 auth ships this. Declare `emailVerifiedAt: v.date().optional()` on the schema, then send the verification after creating the user:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { authService, sendEmailVerification } from "@warlock.js/auth";
|
|
108
|
+
|
|
109
|
+
const user = await User.create({ ...data, password: await hashPassword(data.password) });
|
|
110
|
+
|
|
111
|
+
await sendEmailVerification(user); // hashed, single-use, 24h token via @warlock.js/notifications
|
|
112
|
+
|
|
113
|
+
const tokens = await authService.createTokenPair(user);
|
|
114
|
+
return response.successCreate({ user, tokens });
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Gate routes that need a confirmed address with `requireVerifiedEmail()` after `authMiddleware`. See [`@warlock.js/auth/verify-email-and-reset-password/SKILL.md`](@warlock.js/auth/verify-email-and-reset-password/SKILL.md).
|
|
118
|
+
|
|
119
|
+
## Side effects via auth events
|
|
120
|
+
|
|
121
|
+
Hook post-registration logic:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { authEvents } from "@warlock.js/auth";
|
|
125
|
+
|
|
126
|
+
authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
|
|
127
|
+
if (user.get("created_at") > new Date(Date.now() - 5000)) {
|
|
128
|
+
// freshly created in the last 5s — treat as registration
|
|
129
|
+
await sendWelcomeEmail(user);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
|
|
135
|
+
|
|
136
|
+
## Things NOT to do
|
|
137
|
+
|
|
138
|
+
- Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
|
|
139
|
+
- Don't return the user without `toJsonColumns` / `resource` shaping — the password hash will leak otherwise.
|
|
140
|
+
- Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
|
|
141
|
+
- 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).
|
|
142
|
+
|
|
143
|
+
## See also
|
|
144
|
+
|
|
145
|
+
- [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
|
|
146
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
|
|
147
|
+
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: run-auth-commands
|
|
3
|
-
description: 'Three bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env), warlock auth.cleanup (removes expired refresh tokens), and warlock auth.purge-never-expiring (finds and revokes token rows that can never expire). Register via registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand() and registerAuthPurgeNeverExpiringCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `registerAuthPurgeNeverExpiringCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `warlock auth.purge-never-expiring`, `cleanupExpiredTokens`, `purgeNeverExpiringTokens`, `command`; "token never expires", "no exp claim", "Invalid Date expires_at"; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Run auth commands
|
|
7
|
-
|
|
8
|
-
The package ships three 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
|
-
registerAuthPurgeNeverExpiringCommand,
|
|
16
|
-
registerJWTSecretGeneratorCommand,
|
|
17
|
-
} from "@warlock.js/auth";
|
|
18
|
-
import { defineConfig } from "@warlock.js/core";
|
|
19
|
-
|
|
20
|
-
export default defineConfig({
|
|
21
|
-
cli: {
|
|
22
|
-
commands: [
|
|
23
|
-
registerJWTSecretGeneratorCommand(),
|
|
24
|
-
registerAuthCleanupCommand(),
|
|
25
|
-
registerAuthPurgeNeverExpiringCommand(),
|
|
26
|
-
],
|
|
27
|
-
},
|
|
28
|
-
});
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## `warlock jwt.generate` — JWT secret bootstrap
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npx warlock jwt.generate
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
|
|
38
|
-
|
|
39
|
-
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.
|
|
40
|
-
|
|
41
|
-
**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.
|
|
42
|
-
|
|
43
|
-
## `warlock auth.cleanup` — expired token sweep
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx warlock auth.cleanup
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows and hard-deletes expired or consumed `one_time_tokens` rows (verification/reset tokens, OTP codes, passkey challenges). Fires `token.expired` per refresh token and `cleanup.completed` once.
|
|
50
|
-
|
|
51
|
-
Schedule it periodically. Two common shapes:
|
|
52
|
-
|
|
53
|
-
### Via the scheduler
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { scheduler, job } from "@warlock.js/scheduler";
|
|
57
|
-
import { authService } from "@warlock.js/auth";
|
|
58
|
-
|
|
59
|
-
scheduler.addJob(
|
|
60
|
-
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
61
|
-
.daily()
|
|
62
|
-
.at("03:00")
|
|
63
|
-
.preventOverlap(),
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
scheduler.start();
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
70
|
-
|
|
71
|
-
### Via system cron
|
|
72
|
-
|
|
73
|
-
```cron
|
|
74
|
-
0 3 * * * cd /path/to/app && /usr/local/bin/npx warlock auth.cleanup
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
Out-of-process — works when you don't want the scheduler subsystem running in this service.
|
|
78
|
-
|
|
79
|
-
## How often?
|
|
80
|
-
|
|
81
|
-
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.
|
|
82
|
-
|
|
83
|
-
If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
|
|
84
|
-
|
|
85
|
-
## `warlock auth.purge-never-expiring` — one-off remediation
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npx warlock auth.purge-never-expiring --dry-run # report only
|
|
89
|
-
npx warlock auth.purge-never-expiring # report, then revoke
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Register with `registerAuthPurgeNeverExpiringCommand()`.
|
|
93
|
-
|
|
94
|
-
Finds every token row that **can never retire itself**, on two independent signals: an `expires_at` that is missing or unparseable, and a stored token carrying no `exp` claim. It reports `id` / `user_id` / `user_type` / `expires_at` per row — never the token string, which is a live credential until the row is deleted — and then deletes them. Affected users must log in again.
|
|
95
|
-
|
|
96
|
-
**This is not a substitute for `auth.cleanup`, and `auth.cleanup` is not a substitute for it.** Cleanup selects `expires_at < now`; an `Invalid Date` compares `false` against *every* date, so such a row satisfies neither `< now` nor `> now` and no date predicate can ever reach it. The "no `exp` claim" signal is not in a column at all — it is inside the JWT.
|
|
97
|
-
|
|
98
|
-
Run it once after upgrading to 4.12.0 **if any deployment ever ran an `expiresIn` that `ms` could not parse** (`"30dayz"`, `"0d"`, `""`, a bare number). Those configurations minted tokens that verify forever. It is safe to run when unaffected — it reports nothing and deletes nothing.
|
|
99
|
-
|
|
100
|
-
Unlike `auth.cleanup`, do **not** schedule it: it is a full table scan (it must read rows to judge them, since neither signal is expressible as a `where`), and once the poisoned rows are gone, 4.12.0 cannot create more.
|
|
101
|
-
|
|
102
|
-
## Custom commands
|
|
103
|
-
|
|
104
|
-
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:
|
|
105
|
-
|
|
106
|
-
```ts title="src/app/users/models/user/user.model.ts"
|
|
107
|
-
import { Auth } from "@warlock.js/auth";
|
|
108
|
-
import { RegisterModel } from "@warlock.js/cascade";
|
|
109
|
-
|
|
110
|
-
@RegisterModel()
|
|
111
|
-
export class User extends Auth {
|
|
112
|
-
public static table = "users";
|
|
113
|
-
|
|
114
|
-
public get userType(): string {
|
|
115
|
-
return "user";
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import { command } from "@warlock.js/core";
|
|
122
|
-
import { authService } from "@warlock.js/auth";
|
|
123
|
-
import { User } from "app/users/models/user/user.model";
|
|
124
|
-
|
|
125
|
-
export function registerDeepCleanupCommand() {
|
|
126
|
-
return command({
|
|
127
|
-
name: "auth.deep-cleanup",
|
|
128
|
-
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
129
|
-
preload: {
|
|
130
|
-
env: true,
|
|
131
|
-
config: ["auth", "database"],
|
|
132
|
-
connectors: ["database"],
|
|
133
|
-
},
|
|
134
|
-
action: async () => {
|
|
135
|
-
await authService.cleanupExpiredTokens();
|
|
136
|
-
|
|
137
|
-
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
138
|
-
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
139
|
-
|
|
140
|
-
for (const user of stale) {
|
|
141
|
-
await authService.revokeAllTokens(user);
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
149
|
-
|
|
150
|
-
## Things NOT to do
|
|
151
|
-
|
|
152
|
-
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
153
|
-
- 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.
|
|
154
|
-
- 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.
|
|
155
|
-
|
|
156
|
-
## See also
|
|
157
|
-
|
|
158
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
159
|
-
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
|
|
1
|
+
---
|
|
2
|
+
name: run-auth-commands
|
|
3
|
+
description: 'Three bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env), warlock auth.cleanup (removes expired refresh tokens), and warlock auth.purge-never-expiring (finds and revokes token rows that can never expire). Register via registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand() and registerAuthPurgeNeverExpiringCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `registerAuthPurgeNeverExpiringCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `warlock auth.purge-never-expiring`, `cleanupExpiredTokens`, `purgeNeverExpiringTokens`, `command`; "token never expires", "no exp claim", "Invalid Date expires_at"; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Run auth commands
|
|
7
|
+
|
|
8
|
+
The package ships three 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
|
+
registerAuthPurgeNeverExpiringCommand,
|
|
16
|
+
registerJWTSecretGeneratorCommand,
|
|
17
|
+
} from "@warlock.js/auth";
|
|
18
|
+
import { defineConfig } from "@warlock.js/core";
|
|
19
|
+
|
|
20
|
+
export default defineConfig({
|
|
21
|
+
cli: {
|
|
22
|
+
commands: [
|
|
23
|
+
registerJWTSecretGeneratorCommand(),
|
|
24
|
+
registerAuthCleanupCommand(),
|
|
25
|
+
registerAuthPurgeNeverExpiringCommand(),
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## `warlock jwt.generate` — JWT secret bootstrap
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx warlock jwt.generate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
**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.
|
|
42
|
+
|
|
43
|
+
## `warlock auth.cleanup` — expired token sweep
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx warlock auth.cleanup
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows and hard-deletes expired or consumed `one_time_tokens` rows (verification/reset tokens, OTP codes, passkey challenges). Fires `token.expired` per refresh token and `cleanup.completed` once.
|
|
50
|
+
|
|
51
|
+
Schedule it periodically. Two common shapes:
|
|
52
|
+
|
|
53
|
+
### Via the scheduler
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { scheduler, job } from "@warlock.js/scheduler";
|
|
57
|
+
import { authService } from "@warlock.js/auth";
|
|
58
|
+
|
|
59
|
+
scheduler.addJob(
|
|
60
|
+
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
61
|
+
.daily()
|
|
62
|
+
.at("03:00")
|
|
63
|
+
.preventOverlap(),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
scheduler.start();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
70
|
+
|
|
71
|
+
### Via system cron
|
|
72
|
+
|
|
73
|
+
```cron
|
|
74
|
+
0 3 * * * cd /path/to/app && /usr/local/bin/npx warlock auth.cleanup
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Out-of-process — works when you don't want the scheduler subsystem running in this service.
|
|
78
|
+
|
|
79
|
+
## How often?
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
|
|
84
|
+
|
|
85
|
+
## `warlock auth.purge-never-expiring` — one-off remediation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx warlock auth.purge-never-expiring --dry-run # report only
|
|
89
|
+
npx warlock auth.purge-never-expiring # report, then revoke
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Register with `registerAuthPurgeNeverExpiringCommand()`.
|
|
93
|
+
|
|
94
|
+
Finds every token row that **can never retire itself**, on two independent signals: an `expires_at` that is missing or unparseable, and a stored token carrying no `exp` claim. It reports `id` / `user_id` / `user_type` / `expires_at` per row — never the token string, which is a live credential until the row is deleted — and then deletes them. Affected users must log in again.
|
|
95
|
+
|
|
96
|
+
**This is not a substitute for `auth.cleanup`, and `auth.cleanup` is not a substitute for it.** Cleanup selects `expires_at < now`; an `Invalid Date` compares `false` against *every* date, so such a row satisfies neither `< now` nor `> now` and no date predicate can ever reach it. The "no `exp` claim" signal is not in a column at all — it is inside the JWT.
|
|
97
|
+
|
|
98
|
+
Run it once after upgrading to 4.12.0 **if any deployment ever ran an `expiresIn` that `ms` could not parse** (`"30dayz"`, `"0d"`, `""`, a bare number). Those configurations minted tokens that verify forever. It is safe to run when unaffected — it reports nothing and deletes nothing.
|
|
99
|
+
|
|
100
|
+
Unlike `auth.cleanup`, do **not** schedule it: it is a full table scan (it must read rows to judge them, since neither signal is expressible as a `where`), and once the poisoned rows are gone, 4.12.0 cannot create more.
|
|
101
|
+
|
|
102
|
+
## Custom commands
|
|
103
|
+
|
|
104
|
+
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:
|
|
105
|
+
|
|
106
|
+
```ts title="src/app/users/models/user/user.model.ts"
|
|
107
|
+
import { Auth } from "@warlock.js/auth";
|
|
108
|
+
import { RegisterModel } from "@warlock.js/cascade";
|
|
109
|
+
|
|
110
|
+
@RegisterModel()
|
|
111
|
+
export class User extends Auth {
|
|
112
|
+
public static table = "users";
|
|
113
|
+
|
|
114
|
+
public get userType(): string {
|
|
115
|
+
return "user";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { command } from "@warlock.js/core";
|
|
122
|
+
import { authService } from "@warlock.js/auth";
|
|
123
|
+
import { User } from "app/users/models/user/user.model";
|
|
124
|
+
|
|
125
|
+
export function registerDeepCleanupCommand() {
|
|
126
|
+
return command({
|
|
127
|
+
name: "auth.deep-cleanup",
|
|
128
|
+
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
129
|
+
preload: {
|
|
130
|
+
env: true,
|
|
131
|
+
config: ["auth", "database"],
|
|
132
|
+
connectors: ["database"],
|
|
133
|
+
},
|
|
134
|
+
action: async () => {
|
|
135
|
+
await authService.cleanupExpiredTokens();
|
|
136
|
+
|
|
137
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
138
|
+
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
139
|
+
|
|
140
|
+
for (const user of stale) {
|
|
141
|
+
await authService.revokeAllTokens(user);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
149
|
+
|
|
150
|
+
## Things NOT to do
|
|
151
|
+
|
|
152
|
+
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
153
|
+
- 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.
|
|
154
|
+
- 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.
|
|
155
|
+
|
|
156
|
+
## See also
|
|
157
|
+
|
|
158
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
159
|
+
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
|