@warlock.js/auth 5.24.0 → 5.25.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -21
  3. package/esm/contracts/types.mjs.map +1 -1
  4. package/esm/middleware/auth.middleware.mjs +1 -1
  5. package/esm/middleware/auth.middleware.mjs.map +1 -1
  6. package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
  7. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  8. package/esm/models/access-token/migration.mjs.map +1 -1
  9. package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
  10. package/esm/models/auth-token-family/auth-token-family.model.mjs +5 -0
  11. package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
  12. package/esm/models/auth.model.mjs.map +1 -1
  13. package/esm/models/refresh-token/migration.mjs.map +1 -1
  14. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  15. package/esm/services/auth-config.mjs.map +1 -1
  16. package/esm/services/auth-events.mjs.map +1 -1
  17. package/esm/services/auth.service.d.mts.map +1 -1
  18. package/esm/services/auth.service.mjs +10 -9
  19. package/esm/services/auth.service.mjs.map +1 -1
  20. package/esm/utils/auth-error-codes.mjs.map +1 -1
  21. package/llms-full.txt +1927 -1927
  22. package/package.json +8 -8
  23. package/skills/auth-basics/SKILL.md +1 -1
  24. package/skills/customize-token-storage/SKILL.md +160 -160
  25. package/skills/customize-user-type/SKILL.md +168 -168
  26. package/skills/handle-login-and-logout/SKILL.md +179 -179
  27. package/skills/login-with-providers/SKILL.md +283 -283
  28. package/skills/manage-tokens/SKILL.md +196 -196
  29. package/skills/overview/SKILL.md +74 -74
  30. package/skills/protect-routes/SKILL.md +299 -299
  31. package/skills/register-user/SKILL.md +147 -147
  32. package/skills/run-auth-commands/SKILL.md +159 -159
  33. package/skills/throttle-login-attempts/SKILL.md +117 -117
  34. package/skills/verify-email-and-reset-password/SKILL.md +137 -137
package/package.json CHANGED
@@ -12,13 +12,13 @@
12
12
  "ms": "^2.1.3"
13
13
  },
14
14
  "peerDependencies": {
15
- "@warlock.js/fs": "5.24.0",
16
- "@warlock.js/cache": "5.24.0",
17
- "@warlock.js/cascade": "5.24.0",
18
- "@warlock.js/core": "5.24.0",
19
- "@warlock.js/logger": "5.24.0",
20
- "@warlock.js/seal": "5.24.0",
21
- "@warlock.js/notifications": "5.24.0",
15
+ "@warlock.js/fs": "5.25.0",
16
+ "@warlock.js/cache": "5.25.0",
17
+ "@warlock.js/cascade": "5.25.0",
18
+ "@warlock.js/core": "5.25.0",
19
+ "@warlock.js/logger": "5.25.0",
20
+ "@warlock.js/seal": "5.25.0",
21
+ "@warlock.js/notifications": "5.25.0",
22
22
  "@simplewebauthn/server": "^13.1.0",
23
23
  "jose": "^6.1.0"
24
24
  },
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "author": "hassanzohdy",
49
49
  "license": "MIT",
50
- "version": "5.24.0",
50
+ "version": "5.25.0",
51
51
  "type": "module",
52
52
  "main": "./esm/index.mjs",
53
53
  "module": "./esm/index.mjs",
@@ -71,7 +71,7 @@ export default {
71
71
  user: User,
72
72
  // admin: Admin, // for multi-user-type
73
73
  },
74
- defaultUserType: "user", // needed for authMiddleware() when multiple types exist
74
+ defaultUserType: "user", // needed for authMiddleware() when multiple types exist
75
75
  accessToken: {
76
76
  secret: env("JWT_SECRET"),
77
77
  expiresIn: "1h",
@@ -1,160 +1,160 @@
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
- ```ts title="src/app/users/models/user/user.model.ts"
13
- import { Auth } from "@warlock.js/auth";
14
- import { RegisterModel } from "@warlock.js/cascade";
15
-
16
- @RegisterModel()
17
- export class User extends Auth {
18
- public static table = "users";
19
-
20
- public get userType(): string {
21
- return "user";
22
- }
23
- }
24
- ```
25
-
26
- ## The three pieces
27
-
28
- A storage override is always three coordinated steps. Miss any one and it breaks — see the strict-mode note below.
29
-
30
- ### 1. Extend the model + its schema
31
-
32
- `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).
33
-
34
- ```ts title="src/app/auth/models/app-access-token.ts"
35
- import { AccessToken, accessTokenSchema, type Auth } from "@warlock.js/auth";
36
- import { v } from "@warlock.js/seal";
37
-
38
- export class AppAccessToken extends AccessToken {
39
- public static schema = accessTokenSchema.extend({
40
- organization_id: v.string().exists("Organization", { column: "id" }),
41
- });
42
-
43
- // populate the tenant key on issue — read it off the authenticating user
44
- public static issue(user: Auth, token: string, expiresAt: Date) {
45
- return this.create({
46
- token,
47
- user_id: user.id,
48
- user_type: user.userType,
49
- expires_at: expiresAt,
50
- organization_id: user.get("organization_id"),
51
- });
52
- }
53
- }
54
- ```
55
-
56
- 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.
57
-
58
- ```ts title="src/app/auth/models/app-refresh-token.ts"
59
- import { RefreshToken, refreshTokenSchema, type Auth, type RefreshTokenIssueOptions } from "@warlock.js/auth";
60
- import { v } from "@warlock.js/seal";
61
-
62
- export class AppRefreshToken extends RefreshToken {
63
- public static schema = refreshTokenSchema.extend({
64
- organization_id: v.string().exists("Organization", { column: "id" }),
65
- });
66
-
67
- public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {
68
- return this.create({
69
- token,
70
- user_id: user.id,
71
- user_type: user.userType,
72
- family_id: options.familyId,
73
- expires_at: options.expiresAt,
74
- organization_id: user.get("organization_id"),
75
- });
76
- }
77
- }
78
- ```
79
-
80
- ### 2. Register the subclass in config
81
-
82
- ```ts title="src/config/auth.ts"
83
- import { env } from "@warlock.js/core";
84
- import { User } from "app/users/models/user/user.model";
85
- import { AppAccessToken } from "app/auth/models/app-access-token";
86
- import { AppRefreshToken } from "app/auth/models/app-refresh-token";
87
-
88
- export default {
89
- userType: { user: User },
90
- accessToken: {
91
- model: AppAccessToken, // ← the override
92
- secret: env("JWT_SECRET"),
93
- expiresIn: "1h",
94
- },
95
- refreshToken: {
96
- model: AppRefreshToken, // ← the override
97
- secret: env("JWT_REFRESH_SECRET"),
98
- expiresIn: "30d",
99
- },
100
- };
101
- ```
102
-
103
- 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.
104
-
105
- ### 3. Add the column to the migration
106
-
107
- 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):
108
-
109
- ```ts
110
- this.uuid("organization_id").references("organizations").onDelete("cascade").index();
111
- ```
112
-
113
- See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration mechanics.
114
-
115
- ## Why all three — the strict-mode trap
116
-
117
- 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.
118
-
119
- ## You do NOT need read-side scoping
120
-
121
- 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.
122
-
123
- ## What you're overriding (the contract)
124
-
125
- Your subclass inherits and may override these statics — the service calls them, never raw queries:
126
-
127
- | Static | Role |
128
- | --- | --- |
129
- | `issue(user, token, …)` | persist a freshly-signed token (override to add columns) |
130
- | `findByToken(token)` | look a row up by its token string |
131
- | `findForUser(user, token)` | a user-scoped lookup (logout) |
132
- | `activeFor(user)` | active, unexpired sessions, newest-first |
133
- | `revokeAllFor(user)` / `revokeFamily(id)` | revoke a set, returning the revoked rows |
134
- | `enforceMax(user, max)` | cap concurrent refresh tokens |
135
- | `purgeExpired()` | delete expired rows (CLI cleanup) |
136
- | `findNeverExpiring()` / `purgeNeverExpiring()` | find/delete rows that can never expire (CLI remediation) |
137
-
138
- Plus two instance getters, both of which the route gate consults on every request:
139
-
140
- | Getter | Role |
141
- | --- | --- |
142
- | `isExpired` | whether the row's persisted expiry has passed — **the middleware rejects the request when this is `true`** |
143
- | `neverExpires` | whether nothing can ever retire the row (unusable `expires_at`, or a token with no `exp` claim) |
144
-
145
- If you rename a column, override the statics **and these getters** so they map to your name — the service depends on the method, not the column.
146
-
147
- ⚠️ **Do not drop `isExpired`.** The middleware treats a row that cannot answer as **expired** and rejects the request, so removing the getter locks every user out rather than letting them through. That direction is deliberate: the fail-open reading is what let expired sessions stay usable before 4.12.0.
148
-
149
- ## Things NOT to do
150
-
151
- - **Don't set a column without declaring it in `schema.extend(...)`.** `strictMode: "strip"` removes it; a NOT-NULL column then fails the INSERT.
152
- - **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.
153
- - **Don't fork the package to add a column.** Extend + register — you keep receiving package fixes.
154
- - **Don't forget the migration.** The schema declares the field; only the migration creates the database column.
155
-
156
- ## See also
157
-
158
- - [`@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*).
159
- - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — the token lifecycle your statics power.
160
- - [`@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
+ ```ts title="src/app/users/models/user/user.model.ts"
13
+ import { Auth } from "@warlock.js/auth";
14
+ import { RegisterModel } from "@warlock.js/cascade";
15
+
16
+ @RegisterModel()
17
+ export class User extends Auth {
18
+ public static table = "users";
19
+
20
+ public get userType(): string {
21
+ return "user";
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## The three pieces
27
+
28
+ A storage override is always three coordinated steps. Miss any one and it breaks — see the strict-mode note below.
29
+
30
+ ### 1. Extend the model + its schema
31
+
32
+ `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).
33
+
34
+ ```ts title="src/app/auth/models/app-access-token.ts"
35
+ import { AccessToken, accessTokenSchema, type Auth } from "@warlock.js/auth";
36
+ import { v } from "@warlock.js/seal";
37
+
38
+ export class AppAccessToken extends AccessToken {
39
+ public static schema = accessTokenSchema.extend({
40
+ organization_id: v.string().exists("Organization", { column: "id" }),
41
+ });
42
+
43
+ // populate the tenant key on issue — read it off the authenticating user
44
+ public static issue(user: Auth, token: string, expiresAt: Date) {
45
+ return this.create({
46
+ token,
47
+ user_id: user.id,
48
+ user_type: user.userType,
49
+ expires_at: expiresAt,
50
+ organization_id: user.get("organization_id"),
51
+ });
52
+ }
53
+ }
54
+ ```
55
+
56
+ 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.
57
+
58
+ ```ts title="src/app/auth/models/app-refresh-token.ts"
59
+ import { RefreshToken, refreshTokenSchema, type Auth, type RefreshTokenIssueOptions } from "@warlock.js/auth";
60
+ import { v } from "@warlock.js/seal";
61
+
62
+ export class AppRefreshToken extends RefreshToken {
63
+ public static schema = refreshTokenSchema.extend({
64
+ organization_id: v.string().exists("Organization", { column: "id" }),
65
+ });
66
+
67
+ public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {
68
+ return this.create({
69
+ token,
70
+ user_id: user.id,
71
+ user_type: user.userType,
72
+ family_id: options.familyId,
73
+ expires_at: options.expiresAt,
74
+ organization_id: user.get("organization_id"),
75
+ });
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### 2. Register the subclass in config
81
+
82
+ ```ts title="src/config/auth.ts"
83
+ import { env } from "@warlock.js/core";
84
+ import { User } from "app/users/models/user/user.model";
85
+ import { AppAccessToken } from "app/auth/models/app-access-token";
86
+ import { AppRefreshToken } from "app/auth/models/app-refresh-token";
87
+
88
+ export default {
89
+ userType: { user: User },
90
+ accessToken: {
91
+ model: AppAccessToken, // ← the override
92
+ secret: env("JWT_SECRET"),
93
+ expiresIn: "1h",
94
+ },
95
+ refreshToken: {
96
+ model: AppRefreshToken, // ← the override
97
+ secret: env("JWT_REFRESH_SECRET"),
98
+ expiresIn: "30d",
99
+ },
100
+ };
101
+ ```
102
+
103
+ 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.
104
+
105
+ ### 3. Add the column to the migration
106
+
107
+ 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):
108
+
109
+ ```ts
110
+ this.uuid("organization_id").references("organizations").onDelete("cascade").index();
111
+ ```
112
+
113
+ See [`@warlock.js/cascade/write-migration/SKILL.md`](@warlock.js/cascade/write-migration/SKILL.md) for the migration mechanics.
114
+
115
+ ## Why all three — the strict-mode trap
116
+
117
+ 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.
118
+
119
+ ## You do NOT need read-side scoping
120
+
121
+ 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.
122
+
123
+ ## What you're overriding (the contract)
124
+
125
+ Your subclass inherits and may override these statics — the service calls them, never raw queries:
126
+
127
+ | Static | Role |
128
+ | --- | --- |
129
+ | `issue(user, token, …)` | persist a freshly-signed token (override to add columns) |
130
+ | `findByToken(token)` | look a row up by its token string |
131
+ | `findForUser(user, token)` | a user-scoped lookup (logout) |
132
+ | `activeFor(user)` | active, unexpired sessions, newest-first |
133
+ | `revokeAllFor(user)` / `revokeFamily(id)` | revoke a set, returning the revoked rows |
134
+ | `enforceMax(user, max)` | cap concurrent refresh tokens |
135
+ | `purgeExpired()` | delete expired rows (CLI cleanup) |
136
+ | `findNeverExpiring()` / `purgeNeverExpiring()` | find/delete rows that can never expire (CLI remediation) |
137
+
138
+ Plus two instance getters, both of which the route gate consults on every request:
139
+
140
+ | Getter | Role |
141
+ | --- | --- |
142
+ | `isExpired` | whether the row's persisted expiry has passed — **the middleware rejects the request when this is `true`** |
143
+ | `neverExpires` | whether nothing can ever retire the row (unusable `expires_at`, or a token with no `exp` claim) |
144
+
145
+ If you rename a column, override the statics **and these getters** so they map to your name — the service depends on the method, not the column.
146
+
147
+ ⚠️ **Do not drop `isExpired`.** The middleware treats a row that cannot answer as **expired** and rejects the request, so removing the getter locks every user out rather than letting them through. That direction is deliberate: the fail-open reading is what let expired sessions stay usable before 4.12.0.
148
+
149
+ ## Things NOT to do
150
+
151
+ - **Don't set a column without declaring it in `schema.extend(...)`.** `strictMode: "strip"` removes it; a NOT-NULL column then fails the INSERT.
152
+ - **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.
153
+ - **Don't fork the package to add a column.** Extend + register — you keep receiving package fixes.
154
+ - **Don't forget the migration.** The schema declares the field; only the migration creates the database column.
155
+
156
+ ## See also
157
+
158
+ - [`@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*).
159
+ - [`@warlock.js/auth/manage-tokens/SKILL.md`](@warlock.js/auth/manage-tokens/SKILL.md) — the token lifecycle your statics power.
160
+ - [`@warlock.js/cascade/define-model/SKILL.md`](@warlock.js/cascade/define-model/SKILL.md) — extending models and schemas.