@warlock.js/auth 5.11.0 → 5.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +88 -86
  3. package/esm/commands/auth-cleanup-command.mjs +1 -1
  4. package/esm/commands/auth-cleanup-command.mjs.map +1 -1
  5. package/esm/contracts/index.d.mts +4 -2
  6. package/esm/contracts/providers.d.mts +101 -0
  7. package/esm/contracts/providers.d.mts.map +1 -0
  8. package/esm/contracts/request-locals.d.mts +43 -0
  9. package/esm/contracts/request-locals.d.mts.map +1 -0
  10. package/esm/contracts/types.d.mts +159 -1
  11. package/esm/contracts/types.d.mts.map +1 -1
  12. package/esm/contracts/types.mjs.map +1 -1
  13. package/esm/errors/auth-provider-sdk-missing.error.d.mts +17 -0
  14. package/esm/errors/auth-provider-sdk-missing.error.d.mts.map +1 -0
  15. package/esm/errors/auth-provider-sdk-missing.error.mjs +22 -0
  16. package/esm/errors/auth-provider-sdk-missing.error.mjs.map +1 -0
  17. package/esm/errors/email-not-verified.error.d.mts +16 -0
  18. package/esm/errors/email-not-verified.error.d.mts.map +1 -0
  19. package/esm/errors/email-not-verified.error.mjs +20 -0
  20. package/esm/errors/email-not-verified.error.mjs.map +1 -0
  21. package/esm/errors/index.d.mts +7 -0
  22. package/esm/errors/index.mjs +9 -0
  23. package/esm/errors/invalid-one-time-token.error.d.mts +17 -0
  24. package/esm/errors/invalid-one-time-token.error.d.mts.map +1 -0
  25. package/esm/errors/invalid-one-time-token.error.mjs +21 -0
  26. package/esm/errors/invalid-one-time-token.error.mjs.map +1 -0
  27. package/esm/errors/invalid-passkey.error.d.mts +18 -0
  28. package/esm/errors/invalid-passkey.error.d.mts.map +1 -0
  29. package/esm/errors/invalid-passkey.error.mjs +23 -0
  30. package/esm/errors/invalid-passkey.error.mjs.map +1 -0
  31. package/esm/errors/invalid-provider-callback.error.d.mts +18 -0
  32. package/esm/errors/invalid-provider-callback.error.d.mts.map +1 -0
  33. package/esm/errors/invalid-provider-callback.error.mjs +23 -0
  34. package/esm/errors/invalid-provider-callback.error.mjs.map +1 -0
  35. package/esm/errors/notifications-unavailable.error.d.mts +15 -0
  36. package/esm/errors/notifications-unavailable.error.d.mts.map +1 -0
  37. package/esm/errors/notifications-unavailable.error.mjs +20 -0
  38. package/esm/errors/notifications-unavailable.error.mjs.map +1 -0
  39. package/esm/errors/provider-email-not-verified.error.d.mts +16 -0
  40. package/esm/errors/provider-email-not-verified.error.d.mts.map +1 -0
  41. package/esm/errors/provider-email-not-verified.error.mjs +20 -0
  42. package/esm/errors/provider-email-not-verified.error.mjs.map +1 -0
  43. package/esm/index.d.mts +31 -3
  44. package/esm/index.mjs +29 -1
  45. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  46. package/esm/middleware/auth.middleware.mjs +13 -2
  47. package/esm/middleware/auth.middleware.mjs.map +1 -1
  48. package/esm/middleware/csrf-origin-check.d.mts +40 -0
  49. package/esm/middleware/csrf-origin-check.d.mts.map +1 -0
  50. package/esm/middleware/csrf-origin-check.mjs +105 -0
  51. package/esm/middleware/csrf-origin-check.mjs.map +1 -0
  52. package/esm/middleware/index.d.mts +5 -1
  53. package/esm/middleware/index.mjs +4 -0
  54. package/esm/middleware/one-time-token-throttle.middleware.d.mts +36 -0
  55. package/esm/middleware/one-time-token-throttle.middleware.d.mts.map +1 -0
  56. package/esm/middleware/one-time-token-throttle.middleware.mjs +53 -0
  57. package/esm/middleware/one-time-token-throttle.middleware.mjs.map +1 -0
  58. package/esm/middleware/otp-throttle.middleware.d.mts +30 -0
  59. package/esm/middleware/otp-throttle.middleware.d.mts.map +1 -0
  60. package/esm/middleware/otp-throttle.middleware.mjs +45 -0
  61. package/esm/middleware/otp-throttle.middleware.mjs.map +1 -0
  62. package/esm/middleware/require-verified-email.middleware.d.mts +18 -0
  63. package/esm/middleware/require-verified-email.middleware.d.mts.map +1 -0
  64. package/esm/middleware/require-verified-email.middleware.mjs +24 -0
  65. package/esm/middleware/require-verified-email.middleware.mjs.map +1 -0
  66. package/esm/models/access-token/access-token.model.d.mts +8 -0
  67. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  68. package/esm/models/access-token/access-token.model.mjs +3 -0
  69. package/esm/models/access-token/access-token.model.mjs.map +1 -1
  70. package/esm/models/index.d.mts +3 -1
  71. package/esm/models/index.d.mts.map +1 -1
  72. package/esm/models/index.mjs +16 -1
  73. package/esm/models/index.mjs.map +1 -1
  74. package/esm/models/one-time-token/index.d.mts +1 -0
  75. package/esm/models/one-time-token/index.mjs +3 -0
  76. package/esm/models/one-time-token/migration.mjs +31 -0
  77. package/esm/models/one-time-token/migration.mjs.map +1 -0
  78. package/esm/models/one-time-token/one-time-token.model.d.mts +173 -0
  79. package/esm/models/one-time-token/one-time-token.model.d.mts.map +1 -0
  80. package/esm/models/one-time-token/one-time-token.model.mjs +151 -0
  81. package/esm/models/one-time-token/one-time-token.model.mjs.map +1 -0
  82. package/esm/models/passkey-credential/index.d.mts +1 -0
  83. package/esm/models/passkey-credential/index.mjs +3 -0
  84. package/esm/models/passkey-credential/migration.mjs +25 -0
  85. package/esm/models/passkey-credential/migration.mjs.map +1 -0
  86. package/esm/models/passkey-credential/passkey-credential.model.d.mts +108 -0
  87. package/esm/models/passkey-credential/passkey-credential.model.d.mts.map +1 -0
  88. package/esm/models/passkey-credential/passkey-credential.model.mjs +70 -0
  89. package/esm/models/passkey-credential/passkey-credential.model.mjs.map +1 -0
  90. package/esm/models/provider-account/index.d.mts +1 -0
  91. package/esm/models/provider-account/index.mjs +3 -0
  92. package/esm/models/provider-account/migration.mjs +25 -0
  93. package/esm/models/provider-account/migration.mjs.map +1 -0
  94. package/esm/models/provider-account/provider-account.model.d.mts +75 -0
  95. package/esm/models/provider-account/provider-account.model.d.mts.map +1 -0
  96. package/esm/models/provider-account/provider-account.model.mjs +55 -0
  97. package/esm/models/provider-account/provider-account.model.mjs.map +1 -0
  98. package/esm/models/refresh-token/refresh-token.model.d.mts +7 -0
  99. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  100. package/esm/models/refresh-token/refresh-token.model.mjs +3 -0
  101. package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
  102. package/esm/otp/index.d.mts +1 -0
  103. package/esm/otp/index.mjs +3 -0
  104. package/esm/otp/otp.d.mts +38 -0
  105. package/esm/otp/otp.d.mts.map +1 -0
  106. package/esm/otp/otp.mjs +105 -0
  107. package/esm/otp/otp.mjs.map +1 -0
  108. package/esm/passkeys/index.d.mts +3 -0
  109. package/esm/passkeys/index.mjs +4 -0
  110. package/esm/passkeys/passkey-authentication.d.mts +33 -0
  111. package/esm/passkeys/passkey-authentication.d.mts.map +1 -0
  112. package/esm/passkeys/passkey-authentication.mjs +93 -0
  113. package/esm/passkeys/passkey-authentication.mjs.map +1 -0
  114. package/esm/passkeys/passkey-challenges.mjs +53 -0
  115. package/esm/passkeys/passkey-challenges.mjs.map +1 -0
  116. package/esm/passkeys/passkey-registration.d.mts +29 -0
  117. package/esm/passkeys/passkey-registration.d.mts.map +1 -0
  118. package/esm/passkeys/passkey-registration.mjs +88 -0
  119. package/esm/passkeys/passkey-registration.mjs.map +1 -0
  120. package/esm/passkeys/simplewebauthn.d.mts +21 -0
  121. package/esm/passkeys/simplewebauthn.d.mts.map +1 -0
  122. package/esm/passkeys/simplewebauthn.mjs +13 -0
  123. package/esm/passkeys/simplewebauthn.mjs.map +1 -0
  124. package/esm/providers/google-provider.d.mts +40 -0
  125. package/esm/providers/google-provider.d.mts.map +1 -0
  126. package/esm/providers/google-provider.mjs +105 -0
  127. package/esm/providers/google-provider.mjs.map +1 -0
  128. package/esm/providers/index.d.mts +2 -0
  129. package/esm/providers/index.mjs +4 -0
  130. package/esm/providers/provider-login.d.mts +43 -0
  131. package/esm/providers/provider-login.d.mts.map +1 -0
  132. package/esm/providers/provider-login.mjs +86 -0
  133. package/esm/providers/provider-login.mjs.map +1 -0
  134. package/esm/providers/provider-state-cookie.mjs +63 -0
  135. package/esm/providers/provider-state-cookie.mjs.map +1 -0
  136. package/esm/providers/resolve-provider-user.mjs +45 -0
  137. package/esm/providers/resolve-provider-user.mjs.map +1 -0
  138. package/esm/services/auth-config.mjs +84 -0
  139. package/esm/services/auth-config.mjs.map +1 -1
  140. package/esm/services/auth.service.d.mts +59 -2
  141. package/esm/services/auth.service.d.mts.map +1 -1
  142. package/esm/services/auth.service.mjs +86 -1
  143. package/esm/services/auth.service.mjs.map +1 -1
  144. package/esm/services/current-user.d.mts +21 -0
  145. package/esm/services/current-user.d.mts.map +1 -0
  146. package/esm/services/current-user.mjs +24 -0
  147. package/esm/services/current-user.mjs.map +1 -0
  148. package/esm/services/email-verification.d.mts +35 -0
  149. package/esm/services/email-verification.d.mts.map +1 -0
  150. package/esm/services/email-verification.mjs +55 -0
  151. package/esm/services/email-verification.mjs.map +1 -0
  152. package/esm/services/index.d.mts +5 -1
  153. package/esm/services/index.mjs +4 -0
  154. package/esm/services/one-time-tokens.d.mts +32 -0
  155. package/esm/services/one-time-tokens.d.mts.map +1 -0
  156. package/esm/services/one-time-tokens.mjs +56 -0
  157. package/esm/services/one-time-tokens.mjs.map +1 -0
  158. package/esm/services/optional-peer.mjs +37 -0
  159. package/esm/services/optional-peer.mjs.map +1 -0
  160. package/esm/services/password-reset.d.mts +34 -0
  161. package/esm/services/password-reset.d.mts.map +1 -0
  162. package/esm/services/password-reset.mjs +81 -0
  163. package/esm/services/password-reset.mjs.map +1 -0
  164. package/esm/services/token-notifications.mjs +87 -0
  165. package/esm/services/token-notifications.mjs.map +1 -0
  166. package/esm/utils/auth-error-codes.d.mts +48 -1
  167. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  168. package/esm/utils/auth-error-codes.mjs +41 -0
  169. package/esm/utils/auth-error-codes.mjs.map +1 -1
  170. package/esm/utils/index.mjs +1 -0
  171. package/esm/utils/safe-equal.d.mts +6 -0
  172. package/esm/utils/safe-equal.d.mts.map +1 -0
  173. package/esm/utils/safe-equal.mjs +13 -0
  174. package/esm/utils/safe-equal.mjs.map +1 -0
  175. package/llms-full.txt +402 -29
  176. package/llms.txt +2 -0
  177. package/package.json +21 -7
  178. package/skills/handle-login-and-logout/SKILL.md +1 -1
  179. package/skills/login-with-providers/SKILL.md +154 -0
  180. package/skills/manage-tokens/SKILL.md +7 -1
  181. package/skills/overview/SKILL.md +1 -1
  182. package/skills/protect-routes/SKILL.md +71 -13
  183. package/skills/register-user/SKILL.md +6 -8
  184. package/skills/run-auth-commands/SKILL.md +1 -1
  185. package/skills/verify-email-and-reset-password/SKILL.md +137 -0
@@ -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\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 const separator = loginPath.includes(\"?\") ? \"&\" : \"?\";\r\n const returnUrl = encodeURIComponent(request.url);\r\n\r\n return response.redirect(\r\n `${loginPath}${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({ request, response }, {\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\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 request.clearCurrentUser();\r\n\r\n return rejectUnauthorized({ request, response }, {\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\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({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\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({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\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({ request, response }, {\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\r\n return rejectUnauthorized({ request, response }, {\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 return rejectUnauthorized({ request, response }, {\r\n error: t(\"auth.errors.unauthorized\"),\r\n errorCode: AuthErrorCodes.Unauthorized,\r\n });\r\n }\r\n\r\n request.user = currentUser;\r\n };\r\n\r\n return auth;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAAS,mBAAmB,EAAE,SAAS,YAAyB,SAA8B;CAC5F,MAAM,YAAY,WAAW,SAAS,UAAU;CAEhD,IAAI,aAAa,QAAQ,OAAO,QAAQ;EACtC,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,MAAM;EAClD,MAAM,YAAY,mBAAmB,QAAQ,GAAG;EAEhD,OAAO,SAAS,SACd,GAAG,YAAY,YAAY,WAAW,SAAS,eAAe,EAAE,GAAG,WACrE;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,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAGH,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;GAE/B,QAAQ,iBAAiB;GAEzB,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAOH,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,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,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,QAAQ,OAAO;CACjB;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 { 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\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 const separator = loginPath.includes(\"?\") ? \"&\" : \"?\";\r\n const returnUrl = encodeURIComponent(request.url);\r\n\r\n return response.redirect(\r\n `${loginPath}${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({ request, response }, {\r\n error: t(\"auth.errors.missingAccessToken\"),\r\n errorCode: AuthErrorCodes.MissingAccessToken,\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({ request, response }, {\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\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({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\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({ request, response }, {\r\n error: t(\"auth.errors.invalidAccessToken\"),\r\n errorCode: AuthErrorCodes.InvalidAccessToken,\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({ request, response }, {\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\r\n return rejectUnauthorized({ request, response }, {\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 return rejectUnauthorized({ request, response }, {\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,mBAAmB,EAAE,SAAS,YAAyB,SAA8B;CAC5F,MAAM,YAAY,WAAW,SAAS,UAAU;CAEhD,IAAI,aAAa,QAAQ,OAAO,QAAQ;EACtC,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,MAAM;EAClD,MAAM,YAAY,mBAAmB,QAAQ,GAAG;EAEhD,OAAO,SAAS,SACd,GAAG,YAAY,YAAY,WAAW,SAAS,eAAe,EAAE,GAAG,WACrE;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,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAMH,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,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,QAAQ,qBAAqB;EAK7B,MAAM,cAAc,MADK,OAAO,IAAI,0BAA0B,WACrB,CAAC,CAAC,YAAY,kBAAkB;EAEzE,IAAI,CAAC,aACH,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,gCAAgC;GACzC;EACF,CAAC;EAOH,IAAI,wBAAwB,WAAW,GAAG;GACxC,MAAM,YAAY,QAAQ;GAE1B,OAAO,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,MAAM,WAAW,QAAQ,YAAY,YAAY;EAEjD,IAAI,aAAa,UAAU,CAAC,aAAa,SAAS,QAAQ,GACxD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,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,mBAAmB;IAAE;IAAS;GAAS,GAAG;IAC/C,OAAO,EAAE,gCAAgC;IACzC;GACF,CAAC;EACH;EAEA,IAAI,CAAE,MAAM,YAAY,gBAAgB,WAAW,GACjD,OAAO,mBAAmB;GAAE;GAAS;EAAS,GAAG;GAC/C,OAAO,EAAE,0BAA0B;GACnC;EACF,CAAC;EAGH,QAAQ,OAAO,OAAO;CACxB;CAEA,OAAO;AACT"}
@@ -0,0 +1,40 @@
1
+ import { TokenFrom } from "../contracts/types.mjs";
2
+ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
3
+ import { Request } from "@warlock.js/core";
4
+
5
+ //#region ../auth/src/middleware/csrf-origin-check.d.ts
6
+ /** Why {@link assertCsrfOriginAllowed} refused a request. */
7
+ type CsrfOriginMismatchReason = "origin-mismatch" | "referer-mismatch" | "missing-origin-and-referer";
8
+ /**
9
+ * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`
10
+ * failed the check. `reason` names exactly why,
11
+ * so an error transformer or an incident log never has to string-match the
12
+ * message.
13
+ */
14
+ declare class CsrfOriginMismatchError extends Error {
15
+ readonly reason: CsrfOriginMismatchReason;
16
+ readonly code = AuthErrorCodes.CsrfOriginMismatch;
17
+ constructor(reason: CsrfOriginMismatchReason);
18
+ }
19
+ /**
20
+ * Whether a request is in scope for the CSRF Origin check at all — only a
21
+ * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.
22
+ * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:
23
+ * only a cookie-sourced credential can be silently replayed cross-site by a
24
+ * browser, so only it needs the check.
25
+ */
26
+ declare function requiresCsrfOriginCheck(tokenFrom: TokenFrom, method: string): boolean;
27
+ /**
28
+ * CSRF Origin check for a cookie-authenticated unsafe-method request (lead
29
+ * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`
30
+ * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.
31
+ * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH
32
+ * headers are absent.
33
+ *
34
+ * Callers gate this behind {@link requiresCsrfOriginCheck} — this function
35
+ * itself does not re-check the token source or the method.
36
+ */
37
+ declare function assertCsrfOriginAllowed(request: Request): void;
38
+ //#endregion
39
+ export { CsrfOriginMismatchError, CsrfOriginMismatchReason, assertCsrfOriginAllowed, requiresCsrfOriginCheck };
40
+ //# sourceMappingURL=csrf-origin-check.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-origin-check.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/csrf-origin-check.ts"],"mappings":";;;;;;KASY,wBAAA;AAAZ;;;;AAAoC;AAWpC;AAXA,cAWa,uBAAA,SAAgC,KAAA;EAAA,SAGf,MAAA,EAAQ,wBAAA;EAAA,SAF3B,IAAA,GAAI,cAAA,CAAA,kBAAA;cAEe,MAAA,EAAQ,wBAAA;AAAA;;;;;;;;iBAatB,uBAAA,CAAwB,SAAA,EAAW,SAAS,EAAE,MAAA;;;;;;AAbA;AAa9D;;;;iBAmEgB,uBAAA,CAAwB,OAAgB,EAAP,OAAO"}
@@ -0,0 +1,105 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
3
+
4
+ //#region ../auth/src/middleware/csrf-origin-check.ts
5
+ /** HTTP methods the CSRF Origin check exempts — reads, never state changes. */
6
+ const SAFE_METHODS = new Set([
7
+ "GET",
8
+ "HEAD",
9
+ "OPTIONS"
10
+ ]);
11
+ /**
12
+ * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`
13
+ * failed the check. `reason` names exactly why,
14
+ * so an error transformer or an incident log never has to string-match the
15
+ * message.
16
+ */
17
+ var CsrfOriginMismatchError = class extends Error {
18
+ constructor(reason) {
19
+ super(`CSRF Origin check failed: ${reason}`);
20
+ this.reason = reason;
21
+ this.code = "EC006";
22
+ this.name = "CsrfOriginMismatchError";
23
+ }
24
+ };
25
+ /**
26
+ * Whether a request is in scope for the CSRF Origin check at all — only a
27
+ * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.
28
+ * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:
29
+ * only a cookie-sourced credential can be silently replayed cross-site by a
30
+ * browser, so only it needs the check.
31
+ */
32
+ function requiresCsrfOriginCheck(tokenFrom, method) {
33
+ return tokenFrom.startsWith("cookie:") && !SAFE_METHODS.has(method.toUpperCase());
34
+ }
35
+ /**
36
+ * The request's own origin — what an `Origin`/`Referer` header must match.
37
+ *
38
+ * `request.hostname` (core's `Request`, backed by Express/Fastify's
39
+ * `hostname`) never carries a port. A browser's `Origin` header on a
40
+ * non-default port (e.g. any `warlock dev` session) does, so the port must
41
+ * come from the raw `Host` header instead — core exposes no getter for that,
42
+ * so it is read directly here.
43
+ */
44
+ function ownOrigin(request) {
45
+ const hostHeader = request.header("host");
46
+ const host = typeof hostHeader === "string" && hostHeader ? hostHeader : request.hostname;
47
+ return `${request.protocol}://${host}`;
48
+ }
49
+ /** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */
50
+ function originOf(rawUrl) {
51
+ try {
52
+ const url = new URL(rawUrl);
53
+ return `${url.protocol}//${url.host}`;
54
+ } catch {
55
+ return;
56
+ }
57
+ }
58
+ /**
59
+ * Normalize an origin string (`scheme://host[:port]`) so that a default port
60
+ * (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin
61
+ * written without a port. Falls back to the raw value if it does not parse
62
+ * as a URL (in which case it will simply fail the exact-match comparison).
63
+ */
64
+ function normalizeOrigin(origin) {
65
+ try {
66
+ const url = new URL(origin);
67
+ const isDefaultPort = url.protocol === "http:" && (url.port === "" || url.port === "80") || url.protocol === "https:" && (url.port === "" || url.port === "443");
68
+ return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;
69
+ } catch {
70
+ return origin;
71
+ }
72
+ }
73
+ /** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */
74
+ function isAllowedOrigin(origin, request) {
75
+ if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;
76
+ return authConfig.csrf.allowedOrigins().includes(origin);
77
+ }
78
+ /**
79
+ * CSRF Origin check for a cookie-authenticated unsafe-method request (lead
80
+ * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`
81
+ * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.
82
+ * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH
83
+ * headers are absent.
84
+ *
85
+ * Callers gate this behind {@link requiresCsrfOriginCheck} — this function
86
+ * itself does not re-check the token source or the method.
87
+ */
88
+ function assertCsrfOriginAllowed(request) {
89
+ const origin = request.origin;
90
+ if (origin) {
91
+ if (isAllowedOrigin(origin, request)) return;
92
+ throw new CsrfOriginMismatchError("origin-mismatch");
93
+ }
94
+ const referer = request.header("referer");
95
+ const refererOrigin = typeof referer === "string" ? originOf(referer) : void 0;
96
+ if (refererOrigin) {
97
+ if (isAllowedOrigin(refererOrigin, request)) return;
98
+ throw new CsrfOriginMismatchError("referer-mismatch");
99
+ }
100
+ throw new CsrfOriginMismatchError("missing-origin-and-referer");
101
+ }
102
+
103
+ //#endregion
104
+ export { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck };
105
+ //# sourceMappingURL=csrf-origin-check.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"csrf-origin-check.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/csrf-origin-check.ts"],"sourcesContent":["import type { Request } from \"@warlock.js/core\";\nimport type { TokenFrom } from \"../contracts/types\";\nimport { authConfig } from \"../services/auth-config\";\nimport { AuthErrorCodes } from \"../utils/auth-error-codes\";\n\n/** HTTP methods the CSRF Origin check exempts — reads, never state changes. */\nconst SAFE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\"]);\n\n/** Why {@link assertCsrfOriginAllowed} refused a request. */\nexport type CsrfOriginMismatchReason =\n | \"origin-mismatch\"\n | \"referer-mismatch\"\n | \"missing-origin-and-referer\";\n\n/**\n * A cookie-authenticated, unsafe-method request whose `Origin`/`Referer`\n * failed the check. `reason` names exactly why,\n * so an error transformer or an incident log never has to string-match the\n * message.\n */\nexport class CsrfOriginMismatchError extends Error {\n readonly code = AuthErrorCodes.CsrfOriginMismatch;\n\n constructor(public readonly reason: CsrfOriginMismatchReason) {\n super(`CSRF Origin check failed: ${reason}`);\n this.name = \"CsrfOriginMismatchError\";\n }\n}\n\n/**\n * Whether a request is in scope for the CSRF Origin check at all — only a\n * `cookie:`-sourced credential on an unsafe method (POST/PUT/PATCH/DELETE) is.\n * Header-token auth and safe methods (GET/HEAD/OPTIONS) are always exempt:\n * only a cookie-sourced credential can be silently replayed cross-site by a\n * browser, so only it needs the check.\n */\nexport function requiresCsrfOriginCheck(tokenFrom: TokenFrom, method: string): boolean {\n return tokenFrom.startsWith(\"cookie:\") && !SAFE_METHODS.has(method.toUpperCase());\n}\n\n/**\n * The request's own origin — what an `Origin`/`Referer` header must match.\n *\n * `request.hostname` (core's `Request`, backed by Express/Fastify's\n * `hostname`) never carries a port. A browser's `Origin` header on a\n * non-default port (e.g. any `warlock dev` session) does, so the port must\n * come from the raw `Host` header instead — core exposes no getter for that,\n * so it is read directly here.\n */\nfunction ownOrigin(request: Request): string {\n const hostHeader = request.header(\"host\");\n const host = typeof hostHeader === \"string\" && hostHeader ? hostHeader : request.hostname;\n\n return `${request.protocol}://${host}`;\n}\n\n/** Extract `scheme://host` from a full URL (e.g. a `Referer` header value). */\nfunction originOf(rawUrl: string): string | undefined {\n try {\n const url = new URL(rawUrl);\n\n return `${url.protocol}//${url.host}`;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Normalize an origin string (`scheme://host[:port]`) so that a default port\n * (`:80` on `http:`, `:443` on `https:`) compares equal to the same origin\n * written without a port. Falls back to the raw value if it does not parse\n * as a URL (in which case it will simply fail the exact-match comparison).\n */\nfunction normalizeOrigin(origin: string): string {\n try {\n const url = new URL(origin);\n const isDefaultPort =\n (url.protocol === \"http:\" && (url.port === \"\" || url.port === \"80\")) ||\n (url.protocol === \"https:\" && (url.port === \"\" || url.port === \"443\"));\n\n return `${url.protocol}//${isDefaultPort ? url.hostname : url.host}`;\n } catch {\n return origin;\n }\n}\n\n/** Same-origin, or an explicit entry in `auth.csrf.allowedOrigins` (default `[]`). */\nfunction isAllowedOrigin(origin: string, request: Request): boolean {\n if (normalizeOrigin(origin) === normalizeOrigin(ownOrigin(request))) return true;\n\n return authConfig.csrf.allowedOrigins().includes(origin);\n}\n\n/**\n * CSRF Origin check for a cookie-authenticated unsafe-method request (lead\n * decision 3). Allowed when `Origin` — or, when `Origin` is absent, `Referer`\n * — names the request's own origin or an entry in `auth.csrf.allowedOrigins`.\n * Throws {@link CsrfOriginMismatchError} otherwise, including when BOTH\n * headers are absent.\n *\n * Callers gate this behind {@link requiresCsrfOriginCheck} — this function\n * itself does not re-check the token source or the method.\n */\nexport function assertCsrfOriginAllowed(request: Request): void {\n const origin = request.origin;\n\n if (origin) {\n if (isAllowedOrigin(origin, request)) return;\n\n throw new CsrfOriginMismatchError(\"origin-mismatch\");\n }\n\n const referer = request.header(\"referer\");\n const refererOrigin = typeof referer === \"string\" ? originOf(referer) : undefined;\n\n if (refererOrigin) {\n if (isAllowedOrigin(refererOrigin, request)) return;\n\n throw new CsrfOriginMismatchError(\"referer-mismatch\");\n }\n\n throw new CsrfOriginMismatchError(\"missing-origin-and-referer\");\n}\n"],"mappings":";;;;;AAMA,MAAM,eAAe,IAAI,IAAI;CAAC;CAAO;CAAQ;AAAS,CAAC;;;;;;;AAcvD,IAAa,0BAAb,cAA6C,MAAM;CAGjD,YAAY,AAAgB,QAAkC;EAC5D,MAAM,6BAA6B,QAAQ;EADjB;;EAE1B,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,wBAAwB,WAAsB,QAAyB;CACrF,OAAO,UAAU,WAAW,SAAS,KAAK,CAAC,aAAa,IAAI,OAAO,YAAY,CAAC;AAClF;;;;;;;;;;AAWA,SAAS,UAAU,SAA0B;CAC3C,MAAM,aAAa,QAAQ,OAAO,MAAM;CACxC,MAAM,OAAO,OAAO,eAAe,YAAY,aAAa,aAAa,QAAQ;CAEjF,OAAO,GAAG,QAAQ,SAAS,KAAK;AAClC;;AAGA,SAAS,SAAS,QAAoC;CACpD,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAE1B,OAAO,GAAG,IAAI,SAAS,IAAI,IAAI;CACjC,QAAQ;EACN;CACF;AACF;;;;;;;AAQA,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,MAAM,MAAM,IAAI,IAAI,MAAM;EAC1B,MAAM,gBACH,IAAI,aAAa,YAAY,IAAI,SAAS,MAAM,IAAI,SAAS,SAC7D,IAAI,aAAa,aAAa,IAAI,SAAS,MAAM,IAAI,SAAS;EAEjE,OAAO,GAAG,IAAI,SAAS,IAAI,gBAAgB,IAAI,WAAW,IAAI;CAChE,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,gBAAgB,QAAgB,SAA2B;CAClE,IAAI,gBAAgB,MAAM,MAAM,gBAAgB,UAAU,OAAO,CAAC,GAAG,OAAO;CAE5E,OAAO,WAAW,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM;AACzD;;;;;;;;;;;AAYA,SAAgB,wBAAwB,SAAwB;CAC9D,MAAM,SAAS,QAAQ;CAEvB,IAAI,QAAQ;EACV,IAAI,gBAAgB,QAAQ,OAAO,GAAG;EAEtC,MAAM,IAAI,wBAAwB,iBAAiB;CACrD;CAEA,MAAM,UAAU,QAAQ,OAAO,SAAS;CACxC,MAAM,gBAAgB,OAAO,YAAY,WAAW,SAAS,OAAO,IAAI;CAExE,IAAI,eAAe;EACjB,IAAI,gBAAgB,eAAe,OAAO,GAAG;EAE7C,MAAM,IAAI,wBAAwB,kBAAkB;CACtD;CAEA,MAAM,IAAI,wBAAwB,4BAA4B;AAChE"}
@@ -1,2 +1,6 @@
1
1
  import { authMiddleware } from "./auth.middleware.mjs";
2
- import { LoginThrottleIdentifier, LoginThrottleOptions, loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
2
+ import { CsrfOriginMismatchError, CsrfOriginMismatchReason, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./csrf-origin-check.mjs";
3
+ import { LoginThrottleIdentifier, LoginThrottleOptions, loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
4
+ import { tokenConsumeThrottleMiddleware, tokenIssueThrottleMiddleware } from "./one-time-token-throttle.middleware.mjs";
5
+ import { otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware } from "./otp-throttle.middleware.mjs";
6
+ import { requireVerifiedEmail } from "./require-verified-email.middleware.mjs";
@@ -1,4 +1,8 @@
1
+ import { CsrfOriginMismatchError, assertCsrfOriginAllowed, requiresCsrfOriginCheck } from "./csrf-origin-check.mjs";
1
2
  import { authMiddleware } from "./auth.middleware.mjs";
2
3
  import { loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
4
+ import { tokenConsumeThrottleMiddleware, tokenIssueThrottleMiddleware } from "./one-time-token-throttle.middleware.mjs";
5
+ import { otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware } from "./otp-throttle.middleware.mjs";
6
+ import { requireVerifiedEmail } from "./require-verified-email.middleware.mjs";
3
7
 
4
8
  export { };
@@ -0,0 +1,36 @@
1
+ import { LoginThrottleOptions } from "./login-throttle.middleware.mjs";
2
+ import { Middleware } from "@warlock.js/core";
3
+
4
+ //#region ../auth/src/middleware/one-time-token-throttle.middleware.d.ts
5
+ /**
6
+ * Throttle for routes that ISSUE a verification or reset token ("send
7
+ * verification email", "forgot password"). Built on
8
+ * {@link loginThrottleMiddleware}, tracked per email and per IP, with one
9
+ * difference: EVERY request counts. A reset request answers the same success
10
+ * for known and unknown emails, so a failure-aware counter would never trip.
11
+ *
12
+ * Defaults: 3 requests per 1h window, then a 1h lockout (`429`, `EC004`).
13
+ *
14
+ * @example
15
+ * router.post("/auth/forgot-password", forgotPassword, {
16
+ * middleware: [tokenIssueThrottleMiddleware()],
17
+ * });
18
+ */
19
+ declare function tokenIssueThrottleMiddleware(options?: LoginThrottleOptions): Middleware;
20
+ /**
21
+ * Throttle for routes that CONSUME a token ("verify email", "reset
22
+ * password"). Built on {@link loginThrottleMiddleware}: per IP (the body
23
+ * carries a token, not an account), failure-aware — a rejected token counts,
24
+ * a successful use clears the counter.
25
+ *
26
+ * Defaults: 10 failures per 15m window, then a 15m lockout (`429`, `EC004`).
27
+ *
28
+ * @example
29
+ * router.post("/auth/reset-password", resetPasswordController, {
30
+ * middleware: [tokenConsumeThrottleMiddleware()],
31
+ * });
32
+ */
33
+ declare function tokenConsumeThrottleMiddleware(options?: LoginThrottleOptions): Middleware;
34
+ //#endregion
35
+ export { tokenConsumeThrottleMiddleware, tokenIssueThrottleMiddleware };
36
+ //# sourceMappingURL=one-time-token-throttle.middleware.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"one-time-token-throttle.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/one-time-token-throttle.middleware.ts"],"mappings":";;;;;;AAiBA;;;;;;;;AAA4F;AAwB5F;;;iBAxBgB,4BAAA,CAA6B,OAAA,GAAS,oBAAA,GAA4B,UAAU;;;;;AAwBE;;;;;;;;;iBAA9E,8BAAA,CAA+B,OAAA,GAAS,oBAAA,GAA4B,UAAU"}
@@ -0,0 +1,53 @@
1
+ import { loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
2
+
3
+ //#region ../auth/src/middleware/one-time-token-throttle.middleware.ts
4
+ /**
5
+ * Throttle for routes that ISSUE a verification or reset token ("send
6
+ * verification email", "forgot password"). Built on
7
+ * {@link loginThrottleMiddleware}, tracked per email and per IP, with one
8
+ * difference: EVERY request counts. A reset request answers the same success
9
+ * for known and unknown emails, so a failure-aware counter would never trip.
10
+ *
11
+ * Defaults: 3 requests per 1h window, then a 1h lockout (`429`, `EC004`).
12
+ *
13
+ * @example
14
+ * router.post("/auth/forgot-password", forgotPassword, {
15
+ * middleware: [tokenIssueThrottleMiddleware()],
16
+ * });
17
+ */
18
+ function tokenIssueThrottleMiddleware(options = {}) {
19
+ return loginThrottleMiddleware({
20
+ max: 3,
21
+ window: "1h",
22
+ lockoutDuration: "1h",
23
+ by: ["email", "ip"],
24
+ isFailure: () => true,
25
+ ...options
26
+ });
27
+ }
28
+ /**
29
+ * Throttle for routes that CONSUME a token ("verify email", "reset
30
+ * password"). Built on {@link loginThrottleMiddleware}: per IP (the body
31
+ * carries a token, not an account), failure-aware — a rejected token counts,
32
+ * a successful use clears the counter.
33
+ *
34
+ * Defaults: 10 failures per 15m window, then a 15m lockout (`429`, `EC004`).
35
+ *
36
+ * @example
37
+ * router.post("/auth/reset-password", resetPasswordController, {
38
+ * middleware: [tokenConsumeThrottleMiddleware()],
39
+ * });
40
+ */
41
+ function tokenConsumeThrottleMiddleware(options = {}) {
42
+ return loginThrottleMiddleware({
43
+ max: 10,
44
+ window: "15m",
45
+ lockoutDuration: "15m",
46
+ by: ["ip"],
47
+ ...options
48
+ });
49
+ }
50
+
51
+ //#endregion
52
+ export { tokenConsumeThrottleMiddleware, tokenIssueThrottleMiddleware };
53
+ //# sourceMappingURL=one-time-token-throttle.middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"one-time-token-throttle.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/one-time-token-throttle.middleware.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { loginThrottleMiddleware, type LoginThrottleOptions } from \"./login-throttle.middleware\";\n\n/**\n * Throttle for routes that ISSUE a verification or reset token (\"send\n * verification email\", \"forgot password\"). Built on\n * {@link loginThrottleMiddleware}, tracked per email and per IP, with one\n * difference: EVERY request counts. A reset request answers the same success\n * for known and unknown emails, so a failure-aware counter would never trip.\n *\n * Defaults: 3 requests per 1h window, then a 1h lockout (`429`, `EC004`).\n *\n * @example\n * router.post(\"/auth/forgot-password\", forgotPassword, {\n * middleware: [tokenIssueThrottleMiddleware()],\n * });\n */\nexport function tokenIssueThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n return loginThrottleMiddleware({\n max: 3,\n window: \"1h\",\n lockoutDuration: \"1h\",\n by: [\"email\", \"ip\"],\n isFailure: () => true,\n ...options,\n });\n}\n\n/**\n * Throttle for routes that CONSUME a token (\"verify email\", \"reset\n * password\"). Built on {@link loginThrottleMiddleware}: per IP (the body\n * carries a token, not an account), failure-aware — a rejected token counts,\n * a successful use clears the counter.\n *\n * Defaults: 10 failures per 15m window, then a 15m lockout (`429`, `EC004`).\n *\n * @example\n * router.post(\"/auth/reset-password\", resetPasswordController, {\n * middleware: [tokenConsumeThrottleMiddleware()],\n * });\n */\nexport function tokenConsumeThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n return loginThrottleMiddleware({\n max: 10,\n window: \"15m\",\n lockoutDuration: \"15m\",\n by: [\"ip\"],\n ...options,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAiBA,SAAgB,6BAA6B,UAAgC,CAAC,GAAe;CAC3F,OAAO,wBAAwB;EAC7B,KAAK;EACL,QAAQ;EACR,iBAAiB;EACjB,IAAI,CAAC,SAAS,IAAI;EAClB,iBAAiB;EACjB,GAAG;CACL,CAAC;AACH;;;;;;;;;;;;;;AAeA,SAAgB,+BAA+B,UAAgC,CAAC,GAAe;CAC7F,OAAO,wBAAwB;EAC7B,KAAK;EACL,QAAQ;EACR,iBAAiB;EACjB,IAAI,CAAC,IAAI;EACT,GAAG;CACL,CAAC;AACH"}
@@ -0,0 +1,30 @@
1
+ import { LoginThrottleOptions } from "./login-throttle.middleware.mjs";
2
+ import { Middleware } from "@warlock.js/core";
3
+
4
+ //#region ../auth/src/middleware/otp-throttle.middleware.d.ts
5
+ /**
6
+ * Throttle for the route that SENDS a login code. {@link tokenIssueThrottleMiddleware}
7
+ * keyed on the `phone` input and IP: every request counts (the answer is the
8
+ * same for unknown phones), 3 per 1h, then a 1h lockout (`429`, `EC004`).
9
+ *
10
+ * @example
11
+ * router.post("/auth/otp/request", requestOtpController, {
12
+ * middleware: [otpRequestThrottleMiddleware()],
13
+ * });
14
+ */
15
+ declare function otpRequestThrottleMiddleware(options?: LoginThrottleOptions): Middleware;
16
+ /**
17
+ * Throttle for the route that VERIFIES a login code. {@link loginThrottleMiddleware}
18
+ * keyed on `phone` and IP, failure-aware: 5 failures per 15m, then a 15m lockout
19
+ * (`429`, `EC004`). Complements the per-code attempt cap, which a fresh code
20
+ * would otherwise reset.
21
+ *
22
+ * @example
23
+ * router.post("/auth/otp/verify", verifyOtpController, {
24
+ * middleware: [otpVerifyThrottleMiddleware()],
25
+ * });
26
+ */
27
+ declare function otpVerifyThrottleMiddleware(options?: LoginThrottleOptions): Middleware;
28
+ //#endregion
29
+ export { otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware };
30
+ //# sourceMappingURL=otp-throttle.middleware.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp-throttle.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/otp-throttle.middleware.ts"],"mappings":";;;;;;AAcA;;;;;;;;iBAAgB,4BAAA,CAA6B,OAAA,GAAS,oBAAA,GAA4B,UAAU;AAe5F;;;;;;;;AAA2F;;;AAA3F,iBAAgB,2BAAA,CAA4B,OAAA,GAAS,oBAAA,GAA4B,UAAU"}
@@ -0,0 +1,45 @@
1
+ import { loginThrottleMiddleware } from "./login-throttle.middleware.mjs";
2
+ import { tokenIssueThrottleMiddleware } from "./one-time-token-throttle.middleware.mjs";
3
+
4
+ //#region ../auth/src/middleware/otp-throttle.middleware.ts
5
+ /**
6
+ * Throttle for the route that SENDS a login code. {@link tokenIssueThrottleMiddleware}
7
+ * keyed on the `phone` input and IP: every request counts (the answer is the
8
+ * same for unknown phones), 3 per 1h, then a 1h lockout (`429`, `EC004`).
9
+ *
10
+ * @example
11
+ * router.post("/auth/otp/request", requestOtpController, {
12
+ * middleware: [otpRequestThrottleMiddleware()],
13
+ * });
14
+ */
15
+ function otpRequestThrottleMiddleware(options = {}) {
16
+ return tokenIssueThrottleMiddleware({
17
+ identifierKey: "phone",
18
+ ...options
19
+ });
20
+ }
21
+ /**
22
+ * Throttle for the route that VERIFIES a login code. {@link loginThrottleMiddleware}
23
+ * keyed on `phone` and IP, failure-aware: 5 failures per 15m, then a 15m lockout
24
+ * (`429`, `EC004`). Complements the per-code attempt cap, which a fresh code
25
+ * would otherwise reset.
26
+ *
27
+ * @example
28
+ * router.post("/auth/otp/verify", verifyOtpController, {
29
+ * middleware: [otpVerifyThrottleMiddleware()],
30
+ * });
31
+ */
32
+ function otpVerifyThrottleMiddleware(options = {}) {
33
+ return loginThrottleMiddleware({
34
+ max: 5,
35
+ window: "15m",
36
+ lockoutDuration: "15m",
37
+ by: ["email", "ip"],
38
+ identifierKey: "phone",
39
+ ...options
40
+ });
41
+ }
42
+
43
+ //#endregion
44
+ export { otpRequestThrottleMiddleware, otpVerifyThrottleMiddleware };
45
+ //# sourceMappingURL=otp-throttle.middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"otp-throttle.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/otp-throttle.middleware.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { loginThrottleMiddleware, type LoginThrottleOptions } from \"./login-throttle.middleware\";\nimport { tokenIssueThrottleMiddleware } from \"./one-time-token-throttle.middleware\";\n\n/**\n * Throttle for the route that SENDS a login code. {@link tokenIssueThrottleMiddleware}\n * keyed on the `phone` input and IP: every request counts (the answer is the\n * same for unknown phones), 3 per 1h, then a 1h lockout (`429`, `EC004`).\n *\n * @example\n * router.post(\"/auth/otp/request\", requestOtpController, {\n * middleware: [otpRequestThrottleMiddleware()],\n * });\n */\nexport function otpRequestThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n return tokenIssueThrottleMiddleware({ identifierKey: \"phone\", ...options });\n}\n\n/**\n * Throttle for the route that VERIFIES a login code. {@link loginThrottleMiddleware}\n * keyed on `phone` and IP, failure-aware: 5 failures per 15m, then a 15m lockout\n * (`429`, `EC004`). Complements the per-code attempt cap, which a fresh code\n * would otherwise reset.\n *\n * @example\n * router.post(\"/auth/otp/verify\", verifyOtpController, {\n * middleware: [otpVerifyThrottleMiddleware()],\n * });\n */\nexport function otpVerifyThrottleMiddleware(options: LoginThrottleOptions = {}): Middleware {\n return loginThrottleMiddleware({\n max: 5,\n window: \"15m\",\n lockoutDuration: \"15m\",\n by: [\"email\", \"ip\"],\n identifierKey: \"phone\",\n ...options,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAgB,6BAA6B,UAAgC,CAAC,GAAe;CAC3F,OAAO,6BAA6B;EAAE,eAAe;EAAS,GAAG;CAAQ,CAAC;AAC5E;;;;;;;;;;;;AAaA,SAAgB,4BAA4B,UAAgC,CAAC,GAAe;CAC1F,OAAO,wBAAwB;EAC7B,KAAK;EACL,QAAQ;EACR,iBAAiB;EACjB,IAAI,CAAC,SAAS,IAAI;EAClB,eAAe;EACf,GAAG;CACL,CAAC;AACH"}
@@ -0,0 +1,18 @@
1
+ import { Middleware } from "@warlock.js/core";
2
+
3
+ //#region ../auth/src/middleware/require-verified-email.middleware.d.ts
4
+ /**
5
+ * Opt-in route gate that rejects a user who has not verified their email with
6
+ * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER
7
+ * `authMiddleware` — it reads `request.locals.user`. A request with no user
8
+ * fails closed with the same error.
9
+ *
10
+ * @example
11
+ * router.post("/orders", createOrder, {
12
+ * middleware: [authMiddleware("user"), requireVerifiedEmail()],
13
+ * });
14
+ */
15
+ declare function requireVerifiedEmail(): Middleware;
16
+ //#endregion
17
+ export { requireVerifiedEmail };
18
+ //# sourceMappingURL=require-verified-email.middleware.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-verified-email.middleware.d.mts","names":[],"sources":["../../../../../../../auth/src/middleware/require-verified-email.middleware.ts"],"mappings":";;;;;AAeA;;;;AAAkD;;;;;iBAAlC,oBAAA,IAAwB,UAAU"}
@@ -0,0 +1,24 @@
1
+ import { authConfig } from "../services/auth-config.mjs";
2
+ import { EmailNotVerifiedError } from "../errors/email-not-verified.error.mjs";
3
+
4
+ //#region ../auth/src/middleware/require-verified-email.middleware.ts
5
+ /**
6
+ * Opt-in route gate that rejects a user who has not verified their email with
7
+ * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER
8
+ * `authMiddleware` — it reads `request.locals.user`. A request with no user
9
+ * fails closed with the same error.
10
+ *
11
+ * @example
12
+ * router.post("/orders", createOrder, {
13
+ * middleware: [authMiddleware("user"), requireVerifiedEmail()],
14
+ * });
15
+ */
16
+ function requireVerifiedEmail() {
17
+ return async ({ request }) => {
18
+ if (!request.locals.user?.get?.(authConfig.verification.field())) throw new EmailNotVerifiedError();
19
+ };
20
+ }
21
+
22
+ //#endregion
23
+ export { requireVerifiedEmail };
24
+ //# sourceMappingURL=require-verified-email.middleware.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"require-verified-email.middleware.mjs","names":[],"sources":["../../../../../../../auth/src/middleware/require-verified-email.middleware.ts"],"sourcesContent":["import type { Middleware } from \"@warlock.js/core\";\nimport { EmailNotVerifiedError } from \"../errors/email-not-verified.error\";\nimport { authConfig } from \"../services/auth-config\";\n\n/**\n * Opt-in route gate that rejects a user who has not verified their email with\n * {@link EmailNotVerifiedError} (`403`, `EC007`). Place it AFTER\n * `authMiddleware` — it reads `request.locals.user`. A request with no user\n * fails closed with the same error.\n *\n * @example\n * router.post(\"/orders\", createOrder, {\n * middleware: [authMiddleware(\"user\"), requireVerifiedEmail()],\n * });\n */\nexport function requireVerifiedEmail(): Middleware {\n return async ({ request }) => {\n const user = request.locals.user as { get?: (key: string) => unknown } | undefined;\n\n if (!user?.get?.(authConfig.verification.field())) {\n throw new EmailNotVerifiedError();\n }\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,uBAAmC;CACjD,OAAO,OAAO,EAAE,cAAc;EAG5B,IAAI,CAFS,QAAQ,OAAO,MAEjB,MAAM,WAAW,aAAa,MAAM,CAAC,GAC9C,MAAM,IAAI,sBAAsB;CAEpC;AACF"}
@@ -54,6 +54,14 @@ declare class AccessToken extends Model {
54
54
  user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
55
55
  expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
56
56
  }>>;
57
+ /**
58
+ * Permanent regardless of the data source default — Mongo's driver default
59
+ * is `"trash"`, which would copy live credential material (the JWT itself)
60
+ * into `access_tokensTrash` on every `destroy()` (logout, expiry purge,
61
+ * never-expiring purge). A revoked/expired access token must not survive
62
+ * anywhere.
63
+ */
64
+ static deleteStrategy: "permanent";
57
65
  /** The user this token was issued for. */
58
66
  get userId(): any;
59
67
  /** The user-type slug this token was issued for. */
@@ -1 +1 @@
1
- {"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;MAGT,MAAA;;MAKA,QAAA;;;;;;AAXb;;;;;;;;;;MA8Ba,SAAA;EAqBmD;;;;;;EAAA,IAPnD,YAAA;EAiC8B;;;EAAA,OA1B3B,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAkEZ;;;EAAA,OAtDpC,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;EA/Dd;;;EAAA,OAsEtB,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnEjC;;;EAAA,OA0EN,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,IAAgB,OAAA;;;;;;;;;;;;SAqBhB,iBAAA,IAAqB,OAAA,CAAQ,WAAA;;;;;;SAW7B,kBAAA,IAAsB,OAAA,CAAQ,WAAA;AAAA"}
1
+ {"version":3,"file":"access-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"mappings":";;;;;;AAaA;;;;;;cAAa,iBAAA,6BAAiB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;cAiBjB,WAAA,SAAoB,KAAA;EAAA,OACjB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;SASN,cAAA;;MAGH,MAAA;EAfY;EAAA,IAoBZ,QAAA;;;;;;;;;;;;;;;;MAmBA,SAAA;EAuDyB;;;;;;EAAA,IAzCzB,YAAA;EArDyB;;;EAAA,OA4DtB,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,SAAA,EAAW,IAAA,GAAI,OAAA,CAAA,WAAA;EAzD1C;;;EAAA,OAqEN,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,WAAA;;;;SAOpC,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;;;;SAOvC,gBAAA,CAAiB,IAAA,EAAM,IAAA,GAAI,OAAA;;;;;SAQrB,YAAA,IAAgB,OAAA;;;;;;;;;;;;SAqBhB,iBAAA,IAAqB,OAAA,CAAQ,WAAA;;;;;;SAW7B,kBAAA,IAAsB,OAAA,CAAQ,WAAA;AAAA"}
@@ -34,6 +34,9 @@ var AccessToken = class extends Model {
34
34
  static {
35
35
  this.schema = accessTokenSchema;
36
36
  }
37
+ static {
38
+ this.deleteStrategy = "permanent";
39
+ }
37
40
  /** The user this token was issued for. */
38
41
  get userId() {
39
42
  return this.get("user_id");
@@ -1 +1 @@
1
- {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;CAGvB,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,CAAC,CAAC,IAAI,EAEzB,CAAC,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
1
+ {"version":3,"file":"access-token.model.mjs","names":[],"sources":["../../../../../../../../auth/src/models/access-token/access-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { isNeverExpiring, isUsableExpiry } from \"../../utils/token-expiry\";\r\nimport type { Auth } from \"../auth.model\";\r\n\r\n/**\r\n * Seal schema for the persisted access-token record. Exported so an override\r\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\r\n * base shape.\r\n *\r\n * `last_access` and the `is_active` soft-revoke flag were removed — neither was\r\n * ever read; access tokens are revoked by deleting the row.\r\n */\r\nexport const accessTokenSchema = v.object({\r\n token: v.string().required(),\r\n user_id: v.scalar().required(),\r\n user_type: v.string().required(),\r\n expires_at: v.date().required(),\r\n});\r\n\r\n/**\r\n * Persisted access-token record + the data layer for access tokens.\r\n *\r\n * **Role.** Owns access-token persistence and lookup. The middleware checks a\r\n * presented JWT against this table so deleting a row (logout) invalidates the\r\n * token immediately, before its JWT expiry. The auth service goes through the\r\n * named statics exclusively, so it never hard-codes a column name and an\r\n * override can rename/add columns by registering under\r\n * `config.auth.accessToken.model`.\r\n */\r\nexport class AccessToken extends Model {\r\n public static table = \"access_tokens\";\r\n\r\n public static schema = accessTokenSchema;\r\n\r\n /**\r\n * Permanent regardless of the data source default — Mongo's driver default\r\n * is `\"trash\"`, which would copy live credential material (the JWT itself)\r\n * into `access_tokensTrash` on every `destroy()` (logout, expiry purge,\r\n * never-expiring purge). A revoked/expired access token must not survive\r\n * anywhere.\r\n */\r\n public static deleteStrategy = \"permanent\" as const;\r\n\r\n /** The user this token was issued for. */\r\n public get userId() {\r\n return this.get(\"user_id\");\r\n }\r\n\r\n /** The user-type slug this token was issued for. */\r\n public get userType(): string {\r\n return this.get(\"user_type\");\r\n }\r\n\r\n /**\r\n * Whether the persisted expiry has passed.\r\n *\r\n * The middleware asks this on every request, which is what makes the row the\r\n * authority it always looked like it was: before 4.12.0 the gate only checked\r\n * that the row *existed*, so a row the database knew was dead still let its\r\n * token through — the database was never asked.\r\n *\r\n * **Fails closed.** A missing or unparseable `expires_at` counts as expired.\r\n * `expires_at` is `required` in the schema, so a row that cannot answer \"when\r\n * does this die\" is malformed, and the safe reading of a malformed credential\r\n * is that it is not one. This is deliberately stricter than a naive\r\n * `now > expires_at`, which answers `false` for an `Invalid Date` and thereby\r\n * grants exactly the poisoned rows an unlimited life.\r\n */\r\n public get isExpired(): boolean {\r\n const expiresAt = this.get(\"expires_at\");\r\n\r\n if (!isUsableExpiry(expiresAt)) return true;\r\n\r\n return new Date(expiresAt).getTime() <= Date.now();\r\n }\r\n\r\n /**\r\n * Whether nothing about this row can ever retire it — an unusable\r\n * `expires_at`, or a token carrying no `exp` claim. See\r\n * {@link isNeverExpiring}; this is the predicate `purgeNeverExpiring` selects\r\n * on.\r\n */\r\n public get neverExpires(): boolean {\r\n return isNeverExpiring(this.get(\"token\"), this.get(\"expires_at\"));\r\n }\r\n\r\n /**\r\n * Persist a freshly-signed access token for the user.\r\n */\r\n public static issue(user: Auth, token: string, expiresAt: Date) {\r\n return this.create({\r\n token,\r\n user_id: user.id,\r\n user_type: user.userType,\r\n expires_at: expiresAt,\r\n });\r\n }\r\n\r\n /**\r\n * Find an access-token row by its raw token string.\r\n */\r\n public static findByToken(token: string): Promise<AccessToken | null> {\r\n return this.first({ token });\r\n }\r\n\r\n /**\r\n * Delete a specific token that belongs to the given user.\r\n */\r\n public static deleteForUser(user: Auth, token: string) {\r\n return this.delete({ token, user_id: user.id });\r\n }\r\n\r\n /**\r\n * Delete every access token belonging to the user.\r\n */\r\n public static deleteAllForUser(user: Auth) {\r\n return this.delete({ user_id: user.id });\r\n }\r\n\r\n /**\r\n * Hard-delete every expired access-token row. Returns the number removed.\r\n * Runs from the `auth.cleanup` CLI command (a cold batch path).\r\n */\r\n public static async purgeExpired(): Promise<number> {\r\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\r\n\r\n for (const token of expiredTokens) {\r\n await token.destroy();\r\n }\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Every row that can never retire itself — see {@link neverExpires}.\r\n *\r\n * **A full scan, filtered in memory, on purpose.** The defining case is a row\r\n * whose `expires_at` is an `Invalid Date`, which no date predicate can select\r\n * (`< now` and `> now` are both `false` for it), and the definitive case is a\r\n * token with no `exp` claim, which lives inside the token string rather than\r\n * in a column. Neither is expressible as a `where`, so the rows have to be\r\n * read to be judged. This runs from a one-off remediation command, not a\r\n * request path.\r\n */\r\n public static async findNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.query().get();\r\n\r\n return tokens.filter((token: AccessToken) => token.neverExpires);\r\n }\r\n\r\n /**\r\n * Hard-delete every never-expiring row, returning the rows removed so the\r\n * caller can report what it revoked. Deletion *is* revocation for access\r\n * tokens — the middleware rejects a token with no row.\r\n */\r\n public static async purgeNeverExpiring(): Promise<AccessToken[]> {\r\n const tokens = await this.findNeverExpiring();\r\n\r\n for (const token of tokens) {\r\n await token.destroy();\r\n }\r\n\r\n return tokens;\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,oBAAoB,EAAE,OAAO;CACxC,OAAO,EAAE,OAAO,CAAC,CAAC,SAAS;CAC3B,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS;CAC7B,WAAW,EAAE,OAAO,CAAC,CAAC,SAAS;CAC/B,YAAY,EAAE,KAAK,CAAC,CAAC,SAAS;AAChC,CAAC;;;;;;;;;;;AAYD,IAAa,cAAb,cAAiC,MAAM;;eACf;;;gBAEC;;;wBASQ;;;CAG/B,IAAW,SAAS;EAClB,OAAO,KAAK,IAAI,SAAS;CAC3B;;CAGA,IAAW,WAAmB;EAC5B,OAAO,KAAK,IAAI,WAAW;CAC7B;;;;;;;;;;;;;;;;CAiBA,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EAEvC,IAAI,CAAC,eAAe,SAAS,GAAG,OAAO;EAEvC,OAAO,IAAI,KAAK,SAAS,CAAC,CAAC,QAAQ,KAAK,KAAK,IAAI;CACnD;;;;;;;CAQA,IAAW,eAAwB;EACjC,OAAO,gBAAgB,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,YAAY,CAAC;CAClE;;;;CAKA,OAAc,MAAM,MAAY,OAAe,WAAiB;EAC9D,OAAO,KAAK,OAAO;GACjB;GACA,SAAS,KAAK;GACd,WAAW,KAAK;GAChB,YAAY;EACd,CAAC;CACH;;;;CAKA,OAAc,YAAY,OAA4C;EACpE,OAAO,KAAK,MAAM,EAAE,MAAM,CAAC;CAC7B;;;;CAKA,OAAc,cAAc,MAAY,OAAe;EACrD,OAAO,KAAK,OAAO;GAAE;GAAO,SAAS,KAAK;EAAG,CAAC;CAChD;;;;CAKA,OAAc,iBAAiB,MAAY;EACzC,OAAO,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG,CAAC;CACzC;;;;;CAMA,aAAoB,eAAgC;EAClD,MAAM,gBAAgB,MAAM,KAAK,MAAM,CAAC,CAAC,MAAM,cAAc,qBAAK,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI;EAElF,KAAK,MAAM,SAAS,eAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO,cAAc;CACvB;;;;;;;;;;;;CAaA,aAAoB,oBAA4C;EAG9D,QAAO,MAFc,KAAK,MAAM,CAAC,CAAC,IAAI,EAEzB,CAAC,QAAQ,UAAuB,MAAM,YAAY;CACjE;;;;;;CAOA,aAAoB,qBAA6C;EAC/D,MAAM,SAAS,MAAM,KAAK,kBAAkB;EAE5C,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,QAAQ;EAGtB,OAAO;CACT;AACF"}
@@ -1,7 +1,9 @@
1
1
  import { Auth } from "./auth.model.mjs";
2
2
  import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
3
+ import { OneTimeToken, OneTimeTokenPurpose, oneTimeTokenSchema } from "./one-time-token/one-time-token.model.mjs";
3
4
  import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
4
-
5
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential/passkey-credential.model.mjs";
6
+ import { ProviderAccount, providerAccountSchema } from "./provider-account/provider-account.model.mjs";
5
7
  //#region ../auth/src/models/index.d.ts
6
8
  declare const authMigrations: import("@warlock.js/cascade").MigrationConstructor[];
7
9
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;cAOa,cAAA,gCAAc,oBAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"mappings":";;;;;;;cAaa,cAAA,gCAAc,oBAAA"}
@@ -1,13 +1,28 @@
1
1
  import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
2
2
  import "./access-token/index.mjs";
3
+ import { OneTimeToken, oneTimeTokenSchema } from "./one-time-token/one-time-token.model.mjs";
4
+ import "./one-time-token/index.mjs";
3
5
  import { RefreshToken, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
4
6
  import "./refresh-token/index.mjs";
5
7
  import { AccessTokenMigration } from "./access-token/migration.mjs";
8
+ import { OneTimeTokenMigration } from "./one-time-token/migration.mjs";
9
+ import { PasskeyCredential, passkeyCredentialSchema } from "./passkey-credential/passkey-credential.model.mjs";
10
+ import { PasskeyCredentialMigration } from "./passkey-credential/migration.mjs";
11
+ import { ProviderAccount, providerAccountSchema } from "./provider-account/provider-account.model.mjs";
12
+ import { ProviderAccountMigration } from "./provider-account/migration.mjs";
6
13
  import { RefreshTokenMigration } from "./refresh-token/migration.mjs";
7
14
  import { Auth } from "./auth.model.mjs";
15
+ import "./passkey-credential/index.mjs";
16
+ import "./provider-account/index.mjs";
8
17
 
9
18
  //#region ../auth/src/models/index.ts
10
- const authMigrations = [AccessTokenMigration, RefreshTokenMigration];
19
+ const authMigrations = [
20
+ AccessTokenMigration,
21
+ RefreshTokenMigration,
22
+ OneTimeTokenMigration,
23
+ ProviderAccountMigration,
24
+ PasskeyCredentialMigration
25
+ ];
11
26
 
12
27
  //#endregion
13
28
  export { authMigrations };
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\r\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\r\n\r\nexport * from \"./access-token\";\r\nexport * from \"./auth.model\";\r\nexport * from \"./refresh-token\";\r\n\r\nexport const authMigrations = [AccessTokenMigration, RefreshTokenMigration];\r\n"],"mappings":";;;;;;;;;AAOA,MAAa,iBAAiB,CAAC,sBAAsB,qBAAqB"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\nimport { OneTimeTokenMigration } from \"./one-time-token/migration\";\nimport { PasskeyCredentialMigration } from \"./passkey-credential/migration\";\nimport { ProviderAccountMigration } from \"./provider-account/migration\";\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\n\nexport * from \"./access-token\";\nexport * from \"./auth.model\";\nexport * from \"./one-time-token\";\nexport * from \"./passkey-credential\";\nexport * from \"./provider-account\";\nexport * from \"./refresh-token\";\n\nexport const authMigrations = [\n AccessTokenMigration,\n RefreshTokenMigration,\n OneTimeTokenMigration,\n ProviderAccountMigration,\n PasskeyCredentialMigration,\n];\n"],"mappings":";;;;;;;;;;;;;;;;;;AAaA,MAAa,iBAAiB;CAC5B;CACA;CACA;CACA;CACA;AACF"}