@warlock.js/auth 5.11.0 → 5.13.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 +65 -0
- package/README.md +88 -86
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/contracts/index.d.mts +4 -2
- package/esm/contracts/providers.d.mts +101 -0
- package/esm/contracts/providers.d.mts.map +1 -0
- package/esm/contracts/request-locals.d.mts +43 -0
- package/esm/contracts/request-locals.d.mts.map +1 -0
- package/esm/contracts/types.d.mts +159 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
- package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
- package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
- package/esm/errors/email-not-verified.error.d.mts +16 -0
- package/esm/errors/email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/email-not-verified.error.mjs +20 -0
- package/esm/errors/email-not-verified.error.mjs.map +1 -0
- package/esm/errors/index.d.mts +7 -0
- package/esm/errors/index.mjs +9 -0
- package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
- package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
- package/esm/errors/invalid-one-time-token.error.mjs +21 -0
- package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
- package/esm/errors/invalid-passkey.error.d.mts +18 -0
- package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
- package/esm/errors/invalid-passkey.error.mjs +23 -0
- package/esm/errors/invalid-passkey.error.mjs.map +1 -0
- package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
- package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
- package/esm/errors/invalid-provider-callback.error.mjs +23 -0
- package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
- package/esm/errors/notifications-unavailable.error.d.mts +15 -0
- package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
- package/esm/errors/notifications-unavailable.error.mjs +20 -0
- package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
- package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
- package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
- package/esm/errors/provider-email-not-verified.error.mjs +20 -0
- package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
- package/esm/index.d.mts +31 -3
- package/esm/index.mjs +29 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +13 -2
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/csrf-origin-check.d.mts +40 -0
- package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
- package/esm/middleware/csrf-origin-check.mjs +105 -0
- package/esm/middleware/csrf-origin-check.mjs.map +1 -0
- package/esm/middleware/index.d.mts +5 -1
- package/esm/middleware/index.mjs +4 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
- package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
- package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
- package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/otp-throttle.middleware.mjs +45 -0
- package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
- package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
- package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
- package/esm/middleware/require-verified-email.middleware.mjs +24 -0
- package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
- package/esm/models/access-token/access-token.model.d.mts +8 -0
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +3 -0
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/index.d.mts +3 -1
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +16 -1
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/one-time-token/index.d.mts +1 -0
- package/esm/models/one-time-token/index.mjs +3 -0
- package/esm/models/one-time-token/migration.mjs +31 -0
- package/esm/models/one-time-token/migration.mjs.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
- package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
- package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
- package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
- package/esm/models/passkey-credential/index.d.mts +1 -0
- package/esm/models/passkey-credential/index.mjs +3 -0
- package/esm/models/passkey-credential/migration.mjs +25 -0
- package/esm/models/passkey-credential/migration.mjs.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
- package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
- package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
- package/esm/models/provider-account/index.d.mts +1 -0
- package/esm/models/provider-account/index.mjs +3 -0
- package/esm/models/provider-account/migration.mjs +25 -0
- package/esm/models/provider-account/migration.mjs.map +1 -0
- package/esm/models/provider-account/provider-account.model.d.mts +75 -0
- package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
- package/esm/models/provider-account/provider-account.model.mjs +55 -0
- package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/otp/index.d.mts +1 -0
- package/esm/otp/index.mjs +3 -0
- package/esm/otp/otp.d.mts +38 -0
- package/esm/otp/otp.d.mts.map +1 -0
- package/esm/otp/otp.mjs +105 -0
- package/esm/otp/otp.mjs.map +1 -0
- package/esm/passkeys/index.d.mts +3 -0
- package/esm/passkeys/index.mjs +4 -0
- package/esm/passkeys/passkey-authentication.d.mts +33 -0
- package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
- package/esm/passkeys/passkey-authentication.mjs +93 -0
- package/esm/passkeys/passkey-authentication.mjs.map +1 -0
- package/esm/passkeys/passkey-challenges.mjs +53 -0
- package/esm/passkeys/passkey-challenges.mjs.map +1 -0
- package/esm/passkeys/passkey-registration.d.mts +29 -0
- package/esm/passkeys/passkey-registration.d.mts.map +1 -0
- package/esm/passkeys/passkey-registration.mjs +88 -0
- package/esm/passkeys/passkey-registration.mjs.map +1 -0
- package/esm/passkeys/simplewebauthn.d.mts +21 -0
- package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
- package/esm/passkeys/simplewebauthn.mjs +13 -0
- package/esm/passkeys/simplewebauthn.mjs.map +1 -0
- package/esm/providers/google-provider.d.mts +40 -0
- package/esm/providers/google-provider.d.mts.map +1 -0
- package/esm/providers/google-provider.mjs +105 -0
- package/esm/providers/google-provider.mjs.map +1 -0
- package/esm/providers/index.d.mts +2 -0
- package/esm/providers/index.mjs +4 -0
- package/esm/providers/provider-login.d.mts +43 -0
- package/esm/providers/provider-login.d.mts.map +1 -0
- package/esm/providers/provider-login.mjs +86 -0
- package/esm/providers/provider-login.mjs.map +1 -0
- package/esm/providers/provider-state-cookie.mjs +63 -0
- package/esm/providers/provider-state-cookie.mjs.map +1 -0
- package/esm/providers/resolve-provider-user.mjs +45 -0
- package/esm/providers/resolve-provider-user.mjs.map +1 -0
- package/esm/services/auth-config.mjs +84 -0
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +59 -2
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +86 -1
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/current-user.d.mts +21 -0
- package/esm/services/current-user.d.mts.map +1 -0
- package/esm/services/current-user.mjs +24 -0
- package/esm/services/current-user.mjs.map +1 -0
- package/esm/services/email-verification.d.mts +35 -0
- package/esm/services/email-verification.d.mts.map +1 -0
- package/esm/services/email-verification.mjs +55 -0
- package/esm/services/email-verification.mjs.map +1 -0
- package/esm/services/index.d.mts +5 -1
- package/esm/services/index.mjs +4 -0
- package/esm/services/one-time-tokens.d.mts +32 -0
- package/esm/services/one-time-tokens.d.mts.map +1 -0
- package/esm/services/one-time-tokens.mjs +56 -0
- package/esm/services/one-time-tokens.mjs.map +1 -0
- package/esm/services/optional-peer.mjs +37 -0
- package/esm/services/optional-peer.mjs.map +1 -0
- package/esm/services/password-reset.d.mts +34 -0
- package/esm/services/password-reset.d.mts.map +1 -0
- package/esm/services/password-reset.mjs +81 -0
- package/esm/services/password-reset.mjs.map +1 -0
- package/esm/services/token-notifications.mjs +87 -0
- package/esm/services/token-notifications.mjs.map +1 -0
- package/esm/utils/auth-error-codes.d.mts +48 -1
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +41 -0
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/esm/utils/index.mjs +1 -0
- package/esm/utils/safe-equal.d.mts +6 -0
- package/esm/utils/safe-equal.d.mts.map +1 -0
- package/esm/utils/safe-equal.mjs +13 -0
- package/esm/utils/safe-equal.mjs.map +1 -0
- package/llms-full.txt +402 -29
- package/llms.txt +2 -0
- package/package.json +21 -7
- package/skills/handle-login-and-logout/SKILL.md +1 -1
- package/skills/login-with-providers/SKILL.md +154 -0
- package/skills/manage-tokens/SKILL.md +7 -1
- package/skills/overview/SKILL.md +1 -1
- package/skills/protect-routes/SKILL.md +71 -13
- package/skills/register-user/SKILL.md +6 -8
- package/skills/run-auth-commands/SKILL.md +1 -1
- package/skills/verify-email-and-reset-password/SKILL.md +137 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,71 @@ 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
|
+
## 5.13.0 - 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Email verification and password reset.** `sendEmailVerification(user)`, `verifyEmail(token)`, `requestPasswordReset(Model, email)` and `resetPassword(token, newPassword)`. Tokens are 32 random bytes, stored only as a SHA-256 hash, single-use (consumed with a conditional update, so of two concurrent uses exactly one succeeds), expiring (`auth.verification.expiresIn` default `"24h"`, `auth.passwordReset.expiresIn` default `"60m"`) and purpose-bound. A new reset request invalidates the user's earlier unused reset tokens. `requestPasswordReset` answers the same for unknown emails. `resetPassword` revokes every access token, refresh token and cookie session through `authService.revokeAllTokens`. Rejected tokens throw `InvalidOneTimeTokenError` (`400`, `EC008`).
|
|
12
|
+
- New `one_time_tokens` table (`OneTimeToken` model, overridable via `auth.oneTimeToken.model`), shipped in `authMigrations`. **Run your migrations.**
|
|
13
|
+
- Delivery goes through `@warlock.js/notifications`, now an **optional peer dependency**. Default mail notifications can be replaced via `auth.verification.notification` / `auth.passwordReset.notification`, and link builders set via `.url`. If the package is not installed or not configured, calls throw `NotificationsUnavailableError` before any token is issued.
|
|
14
|
+
- `requireVerifiedEmail()` middleware — rejects users without `auth.verification.field` (default `emailVerifiedAt`) with `EmailNotVerifiedError` (`403`, `EC007`). `isEmailVerified(user)` helper.
|
|
15
|
+
- `tokenIssueThrottleMiddleware()` (every request counts, per email + IP, 3/1h) and `tokenConsumeThrottleMiddleware()` (failures, per IP, 10/15m), both built on `loginThrottleMiddleware`.
|
|
16
|
+
- `AuthErrorCodes.EmailNotVerified` (`"EC007"`) and `AuthErrorCodes.InvalidOneTimeToken` (`"EC008"`).
|
|
17
|
+
- **Login with Google, passkeys and phone codes** (skill `login-with-providers`). Every method ends in the new `authService.completeLogin(user, deviceInfo?)`, which applies `auth.canAuthenticate` and then issues exactly what `authService.login` issues (same tokens, rows and `login.success` event). `login` now shares that ending.
|
|
18
|
+
- **Google:** `startProviderLogin(response, "google")` / `completeProviderLogin(Model, "google", request, response)`. Uses the OIDC authorization code flow with PKCE (S256), `state` and `nonce`, kept in a signed 10-minute HttpOnly cookie. The code is exchanged with `fetch`, and the id_token is verified with `jose` against Google's JWKS (signature, `iss`, `aud`, `exp`, `nonce`). Configure it under `auth.providers.google`. Other providers implement the `AuthProvider` contract and register under `auth.providers.custom`.
|
|
19
|
+
- **Account linking:** new `provider_accounts` table (`ProviderAccount`). A link always decides the user. Without one, only a provider-**verified** email links an existing user or creates a new one; an unverified email throws `ProviderEmailNotVerifiedError` (`403`, `EC010`). Callback rejections throw `InvalidProviderCallbackError` (`400`, `EC009`).
|
|
20
|
+
- **Passkeys:** `generatePasskeyRegistrationOptions` / `verifyPasskeyRegistration` / `generatePasskeyAuthenticationOptions` / `verifyPasskeyAuthentication`, built on `@simplewebauthn/server`. Challenges are stored hashed in `one_time_tokens`, expire, and are used once (consumed even when verification fails). The request `Origin` must be in `auth.passkeys.origin`, and a signature counter that does not advance is rejected. Credentials are stored in the new `passkey_credentials` table (`PasskeyCredential`). Rejections throw `InvalidPasskeyError` (`400`, `EC011`).
|
|
21
|
+
- **Phone OTP:** `requestOtp(Model, phone, { channel })` / `verifyOtp(Model, phone, code)`. Codes have 6 digits, are stored as a salted HMAC in `one_time_tokens` (purpose `otp`), expire after 5 minutes, and are invalidated after 5 attempts (counted atomically). Unknown phones get the same answer. Codes are delivered through the `@warlock.js/notifications` channel you configure (`auth.otp.channel`, default `"sms"`) or `auth.otp.send`; auth ships no SMS/WhatsApp driver. New `otpRequestThrottleMiddleware()` / `otpVerifyThrottleMiddleware()` presets are built on `loginThrottleMiddleware`.
|
|
22
|
+
- `jose` and `@simplewebauthn/server` are new **optional peer dependencies**, loaded lazily. When one is missing, the call throws `AuthProviderSdkMissingError`, whose message names `warlock add auth-google` / `warlock add auth-passkeys`.
|
|
23
|
+
- `one_time_tokens` gains an `attempts` column, and `user_id` becomes nullable (passkey login challenges have no user yet). `authMigrations` now includes `ProviderAccountMigration` and `PasskeyCredentialMigration`. **Run your migrations.**
|
|
24
|
+
- `auth.cleanup` / `authService.cleanupExpiredTokens()` also hard-deletes expired and consumed `one_time_tokens` rows via the new `OneTimeToken.purgeSpent()`. The delete is permanent on every driver, so MongoDB's default "trash" strategy does not copy token hashes into `one_time_tokensTrash`.
|
|
25
|
+
- Local real-database integration suites (`tests/integration/local`, skipped unless `LOCAL_MONGO_*` / `LOCAL_PG_*` are set) prove on MongoDB and Postgres: a one-time token consumed 20 times concurrently succeeds once, 20 concurrent wrong OTP guesses count at most 5 attempts and invalidate the code, a passkey login challenge with no user persists and is consumed, the counter compare-and-set and regression rejection hold, and `provider_accounts` allows one link per identity.
|
|
26
|
+
- Passkey specs now also run the real `@simplewebauthn/server` against a `node:crypto` software authenticator (`none` attestation, ES256 assertions).
|
|
27
|
+
- `AuthErrorCodes.InvalidProviderCallback` (`"EC009"`), `AuthErrorCodes.ProviderEmailNotVerified` (`"EC010"`) and `AuthErrorCodes.InvalidPasskey` (`"EC011"`).
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- **Access and refresh tokens (and one-time tokens, and passkey credentials) are no longer copied into MongoDB trash collections on delete.** `AccessToken`, `RefreshToken`, `OneTimeToken` and `PasskeyCredential` now declare `static deleteStrategy = "permanent"`, overriding the MongoDB driver's default `"trash"` strategy. Previously, `AccessToken.purgeExpired()`, `RefreshToken.purgeExpired()`/`purgeNeverExpiring()`, and `authMiddleware`'s expired-token cleanup all called `destroy()` without a strategy override, so on MongoDB every purge or invalidation copied the live JWT into `access_tokensTrash` / `refresh_tokensTrash` before deleting the original — leaving usable credential material behind after "revocation". `OneTimeToken.purgeSpent()` already forced `"permanent"` per-call (5.13.0 above); that override is now redundant and has been removed in favor of the model-level default. Apps that have been running on MongoDB may want to drop the `access_tokensTrash`, `refresh_tokensTrash` and `one_time_tokensTrash` collections, which may hold copies of now-revoked credentials.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- `verifyPasskeyAuthentication` reported a cloned authenticator as `reason: "authentication-verification-failed"`, because `@simplewebauthn/server` rejects a non-advancing counter itself before auth's own check runs. It now reports `"counter-regression"`. The response (`400`, `EC011`) is unchanged.
|
|
36
|
+
|
|
37
|
+
## 5.12.0 - 2026-09-16
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- `authService.setAuthCookie(response, token, options?)` and `authService.clearAuthCookie(response, options?)` — the write side of the `cookie:<name>` token source `authMiddleware([], "cookie:<name>")` has accepted since 5.0.0 (card 50bf4f1a). `setAuthCookie` accepts a raw token string or an `AccessTokenOutput` (deriving `Max-Age` from `expiresAt` automatically); both are explicit app-controller calls — `login`/`logout` never set cookies implicitly, so upgrading never starts emitting `Set-Cookie` for an existing bearer-only app. Cookie name/path are configurable via the new `auth.cookie` config block (defaults: `"access_token"` / `"/"`); attribute flags (`HttpOnly`, `SameSite=Lax`, `Secure` outside dev) are not — they come from core's `secureCookieDefaults()`, the framework-wide floor.
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
|
|
45
|
+
- **CSRF Origin check for cookie-authenticated writes.** `authMiddleware` now automatically rejects, with `403` (`AuthErrorCodes.CsrfOriginMismatch`, `"EC006"`), any request whose credential came from a `cookie:` source and whose method is unsafe (`POST`/`PUT`/`PATCH`/`DELETE`) unless `Origin` (or, absent that, `Referer`) names the request's own origin or an entry in the new `auth.csrf.allowedOrigins` config (default `[]`). A request with neither header is rejected, fail-closed. Header-token authentication and safe methods (`GET`/`HEAD`/`OPTIONS`) are completely unaffected. This closes the residual CSRF gap `SameSite=Lax` alone leaves open for cookie auth (a same-site GET redirect chain, or a client that ignores `SameSite`); a double-submit token mechanism is deferred to a later release.
|
|
46
|
+
- The CSRF Origin check's own-origin comparison includes the request's port (read from the `Host` header, since core's `request.hostname` never carries one), with default ports (`:80` on `http`, `:443` on `https`) normalised as equivalent to no port. A same-origin cookie-authenticated write on a non-default port — e.g. every `warlock dev` session — is now correctly allowed instead of being rejected with `403 EC006`.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **BREAKING:** the authenticated user now lives at `request.locals.user`, declared by `@warlock.js/auth`. `request.user` is removed (in development it throws with the new location). `@warlock.js/auth`'s middleware writes `request.locals.user` after a successful token resolution and clears it (`= undefined`) on a forged, malformed, expired, or wrong-type token; `RequestUser` — the augmentable, empty-by-default interface apps narrow to their own model — moved from `@warlock.js/core` into `@warlock.js/auth` alongside it.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Before (removed)
|
|
54
|
+
declare module "@warlock.js/core" {
|
|
55
|
+
interface RequestUser {
|
|
56
|
+
id: string | number;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const user = request.user;
|
|
60
|
+
|
|
61
|
+
// After
|
|
62
|
+
declare module "@warlock.js/auth" {
|
|
63
|
+
interface RequestUser {
|
|
64
|
+
id: string | number;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const user = request.locals.user;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A typed `currentUser<UserType extends Auth>()` helper is also exported, wrapping core's `useCurrentUser()` for callers that want `Auth`-derived typing without repeating the assertion at every call site.
|
|
71
|
+
|
|
7
72
|
## 5.11.0 - 2026-09-14
|
|
8
73
|
|
|
9
74
|
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.11.0._
|
package/README.md
CHANGED
|
@@ -1,86 +1,88 @@
|
|
|
1
|
-
# Warlock Auth
|
|
2
|
-
|
|
3
|
-
JWT authentication for [Warlock.js](https://github.com/warlockjs/core) applications — a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` for login / logout / refresh (with refresh-token rotation + replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, brute-force throttling, and three CLI commands.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
yarn add @warlock.js/auth
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
`@warlock.js/auth` is coupled to `@warlock.js/core` — install it inside a Warlock project.
|
|
12
|
-
|
|
13
|
-
## Server-only
|
|
14
|
-
|
|
15
|
-
`@warlock.js/auth`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build treats this as a boundary: app client code cannot value-import this package (type-only imports are still allowed), while server loaders/controllers/modules can import it freely.
|
|
16
|
-
|
|
17
|
-
## Configure
|
|
18
|
-
|
|
19
|
-
```ts title="src/config/auth.ts"
|
|
20
|
-
import { type AuthConfigurations } from "@warlock.js/auth";
|
|
21
|
-
import { env } from "@warlock.js/core";
|
|
22
|
-
import { User } from "app/users/models/user";
|
|
23
|
-
|
|
24
|
-
const authConfigurations: AuthConfigurations = {
|
|
25
|
-
userType: { user: User },
|
|
26
|
-
accessToken: {
|
|
27
|
-
secret: env("JWT_SECRET"),
|
|
28
|
-
expiresIn: "1h",
|
|
29
|
-
},
|
|
30
|
-
refreshToken: {
|
|
31
|
-
secret: env("JWT_REFRESH_SECRET"),
|
|
32
|
-
expiresIn: "30d",
|
|
33
|
-
rotation: true,
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export default authConfigurations;
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
The legacy `jwt: { secret, expiresIn, refresh: {…} }` block is still honored (with a deprecation warning), but prefer the `accessToken` / `refreshToken` blocks.
|
|
41
|
-
|
|
42
|
-
`expiresIn` must be a duration string [`ms`](https://github.com/vercel/ms) parses to a positive value (`"1h"`, `"30 days"`, `NO_EXPIRATION`); anything else throws naming the key on the first token issue, rather than signing a token with no expiry.
|
|
43
|
-
|
|
44
|
-
A token is only accepted while **both** its own `exp` claim and its stored `expires_at` are in the future — a JWT with no `exp` at all is rejected outright, since a verifier with no deadline to check succeeds forever. If any deployment ran a pre-4.12.0 version with an invalid `expiresIn`, tokens issued then may never expire and are still live: run `warlock auth.purge-never-expiring --dry-run` to find them. See the 4.12.0 entry in [`CHANGELOG.md`](./CHANGELOG.md).
|
|
45
|
-
|
|
46
|
-
## Documentation
|
|
47
|
-
|
|
48
|
-
Task-focused guides live under [`skills/`](./skills):
|
|
49
|
-
|
|
50
|
-
- **overview** — what the package does and when to reach for it
|
|
51
|
-
- **auth-basics** — the `Auth` model, middleware, and service
|
|
52
|
-
- **handle-login-and-logout** — issue and revoke tokens
|
|
53
|
-
- **register-user** — sign up + first token pair
|
|
54
|
-
- **protect-routes** — gate routes with `authMiddleware`
|
|
55
|
-
- **manage-tokens** — the full token lifecycle (rotation, revocation, cleanup)
|
|
56
|
-
- **customize-user-type** — multiple user types in one system
|
|
57
|
-
- **customize-token-storage** — override the token models (multi-tenant columns, custom storage)
|
|
58
|
-
- **throttle-login-attempts** — brute-force protection via `loginThrottleMiddleware`
|
|
59
|
-
- **
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
1
|
+
# Warlock Auth
|
|
2
|
+
|
|
3
|
+
JWT authentication for [Warlock.js](https://github.com/warlockjs/core) applications — a base `Auth` model your user types extend, an `authMiddleware` route gate, an `authService` for login / logout / refresh (with refresh-token rotation + replay detection), persisted access + refresh tokens, multi-user-type support, lifecycle events, brute-force throttling, and three CLI commands.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
yarn add @warlock.js/auth
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@warlock.js/auth` is coupled to `@warlock.js/core` — install it inside a Warlock project.
|
|
12
|
+
|
|
13
|
+
## Server-only
|
|
14
|
+
|
|
15
|
+
`@warlock.js/auth`'s entire runtime surface is server-only — its `package.json` declares `"warlock": { "environment": "server" }`. `@warlock.js/web`'s build treats this as a boundary: app client code cannot value-import this package (type-only imports are still allowed), while server loaders/controllers/modules can import it freely.
|
|
16
|
+
|
|
17
|
+
## Configure
|
|
18
|
+
|
|
19
|
+
```ts title="src/config/auth.ts"
|
|
20
|
+
import { type AuthConfigurations } from "@warlock.js/auth";
|
|
21
|
+
import { env } from "@warlock.js/core";
|
|
22
|
+
import { User } from "app/users/models/user";
|
|
23
|
+
|
|
24
|
+
const authConfigurations: AuthConfigurations = {
|
|
25
|
+
userType: { user: User },
|
|
26
|
+
accessToken: {
|
|
27
|
+
secret: env("JWT_SECRET"),
|
|
28
|
+
expiresIn: "1h",
|
|
29
|
+
},
|
|
30
|
+
refreshToken: {
|
|
31
|
+
secret: env("JWT_REFRESH_SECRET"),
|
|
32
|
+
expiresIn: "30d",
|
|
33
|
+
rotation: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default authConfigurations;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The legacy `jwt: { secret, expiresIn, refresh: {…} }` block is still honored (with a deprecation warning), but prefer the `accessToken` / `refreshToken` blocks.
|
|
41
|
+
|
|
42
|
+
`expiresIn` must be a duration string [`ms`](https://github.com/vercel/ms) parses to a positive value (`"1h"`, `"30 days"`, `NO_EXPIRATION`); anything else throws naming the key on the first token issue, rather than signing a token with no expiry.
|
|
43
|
+
|
|
44
|
+
A token is only accepted while **both** its own `exp` claim and its stored `expires_at` are in the future — a JWT with no `exp` at all is rejected outright, since a verifier with no deadline to check succeeds forever. If any deployment ran a pre-4.12.0 version with an invalid `expiresIn`, tokens issued then may never expire and are still live: run `warlock auth.purge-never-expiring --dry-run` to find them. See the 4.12.0 entry in [`CHANGELOG.md`](./CHANGELOG.md).
|
|
45
|
+
|
|
46
|
+
## Documentation
|
|
47
|
+
|
|
48
|
+
Task-focused guides live under [`skills/`](./skills):
|
|
49
|
+
|
|
50
|
+
- **overview** — what the package does and when to reach for it
|
|
51
|
+
- **auth-basics** — the `Auth` model, middleware, and service
|
|
52
|
+
- **handle-login-and-logout** — issue and revoke tokens
|
|
53
|
+
- **register-user** — sign up + first token pair
|
|
54
|
+
- **protect-routes** — gate routes with `authMiddleware`
|
|
55
|
+
- **manage-tokens** — the full token lifecycle (rotation, revocation, cleanup)
|
|
56
|
+
- **customize-user-type** — multiple user types in one system
|
|
57
|
+
- **customize-token-storage** — override the token models (multi-tenant columns, custom storage)
|
|
58
|
+
- **throttle-login-attempts** — brute-force protection via `loginThrottleMiddleware`
|
|
59
|
+
- **verify-email-and-reset-password** — email verification, password reset, `requireVerifiedEmail`
|
|
60
|
+
- **login-with-providers** — Google, passkeys and phone one-time codes, all ending in `completeLogin`
|
|
61
|
+
- **run-auth-commands** — the bundled CLI commands
|
|
62
|
+
|
|
63
|
+
## Generate the JWT secret
|
|
64
|
+
|
|
65
|
+
Register the command in `warlock.config.ts`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { registerJWTSecretGeneratorCommand } from "@warlock.js/auth";
|
|
69
|
+
import { defineConfig } from "@warlock.js/core";
|
|
70
|
+
|
|
71
|
+
export default defineConfig({
|
|
72
|
+
cli: {
|
|
73
|
+
commands: [registerJWTSecretGeneratorCommand()],
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
warlock jwt.generate
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
It generates cryptographically-strong `JWT_SECRET` and `JWT_REFRESH_SECRET` values and writes them to your `.env` file. Existing values are left untouched, so it's safe to re-run.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT © Hasan Zohdy
|
|
@@ -14,7 +14,7 @@ import { command } from "@warlock.js/core";
|
|
|
14
14
|
function registerAuthCleanupCommand() {
|
|
15
15
|
return command({
|
|
16
16
|
name: "auth.cleanup",
|
|
17
|
-
description: "Remove expired refresh tokens from the database",
|
|
17
|
+
description: "Remove expired access/refresh tokens and spent one-time tokens from the database",
|
|
18
18
|
preload: {
|
|
19
19
|
env: true,
|
|
20
20
|
config: ["auth", "database"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-cleanup-command.mjs","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"@warlock.js/core\";\nimport { authService } from \"../services/auth.service\";\n\n/**\n * Register the auth:cleanup CLI command\n *\n * @example\n * ```bash\n * warlock auth:cleanup\n * ```\n */\nexport function registerAuthCleanupCommand() {\n return command({\n name: \"auth.cleanup\",\n description: \"Remove expired refresh tokens from the database\",\n preload: {\n env: true,\n config: [\"auth\", \"database\"],\n connectors: [\"database\"],\n },\n action: async () => {\n console.log(colors.cyan(\"🧹 Cleaning up expired tokens...\"));\n\n const count = await authService.cleanupExpiredTokens();\n\n if (count === 0) {\n console.log(colors.green(\"✅ No expired tokens found.\"));\n } else {\n console.log(colors.green(`✅ Removed ${count} expired token(s).`));\n }\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,6BAA6B;CAC3C,OAAO,QAAQ;EACb,MAAM;EACN,aAAa;EACb,SAAS;GACP,KAAK;GACL,QAAQ,CAAC,QAAQ,UAAU;GAC3B,YAAY,CAAC,UAAU;EACzB;EACA,QAAQ,YAAY;GAClB,QAAQ,IAAI,OAAO,KAAK,kCAAkC,CAAC;GAE3D,MAAM,QAAQ,MAAM,YAAY,qBAAqB;GAErD,IAAI,UAAU,GACZ,QAAQ,IAAI,OAAO,MAAM,4BAA4B,CAAC;QAEtD,QAAQ,IAAI,OAAO,MAAM,aAAa,MAAM,mBAAmB,CAAC;EAEpE;CACF,CAAC;AACH"}
|
|
1
|
+
{"version":3,"file":"auth-cleanup-command.mjs","names":[],"sources":["../../../../../../../auth/src/commands/auth-cleanup-command.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { command } from \"@warlock.js/core\";\nimport { authService } from \"../services/auth.service\";\n\n/**\n * Register the auth:cleanup CLI command\n *\n * @example\n * ```bash\n * warlock auth:cleanup\n * ```\n */\nexport function registerAuthCleanupCommand() {\n return command({\n name: \"auth.cleanup\",\n description: \"Remove expired access/refresh tokens and spent one-time tokens from the database\",\n preload: {\n env: true,\n config: [\"auth\", \"database\"],\n connectors: [\"database\"],\n },\n action: async () => {\n console.log(colors.cyan(\"🧹 Cleaning up expired tokens...\"));\n\n const count = await authService.cleanupExpiredTokens();\n\n if (count === 0) {\n console.log(colors.green(\"✅ No expired tokens found.\"));\n } else {\n console.log(colors.green(`✅ Removed ${count} expired token(s).`));\n }\n },\n });\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,6BAA6B;CAC3C,OAAO,QAAQ;EACb,MAAM;EACN,aAAa;EACb,SAAS;GACP,KAAK;GACL,QAAQ,CAAC,QAAQ,UAAU;GAC3B,YAAY,CAAC,UAAU;EACzB;EACA,QAAQ,YAAY;GAClB,QAAQ,IAAI,OAAO,KAAK,kCAAkC,CAAC;GAE3D,MAAM,QAAQ,MAAM,YAAY,qBAAqB;GAErD,IAAI,UAAU,GACZ,QAAQ,IAAI,OAAO,MAAM,4BAA4B,CAAC;QAEtD,QAAQ,IAAI,OAAO,MAAM,aAAa,MAAM,mBAAmB,CAAC;EAEpE;CACF,CAAC;AACH"}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AuthProvider, GoogleProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig } from "./providers.mjs";
|
|
2
|
+
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
|
|
3
|
+
import { Authenticable } from "./auth-contract.mjs";
|
|
4
|
+
import { RequestUser } from "./request-locals.mjs";
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Auth } from "../models/auth.model.mjs";
|
|
2
|
+
import { ChildModel } from "@warlock.js/cascade";
|
|
3
|
+
|
|
4
|
+
//#region ../auth/src/contracts/providers.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The per-login secrets a redirect provider round-trips: `state` (CSRF),
|
|
7
|
+
* `nonce` (id_token replay binding) and the PKCE `codeVerifier`.
|
|
8
|
+
*/
|
|
9
|
+
type ProviderAuthorizationState = {
|
|
10
|
+
state: string;
|
|
11
|
+
nonce: string;
|
|
12
|
+
codeVerifier: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* What a provider's callback hands {@link AuthProvider.handleCallback}: the
|
|
16
|
+
* callback query and the state that was stored when the login started. The
|
|
17
|
+
* caller has already checked `query.state` against `expected.state`.
|
|
18
|
+
*/
|
|
19
|
+
type ProviderCallbackParams = {
|
|
20
|
+
query: Record<string, unknown>;
|
|
21
|
+
expected: ProviderAuthorizationState;
|
|
22
|
+
};
|
|
23
|
+
/** The identity a provider asserts, normalized across providers. */
|
|
24
|
+
type ProviderProfile = {
|
|
25
|
+
/** Provider name, e.g. `"google"`. */provider: string; /** The provider's stable user id (`sub`). The link key — never the email. */
|
|
26
|
+
providerUserId: string;
|
|
27
|
+
email?: string; /** Whether the PROVIDER verified `email`. Accounts are never linked on `false`. */
|
|
28
|
+
emailVerified: boolean;
|
|
29
|
+
name?: string;
|
|
30
|
+
avatar?: string; /** The verified claims the profile was built from. */
|
|
31
|
+
raw: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A redirect-style login provider (OAuth 2 / OIDC authorization code).
|
|
35
|
+
* Register an instance under `auth.providers.custom.<name>` to add one auth does
|
|
36
|
+
* not ship.
|
|
37
|
+
*/
|
|
38
|
+
interface AuthProvider {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
/** The URL to send the browser to. */
|
|
41
|
+
authorizationUrl(state: ProviderAuthorizationState): string | Promise<string>;
|
|
42
|
+
/** Exchange the callback for a verified profile, or throw. */
|
|
43
|
+
handleCallback(params: ProviderCallbackParams): Promise<ProviderProfile>;
|
|
44
|
+
}
|
|
45
|
+
/** `auth.providers.google`. */
|
|
46
|
+
type GoogleProviderConfig = {
|
|
47
|
+
clientId: string;
|
|
48
|
+
clientSecret: string; /** Must exactly match a redirect URI registered in the Google console. */
|
|
49
|
+
redirectUri: string; /** @default ["openid", "email", "profile"] */
|
|
50
|
+
scopes?: string[];
|
|
51
|
+
};
|
|
52
|
+
/** Creates the app user for a verified provider profile with no matching account. */
|
|
53
|
+
type ProviderUserCreator = (profile: ProviderProfile, Model: ChildModel<Auth>) => Auth | Promise<Auth>;
|
|
54
|
+
/** `auth.providers`. */
|
|
55
|
+
type ProvidersConfig = {
|
|
56
|
+
google?: GoogleProviderConfig;
|
|
57
|
+
/**
|
|
58
|
+
* User attribute a verified provider email is matched against.
|
|
59
|
+
* @default "email"
|
|
60
|
+
*/
|
|
61
|
+
emailField?: string; /** Replace the default user creation (`{ email, name, <verification.field>: now }`). */
|
|
62
|
+
createUser?: ProviderUserCreator; /** Providers auth does not ship, by name. */
|
|
63
|
+
custom?: Record<string, AuthProvider>;
|
|
64
|
+
};
|
|
65
|
+
/** `auth.passkeys`. */
|
|
66
|
+
type PasskeysConfig = {
|
|
67
|
+
/** Relying-party id — the site's registrable domain, e.g. `"example.com"`. */rpID: string; /** Name shown by the authenticator. */
|
|
68
|
+
rpName: string; /** Exact origin(s) the browser ceremony runs on, e.g. `"https://example.com"`. */
|
|
69
|
+
origin: string | string[];
|
|
70
|
+
/**
|
|
71
|
+
* Challenge lifetime — a positive `ms` duration string.
|
|
72
|
+
* @default "5m"
|
|
73
|
+
*/
|
|
74
|
+
challengeExpiresIn?: string; /** Account name shown by the authenticator. @default user email, else id */
|
|
75
|
+
userName?: (user: Auth) => string;
|
|
76
|
+
};
|
|
77
|
+
/** Data an OTP delivery receives. */
|
|
78
|
+
type OtpMessage = {
|
|
79
|
+
/** Ready-to-send text containing the code. */body: string;
|
|
80
|
+
code: string;
|
|
81
|
+
expiresAt: Date;
|
|
82
|
+
};
|
|
83
|
+
/** Delivers an OTP. Replaces the default `notify.channel(channel).send(phone, message)`. */
|
|
84
|
+
type OtpSender = (phone: string, message: OtpMessage, channel: string) => unknown;
|
|
85
|
+
/** `auth.otp`. */
|
|
86
|
+
type OtpConfig = {
|
|
87
|
+
/**
|
|
88
|
+
* Notifications channel the code is sent through. The app registers it
|
|
89
|
+
* (auth ships no SMS/WhatsApp driver).
|
|
90
|
+
* @default "sms"
|
|
91
|
+
*/
|
|
92
|
+
channel?: string; /** User attribute holding the phone number. @default "phone" */
|
|
93
|
+
phoneField?: string; /** Code lifetime. @default "5m" */
|
|
94
|
+
expiresIn?: string; /** Verify attempts before the code is invalidated. @default 5 */
|
|
95
|
+
maxAttempts?: number; /** Build the message text. @default "Your verification code is <code>" */
|
|
96
|
+
message?: (code: string) => string; /** Replace delivery entirely. */
|
|
97
|
+
send?: OtpSender;
|
|
98
|
+
};
|
|
99
|
+
//#endregion
|
|
100
|
+
export { AuthProvider, GoogleProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig };
|
|
101
|
+
//# sourceMappingURL=providers.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/providers.ts"],"mappings":";;;;;;AAOA;;KAAY,0BAAA;EACV,KAAA;EACA,KAAA;EACA,YAAA;AAAA;;AAAY;AAQd;;;KAAY,sBAAA;EACV,KAAA,EAAO,MAAA;EACP,QAAA,EAAU,0BAA0B;AAAA;;KAI1B,eAAA;EAJ0B,sCAMpC,QAAA,UAFyB;EAIzB,cAAA;EACA,KAAA,WAHA;EAKA,aAAA;EACA,IAAA;EACA,MAAA,WADA;EAGA,GAAA,EAAK,MAAM;AAAA;;;AAAA;AAQb;;UAAiB,YAAA;EAAA,SACN,IAAA;EAEqD;EAA9D,gBAAA,CAAiB,KAAA,EAAO,0BAAA,YAAsC,OAAA;EAEN;EAAxD,cAAA,CAAe,MAAA,EAAQ,sBAAA,GAAyB,OAAA,CAAQ,eAAA;AAAA;;KAI9C,oBAAA;EACV,QAAA;EACA,YAAA,UARiB;EAUjB,WAAA,UARA;EAUA,MAAA;AAAA;;KAIU,mBAAA,IACV,OAAA,EAAS,eAAA,EACT,KAAA,EAAO,UAAA,CAAW,IAAA,MACf,IAAA,GAAO,OAAA,CAAQ,IAAA;;KAGR,eAAA;EACV,MAAA,GAAS,oBAAA;EAjBqB;;;;EAsB9B,UAAA,WAlBA;EAoBA,UAAA,GAAa,mBAAA,EAlBP;EAoBN,MAAA,GAAS,MAAA,SAAe,YAAA;AAAA;;KAId,cAAA;EAnBD,8EAqBT,IAAA,UApBO;EAsBP,MAAA,UArBkB;EAuBlB,MAAA;EAvBiB;;;;EA4BjB,kBAAA,WA7BkB;EA+BlB,QAAA,IAAY,IAAA,EAAM,IAAI;AAAA;;KAIZ,UAAA;EAlCY,8CAoCtB,IAAA;EACA,IAAA;EACA,SAAA,EAAW,IAAI;AAAA;;KAIL,SAAA,IAAa,KAAA,UAAe,OAAA,EAAS,UAAU,EAAE,OAAA;;KAGjD,SAAA;EAhCK;;;;;EAsCf,OAAA,WAxCa;EA0Cb,UAAA,WAxCS;EA0CT,SAAA,WA1CoC;EA4CpC,WAAA,WAxCU;EA0CV,OAAA,IAAW,IAAA;EAEX,IAAA,GAAO,SAAS;AAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region ../auth/src/contracts/request-locals.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The authenticated user attached to the current request —
|
|
4
|
+
* `request.locals.user`.
|
|
5
|
+
*
|
|
6
|
+
* Moved here from `@warlock.js/core` in 5.12.0 (core's `Request` no longer
|
|
7
|
+
* has a `user` property at all — see `RequestUserMovedError`). Empty by
|
|
8
|
+
* default, exactly as it was in core, so any shape is assignable at the
|
|
9
|
+
* declaration site; an app narrows it via module augmentation:
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* declare module "@warlock.js/auth" {
|
|
14
|
+
* interface RequestUser {
|
|
15
|
+
* id: string | number;
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Deliberately NOT `extends Auth<ModelSchema>` here: an `interface` can
|
|
21
|
+
* extend at most one class hierarchy, and a library-side
|
|
22
|
+
* `interface RequestUser extends Auth<ModelSchema>` would pre-claim that one
|
|
23
|
+
* extends slot, so an app's own `interface RequestUser extends User {}`
|
|
24
|
+
* would fail to compile (TS2320). See `access/src/middleware/gate.middleware.ts`
|
|
25
|
+
* for the same hazard documented at its call site.
|
|
26
|
+
*/
|
|
27
|
+
interface RequestUser {}
|
|
28
|
+
/**
|
|
29
|
+
* Augments core's `RequestLocals` (`request.locals`) with the `user` key —
|
|
30
|
+
* done in the module that OWNS the key, per `RequestLocals`' own contract
|
|
31
|
+
* (`core/src/http/types.ts`). `authMiddleware` (`middleware/auth.middleware.ts`)
|
|
32
|
+
* is the sole writer: it sets `request.locals.user` after a successful token
|
|
33
|
+
* resolution and clears it (`= undefined`) on a forged, malformed, expired,
|
|
34
|
+
* or wrong-type token.
|
|
35
|
+
*/
|
|
36
|
+
declare module "@warlock.js/core" {
|
|
37
|
+
interface RequestLocals {
|
|
38
|
+
user?: RequestUser;
|
|
39
|
+
}
|
|
40
|
+
} //# sourceMappingURL=request-locals.d.ts.map
|
|
41
|
+
//#endregion
|
|
42
|
+
export { RequestUser };
|
|
43
|
+
//# sourceMappingURL=request-locals.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-locals.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/request-locals.ts"],"mappings":";;AAyBA;;;;AAA4B;AAAG;;;;;;;;;AAYT;;;;;;;;;;UAZL,WAAA;;;;;;;;;;YAWL,aAAA;IACR,IAAA,GAAO,WAAW;EAAA;AAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
2
2
|
import { AccessToken } from "../models/access-token/access-token.model.mjs";
|
|
3
|
+
import { OneTimeToken } from "../models/one-time-token/one-time-token.model.mjs";
|
|
4
|
+
import { OtpConfig, PasskeysConfig, ProvidersConfig } from "./providers.mjs";
|
|
3
5
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
4
6
|
import { ChildModel } from "@warlock.js/cascade";
|
|
5
7
|
import { Algorithm } from "fast-jwt";
|
|
@@ -156,6 +158,73 @@ type PageAuthConfig = {
|
|
|
156
158
|
*/
|
|
157
159
|
returnUrlParam?: string;
|
|
158
160
|
};
|
|
161
|
+
/**
|
|
162
|
+
* Data every email-verification / password-reset notification receives.
|
|
163
|
+
*/
|
|
164
|
+
type OneTimeTokenNotificationData = {
|
|
165
|
+
/** The raw token — the only place it exists; only its SHA-256 hash is stored. */token: string; /** When the token stops working. */
|
|
166
|
+
expiresAt: Date; /** The link built by `auth.verification.url` / `auth.passwordReset.url`, when configured. */
|
|
167
|
+
url?: string;
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Anything auth can deliver a token through — a `defineNotification(...)`
|
|
171
|
+
* result satisfies it, as does any object with a compatible `send`.
|
|
172
|
+
*/
|
|
173
|
+
type AuthNotification = {
|
|
174
|
+
send: (to: Auth, data: OneTimeTokenNotificationData) => unknown;
|
|
175
|
+
};
|
|
176
|
+
/** Builds the link a token notification points at. */
|
|
177
|
+
type OneTimeTokenUrlBuilder = (token: string, user: Auth) => string;
|
|
178
|
+
/** Persists a new plaintext password for a user during a password reset. */
|
|
179
|
+
type PasswordSetter = (user: Auth, plainPassword: string) => void | Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Email-verification configuration (`auth.verification`).
|
|
182
|
+
*/
|
|
183
|
+
type EmailVerificationConfig = {
|
|
184
|
+
/**
|
|
185
|
+
* Token lifetime — a positive `ms` duration string.
|
|
186
|
+
* @default "24h"
|
|
187
|
+
*/
|
|
188
|
+
expiresIn?: string;
|
|
189
|
+
/**
|
|
190
|
+
* User attribute stamped with the verification `Date`. Declare it in the
|
|
191
|
+
* user schema (e.g. `emailVerifiedAt: v.date().optional()`).
|
|
192
|
+
* @default "emailVerifiedAt"
|
|
193
|
+
*/
|
|
194
|
+
field?: string; /** Replace the default verification email. */
|
|
195
|
+
notification?: AuthNotification; /** Build the verification link passed to the notification as `url`. */
|
|
196
|
+
url?: OneTimeTokenUrlBuilder;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Password-reset configuration (`auth.passwordReset`).
|
|
200
|
+
*/
|
|
201
|
+
type PasswordResetConfig = {
|
|
202
|
+
/**
|
|
203
|
+
* Token lifetime — a positive `ms` duration string.
|
|
204
|
+
* @default "60m"
|
|
205
|
+
*/
|
|
206
|
+
expiresIn?: string;
|
|
207
|
+
/**
|
|
208
|
+
* User attribute `requestPasswordReset` looks the account up by.
|
|
209
|
+
* @default "email"
|
|
210
|
+
*/
|
|
211
|
+
identifierField?: string; /** Replace the default reset email. */
|
|
212
|
+
notification?: AuthNotification; /** Build the reset link passed to the notification as `url`. */
|
|
213
|
+
url?: OneTimeTokenUrlBuilder;
|
|
214
|
+
/**
|
|
215
|
+
* Persist the new password yourself. By default auth saves a hash made by
|
|
216
|
+
* the same `hashPassword` login verifies against, and falls back to the
|
|
217
|
+
* plaintext when the model hashes on save (`useHashedPassword()`), checking
|
|
218
|
+
* the stored value with `verifyPassword` either way.
|
|
219
|
+
*/
|
|
220
|
+
setPassword?: PasswordSetter;
|
|
221
|
+
};
|
|
222
|
+
/**
|
|
223
|
+
* One-time token storage configuration (`auth.oneTimeToken`).
|
|
224
|
+
*/
|
|
225
|
+
type OneTimeTokenConfig = {
|
|
226
|
+
/** Override the persisted model — extend {@link OneTimeToken}. */model?: typeof OneTimeToken;
|
|
227
|
+
};
|
|
159
228
|
type AuthConfigurations = {
|
|
160
229
|
/**
|
|
161
230
|
* Define all user types — maps a user-type slug to its `Auth` model class so
|
|
@@ -183,6 +252,40 @@ type AuthConfigurations = {
|
|
|
183
252
|
* Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.
|
|
184
253
|
*/
|
|
185
254
|
pageAuth?: PageAuthConfig;
|
|
255
|
+
/**
|
|
256
|
+
* Cookie auth session config — read by `authService.setAuthCookie` /
|
|
257
|
+
* `clearAuthCookie`. Opt-in: unset ⇒ the helpers fall back to their own
|
|
258
|
+
* defaults (`name: "access_token"`, `path: "/"`).
|
|
259
|
+
*/
|
|
260
|
+
cookie?: CookieAuthConfig;
|
|
261
|
+
/**
|
|
262
|
+
* CSRF Origin-check config for cookie-authenticated unsafe-method requests.
|
|
263
|
+
*/
|
|
264
|
+
csrf?: CsrfConfig;
|
|
265
|
+
/**
|
|
266
|
+
* Email verification — token lifetime, verified field, notification override.
|
|
267
|
+
*/
|
|
268
|
+
verification?: EmailVerificationConfig;
|
|
269
|
+
/**
|
|
270
|
+
* Password reset — token lifetime, lookup field, notification and password-writer overrides.
|
|
271
|
+
*/
|
|
272
|
+
passwordReset?: PasswordResetConfig;
|
|
273
|
+
/**
|
|
274
|
+
* One-time token (verification / reset) storage — model override.
|
|
275
|
+
*/
|
|
276
|
+
oneTimeToken?: OneTimeTokenConfig;
|
|
277
|
+
/**
|
|
278
|
+
* Redirect login providers (`google`, `custom`) and account-linking policy.
|
|
279
|
+
*/
|
|
280
|
+
providers?: ProvidersConfig;
|
|
281
|
+
/**
|
|
282
|
+
* Passkey (WebAuthn) relying-party settings. Requires `@simplewebauthn/server`.
|
|
283
|
+
*/
|
|
284
|
+
passkeys?: PasskeysConfig;
|
|
285
|
+
/**
|
|
286
|
+
* Phone one-time-code login — channel, lifetime, attempts, delivery.
|
|
287
|
+
*/
|
|
288
|
+
otp?: OtpConfig;
|
|
186
289
|
/**
|
|
187
290
|
* @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.
|
|
188
291
|
*/
|
|
@@ -241,6 +344,61 @@ type LoginResult<UserType extends Auth> = {
|
|
|
241
344
|
user: UserType;
|
|
242
345
|
tokens: TokenPair;
|
|
243
346
|
};
|
|
347
|
+
/**
|
|
348
|
+
* Configuration for the cookie an app opts into via `authService.setAuthCookie`
|
|
349
|
+
* / `clearAuthCookie` and reads back with `authMiddleware([], "cookie:<name>")`.
|
|
350
|
+
* Attribute flags (`httpOnly` / `sameSite` / `secure`) are deliberately NOT
|
|
351
|
+
* configurable here — they come from core's `secureCookieDefaults()`, the
|
|
352
|
+
* same floor every `response.cookie()` call gets, so a cookie auth session can
|
|
353
|
+
* never be weakened to a laxer policy than the framework default.
|
|
354
|
+
*/
|
|
355
|
+
type CookieAuthConfig = {
|
|
356
|
+
/**
|
|
357
|
+
* Cookie name `setAuthCookie`/`clearAuthCookie` write and clear, and that a
|
|
358
|
+
* `cookie:<name>` token source must match to read it back.
|
|
359
|
+
* @default "access_token"
|
|
360
|
+
*/
|
|
361
|
+
name?: string;
|
|
362
|
+
/**
|
|
363
|
+
* Cookie `Path` attribute.
|
|
364
|
+
* @default "/"
|
|
365
|
+
*/
|
|
366
|
+
path?: string;
|
|
367
|
+
};
|
|
368
|
+
/**
|
|
369
|
+
* CSRF Origin-check configuration. Read via
|
|
370
|
+
* `authConfig.csrf.allowedOrigins()`.
|
|
371
|
+
*/
|
|
372
|
+
type CsrfConfig = {
|
|
373
|
+
/**
|
|
374
|
+
* Extra origins allowed on a cookie-authenticated unsafe-method request,
|
|
375
|
+
* in addition to the request's own origin.
|
|
376
|
+
* @default []
|
|
377
|
+
*/
|
|
378
|
+
allowedOrigins?: string[];
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Per-call overrides for {@link AuthService.setAuthCookie}. Anything omitted
|
|
382
|
+
* falls back to `auth.cookie.*` config, then the package default. Cookie
|
|
383
|
+
* *attribute* flags (`httpOnly`/`sameSite`/`secure`) are never settable here —
|
|
384
|
+
* see {@link CookieAuthConfig}.
|
|
385
|
+
*/
|
|
386
|
+
type SetAuthCookieOptions = {
|
|
387
|
+
/** Cookie name; defaults to `auth.cookie.name` (package default `"access_token"`). */name?: string; /** Cookie `Path`; defaults to `auth.cookie.path` (package default `"/"`). */
|
|
388
|
+
path?: string;
|
|
389
|
+
/**
|
|
390
|
+
* `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would
|
|
391
|
+
* otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a
|
|
392
|
+
* bare token string with no `maxAge` produces a session cookie (cleared
|
|
393
|
+
* when the browser closes).
|
|
394
|
+
*/
|
|
395
|
+
maxAge?: number;
|
|
396
|
+
};
|
|
397
|
+
/** Per-call overrides for {@link AuthService.clearAuthCookie}. */
|
|
398
|
+
type ClearAuthCookieOptions = {
|
|
399
|
+
/** Cookie name; defaults to `auth.cookie.name` (package default `"access_token"`). */name?: string; /** Cookie `Path`; defaults to `auth.cookie.path` (package default `"/"`). Must match the `Path` the cookie was set with. */
|
|
400
|
+
path?: string;
|
|
401
|
+
};
|
|
244
402
|
//#endregion
|
|
245
|
-
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair };
|
|
403
|
+
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair };
|
|
246
404
|
//# sourceMappingURL=types.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":"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"}
|