@warlock.js/auth 5.18.0 → 5.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/esm/contracts/index.d.mts +1 -1
  3. package/esm/contracts/types.d.mts +23 -2
  4. package/esm/contracts/types.d.mts.map +1 -1
  5. package/esm/contracts/types.mjs.map +1 -1
  6. package/esm/index.d.mts +3 -2
  7. package/esm/index.mjs +2 -1
  8. package/esm/middleware/auth.middleware.d.mts +9 -6
  9. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  10. package/esm/middleware/auth.middleware.mjs +101 -80
  11. package/esm/middleware/auth.middleware.mjs.map +1 -1
  12. package/esm/models/access-token/access-token.model.d.mts +31 -7
  13. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  14. package/esm/models/access-token/access-token.model.mjs +21 -1
  15. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  16. package/esm/models/access-token/family-association.migration.mjs +18 -0
  17. package/esm/models/access-token/family-association.migration.mjs.map +1 -0
  18. package/esm/models/auth-token-family/auth-token-family.model.d.mts +90 -0
  19. package/esm/models/auth-token-family/auth-token-family.model.d.mts.map +1 -0
  20. package/esm/models/auth-token-family/auth-token-family.model.mjs +80 -0
  21. package/esm/models/auth-token-family/auth-token-family.model.mjs.map +1 -0
  22. package/esm/models/auth-token-family/index.d.mts +1 -0
  23. package/esm/models/auth-token-family/index.mjs +3 -0
  24. package/esm/models/auth-token-family/migration.mjs +25 -0
  25. package/esm/models/auth-token-family/migration.mjs.map +1 -0
  26. package/esm/models/index.d.mts +1 -0
  27. package/esm/models/index.d.mts.map +1 -1
  28. package/esm/models/index.mjs +8 -0
  29. package/esm/models/index.mjs.map +1 -1
  30. package/esm/models/refresh-token/refresh-token.model.d.mts +43 -0
  31. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  32. package/esm/models/refresh-token/refresh-token.model.mjs +17 -0
  33. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  34. package/esm/models/refresh-token/successor-association.migration.mjs +22 -0
  35. package/esm/models/refresh-token/successor-association.migration.mjs.map +1 -0
  36. package/esm/services/auth.service.d.mts +22 -1
  37. package/esm/services/auth.service.d.mts.map +1 -1
  38. package/esm/services/auth.service.mjs +216 -32
  39. package/esm/services/auth.service.mjs.map +1 -1
  40. package/esm/services/token-family-operation.mjs +74 -0
  41. package/esm/services/token-family-operation.mjs.map +1 -0
  42. package/llms-full.txt +63 -58
  43. package/llms.txt +9 -9
  44. package/package.json +8 -8
  45. package/skills/auth-basics/SKILL.md +3 -2
  46. package/skills/customize-token-storage/SKILL.md +1 -1
  47. package/skills/customize-user-type/SKILL.md +1 -1
  48. package/skills/handle-login-and-logout/SKILL.md +1 -1
  49. package/skills/login-with-providers/SKILL.md +1 -1
  50. package/skills/manage-tokens/SKILL.md +19 -6
  51. package/skills/overview/SKILL.md +3 -3
  52. package/skills/protect-routes/SKILL.md +30 -39
  53. package/skills/register-user/SKILL.md +1 -1
  54. package/skills/run-auth-commands/SKILL.md +1 -1
  55. package/skills/throttle-login-attempts/SKILL.md +1 -1
  56. package/skills/verify-email-and-reset-password/SKILL.md +1 -1
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.19.0 - 2026-09-23
8
+
9
+ ### Added
10
+
11
+ - `authMiddleware()` now uses `auth.defaultUserType` (or the sole configured user type), and the object overload accepts `{ source, key, optional, refresh, redirect }`. A page-local `redirect` changes only page-route authentication failures; APIs retain `401`. Legacy `authMiddleware(userType, "header" | "cookie:name")` calls remain supported.
12
+ - Durable token families: token pairs associate access and refresh rows with `AuthTokenFamily`; family revoke atomically stamps the family, revokes its refresh rows, and removes its associated access rows. During the additive upgrade, unassociated legacy access rows are revoked conservatively only for the same user and type. Register `authMigrations` and run pending migrations.
13
+ - Automatic cookie renewal is opt-in through the middleware `refresh` descriptor. The finalized coordinator permits only an exact, immediately active successor pair during a bounded duplicate window (default five seconds; configurable from zero through ten seconds). It does not broaden legacy `authService.refreshTokens` replay behavior. A duplicate within that window is tolerated deliberately; after it, old-token reuse remains a replay and revokes the family. Family logout makes late cookies unusable. This does not claim to solve arbitrary out-of-order HTTP cookie delivery.
14
+
15
+ ### Changed
16
+
17
+ - Refined package skill-discovery descriptions and regenerated the llms projections.
18
+
19
+ ### Fixed
20
+
21
+ - Refresh attempts emit token notifications after their owned transaction commits, discarding notifications from rolled-back retries.
22
+ - Issuing into an explicitly reused token family now shares its durable revocation boundary, preventing credentials from being written after concurrent logout.
23
+
7
24
  ## 5.17.0 - 2026-09-21
8
25
 
9
26
  ### Security
@@ -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, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair } from "./types.mjs";
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";
3
3
  import { Authenticable } from "./auth-contract.mjs";
4
4
  import { RequestUser } from "./request-locals.mjs";
@@ -35,6 +35,26 @@ declare const NO_EXPIRATION = "100y";
35
35
  type LogoutWithoutTokenBehavior = "revoke-all" | "error";
36
36
  /** The single request credential source an auth middleware instance accepts. */
37
37
  type TokenFrom = "header" | `cookie:${string}`;
38
+ /** One explicitly named credential location accepted by auth middleware. */
39
+ interface AuthCredentialDescriptor {
40
+ source: "header" | "cookie";
41
+ key?: string;
42
+ }
43
+ /** Cookie credential and bounded duplicate window for automatic renewal. */
44
+ interface AuthRefreshCredentialDescriptor extends AuthCredentialDescriptor {
45
+ /** Milliseconds; the service clamps this to the 0–10 second range (default 5 seconds). */
46
+ overlapMs?: number;
47
+ }
48
+ /** Per-route authentication behavior for the object middleware overload. */
49
+ interface AuthMiddlewareOptions extends Partial<AuthCredentialDescriptor> {
50
+ optional?: boolean;
51
+ /** Opt-in automatic renewal credential. Cookie sources only. */
52
+ refresh?: AuthRefreshCredentialDescriptor;
53
+ redirect?: {
54
+ to: string;
55
+ returnUrlParam?: string;
56
+ };
57
+ }
38
58
  /** Application policy that decides whether a resolved user may authenticate. */
39
59
  type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;
40
60
  /** Password credentials plus the application-defined identity fields. */
@@ -232,7 +252,8 @@ type AuthConfigurations = {
232
252
  */
233
253
  userType: {
234
254
  [userType: string]: ChildModel<Auth>;
235
- };
255
+ }; /** Default user type used by `authMiddleware()` and options-only overloads. */
256
+ defaultUserType?: string;
236
257
  /**
237
258
  * Decide whether a resolved user may authenticate or receive new tokens.
238
259
  * @default () => true
@@ -400,5 +421,5 @@ type ClearAuthCookieOptions = {
400
421
  path?: string;
401
422
  };
402
423
  //#endregion
403
- export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, AuthNotification, CanAuthenticate, ClearAuthCookieOptions, CookieAuthConfig, CsrfConfig, DeviceInfo, EmailVerificationConfig, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, OneTimeTokenConfig, OneTimeTokenNotificationData, OneTimeTokenUrlBuilder, PageAuthConfig, PasswordResetConfig, PasswordSetter, RefreshTokenConfig, SetAuthCookieOptions, TokenFrom, TokenPair };
424
+ 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 };
404
425
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":"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"}
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/** 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 /**\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 `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\";\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 /** 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 `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"}
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, AuthCredentials, AuthNotification, 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, 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";
@@ -26,6 +26,7 @@ import { LoginThrottleIdentifier, LoginThrottleOptions, loginThrottleMiddleware
26
26
  import { tokenConsumeThrottleMiddleware, tokenIssueThrottleMiddleware } from "./middleware/one-time-token-throttle.middleware.mjs";
27
27
  import { otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware } from "./middleware/otp-throttle.middleware.mjs";
28
28
  import { requireVerifiedEmail } from "./middleware/require-verified-email.middleware.mjs";
29
+ import { AuthTokenFamily, authTokenFamilySchema } from "./models/auth-token-family/auth-token-family.model.mjs";
29
30
  import { PasskeyCredential, passkeyCredentialSchema } from "./models/passkey-credential/passkey-credential.model.mjs";
30
31
  import { ProviderAccount, providerAccountSchema } from "./models/provider-account/provider-account.model.mjs";
31
32
  import { authMigrations } from "./models/index.mjs";
@@ -51,5 +52,5 @@ import { IssuedOneTimeToken, consumeOneTimeToken, hashOneTimeToken, issueOneTime
51
52
  import { requestPasswordReset, resetPassword } from "./services/password-reset.mjs";
52
53
  import { safeEqual } from "./utils/safe-equal.mjs";
53
54
  import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
54
- export { APPLE_AUTH_FEATURE, AccessToken, AccessTokenConfig, AccessTokenOutput, AppleJoseModule, AppleProvider, AppleProviderConfig, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthErrorTranslations, AuthEventCallback, AuthEventName, AuthEventPayloads, AuthNotification, AuthProvider, AuthProviderSdkMissingError, 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, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, SetAuthCookieOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, XProvider, XProviderConfig, accessTokenSchema, assertCsrfOriginAllowed, authDefaultTranslations, authEvents, authMiddleware, authMigrations, authService, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
55
+ 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, PasskeyCredential, type PasskeyOptionsJSON, type PasskeyResponseJSON, PasskeysConfig, PasswordResetConfig, PasswordSetter, ProviderAccount, ProviderAuthorizationState, ProviderCallbackParams, ProviderEmailNotVerifiedError, ProviderProfile, ProviderUserCreator, ProvidersConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, RequestUser, 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, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
55
56
  import "./contracts/request-locals.mjs";
package/esm/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { registerAuthTranslations } from "./localization/register-auth-translati
3
3
  import "./localization/init.mjs";
4
4
  import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
5
5
  import { AccessToken, accessTokenSchema } from "./models/access-token/access-token.model.mjs";
6
+ import { AuthTokenFamily, authTokenFamilySchema } from "./models/auth-token-family/auth-token-family.model.mjs";
6
7
  import { OneTimeToken, oneTimeTokenSchema } from "./models/one-time-token/one-time-token.model.mjs";
7
8
  import { RefreshToken, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
8
9
  import { authEvents } from "./services/auth-events.mjs";
@@ -56,4 +57,4 @@ import { completeProviderLogin, resolveAuthProvider, startProviderLogin } from "
56
57
  import "./providers/index.mjs";
57
58
  import "./utils/index.mjs";
58
59
 
59
- export { APPLE_AUTH_FEATURE, AccessToken, AppleProvider, Auth, AuthErrorCodes, AuthProviderSdkMissingError, 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, completeProviderLogin, consumeOneTimeToken, currentUser, generateJWTSecret, generatePasskeyAuthenticationOptions, generatePasskeyRegistrationOptions, hashOneTimeToken, isEmailVerified, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, issueOneTimeToken, jwt, loginThrottleMiddleware, oneTimeTokenModel, oneTimeTokenSchema, otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
60
+ 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, passkeyCredentialSchema, pkceChallenge, providerAccountSchema, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerAuthTranslations, registerJWTSecretGeneratorCommand, requestOtp, requestPasswordReset, requireVerifiedEmail, requiresCsrfOriginCheck, resetPassword, resolveAuthProvider, safeEqual, sendEmailVerification, startProviderLogin, tokenConsumeThrottleMiddleware, tokenHasExpClaim, tokenIssueThrottleMiddleware, verifyEmail, verifyOtp, verifyPasskeyAuthentication, verifyPasskeyRegistration };
@@ -1,18 +1,18 @@
1
- import { TokenFrom } from "../contracts/types.mjs";
1
+ import { AuthMiddlewareOptions, TokenFrom } from "../contracts/types.mjs";
2
2
  import { Middleware } from "@warlock.js/core";
3
3
 
4
4
  //#region ../auth/src/middleware/auth.middleware.d.ts
5
5
  /**
6
- * Build a route gate that always requires an authenticated request.
6
+ * Build a route gate. Authentication is required unless optional is explicit.
7
7
  *
8
- * The argument is mandatory and selects which user types may pass:
8
+ * An omitted user type selects the configured default; an explicit argument
9
+ * selects which user types may pass:
9
10
  * - `[]` — any authenticated user (token required, type not checked).
10
11
  * - `"admin"` / `["admin", "staff"]` — token required AND the user's
11
12
  * `userType` must be one of the listed types.
12
13
  *
13
- * There is no anonymous/optional mode: a request without a valid access
14
- * token is always rejected with `401`. Routes that should be public
15
- * simply omit the middleware.
14
+ * `{ optional: true }` allows anonymous requests. All other forms reject
15
+ * missing or invalid credentials with 401, or a configured page redirect.
16
16
  *
17
17
  * `tokenFrom` selects one credential source and defaults to `"header"`.
18
18
  * A single source avoids making credential precedence depend on array order.
@@ -23,6 +23,9 @@ import { Middleware } from "@warlock.js/core";
23
23
  * router.get("/back-office", authMiddleware(["admin", "staff"]), backOfficeController);
24
24
  * router.get("/browser-account", authMiddleware([], "cookie:token"), accountController);
25
25
  */
26
+ declare function authMiddleware(): Middleware;
27
+ declare function authMiddleware(options: AuthMiddlewareOptions): Middleware;
28
+ declare function authMiddleware(allowedUserType: string | string[], options?: AuthMiddlewareOptions): Middleware;
26
29
  declare function authMiddleware(allowedUserType: string | string[], tokenFrom?: TokenFrom): Middleware;
27
30
  //#endregion
28
31
  export { authMiddleware };
@@ -1 +1 @@
1
- {"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"mappings":";;;;;;AAoHA;;;;;;;;;AAGa;;;;;;;;;;iBAHG,cAAA,CACd,eAAA,qBACA,SAAA,GAAW,SAAA,GACV,UAAU"}
1
+ {"version":3,"file":"auth.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"mappings":";;;;;;AAsJA;;;;AAA4C;AAC5C;;;;;;;;AAA0E;AAC1E;;;;;iBAFgB,cAAA,IAAkB,UAAU;AAAA,iBAC5B,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,UAAU;AAAA,iBAC1D,cAAA,CACd,eAAA,qBACA,OAAA,GAAU,qBAAA,GACT,UAAU;AAAA,iBACG,cAAA,CACd,eAAA,qBACA,SAAA,GAAY,SAAA,GACX,UAAU"}
@@ -28,13 +28,14 @@ import { log } from "@warlock.js/logger";
28
28
  * on — never the absolute URL: a relative target cannot be turned into an
29
29
  * open-redirect off this app's origin.
30
30
  */
31
- function rejectUnauthorized({ request, response }, payload) {
32
- const loginPath = authConfig.pageAuth.loginPath();
31
+ function rejectUnauthorized({ request, response }, payload, redirect) {
32
+ const loginPath = redirect?.to ?? authConfig.pageAuth.loginPath();
33
+ const returnUrlParam = redirect?.returnUrlParam ?? authConfig.pageAuth.returnUrlParam();
33
34
  if (loginPath && request.route?.isPage) {
34
35
  const destination = localizedLoginPath(loginPath, request.locale);
35
36
  const separator = destination.includes("?") ? "&" : "?";
36
37
  const returnUrl = encodeURIComponent(request.url);
37
- return response.redirect(`${destination}${separator}${authConfig.pageAuth.returnUrlParam()}=${returnUrl}`);
38
+ return response.redirect(`${destination}${separator}${returnUrlParam}=${returnUrl}`);
38
39
  }
39
40
  return response.unauthorized(payload);
40
41
  }
@@ -61,39 +62,36 @@ function readCredential(request, tokenFrom) {
61
62
  const value = request.cookie(tokenFrom.slice(7));
62
63
  return value ? String(value) : "";
63
64
  }
64
- /**
65
- * Build a route gate that always requires an authenticated request.
66
- *
67
- * The argument is mandatory and selects which user types may pass:
68
- * - `[]` — any authenticated user (token required, type not checked).
69
- * - `"admin"` / `["admin", "staff"]` — token required AND the user's
70
- * `userType` must be one of the listed types.
71
- *
72
- * There is no anonymous/optional mode: a request without a valid access
73
- * token is always rejected with `401`. Routes that should be public
74
- * simply omit the middleware.
75
- *
76
- * `tokenFrom` selects one credential source and defaults to `"header"`.
77
- * A single source avoids making credential precedence depend on array order.
78
- *
79
- * @example
80
- * router.get("/account", authMiddleware([]), accountController);
81
- * router.get("/admin", authMiddleware("admin"), adminController);
82
- * router.get("/back-office", authMiddleware(["admin", "staff"]), backOfficeController);
83
- * router.get("/browser-account", authMiddleware([], "cookie:token"), accountController);
84
- */
85
- function authMiddleware(allowedUserType, tokenFrom = "header") {
86
- const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];
65
+ function tokenFromDescriptor(descriptor) {
66
+ if (!descriptor.source || descriptor.source === "header") return "header";
67
+ if (!descriptor.key) throw new Error("authMiddleware cookie credentials require a cookie key.");
68
+ return `cookie:${descriptor.key}`;
69
+ }
70
+ function resolveAllowedTypes(value) {
71
+ if (value !== void 0) return Array.isArray(value) ? value : [value];
72
+ const configuredDefault = config.key("auth.defaultUserType");
73
+ if (configuredDefault) return [configuredDefault];
74
+ const userTypes = config.key("auth.userType", {});
75
+ const configuredTypes = Object.keys(userTypes);
76
+ if (configuredTypes.length === 1) return configuredTypes;
77
+ throw new Error("authMiddleware requires a user type when auth.defaultUserType is unset and multiple user types exist.");
78
+ }
79
+ function authMiddleware(userTypeOrOptions, legacyTokenFromOrOptions) {
80
+ const options = typeof userTypeOrOptions === "object" && !Array.isArray(userTypeOrOptions) ? userTypeOrOptions : typeof legacyTokenFromOrOptions === "object" ? legacyTokenFromOrOptions : void 0;
81
+ const allowedTypes = resolveAllowedTypes(typeof userTypeOrOptions === "string" || Array.isArray(userTypeOrOptions) ? userTypeOrOptions : void 0);
82
+ const tokenFrom = options ? tokenFromDescriptor(options) : (typeof legacyTokenFromOrOptions === "string" ? legacyTokenFromOrOptions : void 0) ?? "header";
83
+ const refreshCredential = options?.refresh ? tokenFromDescriptor(options.refresh) : void 0;
84
+ const renewalUserType = allowedTypes.length === 1 ? allowedTypes[0] : void 0;
85
+ if (refreshCredential && (tokenFrom === "header" || refreshCredential === "header" || !renewalUserType)) throw new Error("authMiddleware automatic renewal requires one allowed user type and cookie access and refresh credentials.");
86
+ const optional = options?.optional === true;
87
87
  const auth = async ({ request, response }) => {
88
- const authorizationValue = readCredential(request, tokenFrom);
89
- if (!authorizationValue) return rejectUnauthorized({
88
+ const reject = (payload) => rejectUnauthorized({
90
89
  request,
91
90
  response
92
- }, {
93
- error: t("auth.errors.missingAccessToken"),
94
- errorCode: "EC001"
95
- });
96
- if (requiresCsrfOriginCheck(tokenFrom, request.method)) try {
91
+ }, payload, options?.redirect);
92
+ let authorizationValue = readCredential(request, tokenFrom);
93
+ let renewalAttempted = false;
94
+ if ((authorizationValue || refreshCredential) && requiresCsrfOriginCheck(tokenFrom, request.method)) try {
97
95
  assertCsrfOriginAllowed(request);
98
96
  } catch (error) {
99
97
  if (!(error instanceof CsrfOriginMismatchError)) throw error;
@@ -103,45 +101,68 @@ function authMiddleware(allowedUserType, tokenFrom = "header") {
103
101
  errorCode: "EC006"
104
102
  });
105
103
  }
104
+ const renew = async () => {
105
+ if (!refreshCredential || renewalAttempted) return false;
106
+ renewalAttempted = true;
107
+ const refreshToken = readCredential(request, refreshCredential);
108
+ if (!refreshToken) return false;
109
+ const pair = await authService.renewAutomaticSession(refreshToken, renewalUserType, { overlapMs: options?.refresh?.overlapMs });
110
+ if (!pair?.refreshToken) return false;
111
+ authService.setAuthCookie(response, pair.accessToken, { name: tokenFrom.slice(7) });
112
+ authService.setAuthCookie(response, pair.refreshToken, { name: refreshCredential.slice(7) });
113
+ authorizationValue = pair.accessToken.token;
114
+ return true;
115
+ };
106
116
  let decoded;
107
- try {
108
- decoded = await jwt.verify(authorizationValue);
109
- } catch (error) {
110
- if (!isInvalidCredentialError(error)) throw error;
111
- log.error("http", "auth", error);
112
- request.locals.user = void 0;
113
- return rejectUnauthorized({
114
- request,
115
- response
116
- }, {
117
- error: t("auth.errors.invalidAccessToken"),
118
- errorCode: "EC002"
119
- });
120
- }
121
- request.decodedAccessToken = decoded;
122
- const accessToken = await config.key("auth.accessToken.model", AccessToken).findByToken(authorizationValue);
123
- if (!accessToken) return rejectUnauthorized({
124
- request,
125
- response
126
- }, {
127
- error: t("auth.errors.invalidAccessToken"),
128
- errorCode: "EC002"
129
- });
130
- if (accessTokenRowIsExpired(accessToken)) {
131
- await accessToken.destroy();
132
- return rejectUnauthorized({
133
- request,
134
- response
135
- }, {
136
- error: t("auth.errors.invalidAccessToken"),
137
- errorCode: "EC002"
138
- });
117
+ let accessToken;
118
+ while (true) {
119
+ if (!authorizationValue) {
120
+ request.locals.user = void 0;
121
+ if (await renew()) continue;
122
+ if (optional) return;
123
+ return reject({
124
+ error: t("auth.errors.missingAccessToken"),
125
+ errorCode: "EC001"
126
+ });
127
+ }
128
+ try {
129
+ decoded = await jwt.verify(authorizationValue);
130
+ } catch (error) {
131
+ if (!isInvalidCredentialError(error)) throw error;
132
+ log.error("http", "auth", error);
133
+ request.locals.user = void 0;
134
+ if (await renew()) continue;
135
+ if (optional) return;
136
+ return reject({
137
+ error: t("auth.errors.invalidAccessToken"),
138
+ errorCode: "EC002"
139
+ });
140
+ }
141
+ request.decodedAccessToken = decoded;
142
+ accessToken = await config.key("auth.accessToken.model", AccessToken).findByToken(authorizationValue);
143
+ if (!accessToken) {
144
+ request.locals.user = void 0;
145
+ if (await renew()) continue;
146
+ if (optional) return;
147
+ return reject({
148
+ error: t("auth.errors.invalidAccessToken"),
149
+ errorCode: "EC002"
150
+ });
151
+ }
152
+ if (accessTokenRowIsExpired(accessToken)) {
153
+ await accessToken.destroy();
154
+ request.locals.user = void 0;
155
+ if (await renew()) continue;
156
+ if (optional) return;
157
+ return reject({
158
+ error: t("auth.errors.invalidAccessToken"),
159
+ errorCode: "EC002"
160
+ });
161
+ }
162
+ break;
139
163
  }
140
164
  const userType = decoded.userType ?? accessToken.userType;
141
- if (allowedTypes.length && !allowedTypes.includes(userType)) return rejectUnauthorized({
142
- request,
143
- response
144
- }, {
165
+ if (allowedTypes.length && !allowedTypes.includes(userType)) return response.forbidden({
145
166
  error: t("auth.errors.unauthorized"),
146
167
  errorCode: "EC003"
147
168
  });
@@ -150,21 +171,21 @@ function authMiddleware(allowedUserType, tokenFrom = "header") {
150
171
  const currentUser = await UserModel.find(decoded.id);
151
172
  if (!currentUser) {
152
173
  await accessToken.destroy();
153
- return rejectUnauthorized({
154
- request,
155
- response
156
- }, {
174
+ request.locals.user = void 0;
175
+ if (optional) return;
176
+ return reject({
157
177
  error: t("auth.errors.invalidAccessToken"),
158
178
  errorCode: "EC002"
159
179
  });
160
180
  }
161
- if (!await authService.canAuthenticate(currentUser)) return rejectUnauthorized({
162
- request,
163
- response
164
- }, {
165
- error: t("auth.errors.unauthorized"),
166
- errorCode: "EC003"
167
- });
181
+ if (!await authService.canAuthenticate(currentUser)) {
182
+ request.locals.user = void 0;
183
+ if (optional) return;
184
+ return reject({
185
+ error: t("auth.errors.unauthorized"),
186
+ errorCode: "EC003"
187
+ });
188
+ }
168
189
  request.locals.user = currentUser;
169
190
  };
170
191
  return auth;
@@ -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, type Request } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport type { TokenFrom } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport { authConfig } from \"../services/auth-config\";\r\nimport { authService } from \"../services/auth.service\";\r\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\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({ request, response }: HttpContext, payload: UnauthorizedPayload) {\r\n const loginPath = authConfig.pageAuth.loginPath();\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(\r\n `${destination}${separator}${authConfig.pageAuth.returnUrlParam()}=${returnUrl}`,\r\n );\r\n }\r\n\r\n return response.unauthorized(payload);\r\n}\r\n\r\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Whether the persisted row says the token is dead.\r\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\r\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\r\n}\r\n\r\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\r\n if (tokenFrom === \"header\") {\r\n return request.authorizationValue;\r\n }\r\n\r\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\r\n\r\n return value ? String(value) : \"\";\r\n}\r\n\r\n/**\r\n * Build a route gate that always requires an authenticated request.\r\n *\r\n * The argument is mandatory and 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 * There is no anonymous/optional mode: a request without a valid access\r\n * token is always rejected with `401`. Routes that should be public\r\n * simply omit the middleware.\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(\r\n allowedUserType: string | string[],\r\n tokenFrom: TokenFrom = \"header\",\r\n): Middleware {\r\n const allowedTypes = Array.isArray(allowedUserType) ? allowedUserType : [allowedUserType];\r\n\r\n const auth: Middleware = async ({ request, response }) => {\r\n const authorizationValue = readCredential(request, tokenFrom);\r\n\r\n if (!authorizationValue) {\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n },\r\n );\r\n }\r\n\r\n // CSRF Origin check: only in scope for a cookie-sourced\r\n // credential on an unsafe method. Header-token auth and safe methods\r\n // (GET/HEAD/OPTIONS) never reach `assertCsrfOriginAllowed`.\r\n if (requiresCsrfOriginCheck(tokenFrom, request.method)) {\r\n try {\r\n assertCsrfOriginAllowed(request);\r\n } catch (error) {\r\n if (!(error instanceof CsrfOriginMismatchError)) {\r\n throw error;\r\n }\r\n\r\n log.error(\"http\", \"auth\", error);\r\n\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 let decoded: DecodedAccessToken;\r\n\r\n // The ONLY try in this middleware, and it wraps a single call. Everything\r\n // after it is storage and configuration, where an exception means the\r\n // server is broken rather than the caller (D5).\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) {\r\n // A DB/cache outage, a missing `JWT_SECRET`, or any other server-side\r\n // fault — not a verdict on this credential. Propagate to the request's\r\n // normal error path (a 500 monitoring can see) instead of answering\r\n // 401 and clearing the caller's session.\r\n throw error;\r\n }\r\n\r\n // A forged, malformed, expired token — or (D6) a `tokenType` mismatch —\r\n // is not an incident: it is a request carrying a credential the server\r\n // will never accept.\r\n log.error(\"http\", \"auth\", error);\r\n\r\n // Clearing rather than leaving a stale value matters: a request that\r\n // failed authentication must not carry the identity of whoever\r\n // `request.locals.user` last held (see the two-sided spec below that\r\n // pins this — removing the clear must make the previous identity\r\n // survive).\r\n request.locals.user = undefined;\r\n\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n },\r\n );\r\n }\r\n\r\n request.decodedAccessToken = decoded;\r\n\r\n // A valid signature is not enough — the token must still exist in storage,\r\n // so deleting the row (logout) invalidates it before its JWT expiry.\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n const accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n },\r\n );\r\n }\r\n\r\n // ... and the row must still be live. Existence alone was the whole check\r\n // before 4.12.0, so a row whose own `expires_at` had passed still opened\r\n // the gate. The stored expiry is now enforced, and the dead row is\r\n // removed on the way out rather than left for the cleanup command.\r\n if (accessTokenRowIsExpired(accessToken)) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n },\r\n );\r\n }\r\n\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n },\r\n );\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n },\r\n );\r\n }\r\n\r\n if (!(await authService.canAuthenticate(currentUser))) {\r\n return rejectUnauthorized(\r\n { request, response },\r\n {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n },\r\n );\r\n }\r\n\r\n request.locals.user = currentUser;\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAS,mBAAmB,EAAE,SAAS,YAAyB,SAA8B;CAC5F,MAAM,YAAY,WAAW,SAAS,UAAU;CAEhD,IAAI,aAAa,QAAQ,OAAO,QAAQ;EAMtC,MAAM,cAAc,mBAAmB,WAAW,QAAQ,MAAM;EAChE,MAAM,YAAY,YAAY,SAAS,GAAG,IAAI,MAAM;EACpD,MAAM,YAAY,mBAAmB,QAAQ,GAAG;EAEhD,OAAO,SAAS,SACd,GAAG,cAAc,YAAY,WAAW,SAAS,eAAe,EAAE,GAAG,WACvE;CACF;CAEA,OAAO,SAAS,aAAa,OAAO;AACtC;;;;;;;;;;;;;;;;AA0BA,SAAS,wBAAwB,aAAmC;CAClE,OAAO,OAAO,YAAY,cAAc,YAAY,YAAY,YAAY;AAC9E;AAEA,SAAS,eAAe,SAAkB,WAA8B;CACtE,IAAI,cAAc,UAChB,OAAO,QAAQ;CAGjB,MAAM,QAAQ,QAAQ,OAAO,UAAU,MAAM,CAAgB,CAAC;CAE9D,OAAO,QAAQ,OAAO,KAAK,IAAI;AACjC;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,eACd,iBACA,YAAuB,UACX;CACZ,MAAM,eAAe,MAAM,QAAQ,eAAe,IAAI,kBAAkB,CAAC,eAAe;CAExF,MAAM,OAAmB,OAAO,EAAE,SAAS,eAAe;EACxD,MAAM,qBAAqB,eAAe,SAAS,SAAS;EAE5D,IAAI,CAAC,oBACH,OAAO,mBACL;GAAE;GAAS;EAAS,GACpB;GACE,OAAO,EAAE,gCAAgC;GACzC;EACF,CACF;EAMF,IAAI,wBAAwB,WAAW,QAAQ,MAAM,GACnD,IAAI;GACF,wBAAwB,OAAO;EACjC,SAAS,OAAO;GACd,IAAI,EAAE,iBAAiB,0BACrB,MAAM;GAGR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAE/B,OAAO,SAAS,UAAU;IACxB,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAGF,IAAI;EAKJ,IAAI;GACF,UAAU,MAAM,IAAI,OAA2B,kBAAkB;EACnE,SAAS,OAAO;GACd,IAAI,CAAC,yBAAyB,KAAK,GAKjC,MAAM;GAMR,IAAI,MAAM,QAAQ,QAAQ,KAAK;GAO/B,QAAQ,OAAO,OAAO;GAEtB,OAAO,mBACL;IAAE;IAAS;GAAS,GACpB;IACE,OAAO,EAAE,gCAAgC;IACzC;GACF,CACF;EACF;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,mBACL;GAAE;GAAS;EAAS,GACpB;GACE,OAAO,EAAE,gCAAgC;GACzC;EACF,CACF;EAOF,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBACL;IAAE;IAAS;GAAS,GACpB;IACE,OAAO,EAAE,gCAAgC;IACzC;GACF,CACF;EACF;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,mBACL;GAAE;GAAS;EAAS,GACpB;GACE,OAAO,EAAE,0BAA0B;GACnC;EACF,CACF;EAGF,MAAM,YAAY,OAAO,IAAI,iBAAiB,UAAU;EAExD,IAAI,CAAC,WAGH,MAAM,IAAI,MAAM,aAAa,SAAS,kBAAkB;EAG1D,MAAM,cAAc,MAAM,UAAU,KAAK,QAAQ,EAAE;EAEnD,IAAI,CAAC,aAAa;GAChB,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBACL;IAAE;IAAS;GAAS,GACpB;IACE,OAAO,EAAE,gCAAgC;IACzC;GACF,CACF;EACF;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,mBACL;GAAE;GAAS;EAAS,GACpB;GACE,OAAO,EAAE,0BAA0B;GACnC;EACF,CACF;EAGF,QAAQ,OAAO,OAAO;CACxB;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"auth.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/auth.middleware.ts"],"sourcesContent":["import { config, type HttpContext, t, type Middleware, type Request } 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 { AccessToken } from \"../models/access-token\";\r\nimport { authConfig } from \"../services/auth-config\";\r\nimport { authService } from \"../services/auth.service\";\r\nimport { isInvalidCredentialError, jwt } from \"../services/jwt\";\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\n/**\r\n * Decoded access-token claims the middleware reads. The full payload carries\r\n * more (`created_at`, `tokenType`, `iat`, `exp`) but only these drive routing.\r\n */\r\ntype DecodedAccessToken = {\r\n id: string | number;\r\n userType?: string;\r\n};\r\n\r\n/**\r\n * Whether the persisted row says the token is dead.\r\n *\r\n * The answer belongs to the model (`AccessToken.isExpired`), so a registered\r\n * override that renames or reshapes its expiry column stays authoritative and\r\n * the middleware never touches a column name. A row that cannot answer at all —\r\n * an override that dropped the getter — is treated as **expired**: the failure\r\n * mode of this whole defect class was a check that quietly answered \"fine\" when\r\n * it had nothing to check, and that is not repeated here.\r\n *\r\n * This is independent of the `exp` claim required by `jwt.verify`. That guard\r\n * catches a token whose *claims* carry no deadline; this one catches a token\r\n * whose *row* says the deadline has passed — a logged-out or expired session\r\n * whose JWT is still within its own lifetime. Neither subsumes the other.\r\n */\r\nfunction accessTokenRowIsExpired(accessToken: AccessToken): boolean {\r\n return typeof accessToken.isExpired === \"boolean\" ? accessToken.isExpired : true;\r\n}\r\n\r\nfunction readCredential(request: Request, tokenFrom: TokenFrom): string {\r\n if (tokenFrom === \"header\") {\r\n return request.authorizationValue;\r\n }\r\n\r\n const value = request.cookie(tokenFrom.slice(\"cookie:\".length));\r\n\r\n return value ? String(value) : \"\";\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 tokenFrom = options ? tokenFromDescriptor(options) : (legacyTokenFrom ?? \"header\");\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 let authorizationValue = readCredential(request, tokenFrom);\r\n let renewalAttempted = false;\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 (authorizationValue || refreshCredential) &&\r\n requiresCsrfOriginCheck(tokenFrom, 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 renew = async (): Promise<boolean> => {\r\n if (!refreshCredential || renewalAttempted) return false;\r\n\r\n renewalAttempted = true;\r\n const refreshToken = readCredential(request, refreshCredential);\r\n if (!refreshToken) return false;\r\n\r\n const pair = await authService.renewAutomaticSession(refreshToken, renewalUserType!, {\r\n overlapMs: options?.refresh?.overlapMs,\r\n });\r\n if (!pair?.refreshToken) return false;\r\n\r\n authService.setAuthCookie(response, pair.accessToken, {\r\n name: tokenFrom.slice(\"cookie:\".length),\r\n });\r\n authService.setAuthCookie(response, pair.refreshToken, {\r\n name: refreshCredential.slice(\"cookie:\".length),\r\n });\r\n authorizationValue = pair.accessToken.token;\r\n\r\n return true;\r\n };\r\n\r\n let decoded: DecodedAccessToken;\r\n let accessToken: AccessToken | null;\r\n\r\n // At most one renewal is attempted per request. The renewed access token\r\n // is checked through this normal path; no handler or unsafe request replay\r\n // is performed.\r\n while (true) {\r\n if (!authorizationValue) {\r\n request.locals.user = undefined;\r\n if (await renew()) continue;\r\n if (optional) return;\r\n return reject({\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\r\n });\r\n }\r\n\r\n try {\r\n decoded = await jwt.verify<DecodedAccessToken>(authorizationValue);\r\n } catch (error) {\r\n if (!isInvalidCredentialError(error)) throw error;\r\n log.error(\"http\", \"auth\", error);\r\n request.locals.user = undefined;\r\n if (await renew()) continue;\r\n if (optional) return;\r\n return reject({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n request.decodedAccessToken = decoded;\r\n const AccessTokenModel = config.key(\"auth.accessToken.model\", AccessToken);\r\n accessToken = await AccessTokenModel.findByToken(authorizationValue);\r\n\r\n if (!accessToken) {\r\n request.locals.user = undefined;\r\n if (await renew()) continue;\r\n if (optional) return;\r\n return reject({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n if (accessTokenRowIsExpired(accessToken)) {\r\n await accessToken.destroy();\r\n request.locals.user = undefined;\r\n if (await renew()) continue;\r\n if (optional) return;\r\n return reject({\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\r\n });\r\n }\r\n\r\n break;\r\n }\r\n const userType = decoded.userType ?? accessToken.userType;\r\n\r\n if (allowedTypes.length && !allowedTypes.includes(userType)) {\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 const UserModel = config.key(`auth.userType.${userType}`);\r\n\r\n if (!UserModel) {\r\n // Configuration, not credentials. Throwing keeps a mis-registered app\r\n // loudly broken instead of quietly rejecting every request of this type.\r\n throw new Error(`User type ${userType} is unknown type.`);\r\n }\r\n\r\n const currentUser = await UserModel.find(decoded.id);\r\n\r\n if (!currentUser) {\r\n await accessToken.destroy();\r\n request.locals.user = undefined;\r\n if (optional) return;\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 if (!(await authService.canAuthenticate(currentUser))) {\r\n request.locals.user = undefined;\r\n if (optional) return;\r\n\r\n return reject({\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n request.locals.user = currentUser;\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":"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"}