@warlock.js/auth 4.2.10 → 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.
- package/CHANGELOG.md +56 -50
- package/LICENSE +21 -21
- package/README.md +78 -78
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +1272 -1272
- package/llms.txt +18 -18
- package/package.json +8 -8
- package/skills/auth-basics/SKILL.md +89 -89
- package/skills/customize-token-storage/SKILL.md +112 -112
- package/skills/customize-user-type/SKILL.md +142 -142
- package/skills/handle-login-and-logout/SKILL.md +160 -160
- package/skills/manage-tokens/SKILL.md +169 -169
- package/skills/overview/SKILL.md +72 -72
- package/skills/protect-routes/SKILL.md +106 -106
- package/skills/run-auth-commands/SKILL.md +125 -125
- package/skills/throttle-login-attempts/SKILL.md +116 -116
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): 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`.
|
package/package.json
CHANGED
|
@@ -18,19 +18,19 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"@mongez/copper": "^2.1.2",
|
|
20
20
|
"@mongez/events": "^2.2.6",
|
|
21
|
-
"@mongez/reinforcements": "^3.
|
|
21
|
+
"@mongez/reinforcements": "^3.3.0",
|
|
22
22
|
"fast-jwt": "^6.1.0",
|
|
23
23
|
"ms": "^2.1.3"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@warlock.js/fs": "4.
|
|
27
|
-
"@warlock.js/cache": "4.
|
|
28
|
-
"@warlock.js/cascade": "4.
|
|
29
|
-
"@warlock.js/core": "4.
|
|
30
|
-
"@warlock.js/logger": "4.
|
|
31
|
-
"@warlock.js/seal": "4.
|
|
26
|
+
"@warlock.js/fs": "4.3.0",
|
|
27
|
+
"@warlock.js/cache": "4.3.0",
|
|
28
|
+
"@warlock.js/cascade": "4.3.0",
|
|
29
|
+
"@warlock.js/core": "4.3.0",
|
|
30
|
+
"@warlock.js/logger": "4.3.0",
|
|
31
|
+
"@warlock.js/seal": "4.3.0"
|
|
32
32
|
},
|
|
33
|
-
"version": "4.
|
|
33
|
+
"version": "4.3.0",
|
|
34
34
|
"type": "module",
|
|
35
35
|
"main": "./esm/index.mjs",
|
|
36
36
|
"module": "./esm/index.mjs",
|
|
@@ -1,89 +1,89 @@
|
|
|
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
|
-
yarn 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
|
-
## Pick a skill
|
|
74
|
-
|
|
75
|
-
| If the task is about… | Load |
|
|
76
|
-
| --- | --- |
|
|
77
|
-
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
78
|
-
| `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) |
|
|
79
|
-
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
80
|
-
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
81
|
-
| 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) |
|
|
82
|
-
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
83
|
-
|
|
84
|
-
## Things NOT to do
|
|
85
|
-
|
|
86
|
-
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
87
|
-
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
88
|
-
- 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)).
|
|
89
|
-
- 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
|
+
yarn 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
|
+
## Pick a skill
|
|
74
|
+
|
|
75
|
+
| If the task is about… | Load |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| Gating routes with `authMiddleware(allowedUserType)`, any-authenticated vs typed access | [`@warlock.js/auth/protect-routes/SKILL.md`](@warlock.js/auth/protect-routes/SKILL.md) |
|
|
78
|
+
| `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) |
|
|
79
|
+
| Token lifecycle — `generateAccessToken`, `createRefreshToken`, rotation, family revocation, max-per-user | [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) |
|
|
80
|
+
| Register a new user + issue tokens in one flow | [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) |
|
|
81
|
+
| 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) |
|
|
82
|
+
| `warlock jwt.generate` + `warlock auth.cleanup` CLI commands | [`@warlock.js/auth/run-auth-commands/SKILL.md`](@warlock.js/auth/run-auth-commands/SKILL.md) |
|
|
83
|
+
|
|
84
|
+
## Things NOT to do
|
|
85
|
+
|
|
86
|
+
- Don't write your own JWT signing logic — use `authService` / `jwt` from this package so signature/secret/expiry stay consistent.
|
|
87
|
+
- Don't store the JWT secret in the model layer or anywhere user-modifiable. It lives in `.env` only.
|
|
88
|
+
- 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)).
|
|
89
|
+
- 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,112 +1,112 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: customize-token-storage
|
|
3
|
-
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`.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Customize token storage
|
|
7
|
-
|
|
8
|
-
The package ships `AccessToken` and `RefreshToken` models and runs the whole token flow through their **named statics** — `issue`, `findByToken`, `activeFor`, `revokeAllFor`, etc. The auth service never references a column name directly; it resolves the *active* model from config. So you change token storage by **registering a subclass**, not by forking.
|
|
9
|
-
|
|
10
|
-
Reach for this when you need an extra column on the token tables — the common case is a tenant key (`organization_id`) so tokens are partitioned per organization, or richer device metadata.
|
|
11
|
-
|
|
12
|
-
## The three pieces
|
|
13
|
-
|
|
14
|
-
A storage override is always three coordinated steps. Miss any one and it breaks — see the strict-mode note below.
|
|
15
|
-
|
|
16
|
-
### 1. Extend the model + its schema
|
|
17
|
-
|
|
18
|
-
`accessTokenSchema` / `refreshTokenSchema` are exported so you compose them with `.extend(...)` instead of re-declaring the base shape. Override `issue()` to populate your new column (the user is in hand there).
|
|
19
|
-
|
|
20
|
-
```ts title="src/app/auth/models/app-access-token.ts"
|
|
21
|
-
import { AccessToken, accessTokenSchema, type Auth } from "@warlock.js/auth";
|
|
22
|
-
import { v } from "@warlock.js/seal";
|
|
23
|
-
|
|
24
|
-
export class AppAccessToken extends AccessToken {
|
|
25
|
-
public static schema = accessTokenSchema.extend({
|
|
26
|
-
organization_id: v.string().exists("Organization", { column: "id" }),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// populate the tenant key on issue — read it off the authenticating user
|
|
30
|
-
public static issue(user: Auth, token: string, expiresAt: Date) {
|
|
31
|
-
return this.create({
|
|
32
|
-
token,
|
|
33
|
-
user_id: user.id,
|
|
34
|
-
user_type: user.userType,
|
|
35
|
-
expires_at: expiresAt,
|
|
36
|
-
organization_id: user.get("organization_id"),
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Refresh tokens follow the same shape — extend `refreshTokenSchema`, and override `issue(user, token, options)` (`options` is `{ familyId, expiresAt, deviceInfo? }`) the same way, copying the base fields plus your column.
|
|
43
|
-
|
|
44
|
-
### 2. Register the subclass in config
|
|
45
|
-
|
|
46
|
-
```ts title="src/config/auth.ts"
|
|
47
|
-
import { AppAccessToken } from "app/auth/models/app-access-token";
|
|
48
|
-
import { AppRefreshToken } from "app/auth/models/app-refresh-token";
|
|
49
|
-
|
|
50
|
-
export default {
|
|
51
|
-
userType: { user: User },
|
|
52
|
-
accessToken: {
|
|
53
|
-
model: AppAccessToken, // ← the override
|
|
54
|
-
secret: env("JWT_SECRET"),
|
|
55
|
-
expiresIn: "1h",
|
|
56
|
-
},
|
|
57
|
-
refreshToken: {
|
|
58
|
-
model: AppRefreshToken, // ← the override
|
|
59
|
-
secret: env("JWT_REFRESH_SECRET"),
|
|
60
|
-
expiresIn: "30d",
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
From here the service, the middleware, and every `authService` helper transparently use your model — `findByToken`, `revokeAllFor`, `enforceMax`, and the rest all run against your columns.
|
|
66
|
-
|
|
67
|
-
### 3. Add the column to the migration
|
|
68
|
-
|
|
69
|
-
The new column needs a real database column. Add it to your token-table migration (the FK + index match your `User` model's tenant convention):
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
this.uuid("organization_id").references("organizations").onDelete("cascade").index();
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration mechanics.
|
|
76
|
-
|
|
77
|
-
## Why all three — the strict-mode trap
|
|
78
|
-
|
|
79
|
-
This is the failure people hit. Cascade's `strictMode: "strip"` **drops any field your schema doesn't declare** before the INSERT. So if you set `organization_id` in `issue()` but don't add it to the schema (step 1), the value is silently stripped, and a NOT-NULL `organization_id` column then **fails the INSERT**. The chain is: `issue()` sets it → the *schema* must declare it so it survives → the *migration* must create the column. All three, or nothing.
|
|
80
|
-
|
|
81
|
-
## You do NOT need read-side scoping
|
|
82
|
-
|
|
83
|
-
Token lookups go by the unique `token` string (`findByToken`), which already uniquely identifies a row regardless of tenant. So you don't need a tenant-scoped global query scope on the token model — `organization_id` is there for FK cascade-cleanup when an org is deleted, plus partitioning and analytics, not for lookup safety.
|
|
84
|
-
|
|
85
|
-
## What you're overriding (the contract)
|
|
86
|
-
|
|
87
|
-
Your subclass inherits and may override these statics — the service calls them, never raw queries:
|
|
88
|
-
|
|
89
|
-
| Static | Role |
|
|
90
|
-
| --- | --- |
|
|
91
|
-
| `issue(user, token, …)` | persist a freshly-signed token (override to add columns) |
|
|
92
|
-
| `findByToken(token)` | look a row up by its token string |
|
|
93
|
-
| `findForUser(user, token)` | a user-scoped lookup (logout) |
|
|
94
|
-
| `activeFor(user)` | active, unexpired sessions, newest-first |
|
|
95
|
-
| `revokeAllFor(user)` / `revokeFamily(id)` | revoke a set, returning the revoked rows |
|
|
96
|
-
| `enforceMax(user, max)` | cap concurrent refresh tokens |
|
|
97
|
-
| `purgeExpired()` | delete expired rows (CLI cleanup) |
|
|
98
|
-
|
|
99
|
-
If you rename a column, override the statics that reference it so they map to your name — the service depends on the method, not the column.
|
|
100
|
-
|
|
101
|
-
## Things NOT to do
|
|
102
|
-
|
|
103
|
-
- **Don't set a column without declaring it in `schema.extend(...)`.** `strictMode: "strip"` removes it; a NOT-NULL column then fails the INSERT.
|
|
104
|
-
- **Don't add a tenant-scoped global scope to the token model.** Lookups are by unique token; a leaky scope is a cross-tenant exposure risk for no lookup benefit.
|
|
105
|
-
- **Don't fork the package to add a column.** Extend + register — you keep receiving package fixes.
|
|
106
|
-
- **Don't forget the migration.** The schema declares the field; only the migration creates the database column.
|
|
107
|
-
|
|
108
|
-
## See also
|
|
109
|
-
|
|
110
|
-
- [`@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*).
|
|
111
|
-
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — the token lifecycle your statics power.
|
|
112
|
-
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — extending models and schemas.
|
|
1
|
+
---
|
|
2
|
+
name: customize-token-storage
|
|
3
|
+
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`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Customize token storage
|
|
7
|
+
|
|
8
|
+
The package ships `AccessToken` and `RefreshToken` models and runs the whole token flow through their **named statics** — `issue`, `findByToken`, `activeFor`, `revokeAllFor`, etc. The auth service never references a column name directly; it resolves the *active* model from config. So you change token storage by **registering a subclass**, not by forking.
|
|
9
|
+
|
|
10
|
+
Reach for this when you need an extra column on the token tables — the common case is a tenant key (`organization_id`) so tokens are partitioned per organization, or richer device metadata.
|
|
11
|
+
|
|
12
|
+
## The three pieces
|
|
13
|
+
|
|
14
|
+
A storage override is always three coordinated steps. Miss any one and it breaks — see the strict-mode note below.
|
|
15
|
+
|
|
16
|
+
### 1. Extend the model + its schema
|
|
17
|
+
|
|
18
|
+
`accessTokenSchema` / `refreshTokenSchema` are exported so you compose them with `.extend(...)` instead of re-declaring the base shape. Override `issue()` to populate your new column (the user is in hand there).
|
|
19
|
+
|
|
20
|
+
```ts title="src/app/auth/models/app-access-token.ts"
|
|
21
|
+
import { AccessToken, accessTokenSchema, type Auth } from "@warlock.js/auth";
|
|
22
|
+
import { v } from "@warlock.js/seal";
|
|
23
|
+
|
|
24
|
+
export class AppAccessToken extends AccessToken {
|
|
25
|
+
public static schema = accessTokenSchema.extend({
|
|
26
|
+
organization_id: v.string().exists("Organization", { column: "id" }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// populate the tenant key on issue — read it off the authenticating user
|
|
30
|
+
public static issue(user: Auth, token: string, expiresAt: Date) {
|
|
31
|
+
return this.create({
|
|
32
|
+
token,
|
|
33
|
+
user_id: user.id,
|
|
34
|
+
user_type: user.userType,
|
|
35
|
+
expires_at: expiresAt,
|
|
36
|
+
organization_id: user.get("organization_id"),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Refresh tokens follow the same shape — extend `refreshTokenSchema`, and override `issue(user, token, options)` (`options` is `{ familyId, expiresAt, deviceInfo? }`) the same way, copying the base fields plus your column.
|
|
43
|
+
|
|
44
|
+
### 2. Register the subclass in config
|
|
45
|
+
|
|
46
|
+
```ts title="src/config/auth.ts"
|
|
47
|
+
import { AppAccessToken } from "app/auth/models/app-access-token";
|
|
48
|
+
import { AppRefreshToken } from "app/auth/models/app-refresh-token";
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
userType: { user: User },
|
|
52
|
+
accessToken: {
|
|
53
|
+
model: AppAccessToken, // ← the override
|
|
54
|
+
secret: env("JWT_SECRET"),
|
|
55
|
+
expiresIn: "1h",
|
|
56
|
+
},
|
|
57
|
+
refreshToken: {
|
|
58
|
+
model: AppRefreshToken, // ← the override
|
|
59
|
+
secret: env("JWT_REFRESH_SECRET"),
|
|
60
|
+
expiresIn: "30d",
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
From here the service, the middleware, and every `authService` helper transparently use your model — `findByToken`, `revokeAllFor`, `enforceMax`, and the rest all run against your columns.
|
|
66
|
+
|
|
67
|
+
### 3. Add the column to the migration
|
|
68
|
+
|
|
69
|
+
The new column needs a real database column. Add it to your token-table migration (the FK + index match your `User` model's tenant convention):
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
this.uuid("organization_id").references("organizations").onDelete("cascade").index();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration mechanics.
|
|
76
|
+
|
|
77
|
+
## Why all three — the strict-mode trap
|
|
78
|
+
|
|
79
|
+
This is the failure people hit. Cascade's `strictMode: "strip"` **drops any field your schema doesn't declare** before the INSERT. So if you set `organization_id` in `issue()` but don't add it to the schema (step 1), the value is silently stripped, and a NOT-NULL `organization_id` column then **fails the INSERT**. The chain is: `issue()` sets it → the *schema* must declare it so it survives → the *migration* must create the column. All three, or nothing.
|
|
80
|
+
|
|
81
|
+
## You do NOT need read-side scoping
|
|
82
|
+
|
|
83
|
+
Token lookups go by the unique `token` string (`findByToken`), which already uniquely identifies a row regardless of tenant. So you don't need a tenant-scoped global query scope on the token model — `organization_id` is there for FK cascade-cleanup when an org is deleted, plus partitioning and analytics, not for lookup safety.
|
|
84
|
+
|
|
85
|
+
## What you're overriding (the contract)
|
|
86
|
+
|
|
87
|
+
Your subclass inherits and may override these statics — the service calls them, never raw queries:
|
|
88
|
+
|
|
89
|
+
| Static | Role |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| `issue(user, token, …)` | persist a freshly-signed token (override to add columns) |
|
|
92
|
+
| `findByToken(token)` | look a row up by its token string |
|
|
93
|
+
| `findForUser(user, token)` | a user-scoped lookup (logout) |
|
|
94
|
+
| `activeFor(user)` | active, unexpired sessions, newest-first |
|
|
95
|
+
| `revokeAllFor(user)` / `revokeFamily(id)` | revoke a set, returning the revoked rows |
|
|
96
|
+
| `enforceMax(user, max)` | cap concurrent refresh tokens |
|
|
97
|
+
| `purgeExpired()` | delete expired rows (CLI cleanup) |
|
|
98
|
+
|
|
99
|
+
If you rename a column, override the statics that reference it so they map to your name — the service depends on the method, not the column.
|
|
100
|
+
|
|
101
|
+
## Things NOT to do
|
|
102
|
+
|
|
103
|
+
- **Don't set a column without declaring it in `schema.extend(...)`.** `strictMode: "strip"` removes it; a NOT-NULL column then fails the INSERT.
|
|
104
|
+
- **Don't add a tenant-scoped global scope to the token model.** Lookups are by unique token; a leaky scope is a cross-tenant exposure risk for no lookup benefit.
|
|
105
|
+
- **Don't fork the package to add a column.** Extend + register — you keep receiving package fixes.
|
|
106
|
+
- **Don't forget the migration.** The schema declares the field; only the migration creates the database column.
|
|
107
|
+
|
|
108
|
+
## See also
|
|
109
|
+
|
|
110
|
+
- [`@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*).
|
|
111
|
+
- [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — the token lifecycle your statics power.
|
|
112
|
+
- [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — extending models and schemas.
|