@warlock.js/auth 4.9.2 → 4.10.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
+ ## 4.10.0
8
+
9
+ ### Changed
10
+
11
+ - `protect-routes` documents two things that were previously only discoverable by reading source: that `authMiddleware` gates on **user type by flat string match** and cannot express a permission matrix, role hierarchy, or who-may-act-on-whom — with a worked pointer to `@warlock.js/access` (`gate`, `can`, `definePolicy`) for exactly that; and that auth is **bearer-token by design**, with cookie sessions and CSRF being app-level work rather than an omission
12
+
7
13
  ## 4.2.11
8
14
 
9
15
  ### Changed
package/llms-full.txt CHANGED
@@ -1,11 +1,11 @@
1
- # Warlock Auth — full skills
2
-
3
- > Package: `@warlock.js/auth`
4
-
5
- > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/auth/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
-
7
- ## auth-basics `@warlock.js/auth/auth-basics/SKILL.md`
8
-
1
+ # Warlock Auth — full skills
2
+
3
+ > Package: `@warlock.js/auth`
4
+
5
+ > Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/auth/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
6
+
7
+ ## auth-basics `@warlock.js/auth/auth-basics/SKILL.md`
8
+
9
9
  ---
10
10
  name: auth-basics
11
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`.'
@@ -95,10 +95,10 @@ export default {
95
95
  - Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
96
96
  - 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)).
97
97
  - 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).
98
-
99
-
100
- ## customize-token-storage `@warlock.js/auth/customize-token-storage/SKILL.md`
101
-
98
+
99
+
100
+ ## customize-token-storage `@warlock.js/auth/customize-token-storage/SKILL.md`
101
+
102
102
  ---
103
103
  name: customize-token-storage
104
104
  description: 'Override the persisted AccessToken / RefreshToken models to add columns (multi-tenant `organization_id`, device metadata), rename, or change storage — without forking the package. Extend the model + `schema.extend(...)`, register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`, override `issue()` to populate the new column, and add a migration. Triggers: `accessToken.model`, `refreshToken.model`, `AccessToken.issue`, `RefreshToken.issue`, `accessTokenSchema`, `refreshTokenSchema`, "add a column to the token table", "multi-tenant tokens", "organization_id on access token", "override the token model", "custom token storage"; typical import `import { AccessToken, accessTokenSchema } from "@warlock.js/auth"`. Skip: multiple user TYPES (not token storage) — `@warlock.js/auth/customize-user-type/SKILL.md`; the token lifecycle API — `@warlock.js/auth/manage-tokens/SKILL.md`; the config blocks themselves — `@warlock.js/auth/auth-basics/SKILL.md`.'
@@ -211,10 +211,10 @@ If you rename a column, override the statics that reference it so they map to yo
211
211
  - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — multiple user *types* (a different axis from token *storage*).
212
212
  - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — the token lifecycle your statics power.
213
213
  - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — extending models and schemas.
214
-
215
-
216
- ## customize-user-type `@warlock.js/auth/customize-user-type/SKILL.md`
217
-
214
+
215
+
216
+ ## customize-user-type `@warlock.js/auth/customize-user-type/SKILL.md`
217
+
218
218
  ---
219
219
  name: customize-user-type
220
220
  description: 'Support multiple user types (user / admin / client / staff) in one auth system — each Auth subclass overrides userType, config.auth.userType.<slug> maps slug to model class, authMiddleware(''admin'') gates per type. Triggers: `Auth`, `userType`, `config.auth.userType`, `Authenticable`, `@RegisterModel`, `confirmPassword`; "add admins and users", "multiple user types", "separate client and vendor personas", "per-type login"; typical import `import { Auth } from "@warlock.js/auth"`. Skip: `authMiddleware` semantics — `@warlock.js/auth/protect-routes/SKILL.md`; login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; RBAC libs `casl`, `accesscontrol`, `rbac`.'
@@ -357,10 +357,10 @@ abstract class Auth<TSchema> extends Model<TSchema> implements Authenticable {
357
357
  - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — `authMiddleware` semantics
358
358
  - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — passing the right Model to `login`
359
359
  - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `@RegisterModel`, models in general
360
-
361
-
362
- ## handle-login-and-logout `@warlock.js/auth/handle-login-and-logout/SKILL.md`
363
-
360
+
361
+
362
+ ## handle-login-and-logout `@warlock.js/auth/handle-login-and-logout/SKILL.md`
363
+
364
364
  ---
365
365
  name: handle-login-and-logout
366
366
  description: 'Run the full login flow via authService.login(Model, credentials, deviceInfo?) — verify password, create access + refresh token pair, fire events. Logout via authService.logout(user, accessToken?, refreshToken?) revokes tokens. Triggers: `authService.login`, `authService.logout`, `authService.attemptLogin`, `authService.refreshTokens`, `authService.revokeAllTokens`, `authEvents`; "build a login endpoint", "POST /login controller", "logout from all devices", "verify credentials and issue tokens"; typical import `import { authService, authEvents } from "@warlock.js/auth"`. Skip: token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; sign-up — `@warlock.js/auth/register-user/SKILL.md`; competing libs `passport-local`, `next-auth` credentials.'
@@ -521,10 +521,10 @@ Full event list: `login.attempt`, `login.success`, `login.failed`, `logout`, `lo
521
521
  - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token lifecycle, rotation, family revocation
522
522
  - [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — sign-up that issues tokens after creation
523
523
  - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — where the access token gets consumed
524
-
525
-
526
- ## manage-tokens `@warlock.js/auth/manage-tokens/SKILL.md`
527
-
524
+
525
+
526
+ ## manage-tokens `@warlock.js/auth/manage-tokens/SKILL.md`
527
+
528
528
  ---
529
529
  name: manage-tokens
530
530
  description: 'Token lifecycle — generateAccessToken, createRefreshToken, createTokenPair, refreshTokens (with rotation + replay detection), revokeAllTokens, revokeTokenFamily, cleanupExpiredTokens, getActiveSessions. Triggers: `createTokenPair`, `refreshTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`, `jwt.generate`, `jwt.verify`, `AccessToken`, `RefreshToken`; "rotate refresh tokens", "detect token replay", "logout from all devices", "list active sessions", "clean up expired tokens"; typical import `import { authService, jwt } from "@warlock.js/auth"`. Skip: login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; CLI cleanup — `@warlock.js/auth/run-auth-commands/SKILL.md`; competing libs `jsonwebtoken`, `jose`, `fast-jwt`.'
@@ -694,10 +694,10 @@ The package signs access and refresh tokens with independent secrets — `config
694
694
  - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — full login/logout flow that uses these primitives
695
695
  - [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) — the bundled cleanup command
696
696
  - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — scheduling cleanup
697
-
698
-
699
- ## overview `@warlock.js/auth/overview/SKILL.md`
700
-
697
+
698
+
699
+ ## overview `@warlock.js/auth/overview/SKILL.md`
700
+
701
701
  ---
702
702
  name: overview
703
703
  description: 'Front-door orientation for `@warlock.js/auth` — JWT authentication for Warlock apps: the `Auth` base model, `authMiddleware` route gate, `authService` (login / logout / refresh with token rotation + replay detection), persisted AccessToken + RefreshToken, multi-user-type support, auth lifecycle events, and two CLI commands. Coupled to `@warlock.js/core`. TRIGGER when: code imports anything from `@warlock.js/auth`; user asks "what does @warlock.js/auth do", "how do I add login to my Warlock app", "JWT auth in Warlock", "protect a route", "multiple user types / admin + user", "refresh token rotation"; package.json adds `@warlock.js/auth`. Skip: specific task already known — load the matching task skill directly (`auth-basics`, `protect-routes`, `handle-login-and-logout`, `register-user`, `manage-tokens`, `customize-user-type`, `run-auth-commands`); non-Warlock apps (this package depends on core); session-cookie auth (this is JWT/token-based).'
@@ -770,10 +770,10 @@ Two CLI commands — `warlock jwt.generate` (strong JWT secret → `.env`) and `
770
770
  - [`@warlock.js/core/warlock-conventions/SKILL.md`](@warlock.js/core/warlock-conventions/SKILL.md) — the framework auth runs inside (routing, middleware, config).
771
771
  - [`@warlock.js/cascade/cascade-basics/SKILL.md`](@warlock.js/cascade/cascade-basics/SKILL.md) — the ORM behind the `Auth`, `AccessToken`, and `RefreshToken` models.
772
772
  - `mongez-agent-kit-authoring-skills` (load via agent-kit sync) — how this `overview/SKILL.md` becomes `.claude/skills/warlock-js-auth-overview/`.
773
-
774
-
775
- ## protect-routes `@warlock.js/auth/protect-routes/SKILL.md`
776
-
773
+
774
+
775
+ ## protect-routes `@warlock.js/auth/protect-routes/SKILL.md`
776
+
777
777
  ---
778
778
  name: protect-routes
779
779
  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.'
@@ -868,161 +868,198 @@ async function feedController(request: Request, response: Response) {
868
868
 
869
869
  The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
870
870
 
871
- ## Things NOT to do
872
-
873
- - 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).
874
- - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
875
- - 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.
876
- - 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.
877
-
878
- ## See also
879
-
880
- - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
881
- - [`@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
882
- - [`@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
883
-
884
-
885
- ## register-user `@warlock.js/auth/register-user/SKILL.md`
886
-
887
- ---
888
- name: register-user
889
- description: 'Sign up a new user and issue the initial token pair — User.create({...password: await hashPassword(plain)}) then authService.createTokenPair(user). Triggers: `User.create`, `hashPassword`, `verifyPassword`, `authService.createTokenPair`, `toJsonColumns`, `strongPassword`, `authEvents`; "build a register endpoint", "POST /register controller", "sign up a new user", "hash password on signup", "email verification flow"; typical import `import { authService } from "@warlock.js/auth"; import { hashPassword } from "@warlock.js/core"`. Skip: login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; competing libs `bcrypt`, `bcryptjs`, `argon2`.'
890
- ---
891
-
892
- # Register-and-issue-tokens flow
893
-
894
- Two-step on the server: create the user (with hashed password), then issue tokens. Cascade handles the persistence; `authService` handles the tokens.
895
-
896
- ## The minimal shape
897
-
898
- ```ts
899
- import { authService } from "@warlock.js/auth";
900
- import { hashPassword } from "@warlock.js/core";
901
- import { User } from "@/app/users/models/user.model";
902
-
903
- async function registerController(request: Request, response: Response) {
904
- const { email, password, name } = request.all();
905
-
906
- // 1. Check duplicates
907
- const existing = await User.first({ email });
908
- if (existing) {
909
- return response.conflict({ error: "Email already registered" });
910
- }
911
-
912
- // 2. Create the user with hashed password
913
- const user = await User.create({
914
- email,
915
- name,
916
- password: await hashPassword(password),
917
- });
918
-
919
- // 3. Issue tokens
920
- const tokens = await authService.createTokenPair(user, {
921
- userAgent: request.header("user-agent"),
922
- ip: request.ip,
923
- });
924
-
925
- // 4. Respond
926
- return response.successCreate({
927
- user, // shape via static toJsonColumns / static resource
928
- tokens,
929
- });
930
- }
931
- ```
932
-
933
- That's the whole flow. `User.create({...})` runs the schema validation (including `.email()`, `.min()`, etc. on each field), so you don't need a separate validation pass — see [`@warlock.js/seal/handle-seal-errors/SKILL.md`](@warlock.js/seal/handle-seal-errors/SKILL.md) for catching validation failures.
934
-
935
- ## Hash the password on the way in
936
-
937
- Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
938
-
939
- ```ts
940
- import { hashPassword, verifyPassword } from "@warlock.js/core";
941
-
942
- const hash = await hashPassword("plaintext"); // store this
943
- const ok = await verifyPassword("plaintext", hash); // compare on login
944
- ```
871
+ ## When flat user types aren't enough — reach for `@warlock.js/access`
945
872
 
946
- `authService.attemptLogin` already calls `verifyPassword` against the stored hash you don't compare passwords manually.
873
+ `authMiddleware` gates on **user type** by flat string membership. That answers "is this an admin?" and nothing more. It deliberately cannot express:
947
874
 
948
- ## Schema enforcement
875
+ - a permission matrix (`articles.publish` rather than `"editor"`)
876
+ - role hierarchy or inheritance
877
+ - **who may act on whom** — "an `admin` may create a `teacher`, but only a `superAdmin` may create or promote an `admin`"
949
878
 
950
- Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
879
+ That last one is the difference between a role check and an authorization model: without it, an account-creation endpoint gated on `authMiddleware("admin")` is a privilege-escalation path.
951
880
 
952
- ```ts
953
- const userSchema = v.object({
954
- email: v.string().email(),
955
- name: v.string().min(2).max(120),
956
- password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
957
- // status, role, etc.
958
- });
959
- ```
960
-
961
- But **don't return the password in the public output**:
881
+ Install [`@warlock.js/access`](@warlock.js/access/overview/SKILL.md) for that. It layers RBAC plus per-permission ABAC policies over the same authenticated user:
962
882
 
963
883
  ```ts
964
- @RegisterModel()
965
- export class User extends Model<UserSchema> {
966
- public static table = "users";
967
- public static schema = userSchema;
968
- public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
969
- }
970
- ```
971
-
972
- Without this, `JSON.stringify(user)` in your response leaks the hash.
973
-
974
- ## Email verification flow (extending registration)
884
+ import { gate, can, definePolicy } from "@warlock.js/access";
975
885
 
976
- Common pattern: create the user as `email_verified = false`, send a verification email, mark verified on click. The auth package doesn't ship this; build it on top:
886
+ // Permission-based route gate, in place of a user-type gate
887
+ router.post("/articles", articlesController, { middleware: [authMiddleware([]), gate("articles.create")] });
977
888
 
978
- ```ts
979
- const user = await User.create({
980
- ...data,
981
- email_verified: false,
982
- verification_token: Random.string(64),
983
- });
984
-
985
- await mailer.sendVerificationEmail(user.get("email"), user.get("verification_token"));
889
+ // Who-may-act-on-whom: an ABAC policy on top of the RBAC grant
890
+ definePolicy("users.create", (actor, target, ctx) =>
891
+ ctx.hasRole("superAdmin") || (target as User).userType === "teacher",
892
+ );
986
893
 
987
- const tokens = await authService.createTokenPair(user);
988
- return response.successCreate({ user, tokens });
894
+ if (await can(request.user, "users.create", { resource: payload })) { /* ... */ }
989
895
  ```
990
896
 
991
- Optional: pre-verification, restrict the user to a `unverified` user-type and gate routes accordingly via `authMiddleware("user")`. After verification, swap user-type to `user`.
897
+ Use `authMiddleware` to establish *who the caller is*, and `access` to decide *what they may do*. They compose — `access` reads the user `authMiddleware` put on the request.
992
898
 
993
- ## Side effects via auth events
899
+ ## Sessions are bearer-token, not cookie
994
900
 
995
- Hook post-registration logic:
901
+ The token is read from the `Authorization` header (`Bearer <token>` or `Key <token>`) and nowhere else. "Session" in this package means a **refresh-token row**, not an HTTP cookie session.
996
902
 
997
- ```ts
998
- import { authEvents } from "@warlock.js/auth";
903
+ There is no cookie-backed guard, no session store, and no cookie strategy in `authMiddleware` — that is the intended design, not an omission. Warlock's auth optimises for API services.
999
904
 
1000
- authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
1001
- if (user.get("created_at") > new Date(Date.now() - 5000)) {
1002
- // freshly created in the last 5s — treat as registration
1003
- await sendWelcomeEmail(user);
1004
- }
1005
- });
1006
- ```
1007
-
1008
- Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
905
+ If you want HttpOnly cookie sessions for a server-rendered admin, that is **app-level work**: set the cookie yourself with `response.cookie` (secure by default since 4.10.0) and read it in your own middleware, then hand the token to the same `authService` rotation logic. **You also own CSRF** — the framework ships no CSRF helper, and cookie auth needs one where bearer auth does not. A synchronizer token bound to the session plus `Origin`/`Referer` checks is the usual shape.
1009
906
 
1010
907
  ## Things NOT to do
1011
908
 
1012
- - Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
1013
- - Don't return the user without `toJsonColumns` / `resource` shaping the password hash will leak otherwise.
1014
- - Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
1015
- - Don't run "send welcome email" inline in the controller. Push it to a queue or run it after-commit via the outbox pattern see [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md).
909
+ - 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).
910
+ - Don't manually decode JWTs in the controller. The middleware already does it and exposes the decoded payload via `request.decodedAccessToken`.
911
+ - 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.
912
+ - 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.
1016
913
 
1017
914
  ## See also
1018
915
 
1019
- - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
1020
- - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
1021
- - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
1022
-
1023
-
1024
- ## run-auth-commands `@warlock.js/auth/run-auth-commands/SKILL.md`
1025
-
916
+ - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
917
+ - [`@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
918
+ - [`@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
919
+ - [`@warlock.js/access/overview/SKILL.md`](@warlock.js/access/overview/SKILL.md) — permissions, roles, and who-may-act-on-whom policies, when user-type gating isn't enough
920
+
921
+
922
+ ## register-user `@warlock.js/auth/register-user/SKILL.md`
923
+
924
+ ---
925
+ name: register-user
926
+ description: 'Sign up a new user and issue the initial token pair — User.create({...password: await hashPassword(plain)}) then authService.createTokenPair(user). Triggers: `User.create`, `hashPassword`, `verifyPassword`, `authService.createTokenPair`, `toJsonColumns`, `strongPassword`, `authEvents`; "build a register endpoint", "POST /register controller", "sign up a new user", "hash password on signup", "email verification flow"; typical import `import { authService } from "@warlock.js/auth"; import { hashPassword } from "@warlock.js/core"`. Skip: login — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; competing libs `bcrypt`, `bcryptjs`, `argon2`.'
927
+ ---
928
+
929
+ # Register-and-issue-tokens flow
930
+
931
+ Two-step on the server: create the user (with hashed password), then issue tokens. Cascade handles the persistence; `authService` handles the tokens.
932
+
933
+ ## The minimal shape
934
+
935
+ ```ts
936
+ import { authService } from "@warlock.js/auth";
937
+ import { hashPassword } from "@warlock.js/core";
938
+ import { User } from "@/app/users/models/user.model";
939
+
940
+ async function registerController(request: Request, response: Response) {
941
+ const { email, password, name } = request.all();
942
+
943
+ // 1. Check duplicates
944
+ const existing = await User.first({ email });
945
+ if (existing) {
946
+ return response.conflict({ error: "Email already registered" });
947
+ }
948
+
949
+ // 2. Create the user with hashed password
950
+ const user = await User.create({
951
+ email,
952
+ name,
953
+ password: await hashPassword(password),
954
+ });
955
+
956
+ // 3. Issue tokens
957
+ const tokens = await authService.createTokenPair(user, {
958
+ userAgent: request.header("user-agent"),
959
+ ip: request.ip,
960
+ });
961
+
962
+ // 4. Respond
963
+ return response.successCreate({
964
+ user, // shape via static toJsonColumns / static resource
965
+ tokens,
966
+ });
967
+ }
968
+ ```
969
+
970
+ That's the whole flow. `User.create({...})` runs the schema validation (including `.email()`, `.min()`, etc. on each field), so you don't need a separate validation pass — see [`@warlock.js/seal/handle-seal-errors/SKILL.md`](@warlock.js/seal/handle-seal-errors/SKILL.md) for catching validation failures.
971
+
972
+ ## Hash the password on the way in
973
+
974
+ Always pass `hashPassword(plain)` — never store the plain password. The `hashPassword` helper is `bcrypt`-based and async; the cost factor matches the framework default.
975
+
976
+ ```ts
977
+ import { hashPassword, verifyPassword } from "@warlock.js/core";
978
+
979
+ const hash = await hashPassword("plaintext"); // store this
980
+ const ok = await verifyPassword("plaintext", hash); // compare on login
981
+ ```
982
+
983
+ `authService.attemptLogin` already calls `verifyPassword` against the stored hash — you don't compare passwords manually.
984
+
985
+ ## Schema enforcement
986
+
987
+ Define the password as `v.string().strongPassword(12)` (or similar) in your User schema so weak passwords are rejected at `create()` time:
988
+
989
+ ```ts
990
+ const userSchema = v.object({
991
+ email: v.string().email(),
992
+ name: v.string().min(2).max(120),
993
+ password: v.string().strongPassword(12), // 12+ chars, upper/lower/digit/symbol
994
+ // status, role, etc.
995
+ });
996
+ ```
997
+
998
+ But **don't return the password in the public output**:
999
+
1000
+ ```ts
1001
+ @RegisterModel()
1002
+ export class User extends Model<UserSchema> {
1003
+ public static table = "users";
1004
+ public static schema = userSchema;
1005
+ public static toJsonColumns = ["id", "email", "name", "created_at"]; // omit password
1006
+ }
1007
+ ```
1008
+
1009
+ Without this, `JSON.stringify(user)` in your response leaks the hash.
1010
+
1011
+ ## Email verification flow (extending registration)
1012
+
1013
+ Common pattern: create the user as `email_verified = false`, send a verification email, mark verified on click. The auth package doesn't ship this; build it on top:
1014
+
1015
+ ```ts
1016
+ const user = await User.create({
1017
+ ...data,
1018
+ email_verified: false,
1019
+ verification_token: Random.string(64),
1020
+ });
1021
+
1022
+ await mailer.sendVerificationEmail(user.get("email"), user.get("verification_token"));
1023
+
1024
+ const tokens = await authService.createTokenPair(user);
1025
+ return response.successCreate({ user, tokens });
1026
+ ```
1027
+
1028
+ Optional: pre-verification, restrict the user to a `unverified` user-type and gate routes accordingly via `authMiddleware("user")`. After verification, swap user-type to `user`.
1029
+
1030
+ ## Side effects via auth events
1031
+
1032
+ Hook post-registration logic:
1033
+
1034
+ ```ts
1035
+ import { authEvents } from "@warlock.js/auth";
1036
+
1037
+ authEvents.on("session.created", async (user, refreshToken, deviceInfo) => {
1038
+ if (user.get("created_at") > new Date(Date.now() - 5000)) {
1039
+ // freshly created in the last 5s — treat as registration
1040
+ await sendWelcomeEmail(user);
1041
+ }
1042
+ });
1043
+ ```
1044
+
1045
+ Cleaner alternative: emit your own `user.registered` event from the controller after `User.create`. Decouples auth-package events from your domain events.
1046
+
1047
+ ## Things NOT to do
1048
+
1049
+ - Don't pass the plain password to `User.create()`. `await hashPassword(plain)` first.
1050
+ - Don't return the user without `toJsonColumns` / `resource` shaping — the password hash will leak otherwise.
1051
+ - Don't issue tokens before validating the user shape. `User.create` runs validation; let it throw on bad input before tokens get created.
1052
+ - Don't run "send welcome email" inline in the controller. Push it to a queue or run it after-commit via the outbox pattern — see [`@warlock.js/cascade/manage-transactions/SKILL.md`](@warlock.js/cascade/manage-transactions/SKILL.md).
1053
+
1054
+ ## See also
1055
+
1056
+ - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — login flow (same `createTokenPair` step)
1057
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — token issuance internals
1058
+ - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — `toJsonColumns` / `resource` for public output
1059
+
1060
+
1061
+ ## run-auth-commands `@warlock.js/auth/run-auth-commands/SKILL.md`
1062
+
1026
1063
  ---
1027
1064
  name: run-auth-commands
1028
1065
  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`.'
@@ -1069,7 +1106,7 @@ Run it once when setting up a new project. Each developer typically runs it loca
1069
1106
  yarn warlock auth.cleanup
1070
1107
  ```
1071
1108
 
1072
- Runs `authService.cleanupExpiredTokens()` — deletes every refresh token whose `expires_at` has passed. Fires `token.expired` per token and `cleanup.completed` once.
1109
+ 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.
1073
1110
 
1074
1111
  Schedule it periodically. Two common shapes:
1075
1112
 
@@ -1148,10 +1185,10 @@ Register it the same way as the bundled commands — call the factory inside `de
1148
1185
 
1149
1186
  - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — `cleanupExpiredTokens` internals
1150
1187
  - [`@warlock.js/scheduler/scheduler-basics/SKILL.md`](@warlock.js/scheduler/scheduler-basics/SKILL.md) — in-process scheduling
1151
-
1152
-
1153
- ## throttle-login-attempts `@warlock.js/auth/throttle-login-attempts/SKILL.md`
1154
-
1188
+
1189
+
1190
+ ## throttle-login-attempts `@warlock.js/auth/throttle-login-attempts/SKILL.md`
1191
+
1155
1192
  ---
1156
1193
  name: throttle-login-attempts
1157
1194
  description: 'Brute-force / credential-stuffing protection via `loginThrottleMiddleware` — a failure-aware route gate that counts only failed logins (resets on success), locks per-account and per-source after a threshold, and rejects pre-controller with 429 so the DB lookup and bcrypt verify are skipped. Cache-backed (shared across replicas), fixed-window, fails open on a cache outage. Triggers: `loginThrottleMiddleware`, `AuthErrorCodes.TooManyAttempts`, `EC004`, "rate limit login", "brute force protection", "lock account after failed logins", "throttle login attempts", "too many login attempts 429"; typical import `import { loginThrottleMiddleware } from "@warlock.js/auth"`. Skip: generic per-route request rate limiting that counts every request (use core `middleware.rateLimit`); gating a route by auth — `@warlock.js/auth/protect-routes/SKILL.md`; issuing tokens — `@warlock.js/auth/handle-login-and-logout/SKILL.md`.'
@@ -1268,5 +1305,5 @@ They compose. For a hard ceiling on request volume *and* failure-aware account l
1268
1305
  - [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) — gate a route behind a valid token (`authMiddleware`); stack the throttle in front of it on login routes.
1269
1306
  - [`@warlock.js/auth/handle-login-and-logout/SKILL.md`](@warlock.js/auth/handle-login-and-logout/SKILL.md) — the login flow whose failures this middleware counts.
1270
1307
  - [`@warlock.js/core/use-middleware/SKILL.md`](@warlock.js/core/use-middleware/SKILL.md) — `middleware.rateLimit` and the built-in middleware suite.
1271
-
1272
-
1308
+
1309
+
package/llms.txt CHANGED
@@ -1,18 +1,18 @@
1
- # Warlock Auth
2
-
3
- > Package: `@warlock.js/auth`
4
-
5
- > Authentication system for Warlock.js applications
6
-
7
- ## Skills
8
-
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
- - [customize-token-storage](@warlock.js/auth/customize-token-storage/SKILL.md): Override the persisted AccessToken / RefreshToken models to add columns (multi-tenant `organization_id`, device metadata), rename, or change storage — without forking the package. Extend the model + `schema.extend(...)`, register it under `config.auth.accessToken.model` / `config.auth.refreshToken.model`, override `issue()` to populate the new column, and add a migration. Triggers: `accessToken.model`, `refreshToken.model`, `AccessToken.issue`, `RefreshToken.issue`, `accessTokenSchema`, `refreshTokenSchema`, "add a column to the token table", "multi-tenant tokens", "organization_id on access token", "override the token model", "custom token storage"; typical import `import { AccessToken, accessTokenSchema } from "@warlock.js/auth"`. Skip: multiple user TYPES (not token storage) — `@warlock.js/auth/customize-user-type/SKILL.md`; the token lifecycle API — `@warlock.js/auth/manage-tokens/SKILL.md`; the config blocks themselves — `@warlock.js/auth/auth-basics/SKILL.md`.
11
- - [customize-user-type](@warlock.js/auth/customize-user-type/SKILL.md): Support multiple user types (user / admin / client / staff) in one auth system — each Auth subclass overrides userType, config.auth.userType.<slug> maps slug to model class, authMiddleware('admin') gates per type. Triggers: `Auth`, `userType`, `config.auth.userType`, `Authenticable`, `@RegisterModel`, `confirmPassword`; "add admins and users", "multiple user types", "separate client and vendor personas", "per-type login"; typical import `import { Auth } from "@warlock.js/auth"`. Skip: `authMiddleware` semantics — `@warlock.js/auth/protect-routes/SKILL.md`; login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; RBAC libs `casl`, `accesscontrol`, `rbac`.
12
- - [handle-login-and-logout](@warlock.js/auth/handle-login-and-logout/SKILL.md): Run the full login flow via authService.login(Model, credentials, deviceInfo?) — verify password, create access + refresh token pair, fire events. Logout via authService.logout(user, accessToken?, refreshToken?) revokes tokens. Triggers: `authService.login`, `authService.logout`, `authService.attemptLogin`, `authService.refreshTokens`, `authService.revokeAllTokens`, `authEvents`; "build a login endpoint", "POST /login controller", "logout from all devices", "verify credentials and issue tokens"; typical import `import { authService, authEvents } from "@warlock.js/auth"`. Skip: token internals — `@warlock.js/auth/manage-tokens/SKILL.md`; sign-up — `@warlock.js/auth/register-user/SKILL.md`; competing libs `passport-local`, `next-auth` credentials.
13
- - [manage-tokens](@warlock.js/auth/manage-tokens/SKILL.md): Token lifecycle — generateAccessToken, createRefreshToken, createTokenPair, refreshTokens (with rotation + replay detection), revokeAllTokens, revokeTokenFamily, cleanupExpiredTokens, getActiveSessions. Triggers: `createTokenPair`, `refreshTokens`, `revokeTokenFamily`, `cleanupExpiredTokens`, `getActiveSessions`, `jwt.generate`, `jwt.verify`, `AccessToken`, `RefreshToken`; "rotate refresh tokens", "detect token replay", "logout from all devices", "list active sessions", "clean up expired tokens"; typical import `import { authService, jwt } from "@warlock.js/auth"`. Skip: login flow — `@warlock.js/auth/handle-login-and-logout/SKILL.md`; CLI cleanup — `@warlock.js/auth/run-auth-commands/SKILL.md`; competing libs `jsonwebtoken`, `jose`, `fast-jwt`.
14
- - [overview](@warlock.js/auth/overview/SKILL.md): Front-door orientation for `@warlock.js/auth` — JWT authentication for Warlock apps: the `Auth` base model, `authMiddleware` route gate, `authService` (login / logout / refresh with token rotation + replay detection), persisted AccessToken + RefreshToken, multi-user-type support, auth lifecycle events, and two CLI commands. Coupled to `@warlock.js/core`. TRIGGER when: code imports anything from `@warlock.js/auth`; user asks "what does @warlock.js/auth do", "how do I add login to my Warlock app", "JWT auth in Warlock", "protect a route", "multiple user types / admin + user", "refresh token rotation"; package.json adds `@warlock.js/auth`. Skip: specific task already known — load the matching task skill directly (`auth-basics`, `protect-routes`, `handle-login-and-logout`, `register-user`, `manage-tokens`, `customize-user-type`, `run-auth-commands`); non-Warlock apps (this package depends on core); session-cookie auth (this is JWT/token-based).
15
- - [protect-routes](@warlock.js/auth/protect-routes/SKILL.md): 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.
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): 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`.
18
- - [throttle-login-attempts](@warlock.js/auth/throttle-login-attempts/SKILL.md): Brute-force / credential-stuffing protection via `loginThrottleMiddleware` — a failure-aware route gate that counts only failed logins (resets on success), locks per-account and per-source after a threshold, and rejects pre-controller with 429 so the DB lookup and bcrypt verify are skipped. Cache-backed (shared across replicas), fixed-window, fails open on a cache outage. Triggers: `loginThrottleMiddleware`, `AuthErrorCodes.TooManyAttempts`, `EC004`, "rate limit login", "brute force protection", "lock account after failed logins", "throttle login attempts", "too many login attempts 429"; typical import `import { loginThrottleMiddleware } from "@warlock.js/auth"`. Skip: generic per-route request rate limiting that counts every request (use core `middleware.rateLimit`); gating a route by auth — `@warlock.js/auth/protect-routes/SKILL.md`; issuing tokens — `@warlock.js/auth/handle-login-and-logout/SKILL.md`.
1
+ # Warlock Auth
2
+
3
+ > Package: `@warlock.js/auth`
4
+
5
+ > Authentication system for Warlock.js applications
6
+
7
+ ## Skills
8
+
9
+ - [auth-basics](@warlock.js/auth/auth-basics/SKILL.md): (no description)
10
+ - [customize-token-storage](@warlock.js/auth/customize-token-storage/SKILL.md): (no description)
11
+ - [customize-user-type](@warlock.js/auth/customize-user-type/SKILL.md): (no description)
12
+ - [handle-login-and-logout](@warlock.js/auth/handle-login-and-logout/SKILL.md): (no description)
13
+ - [manage-tokens](@warlock.js/auth/manage-tokens/SKILL.md): (no description)
14
+ - [overview](@warlock.js/auth/overview/SKILL.md): (no description)
15
+ - [protect-routes](@warlock.js/auth/protect-routes/SKILL.md): (no description)
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)
18
+ - [throttle-login-attempts](@warlock.js/auth/throttle-login-attempts/SKILL.md): (no description)
package/package.json CHANGED
@@ -23,14 +23,14 @@
23
23
  "ms": "^2.1.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "@warlock.js/fs": "4.9.2",
27
- "@warlock.js/cache": "4.9.2",
28
- "@warlock.js/cascade": "4.9.2",
29
- "@warlock.js/core": "4.9.2",
30
- "@warlock.js/logger": "4.9.2",
31
- "@warlock.js/seal": "4.9.2"
26
+ "@warlock.js/fs": "4.10.0",
27
+ "@warlock.js/cache": "4.10.0",
28
+ "@warlock.js/cascade": "4.10.0",
29
+ "@warlock.js/core": "4.10.0",
30
+ "@warlock.js/logger": "4.10.0",
31
+ "@warlock.js/seal": "4.10.0"
32
32
  },
33
- "version": "4.9.2",
33
+ "version": "4.10.0",
34
34
  "type": "module",
35
35
  "main": "./esm/index.mjs",
36
36
  "module": "./esm/index.mjs",
@@ -92,6 +92,42 @@ async function feedController(request: Request, response: Response) {
92
92
 
93
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
94
 
95
+ ## When flat user types aren't enough — reach for `@warlock.js/access`
96
+
97
+ `authMiddleware` gates on **user type** by flat string membership. That answers "is this an admin?" and nothing more. It deliberately cannot express:
98
+
99
+ - a permission matrix (`articles.publish` rather than `"editor"`)
100
+ - role hierarchy or inheritance
101
+ - **who may act on whom** — "an `admin` may create a `teacher`, but only a `superAdmin` may create or promote an `admin`"
102
+
103
+ That last one is the difference between a role check and an authorization model: without it, an account-creation endpoint gated on `authMiddleware("admin")` is a privilege-escalation path.
104
+
105
+ Install [`@warlock.js/access`](@warlock.js/access/overview/SKILL.md) for that. It layers RBAC plus per-permission ABAC policies over the same authenticated user:
106
+
107
+ ```ts
108
+ import { gate, can, definePolicy } from "@warlock.js/access";
109
+
110
+ // Permission-based route gate, in place of a user-type gate
111
+ router.post("/articles", articlesController, { middleware: [authMiddleware([]), gate("articles.create")] });
112
+
113
+ // Who-may-act-on-whom: an ABAC policy on top of the RBAC grant
114
+ definePolicy("users.create", (actor, target, ctx) =>
115
+ ctx.hasRole("superAdmin") || (target as User).userType === "teacher",
116
+ );
117
+
118
+ if (await can(request.user, "users.create", { resource: payload })) { /* ... */ }
119
+ ```
120
+
121
+ Use `authMiddleware` to establish *who the caller is*, and `access` to decide *what they may do*. They compose — `access` reads the user `authMiddleware` put on the request.
122
+
123
+ ## Sessions are bearer-token, not cookie
124
+
125
+ The token is read from the `Authorization` header (`Bearer <token>` or `Key <token>`) and nowhere else. "Session" in this package means a **refresh-token row**, not an HTTP cookie session.
126
+
127
+ There is no cookie-backed guard, no session store, and no cookie strategy in `authMiddleware` — that is the intended design, not an omission. Warlock's auth optimises for API services.
128
+
129
+ If you want HttpOnly cookie sessions for a server-rendered admin, that is **app-level work**: set the cookie yourself with `response.cookie` (secure by default since 4.10.0) and read it in your own middleware, then hand the token to the same `authService` rotation logic. **You also own CSRF** — the framework ships no CSRF helper, and cookie auth needs one where bearer auth does not. A synchronizer token bound to the session plus `Origin`/`Referer` checks is the usual shape.
130
+
95
131
  ## Things NOT to do
96
132
 
97
133
  - 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).
@@ -104,3 +140,4 @@ The middleware uses the framework's `response.unauthorized({...})` shape. To ove
104
140
  - [`@warlock.js/auth/customize-user-type/SKILL.md`](@warlock.js/auth/customize-user-type/SKILL.md) — config and multi-user-type semantics
105
141
  - [`@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
142
  - [`@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
143
+ - [`@warlock.js/access/overview/SKILL.md`](@warlock.js/access/overview/SKILL.md) — permissions, roles, and who-may-act-on-whom policies, when user-type gating isn't enough