@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 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
- pnpm add @warlock.js/auth
24
- ```
25
-
26
- ## Foundations
27
-
28
- 1. **Users extend `Auth`.** Your `User`, `Admin`, etc. extend the shared base model that knows how to issue tokens and verify passwords. Multiple user types coexist (see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md)).
29
- 2. **`auth.userType.<name>` config maps a user-type slug to the model class.** The middleware uses this to hydrate the right model from a token.
30
- 3. **Tokens persist.** Both `AccessToken` and `RefreshToken` are Cascade models — issuing a token writes a row; logout / revoke deletes or marks-revoked. Stateless JWT verification + stateful revocation list.
31
- 4. **`authMiddleware(allowedUserType)` gates routes.** The argument is required and a valid token is always required. `[]` → any authenticated user; a user-type → required auth scoped to those types. Public routes omit the middleware entirely.
32
- 5. **`authService.login(Model, credentials, deviceInfo?)` is the full happy path.** Verifies credentials, creates token pair (access + refresh), emits events, returns `{ user, tokens }`.
33
- 6. **Refresh-token rotation is on by default.** Each refresh consumes the old token and issues new ones from the same "family" — replay detection revokes the family.
34
- 7. **JWT secret lives in the env.** Generate with `warlock jwt.generate` (see [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md)).
35
-
36
- ## Minimal wire-up
37
-
38
- ```ts title="warlock.config.ts"
39
- import {
40
- authMigrations,
41
- registerAuthCleanupCommand,
42
- registerJWTSecretGeneratorCommand,
43
- } from "@warlock.js/auth";
44
- import { defineConfig } from "@warlock.js/core";
45
-
46
- export default defineConfig({
47
- cli: {
48
- commands: [
49
- registerJWTSecretGeneratorCommand(),
50
- registerAuthCleanupCommand(),
51
- ],
52
- },
53
- database: {
54
- migrations: authMigrations,
55
- },
56
- });
57
- ```
58
-
59
- ```ts title="src/config/auth.ts"
60
- import { User } from "@/app/users/models/user.model";
61
-
62
- export default {
63
- userType: {
64
- user: User,
65
- // admin: Admin, // for multi-user-type
66
- },
67
- 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
- pnpm 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
- pnpm 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/pnpm 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
- pnpm warlock auth.purge-never-expiring --dry-run # report only
1201
- pnpm 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
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): (no description)
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 description)
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.4.0",
16
- "@warlock.js/cache": "5.4.0",
17
- "@warlock.js/cascade": "5.4.0",
18
- "@warlock.js/core": "5.4.0",
19
- "@warlock.js/logger": "5.4.0",
20
- "@warlock.js/seal": "5.4.0"
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.4.0",
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
- pnpm add @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
+ ---
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
- pnpm 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
- pnpm 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/pnpm 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
- pnpm warlock auth.purge-never-expiring --dry-run # report only
89
- pnpm 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
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