@warlock.js/auth 5.19.1 → 5.21.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 +25 -1
- package/esm/contracts/request-locals.d.mts +5 -0
- package/esm/contracts/request-locals.d.mts.map +1 -1
- package/esm/contracts/types.d.mts +13 -0
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +3 -1
- package/esm/index.mjs +6 -3
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +37 -114
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs +6 -2
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/services/auth-config.mjs +2 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +36 -1
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +67 -0
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/index.d.mts +2 -1
- package/esm/services/index.mjs +1 -0
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +4 -1
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/services/resolve-request-user.d.mts +49 -0
- package/esm/services/resolve-request-user.d.mts.map +1 -0
- package/esm/services/resolve-request-user.mjs +131 -0
- package/esm/services/resolve-request-user.mjs.map +1 -0
- package/esm/session/index.d.mts +1 -0
- package/esm/session/index.mjs +3 -0
- package/esm/session/page-session.d.mts +49 -0
- package/esm/session/page-session.d.mts.map +1 -0
- package/esm/session/page-session.mjs +119 -0
- package/esm/session/page-session.mjs.map +1 -0
- package/esm/utils/index.mjs +1 -1
- package/llms-full.txt +1565 -1564
- package/llms.txt +8 -8
- package/package.json +9 -9
- package/skills/customize-token-storage/SKILL.md +160 -160
- package/skills/customize-user-type/SKILL.md +168 -168
- package/skills/handle-login-and-logout/SKILL.md +183 -179
- package/skills/index.md +22 -0
- package/skills/login-with-providers/SKILL.md +283 -283
- package/skills/manage-tokens/SKILL.md +15 -15
- package/skills/protect-routes/SKILL.md +288 -288
- package/skills/register-user/SKILL.md +147 -147
- package/skills/run-auth-commands/SKILL.md +159 -159
- package/skills/throttle-login-attempts/SKILL.md +2 -1
- package/skills/verify-email-and-reset-password/SKILL.md +137 -137
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: verify-email-and-reset-password
|
|
3
|
-
description:
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Verify email + reset password
|
|
7
|
-
|
|
8
|
-
Four service functions, one guard, two throttle presets. Auth ships no controllers — wire the routes yourself (examples below).
|
|
9
|
-
|
|
10
|
-
## Prerequisites
|
|
11
|
-
|
|
12
|
-
1. **`@warlock.js/notifications` installed and configured with a `mail` channel.** It is an optional peer; without it (or without `src/config/notifications.ts`) every send/request call throws `NotificationsUnavailableError` **before** a token is issued — never a silent skip.
|
|
13
|
-
2. **The `one_time_tokens` table.** It ships in `authMigrations` next to the access/refresh tables; run your migrations.
|
|
14
|
-
3. **A verified-date field on the user schema** (default `emailVerifiedAt`):
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
export const userSchema = v.object({
|
|
18
|
-
// ...
|
|
19
|
-
emailVerifiedAt: v.date().optional(),
|
|
20
|
-
});
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
If the schema strips it, `verifyEmail` throws naming the field instead of pretending it worked.
|
|
24
|
-
|
|
25
|
-
## Tokens
|
|
26
|
-
|
|
27
|
-
- 32 random bytes, base64url. Only the **SHA-256 hash** is stored; the raw token exists only in the notification.
|
|
28
|
-
- **Single use**, consumed with a conditional update — of two concurrent uses, exactly one succeeds.
|
|
29
|
-
- **Expiring**: verification `24h`, reset `60m` by default.
|
|
30
|
-
- **Purpose-bound**: a verification token is never accepted by `resetPassword`, and vice versa.
|
|
31
|
-
- A new reset request **invalidates the user's earlier unused reset tokens**. Verification tokens are not invalidated by a resend.
|
|
32
|
-
- Unknown, wrong-purpose, expired and used tokens all throw one `InvalidOneTimeTokenError` (`400`, `EC008`).
|
|
33
|
-
|
|
34
|
-
## Email verification
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { requireVerifiedEmail, sendEmailVerification, verifyEmail } from "@warlock.js/auth";
|
|
38
|
-
import { type RequestHandler } from "@warlock.js/core";
|
|
39
|
-
|
|
40
|
-
// after User.create(...) in your register controller
|
|
41
|
-
await sendEmailVerification(user);
|
|
42
|
-
|
|
43
|
-
export const verifyEmailController: RequestHandler = async ({ request, response }) => {
|
|
44
|
-
const user = await verifyEmail(request.input("token")); // throws InvalidOneTimeTokenError
|
|
45
|
-
return response.success({ user });
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
router.post("/auth/verify-email", verifyEmailController, {
|
|
49
|
-
middleware: [tokenConsumeThrottleMiddleware()],
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
router.post("/auth/verify-email/resend", resendController, {
|
|
53
|
-
middleware: [authMiddleware("user"), tokenIssueThrottleMiddleware({ by: ["ip"] })],
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Opt-in guard, AFTER authMiddleware: 403 EmailNotVerifiedError (EC007)
|
|
57
|
-
router.post("/orders", createOrder, {
|
|
58
|
-
middleware: [authMiddleware("user"), requireVerifiedEmail()],
|
|
59
|
-
});
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
`isEmailVerified(user)` answers the same question in code.
|
|
63
|
-
|
|
64
|
-
## Password reset
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
import { requestPasswordReset, resetPassword } from "@warlock.js/auth";
|
|
68
|
-
|
|
69
|
-
export const forgotPasswordController: RequestHandler = async ({ request, response }) => {
|
|
70
|
-
await requestPasswordReset(User, request.input("email"));
|
|
71
|
-
// Same answer whether or not the account exists.
|
|
72
|
-
return response.success({ message: "If that account exists, we sent a reset link." });
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
export const resetPasswordController: RequestHandler = async ({ request, response }) => {
|
|
76
|
-
// Validate strength FIRST — the token is consumed before the password is written.
|
|
77
|
-
await resetPassword(request.input("token"), request.input("password"));
|
|
78
|
-
return response.success({ message: "Password updated. Please log in again." });
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
router.post("/auth/forgot-password", forgotPasswordController, {
|
|
82
|
-
middleware: [tokenIssueThrottleMiddleware()], // 3 / 1h per email + per IP
|
|
83
|
-
});
|
|
84
|
-
router.post("/auth/reset-password", resetPasswordController, {
|
|
85
|
-
middleware: [tokenConsumeThrottleMiddleware()], // 10 failures / 15m per IP
|
|
86
|
-
});
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
`resetPassword` writes the password, then calls `authService.revokeAllTokens(user)`: every refresh token is revoked and every access token deleted, which also kills cookie sessions (a cookie holds an access token). It emits `password.reset`; `requestPasswordReset` emits `password.resetRequested`.
|
|
90
|
-
|
|
91
|
-
**Password hashing.** By default auth saves `hashPassword(plain)` and checks it with `verifyPassword`, the same check login uses. If your model hashes on save (`useHashedPassword()`), that first write gets hashed twice and fails the check, so auth saves the plaintext and lets your transformer hash it once. If neither write passes the check, it throws. Plaintext is never the first thing written. To write it yourself, set `auth.passwordReset.setPassword`.
|
|
92
|
-
|
|
93
|
-
## Throttling
|
|
94
|
-
|
|
95
|
-
Both presets wrap `loginThrottleMiddleware` and accept its options:
|
|
96
|
-
|
|
97
|
-
| Preset | Tracks | Counts | Default |
|
|
98
|
-
| --- | --- | --- | --- |
|
|
99
|
-
| `tokenIssueThrottleMiddleware` | email + IP | **every** request (a reset request's success is the anti-enumeration answer, so failure-aware counting would never trip) | 3 / 1h, 1h lock |
|
|
100
|
-
| `tokenConsumeThrottleMiddleware` | IP | failures only, cleared on success | 10 / 15m, 15m lock |
|
|
101
|
-
|
|
102
|
-
## Configuration (`src/config/auth.ts`)
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
const authConfigurations: AuthConfigurations = {
|
|
106
|
-
// ...
|
|
107
|
-
verification: {
|
|
108
|
-
expiresIn: "24h",
|
|
109
|
-
field: "emailVerifiedAt",
|
|
110
|
-
url: (token) => `${env("APP_URL")}/verify-email?token=${token}`,
|
|
111
|
-
notification: myVerificationNotification, // optional replacement
|
|
112
|
-
},
|
|
113
|
-
passwordReset: {
|
|
114
|
-
expiresIn: "60m",
|
|
115
|
-
identifierField: "email",
|
|
116
|
-
url: (token) => `${env("APP_URL")}/reset-password?token=${token}`,
|
|
117
|
-
notification: myResetNotification,
|
|
118
|
-
setPassword: async (user, plain) => { /* optional */ },
|
|
119
|
-
},
|
|
120
|
-
oneTimeToken: { model: AppOneTimeToken }, // optional model override
|
|
121
|
-
};
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
The default notifications are `defineNotification` objects (`type: "auth.email-verification"` / `"auth.password-reset"`, `via: ["mail"]`). To replace one, pass anything with a `send(user, { token, expiresAt, url? })` method, for example your own `defineNotification<OneTimeTokenNotificationData>({...})`. `notifications` still has to be installed and configured.
|
|
125
|
-
|
|
126
|
-
## Gotchas
|
|
127
|
-
|
|
128
|
-
- **Without `url` config, the email contains the raw token**, not a link. Set `url` for anything user-facing.
|
|
129
|
-
- **Timing isn't equalised.** An unknown email skips the database write and the send, so the response can come back measurably faster. The throttle limits probing, but it doesn't hide the timing difference.
|
|
130
|
-
- **Channel send failures don't throw.** `@warlock.js/notifications` only rethrows configuration errors (such as a missing `mail` channel). An SMTP failure goes to its `failed` event, so observe it there.
|
|
131
|
-
- `auth.cleanup` hard-deletes expired and consumed `one_time_tokens` rows (never trashed, even on MongoDB).
|
|
132
|
-
|
|
133
|
-
## See also
|
|
134
|
-
|
|
135
|
-
- [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — where `sendEmailVerification` is usually called
|
|
136
|
-
- [`@warlock.js/auth/throttle-login-attempts/SKILL.md`](@warlock.js/auth/throttle-login-attempts/SKILL.md) — the underlying throttle
|
|
137
|
-
- [`@warlock.js/notifications/configure-notifications/SKILL.md`](@warlock.js/notifications/configure-notifications/SKILL.md) — mail channel setup
|
|
1
|
+
---
|
|
2
|
+
name: verify-email-and-reset-password
|
|
3
|
+
description: 'Email verification and password reset with hashed, single-use, expiring, purpose-bound tokens delivered through `@warlock.js/notifications` — `sendEmailVerification(user)`, `verifyEmail(token)`, `requestPasswordReset(Model, email)`, `resetPassword(token, newPassword)`, the `requireVerifiedEmail()` guard, and the `tokenIssueThrottleMiddleware` / `tokenConsumeThrottleMiddleware` presets. Reset revokes every access token, refresh token and cookie session; a reset request answers the same for unknown emails. Triggers: `sendEmailVerification`, `verifyEmail`, `requestPasswordReset`, `resetPassword`, `requireVerifiedEmail`, `isEmailVerified`, `OneTimeToken`, `InvalidOneTimeTokenError`, `EmailNotVerifiedError`, `NotificationsUnavailableError`, `EC007`, `EC008`, `auth.verification`, `auth.passwordReset`; "verify email", "confirm email address", "forgot password", "reset password link", "block unverified users", "resend verification email"; typical import `import { requestPasswordReset, resetPassword } from "@warlock.js/auth"`. Skip: sign-up itself — `@warlock.js/auth/register-user/SKILL.md`; login throttling — `@warlock.js/auth/throttle-login-attempts/SKILL.md`; defining notifications — `@warlock.js/notifications/define-notification/SKILL.md`.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify email + reset password
|
|
7
|
+
|
|
8
|
+
Four service functions, one guard, two throttle presets. Auth ships no controllers — wire the routes yourself (examples below).
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
1. **`@warlock.js/notifications` installed and configured with a `mail` channel.** It is an optional peer; without it (or without `src/config/notifications.ts`) every send/request call throws `NotificationsUnavailableError` **before** a token is issued — never a silent skip.
|
|
13
|
+
2. **The `one_time_tokens` table.** It ships in `authMigrations` next to the access/refresh tables; run your migrations.
|
|
14
|
+
3. **A verified-date field on the user schema** (default `emailVerifiedAt`):
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
export const userSchema = v.object({
|
|
18
|
+
// ...
|
|
19
|
+
emailVerifiedAt: v.date().optional(),
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If the schema strips it, `verifyEmail` throws naming the field instead of pretending it worked.
|
|
24
|
+
|
|
25
|
+
## Tokens
|
|
26
|
+
|
|
27
|
+
- 32 random bytes, base64url. Only the **SHA-256 hash** is stored; the raw token exists only in the notification.
|
|
28
|
+
- **Single use**, consumed with a conditional update — of two concurrent uses, exactly one succeeds.
|
|
29
|
+
- **Expiring**: verification `24h`, reset `60m` by default.
|
|
30
|
+
- **Purpose-bound**: a verification token is never accepted by `resetPassword`, and vice versa.
|
|
31
|
+
- A new reset request **invalidates the user's earlier unused reset tokens**. Verification tokens are not invalidated by a resend.
|
|
32
|
+
- Unknown, wrong-purpose, expired and used tokens all throw one `InvalidOneTimeTokenError` (`400`, `EC008`).
|
|
33
|
+
|
|
34
|
+
## Email verification
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { requireVerifiedEmail, sendEmailVerification, verifyEmail } from "@warlock.js/auth";
|
|
38
|
+
import { type RequestHandler } from "@warlock.js/core";
|
|
39
|
+
|
|
40
|
+
// after User.create(...) in your register controller
|
|
41
|
+
await sendEmailVerification(user);
|
|
42
|
+
|
|
43
|
+
export const verifyEmailController: RequestHandler = async ({ request, response }) => {
|
|
44
|
+
const user = await verifyEmail(request.input("token")); // throws InvalidOneTimeTokenError
|
|
45
|
+
return response.success({ user });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
router.post("/auth/verify-email", verifyEmailController, {
|
|
49
|
+
middleware: [tokenConsumeThrottleMiddleware()],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.post("/auth/verify-email/resend", resendController, {
|
|
53
|
+
middleware: [authMiddleware("user"), tokenIssueThrottleMiddleware({ by: ["ip"] })],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Opt-in guard, AFTER authMiddleware: 403 EmailNotVerifiedError (EC007)
|
|
57
|
+
router.post("/orders", createOrder, {
|
|
58
|
+
middleware: [authMiddleware("user"), requireVerifiedEmail()],
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`isEmailVerified(user)` answers the same question in code.
|
|
63
|
+
|
|
64
|
+
## Password reset
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { requestPasswordReset, resetPassword } from "@warlock.js/auth";
|
|
68
|
+
|
|
69
|
+
export const forgotPasswordController: RequestHandler = async ({ request, response }) => {
|
|
70
|
+
await requestPasswordReset(User, request.input("email"));
|
|
71
|
+
// Same answer whether or not the account exists.
|
|
72
|
+
return response.success({ message: "If that account exists, we sent a reset link." });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const resetPasswordController: RequestHandler = async ({ request, response }) => {
|
|
76
|
+
// Validate strength FIRST — the token is consumed before the password is written.
|
|
77
|
+
await resetPassword(request.input("token"), request.input("password"));
|
|
78
|
+
return response.success({ message: "Password updated. Please log in again." });
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
router.post("/auth/forgot-password", forgotPasswordController, {
|
|
82
|
+
middleware: [tokenIssueThrottleMiddleware()], // 3 / 1h per email + per IP
|
|
83
|
+
});
|
|
84
|
+
router.post("/auth/reset-password", resetPasswordController, {
|
|
85
|
+
middleware: [tokenConsumeThrottleMiddleware()], // 10 failures / 15m per IP
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`resetPassword` writes the password, then calls `authService.revokeAllTokens(user)`: every refresh token is revoked and every access token deleted, which also kills cookie sessions (a cookie holds an access token). It emits `password.reset`; `requestPasswordReset` emits `password.resetRequested`.
|
|
90
|
+
|
|
91
|
+
**Password hashing.** By default auth saves `hashPassword(plain)` and checks it with `verifyPassword`, the same check login uses. If your model hashes on save (`useHashedPassword()`), that first write gets hashed twice and fails the check, so auth saves the plaintext and lets your transformer hash it once. If neither write passes the check, it throws. Plaintext is never the first thing written. To write it yourself, set `auth.passwordReset.setPassword`.
|
|
92
|
+
|
|
93
|
+
## Throttling
|
|
94
|
+
|
|
95
|
+
Both presets wrap `loginThrottleMiddleware` and accept its options:
|
|
96
|
+
|
|
97
|
+
| Preset | Tracks | Counts | Default |
|
|
98
|
+
| --- | --- | --- | --- |
|
|
99
|
+
| `tokenIssueThrottleMiddleware` | email + IP | **every** request (a reset request's success is the anti-enumeration answer, so failure-aware counting would never trip) | 3 / 1h, 1h lock |
|
|
100
|
+
| `tokenConsumeThrottleMiddleware` | IP | failures only, cleared on success | 10 / 15m, 15m lock |
|
|
101
|
+
|
|
102
|
+
## Configuration (`src/config/auth.ts`)
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
const authConfigurations: AuthConfigurations = {
|
|
106
|
+
// ...
|
|
107
|
+
verification: {
|
|
108
|
+
expiresIn: "24h",
|
|
109
|
+
field: "emailVerifiedAt",
|
|
110
|
+
url: (token) => `${env("APP_URL")}/verify-email?token=${token}`,
|
|
111
|
+
notification: myVerificationNotification, // optional replacement
|
|
112
|
+
},
|
|
113
|
+
passwordReset: {
|
|
114
|
+
expiresIn: "60m",
|
|
115
|
+
identifierField: "email",
|
|
116
|
+
url: (token) => `${env("APP_URL")}/reset-password?token=${token}`,
|
|
117
|
+
notification: myResetNotification,
|
|
118
|
+
setPassword: async (user, plain) => { /* optional */ },
|
|
119
|
+
},
|
|
120
|
+
oneTimeToken: { model: AppOneTimeToken }, // optional model override
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The default notifications are `defineNotification` objects (`type: "auth.email-verification"` / `"auth.password-reset"`, `via: ["mail"]`). To replace one, pass anything with a `send(user, { token, expiresAt, url? })` method, for example your own `defineNotification<OneTimeTokenNotificationData>({...})`. `notifications` still has to be installed and configured.
|
|
125
|
+
|
|
126
|
+
## Gotchas
|
|
127
|
+
|
|
128
|
+
- **Without `url` config, the email contains the raw token**, not a link. Set `url` for anything user-facing.
|
|
129
|
+
- **Timing isn't equalised.** An unknown email skips the database write and the send, so the response can come back measurably faster. The throttle limits probing, but it doesn't hide the timing difference.
|
|
130
|
+
- **Channel send failures don't throw.** `@warlock.js/notifications` only rethrows configuration errors (such as a missing `mail` channel). An SMTP failure goes to its `failed` event, so observe it there.
|
|
131
|
+
- `auth.cleanup` hard-deletes expired and consumed `one_time_tokens` rows (never trashed, even on MongoDB).
|
|
132
|
+
|
|
133
|
+
## See also
|
|
134
|
+
|
|
135
|
+
- [`@warlock.js/auth/register-user/SKILL.md`](@warlock.js/auth/register-user/SKILL.md) — where `sendEmailVerification` is usually called
|
|
136
|
+
- [`@warlock.js/auth/throttle-login-attempts/SKILL.md`](@warlock.js/auth/throttle-login-attempts/SKILL.md) — the underlying throttle
|
|
137
|
+
- [`@warlock.js/notifications/configure-notifications/SKILL.md`](@warlock.js/notifications/configure-notifications/SKILL.md) — mail channel setup
|