@warlock.js/auth 5.24.0 → 5.26.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 +26 -9
- package/LICENSE +21 -21
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/request-locals.d.mts +1 -1
- package/esm/contracts/types.d.mts +8 -3
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +3 -3
- package/esm/index.mjs +2 -2
- package/esm/middleware/auth.middleware.mjs +1 -1
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -1
- package/esm/models/auth-token-family/auth-token-family.model.mjs +5 -0
- package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs +3 -1
- package/esm/services/auth-config.mjs.map +1 -1
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +9 -4
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +45 -17
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/index.d.mts +1 -1
- package/esm/services/index.mjs +1 -1
- package/esm/services/resolve-request-user.d.mts +3 -2
- package/esm/services/resolve-request-user.d.mts.map +1 -1
- package/esm/services/resolve-request-user.mjs +8 -2
- package/esm/services/resolve-request-user.mjs.map +1 -1
- package/esm/session/page-session.d.mts +3 -1
- package/esm/session/page-session.d.mts.map +1 -1
- package/esm/session/page-session.mjs +2 -1
- package/esm/session/page-session.mjs.map +1 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +1946 -1927
- package/package.json +8 -8
- package/skills/auth-basics/SKILL.md +1 -1
- 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 +195 -180
- package/skills/login-with-providers/SKILL.md +283 -283
- package/skills/manage-tokens/SKILL.md +196 -196
- package/skills/overview/SKILL.md +74 -74
- package/skills/protect-routes/SKILL.md +299 -299
- package/skills/register-user/SKILL.md +147 -147
- package/skills/run-auth-commands/SKILL.md +159 -159
- package/skills/throttle-login-attempts/SKILL.md +117 -117
- package/skills/verify-email-and-reset-password/SKILL.md +137 -137
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.26.0 - 2026-09-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Cookie sessions now support `auth.cookie.cookieDomain(request)` and `pageSession`/`sessionMiddleware`'s `cookieDomain(request)` override. Returning a Domain scopes issued and renewed cookies to it; returning `undefined` preserves host-only cookies. Clearing a Domain-scoped session also clears the legacy host-only cookie names. When configured, pass `request` to `setSessionCookies(response, tokens, request)`, `clearSessionCookies(response, request)`, and `clearAuthCookie(response, options, request)`; omitting it throws `CookieDomainRequestRequiredError` rather than issuing or clearing a host-only cookie.
|
|
12
|
+
|
|
13
|
+
### Notes
|
|
14
|
+
|
|
15
|
+
- A page session configured with multiple `userTypes` cannot renew its cookie: the resolver only supplies a renewal user type when exactly one type is allowed, so it reads the session but skips automatic renewal.
|
|
16
|
+
|
|
17
|
+
## 5.25.0 - 2026-09-28
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- `authMiddleware` now returns its normal `401` missing-credential response for an unsafe dual-source request that presents neither access nor refresh cookies, instead of treating the configured refresh descriptor as a presented credential and rejecting its Origin with `403`.
|
|
22
|
+
- Token-pair issuance now ensures its durable token family once, and repeated ensures return the existing family without another upsert.
|
|
23
|
+
|
|
7
24
|
## 5.24.0 - 2026-09-27
|
|
8
25
|
|
|
9
26
|
### Changed
|
|
@@ -58,10 +75,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
58
75
|
- **BREAKING:** `authMiddleware` takes an ordered header/cookie `sources` list; a present Authorization header always wins and never falls back to the cookie, so an invalid header now returns `401`.
|
|
59
76
|
- JWTs carry a random `jti`, so tokens issued in the same second differ.
|
|
60
77
|
|
|
61
|
-
|
|
62
78
|
### Changed
|
|
63
79
|
|
|
64
80
|
- Lockstep release maintenance and dependency refresh.
|
|
81
|
+
|
|
65
82
|
## 5.20.0 - 2026-09-24
|
|
66
83
|
|
|
67
84
|
### Changed
|
|
@@ -251,17 +268,17 @@ changelogs.
|
|
|
251
268
|
|
|
252
269
|
- **A token with no `exp` claim is now rejected instead of being accepted forever.** `fast-jwt` has no deadline to check on such a token, so verification simply succeeds — measured against `fast-jwt@6.2.4`, a token with no `exp` verifies unchanged at `clockTimestamp` + 100 years. Both `jwt.verify` and `jwt.verifyRefreshToken` now require an `exp` claim.
|
|
253
270
|
|
|
254
|
-
This is deliberately on the
|
|
271
|
+
This is deliberately on the _verify_ side rather than the issue side: the tokens that lack an `exp` were minted by a version that no longer runs, so a guard on issue would not reach a single one of them. A caller may add to `requiredClaims`, never subtract — `jwt.verify(token, { requiredClaims: ["iat"] })` still requires `exp` too.
|
|
255
272
|
|
|
256
|
-
- **The persisted `expires_at` is now enforced on every request.** `authMiddleware` previously checked only that the access-token row
|
|
273
|
+
- **The persisted `expires_at` is now enforced on every request.** `authMiddleware` previously checked only that the access-token row _existed_; a row whose own expiry had passed still opened the gate, because nothing ever asked. The row is now checked against the clock and deleted on rejection.
|
|
257
274
|
|
|
258
|
-
These are two independent defences. The first catches a token whose
|
|
275
|
+
These are two independent defences. The first catches a token whose _claims_ carry no deadline; the second catches a token whose _row_ says the deadline has passed (a logged-out or expired session whose JWT is still within its own lifetime). Neither subsumes the other.
|
|
259
276
|
|
|
260
277
|
### Added
|
|
261
278
|
|
|
262
279
|
- **`warlock auth.purge-never-expiring`** — remediation for rows written by the `expiresIn` defect below. Finds every access- and refresh-token row that can never retire itself, on two independent signals: an `expires_at` that is missing or unparseable, and a persisted token carrying no `exp` claim. Reports `id`, `user_id`, `user_type` and `expires_at` per row (never the token string — it is a live credential until the command removes it), then deletes them. Pass `--dry-run` to report without deleting.
|
|
263
280
|
|
|
264
|
-
`auth.cleanup` cannot find these rows and never could: its predicate is `expires_at < now`, and an `Invalid Date` compares `false` against
|
|
281
|
+
`auth.cleanup` cannot find these rows and never could: its predicate is `expires_at < now`, and an `Invalid Date` compares `false` against _every_ date, so such a row satisfies neither `< now` nor `> now`. It is outside the reach of every date predicate rather than merely wrong. The "no `exp` claim" signal is not in a column at all.
|
|
265
282
|
|
|
266
283
|
Exposed programmatically as `authService.findNeverExpiringTokens()` (read-only) and `authService.purgeNeverExpiringTokens()`, and per model as `findNeverExpiring()` / `purgeNeverExpiring()`.
|
|
267
284
|
|
|
@@ -281,8 +298,8 @@ Affected users must log in again. To check **without deploying anything**, the r
|
|
|
281
298
|
- **MongoDB** — `bson` serialises an `Invalid Date` to **epoch 0**, so the row reads `1970-01-01T00:00:00Z` rather than an invalid value:
|
|
282
299
|
|
|
283
300
|
```js
|
|
284
|
-
db.access_tokens.find({ expires_at: { $lte: new Date(0) } })
|
|
285
|
-
db.refresh_tokens.find({ expires_at: { $lte: new Date(0) } })
|
|
301
|
+
db.access_tokens.find({ expires_at: { $lte: new Date(0) } });
|
|
302
|
+
db.refresh_tokens.find({ expires_at: { $lte: new Date(0) } });
|
|
286
303
|
```
|
|
287
304
|
|
|
288
305
|
- **PostgreSQL** — `pg` serialises an `Invalid Date` to the literal `0NaN-NaN-NaNTNaN:NaN:NaN.NaN+NaN:NaN`, which a `timestamp` column rejects, so the `INSERT` most likely failed and no row was written (the token was still signed and returned to the client — it is then rejected by the row check, since it has no row). Any rows that did land are visible as:
|
|
@@ -292,7 +309,7 @@ Affected users must log in again. To check **without deploying anything**, the r
|
|
|
292
309
|
SELECT id, user_id, user_type, expires_at FROM refresh_tokens WHERE expires_at IS NULL OR expires_at <= 'epoch';
|
|
293
310
|
```
|
|
294
311
|
|
|
295
|
-
These queries find the row-level shape only. The definitive test —
|
|
312
|
+
These queries find the row-level shape only. The definitive test — _does the stored token carry an `exp` claim at all_ — reads the JWT rather than a column, which is why the command exists and why it is the recommended path.
|
|
296
313
|
|
|
297
314
|
### Fixed
|
|
298
315
|
|
|
@@ -308,7 +325,7 @@ These queries find the row-level shape only. The definitive test — *does the s
|
|
|
308
325
|
|
|
309
326
|
- **`expiresIn: "0d"` (and any non-positive duration) is rejected too.** It is truthy and parses cleanly to `0`, so it survived any guard that only rejects `undefined` — and `fast-jwt` skips its own validation for `0`, emitting a token with **no `exp` claim** while the persisted row claims it expired immediately.
|
|
310
327
|
|
|
311
|
-
- A bare number (`expiresIn: 2592000`) is now rejected instead of silently corrupting the expiry. `ms`
|
|
328
|
+
- A bare number (`expiresIn: 2592000`) is now rejected instead of silently corrupting the expiry. `ms` _formats_ numbers rather than parsing them (`2592000` ⇒ `"43m"`), which then poisoned `Date.now() + expiresIn` into `Invalid Date`. Write `"30d"`.
|
|
312
329
|
|
|
313
330
|
### Changed
|
|
314
331
|
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) Hassan Zohdy
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Hassan Zohdy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { AppleProviderConfig, AuthProvider, DiscordProviderConfig, FacebookProviderConfig, GitHubProviderConfig, GoogleProviderConfig, LinkedInProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig, XProviderConfig } from "./providers.mjs";
|
|
2
|
-
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
|
|
2
|
+
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CookieDomainResolver, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
|
|
3
3
|
import { Authenticable } from "./auth-contract.mjs";
|
|
4
4
|
import { RequestUser } from "./request-locals.mjs";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `request.locals.user`.
|
|
5
5
|
*
|
|
6
6
|
* Moved here from `@warlock.js/core` in 5.12.0 (core's `Request` no longer
|
|
7
|
-
* has a `user` property at all
|
|
7
|
+
* has a `user` property at all). Empty by
|
|
8
8
|
* default, exactly as it was in core, so any shape is assignable at the
|
|
9
9
|
* declaration site; an app narrows it via module augmentation:
|
|
10
10
|
*
|
|
@@ -3,6 +3,7 @@ import { AccessToken } from "../models/access-token/access-token.model.mjs";
|
|
|
3
3
|
import { OneTimeToken } from "../models/one-time-token/one-time-token.model.mjs";
|
|
4
4
|
import { OtpConfig, PasskeysConfig, ProvidersConfig } from "./providers.mjs";
|
|
5
5
|
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
6
|
+
import { Request } from "@warlock.js/core";
|
|
6
7
|
import { ChildModel } from "@warlock.js/cascade";
|
|
7
8
|
import { Algorithm } from "fast-jwt";
|
|
8
9
|
|
|
@@ -373,6 +374,8 @@ type LoginResult<UserType extends Auth> = {
|
|
|
373
374
|
user: UserType;
|
|
374
375
|
tokens: TokenPair;
|
|
375
376
|
};
|
|
377
|
+
/** Selects a cookie Domain for one request; return `undefined` for a host-only cookie. */
|
|
378
|
+
type CookieDomainResolver = (request: Request) => string | undefined;
|
|
376
379
|
/**
|
|
377
380
|
* Configuration for the cookie an app opts into via `authService.setAuthCookie`
|
|
378
381
|
* / `clearAuthCookie` and reads back with `authMiddleware([], "cookie:<name>")`.
|
|
@@ -397,7 +400,8 @@ type CookieAuthConfig = {
|
|
|
397
400
|
* Cookie `Path` attribute.
|
|
398
401
|
* @default "/"
|
|
399
402
|
*/
|
|
400
|
-
path?: string;
|
|
403
|
+
path?: string; /** Select a request-scoped Domain; `undefined` keeps the cookie host-only. */
|
|
404
|
+
cookieDomain?: CookieDomainResolver;
|
|
401
405
|
};
|
|
402
406
|
/**
|
|
403
407
|
* CSRF Origin-check configuration. Read via
|
|
@@ -419,7 +423,8 @@ type CsrfConfig = {
|
|
|
419
423
|
*/
|
|
420
424
|
type SetAuthCookieOptions = {
|
|
421
425
|
/** Cookie name; defaults to `auth.cookie.name` (package default `"access_token"`). */name?: string; /** Cookie `Path`; defaults to `auth.cookie.path` (package default `"/"`). */
|
|
422
|
-
path?: string;
|
|
426
|
+
path?: string; /** Cookie `Domain` attribute. Omit for a host-only cookie. */
|
|
427
|
+
domain?: string;
|
|
423
428
|
/**
|
|
424
429
|
* `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would
|
|
425
430
|
* otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a
|
|
@@ -434,5 +439,5 @@ type ClearAuthCookieOptions = {
|
|
|
434
439
|
path?: string;
|
|
435
440
|
};
|
|
436
441
|
//#endregion
|
|
437
|
-
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair };
|
|
442
|
+
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CookieDomainResolver, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair };
|
|
438
443
|
//# 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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { OneTimeToken } from \"../models/one-time-token\";\r\nimport type { OtpConfig, PasskeysConfig, ProvidersConfig } from \"./providers\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/** The single request credential source an auth middleware instance accepts. */\r\nexport type TokenFrom = \"header\" | `cookie:${string}`;\r\n\r\n/** One explicitly named credential location accepted by auth middleware. */\r\nexport interface AuthCredentialDescriptor {\r\n source: \"header\" | \"cookie\";\r\n key?: string;\r\n}\r\n\r\n/** Cookie credential and bounded duplicate window for automatic renewal. */\r\nexport interface AuthRefreshCredentialDescriptor extends AuthCredentialDescriptor {\r\n /** Milliseconds; the service clamps this to the 0–10 second range (default 5 seconds). */\r\n overlapMs?: number;\r\n}\r\n\r\n/** Per-route authentication behavior for the object middleware overload. */\r\nexport interface AuthMiddlewareOptions extends Partial<AuthCredentialDescriptor> {\r\n optional?: boolean;\r\n /**\r\n * Accept both a header and a cookie credential (at most one of each).\r\n * Precedence is fixed and independent of array order: a present\r\n * `Authorization` header wins, an invalid header rejects and never falls back\r\n * to the cookie, and renewal plus the CSRF Origin check apply only when the\r\n * cookie is the credential actually used. Cannot be combined with `source`.\r\n */\r\n sources?: AuthCredentialDescriptor[];\r\n /** Opt-in automatic renewal credential. Cookie sources only. */\r\n refresh?: AuthRefreshCredentialDescriptor;\r\n redirect?: { to: string; returnUrlParam?: string };\r\n}\r\n\r\n/** Application policy that decides whether a resolved user may authenticate. */\r\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\r\n\r\n/** Password credentials plus the application-defined identity fields. */\r\nexport type AuthCredentials = Record<string, unknown> & {\r\n password: string;\r\n};\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\r\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\r\n * first token issue: each of them would otherwise sign a token with no `exp`\r\n * claim at all.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds; an unusable value throws naming this key on issue\r\n * (see {@link AccessTokenConfig.expiresIn}).\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\n/**\r\n * Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,\r\n * `route.isPage`) rejects a logged-out browser, this decides whether it redirects\r\n * to a login page instead of returning the API-style JSON 401.\r\n *\r\n * Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes\r\n * always keep the JSON 401 contract regardless of this setting.\r\n */\r\nexport type PageAuthConfig = {\r\n /**\r\n * Login path a guarded page route redirects a logged-out browser to (e.g.\r\n * `\"/login\"`). Unset ⇒ page-route auth failures keep the JSON 401, so the\r\n * behavior is opt-in and backward-compatible.\r\n */\r\n loginPath?: string;\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed.\r\n * @default \"returnUrl\"\r\n */\r\n returnUrlParam?: string;\r\n};\r\n\r\n/**\r\n * Data every email-verification / password-reset notification receives.\r\n */\r\nexport type OneTimeTokenNotificationData = {\r\n /** The raw token — the only place it exists; only its SHA-256 hash is stored. */\r\n token: string;\r\n /** When the token stops working. */\r\n expiresAt: Date;\r\n /** The link built by `auth.verification.url` / `auth.passwordReset.url`, when configured. */\r\n url?: string;\r\n};\r\n\r\n/**\r\n * Anything auth can deliver a token through — a `defineNotification(...)`\r\n * result satisfies it, as does any object with a compatible `send`.\r\n */\r\nexport type AuthNotification = {\r\n send: (to: Auth, data: OneTimeTokenNotificationData) => unknown;\r\n};\r\n\r\n/** Builds the link a token notification points at. */\r\nexport type OneTimeTokenUrlBuilder = (token: string, user: Auth) => string;\r\n\r\n/** Persists a new plaintext password for a user during a password reset. */\r\nexport type PasswordSetter = (user: Auth, plainPassword: string) => void | Promise<void>;\r\n\r\n/**\r\n * Email-verification configuration (`auth.verification`).\r\n */\r\nexport type EmailVerificationConfig = {\r\n /**\r\n * Token lifetime — a positive `ms` duration string.\r\n * @default \"24h\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * User attribute stamped with the verification `Date`. Declare it in the\r\n * user schema (e.g. `emailVerifiedAt: v.date().optional()`).\r\n * @default \"emailVerifiedAt\"\r\n */\r\n field?: string;\r\n /** Replace the default verification email. */\r\n notification?: AuthNotification;\r\n /** Build the verification link passed to the notification as `url`. */\r\n url?: OneTimeTokenUrlBuilder;\r\n};\r\n\r\n/**\r\n * Password-reset configuration (`auth.passwordReset`).\r\n */\r\nexport type PasswordResetConfig = {\r\n /**\r\n * Token lifetime — a positive `ms` duration string.\r\n * @default \"60m\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * User attribute `requestPasswordReset` looks the account up by.\r\n * @default \"email\"\r\n */\r\n identifierField?: string;\r\n /** Replace the default reset email. */\r\n notification?: AuthNotification;\r\n /** Build the reset link passed to the notification as `url`. */\r\n url?: OneTimeTokenUrlBuilder;\r\n /**\r\n * Persist the new password yourself. By default auth saves a hash made by\r\n * the same `hashPassword` login verifies against, and falls back to the\r\n * plaintext when the model hashes on save (`useHashedPassword()`), checking\r\n * the stored value with `verifyPassword` either way.\r\n */\r\n setPassword?: PasswordSetter;\r\n};\r\n\r\n/**\r\n * One-time token storage configuration (`auth.oneTimeToken`).\r\n */\r\nexport type OneTimeTokenConfig = {\r\n /** Override the persisted model — extend {@link OneTimeToken}. */\r\n model?: typeof OneTimeToken;\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /** Default user type used by `authMiddleware()` and options-only overloads. */\r\n defaultUserType?: string;\r\n /**\r\n * Decide whether a resolved user may authenticate or receive new tokens.\r\n * @default () => true\r\n */\r\n canAuthenticate?: CanAuthenticate;\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * Page-route auth-failure behavior — set `loginPath` to redirect a logged-out\r\n * browser on a guarded PAGE route to a login page instead of the JSON 401.\r\n * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.\r\n */\r\n pageAuth?: PageAuthConfig;\r\n /**\r\n * Cookie auth session config — read by `authService.setAuthCookie` /\r\n * `clearAuthCookie`. Opt-in: unset ⇒ the helpers fall back to their own\r\n * defaults (`name: \"access_token\"`, `path: \"/\"`).\r\n */\r\n cookie?: CookieAuthConfig;\r\n /**\r\n * CSRF Origin-check config for cookie-authenticated unsafe-method requests.\r\n */\r\n csrf?: CsrfConfig;\r\n /**\r\n * Email verification — token lifetime, verified field, notification override.\r\n */\r\n verification?: EmailVerificationConfig;\r\n /**\r\n * Password reset — token lifetime, lookup field, notification and password-writer overrides.\r\n */\r\n passwordReset?: PasswordResetConfig;\r\n /**\r\n * One-time token (verification / reset) storage — model override.\r\n */\r\n oneTimeToken?: OneTimeTokenConfig;\r\n /**\r\n * Redirect login providers (`google`, `custom`) and account-linking policy.\r\n */\r\n providers?: ProvidersConfig;\r\n /**\r\n * Passkey (WebAuthn) relying-party settings. Requires `@simplewebauthn/server`.\r\n */\r\n passkeys?: PasskeysConfig;\r\n /**\r\n * Phone one-time-code login — channel, lifetime, attempts, delivery.\r\n */\r\n otp?: OtpConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, unknown>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n\r\n/**\r\n * Configuration for the cookie an app opts into via `authService.setAuthCookie`\r\n * / `clearAuthCookie` and reads back with `authMiddleware([], \"cookie:<name>\")`.\r\n * Attribute flags (`httpOnly` / `sameSite` / `secure`) are deliberately NOT\r\n * configurable here — they come from core's `secureCookieDefaults()`, the\r\n * same floor every `response.cookie()` call gets, so a cookie auth session can\r\n * never be weakened to a laxer policy than the framework default.\r\n */\r\nexport type CookieAuthConfig = {\r\n /**\r\n * Cookie name `setAuthCookie`/`clearAuthCookie` write and clear, and that a\r\n * `cookie:<name>` token source must match to read it back.\r\n * @default \"access_token\"\r\n */\r\n name?: string;\r\n /**\r\n * Cookie name the session helpers write and clear for the refresh token.\r\n * @default \"refresh_token\"\r\n */\r\n refreshName?: string;\r\n /**\r\n * Cookie `Path` attribute.\r\n * @default \"/\"\r\n */\r\n path?: string;\r\n};\r\n\r\n/**\r\n * CSRF Origin-check configuration. Read via\r\n * `authConfig.csrf.allowedOrigins()`.\r\n */\r\nexport type CsrfConfig = {\r\n /**\r\n * Extra origins allowed on a cookie-authenticated unsafe-method request,\r\n * in addition to the request's own origin.\r\n * @default []\r\n */\r\n allowedOrigins?: string[];\r\n};\r\n\r\n/**\r\n * Per-call overrides for {@link AuthService.setAuthCookie}. Anything omitted\r\n * falls back to `auth.cookie.*` config, then the package default. Cookie\r\n * *attribute* flags (`httpOnly`/`sameSite`/`secure`) are never settable here —\r\n * see {@link CookieAuthConfig}.\r\n */\r\nexport type SetAuthCookieOptions = {\r\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\r\n name?: string;\r\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). */\r\n path?: string;\r\n /**\r\n * `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would\r\n * otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a\r\n * bare token string with no `maxAge` produces a session cookie (cleared\r\n * when the browser closes).\r\n */\r\n maxAge?: number;\r\n};\r\n\r\n/** Per-call overrides for {@link AuthService.clearAuthCookie}. */\r\nexport type ClearAuthCookieOptions = {\r\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\r\n name?: string;\r\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). Must match the `Path` the cookie was set with. */\r\n path?: string;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2BA,MAAa,gBAAgB"}
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\nimport type { Request } from \"@warlock.js/core\";\nimport { type Algorithm } from \"fast-jwt\";\nimport type { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { OneTimeToken } from \"../models/one-time-token\";\nimport type { OtpConfig, PasskeysConfig, ProvidersConfig } from \"./providers\";\nimport type { RefreshToken } from \"../models/refresh-token\";\n\n/**\n * Symbol to indicate no expiration for tokens\n * Use this when you explicitly want tokens to never expire\n *\n * @example\n * ```typescript\n * // src/config/auth.ts\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\n *\n * const authConfigurations: AuthConfigurations = {\n * accessToken: {\n * secret: env(\"JWT_SECRET\"),\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\n * },\n * };\n *\n * export default authConfigurations;\n * ```\n */\nexport const NO_EXPIRATION = \"100y\";\n\n/**\n * Behavior when logout is called without a refresh token\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\n * - \"error\": Return an error requiring the refresh token\n */\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\n\n/** The single request credential source an auth middleware instance accepts. */\nexport type TokenFrom = \"header\" | `cookie:${string}`;\n\n/** One explicitly named credential location accepted by auth middleware. */\nexport interface AuthCredentialDescriptor {\n source: \"header\" | \"cookie\";\n key?: string;\n}\n\n/** Cookie credential and bounded duplicate window for automatic renewal. */\nexport interface AuthRefreshCredentialDescriptor extends AuthCredentialDescriptor {\n /** Milliseconds; the service clamps this to the 0–10 second range (default 5 seconds). */\n overlapMs?: number;\n}\n\n/** Per-route authentication behavior for the object middleware overload. */\nexport interface AuthMiddlewareOptions extends Partial<AuthCredentialDescriptor> {\n optional?: boolean;\n /**\n * Accept both a header and a cookie credential (at most one of each).\n * Precedence is fixed and independent of array order: a present\n * `Authorization` header wins, an invalid header rejects and never falls back\n * to the cookie, and renewal plus the CSRF Origin check apply only when the\n * cookie is the credential actually used. Cannot be combined with `source`.\n */\n sources?: AuthCredentialDescriptor[];\n /** Opt-in automatic renewal credential. Cookie sources only. */\n refresh?: AuthRefreshCredentialDescriptor;\n redirect?: { to: string; returnUrlParam?: string };\n}\n\n/** Application policy that decides whether a resolved user may authenticate. */\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\n\n/** Password credentials plus the application-defined identity fields. */\nexport type AuthCredentials = Record<string, unknown> & {\n password: string;\n};\n\n/**\n * Access-token configuration.\n *\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\n * backward-compatible shim); one of the two is required at runtime.\n */\nexport type AccessTokenConfig = {\n /**\n * Override the persisted access-token model — extend {@link AccessToken} to\n * add columns (e.g. a tenant key) or rename them, then register the subclass\n * here. Defaults to the package's `AccessToken`.\n */\n model?: typeof AccessToken;\n /**\n * Secret used to sign access tokens.\n */\n secret?: string;\n /**\n * JWT algorithm.\n * @default \"HS256\"\n */\n algorithm?: Algorithm;\n /**\n * Access-token lifetime — a duration string `ms` parses to a **positive**\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\n * first token issue: each of them would otherwise sign a token with no `exp`\n * claim at all.\n * @example \"1h\" or NO_EXPIRATION\n * @default \"1h\"\n */\n expiresIn?: string;\n};\n\n/**\n * Refresh-token configuration.\n */\nexport type RefreshTokenConfig = {\n /**\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\n * Defaults to the package's `RefreshToken`.\n */\n model?: typeof RefreshToken;\n /**\n * Enable refresh tokens.\n * @default true\n */\n enabled?: boolean;\n /**\n * Separate secret for refresh tokens (recommended for security). Falls back\n * to the access-token secret when omitted.\n */\n secret?: string;\n /**\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\n * number of milliseconds; an unusable value throws naming this key on issue\n * (see {@link AccessTokenConfig.expiresIn}).\n * @example \"7d\" or \"1w\"\n * @default \"7d\"\n */\n expiresIn?: string;\n /**\n * Rotate the refresh token on each use (old token revoked, replay detected).\n * @default true\n */\n rotation?: boolean;\n /**\n * Maximum active refresh tokens per user; the oldest are revoked past it.\n * @default 5\n */\n maxPerUser?: number;\n /**\n * Behavior when logout is called without a refresh token.\n * @default \"revoke-all\"\n */\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\n};\n\n/**\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\n * deprecation warning.\n *\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\n */\nexport type LegacyJwtConfig = {\n secret?: string;\n algorithm?: Algorithm;\n expiresIn?: string;\n refresh?: {\n secret?: string;\n enabled?: boolean;\n expiresIn?: string;\n rotation?: boolean;\n maxPerUser?: number;\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\n };\n};\n\n/**\n * Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,\n * `route.isPage`) rejects a logged-out browser, this decides whether it redirects\n * to a login page instead of returning the API-style JSON 401.\n *\n * Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes\n * always keep the JSON 401 contract regardless of this setting.\n */\nexport type PageAuthConfig = {\n /**\n * Login path a guarded page route redirects a logged-out browser to (e.g.\n * `\"/login\"`). Unset ⇒ page-route auth failures keep the JSON 401, so the\n * behavior is opt-in and backward-compatible.\n */\n loginPath?: string;\n /**\n * Query parameter the redirect carries the originally-requested path in, so\n * the login flow can send the user back where they were headed.\n * @default \"returnUrl\"\n */\n returnUrlParam?: string;\n};\n\n/**\n * Data every email-verification / password-reset notification receives.\n */\nexport type OneTimeTokenNotificationData = {\n /** The raw token — the only place it exists; only its SHA-256 hash is stored. */\n token: string;\n /** When the token stops working. */\n expiresAt: Date;\n /** The link built by `auth.verification.url` / `auth.passwordReset.url`, when configured. */\n url?: string;\n};\n\n/**\n * Anything auth can deliver a token through — a `defineNotification(...)`\n * result satisfies it, as does any object with a compatible `send`.\n */\nexport type AuthNotification = {\n send: (to: Auth, data: OneTimeTokenNotificationData) => unknown;\n};\n\n/** Builds the link a token notification points at. */\nexport type OneTimeTokenUrlBuilder = (token: string, user: Auth) => string;\n\n/** Persists a new plaintext password for a user during a password reset. */\nexport type PasswordSetter = (user: Auth, plainPassword: string) => void | Promise<void>;\n\n/**\n * Email-verification configuration (`auth.verification`).\n */\nexport type EmailVerificationConfig = {\n /**\n * Token lifetime — a positive `ms` duration string.\n * @default \"24h\"\n */\n expiresIn?: string;\n /**\n * User attribute stamped with the verification `Date`. Declare it in the\n * user schema (e.g. `emailVerifiedAt: v.date().optional()`).\n * @default \"emailVerifiedAt\"\n */\n field?: string;\n /** Replace the default verification email. */\n notification?: AuthNotification;\n /** Build the verification link passed to the notification as `url`. */\n url?: OneTimeTokenUrlBuilder;\n};\n\n/**\n * Password-reset configuration (`auth.passwordReset`).\n */\nexport type PasswordResetConfig = {\n /**\n * Token lifetime — a positive `ms` duration string.\n * @default \"60m\"\n */\n expiresIn?: string;\n /**\n * User attribute `requestPasswordReset` looks the account up by.\n * @default \"email\"\n */\n identifierField?: string;\n /** Replace the default reset email. */\n notification?: AuthNotification;\n /** Build the reset link passed to the notification as `url`. */\n url?: OneTimeTokenUrlBuilder;\n /**\n * Persist the new password yourself. By default auth saves a hash made by\n * the same `hashPassword` login verifies against, and falls back to the\n * plaintext when the model hashes on save (`useHashedPassword()`), checking\n * the stored value with `verifyPassword` either way.\n */\n setPassword?: PasswordSetter;\n};\n\n/**\n * One-time token storage configuration (`auth.oneTimeToken`).\n */\nexport type OneTimeTokenConfig = {\n /** Override the persisted model — extend {@link OneTimeToken}. */\n model?: typeof OneTimeToken;\n};\n\nexport type AuthConfigurations = {\n /**\n * Define all user types — maps a user-type slug to its `Auth` model class so\n * tokens and the middleware can resolve the right model.\n */\n userType: {\n [userType: string]: ChildModel<Auth>;\n };\n /** Default user type used by `authMiddleware()` and options-only overloads. */\n defaultUserType?: string;\n /**\n * Decide whether a resolved user may authenticate or receive new tokens.\n * @default () => true\n */\n canAuthenticate?: CanAuthenticate;\n /**\n * Access-token configuration (secret, expiry, algorithm, model override).\n */\n accessToken?: AccessTokenConfig;\n /**\n * Refresh-token configuration (rotation, lifetime, cap, model override).\n */\n refreshToken?: RefreshTokenConfig;\n /**\n * Page-route auth-failure behavior — set `loginPath` to redirect a logged-out\n * browser on a guarded PAGE route to a login page instead of the JSON 401.\n * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.\n */\n pageAuth?: PageAuthConfig;\n /**\n * Cookie auth session config — read by `authService.setAuthCookie` /\n * `clearAuthCookie`. Opt-in: unset ⇒ the helpers fall back to their own\n * defaults (`name: \"access_token\"`, `path: \"/\"`).\n */\n cookie?: CookieAuthConfig;\n /**\n * CSRF Origin-check config for cookie-authenticated unsafe-method requests.\n */\n csrf?: CsrfConfig;\n /**\n * Email verification — token lifetime, verified field, notification override.\n */\n verification?: EmailVerificationConfig;\n /**\n * Password reset — token lifetime, lookup field, notification and password-writer overrides.\n */\n passwordReset?: PasswordResetConfig;\n /**\n * One-time token (verification / reset) storage — model override.\n */\n oneTimeToken?: OneTimeTokenConfig;\n /**\n * Redirect login providers (`google`, `custom`) and account-linking policy.\n */\n providers?: ProvidersConfig;\n /**\n * Passkey (WebAuthn) relying-party settings. Requires `@simplewebauthn/server`.\n */\n passkeys?: PasskeysConfig;\n /**\n * Phone one-time-code login — channel, lifetime, attempts, delivery.\n */\n otp?: OtpConfig;\n /**\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\n */\n jwt?: LegacyJwtConfig;\n};\n\nexport type AccessTokenOutput = {\n /**\n * JWT Token\n */\n token: string;\n /**\n * Exprie time in ISO format UTC time\n */\n expiresAt: string;\n};\n\n/**\n * Token pair returned after login or token refresh\n */\nexport type TokenPair = {\n /**\n * JWT access token (short-lived)\n */\n accessToken: AccessTokenOutput;\n /**\n * JWT refresh token (long-lived)\n */\n refreshToken?: AccessTokenOutput;\n};\n\n/**\n * Device information for session tracking\n */\nexport type DeviceInfo = {\n /**\n * User agent string from request\n */\n userAgent?: string;\n /**\n * Client IP address\n */\n ip?: string;\n /**\n * Optional device identifier\n */\n deviceId?: string;\n /**\n * Token family ID (for rotation tracking)\n * @internal\n */\n familyId?: string;\n /**\n * Access token payload\n */\n payload?: Record<string, unknown>;\n};\n\nexport type LoginResult<UserType extends Auth> = {\n user: UserType;\n tokens: TokenPair;\n};\n\n/** Selects a cookie Domain for one request; return `undefined` for a host-only cookie. */\nexport type CookieDomainResolver = (request: Request) => string | undefined;\n\n/**\n * Configuration for the cookie an app opts into via `authService.setAuthCookie`\n * / `clearAuthCookie` and reads back with `authMiddleware([], \"cookie:<name>\")`.\n * Attribute flags (`httpOnly` / `sameSite` / `secure`) are deliberately NOT\n * configurable here — they come from core's `secureCookieDefaults()`, the\n * same floor every `response.cookie()` call gets, so a cookie auth session can\n * never be weakened to a laxer policy than the framework default.\n */\nexport type CookieAuthConfig = {\n /**\n * Cookie name `setAuthCookie`/`clearAuthCookie` write and clear, and that a\n * `cookie:<name>` token source must match to read it back.\n * @default \"access_token\"\n */\n name?: string;\n /**\n * Cookie name the session helpers write and clear for the refresh token.\n * @default \"refresh_token\"\n */\n refreshName?: string;\n /**\n * Cookie `Path` attribute.\n * @default \"/\"\n */\n path?: string;\n /** Select a request-scoped Domain; `undefined` keeps the cookie host-only. */\n cookieDomain?: CookieDomainResolver;\n};\n\n/**\n * CSRF Origin-check configuration. Read via\n * `authConfig.csrf.allowedOrigins()`.\n */\nexport type CsrfConfig = {\n /**\n * Extra origins allowed on a cookie-authenticated unsafe-method request,\n * in addition to the request's own origin.\n * @default []\n */\n allowedOrigins?: string[];\n};\n\n/**\n * Per-call overrides for {@link AuthService.setAuthCookie}. Anything omitted\n * falls back to `auth.cookie.*` config, then the package default. Cookie\n * *attribute* flags (`httpOnly`/`sameSite`/`secure`) are never settable here —\n * see {@link CookieAuthConfig}.\n */\nexport type SetAuthCookieOptions = {\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\n name?: string;\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). */\n path?: string;\n /** Cookie `Domain` attribute. Omit for a host-only cookie. */\n domain?: string;\n /**\n * `Max-Age`, in seconds. Overrides the expiry `setAuthCookie` would\n * otherwise derive from an `AccessTokenOutput`'s `expiresAt`. Passing a\n * bare token string with no `maxAge` produces a session cookie (cleared\n * when the browser closes).\n */\n maxAge?: number;\n};\n\n/** Per-call overrides for {@link AuthService.clearAuthCookie}. */\nexport type ClearAuthCookieOptions = {\n /** Cookie name; defaults to `auth.cookie.name` (package default `\"access_token\"`). */\n name?: string;\n /** Cookie `Path`; defaults to `auth.cookie.path` (package default `\"/\"`). Must match the `Path` the cookie was set with. */\n path?: string;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,gBAAgB"}
|
package/esm/index.d.mts
CHANGED
|
@@ -5,7 +5,7 @@ import { Auth } from "./models/auth.model.mjs";
|
|
|
5
5
|
import { AccessToken, accessTokenSchema } from "./models/access-token/access-token.model.mjs";
|
|
6
6
|
import { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema } from "./models/one-time-token/one-time-token.model.mjs";
|
|
7
7
|
import { AppleProviderConfig, AuthProvider, DiscordProviderConfig, FacebookProviderConfig, GitHubProviderConfig, GoogleProviderConfig, LinkedInProviderConfig, OtpConfig, OtpMessage, OtpSender, PasskeysConfig, ProviderAuthorizationState, ProviderCallbackParams, ProviderProfile, ProviderUserCreator, ProvidersConfig, XProviderConfig } from "./contracts/providers.mjs";
|
|
8
|
-
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./contracts/types.mjs";
|
|
8
|
+
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthMiddlewareOptions, AuthNotification, AuthRefreshCredentialDescriptor, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CookieDomainResolver, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./contracts/types.mjs";
|
|
9
9
|
import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
|
|
10
10
|
import { Authenticable } from "./contracts/auth-contract.mjs";
|
|
11
11
|
import { RequestUser } from "./contracts/request-locals.mjs";
|
|
@@ -43,7 +43,7 @@ import { LINKEDIN_AUTH_FEATURE, LinkedInProvider } from "./providers/linkedin-pr
|
|
|
43
43
|
import { completeProviderLogin, resolveAuthProvider, startProviderLogin } from "./providers/provider-login.mjs";
|
|
44
44
|
import { XProvider } from "./providers/x-provider.mjs";
|
|
45
45
|
import { AuthEventCallback, AuthEventName, AuthEventPayloads, authEvents } from "./services/auth-events.mjs";
|
|
46
|
-
import { authService } from "./services/auth.service.mjs";
|
|
46
|
+
import { CookieDomainRequestRequiredError, authService } from "./services/auth.service.mjs";
|
|
47
47
|
import { currentUser } from "./services/current-user.mjs";
|
|
48
48
|
import { generateJWTSecret } from "./services/generate-jwt-secret.mjs";
|
|
49
49
|
import { TokenType, TokenTypeError, isInvalidCredentialError, jwt } from "./services/jwt.mjs";
|
|
@@ -54,5 +54,5 @@ import { RequestUserFailure, RequestUserOutcome, ResolveRequestUserOptions, read
|
|
|
54
54
|
import { PageSessionOptions, PageSessionResolver, SessionMiddlewareOptions, SessionResolverOptions, SessionSource, pageSession, sessionMiddleware } from "./session/page-session.mjs";
|
|
55
55
|
import { safeEqual } from "./utils/safe-equal.mjs";
|
|
56
56
|
import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
|
|
57
|
-
export { APPLE_AUTH_FEATURE, AccessToken, AccessTokenConfig, AccessTokenOutput, AppleJoseModule, AppleProvider, AppleProviderConfig, Auth, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthErrorCodes, AuthErrorTranslations, AuthEventCallback, AuthEventName, AuthEventPayloads, AuthMiddlewareOptions, AuthNotification, AuthProvider, AuthProviderSdkMissingError, AuthRefreshCredentialDescriptor, AuthTokenFamily, AuthTranslations, Authenticable, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, CsrfOriginMismatchError, CsrfOriginMismatchReason, DeviceInfo, DiscordProvider, DiscordProviderConfig, EmailNotVerifiedError, EmailVerificationConfig, FacebookProvider, FacebookProviderConfig, GOOGLE_AUTH_FEATURE, GitHubProvider, GitHubProviderConfig, GoogleProvider, GoogleProviderConfig, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, IssuedOneTimeToken, JoseModule, LINKEDIN_AUTH_FEATURE, LegacyJwtConfig, LinkedInProvider, LinkedInProviderConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenPurpose, OneTimeTokenUrlBuilder, OtpConfig, OtpMessage, OtpSender, PageAuthConfig, PageSessionOptions, PageSessionResolver, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, RequestUserFailure, RequestUserOutcome, ResolveRequestUserOptions, SessionMiddlewareOptions, SessionResolverOptions, SessionSource, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, XProvider, XProviderConfig, accessTokenSchema, assertCsrfOriginAllowed, authDefaultTranslations, authEvents, authMiddleware, authMigrations, authService, authTokenFamilySchema, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, pageSession, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, readCredential, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, resolveRequestUser, resolveRequestUserOutcome, safeEqual, sendEmailVerification, sessionMiddleware, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
57
|
+
export { APPLE_AUTH_FEATURE, AccessToken, AccessTokenConfig, AccessTokenOutput, AppleJoseModule, AppleProvider, AppleProviderConfig, Auth, AuthConfigurations, AuthCredentialDescriptor, AuthCredentials, AuthErrorCodes, AuthErrorTranslations, AuthEventCallback, AuthEventName, AuthEventPayloads, AuthMiddlewareOptions, AuthNotification, AuthProvider, AuthProviderSdkMissingError, AuthRefreshCredentialDescriptor, AuthTokenFamily, AuthTranslations, Authenticable, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CookieDomainRequestRequiredError, CookieDomainResolver, CsrfConfig, CsrfOriginMismatchError, CsrfOriginMismatchReason, DeviceInfo, DiscordProvider, DiscordProviderConfig, EmailNotVerifiedError, EmailVerificationConfig, FacebookProvider, FacebookProviderConfig, GOOGLE_AUTH_FEATURE, GitHubProvider, GitHubProviderConfig, GoogleProvider, GoogleProviderConfig, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, IssuedOneTimeToken, JoseModule, LINKEDIN_AUTH_FEATURE, LegacyJwtConfig, LinkedInProvider, LinkedInProviderConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenPurpose, OneTimeTokenUrlBuilder, OtpConfig, OtpMessage, OtpSender, PageAuthConfig, PageSessionOptions, PageSessionResolver, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, RequestUserFailure, RequestUserOutcome, ResolveRequestUserOptions, SessionMiddlewareOptions, SessionResolverOptions, SessionSource, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, XProvider, XProviderConfig, accessTokenSchema, assertCsrfOriginAllowed, authDefaultTranslations, authEvents, authMiddleware, authMigrations, authService, authTokenFamilySchema, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, pageSession, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, readCredential, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, resolveRequestUser, resolveRequestUserOutcome, safeEqual, sendEmailVerification, sessionMiddleware, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
58
58
|
import "./contracts/request-locals.mjs";
|
package/esm/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { OneTimeToken, oneTimeTokenSchema } from "./models/one-time-token/one-ti
|
|
|
10
10
|
import { RefreshToken, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
|
|
11
11
|
import { authEvents } from "./services/auth-events.mjs";
|
|
12
12
|
import { TokenTypeError, isInvalidCredentialError, jwt } from "./services/jwt.mjs";
|
|
13
|
-
import { authService } from "./services/auth.service.mjs";
|
|
13
|
+
import { CookieDomainRequestRequiredError, authService } from "./services/auth.service.mjs";
|
|
14
14
|
import { registerAuthCleanupCommand } from "./commands/auth-cleanup-command.mjs";
|
|
15
15
|
import { registerAuthPurgeNeverExpiringCommand } from "./commands/auth-purge-never-expiring-command.mjs";
|
|
16
16
|
import { generateJWTSecret } from "./services/generate-jwt-secret.mjs";
|
|
@@ -60,4 +60,4 @@ import { pageSession, sessionMiddleware } from "./session/page-session.mjs";
|
|
|
60
60
|
import "./session/index.mjs";
|
|
61
61
|
import "./utils/index.mjs";
|
|
62
62
|
|
|
63
|
-
export { APPLE_AUTH_FEATURE, AccessToken, AppleProvider, Auth, AuthErrorCodes, AuthProviderSdkMissingError, AuthTokenFamily, CsrfOriginMismatchError, DiscordProvider, EmailNotVerifiedError, FacebookProvider, GOOGLE_AUTH_FEATURE, GitHubProvider, GoogleProvider, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, LINKEDIN_AUTH_FEATURE, LinkedInProvider, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, PasskeyCredential, ProviderAccount, ProviderEmailNotVerifiedError, RefreshToken, TokenTypeError, XProvider, accessTokenSchema, assertCsrfOriginAllowed, authDefaultTranslations, authEvents, authMiddleware, authMigrations, authService, authTokenFamilySchema, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, pageSession, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, readCredential, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, resolveRequestUser, resolveRequestUserOutcome, safeEqual, sendEmailVerification, sessionMiddleware, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
63
|
+
export { APPLE_AUTH_FEATURE, AccessToken, AppleProvider, Auth, AuthErrorCodes, AuthProviderSdkMissingError, AuthTokenFamily, CookieDomainRequestRequiredError, CsrfOriginMismatchError, DiscordProvider, EmailNotVerifiedError, FacebookProvider, GOOGLE_AUTH_FEATURE, GitHubProvider, GoogleProvider, InvalidOneTimeTokenError, InvalidPasskeyError, InvalidProviderCallbackError, LINKEDIN_AUTH_FEATURE, LinkedInProvider, NO_EXPIRATION, NotificationsUnavailableError, OneTimeToken, PasskeyCredential, ProviderAccount, ProviderEmailNotVerifiedError, RefreshToken, TokenTypeError, XProvider, accessTokenSchema, assertCsrfOriginAllowed, authDefaultTranslations, authEvents, authMiddleware, authMigrations, authService, authTokenFamilySchema, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, pageSession, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, readCredential, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, resolveRequestUser, resolveRequestUserOutcome, safeEqual, sendEmailVerification, sessionMiddleware, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
|
|
@@ -72,7 +72,7 @@ function authMiddleware(userTypeOrOptions, legacyTokenFromOrOptions) {
|
|
|
72
72
|
const headerWins = dualHeader && Boolean(request.authorizationValue);
|
|
73
73
|
const usedTokenFrom = headerWins ? "header" : tokenFrom;
|
|
74
74
|
const usedRefresh = headerWins ? void 0 : refreshCredential;
|
|
75
|
-
if ((readCredential(request, usedTokenFrom) || usedRefresh) && requiresCsrfOriginCheck(usedTokenFrom, request.method)) try {
|
|
75
|
+
if ((readCredential(request, usedTokenFrom) || usedRefresh && readCredential(request, usedRefresh)) && requiresCsrfOriginCheck(usedTokenFrom, request.method)) try {
|
|
76
76
|
assertCsrfOriginAllowed(request);
|
|
77
77
|
} catch (error) {
|
|
78
78
|
if (!(error instanceof CsrfOriginMismatchError)) throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, type HttpContext, t, type Middleware } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type {\r\n AuthCredentialDescriptor,\r\n AuthMiddlewareOptions,\r\n TokenFrom,\r\n} from \"../contracts/types\";\r\nimport { authConfig } from \"../services/auth-config\";\r\nimport { readCredential, resolveRequestUserOutcome } from \"../services/resolve-request-user\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\nimport {\r\n assertCsrfOriginAllowed,\r\n CsrfOriginMismatchError,\r\n requiresCsrfOriginCheck,\r\n} from \"./csrf-origin-check\";\r\nimport { localizedLoginPath } from \"./localized-login-path\";\r\n\r\n/** The 401 body shape every rejection in this middleware carries. */\r\ntype UnauthorizedPayload = { error: string; errorCode: string };\r\n\r\n/**\r\n * Reject an unauthenticated request, choosing the representation by ROUTE KIND.\r\n *\r\n * An API route always gets the JSON 401 it has always gotten. A PAGE route\r\n * (`request.route.isPage` — the React-SSR pages) instead redirects a logged-out\r\n * browser to the configured login path with a `returnUrl`, so a human\r\n * navigating to a guarded page lands on the login screen rather than reading a\r\n * raw JSON error blob — the defect in finding b9ab9804.\r\n *\r\n * The redirect is OPT-IN and backward-compatible: with no `auth.pageAuth.loginPath`\r\n * configured, even a page route falls back to the JSON 401, so an app that has\r\n * not asked for page-login UX keeps exactly its current behavior. The API 401\r\n * contract is never changed for API routes.\r\n *\r\n * `returnUrl` carries `request.url` — the relative path + query the browser was\r\n * on — never the absolute URL: a relative target cannot be turned into an\r\n * open-redirect off this app's origin.\r\n */\r\nfunction rejectUnauthorized(\r\n { request, response }: HttpContext,\r\n payload: UnauthorizedPayload,\r\n redirect?: AuthMiddlewareOptions[\"redirect\"],\r\n) {\r\n const loginPath = redirect?.to ?? authConfig.pageAuth.loginPath();\r\n const returnUrlParam = redirect?.returnUrlParam ?? authConfig.pageAuth.returnUrlParam();\r\n\r\n if (loginPath && request.route?.isPage) {\r\n // Under an active `web.localeRouting` strategy, the login destination\r\n // itself must carry the request's locale prefix — otherwise an anonymous\r\n // `/ar/admin` bounces to the DEFAULT locale's `/login` rather than\r\n // `/ar/login`, even though `returnUrl` below correctly points back at\r\n // `/ar/admin`. See `./localized-login-path.ts`.\r\n const destination = localizedLoginPath(loginPath, request.locale);\r\n const separator = destination.includes(\"?\") ? \"&\" : \"?\";\r\n const returnUrl = encodeURIComponent(request.url);\r\n\r\n return response.redirect(`${destination}${separator}${returnUrlParam}=${returnUrl}`);\r\n }\r\n\r\n return response.unauthorized(payload);\r\n}\r\n\r\nfunction tokenFromDescriptor(descriptor: Partial<AuthCredentialDescriptor>): TokenFrom {\r\n if (!descriptor.source || descriptor.source === \"header\") return \"header\";\r\n\r\n if (!descriptor.key) {\r\n throw new Error(\"authMiddleware cookie credentials require a cookie key.\");\r\n }\r\n\r\n return `cookie:${descriptor.key}`;\r\n}\r\n\r\nfunction resolveAllowedTypes(value: string | string[] | undefined): string[] {\r\n if (value !== undefined) return Array.isArray(value) ? value : [value];\r\n\r\n const configuredDefault = config.key(\"auth.defaultUserType\") as string | undefined;\r\n\r\n if (configuredDefault) return [configuredDefault];\r\n\r\n const userTypes = config.key(\"auth.userType\", {}) as Record<string, unknown>;\r\n const configuredTypes = Object.keys(userTypes);\r\n\r\n if (configuredTypes.length === 1) return configuredTypes;\r\n\r\n throw new Error(\r\n \"authMiddleware requires a user type when auth.defaultUserType is unset and multiple user types exist.\",\r\n );\r\n}\r\n\r\n/**\r\n * Build a route gate. Authentication is required unless optional is explicit.\r\n *\r\n * An omitted user type selects the configured default; an explicit argument\r\n * selects which user types may pass:\r\n * - `[]` — any authenticated user (token required, type not checked).\r\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\r\n * `userType` must be one of the listed types.\r\n *\r\n * `{ optional: true }` allows anonymous requests. All other forms reject\r\n * missing or invalid credentials with 401, or a configured page redirect.\r\n *\r\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\r\n * A single source avoids making credential precedence depend on array order.\r\n *\r\n * @example\r\n * router.get(\"/account\", authMiddleware([]), accountController);\r\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\r\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\r\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\r\n */\r\nexport function authMiddleware(): Middleware;\r\nexport function authMiddleware(options: AuthMiddlewareOptions): Middleware;\r\nexport function authMiddleware(\r\n allowedUserType: string | string[],\r\n options?: AuthMiddlewareOptions,\r\n): Middleware;\r\nexport function authMiddleware(\r\n allowedUserType: string | string[],\r\n tokenFrom?: TokenFrom,\r\n): Middleware;\r\nexport function authMiddleware(\r\n userTypeOrOptions?: string | string[] | AuthMiddlewareOptions,\r\n legacyTokenFromOrOptions?: TokenFrom | AuthMiddlewareOptions,\r\n): Middleware {\r\n const options =\r\n typeof userTypeOrOptions === \"object\" && !Array.isArray(userTypeOrOptions)\r\n ? userTypeOrOptions\r\n : typeof legacyTokenFromOrOptions === \"object\"\r\n ? legacyTokenFromOrOptions\r\n : undefined;\r\n const selectedUserType =\r\n typeof userTypeOrOptions === \"string\" || Array.isArray(userTypeOrOptions)\r\n ? userTypeOrOptions\r\n : undefined;\r\n const allowedTypes = resolveAllowedTypes(selectedUserType);\r\n const legacyTokenFrom =\r\n typeof legacyTokenFromOrOptions === \"string\" ? legacyTokenFromOrOptions : undefined;\r\n const dualSources = options?.sources?.length ? options.sources.map(tokenFromDescriptor) : undefined;\r\n\r\n if (dualSources && options?.source) {\r\n throw new Error(\"authMiddleware accepts either `source` or `sources`, not both.\");\r\n }\r\n\r\n const dualHeader = dualSources?.includes(\"header\") ?? false;\r\n const dualCookies = dualSources?.filter(source => source !== \"header\") ?? [];\r\n\r\n if (dualSources && (dualCookies.length > 1 || dualSources.length - dualCookies.length > 1)) {\r\n throw new Error(\"authMiddleware `sources` accepts at most one header and one cookie.\");\r\n }\r\n\r\n // With dual sources the configured cookie is the base credential; the header\r\n // is chosen per request, ahead of it.\r\n const cookieTokenFrom: TokenFrom = dualSources\r\n ? (dualCookies[0] ?? \"header\")\r\n : options\r\n ? tokenFromDescriptor(options)\r\n : (legacyTokenFrom ?? \"header\");\r\n const tokenFrom = cookieTokenFrom;\r\n const refreshCredential = options?.refresh ? tokenFromDescriptor(options.refresh) : undefined;\r\n const renewalUserType = allowedTypes.length === 1 ? allowedTypes[0] : undefined;\r\n\r\n if (\r\n refreshCredential &&\r\n (tokenFrom === \"header\" || refreshCredential === \"header\" || !renewalUserType)\r\n ) {\r\n throw new Error(\r\n \"authMiddleware automatic renewal requires one allowed user type and cookie access and refresh credentials.\",\r\n );\r\n }\r\n\r\n const optional = options?.optional === true;\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const reject = (payload: UnauthorizedPayload) =>\r\n rejectUnauthorized({ request, response }, payload, options?.redirect);\r\n\r\n // A present Authorization header always wins and never falls back to the\r\n // cookie: the credential used is the header, so no renewal and no CSRF check.\r\n const headerWins = dualHeader && Boolean(request.authorizationValue);\r\n const usedTokenFrom: TokenFrom = headerWins ? \"header\" : tokenFrom;\r\n const usedRefresh = headerWins ? undefined : refreshCredential;\r\n\r\n // Renewal can mint cookies, so a cookie-authenticated unsafe request must\r\n // pass the same CSRF check even when its access cookie is missing.\r\n if (\r\n (readCredential(request, usedTokenFrom) || usedRefresh) &&\r\n requiresCsrfOriginCheck(usedTokenFrom, request.method)\r\n ) {\r\n try {\r\n assertCsrfOriginAllowed(request);\r\n } catch (error) {\r\n if (!(error instanceof CsrfOriginMismatchError)) throw error;\r\n\r\n log.error(\"http\", \"auth\", error);\r\n return response.forbidden({\r\n error: t(\"auth.errors.csrfOriginMismatch\"),\r\n errorCode: AuthErrorCodes.CsrfOriginMismatch,\r\n });\r\n }\r\n }\r\n\r\n const outcome = await resolveRequestUserOutcome(request, response, {\r\n tokenFrom: usedTokenFrom,\r\n refreshCredential: usedRefresh,\r\n overlapMs: options?.refresh?.overlapMs,\r\n allowedTypes,\r\n renewalUserType,\r\n });\r\n\r\n if (outcome.user) {\r\n request.locals.user = outcome.user;\r\n return;\r\n }\r\n\r\n if (outcome.failure === \"forbidden\") {\r\n return response.forbidden({\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n if (optional) return;\r\n\r\n if (outcome.failure === \"missing\") {\r\n return reject({\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n if (outcome.failure === \"unauthorized\") {\r\n return reject({\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n return reject({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, type HttpContext, t, type Middleware } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport type {\n AuthCredentialDescriptor,\n AuthMiddlewareOptions,\n TokenFrom,\n} from \"../contracts/types\";\nimport { authConfig } from \"../services/auth-config\";\nimport { readCredential, resolveRequestUserOutcome } from \"../services/resolve-request-user\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\nimport {\n assertCsrfOriginAllowed,\n CsrfOriginMismatchError,\n requiresCsrfOriginCheck,\n} from \"./csrf-origin-check\";\nimport { localizedLoginPath } from \"./localized-login-path\";\n\n/** The 401 body shape every rejection in this middleware carries. */\ntype UnauthorizedPayload = { error: string; errorCode: string };\n\n/**\n * Reject an unauthenticated request, choosing the representation by ROUTE KIND.\n *\n * An API route always gets the JSON 401 it has always gotten. A PAGE route\n * (`request.route.isPage` — the React-SSR pages) instead redirects a logged-out\n * browser to the configured login path with a `returnUrl`, so a human\n * navigating to a guarded page lands on the login screen rather than reading a\n * raw JSON error blob — the defect in finding b9ab9804.\n *\n * The redirect is OPT-IN and backward-compatible: with no `auth.pageAuth.loginPath`\n * configured, even a page route falls back to the JSON 401, so an app that has\n * not asked for page-login UX keeps exactly its current behavior. The API 401\n * contract is never changed for API routes.\n *\n * `returnUrl` carries `request.url` — the relative path + query the browser was\n * on — never the absolute URL: a relative target cannot be turned into an\n * open-redirect off this app's origin.\n */\nfunction rejectUnauthorized(\n { request, response }: HttpContext,\n payload: UnauthorizedPayload,\n redirect?: AuthMiddlewareOptions[\"redirect\"],\n) {\n const loginPath = redirect?.to ?? authConfig.pageAuth.loginPath();\n const returnUrlParam = redirect?.returnUrlParam ?? authConfig.pageAuth.returnUrlParam();\n\n if (loginPath && request.route?.isPage) {\n // Under an active `web.localeRouting` strategy, the login destination\n // itself must carry the request's locale prefix — otherwise an anonymous\n // `/ar/admin` bounces to the DEFAULT locale's `/login` rather than\n // `/ar/login`, even though `returnUrl` below correctly points back at\n // `/ar/admin`. See `./localized-login-path.ts`.\n const destination = localizedLoginPath(loginPath, request.locale);\n const separator = destination.includes(\"?\") ? \"&\" : \"?\";\n const returnUrl = encodeURIComponent(request.url);\n\n return response.redirect(`${destination}${separator}${returnUrlParam}=${returnUrl}`);\n }\n\n return response.unauthorized(payload);\n}\n\nfunction tokenFromDescriptor(descriptor: Partial<AuthCredentialDescriptor>): TokenFrom {\n if (!descriptor.source || descriptor.source === \"header\") return \"header\";\n\n if (!descriptor.key) {\n throw new Error(\"authMiddleware cookie credentials require a cookie key.\");\n }\n\n return `cookie:${descriptor.key}`;\n}\n\nfunction resolveAllowedTypes(value: string | string[] | undefined): string[] {\n if (value !== undefined) return Array.isArray(value) ? value : [value];\n\n const configuredDefault = config.key(\"auth.defaultUserType\") as string | undefined;\n\n if (configuredDefault) return [configuredDefault];\n\n const userTypes = config.key(\"auth.userType\", {}) as Record<string, unknown>;\n const configuredTypes = Object.keys(userTypes);\n\n if (configuredTypes.length === 1) return configuredTypes;\n\n throw new Error(\n \"authMiddleware requires a user type when auth.defaultUserType is unset and multiple user types exist.\",\n );\n}\n\n/**\n * Build a route gate. Authentication is required unless optional is explicit.\n *\n * An omitted user type selects the configured default; an explicit argument\n * selects which user types may pass:\n * - `[]` — any authenticated user (token required, type not checked).\n * - `\"admin\"` / `[\"admin\", \"staff\"]` — token required AND the user's\n * `userType` must be one of the listed types.\n *\n * `{ optional: true }` allows anonymous requests. All other forms reject\n * missing or invalid credentials with 401, or a configured page redirect.\n *\n * `tokenFrom` selects one credential source and defaults to `\"header\"`.\n * A single source avoids making credential precedence depend on array order.\n *\n * @example\n * router.get(\"/account\", authMiddleware([]), accountController);\n * router.get(\"/admin\", authMiddleware(\"admin\"), adminController);\n * router.get(\"/back-office\", authMiddleware([\"admin\", \"staff\"]), backOfficeController);\n * router.get(\"/browser-account\", authMiddleware([], \"cookie:token\"), accountController);\n */\nexport function authMiddleware(): Middleware;\nexport function authMiddleware(options: AuthMiddlewareOptions): Middleware;\nexport function authMiddleware(\n allowedUserType: string | string[],\n options?: AuthMiddlewareOptions,\n): Middleware;\nexport function authMiddleware(\n allowedUserType: string | string[],\n tokenFrom?: TokenFrom,\n): Middleware;\nexport function authMiddleware(\n userTypeOrOptions?: string | string[] | AuthMiddlewareOptions,\n legacyTokenFromOrOptions?: TokenFrom | AuthMiddlewareOptions,\n): Middleware {\n const options =\n typeof userTypeOrOptions === \"object\" && !Array.isArray(userTypeOrOptions)\n ? userTypeOrOptions\n : typeof legacyTokenFromOrOptions === \"object\"\n ? legacyTokenFromOrOptions\n : undefined;\n const selectedUserType =\n typeof userTypeOrOptions === \"string\" || Array.isArray(userTypeOrOptions)\n ? userTypeOrOptions\n : undefined;\n const allowedTypes = resolveAllowedTypes(selectedUserType);\n const legacyTokenFrom =\n typeof legacyTokenFromOrOptions === \"string\" ? legacyTokenFromOrOptions : undefined;\n const dualSources = options?.sources?.length ? options.sources.map(tokenFromDescriptor) : undefined;\n\n if (dualSources && options?.source) {\n throw new Error(\"authMiddleware accepts either `source` or `sources`, not both.\");\n }\n\n const dualHeader = dualSources?.includes(\"header\") ?? false;\n const dualCookies = dualSources?.filter(source => source !== \"header\") ?? [];\n\n if (dualSources && (dualCookies.length > 1 || dualSources.length - dualCookies.length > 1)) {\n throw new Error(\"authMiddleware `sources` accepts at most one header and one cookie.\");\n }\n\n // With dual sources the configured cookie is the base credential; the header\n // is chosen per request, ahead of it.\n const cookieTokenFrom: TokenFrom = dualSources\n ? (dualCookies[0] ?? \"header\")\n : options\n ? tokenFromDescriptor(options)\n : (legacyTokenFrom ?? \"header\");\n const tokenFrom = cookieTokenFrom;\n const refreshCredential = options?.refresh ? tokenFromDescriptor(options.refresh) : undefined;\n const renewalUserType = allowedTypes.length === 1 ? allowedTypes[0] : undefined;\n\n if (\n refreshCredential &&\n (tokenFrom === \"header\" || refreshCredential === \"header\" || !renewalUserType)\n ) {\n throw new Error(\n \"authMiddleware automatic renewal requires one allowed user type and cookie access and refresh credentials.\",\n );\n }\n\n const optional = options?.optional === true;\n\n const auth: Middleware = async ({ request, response }) => {\n const reject = (payload: UnauthorizedPayload) =>\n rejectUnauthorized({ request, response }, payload, options?.redirect);\n\n // A present Authorization header always wins and never falls back to the\n // cookie: the credential used is the header, so no renewal and no CSRF check.\n const headerWins = dualHeader && Boolean(request.authorizationValue);\n const usedTokenFrom: TokenFrom = headerWins ? \"header\" : tokenFrom;\n const usedRefresh = headerWins ? undefined : refreshCredential;\n\n // Renewal can mint cookies, so a cookie-authenticated unsafe request must\n // pass the same CSRF check even when its access cookie is missing.\n if (\n (readCredential(request, usedTokenFrom) ||\n (usedRefresh && readCredential(request, usedRefresh))) &&\n requiresCsrfOriginCheck(usedTokenFrom, request.method)\n ) {\n try {\n assertCsrfOriginAllowed(request);\n } catch (error) {\n if (!(error instanceof CsrfOriginMismatchError)) throw error;\n\n log.error(\"http\", \"auth\", error);\n return response.forbidden({\n error: t(\"auth.errors.csrfOriginMismatch\"),\n errorCode: AuthErrorCodes.CsrfOriginMismatch,\n });\n }\n }\n\n const outcome = await resolveRequestUserOutcome(request, response, {\n tokenFrom: usedTokenFrom,\n refreshCredential: usedRefresh,\n overlapMs: options?.refresh?.overlapMs,\n allowedTypes,\n renewalUserType,\n });\n\n if (outcome.user) {\n request.locals.user = outcome.user;\n return;\n }\n\n if (outcome.failure === \"forbidden\") {\n return response.forbidden({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n }\n\n if (optional) return;\n\n if (outcome.failure === \"missing\") {\n return reject({\n error: t(\"auth.errors.missingAccessToken\"),\n errorCode: AuthErrorCodes.MissingAccessToken,\n });\n }\n\n if (outcome.failure === \"unauthorized\") {\n return reject({\n error: t(\"auth.errors.unauthorized\"),\n errorCode: AuthErrorCodes.Unauthorized,\n });\n }\n\n return reject({\n error: t(\"auth.errors.invalidAccessToken\"),\n errorCode: AuthErrorCodes.InvalidAccessToken,\n });\n };\n\n return auth;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\r\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\r\n\r\n/**\r\n * Built-in identifier kinds the throttle knows how to extract from a request.\r\n *\r\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\r\n * many IPs.\r\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\r\n * one IP.\r\n *\r\n * Lockout trips when ANY tracked identifier crosses `max` — defense-in-depth.\r\n */\r\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\r\n\r\n/**\r\n * Options for {@link loginThrottleMiddleware}.\r\n *\r\n * Failure-aware by design: only counts requests whose response is non-2xx\r\n * (or whatever `isFailure` says) and resets on success, so a user who types\r\n * a wrong password three times then the right one is not penalised.\r\n *\r\n * @example\r\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\r\n */\r\nexport type LoginThrottleOptions = {\r\n /**\r\n * Failures allowed within the window before the identifier is locked.\r\n *\r\n * @default 5\r\n */\r\n max?: number;\r\n\r\n /**\r\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\r\n *\r\n * @default \"15m\"\r\n */\r\n window?: string | number;\r\n\r\n /**\r\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\r\n *\r\n * @default \"15m\"\r\n */\r\n lockoutDuration?: string | number;\r\n\r\n /**\r\n * Identifiers tracked. Each is counted and locked independently.\r\n *\r\n * @default [\"email\", \"ip\"]\r\n */\r\n by?: LoginThrottleIdentifier[];\r\n\r\n /**\r\n * Credential field consulted when `\"email\"` is tracked. Switch to\r\n * `\"username\"` (or any other input field) for systems that don't\r\n * authenticate by email.\r\n *\r\n * @default \"email\"\r\n */\r\n identifierKey?: string;\r\n\r\n /**\r\n * Override the default error message returned with the 429 response.\r\n */\r\n errorMessage?: string;\r\n\r\n /**\r\n * Decide whether the response represents a failed login. Defaults to\r\n * \"non-2xx is a failure\"; override if your controller signals failure\r\n * through a 200-style body.\r\n *\r\n * @default (response) => !response.isOk\r\n */\r\n isFailure?: (response: Response) => boolean;\r\n\r\n /**\r\n * Compute the identifiers tracked for this request. Override to derive the\r\n * account key from a header, a JWT subject, or any other shape that does\r\n * not match the built-in `email` / `ip` extraction.\r\n */\r\n identify?: (request: Request) => string[];\r\n};\r\n\r\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\r\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\r\n\r\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\r\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\r\n\r\n/**\r\n * Built-in identifier extractor — reads the configured credential field from\r\n * the request body for the per-account key, and `request.detectIp()` for the\r\n * per-source key. Untruthy values are dropped; a request that yields zero\r\n * identifiers is left untracked so the middleware no-ops for it.\r\n */\r\nfunction buildDefaultIdentifiers(\r\n request: Request,\r\n tracked: LoginThrottleIdentifier[],\r\n credentialField: string,\r\n): string[] {\r\n const identifiers: string[] = [];\r\n\r\n if (tracked.includes(\"email\")) {\r\n const value = request.input(credentialField);\r\n\r\n if (typeof value === \"string\" && value.length > 0) {\r\n identifiers.push(`email.${value.toLowerCase()}`);\r\n }\r\n }\r\n\r\n if (tracked.includes(\"ip\")) {\r\n const ip = request.detectIp();\r\n\r\n if (ip) {\r\n identifiers.push(`ip.${ip}`);\r\n }\r\n }\r\n\r\n return identifiers;\r\n}\r\n\r\n/**\r\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\r\n * after too many failed login attempts within a window — without penalising\r\n * a user who eventually types the right password.\r\n *\r\n * Mechanism (per identifier):\r\n *\r\n * 1. **Before the controller** — reject with `429` if a lock key is set,\r\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\r\n * CPU-DoS angle of brute-forcing).\r\n * 2. **After the controller** — observe the outcome via `response.onSent`.\r\n * On failure (non-2xx by default), bump a fixed-window counter; once it\r\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\r\n * clear both the counter and the lock for that identifier.\r\n *\r\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\r\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\r\n * across replicas — correct security model, because an attacker can't bypass\r\n * by round-robin to a different pod. Every cache call is guarded so a\r\n * misconfigured driver fails open (a throttle outage must never escalate\r\n * into a full auth outage).\r\n *\r\n * @example\r\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\r\n * import { router } from \"@warlock.js/core\";\r\n *\r\n * router.post(\"/auth/login\", loginController, {\r\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\r\n * });\r\n *\r\n * @example\r\n * // Per-IP only — avoid account-lockout-as-DoS on anonymous-attack vectors\r\n * router.post(\"/auth/refresh-token\", refreshController, {\r\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\r\n * });\r\n */\r\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\r\n const max = options.max ?? 5;\r\n const window = options.window ?? \"15m\";\r\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\r\n const tracked = options.by ?? [\"email\", \"ip\"];\r\n const credentialField = options.identifierKey ?? \"email\";\r\n\r\n return async ({ request, response }) => {\r\n const identifiers = options.identify\r\n ? options.identify(request)\r\n : buildDefaultIdentifiers(request, tracked, credentialField);\r\n\r\n if (identifiers.length === 0) return;\r\n\r\n try {\r\n for (const identifier of identifiers) {\r\n const locked = await cache.get(lockKey(identifier));\r\n\r\n if (locked) {\r\n return response.tooManyRequests({\r\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\r\n errorCode: AuthErrorCodes.TooManyAttempts,\r\n });\r\n }\r\n }\r\n } catch (error) {\r\n // Fail open — a cache outage (driver not initialised, network blip)\r\n // must never escalate into a full auth outage. Log and let the request\r\n // through; the onSent recorder below is independently guarded so an\r\n // outage there is also harmless.\r\n log.error(\"auth\", \"login-throttle\", error);\r\n\r\n return;\r\n }\r\n\r\n response.onSent(async (resolvedResponse: Response) => {\r\n try {\r\n const failed = options.isFailure\r\n ? options.isFailure(resolvedResponse)\r\n : !resolvedResponse.isOk;\r\n\r\n if (!failed) {\r\n for (const identifier of identifiers) {\r\n await cache.remove(counterKey(identifier));\r\n await cache.remove(lockKey(identifier));\r\n }\r\n\r\n return;\r\n }\r\n\r\n for (const identifier of identifiers) {\r\n const key = counterKey(identifier);\r\n\r\n // Open the window on the first failure: the create-only write means\r\n // only one of several racing servers sets the TTL, and an existing\r\n // window keeps its deadline (fixed window). The bump itself is an\r\n // atomic increment, so concurrent failures across servers are never\r\n // lost (redis INCRBY / pg upsert keep the window's TTL).\r\n await cache.set(key, 0, { ttl: window, onConflict: \"create\" });\r\n const count = await cache.increment(key);\r\n\r\n if (count >= max) {\r\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\r\n }\r\n }\r\n } catch (error) {\r\n log.error(\"auth\", \"login-throttle\", error);\r\n }\r\n });\r\n };\r\n}\r\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KACpC,MAAM,MAAM,WAAW,UAAU;KAOjC,MAAM,MAAM,IAAI,KAAK,GAAG;MAAE,KAAK;MAAQ,YAAY;KAAS,CAAC;KAG7D,IAAI,MAFgB,MAAM,UAAU,GAAG,KAE1B,KACX,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
|
|
1
|
+
{"version":3,"file":"login-throttle.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/login-throttle.middleware.ts"],"sourcesContent":["import { cache } from \"@warlock.js/cache\";\nimport { t, type Middleware, type Request, type Response } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/**\n * Built-in identifier kinds the throttle knows how to extract from a request.\n *\n * - `\"email\"` tracks per-account, so an attacker can't spray one user from\n * many IPs.\n * - `\"ip\"` tracks per-source, so an attacker can't stuff many accounts from\n * one IP.\n *\n * Lockout trips when ANY tracked identifier crosses `max` — defense-in-depth.\n */\nexport type LoginThrottleIdentifier = \"email\" | \"ip\";\n\n/**\n * Options for {@link loginThrottleMiddleware}.\n *\n * Failure-aware by design: only counts requests whose response is non-2xx\n * (or whatever `isFailure` says) and resets on success, so a user who types\n * a wrong password three times then the right one is not penalised.\n *\n * @example\n * loginThrottleMiddleware({ max: 5, window: \"15m\", lockoutDuration: \"15m\" });\n */\nexport type LoginThrottleOptions = {\n /**\n * Failures allowed within the window before the identifier is locked.\n *\n * @default 5\n */\n max?: number;\n\n /**\n * Counting window. `ms`-format string (`\"15m\"`, `\"1h\"`) or seconds as a number.\n *\n * @default \"15m\"\n */\n window?: string | number;\n\n /**\n * Lockout duration once the threshold is tripped. `ms`-format string or seconds.\n *\n * @default \"15m\"\n */\n lockoutDuration?: string | number;\n\n /**\n * Identifiers tracked. Each is counted and locked independently.\n *\n * @default [\"email\", \"ip\"]\n */\n by?: LoginThrottleIdentifier[];\n\n /**\n * Credential field consulted when `\"email\"` is tracked. Switch to\n * `\"username\"` (or any other input field) for systems that don't\n * authenticate by email.\n *\n * @default \"email\"\n */\n identifierKey?: string;\n\n /**\n * Override the default error message returned with the 429 response.\n */\n errorMessage?: string;\n\n /**\n * Decide whether the response represents a failed login. Defaults to\n * \"non-2xx is a failure\"; override if your controller signals failure\n * through a 200-style body.\n *\n * @default (response) => !response.isOk\n */\n isFailure?: (response: Response) => boolean;\n\n /**\n * Compute the identifiers tracked for this request. Override to derive the\n * account key from a header, a JWT subject, or any other shape that does\n * not match the built-in `email` / `ip` extraction.\n */\n identify?: (request: Request) => string[];\n};\n\nconst COUNTER_PREFIX = \"auth.throttle.count.\";\nconst LOCK_PREFIX = \"auth.throttle.lock.\";\n\nconst counterKey = (identifier: string): string => `${COUNTER_PREFIX}${identifier}`;\nconst lockKey = (identifier: string): string => `${LOCK_PREFIX}${identifier}`;\n\n/**\n * Built-in identifier extractor — reads the configured credential field from\n * the request body for the per-account key, and `request.detectIp()` for the\n * per-source key. Untruthy values are dropped; a request that yields zero\n * identifiers is left untracked so the middleware no-ops for it.\n */\nfunction buildDefaultIdentifiers(\n request: Request,\n tracked: LoginThrottleIdentifier[],\n credentialField: string,\n): string[] {\n const identifiers: string[] = [];\n\n if (tracked.includes(\"email\")) {\n const value = request.input(credentialField);\n\n if (typeof value === \"string\" && value.length > 0) {\n identifiers.push(`email.${value.toLowerCase()}`);\n }\n }\n\n if (tracked.includes(\"ip\")) {\n const ip = request.detectIp();\n\n if (ip) {\n identifiers.push(`ip.${ip}`);\n }\n }\n\n return identifiers;\n}\n\n/**\n * Failure-aware login throttle. Locks an identifier (account, IP, or both)\n * after too many failed login attempts within a window — without penalising\n * a user who eventually types the right password.\n *\n * Mechanism (per identifier):\n *\n * 1. **Before the controller** — reject with `429` if a lock key is set,\n * skipping the DB lookup AND the bcrypt verify (this also neutralises the\n * CPU-DoS angle of brute-forcing).\n * 2. **After the controller** — observe the outcome via `response.onSent`.\n * On failure (non-2xx by default), bump a fixed-window counter; once it\n * reaches `max`, set a lock key with `lockoutDuration` TTL. On success,\n * clear both the counter and the lock for that identifier.\n *\n * Storage is `@warlock.js/cache` (a peer dependency of `@warlock.js/auth`,\n * transitively guaranteed via `@warlock.js/core`). Lockout state is shared\n * across replicas — correct security model, because an attacker can't bypass\n * by round-robin to a different pod. Every cache call is guarded so a\n * misconfigured driver fails open (a throttle outage must never escalate\n * into a full auth outage).\n *\n * @example\n * import { loginThrottleMiddleware } from \"@warlock.js/auth\";\n * import { router } from \"@warlock.js/core\";\n *\n * router.post(\"/auth/login\", loginController, {\n * middleware: [loginThrottleMiddleware({ max: 5, window: \"15m\" })],\n * });\n *\n * @example\n * // Per-IP only — avoid account-lockout-as-DoS on anonymous-attack vectors\n * router.post(\"/auth/refresh-token\", refreshController, {\n * middleware: [loginThrottleMiddleware({ by: [\"ip\"], max: 10, window: \"1m\" })],\n * });\n */\nexport function loginThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n const max = options.max ?? 5;\n const window = options.window ?? \"15m\";\n const lockoutDuration = options.lockoutDuration ?? \"15m\";\n const tracked = options.by ?? [\"email\", \"ip\"];\n const credentialField = options.identifierKey ?? \"email\";\n\n return async ({ request, response }) => {\n const identifiers = options.identify\n ? options.identify(request)\n : buildDefaultIdentifiers(request, tracked, credentialField);\n\n if (identifiers.length === 0) return;\n\n try {\n for (const identifier of identifiers) {\n const locked = await cache.get(lockKey(identifier));\n\n if (locked) {\n return response.tooManyRequests({\n error: options.errorMessage ?? t(\"auth.errors.tooManyAttempts\"),\n errorCode: AuthErrorCodes.TooManyAttempts,\n });\n }\n }\n } catch (error) {\n // Fail open — a cache outage (driver not initialised, network blip)\n // must never escalate into a full auth outage. Log and let the request\n // through; the onSent recorder below is independently guarded so an\n // outage there is also harmless.\n log.error(\"auth\", \"login-throttle\", error);\n\n return;\n }\n\n response.onSent(async (resolvedResponse: Response) => {\n try {\n const failed = options.isFailure\n ? options.isFailure(resolvedResponse)\n : !resolvedResponse.isOk;\n\n if (!failed) {\n for (const identifier of identifiers) {\n await cache.remove(counterKey(identifier));\n await cache.remove(lockKey(identifier));\n }\n\n return;\n }\n\n for (const identifier of identifiers) {\n const key = counterKey(identifier);\n\n // Open the window on the first failure: the create-only write means\n // only one of several racing servers sets the TTL, and an existing\n // window keeps its deadline (fixed window). The bump itself is an\n // atomic increment, so concurrent failures across servers are never\n // lost (redis INCRBY / pg upsert keep the window's TTL).\n await cache.set(key, 0, { ttl: window, onConflict: \"create\" });\n const count = await cache.increment(key);\n\n if (count >= max) {\n await cache.set(lockKey(identifier), true, { ttl: lockoutDuration });\n }\n }\n } catch (error) {\n log.error(\"auth\", \"login-throttle\", error);\n }\n });\n };\n}\n"],"mappings":";;;;;;AAuFA,MAAM,iBAAiB;AACvB,MAAM,cAAc;AAEpB,MAAM,cAAc,eAA+B,GAAG,iBAAiB;AACvE,MAAM,WAAW,eAA+B,GAAG,cAAc;;;;;;;AAQjE,SAAS,wBACP,SACA,SACA,iBACU;CACV,MAAM,cAAwB,CAAC;CAE/B,IAAI,QAAQ,SAAS,OAAO,GAAG;EAC7B,MAAM,QAAQ,QAAQ,MAAM,eAAe;EAE3C,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,YAAY,KAAK,SAAS,MAAM,YAAY,GAAG;CAEnD;CAEA,IAAI,QAAQ,SAAS,IAAI,GAAG;EAC1B,MAAM,KAAK,QAAQ,SAAS;EAE5B,IAAI,IACF,YAAY,KAAK,MAAM,IAAI;CAE/B;CAEA,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,SAAgB,wBAAwB,UAAgC,CAAC,GAAe;CACtF,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,kBAAkB,QAAQ,mBAAmB;CACnD,MAAM,UAAU,QAAQ,MAAM,CAAC,SAAS,IAAI;CAC5C,MAAM,kBAAkB,QAAQ,iBAAiB;CAEjD,OAAO,OAAO,EAAE,SAAS,eAAe;EACtC,MAAM,cAAc,QAAQ,WACxB,QAAQ,SAAS,OAAO,IACxB,wBAAwB,SAAS,SAAS,eAAe;EAE7D,IAAI,YAAY,WAAW,GAAG;EAE9B,IAAI;GACF,KAAK,MAAM,cAAc,aAGvB,IAAI,MAFiB,MAAM,IAAI,QAAQ,UAAU,CAAC,GAGhD,OAAO,SAAS,gBAAgB;IAC9B,OAAO,QAAQ,gBAAgB,EAAE,6BAA6B;IAC9D;GACF,CAAC;EAGP,SAAS,OAAO;GAKd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAEzC;EACF;EAEA,SAAS,OAAO,OAAO,qBAA+B;GACpD,IAAI;IAKF,IAAI,EAJW,QAAQ,YACnB,QAAQ,UAAU,gBAAgB,IAClC,CAAC,iBAAiB,OAET;KACX,KAAK,MAAM,cAAc,aAAa;MACpC,MAAM,MAAM,OAAO,WAAW,UAAU,CAAC;MACzC,MAAM,MAAM,OAAO,QAAQ,UAAU,CAAC;KACxC;KAEA;IACF;IAEA,KAAK,MAAM,cAAc,aAAa;KACpC,MAAM,MAAM,WAAW,UAAU;KAOjC,MAAM,MAAM,IAAI,KAAK,GAAG;MAAE,KAAK;MAAQ,YAAY;KAAS,CAAC;KAG7D,IAAI,MAFgB,MAAM,UAAU,GAAG,KAE1B,KACX,MAAM,MAAM,IAAI,QAAQ,UAAU,GAAG,MAAM,EAAE,KAAK,gBAAgB,CAAC;IAEvE;GACF,SAAS,OAAO;IACd,IAAI,MAAM,QAAQ,kBAAkB,KAAK;GAC3C;EACF,CAAC;CACH;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\
|
|
1
|
+
{"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted access-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape.\n *\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\n * ever read; access tokens are revoked by deleting the row.\n */\nexport const accessTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n /** Null for tokens issued before family tracking was introduced. */\n family_id: v.string().optional(),\n expires_at: v.date().required(),\n});\n\n/**\n * Persisted access-token record + the data layer for access tokens.\n *\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\n * presented JWT against this table so deleting a row (logout) invalidates the\n * token immediately, before its JWT expiry. The auth service goes through the\n * named statics exclusively, so it never hard-codes a column name and an\n * override can rename/add columns by registering under\n * `config.auth.accessToken.model`.\n */\nexport class AccessToken extends Model {\n public static table = \"access_tokens\";\n\n public static schema = accessTokenSchema;\n\n /**\n * The raw signed JWT is a bearer credential — never serialize it. `hidden`\n * only strips it from `toJSON()` / `JSON.stringify(model)` (so\n * `response.json(token)` or a resource cannot leak it); `get(\"token\")`,\n * `findByToken` and every query still read the column.\n */\n public static hidden = [\"token\"];\n\n /**\n * Permanent regardless of the data source default — Mongo's driver default\n * is `\"trash\"`, which would copy live credential material (the JWT itself)\n * into `access_tokensTrash` on every `destroy()` (logout, expiry purge,\n * never-expiring purge). A revoked/expired access token must not survive\n * anywhere.\n */\n public static deleteStrategy = \"permanent\" as const;\n\n /** The user this token was issued for. */\n public get userId() {\n return this.get(\"user_id\");\n }\n\n /** The user-type slug this token was issued for. */\n public get userType(): string {\n return this.get(\"user_type\");\n }\n\n /**\n * Whether the persisted expiry has passed.\n *\n * The middleware asks this on every request, which is what makes the row the\n * authority it always looked like it was: before 4.12.0 the gate only checked\n * that the row *existed*, so a row the database knew was dead still let its\n * token through — the database was never asked.\n *\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\n * does this die\" is malformed, and the safe reading of a malformed credential\n * is that it is not one. This is deliberately stricter than a naive\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\n * grants exactly the poisoned rows an unlimited life.\n */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!isUsableExpiry(expiresAt)) return true;\n\n return new Date(expiresAt).getTime() <= Date.now();\n }\n\n /**\n * Whether nothing about this row can ever retire it — an unusable\n * `expires_at`, or a token carrying no `exp` claim. See\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\n * on.\n */\n public get neverExpires(): boolean {\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\n }\n\n /**\n * Persist a freshly-signed access token for the user.\n */\n public static issue(user: Auth, token: string, expiresAt: Date, familyId?: string) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n family_id: familyId,\n expires_at: expiresAt,\n });\n }\n\n /**\n * Find an access-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<AccessToken | null> {\n return this.first({ token });\n }\n\n /**\n * Delete a specific token that belongs to the given user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Delete every access token belonging to the user.\n */\n public static deleteAllForUser(user: Auth) {\n return this.delete({ user_id: user.id });\n }\n\n /**\n * Delete every access token attached to one family, plus pre-family rows for\n * the same user type. The latter is deliberately conservative during the\n * additive upgrade: old access rows cannot be assigned safely to a family.\n */\n public static async deleteFamilyAndLegacyForUser(user: Auth, familyId: string): Promise<void> {\n await this.delete({ user_id: user.id, user_type: user.userType, family_id: familyId });\n await this.delete({ user_id: user.id, user_type: user.userType, family_id: null });\n }\n\n /**\n * Hard-delete every expired access-token row. Returns the number removed.\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\n */\n public static async purgeExpired(): Promise<number> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens.length;\n }\n\n /**\n * Every row that can never retire itself — see {@link neverExpires}.\n *\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\n * token with no `exp` claim, which lives inside the token string rather than\n * in a column. Neither is expressible as a `where`, so the rows have to be\n * read to be judged. This runs from a one-off remediation command, not a\n * request path.\n */\n public static async findNeverExpiring(): Promise<AccessToken[]> {\n const tokens = await this.query().get();\n\n return tokens.filter((token: AccessToken) => token.neverExpires);\n }\n\n /**\n * Hard-delete every never-expiring row, returning the rows removed so the\n * caller can report what it revoked. Deletion *is* revocation for access\n * tokens — the middleware rejects a token with no row.\n */\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\n const tokens = await this.findNeverExpiring();\n\n for (const token of tokens) {\n await token.destroy();\n }\n\n return tokens;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;;CAE/B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;gBAQA,CAAC,OAAO;;;wBASA;;;CAG/B,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB,UAAmB;EACjF,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,WAAW;GACX,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;;CAOA,aAAoB,6BAA6B,MAAY,UAAiC;EAC5F,MAAM,KAAK,OAAO;GAAE,SAAS,KAAK;GAAI,WAAW,KAAK;GAAU,WAAW;EAAS,CAAC;EACrF,MAAM,KAAK,OAAO;GAAE,SAAS,KAAK;GAAI,WAAW,KAAK;GAAU,WAAW;EAAK,CAAC;CACnF;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,CAAC,CAAC,IAAI,EAEzB,CAAC,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\
|
|
1
|
+
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { AccessToken } from \"./access-token.model\";\n\nexport const AccessTokenMigration = migrate(AccessToken, {\n name: \"accessToken\",\n up() {\n // Create table\n this.createTableIfNotExists();\n\n // Primary key\n this.primaryUuid();\n\n // Token field\n this.text(\"token\").unique();\n\n // User reference (flat columns for cross-driver compatibility).\n // `foreignId` matches the DataSource's default primary-key type, so the\n // column stays compatible with the user model's `id` whether the app uses\n // integer (the default) or uuid PKs — hardcoding uuid here 500'd every\n // login on a default integer-PK app (finding 5e47bdb3).\n this.foreignId(\"user_id\").index();\n this.string(\"user_type\", 50).nullable();\n\n // Expiry — enables server-side cleanup of stale access-token rows\n this.timestamp(\"expires_at\").index().nullable();\n\n // Timestamps\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,uBAAuB,QAAQ,aAAa;CACvD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAO1B,KAAK,UAAU,SAAS,CAAC,CAAC,MAAM;EAChC,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EAGtC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAG9C,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-token-family.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/auth-token-family/auth-token-family.model.ts"],"mappings":";;;;;cAKa,qBAAA,6BAAqB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cASrB,eAAA,SAAwB,KAAA;EAAA,OACrB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAGT,SAAA;;SAKG,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,QAAA,WAAgB,OAAA,CAAA,eAAA;EAAA,OASlC,cAAA,CAAe,QAAA,WAAmB,OAAA,CAAQ,eAAA;;SAKpC,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,QAAA,WAAmB,OAAA,CAAQ,eAAA;;
|
|
1
|
+
{"version":3,"file":"auth-token-family.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/auth-token-family/auth-token-family.model.ts"],"mappings":";;;;;cAKa,qBAAA,6BAAqB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cASrB,eAAA,SAAwB,KAAA;EAAA,OACrB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAGT,SAAA;;SAKG,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,QAAA,WAAgB,OAAA,CAAA,eAAA;EAAA,OASlC,cAAA,CAAe,QAAA,WAAmB,OAAA,CAAQ,eAAA;;SAKpC,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,QAAA,WAAmB,OAAA,CAAQ,eAAA;;SAgCpD,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,eAAA;;AAzD/C;;;SAmEgB,eAAA,CAAgB,QAAA,UAAkB,gBAAA,WAA2B,OAAA;;SAQ7D,MAAA,CAAO,QAAA,UAAkB,gBAAA,WAA2B,OAAA;AAAA"}
|
|
@@ -36,6 +36,11 @@ var AuthTokenFamily = class extends Model {
|
|
|
36
36
|
}
|
|
37
37
|
/** Add a durable row for an existing lineage through a driver-native upsert. */
|
|
38
38
|
static async ensure(user, familyId) {
|
|
39
|
+
const existing = await this.findByFamilyId(familyId);
|
|
40
|
+
if (existing) {
|
|
41
|
+
if (existing.get("user_id") !== user.id || existing.get("user_type") !== user.userType) throw new Error("Token family belongs to a different user");
|
|
42
|
+
return existing;
|
|
43
|
+
}
|
|
39
44
|
await this.atomic({ family_id: familyId }, { $setOnInsert: {
|
|
40
45
|
user_id: user.id,
|
|
41
46
|
user_type: user.userType,
|