@warlock.js/auth 4.2.11 → 4.3.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.
@@ -1,106 +1,106 @@
1
- ---
2
- name: protect-routes
3
- description: 'Gate HTTP routes via authMiddleware(allowedUserType) — the argument is required and a valid token is always required: [] allows any authenticated user, a user-type restricts to those types. Sets request.user + request.decodedAccessToken on success, 401 on failure. Triggers: `authMiddleware`, `request.user`, `request.decodedAccessToken`, `AuthErrorCodes`, `MissingAccessToken`, `InvalidAccessToken`; "how do I protect a route", "restrict route by user type", "require any logged-in user"; typical import `import { authMiddleware } from "@warlock.js/auth"`. Skip: multi-user-type config — `@warlock.js/auth/customize-user-type/SKILL.md`; issuing the token — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `express-jwt`, `next-auth` middleware.'
4
- ---
5
-
6
- # Gate routes with `authMiddleware`
7
-
8
- `authMiddleware(allowedUserType: string | string[])` returns a Warlock middleware. Attach it to routes or route groups. The argument is **required** — there is no anonymous/optional mode. A request without a valid access token is always rejected with `401`; public routes simply omit the middleware.
9
-
10
- ## Two modes
11
-
12
- Middleware is attached via the route's `options.middleware` array (the third argument) — never as a positional argument.
13
-
14
- ```ts
15
- import { authMiddleware } from "@warlock.js/auth";
16
-
17
- // Mode 1 — required, any user type
18
- // Rejects with 401 if no valid token; any authenticated user passes.
19
- router.get("/account", accountController, {
20
- middleware: [authMiddleware([])], // empty array = "any logged-in user"
21
- });
22
-
23
- // Mode 2 — required, specific user type(s)
24
- // Rejects with 401 if no token OR if token's userType isn't allowed.
25
- router.get("/admin", adminController, {
26
- middleware: [authMiddleware("admin")],
27
- });
28
-
29
- router.get("/staff", staffController, {
30
- middleware: [authMiddleware(["admin", "staff"])],
31
- });
32
- ```
33
-
34
- The `userType` slug must match a key in `config.auth.userType.<name>` — see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md).
35
-
36
- ## What the middleware does
37
-
38
- On success, before your controller runs:
39
-
40
- ```ts
41
- request.user = <hydrated user model instance>;
42
- request.decodedAccessToken = <decoded JWT payload>;
43
- ```
44
-
45
- The user is loaded via `Model.find(decodedToken.id)` against the `config.auth.userType[userType]` class. If the user no longer exists (deleted), the access token row is destroyed and the request gets 401.
46
-
47
- On failure, the middleware returns one of these 401 responses:
48
-
49
- | Error code | When |
50
- | --- | --- |
51
- | `MissingAccessToken` | No `Authorization` header |
52
- | `InvalidAccessToken` | Token doesn't verify (signature, expired, doesn't match DB) |
53
- | `Unauthorized` | Token valid but user-type isn't in the allowed list |
54
-
55
- ## Reading the user in a controller
56
-
57
- ```ts
58
- async function accountController(request: Request, response: Response) {
59
- const user = request.user!; // typed via your Auth subclass
60
- return response.success({
61
- id: user.id,
62
- email: user.get("email"),
63
- });
64
- }
65
- ```
66
-
67
- Because the middleware always requires a valid token, `request.user` is guaranteed present inside any gated controller (the middleware would have responded 401 otherwise). The `!` is safe here.
68
-
69
- ## Route-group protection
70
-
71
- ```ts
72
- router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () => {
73
- router.get("/users", listUsersController);
74
- router.post("/users", createUserController);
75
- });
76
- ```
77
-
78
- Every route inside the group is gated — the group's `middleware` array applies to each route in the callback. Cleaner than repeating the middleware on each route.
79
-
80
- ## No optional / fallthrough auth
81
-
82
- There is no "hydrate `request.user` if a token is present, otherwise continue" mode. `authMiddleware` always requires a valid token. If a route should be reachable anonymously, leave the middleware off — and read the token yourself in the controller if you want soft personalization:
83
-
84
- ```ts
85
- async function feedController(request: Request, response: Response) {
86
- const token = request.authorizationValue;
87
- // optionally decode/hydrate manually when a token is present
88
- }
89
- ```
90
-
91
- ## Custom error responses
92
-
93
- The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
94
-
95
- ## Things NOT to do
96
-
97
- - Don't call `authMiddleware` outside route definition. It returns a function — the function is what runs per-request. Calling it once per request creates a fresh middleware on every hit (wasteful) and a fresh allowed-types Set (correctness if the input changes per call).
98
- - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
99
- - Don't trust `request.user` set by client-supplied headers. The middleware is the only place that sets it on the server — client headers can't reach this slot.
100
- - Don't pass an unknown user-type to `authMiddleware("typo")`. The middleware will reject every request because the lookup fails. Test the wire-up with a real token of each user type.
101
-
102
- ## See also
103
-
104
- - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
105
- - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — where the access token gets issued in the first place
106
- - [`@warlock.js/auth/throttle-login-attempts/SKILL.md`](@warlock.js/auth/throttle-login-attempts/SKILL.md) — brute-force throttle to stack in front of the login route
1
+ ---
2
+ name: protect-routes
3
+ description: 'Gate HTTP routes via authMiddleware(allowedUserType) — the argument is required and a valid token is always required: [] allows any authenticated user, a user-type restricts to those types. Sets request.user + request.decodedAccessToken on success, 401 on failure. Triggers: `authMiddleware`, `request.user`, `request.decodedAccessToken`, `AuthErrorCodes`, `MissingAccessToken`, `InvalidAccessToken`; "how do I protect a route", "restrict route by user type", "require any logged-in user"; typical import `import { authMiddleware } from "@warlock.js/auth"`. Skip: multi-user-type config — `@warlock.js/auth/customize-user-type/SKILL.md`; issuing the token — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; competing libs `passport`, `express-jwt`, `next-auth` middleware.'
4
+ ---
5
+
6
+ # Gate routes with `authMiddleware`
7
+
8
+ `authMiddleware(allowedUserType: string | string[])` returns a Warlock middleware. Attach it to routes or route groups. The argument is **required** — there is no anonymous/optional mode. A request without a valid access token is always rejected with `401`; public routes simply omit the middleware.
9
+
10
+ ## Two modes
11
+
12
+ Middleware is attached via the route's `options.middleware` array (the third argument) — never as a positional argument.
13
+
14
+ ```ts
15
+ import { authMiddleware } from "@warlock.js/auth";
16
+
17
+ // Mode 1 — required, any user type
18
+ // Rejects with 401 if no valid token; any authenticated user passes.
19
+ router.get("/account", accountController, {
20
+ middleware: [authMiddleware([])], // empty array = "any logged-in user"
21
+ });
22
+
23
+ // Mode 2 — required, specific user type(s)
24
+ // Rejects with 401 if no token OR if token's userType isn't allowed.
25
+ router.get("/admin", adminController, {
26
+ middleware: [authMiddleware("admin")],
27
+ });
28
+
29
+ router.get("/staff", staffController, {
30
+ middleware: [authMiddleware(["admin", "staff"])],
31
+ });
32
+ ```
33
+
34
+ The `userType` slug must match a key in `config.auth.userType.<name>` — see [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md).
35
+
36
+ ## What the middleware does
37
+
38
+ On success, before your controller runs:
39
+
40
+ ```ts
41
+ request.user = <hydrated user model instance>;
42
+ request.decodedAccessToken = <decoded JWT payload>;
43
+ ```
44
+
45
+ The user is loaded via `Model.find(decodedToken.id)` against the `config.auth.userType[userType]` class. If the user no longer exists (deleted), the access token row is destroyed and the request gets 401.
46
+
47
+ On failure, the middleware returns one of these 401 responses:
48
+
49
+ | Error code | When |
50
+ | --- | --- |
51
+ | `MissingAccessToken` | No `Authorization` header |
52
+ | `InvalidAccessToken` | Token doesn't verify (signature, expired, doesn't match DB) |
53
+ | `Unauthorized` | Token valid but user-type isn't in the allowed list |
54
+
55
+ ## Reading the user in a controller
56
+
57
+ ```ts
58
+ async function accountController(request: Request, response: Response) {
59
+ const user = request.user!; // typed via your Auth subclass
60
+ return response.success({
61
+ id: user.id,
62
+ email: user.get("email"),
63
+ });
64
+ }
65
+ ```
66
+
67
+ Because the middleware always requires a valid token, `request.user` is guaranteed present inside any gated controller (the middleware would have responded 401 otherwise). The `!` is safe here.
68
+
69
+ ## Route-group protection
70
+
71
+ ```ts
72
+ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () => {
73
+ router.get("/users", listUsersController);
74
+ router.post("/users", createUserController);
75
+ });
76
+ ```
77
+
78
+ Every route inside the group is gated — the group's `middleware` array applies to each route in the callback. Cleaner than repeating the middleware on each route.
79
+
80
+ ## No optional / fallthrough auth
81
+
82
+ There is no "hydrate `request.user` if a token is present, otherwise continue" mode. `authMiddleware` always requires a valid token. If a route should be reachable anonymously, leave the middleware off — and read the token yourself in the controller if you want soft personalization:
83
+
84
+ ```ts
85
+ async function feedController(request: Request, response: Response) {
86
+ const token = request.authorizationValue;
87
+ // optionally decode/hydrate manually when a token is present
88
+ }
89
+ ```
90
+
91
+ ## Custom error responses
92
+
93
+ The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
94
+
95
+ ## Things NOT to do
96
+
97
+ - Don't call `authMiddleware` outside route definition. It returns a function — the function is what runs per-request. Calling it once per request creates a fresh middleware on every hit (wasteful) and a fresh allowed-types Set (correctness if the input changes per call).
98
+ - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
99
+ - Don't trust `request.user` set by client-supplied headers. The middleware is the only place that sets it on the server — client headers can't reach this slot.
100
+ - Don't pass an unknown user-type to `authMiddleware("typo")`. The middleware will reject every request because the lookup fails. Test the wire-up with a real token of each user type.
101
+
102
+ ## See also
103
+
104
+ - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
105
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — where the access token gets issued in the first place
106
+ - [`@warlock.js/auth/throttle-login-attempts/SKILL.md`](@warlock.js/auth/throttle-login-attempts/SKILL.md) — brute-force throttle to stack in front of the login route
@@ -1,125 +1,125 @@
1
- ---
2
- name: run-auth-commands
3
- description: 'Two bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env) and warlock auth.cleanup (removes expired refresh tokens). Register via registerJWTSecretGeneratorCommand() and registerAuthCleanupCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `cleanupExpiredTokens`, `command`; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.'
4
- ---
5
-
6
- # Run auth commands
7
-
8
- The package ships two CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
9
-
10
- ## Register
11
-
12
- ```ts title="warlock.config.ts"
13
- import {
14
- registerAuthCleanupCommand,
15
- registerJWTSecretGeneratorCommand,
16
- } from "@warlock.js/auth";
17
- import { defineConfig } from "@warlock.js/core";
18
-
19
- export default defineConfig({
20
- cli: {
21
- commands: [
22
- registerJWTSecretGeneratorCommand(),
23
- registerAuthCleanupCommand(),
24
- ],
25
- },
26
- });
27
- ```
28
-
29
- ## `warlock jwt.generate` — JWT secret bootstrap
30
-
31
- ```bash
32
- yarn warlock jwt.generate
33
- ```
34
-
35
- Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
36
-
37
- Run it once when setting up a new project. Each developer typically runs it locally; production secrets come from your secret manager (Vault, AWS Secrets Manager, k8s secrets) and bypass this command.
38
-
39
- **Don't commit `.env`.** The generated secret should never live in the repo. The command writes to `.env`, which `.gitignore` already excludes in a default Warlock project.
40
-
41
- ## `warlock auth.cleanup` — expired token sweep
42
-
43
- ```bash
44
- yarn warlock auth.cleanup
45
- ```
46
-
47
- Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. Fires `token.expired` per refresh token and `cleanup.completed` once.
48
-
49
- Schedule it periodically. Two common shapes:
50
-
51
- ### Via the scheduler
52
-
53
- ```ts
54
- import { scheduler, job } from "@warlock.js/scheduler";
55
- import { authService } from "@warlock.js/auth";
56
-
57
- scheduler.addJob(
58
- job("auth-cleanup", () => authService.cleanupExpiredTokens())
59
- .daily()
60
- .at("03:00")
61
- .preventOverlap(),
62
- );
63
-
64
- scheduler.start();
65
- ```
66
-
67
- In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
68
-
69
- ### Via system cron
70
-
71
- ```cron
72
- 0 3 * * * cd /path/to/app && /usr/local/bin/yarn warlock auth.cleanup
73
- ```
74
-
75
- Out-of-process — works when you don't want the scheduler subsystem running in this service.
76
-
77
- ## How often?
78
-
79
- Once a day is usually enough. The check is cheap (single indexed DELETE on `expires_at < now()`), and refresh tokens that have already expired don't grant access — cleanup is housekeeping, not security.
80
-
81
- If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
82
-
83
- ## Custom commands
84
-
85
- If `auth.cleanup` doesn't cover everything your app needs (e.g. you also want to revoke tokens for inactive users), write your own command and combine the auth service helpers:
86
-
87
- ```ts
88
- import { command } from "@warlock.js/core";
89
- import { authService } from "@warlock.js/auth";
90
- import { User } from "@/app/users/models/user.model";
91
-
92
- export function registerDeepCleanupCommand() {
93
- return command({
94
- name: "auth.deep-cleanup",
95
- description: "Expire stale tokens AND revoke tokens for inactive users",
96
- preload: {
97
- env: true,
98
- config: ["auth", "database"],
99
- connectors: ["database"],
100
- },
101
- action: async () => {
102
- await authService.cleanupExpiredTokens();
103
-
104
- const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
105
-
106
- for (const user of stale) {
107
- await authService.revokeAllTokens(user);
108
- }
109
- },
110
- });
111
- }
112
- ```
113
-
114
- Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
115
-
116
- ## Things NOT to do
117
-
118
- - Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
119
- - Don't run `auth.cleanup` from a long-running scheduler at sub-minute intervals. The DELETE itself is cheap, but the per-token `token.expired` event fan-out has cost. Hourly is plenty even at scale.
120
- - Don't put the JWT secret in your codebase fallback (`env("JWT_SECRET", "dev-secret")`). A missing secret should fail the boot — not silently degrade to a dev value.
121
-
122
- ## See also
123
-
124
- - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
125
- - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
1
+ ---
2
+ name: run-auth-commands
3
+ description: 'Two bundled CLI commands — warlock jwt.generate (creates strong JWT secret + writes to .env) and warlock auth.cleanup (removes expired refresh tokens). Register via registerJWTSecretGeneratorCommand() and registerAuthCleanupCommand(). Triggers: `registerJWTSecretGeneratorCommand`, `registerAuthCleanupCommand`, `warlock jwt.generate`, `warlock auth.cleanup`, `cleanupExpiredTokens`, `command`; "generate JWT secret", "bootstrap .env JWT_SECRET", "cron job for expired tokens", "schedule auth cleanup"; typical import `import { registerJWTSecretGeneratorCommand, registerAuthCleanupCommand } from "@warlock.js/auth"`. Skip: programmatic cleanup — `@warlock.js/auth/manage-tokens/SKILL.md`; in-process scheduling — `@warlock.js/scheduler/scheduler-basics/SKILL.md`; competing tools `dotenv-cli`, `node-cron`.'
4
+ ---
5
+
6
+ # Run auth commands
7
+
8
+ The package ships two CLI commands. Register them in `warlock.config.ts`; the framework picks them up.
9
+
10
+ ## Register
11
+
12
+ ```ts title="warlock.config.ts"
13
+ import {
14
+ registerAuthCleanupCommand,
15
+ registerJWTSecretGeneratorCommand,
16
+ } from "@warlock.js/auth";
17
+ import { defineConfig } from "@warlock.js/core";
18
+
19
+ export default defineConfig({
20
+ cli: {
21
+ commands: [
22
+ registerJWTSecretGeneratorCommand(),
23
+ registerAuthCleanupCommand(),
24
+ ],
25
+ },
26
+ });
27
+ ```
28
+
29
+ ## `warlock jwt.generate` — JWT secret bootstrap
30
+
31
+ ```bash
32
+ yarn warlock jwt.generate
33
+ ```
34
+
35
+ Generates a cryptographically strong secret string and writes it to your `.env` as `JWT_SECRET=...` (and `JWT_REFRESH_SECRET=...` if refresh tokens are enabled).
36
+
37
+ Run it once when setting up a new project. Each developer typically runs it locally; production secrets come from your secret manager (Vault, AWS Secrets Manager, k8s secrets) and bypass this command.
38
+
39
+ **Don't commit `.env`.** The generated secret should never live in the repo. The command writes to `.env`, which `.gitignore` already excludes in a default Warlock project.
40
+
41
+ ## `warlock auth.cleanup` — expired token sweep
42
+
43
+ ```bash
44
+ yarn warlock auth.cleanup
45
+ ```
46
+
47
+ Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed, then sweeps expired access-token rows too. Fires `token.expired` per refresh token and `cleanup.completed` once.
48
+
49
+ Schedule it periodically. Two common shapes:
50
+
51
+ ### Via the scheduler
52
+
53
+ ```ts
54
+ import { scheduler, job } from "@warlock.js/scheduler";
55
+ import { authService } from "@warlock.js/auth";
56
+
57
+ scheduler.addJob(
58
+ job("auth-cleanup", () => authService.cleanupExpiredTokens())
59
+ .daily()
60
+ .at("03:00")
61
+ .preventOverlap(),
62
+ );
63
+
64
+ scheduler.start();
65
+ ```
66
+
67
+ In-process — no shell call. See [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md).
68
+
69
+ ### Via system cron
70
+
71
+ ```cron
72
+ 0 3 * * * cd /path/to/app && /usr/local/bin/yarn warlock auth.cleanup
73
+ ```
74
+
75
+ Out-of-process — works when you don't want the scheduler subsystem running in this service.
76
+
77
+ ## How often?
78
+
79
+ Once a day is usually enough. The check is cheap (single indexed DELETE on `expires_at < now()`), and refresh tokens that have already expired don't grant access — cleanup is housekeeping, not security.
80
+
81
+ If you have very-short-lived refresh tokens (1h expiry) and a million-user scale where the table grows fast, cleanup more often (hourly).
82
+
83
+ ## Custom commands
84
+
85
+ If `auth.cleanup` doesn't cover everything your app needs (e.g. you also want to revoke tokens for inactive users), write your own command and combine the auth service helpers:
86
+
87
+ ```ts
88
+ import { command } from "@warlock.js/core";
89
+ import { authService } from "@warlock.js/auth";
90
+ import { User } from "@/app/users/models/user.model";
91
+
92
+ export function registerDeepCleanupCommand() {
93
+ return command({
94
+ name: "auth.deep-cleanup",
95
+ description: "Expire stale tokens AND revoke tokens for inactive users",
96
+ preload: {
97
+ env: true,
98
+ config: ["auth", "database"],
99
+ connectors: ["database"],
100
+ },
101
+ action: async () => {
102
+ await authService.cleanupExpiredTokens();
103
+
104
+ const stale = await User.where("last_seen_at", "<", thirtyDaysAgo).get();
105
+
106
+ for (const user of stale) {
107
+ await authService.revokeAllTokens(user);
108
+ }
109
+ },
110
+ });
111
+ }
112
+ ```
113
+
114
+ Register it the same way as the bundled commands — call the factory inside `defineConfig({ cli: { commands: [...] } })`.
115
+
116
+ ## Things NOT to do
117
+
118
+ - Don't run `jwt.generate` repeatedly in production. It changes the secret, which invalidates every token in flight. Generate once per environment.
119
+ - Don't run `auth.cleanup` from a long-running scheduler at sub-minute intervals. The DELETE itself is cheap, but the per-token `token.expired` event fan-out has cost. Hourly is plenty even at scale.
120
+ - Don't put the JWT secret in your codebase fallback (`env("JWT_SECRET", "dev-secret")`). A missing secret should fail the boot — not silently degrade to a dev value.
121
+
122
+ ## See also
123
+
124
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
125
+ - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling