@warlock.js/auth 5.4.0 → 5.6.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 +6 -0
- package/llms-full.txt +248 -248
- package/llms.txt +2 -2
- package/package.json +7 -7
- package/skills/auth-basics/SKILL.md +104 -104
- package/skills/run-auth-commands/SKILL.md +144 -144
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to `@warlock.js/auth` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.5.0 - 2026-09-07
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Documentation shipped in this package's `skills/` told users to run `pnpm`-specific commands. `pnpm <binary>` has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
|
|
12
|
+
|
|
7
13
|
## 5.2.3 - 2026-09-02
|
|
8
14
|
|
|
9
15
|
### Fixed
|
package/llms-full.txt
CHANGED
|
@@ -6,110 +6,110 @@
|
|
|
6
6
|
|
|
7
7
|
## auth-basics `@warlock.js/auth/auth-basics/SKILL.md`
|
|
8
8
|
|
|
9
|
-
---
|
|
10
|
-
name: auth-basics
|
|
11
|
-
description: 'Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.'
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
# Auth basics
|
|
15
|
-
|
|
16
|
-
JWT-based authentication for Warlock. `Auth` base model + `authMiddleware` gate + `authService` for login/logout/refresh + `AccessToken` / `RefreshToken` persistence + multi-user-type support.
|
|
17
|
-
|
|
18
|
-
> This skill is the auth **map** — read it first, then load the specific skill for the task.
|
|
19
|
-
|
|
20
|
-
## Install
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## Foundations
|
|
27
|
-
|
|
28
|
-
1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
|
|
29
|
-
2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
|
|
30
|
-
3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
|
|
31
|
-
4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
|
|
32
|
-
5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
|
|
33
|
-
6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
|
|
34
|
-
7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
|
|
35
|
-
|
|
36
|
-
## Minimal wire-up
|
|
37
|
-
|
|
38
|
-
```ts title="warlock.config.ts"
|
|
39
|
-
import {
|
|
40
|
-
authMigrations,
|
|
41
|
-
registerAuthCleanupCommand,
|
|
42
|
-
registerJWTSecretGeneratorCommand,
|
|
43
|
-
} from "@warlock.js/auth";
|
|
44
|
-
import { defineConfig } from "@warlock.js/core";
|
|
45
|
-
|
|
46
|
-
export default defineConfig({
|
|
47
|
-
cli: {
|
|
48
|
-
commands: [
|
|
49
|
-
registerJWTSecretGeneratorCommand(),
|
|
50
|
-
registerAuthCleanupCommand(),
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
database: {
|
|
54
|
-
migrations: authMigrations,
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
```ts title="src/config/auth.ts"
|
|
60
|
-
import { User } from "@/app/users/models/user.model";
|
|
61
|
-
|
|
62
|
-
export default {
|
|
63
|
-
userType: {
|
|
64
|
-
user: User,
|
|
65
|
-
// admin: Admin, // for multi-user-type
|
|
66
|
-
},
|
|
67
|
-
accessToken: {
|
|
68
|
-
secret: env("JWT_SECRET"),
|
|
69
|
-
expiresIn: "1h",
|
|
70
|
-
},
|
|
71
|
-
refreshToken: {
|
|
72
|
-
secret: env("JWT_REFRESH_SECRET"), // recommended: a separate refresh secret
|
|
73
|
-
enabled: true,
|
|
74
|
-
expiresIn: "30d",
|
|
75
|
-
rotation: true,
|
|
76
|
-
maxPerUser: 5,
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Token lifetimes are validated — a bad one throws
|
|
82
|
-
|
|
83
|
-
`expiresIn` (both blocks) must be a duration string the [`ms`](https://github.com/vercel/ms) package parses to a **positive** number: `"1h"`, `"30m"`, `"7d"`, `"30 days"`, or `NO_EXPIRATION` (`"100y"`). Anything else throws on the first token issue, naming the key:
|
|
84
|
-
|
|
85
|
-
```
|
|
86
|
-
auth.accessToken.expiresIn: "30dayz" is not a valid ms duration — use a positive duration string such as "1h", "7d", or NO_EXPIRATION.
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
That is deliberate. `ms` answers `undefined` for a string it can't parse (`"30dayz"`, `"thirty days"`) and `0` for `"0d"`, and a signer given either emits a JWT with **no `exp` claim** — a credential that never expires. A typo must not be able to produce that quietly, so it fails loudly instead of falling back to a default nobody asked for. Defaults still apply when the key is **absent**: `1h` for access, `7d` for refresh.
|
|
90
|
-
|
|
91
|
-
Two values that look right and are not:
|
|
92
|
-
|
|
93
|
-
- `expiresIn: 2592000` — a bare number. `ms` *formats* numbers instead of parsing them, so this is rejected; write `"30d"`.
|
|
94
|
-
- `expiresIn: env("JWT_TTL")` where the variable is unset — resolves to `""` and is rejected. Give the env read a default: `env("JWT_TTL") || "1h"`.
|
|
95
|
-
|
|
96
|
-
## Pick a skill
|
|
97
|
-
|
|
98
|
-
| If the task is about… | Load |
|
|
99
|
-
| --- | --- |
|
|
100
|
-
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
101
|
-
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) |
|
|
102
|
-
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
103
|
-
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
104
|
-
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) |
|
|
105
|
-
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
106
|
-
|
|
107
|
-
## Things NOT to do
|
|
108
|
-
|
|
109
|
-
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
110
|
-
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
111
|
-
- Don't return the raw `User` from a login endpoint without shaping output. Configure `static toJsonColumns` or `static resource` (see [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md)).
|
|
112
|
-
- Don't run `auth.cleanup` from app boot. Schedule it (cron, scheduler) as a periodic task — see [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
9
|
+
---
|
|
10
|
+
name: auth-basics
|
|
11
|
+
description: 'Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Auth basics
|
|
15
|
+
|
|
16
|
+
JWT-based authentication for Warlock. `Auth` base model + `authMiddleware` gate + `authService` for login/logout/refresh + `AccessToken` / `RefreshToken` persistence + multi-user-type support.
|
|
17
|
+
|
|
18
|
+
> This skill is the auth **map** — read it first, then load the specific skill for the task.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @warlock.js/auth
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Foundations
|
|
27
|
+
|
|
28
|
+
1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
|
|
29
|
+
2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
|
|
30
|
+
3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
|
|
31
|
+
4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
|
|
32
|
+
5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
|
|
33
|
+
6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
|
|
34
|
+
7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
|
|
35
|
+
|
|
36
|
+
## Minimal wire-up
|
|
37
|
+
|
|
38
|
+
```ts title="warlock.config.ts"
|
|
39
|
+
import {
|
|
40
|
+
authMigrations,
|
|
41
|
+
registerAuthCleanupCommand,
|
|
42
|
+
registerJWTSecretGeneratorCommand,
|
|
43
|
+
} from "@warlock.js/auth";
|
|
44
|
+
import { defineConfig } from "@warlock.js/core";
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
cli: {
|
|
48
|
+
commands: [
|
|
49
|
+
registerJWTSecretGeneratorCommand(),
|
|
50
|
+
registerAuthCleanupCommand(),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
database: {
|
|
54
|
+
migrations: authMigrations,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```ts title="src/config/auth.ts"
|
|
60
|
+
import { User } from "@/app/users/models/user.model";
|
|
61
|
+
|
|
62
|
+
export default {
|
|
63
|
+
userType: {
|
|
64
|
+
user: User,
|
|
65
|
+
// admin: Admin, // for multi-user-type
|
|
66
|
+
},
|
|
67
|
+
accessToken: {
|
|
68
|
+
secret: env("JWT_SECRET"),
|
|
69
|
+
expiresIn: "1h",
|
|
70
|
+
},
|
|
71
|
+
refreshToken: {
|
|
72
|
+
secret: env("JWT_REFRESH_SECRET"), // recommended: a separate refresh secret
|
|
73
|
+
enabled: true,
|
|
74
|
+
expiresIn: "30d",
|
|
75
|
+
rotation: true,
|
|
76
|
+
maxPerUser: 5,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Token lifetimes are validated — a bad one throws
|
|
82
|
+
|
|
83
|
+
`expiresIn` (both blocks) must be a duration string the [`ms`](https://github.com/vercel/ms) package parses to a **positive** number: `"1h"`, `"30m"`, `"7d"`, `"30 days"`, or `NO_EXPIRATION` (`"100y"`). Anything else throws on the first token issue, naming the key:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
auth.accessToken.expiresIn: "30dayz" is not a valid ms duration — use a positive duration string such as "1h", "7d", or NO_EXPIRATION.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
That is deliberate. `ms` answers `undefined` for a string it can't parse (`"30dayz"`, `"thirty days"`) and `0` for `"0d"`, and a signer given either emits a JWT with **no `exp` claim** — a credential that never expires. A typo must not be able to produce that quietly, so it fails loudly instead of falling back to a default nobody asked for. Defaults still apply when the key is **absent**: `1h` for access, `7d` for refresh.
|
|
90
|
+
|
|
91
|
+
Two values that look right and are not:
|
|
92
|
+
|
|
93
|
+
- `expiresIn: 2592000` — a bare number. `ms` *formats* numbers instead of parsing them, so this is rejected; write `"30d"`.
|
|
94
|
+
- `expiresIn: env("JWT_TTL")` where the variable is unset — resolves to `""` and is rejected. Give the env read a default: `env("JWT_TTL") || "1h"`.
|
|
95
|
+
|
|
96
|
+
## Pick a skill
|
|
97
|
+
|
|
98
|
+
| If the task is about… | Load |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
101
|
+
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) |
|
|
102
|
+
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
103
|
+
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
104
|
+
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) |
|
|
105
|
+
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
106
|
+
|
|
107
|
+
## Things NOT to do
|
|
108
|
+
|
|
109
|
+
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
110
|
+
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
111
|
+
- Don't return the raw `User` from a login endpoint without shaping output. Configure `static toJsonColumns` or `static resource` (see [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md)).
|
|
112
|
+
- Don't run `auth.cleanup` from app boot. Schedule it (cron, scheduler) as a periodic task — see [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
113
113
|
|
|
114
114
|
|
|
115
115
|
## customize-token-storage `@warlock.js/auth/customize-token-storage/SKILL.md`
|
|
@@ -1110,150 +1110,150 @@ Cleaner alternative: emit your own `user.registered` event from the controller a
|
|
|
1110
1110
|
|
|
1111
1111
|
## run-auth-commands `@warlock.js/auth/run-auth-commands/SKILL.md`
|
|
1112
1112
|
|
|
1113
|
-
---
|
|
1114
|
-
name: run-auth-commands
|
|
1115
|
-
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`.'
|
|
1116
|
-
---
|
|
1117
|
-
|
|
1118
|
-
# Run auth commands
|
|
1119
|
-
|
|
1120
|
-
The package ships three CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
|
|
1121
|
-
|
|
1122
|
-
## Register
|
|
1123
|
-
|
|
1124
|
-
```ts title="warlock.config.ts"
|
|
1125
|
-
import {
|
|
1126
|
-
registerAuthCleanupCommand,
|
|
1127
|
-
registerAuthPurgeNeverExpiringCommand,
|
|
1128
|
-
registerJWTSecretGeneratorCommand,
|
|
1129
|
-
} from "@warlock.js/auth";
|
|
1130
|
-
import { defineConfig } from "@warlock.js/core";
|
|
1131
|
-
|
|
1132
|
-
export default defineConfig({
|
|
1133
|
-
cli: {
|
|
1134
|
-
commands: [
|
|
1135
|
-
registerJWTSecretGeneratorCommand(),
|
|
1136
|
-
registerAuthCleanupCommand(),
|
|
1137
|
-
registerAuthPurgeNeverExpiringCommand(),
|
|
1138
|
-
],
|
|
1139
|
-
},
|
|
1140
|
-
});
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
## `warlock jwt.generate` — JWT secret bootstrap
|
|
1144
|
-
|
|
1145
|
-
```bash
|
|
1146
|
-
|
|
1147
|
-
```
|
|
1148
|
-
|
|
1149
|
-
Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
|
|
1150
|
-
|
|
1151
|
-
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.
|
|
1152
|
-
|
|
1153
|
-
**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.
|
|
1154
|
-
|
|
1155
|
-
## `warlock auth.cleanup` — expired token sweep
|
|
1156
|
-
|
|
1157
|
-
```bash
|
|
1158
|
-
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. Fires `token.expired` per refresh token and `cleanup.completed` once.
|
|
1162
|
-
|
|
1163
|
-
Schedule it periodically. Two common shapes:
|
|
1164
|
-
|
|
1165
|
-
### Via the scheduler
|
|
1166
|
-
|
|
1167
|
-
```ts
|
|
1168
|
-
import { scheduler, job } from "@warlock.js/scheduler";
|
|
1169
|
-
import { authService } from "@warlock.js/auth";
|
|
1170
|
-
|
|
1171
|
-
scheduler.addJob(
|
|
1172
|
-
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
1173
|
-
.daily()
|
|
1174
|
-
.at("03:00")
|
|
1175
|
-
.preventOverlap(),
|
|
1176
|
-
);
|
|
1177
|
-
|
|
1178
|
-
scheduler.start();
|
|
1179
|
-
```
|
|
1180
|
-
|
|
1181
|
-
In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
1182
|
-
|
|
1183
|
-
### Via system cron
|
|
1184
|
-
|
|
1185
|
-
```cron
|
|
1186
|
-
0 3 * * * cd /path/to/app && /usr/local/bin/
|
|
1187
|
-
```
|
|
1188
|
-
|
|
1189
|
-
Out-of-process — works when you don't want the scheduler subsystem running in this service.
|
|
1190
|
-
|
|
1191
|
-
## How often?
|
|
1192
|
-
|
|
1193
|
-
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.
|
|
1194
|
-
|
|
1195
|
-
If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
|
|
1196
|
-
|
|
1197
|
-
## `warlock auth.purge-never-expiring` — one-off remediation
|
|
1198
|
-
|
|
1199
|
-
```bash
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
```
|
|
1203
|
-
|
|
1204
|
-
Register with `registerAuthPurgeNeverExpiringCommand()`.
|
|
1205
|
-
|
|
1206
|
-
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.
|
|
1207
|
-
|
|
1208
|
-
**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.
|
|
1209
|
-
|
|
1210
|
-
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.
|
|
1211
|
-
|
|
1212
|
-
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.
|
|
1213
|
-
|
|
1214
|
-
## Custom commands
|
|
1215
|
-
|
|
1216
|
-
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:
|
|
1217
|
-
|
|
1218
|
-
```ts
|
|
1219
|
-
import { command } from "@warlock.js/core";
|
|
1220
|
-
import { authService } from "@warlock.js/auth";
|
|
1221
|
-
import { User } from "@/app/users/models/user.model";
|
|
1222
|
-
|
|
1223
|
-
export function registerDeepCleanupCommand() {
|
|
1224
|
-
return command({
|
|
1225
|
-
name: "auth.deep-cleanup",
|
|
1226
|
-
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
1227
|
-
preload: {
|
|
1228
|
-
env: true,
|
|
1229
|
-
config: ["auth", "database"],
|
|
1230
|
-
connectors: ["database"],
|
|
1231
|
-
},
|
|
1232
|
-
action: async () => {
|
|
1233
|
-
await authService.cleanupExpiredTokens();
|
|
1234
|
-
|
|
1235
|
-
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
1236
|
-
|
|
1237
|
-
for (const user of stale) {
|
|
1238
|
-
await authService.revokeAllTokens(user);
|
|
1239
|
-
}
|
|
1240
|
-
},
|
|
1241
|
-
});
|
|
1242
|
-
}
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
1246
|
-
|
|
1247
|
-
## Things NOT to do
|
|
1248
|
-
|
|
1249
|
-
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
1250
|
-
- 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.
|
|
1251
|
-
- 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.
|
|
1252
|
-
|
|
1253
|
-
## See also
|
|
1254
|
-
|
|
1255
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
1256
|
-
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
|
|
1113
|
+
---
|
|
1114
|
+
name: run-auth-commands
|
|
1115
|
+
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`.'
|
|
1116
|
+
---
|
|
1117
|
+
|
|
1118
|
+
# Run auth commands
|
|
1119
|
+
|
|
1120
|
+
The package ships three CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
|
|
1121
|
+
|
|
1122
|
+
## Register
|
|
1123
|
+
|
|
1124
|
+
```ts title="warlock.config.ts"
|
|
1125
|
+
import {
|
|
1126
|
+
registerAuthCleanupCommand,
|
|
1127
|
+
registerAuthPurgeNeverExpiringCommand,
|
|
1128
|
+
registerJWTSecretGeneratorCommand,
|
|
1129
|
+
} from "@warlock.js/auth";
|
|
1130
|
+
import { defineConfig } from "@warlock.js/core";
|
|
1131
|
+
|
|
1132
|
+
export default defineConfig({
|
|
1133
|
+
cli: {
|
|
1134
|
+
commands: [
|
|
1135
|
+
registerJWTSecretGeneratorCommand(),
|
|
1136
|
+
registerAuthCleanupCommand(),
|
|
1137
|
+
registerAuthPurgeNeverExpiringCommand(),
|
|
1138
|
+
],
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
## `warlock jwt.generate` — JWT secret bootstrap
|
|
1144
|
+
|
|
1145
|
+
```bash
|
|
1146
|
+
npx warlock jwt.generate
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
|
|
1150
|
+
|
|
1151
|
+
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.
|
|
1152
|
+
|
|
1153
|
+
**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.
|
|
1154
|
+
|
|
1155
|
+
## `warlock auth.cleanup` — expired token sweep
|
|
1156
|
+
|
|
1157
|
+
```bash
|
|
1158
|
+
npx warlock auth.cleanup
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. Fires `token.expired` per refresh token and `cleanup.completed` once.
|
|
1162
|
+
|
|
1163
|
+
Schedule it periodically. Two common shapes:
|
|
1164
|
+
|
|
1165
|
+
### Via the scheduler
|
|
1166
|
+
|
|
1167
|
+
```ts
|
|
1168
|
+
import { scheduler, job } from "@warlock.js/scheduler";
|
|
1169
|
+
import { authService } from "@warlock.js/auth";
|
|
1170
|
+
|
|
1171
|
+
scheduler.addJob(
|
|
1172
|
+
job("auth-cleanup", () => authService.cleanupExpiredTokens())
|
|
1173
|
+
.daily()
|
|
1174
|
+
.at("03:00")
|
|
1175
|
+
.preventOverlap(),
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
scheduler.start();
|
|
1179
|
+
```
|
|
1180
|
+
|
|
1181
|
+
In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
1182
|
+
|
|
1183
|
+
### Via system cron
|
|
1184
|
+
|
|
1185
|
+
```cron
|
|
1186
|
+
0 3 * * * cd /path/to/app && /usr/local/bin/npx warlock auth.cleanup
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Out-of-process — works when you don't want the scheduler subsystem running in this service.
|
|
1190
|
+
|
|
1191
|
+
## How often?
|
|
1192
|
+
|
|
1193
|
+
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.
|
|
1194
|
+
|
|
1195
|
+
If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
|
|
1196
|
+
|
|
1197
|
+
## `warlock auth.purge-never-expiring` — one-off remediation
|
|
1198
|
+
|
|
1199
|
+
```bash
|
|
1200
|
+
npx warlock auth.purge-never-expiring --dry-run # report only
|
|
1201
|
+
npx warlock auth.purge-never-expiring # report, then revoke
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
Register with `registerAuthPurgeNeverExpiringCommand()`.
|
|
1205
|
+
|
|
1206
|
+
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.
|
|
1207
|
+
|
|
1208
|
+
**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.
|
|
1209
|
+
|
|
1210
|
+
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.
|
|
1211
|
+
|
|
1212
|
+
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.
|
|
1213
|
+
|
|
1214
|
+
## Custom commands
|
|
1215
|
+
|
|
1216
|
+
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:
|
|
1217
|
+
|
|
1218
|
+
```ts
|
|
1219
|
+
import { command } from "@warlock.js/core";
|
|
1220
|
+
import { authService } from "@warlock.js/auth";
|
|
1221
|
+
import { User } from "@/app/users/models/user.model";
|
|
1222
|
+
|
|
1223
|
+
export function registerDeepCleanupCommand() {
|
|
1224
|
+
return command({
|
|
1225
|
+
name: "auth.deep-cleanup",
|
|
1226
|
+
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
1227
|
+
preload: {
|
|
1228
|
+
env: true,
|
|
1229
|
+
config: ["auth", "database"],
|
|
1230
|
+
connectors: ["database"],
|
|
1231
|
+
},
|
|
1232
|
+
action: async () => {
|
|
1233
|
+
await authService.cleanupExpiredTokens();
|
|
1234
|
+
|
|
1235
|
+
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
1236
|
+
|
|
1237
|
+
for (const user of stale) {
|
|
1238
|
+
await authService.revokeAllTokens(user);
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
1246
|
+
|
|
1247
|
+
## Things NOT to do
|
|
1248
|
+
|
|
1249
|
+
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
1250
|
+
- 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.
|
|
1251
|
+
- 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.
|
|
1252
|
+
|
|
1253
|
+
## See also
|
|
1254
|
+
|
|
1255
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
1256
|
+
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
|
|
1257
1257
|
|
|
1258
1258
|
|
|
1259
1259
|
## throttle-login-attempts `@warlock.js/auth/throttle-login-attempts/SKILL.md`
|
package/llms.txt
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
-
- [auth-basics](@warlock.js/auth/auth-basics/SKILL.md): (
|
|
9
|
+
- [auth-basics](@warlock.js/auth/auth-basics/SKILL.md): Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.
|
|
10
10
|
- [customize-token-storage](@warlock.js/auth/customize-token-storage/SKILL.md): (no description)
|
|
11
11
|
- [customize-user-type](@warlock.js/auth/customize-user-type/SKILL.md): (no description)
|
|
12
12
|
- [handle-login-and-logout](@warlock.js/auth/handle-login-and-logout/SKILL.md): (no description)
|
|
@@ -14,5 +14,5 @@
|
|
|
14
14
|
- [overview](@warlock.js/auth/overview/SKILL.md): (no description)
|
|
15
15
|
- [protect-routes](@warlock.js/auth/protect-routes/SKILL.md): (no description)
|
|
16
16
|
- [register-user](@warlock.js/auth/register-user/SKILL.md): Sign up a new user and issue the initial token pair — User.create({...password: await hashPassword(plain)}) then authService.createTokenPair(user). Triggers: `User.create`, `hashPassword`, `verifyPassword`, `authService.createTokenPair`, `toJsonColumns`, `strongPassword`, `authEvents`; "build a register endpoint", "POST /register controller", "sign up a new user", "hash password on signup", "email verification flow"; typical import `import { authService } from "@warlock.js/auth"; import { hashPassword } from "@warlock.js/core"`. Skip: login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; competing libs `bcrypt`, `bcryptjs`, `argon2`.
|
|
17
|
-
- [run-auth-commands](@warlock.js/auth/run-auth-commands/SKILL.md): (no
|
|
17
|
+
- [run-auth-commands](@warlock.js/auth/run-auth-commands/SKILL.md): Three bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env), warlock auth.cleanup (removes expired refresh tokens), and warlock auth.purge-never-expiring (finds and revokes token rows that can never expire). Register via registerJWTSecretGeneratorCommand(), registerAuthCleanupCommand() and registerAuthPurgeNeverExpiringCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `registerAuthPurgeNeverExpiringCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `warlock auth.purge-never-expiring`, `cleanupExpiredTokens`, `purgeNeverExpiringTokens`, `command`; "token never expires", "no exp claim", "Invalid Date expires_at"; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.
|
|
18
18
|
- [throttle-login-attempts](@warlock.js/auth/throttle-login-attempts/SKILL.md): (no description)
|
package/package.json
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"ms": "^2.1.3"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
16
|
-
"@warlock.js/cache": "5.
|
|
17
|
-
"@warlock.js/cascade": "5.
|
|
18
|
-
"@warlock.js/core": "5.
|
|
19
|
-
"@warlock.js/logger": "5.
|
|
20
|
-
"@warlock.js/seal": "5.
|
|
15
|
+
"@warlock.js/fs": "5.6.0",
|
|
16
|
+
"@warlock.js/cache": "5.6.0",
|
|
17
|
+
"@warlock.js/cascade": "5.6.0",
|
|
18
|
+
"@warlock.js/core": "5.6.0",
|
|
19
|
+
"@warlock.js/logger": "5.6.0",
|
|
20
|
+
"@warlock.js/seal": "5.6.0"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"author": "hassanzohdy",
|
|
35
35
|
"license": "MIT",
|
|
36
|
-
"version": "5.
|
|
36
|
+
"version": "5.6.0",
|
|
37
37
|
"type": "module",
|
|
38
38
|
"main": "./esm/index.mjs",
|
|
39
39
|
"module": "./esm/index.mjs",
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: auth-basics
|
|
3
|
-
description: 'Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Auth basics
|
|
7
|
-
|
|
8
|
-
JWT-based authentication for Warlock. `Auth` base model + `authMiddleware` gate + `authService` for login/logout/refresh + `AccessToken` / `RefreshToken` persistence + multi-user-type support.
|
|
9
|
-
|
|
10
|
-
> This skill is the auth **map** — read it first, then load the specific skill for the task.
|
|
11
|
-
|
|
12
|
-
## Install
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Foundations
|
|
19
|
-
|
|
20
|
-
1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
|
|
21
|
-
2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
|
|
22
|
-
3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
|
|
23
|
-
4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
|
|
24
|
-
5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
|
|
25
|
-
6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
|
|
26
|
-
7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
|
|
27
|
-
|
|
28
|
-
## Minimal wire-up
|
|
29
|
-
|
|
30
|
-
```ts title="warlock.config.ts"
|
|
31
|
-
import {
|
|
32
|
-
authMigrations,
|
|
33
|
-
registerAuthCleanupCommand,
|
|
34
|
-
registerJWTSecretGeneratorCommand,
|
|
35
|
-
} from "@warlock.js/auth";
|
|
36
|
-
import { defineConfig } from "@warlock.js/core";
|
|
37
|
-
|
|
38
|
-
export default defineConfig({
|
|
39
|
-
cli: {
|
|
40
|
-
commands: [
|
|
41
|
-
registerJWTSecretGeneratorCommand(),
|
|
42
|
-
registerAuthCleanupCommand(),
|
|
43
|
-
],
|
|
44
|
-
},
|
|
45
|
-
database: {
|
|
46
|
-
migrations: authMigrations,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
```ts title="src/config/auth.ts"
|
|
52
|
-
import { User } from "@/app/users/models/user.model";
|
|
53
|
-
|
|
54
|
-
export default {
|
|
55
|
-
userType: {
|
|
56
|
-
user: User,
|
|
57
|
-
// admin: Admin, // for multi-user-type
|
|
58
|
-
},
|
|
59
|
-
accessToken: {
|
|
60
|
-
secret: env("JWT_SECRET"),
|
|
61
|
-
expiresIn: "1h",
|
|
62
|
-
},
|
|
63
|
-
refreshToken: {
|
|
64
|
-
secret: env("JWT_REFRESH_SECRET"), // recommended: a separate refresh secret
|
|
65
|
-
enabled: true,
|
|
66
|
-
expiresIn: "30d",
|
|
67
|
-
rotation: true,
|
|
68
|
-
maxPerUser: 5,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### Token lifetimes are validated — a bad one throws
|
|
74
|
-
|
|
75
|
-
`expiresIn` (both blocks) must be a duration string the [`ms`](https://github.com/vercel/ms) package parses to a **positive** number: `"1h"`, `"30m"`, `"7d"`, `"30 days"`, or `NO_EXPIRATION` (`"100y"`). Anything else throws on the first token issue, naming the key:
|
|
76
|
-
|
|
77
|
-
```
|
|
78
|
-
auth.accessToken.expiresIn: "30dayz" is not a valid ms duration — use a positive duration string such as "1h", "7d", or NO_EXPIRATION.
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
That is deliberate. `ms` answers `undefined` for a string it can't parse (`"30dayz"`, `"thirty days"`) and `0` for `"0d"`, and a signer given either emits a JWT with **no `exp` claim** — a credential that never expires. A typo must not be able to produce that quietly, so it fails loudly instead of falling back to a default nobody asked for. Defaults still apply when the key is **absent**: `1h` for access, `7d` for refresh.
|
|
82
|
-
|
|
83
|
-
Two values that look right and are not:
|
|
84
|
-
|
|
85
|
-
- `expiresIn: 2592000` — a bare number. `ms` *formats* numbers instead of parsing them, so this is rejected; write `"30d"`.
|
|
86
|
-
- `expiresIn: env("JWT_TTL")` where the variable is unset — resolves to `""` and is rejected. Give the env read a default: `env("JWT_TTL") || "1h"`.
|
|
87
|
-
|
|
88
|
-
## Pick a skill
|
|
89
|
-
|
|
90
|
-
| If the task is about… | Load |
|
|
91
|
-
| --- | --- |
|
|
92
|
-
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
93
|
-
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) |
|
|
94
|
-
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
95
|
-
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
96
|
-
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) |
|
|
97
|
-
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
98
|
-
|
|
99
|
-
## Things NOT to do
|
|
100
|
-
|
|
101
|
-
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
102
|
-
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
103
|
-
- Don't return the raw `User` from a login endpoint without shaping output. Configure `static toJsonColumns` or `static resource` (see [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md)).
|
|
104
|
-
- Don't run `auth.cleanup` from app boot. Schedule it (cron, scheduler) as a periodic task — see [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
1
|
+
---
|
|
2
|
+
name: auth-basics
|
|
3
|
+
description: 'Start with @warlock.js/auth — JWT auth, Auth base model, authMiddleware route gate, authService (login / logout / refresh), AccessToken + RefreshToken persistence, multi-user-type support. Triggers: `Auth`, `authMiddleware`, `authService`, `AccessToken`, `RefreshToken`, `authMigrations`; "set up auth in a new app", "which auth skill do I need", "JWT authentication overview", "wire warlock auth"; typical import `import { authMiddleware, authService, Auth, authMigrations } from "@warlock.js/auth"`. Skip: routing — `@warlock.js/auth/protect-routes/SKILL.md`; login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `next-auth`, `lucia-auth`, `auth0`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Auth basics
|
|
7
|
+
|
|
8
|
+
JWT-based authentication for Warlock. `Auth` base model + `authMiddleware` gate + `authService` for login/logout/refresh + `AccessToken` / `RefreshToken` persistence + multi-user-type support.
|
|
9
|
+
|
|
10
|
+
> This skill is the auth **map** — read it first, then load the specific skill for the task.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @warlock.js/auth
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Foundations
|
|
19
|
+
|
|
20
|
+
1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
|
|
21
|
+
2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
|
|
22
|
+
3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
|
|
23
|
+
4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
|
|
24
|
+
5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
|
|
25
|
+
6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
|
|
26
|
+
7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
|
|
27
|
+
|
|
28
|
+
## Minimal wire-up
|
|
29
|
+
|
|
30
|
+
```ts title="warlock.config.ts"
|
|
31
|
+
import {
|
|
32
|
+
authMigrations,
|
|
33
|
+
registerAuthCleanupCommand,
|
|
34
|
+
registerJWTSecretGeneratorCommand,
|
|
35
|
+
} from "@warlock.js/auth";
|
|
36
|
+
import { defineConfig } from "@warlock.js/core";
|
|
37
|
+
|
|
38
|
+
export default defineConfig({
|
|
39
|
+
cli: {
|
|
40
|
+
commands: [
|
|
41
|
+
registerJWTSecretGeneratorCommand(),
|
|
42
|
+
registerAuthCleanupCommand(),
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
database: {
|
|
46
|
+
migrations: authMigrations,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```ts title="src/config/auth.ts"
|
|
52
|
+
import { User } from "@/app/users/models/user.model";
|
|
53
|
+
|
|
54
|
+
export default {
|
|
55
|
+
userType: {
|
|
56
|
+
user: User,
|
|
57
|
+
// admin: Admin, // for multi-user-type
|
|
58
|
+
},
|
|
59
|
+
accessToken: {
|
|
60
|
+
secret: env("JWT_SECRET"),
|
|
61
|
+
expiresIn: "1h",
|
|
62
|
+
},
|
|
63
|
+
refreshToken: {
|
|
64
|
+
secret: env("JWT_REFRESH_SECRET"), // recommended: a separate refresh secret
|
|
65
|
+
enabled: true,
|
|
66
|
+
expiresIn: "30d",
|
|
67
|
+
rotation: true,
|
|
68
|
+
maxPerUser: 5,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Token lifetimes are validated — a bad one throws
|
|
74
|
+
|
|
75
|
+
`expiresIn` (both blocks) must be a duration string the [`ms`](https://github.com/vercel/ms) package parses to a **positive** number: `"1h"`, `"30m"`, `"7d"`, `"30 days"`, or `NO_EXPIRATION` (`"100y"`). Anything else throws on the first token issue, naming the key:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
auth.accessToken.expiresIn: "30dayz" is not a valid ms duration — use a positive duration string such as "1h", "7d", or NO_EXPIRATION.
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
That is deliberate. `ms` answers `undefined` for a string it can't parse (`"30dayz"`, `"thirty days"`) and `0` for `"0d"`, and a signer given either emits a JWT with **no `exp` claim** — a credential that never expires. A typo must not be able to produce that quietly, so it fails loudly instead of falling back to a default nobody asked for. Defaults still apply when the key is **absent**: `1h` for access, `7d` for refresh.
|
|
82
|
+
|
|
83
|
+
Two values that look right and are not:
|
|
84
|
+
|
|
85
|
+
- `expiresIn: 2592000` — a bare number. `ms` *formats* numbers instead of parsing them, so this is rejected; write `"30d"`.
|
|
86
|
+
- `expiresIn: env("JWT_TTL")` where the variable is unset — resolves to `""` and is rejected. Give the env read a default: `env("JWT_TTL") || "1h"`.
|
|
87
|
+
|
|
88
|
+
## Pick a skill
|
|
89
|
+
|
|
90
|
+
| If the task is about… | Load |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
93
|
+
| `authService.login(...)`, `attemptLogin`, full credentials-to-tokens flow + logout | [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) |
|
|
94
|
+
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
95
|
+
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
96
|
+
| Multi-user-type apps (`user`, `admin`, `client`), `config.auth.userType.<name>` mapping | [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) |
|
|
97
|
+
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
98
|
+
|
|
99
|
+
## Things NOT to do
|
|
100
|
+
|
|
101
|
+
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
102
|
+
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
103
|
+
- Don't return the raw `User` from a login endpoint without shaping output. Configure `static toJsonColumns` or `static resource` (see [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md)).
|
|
104
|
+
- Don't run `auth.cleanup` from app boot. Schedule it (cron, scheduler) as a periodic task — see [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
|
|
@@ -1,144 +1,144 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. 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/
|
|
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
|
-
|
|
89
|
-
|
|
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
|
|
107
|
-
import { command } from "@warlock.js/core";
|
|
108
|
-
import { authService } from "@warlock.js/auth";
|
|
109
|
-
import { User } from "@/app/users/models/user.model";
|
|
110
|
-
|
|
111
|
-
export function registerDeepCleanupCommand() {
|
|
112
|
-
return command({
|
|
113
|
-
name: "auth.deep-cleanup",
|
|
114
|
-
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
115
|
-
preload: {
|
|
116
|
-
env: true,
|
|
117
|
-
config: ["auth", "database"],
|
|
118
|
-
connectors: ["database"],
|
|
119
|
-
},
|
|
120
|
-
action: async () => {
|
|
121
|
-
await authService.cleanupExpiredTokens();
|
|
122
|
-
|
|
123
|
-
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
124
|
-
|
|
125
|
-
for (const user of stale) {
|
|
126
|
-
await authService.revokeAllTokens(user);
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
134
|
-
|
|
135
|
-
## Things NOT to do
|
|
136
|
-
|
|
137
|
-
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
138
|
-
- 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.
|
|
139
|
-
- 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.
|
|
140
|
-
|
|
141
|
-
## See also
|
|
142
|
-
|
|
143
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
144
|
-
- [`@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 too. 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
|
|
107
|
+
import { command } from "@warlock.js/core";
|
|
108
|
+
import { authService } from "@warlock.js/auth";
|
|
109
|
+
import { User } from "@/app/users/models/user.model";
|
|
110
|
+
|
|
111
|
+
export function registerDeepCleanupCommand() {
|
|
112
|
+
return command({
|
|
113
|
+
name: "auth.deep-cleanup",
|
|
114
|
+
description: "Expire stale tokens AND revoke tokens for inactive users",
|
|
115
|
+
preload: {
|
|
116
|
+
env: true,
|
|
117
|
+
config: ["auth", "database"],
|
|
118
|
+
connectors: ["database"],
|
|
119
|
+
},
|
|
120
|
+
action: async () => {
|
|
121
|
+
await authService.cleanupExpiredTokens();
|
|
122
|
+
|
|
123
|
+
const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
|
|
124
|
+
|
|
125
|
+
for (const user of stale) {
|
|
126
|
+
await authService.revokeAllTokens(user);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
|
|
134
|
+
|
|
135
|
+
## Things NOT to do
|
|
136
|
+
|
|
137
|
+
- Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
|
|
138
|
+
- 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.
|
|
139
|
+
- 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.
|
|
140
|
+
|
|
141
|
+
## See also
|
|
142
|
+
|
|
143
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
|
|
144
|
+
- [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
|