@lastshotlabs/bunshot 0.0.27 → 0.1.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 (742) hide show
  1. package/.oclif.manifest.json +39 -0
  2. package/README.md +8282 -2147
  3. package/dist/cli/commands/init.js +690 -0
  4. package/dist/cli/index.js +6 -0
  5. package/dist/cli.js +4 -4
  6. package/dist/packages/bunshot-admin/src/index.d.ts +15 -0
  7. package/dist/packages/bunshot-admin/src/index.js +11 -0
  8. package/dist/packages/bunshot-admin/src/lib/resourceTypes.d.ts +8 -0
  9. package/dist/packages/bunshot-admin/src/lib/resourceTypes.js +33 -0
  10. package/dist/packages/bunshot-admin/src/lib/typedRoute.d.ts +14 -0
  11. package/dist/packages/bunshot-admin/src/lib/typedRoute.js +17 -0
  12. package/dist/packages/bunshot-admin/src/plugin.d.ts +4 -0
  13. package/dist/packages/bunshot-admin/src/plugin.js +46 -0
  14. package/dist/packages/bunshot-admin/src/providers/auth0Access.d.ts +6 -0
  15. package/dist/packages/bunshot-admin/src/providers/auth0Access.js +32 -0
  16. package/dist/packages/bunshot-admin/src/routes/admin.d.ts +10 -0
  17. package/dist/packages/bunshot-admin/src/routes/admin.js +923 -0
  18. package/dist/packages/bunshot-admin/src/routes/mail.d.ts +6 -0
  19. package/dist/packages/bunshot-admin/src/routes/mail.js +114 -0
  20. package/dist/packages/bunshot-admin/src/routes/permissions.d.ts +8 -0
  21. package/dist/packages/bunshot-admin/src/routes/permissions.js +315 -0
  22. package/dist/packages/bunshot-admin/src/types/config.d.ts +16 -0
  23. package/dist/packages/bunshot-admin/src/types/config.js +37 -0
  24. package/dist/packages/bunshot-admin/src/types/env.d.ts +14 -0
  25. package/dist/packages/bunshot-admin/src/types/provider.d.ts +1 -0
  26. package/dist/packages/bunshot-admin/src/types/provider.js +4 -0
  27. package/dist/packages/bunshot-auth/src/adapters/memoryAuth.d.ts +66 -0
  28. package/dist/packages/bunshot-auth/src/adapters/memoryAuth.js +1063 -0
  29. package/dist/packages/bunshot-auth/src/adapters/mongoAuth.d.ts +2 -0
  30. package/dist/packages/bunshot-auth/src/adapters/mongoAuth.js +536 -0
  31. package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.d.ts +88 -0
  32. package/dist/packages/bunshot-auth/src/adapters/sqliteAuth.js +1366 -0
  33. package/dist/packages/bunshot-auth/src/admin/bunshotAccess.d.ts +2 -0
  34. package/dist/packages/bunshot-auth/src/admin/bunshotAccess.js +23 -0
  35. package/dist/packages/bunshot-auth/src/admin/bunshotUsers.d.ts +5 -0
  36. package/dist/packages/bunshot-auth/src/admin/bunshotUsers.js +131 -0
  37. package/dist/packages/bunshot-auth/src/bootstrap.d.ts +38 -0
  38. package/dist/packages/bunshot-auth/src/bootstrap.js +384 -0
  39. package/dist/packages/bunshot-auth/src/config/appConfig.d.ts +3 -0
  40. package/dist/packages/bunshot-auth/src/config/appConfig.js +4 -0
  41. package/dist/packages/bunshot-auth/src/config/authConfig.d.ts +478 -0
  42. package/dist/packages/bunshot-auth/src/config/authConfig.js +46 -0
  43. package/dist/packages/bunshot-auth/src/config/configLock.d.ts +2 -0
  44. package/dist/packages/bunshot-auth/src/config/configLock.js +10 -0
  45. package/dist/packages/bunshot-auth/src/index.d.ts +25 -0
  46. package/dist/packages/bunshot-auth/src/index.js +23 -0
  47. package/dist/packages/bunshot-auth/src/infra/mongo.d.ts +15 -0
  48. package/dist/packages/bunshot-auth/src/infra/mongo.js +44 -0
  49. package/dist/packages/bunshot-auth/src/infra/queue.d.ts +14 -0
  50. package/dist/packages/bunshot-auth/src/infra/queue.js +27 -0
  51. package/dist/packages/bunshot-auth/src/infra/redis.d.ts +5 -0
  52. package/dist/packages/bunshot-auth/src/infra/redis.js +15 -0
  53. package/dist/packages/bunshot-auth/src/infra/signing.d.ts +7 -0
  54. package/dist/packages/bunshot-auth/src/infra/signing.js +8 -0
  55. package/dist/packages/bunshot-auth/src/lib/accountLockout.d.ts +34 -0
  56. package/dist/packages/bunshot-auth/src/lib/accountLockout.js +244 -0
  57. package/dist/packages/bunshot-auth/src/lib/adapterTiers.d.ts +1 -0
  58. package/dist/packages/bunshot-auth/src/lib/adapterTiers.js +1 -0
  59. package/dist/packages/bunshot-auth/src/lib/authAdapter.d.ts +1 -0
  60. package/dist/packages/bunshot-auth/src/lib/authAdapter.js +1 -0
  61. package/dist/packages/bunshot-auth/src/lib/authContext.d.ts +15 -0
  62. package/dist/packages/bunshot-auth/src/lib/authContext.js +1 -0
  63. package/dist/packages/bunshot-auth/src/lib/authEventBus.d.ts +4 -0
  64. package/dist/packages/bunshot-auth/src/lib/authEventBus.js +15 -0
  65. package/dist/packages/bunshot-auth/src/lib/authRateLimit.d.ts +28 -0
  66. package/dist/packages/bunshot-auth/src/lib/authRateLimit.js +205 -0
  67. package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.d.ts +8 -2
  68. package/dist/{lib → packages/bunshot-auth/src/lib}/breachedPassword.js +22 -9
  69. package/dist/packages/bunshot-auth/src/lib/cache.d.ts +12 -0
  70. package/dist/packages/bunshot-auth/src/lib/cache.js +120 -0
  71. package/dist/packages/bunshot-auth/src/lib/clientIp.d.ts +4 -0
  72. package/dist/{lib → packages/bunshot-auth/src/lib}/clientIp.js +14 -7
  73. package/dist/packages/bunshot-auth/src/lib/cookieOptions.d.ts +27 -0
  74. package/dist/packages/bunshot-auth/src/lib/cookieOptions.js +33 -0
  75. package/dist/packages/bunshot-auth/src/lib/credentialStuffing.d.ts +40 -0
  76. package/dist/packages/bunshot-auth/src/lib/credentialStuffing.js +211 -0
  77. package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.d.ts +19 -0
  78. package/dist/packages/bunshot-auth/src/lib/deletionCancelToken.js +148 -0
  79. package/dist/packages/bunshot-auth/src/lib/emailTemplates.d.ts +23 -0
  80. package/dist/packages/bunshot-auth/src/lib/emailTemplates.js +265 -0
  81. package/dist/packages/bunshot-auth/src/lib/emailVerification.d.ts +30 -0
  82. package/dist/packages/bunshot-auth/src/lib/emailVerification.js +200 -0
  83. package/dist/packages/bunshot-auth/src/lib/env.d.ts +1 -0
  84. package/dist/packages/bunshot-auth/src/lib/env.js +3 -0
  85. package/dist/packages/bunshot-auth/src/lib/fingerprint.js +36 -0
  86. package/dist/{lib → packages/bunshot-auth/src/lib}/groups.d.ts +15 -16
  87. package/dist/{lib → packages/bunshot-auth/src/lib}/groups.js +22 -34
  88. package/dist/packages/bunshot-auth/src/lib/jwks.d.ts +28 -0
  89. package/dist/packages/bunshot-auth/src/lib/jwks.js +79 -0
  90. package/dist/packages/bunshot-auth/src/lib/jwt.d.ts +12 -0
  91. package/dist/packages/bunshot-auth/src/lib/jwt.js +86 -0
  92. package/dist/{lib → packages/bunshot-auth/src/lib}/logger.js +3 -3
  93. package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.d.ts +5 -4
  94. package/dist/{lib → packages/bunshot-auth/src/lib}/m2m.js +6 -10
  95. package/dist/packages/bunshot-auth/src/lib/magicLink.d.ts +13 -0
  96. package/dist/packages/bunshot-auth/src/lib/magicLink.js +145 -0
  97. package/dist/packages/bunshot-auth/src/lib/mfaChallenge.d.ts +60 -0
  98. package/dist/packages/bunshot-auth/src/lib/mfaChallenge.js +419 -0
  99. package/dist/packages/bunshot-auth/src/lib/oauth.d.ts +82 -0
  100. package/dist/packages/bunshot-auth/src/lib/oauth.js +177 -0
  101. package/dist/packages/bunshot-auth/src/lib/oauthCode.d.ts +19 -0
  102. package/dist/packages/bunshot-auth/src/lib/oauthCode.js +182 -0
  103. package/dist/packages/bunshot-auth/src/lib/oauthReauth.d.ts +19 -0
  104. package/dist/packages/bunshot-auth/src/lib/oauthReauth.js +255 -0
  105. package/dist/packages/bunshot-auth/src/lib/organization.d.ts +66 -0
  106. package/dist/packages/bunshot-auth/src/lib/organization.js +225 -0
  107. package/dist/packages/bunshot-auth/src/lib/passwordHistory.d.ts +12 -0
  108. package/dist/packages/bunshot-auth/src/lib/passwordHistory.js +31 -0
  109. package/dist/packages/bunshot-auth/src/lib/resetPassword.d.ts +20 -0
  110. package/dist/packages/bunshot-auth/src/lib/resetPassword.js +148 -0
  111. package/dist/packages/bunshot-auth/src/lib/roles.d.ts +9 -0
  112. package/dist/packages/bunshot-auth/src/lib/roles.js +93 -0
  113. package/dist/packages/bunshot-auth/src/lib/saml.d.ts +29 -0
  114. package/dist/packages/bunshot-auth/src/lib/saml.js +73 -0
  115. package/dist/packages/bunshot-auth/src/lib/samlRequestId.d.ts +13 -0
  116. package/dist/packages/bunshot-auth/src/lib/samlRequestId.js +129 -0
  117. package/dist/{lib → packages/bunshot-auth/src/lib}/scim.d.ts +7 -7
  118. package/dist/{lib → packages/bunshot-auth/src/lib}/scim.js +15 -13
  119. package/dist/packages/bunshot-auth/src/lib/securityEventWiring.d.ts +22 -0
  120. package/dist/packages/bunshot-auth/src/lib/securityEventWiring.js +65 -0
  121. package/dist/packages/bunshot-auth/src/lib/session.d.ts +45 -0
  122. package/dist/packages/bunshot-auth/src/lib/session.js +1211 -0
  123. package/dist/packages/bunshot-auth/src/lib/storeInfra.d.ts +26 -0
  124. package/dist/packages/bunshot-auth/src/lib/storeInfra.js +18 -0
  125. package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.d.ts +3 -2
  126. package/dist/{lib → packages/bunshot-auth/src/lib}/suspension.js +2 -5
  127. package/dist/packages/bunshot-auth/src/lib/validateAdapter.d.ts +16 -0
  128. package/dist/packages/bunshot-auth/src/lib/validateAdapter.js +161 -0
  129. package/dist/packages/bunshot-auth/src/middleware/bearerAuth.d.ts +13 -0
  130. package/dist/packages/bunshot-auth/src/middleware/bearerAuth.js +58 -0
  131. package/dist/{middleware → packages/bunshot-auth/src/middleware}/csrf.d.ts +5 -4
  132. package/dist/packages/bunshot-auth/src/middleware/csrf.js +138 -0
  133. package/dist/packages/bunshot-auth/src/middleware/identify.d.ts +4 -0
  134. package/dist/packages/bunshot-auth/src/middleware/identify.js +124 -0
  135. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.d.ts +2 -2
  136. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireMfaSetup.js +10 -8
  137. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.d.ts +2 -2
  138. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireRole.js +20 -16
  139. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.d.ts +2 -2
  140. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireScope.js +6 -6
  141. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.d.ts +2 -2
  142. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireStepUp.js +8 -7
  143. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.d.ts +2 -2
  144. package/dist/{middleware → packages/bunshot-auth/src/middleware}/requireVerifiedEmail.js +7 -6
  145. package/dist/packages/bunshot-auth/src/middleware/scimAuth.d.ts +8 -0
  146. package/dist/packages/bunshot-auth/src/middleware/scimAuth.js +29 -0
  147. package/dist/packages/bunshot-auth/src/middleware/userAuth.d.ts +3 -0
  148. package/dist/packages/bunshot-auth/src/middleware/userAuth.js +6 -0
  149. package/dist/{models → packages/bunshot-auth/src/models}/AuthUser.d.ts +12 -8
  150. package/dist/packages/bunshot-auth/src/models/AuthUser.js +53 -0
  151. package/dist/packages/bunshot-auth/src/models/Group.d.ts +19 -0
  152. package/dist/packages/bunshot-auth/src/models/Group.js +22 -0
  153. package/dist/{models → packages/bunshot-auth/src/models}/GroupMembership.d.ts +6 -8
  154. package/dist/packages/bunshot-auth/src/models/GroupMembership.js +19 -0
  155. package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.d.ts +1 -1
  156. package/dist/{models → packages/bunshot-auth/src/models}/M2MClient.js +5 -5
  157. package/dist/packages/bunshot-auth/src/models/TenantRole.d.ts +13 -0
  158. package/dist/packages/bunshot-auth/src/models/TenantRole.js +17 -0
  159. package/dist/packages/bunshot-auth/src/plugin.d.ts +4 -0
  160. package/dist/packages/bunshot-auth/src/plugin.js +277 -0
  161. package/dist/packages/bunshot-auth/src/routes/auth.d.ts +15 -0
  162. package/dist/packages/bunshot-auth/src/routes/auth.js +1624 -0
  163. package/dist/packages/bunshot-auth/src/routes/groups.d.ts +4 -0
  164. package/dist/packages/bunshot-auth/src/routes/groups.js +481 -0
  165. package/dist/packages/bunshot-auth/src/routes/m2m.d.ts +2 -0
  166. package/dist/packages/bunshot-auth/src/routes/m2m.js +145 -0
  167. package/dist/packages/bunshot-auth/src/routes/mfa.d.ts +6 -0
  168. package/dist/packages/bunshot-auth/src/routes/mfa.js +991 -0
  169. package/dist/packages/bunshot-auth/src/routes/oauth.d.ts +3 -0
  170. package/dist/packages/bunshot-auth/src/routes/oauth.js +1727 -0
  171. package/dist/packages/bunshot-auth/src/routes/oidc.d.ts +2 -0
  172. package/dist/packages/bunshot-auth/src/routes/oidc.js +84 -0
  173. package/dist/packages/bunshot-auth/src/routes/organizations.d.ts +3 -0
  174. package/dist/packages/bunshot-auth/src/routes/organizations.js +741 -0
  175. package/dist/packages/bunshot-auth/src/routes/passkey.d.ts +2 -0
  176. package/dist/packages/bunshot-auth/src/routes/passkey.js +199 -0
  177. package/dist/packages/bunshot-auth/src/routes/saml.d.ts +2 -0
  178. package/dist/packages/bunshot-auth/src/routes/saml.js +226 -0
  179. package/dist/packages/bunshot-auth/src/routes/scim.d.ts +3 -0
  180. package/dist/packages/bunshot-auth/src/routes/scim.js +588 -0
  181. package/dist/packages/bunshot-auth/src/runtime.d.ts +52 -0
  182. package/dist/packages/bunshot-auth/src/runtime.js +11 -0
  183. package/dist/{schemas → packages/bunshot-auth/src/schemas}/auth.d.ts +4 -5
  184. package/dist/packages/bunshot-auth/src/schemas/auth.js +24 -0
  185. package/dist/packages/bunshot-auth/src/schemas/error.d.ts +10 -0
  186. package/dist/packages/bunshot-auth/src/schemas/error.js +10 -0
  187. package/dist/packages/bunshot-auth/src/schemas/success.d.ts +10 -0
  188. package/dist/packages/bunshot-auth/src/schemas/success.js +10 -0
  189. package/dist/packages/bunshot-auth/src/services/auth.d.ts +39 -0
  190. package/dist/packages/bunshot-auth/src/services/auth.js +378 -0
  191. package/dist/{services → packages/bunshot-auth/src/services}/mfa.d.ts +41 -17
  192. package/dist/{services → packages/bunshot-auth/src/services}/mfa.js +259 -183
  193. package/dist/packages/bunshot-auth/src/testing.d.ts +31 -0
  194. package/dist/packages/bunshot-auth/src/testing.js +23 -0
  195. package/dist/packages/bunshot-auth/src/types/adapter.d.ts +1 -0
  196. package/dist/packages/bunshot-auth/src/types/adapter.js +1 -0
  197. package/dist/packages/bunshot-auth/src/types/config.d.ts +152 -0
  198. package/dist/packages/bunshot-auth/src/types/config.js +179 -0
  199. package/dist/{routes → packages/bunshot-auth/src/types}/groups.d.ts +2 -3
  200. package/dist/packages/bunshot-auth/src/types/groups.js +1 -0
  201. package/dist/packages/bunshot-auth/src/types/oauthCode.d.ts +6 -0
  202. package/dist/packages/bunshot-auth/src/types/oauthCode.js +1 -0
  203. package/dist/packages/bunshot-auth/src/types/oauthReauth.d.ts +13 -0
  204. package/dist/packages/bunshot-auth/src/types/oauthReauth.js +1 -0
  205. package/dist/packages/bunshot-auth/src/types/redis.d.ts +1 -0
  206. package/dist/packages/bunshot-auth/src/types/redis.js +1 -0
  207. package/dist/packages/bunshot-auth/src/types/saml.d.ts +10 -0
  208. package/dist/packages/bunshot-auth/src/types/saml.js +1 -0
  209. package/dist/packages/bunshot-auth/src/types/session.d.ts +18 -0
  210. package/dist/packages/bunshot-auth/src/types/session.js +1 -0
  211. package/dist/packages/bunshot-auth/src/types/store.d.ts +1 -0
  212. package/dist/packages/bunshot-auth/src/types/store.js +1 -0
  213. package/dist/packages/bunshot-core/src/adminProvider.d.ts +95 -0
  214. package/dist/packages/bunshot-core/src/adminProvider.js +1 -0
  215. package/dist/packages/bunshot-core/src/auditLog.d.ts +34 -0
  216. package/dist/packages/bunshot-core/src/auditLog.js +1 -0
  217. package/dist/packages/bunshot-core/src/auth-adapter.d.ts +227 -0
  218. package/dist/packages/bunshot-core/src/auth-adapter.js +4 -0
  219. package/dist/packages/bunshot-core/src/authVariables.d.ts +14 -0
  220. package/dist/packages/bunshot-core/src/authVariables.js +4 -0
  221. package/dist/packages/bunshot-core/src/cache.d.ts +12 -0
  222. package/dist/packages/bunshot-core/src/cache.js +21 -0
  223. package/dist/{lib → packages/bunshot-core/src}/captcha.d.ts +1 -10
  224. package/dist/packages/bunshot-core/src/captcha.js +1 -0
  225. package/dist/packages/bunshot-core/src/clearRegistry.d.ts +6 -0
  226. package/dist/packages/bunshot-core/src/clearRegistry.js +17 -0
  227. package/dist/packages/bunshot-core/src/clientIp.d.ts +3 -0
  228. package/dist/packages/bunshot-core/src/clientIp.js +45 -0
  229. package/dist/packages/bunshot-core/src/configLock.d.ts +4 -0
  230. package/dist/packages/bunshot-core/src/configLock.js +7 -0
  231. package/dist/packages/bunshot-core/src/configValidation.d.ts +22 -0
  232. package/dist/packages/bunshot-core/src/configValidation.js +39 -0
  233. package/dist/packages/bunshot-core/src/constants.js +10 -0
  234. package/dist/packages/bunshot-core/src/context/bunshotContext.d.ts +232 -0
  235. package/dist/packages/bunshot-core/src/context/bunshotContext.js +1 -0
  236. package/dist/packages/bunshot-core/src/context/contextAccess.d.ts +3 -0
  237. package/dist/packages/bunshot-core/src/context/contextAccess.js +16 -0
  238. package/dist/packages/bunshot-core/src/context/contextStore.d.ts +16 -0
  239. package/dist/packages/bunshot-core/src/context/contextStore.js +31 -0
  240. package/dist/packages/bunshot-core/src/context/frameworkConfig.d.ts +38 -0
  241. package/dist/packages/bunshot-core/src/context/frameworkConfig.js +1 -0
  242. package/dist/packages/bunshot-core/src/context/index.d.ts +4 -0
  243. package/dist/packages/bunshot-core/src/context/index.js +2 -0
  244. package/dist/packages/bunshot-core/src/context.d.ts +40 -0
  245. package/dist/packages/bunshot-core/src/context.js +35 -0
  246. package/dist/packages/bunshot-core/src/coreContracts.d.ts +47 -0
  247. package/dist/packages/bunshot-core/src/coreContracts.js +1 -0
  248. package/dist/packages/bunshot-core/src/coreRegistrar.d.ts +6 -0
  249. package/dist/packages/bunshot-core/src/coreRegistrar.js +42 -0
  250. package/dist/{lib → packages/bunshot-core/src}/createRoute.d.ts +4 -30
  251. package/dist/{lib → packages/bunshot-core/src}/createRoute.js +39 -88
  252. package/dist/packages/bunshot-core/src/cronRegistry.d.ts +11 -0
  253. package/dist/packages/bunshot-core/src/cronRegistry.js +1 -0
  254. package/dist/packages/bunshot-core/src/crypto.d.ts +43 -0
  255. package/dist/packages/bunshot-core/src/crypto.js +74 -0
  256. package/dist/packages/bunshot-core/src/csrf.d.ts +8 -0
  257. package/dist/packages/bunshot-core/src/csrf.js +1 -0
  258. package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.d.ts +7 -0
  259. package/dist/packages/bunshot-core/src/defaults/defaultFingerprint.js +19 -0
  260. package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.d.ts +6 -0
  261. package/dist/packages/bunshot-core/src/defaults/memoryCacheAdapter.js +40 -0
  262. package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.d.ts +6 -0
  263. package/dist/packages/bunshot-core/src/defaults/memoryRateLimit.js +24 -0
  264. package/dist/packages/bunshot-core/src/emailTemplates.d.ts +5 -0
  265. package/dist/packages/bunshot-core/src/emailTemplates.js +10 -0
  266. package/dist/{lib/HttpError.d.ts → packages/bunshot-core/src/errors.d.ts} +4 -1
  267. package/dist/{lib/HttpError.js → packages/bunshot-core/src/errors.js} +7 -1
  268. package/dist/packages/bunshot-core/src/eventBus.d.ts +270 -0
  269. package/dist/packages/bunshot-core/src/eventBus.js +143 -0
  270. package/dist/packages/bunshot-core/src/idempotency.d.ts +18 -0
  271. package/dist/packages/bunshot-core/src/idempotency.js +1 -0
  272. package/dist/packages/bunshot-core/src/index.d.ts +60 -0
  273. package/dist/packages/bunshot-core/src/index.js +34 -0
  274. package/dist/packages/bunshot-core/src/mail.d.ts +14 -0
  275. package/dist/packages/bunshot-core/src/mail.js +8 -0
  276. package/dist/packages/bunshot-core/src/memoryEviction.d.ts +24 -0
  277. package/dist/packages/bunshot-core/src/memoryEviction.js +52 -0
  278. package/dist/packages/bunshot-core/src/pagination.d.ts +45 -0
  279. package/dist/packages/bunshot-core/src/pagination.js +61 -0
  280. package/dist/packages/bunshot-core/src/permissions.d.ts +64 -0
  281. package/dist/packages/bunshot-core/src/permissions.js +27 -0
  282. package/dist/packages/bunshot-core/src/plugin.d.ts +44 -0
  283. package/dist/packages/bunshot-core/src/plugin.js +1 -0
  284. package/dist/packages/bunshot-core/src/rateLimit.d.ts +5 -0
  285. package/dist/packages/bunshot-core/src/rateLimit.js +18 -0
  286. package/dist/packages/bunshot-core/src/redis.d.ts +21 -0
  287. package/dist/packages/bunshot-core/src/redis.js +1 -0
  288. package/dist/packages/bunshot-core/src/routeAuth.d.ts +5 -0
  289. package/dist/packages/bunshot-core/src/routeAuth.js +11 -0
  290. package/dist/packages/bunshot-core/src/routeOverrides.d.ts +24 -0
  291. package/dist/packages/bunshot-core/src/routeOverrides.js +25 -0
  292. package/dist/packages/bunshot-core/src/routerAdapter.d.ts +6 -0
  293. package/dist/packages/bunshot-core/src/routerAdapter.js +56 -0
  294. package/dist/packages/bunshot-core/src/secrets.d.ts +48 -0
  295. package/dist/packages/bunshot-core/src/secrets.js +8 -0
  296. package/dist/packages/bunshot-core/src/signing.d.ts +41 -0
  297. package/dist/packages/bunshot-core/src/signing.js +1 -0
  298. package/dist/packages/bunshot-core/src/sse.d.ts +36 -0
  299. package/dist/packages/bunshot-core/src/sse.js +1 -0
  300. package/dist/packages/bunshot-core/src/storageAdapter.js +1 -0
  301. package/dist/packages/bunshot-core/src/storeInfra.d.ts +44 -0
  302. package/dist/packages/bunshot-core/src/storeInfra.js +18 -0
  303. package/dist/packages/bunshot-core/src/storeType.d.ts +7 -0
  304. package/dist/packages/bunshot-core/src/storeType.js +1 -0
  305. package/dist/packages/bunshot-core/src/testing.d.ts +1 -0
  306. package/dist/packages/bunshot-core/src/testing.js +1 -0
  307. package/dist/packages/bunshot-core/src/uploadRegistry.d.ts +23 -0
  308. package/dist/packages/bunshot-core/src/uploadRegistry.js +4 -0
  309. package/dist/packages/bunshot-core/src/userResolver.d.ts +5 -0
  310. package/dist/packages/bunshot-core/src/userResolver.js +14 -0
  311. package/dist/packages/bunshot-core/src/wsMessages.d.ts +42 -0
  312. package/dist/packages/bunshot-core/src/wsMessages.js +4 -0
  313. package/dist/packages/bunshot-permissions/src/adapters/memory.d.ts +7 -0
  314. package/dist/packages/bunshot-permissions/src/adapters/memory.js +73 -0
  315. package/dist/packages/bunshot-permissions/src/index.d.ts +10 -0
  316. package/dist/packages/bunshot-permissions/src/index.js +5 -0
  317. package/dist/packages/bunshot-permissions/src/lib/bootstrap.d.ts +7 -0
  318. package/dist/packages/bunshot-permissions/src/lib/bootstrap.js +12 -0
  319. package/dist/packages/bunshot-permissions/src/lib/evaluator.d.ts +10 -0
  320. package/dist/packages/bunshot-permissions/src/lib/evaluator.js +165 -0
  321. package/dist/packages/bunshot-permissions/src/lib/registry.d.ts +2 -0
  322. package/dist/packages/bunshot-permissions/src/lib/registry.js +31 -0
  323. package/dist/packages/bunshot-permissions/src/lib/validation.d.ts +1 -0
  324. package/dist/packages/bunshot-permissions/src/lib/validation.js +1 -0
  325. package/dist/packages/bunshot-permissions/src/types/adapter.d.ts +1 -0
  326. package/dist/packages/bunshot-permissions/src/types/adapter.js +1 -0
  327. package/dist/packages/bunshot-permissions/src/types/evaluator.d.ts +1 -0
  328. package/dist/packages/bunshot-permissions/src/types/evaluator.js +1 -0
  329. package/dist/packages/bunshot-permissions/src/types/models.d.ts +1 -0
  330. package/dist/packages/bunshot-permissions/src/types/models.js +1 -0
  331. package/dist/packages/bunshot-permissions/src/types/registry.d.ts +1 -0
  332. package/dist/packages/bunshot-permissions/src/types/registry.js +1 -0
  333. package/dist/packages/bunshot-postgres/src/adapter.d.ts +6 -0
  334. package/dist/packages/bunshot-postgres/src/adapter.js +794 -0
  335. package/dist/packages/bunshot-postgres/src/connection.d.ts +15 -0
  336. package/dist/packages/bunshot-postgres/src/connection.js +16 -0
  337. package/dist/packages/bunshot-postgres/src/index.d.ts +4 -0
  338. package/dist/packages/bunshot-postgres/src/index.js +2 -0
  339. package/dist/packages/bunshot-postgres/src/schema.d.ts +997 -0
  340. package/dist/packages/bunshot-postgres/src/schema.js +105 -0
  341. package/dist/src/app.d.ts +230 -0
  342. package/dist/src/app.js +182 -0
  343. package/dist/src/cli/commands/init.d.ts +10 -0
  344. package/dist/src/cli/commands/init.js +709 -0
  345. package/dist/src/cli/index.d.ts +1 -0
  346. package/dist/src/cli/index.js +3 -0
  347. package/dist/src/entrypoints/mongo.d.ts +6 -0
  348. package/dist/src/entrypoints/mongo.js +4 -0
  349. package/dist/src/entrypoints/queue.d.ts +2 -0
  350. package/dist/src/entrypoints/queue.js +1 -0
  351. package/dist/src/entrypoints/redis.d.ts +1 -0
  352. package/dist/src/entrypoints/redis.js +1 -0
  353. package/dist/{adapters → src/framework/adapters}/localStorage.d.ts +1 -1
  354. package/dist/{adapters → src/framework/adapters}/localStorage.js +10 -10
  355. package/dist/src/framework/adapters/memoryStorage.d.ts +2 -0
  356. package/dist/src/framework/adapters/memoryStorage.js +45 -0
  357. package/dist/{adapters → src/framework/adapters}/s3Storage.d.ts +1 -1
  358. package/dist/{adapters → src/framework/adapters}/s3Storage.js +12 -12
  359. package/dist/src/framework/admin/bunshotAccess.d.ts +2 -0
  360. package/dist/src/framework/admin/bunshotAccess.js +23 -0
  361. package/dist/src/framework/admin/bunshotUsers.d.ts +2 -0
  362. package/dist/src/framework/admin/bunshotUsers.js +103 -0
  363. package/dist/src/framework/admin/index.d.ts +7 -0
  364. package/dist/src/framework/admin/index.js +21 -0
  365. package/dist/src/framework/boundaryAdapters/cacheFactories.d.ts +13 -0
  366. package/dist/src/framework/boundaryAdapters/cacheFactories.js +86 -0
  367. package/dist/src/framework/boundaryAdapters/index.d.ts +2 -0
  368. package/dist/src/framework/boundaryAdapters/index.js +1 -0
  369. package/dist/src/framework/boundaryAdapters.d.ts +17 -0
  370. package/dist/src/framework/boundaryAdapters.js +62 -0
  371. package/dist/src/framework/buildContext.d.ts +33 -0
  372. package/dist/src/framework/buildContext.js +119 -0
  373. package/dist/src/framework/config/schema.d.ts +447 -0
  374. package/dist/src/framework/config/schema.js +528 -0
  375. package/dist/src/framework/createInfrastructure.d.ts +76 -0
  376. package/dist/src/framework/createInfrastructure.js +221 -0
  377. package/dist/src/framework/lib/auditLog.d.ts +23 -0
  378. package/dist/src/framework/lib/auditLog.js +416 -0
  379. package/dist/src/framework/lib/captcha.d.ts +11 -0
  380. package/dist/{lib → src/framework/lib}/captcha.js +13 -10
  381. package/dist/{lib → src/framework/lib}/createDtoMapper.js +4 -4
  382. package/dist/src/framework/lib/createRoute.d.ts +1 -0
  383. package/dist/src/framework/lib/createRoute.js +2 -0
  384. package/dist/{lib → src/framework/lib}/idempotency.d.ts +2 -6
  385. package/dist/src/framework/lib/idempotency.js +74 -0
  386. package/dist/src/framework/lib/logger.d.ts +3 -0
  387. package/dist/src/framework/lib/logger.js +14 -0
  388. package/dist/src/framework/lib/metrics.d.ts +34 -0
  389. package/dist/{lib → src/framework/lib}/metrics.js +49 -57
  390. package/dist/src/framework/lib/pagination.d.ts +42 -0
  391. package/dist/src/framework/lib/pagination.js +51 -0
  392. package/dist/src/framework/lib/redisTransport.d.ts +38 -0
  393. package/dist/src/framework/lib/redisTransport.js +107 -0
  394. package/dist/src/framework/lib/resolveUserId.d.ts +2 -0
  395. package/dist/src/framework/lib/resolveUserId.js +5 -0
  396. package/dist/src/framework/lib/sseCollision.d.ts +6 -0
  397. package/dist/src/framework/lib/sseCollision.js +26 -0
  398. package/dist/src/framework/lib/storageAdapter.d.ts +1 -0
  399. package/dist/src/framework/lib/storageAdapter.js +1 -0
  400. package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.js +4 -4
  401. package/dist/src/framework/lib/tenant.d.ts +21 -0
  402. package/dist/src/framework/lib/tenant.js +70 -0
  403. package/dist/{lib → src/framework/lib}/upload.d.ts +11 -10
  404. package/dist/src/framework/lib/upload.js +132 -0
  405. package/dist/src/framework/lib/uploadRegistry.d.ts +23 -0
  406. package/dist/src/framework/lib/uploadRegistry.js +34 -0
  407. package/dist/{lib → src/framework/lib}/validate.d.ts +1 -1
  408. package/dist/{lib → src/framework/lib}/validate.js +2 -2
  409. package/dist/src/framework/lib/ws.d.ts +19 -0
  410. package/dist/src/framework/lib/ws.js +130 -0
  411. package/dist/src/framework/lib/wsHeartbeat.d.ts +12 -0
  412. package/dist/src/framework/lib/wsHeartbeat.js +53 -0
  413. package/dist/src/framework/lib/wsMessages.d.ts +25 -0
  414. package/dist/src/framework/lib/wsMessages.js +45 -0
  415. package/dist/src/framework/lib/wsNamespace.d.ts +17 -0
  416. package/dist/src/framework/lib/wsNamespace.js +19 -0
  417. package/dist/src/framework/lib/wsPresence.d.ts +17 -0
  418. package/dist/src/framework/lib/wsPresence.js +84 -0
  419. package/dist/src/framework/lib/wsTransport.d.ts +38 -0
  420. package/dist/src/framework/lib/wsTransport.js +9 -0
  421. package/dist/{lib → src/framework/lib}/zodToMongoose.d.ts +1 -1
  422. package/dist/{lib → src/framework/lib}/zodToMongoose.js +11 -11
  423. package/dist/{middleware → src/framework/middleware}/auditLog.d.ts +4 -3
  424. package/dist/src/framework/middleware/auditLog.js +42 -0
  425. package/dist/{middleware → src/framework/middleware}/botProtection.d.ts +2 -2
  426. package/dist/{middleware → src/framework/middleware}/botProtection.js +8 -9
  427. package/dist/src/framework/middleware/cacheResponse.d.ts +35 -0
  428. package/dist/src/framework/middleware/cacheResponse.js +126 -0
  429. package/dist/{middleware → src/framework/middleware}/captcha.d.ts +2 -3
  430. package/dist/src/framework/middleware/captcha.js +37 -0
  431. package/dist/{middleware → src/framework/middleware}/errorHandler.d.ts +1 -1
  432. package/dist/{middleware → src/framework/middleware}/errorHandler.js +2 -2
  433. package/dist/src/framework/middleware/index.js +1 -0
  434. package/dist/{middleware → src/framework/middleware}/logger.d.ts +1 -1
  435. package/dist/src/framework/middleware/metrics.d.ts +12 -0
  436. package/dist/src/framework/middleware/metrics.js +26 -0
  437. package/dist/{middleware → src/framework/middleware}/rateLimit.d.ts +2 -2
  438. package/dist/src/framework/middleware/rateLimit.js +22 -0
  439. package/dist/src/framework/middleware/requestId.d.ts +3 -0
  440. package/dist/{middleware → src/framework/middleware}/requestId.js +2 -2
  441. package/dist/{middleware → src/framework/middleware}/requestLogger.d.ts +3 -3
  442. package/dist/{middleware → src/framework/middleware}/requestLogger.js +17 -12
  443. package/dist/{middleware → src/framework/middleware}/requestSigning.d.ts +2 -2
  444. package/dist/{middleware → src/framework/middleware}/requestSigning.js +18 -20
  445. package/dist/src/framework/middleware/tenant.d.ts +14 -0
  446. package/dist/{middleware → src/framework/middleware}/tenant.js +31 -27
  447. package/dist/src/framework/middleware/upload.d.ts +5 -0
  448. package/dist/{middleware → src/framework/middleware}/upload.js +4 -4
  449. package/dist/{middleware → src/framework/middleware}/webhookAuth.d.ts +3 -3
  450. package/dist/{middleware → src/framework/middleware}/webhookAuth.js +11 -12
  451. package/dist/src/framework/models/AuditLog.d.ts +21 -0
  452. package/dist/src/framework/models/AuditLog.js +31 -0
  453. package/dist/src/framework/mountMiddleware.d.ts +91 -0
  454. package/dist/src/framework/mountMiddleware.js +128 -0
  455. package/dist/src/framework/mountOptionalEndpoints.d.ts +103 -0
  456. package/dist/src/framework/mountOptionalEndpoints.js +64 -0
  457. package/dist/src/framework/mountRoutes.d.ts +21 -0
  458. package/dist/src/framework/mountRoutes.js +144 -0
  459. package/dist/src/framework/persistence/cronRegistry.d.ts +28 -0
  460. package/dist/src/framework/persistence/cronRegistry.js +139 -0
  461. package/dist/src/framework/persistence/idempotency.d.ts +26 -0
  462. package/dist/src/framework/persistence/idempotency.js +178 -0
  463. package/dist/src/framework/persistence/index.d.ts +6 -0
  464. package/dist/src/framework/persistence/index.js +8 -0
  465. package/dist/src/framework/persistence/storeInfra.d.ts +9 -0
  466. package/dist/src/framework/persistence/storeInfra.js +1 -0
  467. package/dist/src/framework/persistence/uploadRegistry.d.ts +35 -0
  468. package/dist/src/framework/persistence/uploadRegistry.js +235 -0
  469. package/dist/src/framework/persistence/wsMessages.d.ts +22 -0
  470. package/dist/src/framework/persistence/wsMessages.js +296 -0
  471. package/dist/src/framework/preloadSchemas.d.ts +24 -0
  472. package/dist/src/framework/preloadSchemas.js +42 -0
  473. package/dist/src/framework/registerBoundaryAdapters.d.ts +23 -0
  474. package/dist/src/framework/registerBoundaryAdapters.js +46 -0
  475. package/dist/src/framework/routes/admin.d.ts +9 -0
  476. package/dist/src/framework/routes/admin.js +361 -0
  477. package/dist/src/framework/routes/health.d.ts +1 -0
  478. package/dist/src/framework/routes/health.js +21 -0
  479. package/dist/src/framework/routes/home.d.ts +1 -0
  480. package/dist/src/framework/routes/home.js +18 -0
  481. package/dist/src/framework/routes/jobs.d.ts +3 -0
  482. package/dist/{routes → src/framework/routes}/jobs.js +128 -103
  483. package/dist/src/framework/routes/metrics.d.ts +10 -0
  484. package/dist/src/framework/routes/metrics.js +57 -0
  485. package/dist/{routes → src/framework/routes}/uploads.d.ts +3 -3
  486. package/dist/src/framework/routes/uploads.js +262 -0
  487. package/dist/src/framework/runPluginLifecycle.d.ts +27 -0
  488. package/dist/src/framework/runPluginLifecycle.js +121 -0
  489. package/dist/src/framework/secrets/frameworkSecretSchema.d.ts +58 -0
  490. package/dist/src/framework/secrets/frameworkSecretSchema.js +20 -0
  491. package/dist/src/framework/secrets/index.d.ts +9 -0
  492. package/dist/src/framework/secrets/index.js +7 -0
  493. package/dist/src/framework/secrets/providers/envProvider.d.ts +15 -0
  494. package/dist/src/framework/secrets/providers/envProvider.js +18 -0
  495. package/dist/src/framework/secrets/providers/fileProvider.d.ts +8 -0
  496. package/dist/src/framework/secrets/providers/fileProvider.js +82 -0
  497. package/dist/src/framework/secrets/providers/ssmProvider.d.ts +20 -0
  498. package/dist/src/framework/secrets/providers/ssmProvider.js +127 -0
  499. package/dist/src/framework/secrets/resolveSecretBundle.d.ts +53 -0
  500. package/dist/src/framework/secrets/resolveSecretBundle.js +84 -0
  501. package/dist/src/framework/secrets/resolveSecrets.d.ts +18 -0
  502. package/dist/src/framework/secrets/resolveSecrets.js +34 -0
  503. package/dist/src/framework/sse/index.d.ts +21 -0
  504. package/dist/src/framework/sse/index.js +109 -0
  505. package/dist/src/framework/ws/index.d.ts +11 -0
  506. package/dist/src/framework/ws/index.js +8 -0
  507. package/dist/src/index.d.ts +87 -0
  508. package/dist/src/index.js +58 -0
  509. package/dist/src/lib/appConfig.d.ts +7 -0
  510. package/dist/src/lib/appConfig.js +27 -0
  511. package/dist/src/lib/appMeta.d.ts +7 -0
  512. package/dist/src/lib/appMeta.js +3 -0
  513. package/dist/src/lib/authConfig.d.ts +532 -0
  514. package/dist/{lib/appConfig.js → src/lib/authConfig.js} +75 -17
  515. package/dist/{lib → src/lib}/context.d.ts +6 -12
  516. package/dist/{lib → src/lib}/context.js +5 -5
  517. package/dist/src/lib/logger.d.ts +1 -0
  518. package/dist/src/lib/logger.js +1 -0
  519. package/dist/src/lib/mongo.d.ts +58 -0
  520. package/dist/src/lib/mongo.js +96 -0
  521. package/dist/src/lib/queue.d.ts +72 -0
  522. package/dist/src/lib/queue.js +152 -0
  523. package/dist/src/lib/redis.d.ts +28 -0
  524. package/dist/src/lib/redis.js +72 -0
  525. package/dist/{lib → src/lib}/signing.d.ts +2 -2
  526. package/dist/src/lib/signing.js +210 -0
  527. package/dist/src/lib/signingConfig.d.ts +40 -0
  528. package/dist/src/lib/signingConfig.js +28 -0
  529. package/dist/src/server.d.ts +146 -0
  530. package/dist/src/server.js +469 -0
  531. package/dist/src/shared/lib/HttpError.d.ts +1 -0
  532. package/dist/src/shared/lib/HttpError.js +2 -0
  533. package/dist/src/shared/lib/constants.d.ts +10 -0
  534. package/dist/src/shared/lib/crypto.d.ts +43 -0
  535. package/dist/src/shared/lib/crypto.js +74 -0
  536. package/dist/src/shared/lib/signing.d.ts +52 -0
  537. package/dist/{lib → src/shared/lib}/signing.js +35 -8
  538. package/dist/src/testing.d.ts +34 -0
  539. package/dist/src/testing.js +93 -0
  540. package/package.json +100 -26
  541. package/dist/adapters/memoryAuth.d.ts +0 -52
  542. package/dist/adapters/memoryAuth.js +0 -749
  543. package/dist/adapters/memoryStorage.d.ts +0 -3
  544. package/dist/adapters/memoryStorage.js +0 -44
  545. package/dist/adapters/mongoAuth.d.ts +0 -2
  546. package/dist/adapters/mongoAuth.js +0 -403
  547. package/dist/adapters/sqliteAuth.d.ts +0 -72
  548. package/dist/adapters/sqliteAuth.js +0 -858
  549. package/dist/app.d.ts +0 -559
  550. package/dist/app.js +0 -651
  551. package/dist/entrypoints/mongo.d.ts +0 -5
  552. package/dist/entrypoints/mongo.js +0 -4
  553. package/dist/entrypoints/queue.d.ts +0 -2
  554. package/dist/entrypoints/queue.js +0 -1
  555. package/dist/entrypoints/redis.d.ts +0 -1
  556. package/dist/entrypoints/redis.js +0 -1
  557. package/dist/index.d.ts +0 -117
  558. package/dist/index.js +0 -88
  559. package/dist/lib/appConfig.d.ts +0 -275
  560. package/dist/lib/auditLog.d.ts +0 -58
  561. package/dist/lib/auditLog.js +0 -218
  562. package/dist/lib/authAdapter.d.ts +0 -246
  563. package/dist/lib/authAdapter.js +0 -7
  564. package/dist/lib/authRateLimit.d.ts +0 -13
  565. package/dist/lib/authRateLimit.js +0 -117
  566. package/dist/lib/clientIp.d.ts +0 -14
  567. package/dist/lib/credentialStuffing.d.ts +0 -31
  568. package/dist/lib/credentialStuffing.js +0 -77
  569. package/dist/lib/crypto.d.ts +0 -11
  570. package/dist/lib/crypto.js +0 -22
  571. package/dist/lib/deletionCancelToken.d.ts +0 -12
  572. package/dist/lib/deletionCancelToken.js +0 -88
  573. package/dist/lib/emailVerification.d.ts +0 -19
  574. package/dist/lib/emailVerification.js +0 -129
  575. package/dist/lib/fingerprint.js +0 -36
  576. package/dist/lib/idempotency.js +0 -182
  577. package/dist/lib/jwks.d.ts +0 -25
  578. package/dist/lib/jwks.js +0 -51
  579. package/dist/lib/jwt.d.ts +0 -15
  580. package/dist/lib/jwt.js +0 -111
  581. package/dist/lib/metrics.d.ts +0 -14
  582. package/dist/lib/mfaChallenge.d.ts +0 -55
  583. package/dist/lib/mfaChallenge.js +0 -398
  584. package/dist/lib/mongo.d.ts +0 -39
  585. package/dist/lib/mongo.js +0 -124
  586. package/dist/lib/oauth.d.ts +0 -40
  587. package/dist/lib/oauth.js +0 -101
  588. package/dist/lib/oauthCode.d.ts +0 -15
  589. package/dist/lib/oauthCode.js +0 -95
  590. package/dist/lib/pagination.d.ts +0 -119
  591. package/dist/lib/pagination.js +0 -166
  592. package/dist/lib/queue.d.ts +0 -37
  593. package/dist/lib/queue.js +0 -117
  594. package/dist/lib/redis.d.ts +0 -9
  595. package/dist/lib/redis.js +0 -61
  596. package/dist/lib/resetPassword.d.ts +0 -12
  597. package/dist/lib/resetPassword.js +0 -93
  598. package/dist/lib/roles.d.ts +0 -7
  599. package/dist/lib/roles.js +0 -49
  600. package/dist/lib/saml.d.ts +0 -25
  601. package/dist/lib/saml.js +0 -64
  602. package/dist/lib/securityEvents.d.ts +0 -28
  603. package/dist/lib/securityEvents.js +0 -26
  604. package/dist/lib/session.d.ts +0 -49
  605. package/dist/lib/session.js +0 -597
  606. package/dist/lib/tenant.d.ts +0 -15
  607. package/dist/lib/tenant.js +0 -65
  608. package/dist/lib/upload.js +0 -112
  609. package/dist/lib/uploadRegistry.d.ts +0 -18
  610. package/dist/lib/uploadRegistry.js +0 -83
  611. package/dist/lib/ws.d.ts +0 -22
  612. package/dist/lib/ws.js +0 -96
  613. package/dist/lib/wsHeartbeat.d.ts +0 -12
  614. package/dist/lib/wsHeartbeat.js +0 -57
  615. package/dist/lib/wsMessages.d.ts +0 -40
  616. package/dist/lib/wsMessages.js +0 -330
  617. package/dist/lib/wsPresence.d.ts +0 -25
  618. package/dist/lib/wsPresence.js +0 -99
  619. package/dist/middleware/auditLog.js +0 -39
  620. package/dist/middleware/bearerAuth.d.ts +0 -2
  621. package/dist/middleware/bearerAuth.js +0 -11
  622. package/dist/middleware/cacheResponse.d.ts +0 -15
  623. package/dist/middleware/cacheResponse.js +0 -178
  624. package/dist/middleware/captcha.js +0 -36
  625. package/dist/middleware/csrf.js +0 -129
  626. package/dist/middleware/identify.d.ts +0 -3
  627. package/dist/middleware/identify.js +0 -122
  628. package/dist/middleware/index.js +0 -1
  629. package/dist/middleware/metrics.d.ts +0 -9
  630. package/dist/middleware/metrics.js +0 -26
  631. package/dist/middleware/rateLimit.js +0 -22
  632. package/dist/middleware/requestId.d.ts +0 -3
  633. package/dist/middleware/scimAuth.d.ts +0 -8
  634. package/dist/middleware/scimAuth.js +0 -29
  635. package/dist/middleware/tenant.d.ts +0 -5
  636. package/dist/middleware/upload.d.ts +0 -5
  637. package/dist/middleware/userAuth.d.ts +0 -3
  638. package/dist/middleware/userAuth.js +0 -6
  639. package/dist/models/AuditLog.d.ts +0 -30
  640. package/dist/models/AuditLog.js +0 -39
  641. package/dist/models/AuthUser.js +0 -55
  642. package/dist/models/Group.d.ts +0 -21
  643. package/dist/models/Group.js +0 -28
  644. package/dist/models/GroupMembership.js +0 -25
  645. package/dist/models/TenantRole.d.ts +0 -15
  646. package/dist/models/TenantRole.js +0 -23
  647. package/dist/routes/auth.d.ts +0 -12
  648. package/dist/routes/auth.js +0 -744
  649. package/dist/routes/groups.js +0 -346
  650. package/dist/routes/health.d.ts +0 -1
  651. package/dist/routes/health.js +0 -22
  652. package/dist/routes/home.d.ts +0 -1
  653. package/dist/routes/home.js +0 -16
  654. package/dist/routes/jobs.d.ts +0 -2
  655. package/dist/routes/m2m.d.ts +0 -2
  656. package/dist/routes/m2m.js +0 -72
  657. package/dist/routes/metrics.d.ts +0 -8
  658. package/dist/routes/metrics.js +0 -55
  659. package/dist/routes/mfa.d.ts +0 -5
  660. package/dist/routes/mfa.js +0 -628
  661. package/dist/routes/oauth.d.ts +0 -2
  662. package/dist/routes/oauth.js +0 -520
  663. package/dist/routes/oidc.d.ts +0 -2
  664. package/dist/routes/oidc.js +0 -29
  665. package/dist/routes/passkey.d.ts +0 -1
  666. package/dist/routes/passkey.js +0 -157
  667. package/dist/routes/saml.d.ts +0 -2
  668. package/dist/routes/saml.js +0 -86
  669. package/dist/routes/scim.d.ts +0 -2
  670. package/dist/routes/scim.js +0 -255
  671. package/dist/routes/uploads.js +0 -227
  672. package/dist/schemas/auth.js +0 -30
  673. package/dist/server.d.ts +0 -57
  674. package/dist/server.js +0 -112
  675. package/dist/services/auth.d.ts +0 -29
  676. package/dist/services/auth.js +0 -238
  677. package/dist/ws/index.d.ts +0 -10
  678. package/dist/ws/index.js +0 -39
  679. package/docs/sections/adding-middleware/full.md +0 -35
  680. package/docs/sections/adding-models/full.md +0 -125
  681. package/docs/sections/adding-models/overview.md +0 -13
  682. package/docs/sections/adding-routes/full.md +0 -182
  683. package/docs/sections/adding-routes/overview.md +0 -23
  684. package/docs/sections/auth-flow/full.md +0 -790
  685. package/docs/sections/auth-flow/overview.md +0 -10
  686. package/docs/sections/auth-security-examples/full.md +0 -388
  687. package/docs/sections/authentication/full.md +0 -130
  688. package/docs/sections/authentication/overview.md +0 -5
  689. package/docs/sections/cli/full.md +0 -42
  690. package/docs/sections/configuration/full.md +0 -172
  691. package/docs/sections/configuration/overview.md +0 -18
  692. package/docs/sections/configuration-example/full.md +0 -117
  693. package/docs/sections/configuration-example/overview.md +0 -30
  694. package/docs/sections/documentation/full.md +0 -171
  695. package/docs/sections/environment-variables/full.md +0 -55
  696. package/docs/sections/exports/full.md +0 -123
  697. package/docs/sections/extending-context/full.md +0 -59
  698. package/docs/sections/header.md +0 -3
  699. package/docs/sections/installation/full.md +0 -6
  700. package/docs/sections/jobs/full.md +0 -140
  701. package/docs/sections/jobs/overview.md +0 -15
  702. package/docs/sections/logging/full.md +0 -83
  703. package/docs/sections/metrics/full.md +0 -131
  704. package/docs/sections/mongodb-connections/full.md +0 -45
  705. package/docs/sections/mongodb-connections/overview.md +0 -7
  706. package/docs/sections/multi-tenancy/full.md +0 -66
  707. package/docs/sections/multi-tenancy/overview.md +0 -15
  708. package/docs/sections/oauth/full.md +0 -189
  709. package/docs/sections/oauth/overview.md +0 -16
  710. package/docs/sections/package-development/full.md +0 -7
  711. package/docs/sections/pagination/full.md +0 -93
  712. package/docs/sections/passkey-login/full.md +0 -90
  713. package/docs/sections/passkey-login/overview.md +0 -1
  714. package/docs/sections/peer-dependencies/full.md +0 -47
  715. package/docs/sections/quick-start/full.md +0 -43
  716. package/docs/sections/response-caching/full.md +0 -117
  717. package/docs/sections/response-caching/overview.md +0 -13
  718. package/docs/sections/roles/full.md +0 -225
  719. package/docs/sections/roles/overview.md +0 -14
  720. package/docs/sections/running-without-redis/full.md +0 -16
  721. package/docs/sections/running-without-redis-or-mongodb/full.md +0 -60
  722. package/docs/sections/signing/full.md +0 -203
  723. package/docs/sections/stack/full.md +0 -10
  724. package/docs/sections/uploads/full.md +0 -208
  725. package/docs/sections/versioning/full.md +0 -85
  726. package/docs/sections/webhook-auth/full.md +0 -100
  727. package/docs/sections/websocket/full.md +0 -196
  728. package/docs/sections/websocket/overview.md +0 -5
  729. package/docs/sections/websocket-rooms/full.md +0 -102
  730. package/docs/sections/websocket-rooms/overview.md +0 -5
  731. /package/dist/{lib/storageAdapter.js → packages/bunshot-admin/src/types/env.js} +0 -0
  732. /package/dist/{lib → packages/bunshot-auth/src/lib}/fingerprint.d.ts +0 -0
  733. /package/dist/{lib → packages/bunshot-auth/src/lib}/logger.d.ts +0 -0
  734. /package/dist/{lib → packages/bunshot-core/src}/constants.d.ts +0 -0
  735. /package/dist/{lib → packages/bunshot-core/src}/storageAdapter.d.ts +0 -0
  736. /package/dist/{lib → src/framework/lib}/createDtoMapper.d.ts +0 -0
  737. /package/dist/{lib → src/framework/lib}/stripUnreferencedSchemas.d.ts +0 -0
  738. /package/dist/{middleware → src/framework/middleware}/cors.d.ts +0 -0
  739. /package/dist/{middleware → src/framework/middleware}/cors.js +0 -0
  740. /package/dist/{middleware → src/framework/middleware}/index.d.ts +0 -0
  741. /package/dist/{middleware → src/framework/middleware}/logger.js +0 -0
  742. /package/dist/{lib → src/shared/lib}/constants.js +0 -0
@@ -0,0 +1,1624 @@
1
+ import { checkBreachedPassword } from '../lib/breachedPassword';
2
+ import { getAuthCookieOptions } from '../lib/cookieOptions';
3
+ import { consumeDeletionCancelToken, createDeletionCancelToken, } from '../lib/deletionCancelToken';
4
+ import { consumeVerificationToken, createVerificationToken } from '../lib/emailVerification';
5
+ import { isProd } from '../lib/env';
6
+ import { consumeMagicLinkToken, createMagicLinkToken } from '../lib/magicLink';
7
+ import { checkPasswordNotReused, recordPasswordChange } from '../lib/passwordHistory';
8
+ import { consumeResetToken, createResetToken } from '../lib/resetPassword';
9
+ import { clearCsrfToken, refreshCsrfToken } from '../middleware/csrf';
10
+ import { userAuth } from '../middleware/userAuth';
11
+ import { makeLoginSchema, makeRegisterSchema, resetPasswordSchema } from '../schemas/auth';
12
+ import { ErrorResponse } from '../schemas/error';
13
+ import { SuccessResponse } from '../schemas/success';
14
+ import * as AuthService from '../services/auth';
15
+ import { emitLoginSuccess } from '../services/auth';
16
+ import { deleteCookie, getCookie, setCookie } from 'hono/cookie';
17
+ import { z } from 'zod';
18
+ import { createRoute, withSecurity } from '../../../bunshot-core/src/index.js';
19
+ import { COOKIE_REFRESH_TOKEN, COOKIE_TOKEN, HEADER_REFRESH_TOKEN, HEADER_USER_TOKEN, HttpError, createRouter, } from '../../../bunshot-core/src/index.js';
20
+ import { getClientIp } from '../../../bunshot-core/src/index.js';
21
+ const TokenResponse = z
22
+ .object({
23
+ token: z
24
+ .string()
25
+ .describe('JWT session token. Also set as an HttpOnly session cookie. Empty string when mfaRequired is true.'),
26
+ userId: z.string().describe('Unique user ID.'),
27
+ email: z
28
+ .string()
29
+ .optional()
30
+ .describe("User's email address (present when primaryField is 'email')."),
31
+ emailVerified: z
32
+ .boolean()
33
+ .optional()
34
+ .describe('Whether the email address has been verified (present when emailVerification is configured).'),
35
+ googleLinked: z
36
+ .boolean()
37
+ .optional()
38
+ .describe('Whether a Google OAuth account is linked to this user.'),
39
+ refreshToken: z
40
+ .string()
41
+ .optional()
42
+ .describe('Refresh token (present when refreshTokens is configured). Also set as an HttpOnly cookie.'),
43
+ mfaRequired: z
44
+ .boolean()
45
+ .optional()
46
+ .describe('When true, complete MFA via POST /auth/mfa/verify before accessing the API.'),
47
+ mfaToken: z
48
+ .string()
49
+ .optional()
50
+ .describe('MFA challenge token. Pass to POST /auth/mfa/verify with a TOTP or recovery code.'),
51
+ mfaMethods: z
52
+ .array(z.string())
53
+ .optional()
54
+ .describe("Available MFA methods when mfaRequired is true (e.g., 'totp', 'emailOtp', 'webauthn')."),
55
+ webauthnOptions: z
56
+ .record(z.string(), z.unknown())
57
+ .optional()
58
+ .describe("WebAuthn assertion options (present when mfaMethods includes 'webauthn'). Pass directly to navigator.credentials.get()."),
59
+ })
60
+ .openapi('TokenResponse');
61
+ const PasswordUpdateResponse = SuccessResponse.extend({
62
+ token: z.string().optional().describe('Replacement session token when sessions are reissued.'),
63
+ }).openapi('PasswordUpdateResponse');
64
+ const tags = ['Auth'];
65
+ const hookCtx = (c) => ({
66
+ ip: getClientIp(c) !== 'unknown' ? getClientIp(c) : undefined,
67
+ userAgent: c.req.header('user-agent') ?? undefined,
68
+ requestId: c.get('requestId'),
69
+ });
70
+ export const createAuthRouter = ({ primaryField, emailVerification, passwordReset, rateLimit, accountDeletion, refreshTokens, stepUp, sessionPolicy, concealRegistration, magicLink, }, runtime) => {
71
+ const { adapter, eventBus } = runtime;
72
+ const getConfig = () => runtime.config;
73
+ const cookieOptions = (maxAge) => getAuthCookieOptions(isProd(), runtime.config, maxAge);
74
+ const router = createRouter();
75
+ const RegisterSchema = makeRegisterSchema(primaryField, runtime.config.passwordPolicy);
76
+ const LoginSchema = makeLoginSchema(primaryField);
77
+ const fieldLabel = primaryField.charAt(0).toUpperCase() + primaryField.slice(1);
78
+ const alreadyRegisteredMsg = `${fieldLabel} already registered`;
79
+ // Resolve limits with defaults
80
+ const loginOpts = {
81
+ windowMs: rateLimit?.login?.windowMs ?? 15 * 60 * 1000,
82
+ max: rateLimit?.login?.max ?? 10,
83
+ };
84
+ const registerOpts = {
85
+ windowMs: rateLimit?.register?.windowMs ?? 60 * 60 * 1000,
86
+ max: rateLimit?.register?.max ?? 5,
87
+ };
88
+ const verifyOpts = {
89
+ windowMs: rateLimit?.verifyEmail?.windowMs ?? 15 * 60 * 1000,
90
+ max: rateLimit?.verifyEmail?.max ?? 10,
91
+ };
92
+ const resendOpts = {
93
+ windowMs: rateLimit?.resendVerification?.windowMs ?? 60 * 60 * 1000,
94
+ max: rateLimit?.resendVerification?.max ?? 3,
95
+ };
96
+ const forgotOpts = {
97
+ windowMs: rateLimit?.forgotPassword?.windowMs ?? 15 * 60 * 1000,
98
+ max: rateLimit?.forgotPassword?.max ?? 5,
99
+ };
100
+ const resetOpts = {
101
+ windowMs: rateLimit?.resetPassword?.windowMs ?? 15 * 60 * 1000,
102
+ max: rateLimit?.resetPassword?.max ?? 10,
103
+ };
104
+ const CONCEALED_REGISTER_MESSAGE = "If this email isn't registered yet, check your inbox to complete sign-up.";
105
+ if (concealRegistration) {
106
+ router.openapi(createRoute({
107
+ method: 'post',
108
+ path: '/auth/register',
109
+ summary: 'Register a new account',
110
+ description: 'Creates a new user account. When concealRegistration is enabled, always returns 200 with a generic message regardless of whether the email is already registered. Rate-limited by IP.',
111
+ tags,
112
+ request: {
113
+ body: {
114
+ content: { 'application/json': { schema: RegisterSchema } },
115
+ description: 'Registration credentials.',
116
+ },
117
+ },
118
+ responses: {
119
+ 200: {
120
+ content: { 'application/json': { schema: z.object({ message: z.string() }) } },
121
+ description: 'Request received. Check your inbox to complete sign-up.',
122
+ },
123
+ 400: {
124
+ content: { 'application/json': { schema: ErrorResponse } },
125
+ description: 'Validation error (e.g. missing field, password too short).',
126
+ },
127
+ 429: {
128
+ content: { 'application/json': { schema: ErrorResponse } },
129
+ description: 'Too many registration attempts from this IP. Try again later.',
130
+ },
131
+ },
132
+ }), async (c) => {
133
+ const ip = getClientIp(c);
134
+ if (await runtime.rateLimit.trackAttempt(`register:${ip}`, registerOpts)) {
135
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
136
+ return c.json({ error: 'Too many registration attempts. Try again later.' }, 429);
137
+ }
138
+ const body = c.req.valid('json');
139
+ const identifier = body[primaryField];
140
+ const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
141
+ const existing = await findFn(identifier);
142
+ if (existing) {
143
+ eventBus.emit('security.auth.register.concealed', { meta: { identifier } });
144
+ if (concealRegistration.onExistingAccount) {
145
+ void concealRegistration.onExistingAccount(identifier).catch(err => {
146
+ console.error('[concealRegistration] onExistingAccount error:', err instanceof Error ? err.message : String(err));
147
+ });
148
+ }
149
+ return c.json({ message: CONCEALED_REGISTER_MESSAGE }, 200);
150
+ }
151
+ const breachConfig = getConfig().breachedPassword;
152
+ if (breachConfig) {
153
+ const { breached } = await checkBreachedPassword(body.password, breachConfig, undefined, runtime.eventBus);
154
+ if (breached && breachConfig.block !== false) {
155
+ throw new HttpError(400, 'This password has appeared in a data breach. Please choose a different password.', 'BREACHED_PASSWORD');
156
+ }
157
+ }
158
+ const metadata = {
159
+ ipAddress: ip !== 'unknown' ? ip : undefined,
160
+ userAgent: c.req.header('user-agent') ?? undefined,
161
+ };
162
+ await AuthService.register(identifier, body.password, runtime, {
163
+ metadata,
164
+ skipSession: true,
165
+ hookContext: hookCtx(c),
166
+ });
167
+ return c.json({ message: CONCEALED_REGISTER_MESSAGE }, 200);
168
+ });
169
+ }
170
+ else {
171
+ router.openapi(createRoute({
172
+ method: 'post',
173
+ path: '/auth/register',
174
+ summary: 'Register a new account',
175
+ description: 'Creates a new user account and returns a JWT session token. The token is also set as an HttpOnly session cookie. Rate-limited by IP.',
176
+ tags,
177
+ request: {
178
+ body: {
179
+ content: { 'application/json': { schema: RegisterSchema } },
180
+ description: 'Registration credentials.',
181
+ },
182
+ },
183
+ responses: {
184
+ 201: {
185
+ content: { 'application/json': { schema: TokenResponse } },
186
+ description: 'Account created. Returns a session token.',
187
+ },
188
+ 400: {
189
+ content: { 'application/json': { schema: ErrorResponse } },
190
+ description: 'Validation error (e.g. missing field, password too short).',
191
+ },
192
+ 409: {
193
+ content: { 'application/json': { schema: ErrorResponse } },
194
+ description: alreadyRegisteredMsg,
195
+ },
196
+ 429: {
197
+ content: { 'application/json': { schema: ErrorResponse } },
198
+ description: 'Too many registration attempts from this IP. Try again later.',
199
+ },
200
+ },
201
+ }), async (c) => {
202
+ const ip = getClientIp(c);
203
+ if (await runtime.rateLimit.trackAttempt(`register:${ip}`, registerOpts)) {
204
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
205
+ return c.json({ error: 'Too many registration attempts. Try again later.' }, 429);
206
+ }
207
+ const body = c.req.valid('json');
208
+ const identifier = body[primaryField];
209
+ const breachConfig = getConfig().breachedPassword;
210
+ if (breachConfig) {
211
+ const { breached } = await checkBreachedPassword(body.password, breachConfig, undefined, runtime.eventBus);
212
+ if (breached && breachConfig.block !== false) {
213
+ throw new HttpError(400, 'This password has appeared in a data breach. Please choose a different password.', 'BREACHED_PASSWORD');
214
+ }
215
+ }
216
+ const metadata = {
217
+ ipAddress: ip !== 'unknown' ? ip : undefined,
218
+ userAgent: c.req.header('user-agent') ?? undefined,
219
+ };
220
+ const result = await AuthService.register(identifier, body.password, runtime, {
221
+ metadata,
222
+ hookContext: hookCtx(c),
223
+ });
224
+ setCookie(c, COOKIE_TOKEN, result.token, cookieOptions(refreshTokens ? (getConfig().refreshToken?.accessTokenExpiry ?? 900) : undefined));
225
+ if (result.refreshToken) {
226
+ setCookie(c, COOKIE_REFRESH_TOKEN, result.refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
227
+ }
228
+ if (getConfig().csrfEnabled)
229
+ refreshCsrfToken(c);
230
+ return c.json(result, 201);
231
+ });
232
+ }
233
+ // verify-and-login — only mounted when concealRegistration is configured
234
+ // Consumes an email verification token and creates a session (login) in one step.
235
+ if (concealRegistration) {
236
+ router.openapi(createRoute({
237
+ method: 'post',
238
+ path: '/auth/verify-and-login',
239
+ summary: 'Verify email and create session',
240
+ description: 'Consumes a single-use email verification token, marks the account as verified, and issues a session token. Only available when concealRegistration is configured. Rate-limited by IP.',
241
+ tags,
242
+ request: {
243
+ body: {
244
+ content: {
245
+ 'application/json': {
246
+ schema: z.object({
247
+ token: z.string().describe('Single-use verification token received via email.'),
248
+ }),
249
+ },
250
+ },
251
+ description: 'Verification token.',
252
+ },
253
+ },
254
+ responses: {
255
+ 200: {
256
+ content: { 'application/json': { schema: TokenResponse } },
257
+ description: 'Email verified and session created. Returns a session token.',
258
+ },
259
+ 400: {
260
+ content: { 'application/json': { schema: ErrorResponse } },
261
+ description: 'Invalid or expired verification token.',
262
+ },
263
+ 429: {
264
+ content: { 'application/json': { schema: ErrorResponse } },
265
+ description: 'Too many verification attempts from this IP. Try again later.',
266
+ },
267
+ },
268
+ }), async (c) => {
269
+ const ip = getClientIp(c);
270
+ if (await runtime.rateLimit.trackAttempt(`verify:${ip}`, verifyOpts)) {
271
+ return c.json({ error: 'Too many verification attempts. Try again later.' }, 429);
272
+ }
273
+ const { token } = c.req.valid('json');
274
+ const entry = await consumeVerificationToken(runtime.repos.verificationToken, token);
275
+ if (!entry)
276
+ return c.json({ error: 'Invalid or expired verification token' }, 400);
277
+ if (adapter.setEmailVerified)
278
+ await adapter.setEmailVerified(entry.userId, true);
279
+ eventBus.emit('auth:email.verified', { userId: entry.userId, email: entry.email });
280
+ const metadata = {
281
+ ipAddress: ip !== 'unknown' ? ip : undefined,
282
+ userAgent: c.req.header('user-agent') ?? undefined,
283
+ };
284
+ const { createSessionForUser } = await import('../services/auth');
285
+ const { token: sessionToken, refreshToken, sessionId, } = await createSessionForUser(entry.userId, runtime, metadata, hookCtx(c));
286
+ setCookie(c, COOKIE_TOKEN, sessionToken, cookieOptions(refreshTokens ? (getConfig().refreshToken?.accessTokenExpiry ?? 900) : undefined));
287
+ if (refreshToken) {
288
+ setCookie(c, COOKIE_REFRESH_TOKEN, refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
289
+ }
290
+ if (getConfig().csrfEnabled)
291
+ refreshCsrfToken(c);
292
+ emitLoginSuccess(entry.userId, sessionId, runtime);
293
+ const result = {
294
+ token: sessionToken,
295
+ userId: entry.userId,
296
+ email: entry.email,
297
+ emailVerified: true,
298
+ refreshToken,
299
+ };
300
+ return c.json(result, 200);
301
+ });
302
+ }
303
+ router.openapi(createRoute({
304
+ method: 'post',
305
+ path: '/auth/login',
306
+ summary: 'Log in',
307
+ description: 'Authenticates with credentials and returns a JWT session token. The token is also set as an HttpOnly session cookie. Failed attempts are rate-limited per identifier.',
308
+ tags,
309
+ request: {
310
+ body: {
311
+ content: { 'application/json': { schema: LoginSchema } },
312
+ description: 'Login credentials.',
313
+ },
314
+ },
315
+ responses: {
316
+ 200: {
317
+ content: { 'application/json': { schema: TokenResponse } },
318
+ description: 'Authenticated. Returns a session token.',
319
+ },
320
+ 401: {
321
+ content: { 'application/json': { schema: ErrorResponse } },
322
+ description: 'Invalid credentials.',
323
+ },
324
+ 403: {
325
+ content: { 'application/json': { schema: ErrorResponse } },
326
+ description: 'Email not verified. Verification is required before login.',
327
+ },
328
+ 423: {
329
+ content: { 'application/json': { schema: ErrorResponse } },
330
+ description: 'Account is locked due to too many failed login attempts. Wait for the lockout to expire or contact support.',
331
+ },
332
+ 429: {
333
+ content: { 'application/json': { schema: ErrorResponse } },
334
+ description: 'Too many failed login attempts for this identifier. Try again later.',
335
+ },
336
+ },
337
+ }), async (c) => {
338
+ const body = c.req.valid('json');
339
+ const identifier = body[primaryField];
340
+ const limitKey = `login:${identifier}`;
341
+ const clientIp = getClientIp(c) ?? 'unknown';
342
+ if (await runtime.credentialStuffing?.isStuffingBlocked(clientIp, identifier)) {
343
+ eventBus.emit('security.credential_stuffing.detected', {
344
+ ip: clientIp,
345
+ meta: { identifier },
346
+ });
347
+ throw new HttpError(429, 'Too many login attempts from this source', 'CREDENTIAL_STUFFING_BLOCKED');
348
+ }
349
+ if (await runtime.rateLimit.isLimited(limitKey, loginOpts)) {
350
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
351
+ return c.json({ error: 'Too many failed login attempts. Try again later.' }, 429);
352
+ }
353
+ const metadata = {
354
+ ipAddress: clientIp !== 'unknown' ? clientIp : undefined,
355
+ userAgent: c.req.header('user-agent') ?? undefined,
356
+ };
357
+ try {
358
+ const result = await AuthService.login(identifier, body.password, runtime, metadata, hookCtx(c));
359
+ await runtime.rateLimit.bustAuthLimit(limitKey); // success — clear failure count
360
+ if (!result.mfaRequired) {
361
+ setCookie(c, COOKIE_TOKEN, result.token, cookieOptions(refreshTokens ? (getConfig().refreshToken?.accessTokenExpiry ?? 900) : undefined));
362
+ if (result.refreshToken) {
363
+ setCookie(c, COOKIE_REFRESH_TOKEN, result.refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
364
+ }
365
+ if (getConfig().csrfEnabled)
366
+ refreshCsrfToken(c);
367
+ }
368
+ return c.json(result, 200);
369
+ }
370
+ catch (err) {
371
+ if (err instanceof HttpError && err.status === 401) {
372
+ const nowBlocked = await runtime.credentialStuffing?.trackFailedLogin(clientIp, identifier);
373
+ if (nowBlocked) {
374
+ eventBus.emit('security.credential_stuffing.detected', {
375
+ ip: clientIp,
376
+ meta: { identifier },
377
+ });
378
+ }
379
+ }
380
+ await runtime.rateLimit.trackAttempt(limitKey, loginOpts); // failure — count it
381
+ throw err;
382
+ }
383
+ });
384
+ router.use('/auth/me', userAuth);
385
+ router.openapi(withSecurity(createRoute({
386
+ method: 'get',
387
+ path: '/auth/me',
388
+ summary: 'Get current user',
389
+ description: "Returns the authenticated user's profile. Requires a valid session via cookie or x-user-token header.",
390
+ tags,
391
+ responses: {
392
+ 200: {
393
+ content: {
394
+ 'application/json': {
395
+ schema: z.object({
396
+ userId: z.string().describe('Unique user ID.'),
397
+ email: z.string().optional().describe("User's email address."),
398
+ emailVerified: z
399
+ .boolean()
400
+ .optional()
401
+ .describe('Whether the email address has been verified.'),
402
+ googleLinked: z
403
+ .boolean()
404
+ .optional()
405
+ .describe('Whether a Google OAuth account is linked.'),
406
+ userMetadata: z
407
+ .record(z.string(), z.unknown())
408
+ .optional()
409
+ .describe('User-editable metadata blob.'),
410
+ }),
411
+ },
412
+ },
413
+ description: "Authenticated user's profile.",
414
+ },
415
+ 401: {
416
+ content: { 'application/json': { schema: ErrorResponse } },
417
+ description: 'No valid session.',
418
+ },
419
+ },
420
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
421
+ const authUserId = c.get('authUserId');
422
+ const user = adapter.getUser ? await adapter.getUser(authUserId) : null;
423
+ const googleLinked = user?.providerIds?.some(id => id.startsWith('google:')) ?? false;
424
+ return c.json({
425
+ userId: authUserId,
426
+ email: user?.email,
427
+ emailVerified: user?.emailVerified,
428
+ googleLinked,
429
+ userMetadata: user?.userMetadata ?? {},
430
+ }, 200);
431
+ });
432
+ router.openapi(withSecurity(createRoute({
433
+ method: 'patch',
434
+ path: '/auth/me',
435
+ summary: 'Update current user profile',
436
+ description: "Updates the authenticated user's profile fields and/or user-editable metadata. Requires a valid session.",
437
+ tags,
438
+ request: {
439
+ body: {
440
+ content: {
441
+ 'application/json': {
442
+ schema: z.object({
443
+ displayName: z.string().optional().describe("User's display name."),
444
+ firstName: z.string().optional().describe("User's first name."),
445
+ lastName: z.string().optional().describe("User's last name."),
446
+ userMetadata: z
447
+ .record(z.string(), z.unknown())
448
+ .optional()
449
+ .describe('User-editable metadata blob (replaces existing).'),
450
+ }),
451
+ },
452
+ },
453
+ description: 'Fields to update.',
454
+ },
455
+ },
456
+ responses: {
457
+ 200: {
458
+ content: { 'application/json': { schema: SuccessResponse } },
459
+ description: 'Profile updated.',
460
+ },
461
+ 401: {
462
+ content: { 'application/json': { schema: ErrorResponse } },
463
+ description: 'No valid session.',
464
+ },
465
+ 501: {
466
+ content: { 'application/json': { schema: ErrorResponse } },
467
+ description: 'Auth adapter does not support profile updates.',
468
+ },
469
+ },
470
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
471
+ const authUserId = c.get('authUserId');
472
+ const body = c.req.valid('json');
473
+ if (body.userMetadata !== undefined && adapter.setUserMetadata) {
474
+ await adapter.setUserMetadata(authUserId, body.userMetadata);
475
+ }
476
+ const profileFields = {};
477
+ if ('displayName' in body)
478
+ profileFields.displayName = body.displayName;
479
+ if ('firstName' in body)
480
+ profileFields.firstName = body.firstName;
481
+ if ('lastName' in body)
482
+ profileFields.lastName = body.lastName;
483
+ if (Object.keys(profileFields).length > 0) {
484
+ if (!adapter.updateProfile) {
485
+ return c.json({ error: 'Auth adapter does not support profile updates' }, 501);
486
+ }
487
+ await adapter.updateProfile(authUserId, profileFields);
488
+ }
489
+ return c.json({ ok: true }, 200);
490
+ });
491
+ // ---------------------------------------------------------------------------
492
+ // Account deletion
493
+ // ---------------------------------------------------------------------------
494
+ const deleteAccountOpts = {
495
+ windowMs: rateLimit?.deleteAccount?.windowMs ?? 60 * 60 * 1000,
496
+ max: rateLimit?.deleteAccount?.max ?? 3,
497
+ };
498
+ const setPasswordOpts = {
499
+ windowMs: rateLimit?.setPassword?.windowMs ?? 15 * 60 * 1000,
500
+ max: rateLimit?.setPassword?.max ?? 5,
501
+ };
502
+ // Expanded verification schema for account deletion, step-up, and MFA disable
503
+ const verificationSchema = z.object({
504
+ method: z
505
+ .enum(['totp', 'emailOtp', 'webauthn', 'password', 'recovery'])
506
+ .optional()
507
+ .describe('Verification method to use.'),
508
+ code: z.string().optional().describe('TOTP code, email OTP code, or recovery code.'),
509
+ password: z.string().optional().describe('Account password.'),
510
+ reauthToken: z
511
+ .string()
512
+ .optional()
513
+ .describe('Reauth challenge token (required for emailOtp and webauthn methods).'),
514
+ webauthnResponse: z
515
+ .record(z.string(), z.unknown())
516
+ .optional()
517
+ .describe('WebAuthn assertion response (required for webauthn method).'),
518
+ });
519
+ router.openapi(withSecurity(createRoute({
520
+ method: 'delete',
521
+ path: '/auth/me',
522
+ summary: 'Delete account',
523
+ description: "Permanently deletes the authenticated user's account. Requires factor verification for accounts that have a password or MFA. OAuth-only accounts may be deleted freely unless requireVerification is set. Revokes all active sessions.",
524
+ tags,
525
+ request: {
526
+ body: {
527
+ content: {
528
+ 'application/json': {
529
+ schema: verificationSchema,
530
+ },
531
+ },
532
+ description: 'Factor verification.',
533
+ },
534
+ },
535
+ responses: {
536
+ 200: {
537
+ content: { 'application/json': { schema: SuccessResponse } },
538
+ description: 'Account deleted.',
539
+ },
540
+ 202: {
541
+ content: { 'application/json': { schema: SuccessResponse } },
542
+ description: 'Account deletion has been scheduled.',
543
+ },
544
+ 400: {
545
+ content: { 'application/json': { schema: ErrorResponse } },
546
+ description: 'Verification is required but not provided.',
547
+ },
548
+ 401: {
549
+ content: { 'application/json': { schema: ErrorResponse } },
550
+ description: 'Invalid verification or no valid session.',
551
+ },
552
+ 429: {
553
+ content: { 'application/json': { schema: ErrorResponse } },
554
+ description: 'Too many deletion attempts. Try again later.',
555
+ },
556
+ 501: {
557
+ content: { 'application/json': { schema: ErrorResponse } },
558
+ description: 'The configured auth adapter does not support deleteUser.',
559
+ },
560
+ },
561
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
562
+ const authUserId = c.get('authUserId');
563
+ const sessionId = c.get('sessionId');
564
+ if (await runtime.rateLimit.trackAttempt(`deleteaccount:${authUserId}`, deleteAccountOpts)) {
565
+ return c.json({ error: 'Too many deletion attempts. Try again later.' }, 429);
566
+ }
567
+ if (!adapter.deleteUser) {
568
+ return c.json({ error: 'Auth adapter does not support deleteUser' }, 501);
569
+ }
570
+ const body = c.req.valid('json');
571
+ const hasPassword = adapter.hasPassword ? await adapter.hasPassword(authUserId) : false;
572
+ const mfaMethods = getConfig().mfa && adapter.getMfaMethods ? await adapter.getMfaMethods(authUserId) : [];
573
+ const hasVerifiableFactor = hasPassword || mfaMethods.length > 0;
574
+ if (hasVerifiableFactor) {
575
+ // Infer method from provided credentials when not specified
576
+ const method = body.method ?? (body.password ? 'password' : body.code ? 'totp' : undefined);
577
+ if (!method) {
578
+ return c.json({
579
+ error: 'Verification is required to delete this account. Provide method and credentials.',
580
+ }, 400);
581
+ }
582
+ const { verifyAnyFactor } = await import('../services/mfa');
583
+ const valid = await verifyAnyFactor(authUserId, sessionId, runtime, {
584
+ method,
585
+ code: body.code,
586
+ password: body.password,
587
+ reauthToken: body.reauthToken,
588
+ webauthnResponse: body.webauthnResponse,
589
+ });
590
+ if (!valid) {
591
+ return c.json({ error: 'Invalid verification' }, 401);
592
+ }
593
+ }
594
+ else {
595
+ // OAuth-only account — no verifiable factor
596
+ if (accountDeletion?.requireVerification) {
597
+ return c.json({
598
+ error: 'Account deletion requires a verifiable factor. Please set a password or enable MFA first.',
599
+ }, 400);
600
+ }
601
+ // else: allow deletion without verification
602
+ }
603
+ // Call onBeforeDelete hook
604
+ if (accountDeletion?.onBeforeDelete) {
605
+ await accountDeletion.onBeforeDelete(authUserId);
606
+ }
607
+ // Queued deletion via BullMQ
608
+ if (accountDeletion?.queued) {
609
+ if (!runtime.queueFactory)
610
+ throw new Error('[bunshot-auth] accountDeletion.queued requires Redis and BullMQ');
611
+ const appName = runtime.config.appName;
612
+ const queue = runtime.queueFactory.createQueue(`${appName}:account-deletions`);
613
+ const delayMs = (accountDeletion.gracePeriod ?? 0) * 1000;
614
+ const job = await queue.add('delete-account', { userId: authUserId }, {
615
+ delay: delayMs,
616
+ attempts: 3,
617
+ backoff: { type: 'exponential', delay: 1000 },
618
+ removeOnComplete: true,
619
+ removeOnFail: 100,
620
+ });
621
+ await queue.close();
622
+ // Revoke sessions immediately so the user is logged out
623
+ {
624
+ const sr = runtime.repos.session;
625
+ const ss = await sr.getUserSessions(authUserId, runtime.config);
626
+ await Promise.all(ss.map(s => sr.deleteSession(s.sessionId, runtime.config)));
627
+ }
628
+ if (accountDeletion.gracePeriod) {
629
+ const cancelToken = await createDeletionCancelToken(runtime.repos.deletionCancelToken, authUserId, job.id, accountDeletion.gracePeriod);
630
+ eventBus.emit('auth:account.deletion.scheduled', {
631
+ userId: authUserId,
632
+ cancelToken,
633
+ gracePeriodSeconds: accountDeletion.gracePeriod ?? 0,
634
+ });
635
+ const user = adapter.getUser ? await adapter.getUser(authUserId) : null;
636
+ const email = user?.email ?? '';
637
+ if (email) {
638
+ eventBus.emit('auth:delivery.account_deletion', {
639
+ userId: authUserId,
640
+ email,
641
+ cancelToken,
642
+ gracePeriodSeconds: accountDeletion.gracePeriod ?? 0,
643
+ });
644
+ }
645
+ }
646
+ else {
647
+ // No grace period — deletion is immediate via the queue (delay=0).
648
+ // Emit the same events as the synchronous path so listeners are notified.
649
+ eventBus.emit('security.auth.account.deleted', { userId: authUserId });
650
+ eventBus.emit('auth:user.deleted', { userId: authUserId });
651
+ }
652
+ deleteCookie(c, COOKIE_TOKEN, { path: '/' });
653
+ return c.json({ ok: true }, 202);
654
+ }
655
+ // Synchronous deletion (default)
656
+ const hooks = getConfig().hooks;
657
+ if (hooks.preDeleteAccount)
658
+ await hooks.preDeleteAccount({ userId: authUserId, ...hookCtx(c) });
659
+ {
660
+ const sr = runtime.repos.session;
661
+ const ss = await sr.getUserSessions(authUserId, runtime.config);
662
+ await Promise.all(ss.map(s => sr.deleteSession(s.sessionId, runtime.config)));
663
+ }
664
+ await adapter.deleteUser(authUserId);
665
+ eventBus.emit('security.auth.account.deleted', { userId: authUserId });
666
+ eventBus.emit('auth:user.deleted', { userId: authUserId });
667
+ if (accountDeletion?.onAfterDelete) {
668
+ await accountDeletion.onAfterDelete(authUserId);
669
+ }
670
+ if (hooks.postDeleteAccount) {
671
+ Promise.resolve()
672
+ .then(() => hooks.postDeleteAccount({ userId: authUserId, ...hookCtx(c) }))
673
+ .catch(e => console.error('[lifecycle] postDeleteAccount hook error:', e instanceof Error ? e.message : String(e)));
674
+ }
675
+ deleteCookie(c, COOKIE_TOKEN, { path: '/' });
676
+ return c.json({ ok: true }, 200);
677
+ });
678
+ router.use('/auth/set-password', userAuth);
679
+ router.openapi(withSecurity(createRoute({
680
+ method: 'post',
681
+ path: '/auth/set-password',
682
+ summary: 'Set or update password',
683
+ description: 'Sets or updates the password for the authenticated user. Useful for OAuth-only users who want to add a password. If the account already has a password set, `currentPassword` is required. Requires a valid session.',
684
+ tags,
685
+ request: {
686
+ body: {
687
+ content: {
688
+ 'application/json': {
689
+ schema: z.object({
690
+ password: z.string().min(8).describe('New password.'),
691
+ currentPassword: z
692
+ .string()
693
+ .optional()
694
+ .describe('Current password. Required if the account already has a password set.'),
695
+ }),
696
+ },
697
+ },
698
+ description: 'New password.',
699
+ },
700
+ },
701
+ responses: {
702
+ 200: {
703
+ content: { 'application/json': { schema: PasswordUpdateResponse } },
704
+ description: 'Password updated successfully.',
705
+ },
706
+ 400: {
707
+ content: { 'application/json': { schema: ErrorResponse } },
708
+ description: 'Validation error, or current password is required when one is already set.',
709
+ },
710
+ 401: {
711
+ content: { 'application/json': { schema: ErrorResponse } },
712
+ description: 'No valid session, or current password is incorrect.',
713
+ },
714
+ 429: {
715
+ content: { 'application/json': { schema: ErrorResponse } },
716
+ description: 'Too many password change attempts. Try again later.',
717
+ },
718
+ 501: {
719
+ content: { 'application/json': { schema: ErrorResponse } },
720
+ description: 'The configured auth adapter does not support setPassword.',
721
+ },
722
+ },
723
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
724
+ if (!adapter.setPassword) {
725
+ return c.json({ error: 'Auth adapter does not support setPassword' }, 501);
726
+ }
727
+ const { password, currentPassword } = c.req.valid('json');
728
+ const authUserId = c.get('authUserId');
729
+ const currentSessionId = c.get('sessionId');
730
+ if (await runtime.rateLimit.trackAttempt(`setpassword:${authUserId}`, setPasswordOpts)) {
731
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
732
+ return c.json({ error: 'Too many password change attempts. Try again later.' }, 429);
733
+ }
734
+ // If the user already has a password, require currentPassword to change it
735
+ const hasExistingPassword = adapter.hasPassword
736
+ ? await adapter.hasPassword(authUserId)
737
+ : false;
738
+ if (hasExistingPassword) {
739
+ if (!currentPassword) {
740
+ return c.json({ error: 'Current password is required to change an existing password.' }, 400);
741
+ }
742
+ if (!(await adapter.verifyPassword(authUserId, currentPassword))) {
743
+ return c.json({ error: 'Current password is incorrect.' }, 401);
744
+ }
745
+ }
746
+ // Breached password check
747
+ const breachConfig = getConfig().breachedPassword;
748
+ if (breachConfig) {
749
+ const { breached } = await checkBreachedPassword(password, breachConfig, undefined, runtime.eventBus);
750
+ if (breached && breachConfig.block !== false) {
751
+ throw new HttpError(400, 'This password has appeared in a data breach. Please choose a different password.', 'BREACHED_PASSWORD');
752
+ }
753
+ }
754
+ const hooks = getConfig().hooks;
755
+ if (hooks.prePasswordChange)
756
+ await hooks.prePasswordChange({ userId: authUserId });
757
+ const passwordHash = await Bun.password.hash(password);
758
+ // Password reuse check
759
+ const preventReuse = getConfig().passwordPolicy.preventReuse ?? 0;
760
+ if (preventReuse > 0) {
761
+ const isNew = await checkPasswordNotReused(adapter, authUserId, password, preventReuse);
762
+ if (!isNew) {
763
+ return c.json({ error: 'You cannot reuse a recent password.', code: 'PASSWORD_PREVIOUSLY_USED' }, 400);
764
+ }
765
+ }
766
+ await adapter.setPassword(authUserId, passwordHash);
767
+ if (preventReuse > 0)
768
+ await recordPasswordChange(adapter, authUserId, passwordHash, preventReuse);
769
+ eventBus.emit('security.auth.password.change', { userId: authUserId });
770
+ if (hooks.postPasswordChange) {
771
+ Promise.resolve()
772
+ .then(() => hooks.postPasswordChange({ userId: authUserId }))
773
+ .catch(e => console.error('[lifecycle] postPasswordChange hook error:', e instanceof Error ? e.message : String(e)));
774
+ }
775
+ // Session revocation policy on password change
776
+ const pwChangePolicy = sessionPolicy?.onPasswordChange ?? 'revoke_others';
777
+ if (pwChangePolicy === 'revoke_all_and_reissue') {
778
+ // Revoke all sessions including current, create a new session
779
+ {
780
+ const sr = runtime.repos.session;
781
+ const ss = await sr.getUserSessions(authUserId, runtime.config);
782
+ await Promise.all(ss.map(s => sr.deleteSession(s.sessionId, runtime.config)));
783
+ }
784
+ const { createSessionForUser } = await import('../services/auth');
785
+ const metadata = {
786
+ ipAddress: getClientIp(c) !== 'unknown' ? getClientIp(c) : undefined,
787
+ userAgent: c.req.header('user-agent') ?? undefined,
788
+ };
789
+ const newSession = await createSessionForUser(authUserId, runtime, metadata, hookCtx(c));
790
+ setCookie(c, COOKIE_TOKEN, newSession.token, cookieOptions(refreshTokens ? (getConfig().refreshToken?.accessTokenExpiry ?? 900) : undefined));
791
+ if (newSession.refreshToken) {
792
+ setCookie(c, COOKIE_REFRESH_TOKEN, newSession.refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
793
+ }
794
+ return c.json({ ok: true, token: newSession.token }, 200);
795
+ }
796
+ else if (pwChangePolicy === 'revoke_others') {
797
+ {
798
+ const sr = runtime.repos.session;
799
+ const ss = await sr.getUserSessions(authUserId, runtime.config);
800
+ const others = ss.filter(s => s.sessionId !== currentSessionId);
801
+ await Promise.all(others.map(s => sr.deleteSession(s.sessionId, runtime.config)));
802
+ }
803
+ }
804
+ // "none" or unrecognized: no session action
805
+ return c.json({ ok: true }, 200);
806
+ });
807
+ router.openapi(createRoute({
808
+ method: 'post',
809
+ path: '/auth/logout',
810
+ summary: 'Log out',
811
+ description: 'Invalidates the current session and clears the session cookie. Safe to call even without an active session.',
812
+ tags,
813
+ responses: {
814
+ 200: {
815
+ content: { 'application/json': { schema: SuccessResponse } },
816
+ description: 'Logged out. Session is invalidated and cookie is cleared.',
817
+ },
818
+ },
819
+ }), async (c) => {
820
+ const token = getCookie(c, COOKIE_TOKEN) ?? c.req.header(HEADER_USER_TOKEN) ?? null;
821
+ await AuthService.logout(token, runtime);
822
+ deleteCookie(c, COOKIE_TOKEN, { path: '/' });
823
+ deleteCookie(c, COOKIE_REFRESH_TOKEN, { path: '/' });
824
+ if (getConfig().csrfEnabled)
825
+ clearCsrfToken(c);
826
+ return c.json({ ok: true }, 200);
827
+ });
828
+ // Email verification routes — only mounted when emailVerification is configured and primaryField is "email"
829
+ if (emailVerification && primaryField === 'email') {
830
+ router.openapi(createRoute({
831
+ method: 'post',
832
+ path: '/auth/verify-email',
833
+ summary: 'Verify email address',
834
+ description: 'Consumes a single-use email verification token and marks the account as verified. The token is delivered via the auth:delivery.email_verification bus event. Rate-limited by IP.',
835
+ tags,
836
+ request: {
837
+ body: {
838
+ content: {
839
+ 'application/json': {
840
+ schema: z.object({
841
+ token: z.string().describe('Single-use verification token received via email.'),
842
+ }),
843
+ },
844
+ },
845
+ description: 'Verification token.',
846
+ },
847
+ },
848
+ responses: {
849
+ 200: {
850
+ content: { 'application/json': { schema: SuccessResponse } },
851
+ description: 'Email verified successfully.',
852
+ },
853
+ 400: {
854
+ content: { 'application/json': { schema: ErrorResponse } },
855
+ description: 'Invalid or expired verification token.',
856
+ },
857
+ 429: {
858
+ content: { 'application/json': { schema: ErrorResponse } },
859
+ description: 'Too many verification attempts from this IP. Try again later.',
860
+ },
861
+ },
862
+ }), async (c) => {
863
+ const ip = getClientIp(c);
864
+ if (await runtime.rateLimit.trackAttempt(`verify:${ip}`, verifyOpts)) {
865
+ return c.json({ error: 'Too many verification attempts. Try again later.' }, 429);
866
+ }
867
+ const { token } = c.req.valid('json');
868
+ const entry = await consumeVerificationToken(runtime.repos.verificationToken, token);
869
+ if (!entry)
870
+ return c.json({ error: 'Invalid or expired verification token' }, 400);
871
+ if (adapter.setEmailVerified)
872
+ await adapter.setEmailVerified(entry.userId, true);
873
+ eventBus.emit('auth:email.verified', { userId: entry.userId, email: entry.email });
874
+ if (getConfig().csrfEnabled)
875
+ refreshCsrfToken(c);
876
+ return c.json({ ok: true }, 200);
877
+ });
878
+ router.openapi(createRoute({
879
+ method: 'post',
880
+ path: '/auth/resend-verification',
881
+ summary: 'Resend verification email',
882
+ description: 'Authenticates with credentials and sends a new verification email. Always returns 200 for valid credentials regardless of verification status, to prevent user enumeration. Rate-limited per identifier. Does not require a session.',
883
+ tags,
884
+ request: {
885
+ body: {
886
+ content: { 'application/json': { schema: LoginSchema } },
887
+ description: 'Login credentials to identify the account.',
888
+ },
889
+ },
890
+ responses: {
891
+ 200: {
892
+ content: { 'application/json': { schema: z.object({ message: z.string() }) } },
893
+ description: 'Verification email sent, or account is already verified (indistinguishable by design).',
894
+ },
895
+ 400: {
896
+ content: { 'application/json': { schema: ErrorResponse } },
897
+ description: 'No email address on file for this account.',
898
+ },
899
+ 401: {
900
+ content: { 'application/json': { schema: ErrorResponse } },
901
+ description: 'Invalid credentials.',
902
+ },
903
+ 429: {
904
+ content: { 'application/json': { schema: ErrorResponse } },
905
+ description: 'Too many resend attempts for this identifier. Try again later.',
906
+ },
907
+ 501: {
908
+ content: { 'application/json': { schema: ErrorResponse } },
909
+ description: 'The configured auth adapter does not support email verification.',
910
+ },
911
+ },
912
+ }), async (c) => {
913
+ if (!adapter.getEmailVerified || !adapter.getUser) {
914
+ return c.json({ error: 'Auth adapter does not support email verification' }, 501);
915
+ }
916
+ const body = c.req.valid('json');
917
+ const identifier = body[primaryField];
918
+ if (await runtime.rateLimit.trackAttempt(`resend:${identifier}`, resendOpts)) {
919
+ return c.json({ error: 'Too many resend attempts. Try again later.' }, 429);
920
+ }
921
+ const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
922
+ const user = await findFn(identifier);
923
+ // Always call Bun.password.verify to prevent timing-based user enumeration.
924
+ // When user is not found or has no passwordHash (OAuth-only account), verify
925
+ // against a dummy hash so response time is constant regardless of user existence.
926
+ const RESEND_DUMMY_HASH = '$argon2id$v=19$m=65536,t=2,p=1$dummysaltfortimingleak00000000000$dummyhashoutputfortimingprotection0000000';
927
+ const hashToVerify = user?.passwordHash ?? RESEND_DUMMY_HASH;
928
+ const passwordValid = await Bun.password.verify(body.password, hashToVerify);
929
+ if (!user || !passwordValid) {
930
+ return c.json({ error: 'Invalid credentials' }, 401);
931
+ }
932
+ const alreadyVerified = await adapter.getEmailVerified(user.id);
933
+ // Return 200 (not 400) to avoid revealing whether the account is verified —
934
+ // distinguishing verified vs unverified would let attackers confirm valid credentials.
935
+ if (alreadyVerified)
936
+ return c.json({ message: 'Verification email sent if not already verified' }, 200);
937
+ const fullUser = await adapter.getUser(user.id);
938
+ if (!fullUser?.email)
939
+ return c.json({ error: 'No email address on file' }, 400);
940
+ const verificationToken = await createVerificationToken(runtime.repos.verificationToken, user.id, fullUser.email, runtime.config);
941
+ eventBus.emit('auth:delivery.email_verification', {
942
+ email: fullUser.email,
943
+ token: verificationToken,
944
+ userId: user.id,
945
+ });
946
+ return c.json({ message: 'Verification email sent' }, 200);
947
+ });
948
+ }
949
+ // Password reset routes — only mounted when passwordReset is configured and primaryField is "email"
950
+ if (passwordReset && primaryField === 'email') {
951
+ router.openapi(createRoute({
952
+ method: 'post',
953
+ path: '/auth/forgot-password',
954
+ summary: 'Request password reset',
955
+ description: 'Sends a password reset email if the address is registered. Always returns 200 regardless of whether the address exists, to prevent email enumeration. Rate-limited by both IP and email address.',
956
+ tags,
957
+ request: {
958
+ body: {
959
+ content: {
960
+ 'application/json': {
961
+ schema: z.object({
962
+ email: z.string().email().describe('Email address to send the reset link to.'),
963
+ }),
964
+ },
965
+ },
966
+ description: 'Email address for the account to reset.',
967
+ },
968
+ },
969
+ responses: {
970
+ 200: {
971
+ content: { 'application/json': { schema: z.object({ message: z.string() }) } },
972
+ description: 'Request received. A reset email will be sent if the address is registered.',
973
+ },
974
+ 400: {
975
+ content: { 'application/json': { schema: ErrorResponse } },
976
+ description: 'Validation error (e.g. not a valid email address).',
977
+ },
978
+ 429: {
979
+ content: { 'application/json': { schema: ErrorResponse } },
980
+ description: 'Too many attempts from this IP or for this email address. Try again later.',
981
+ },
982
+ },
983
+ }), async (c) => {
984
+ const ip = getClientIp(c);
985
+ const { email } = c.req.valid('json');
986
+ // Rate-limit by both IP and email to prevent distributed email-bombing
987
+ const ipLimited = await runtime.rateLimit.trackAttempt(`forgot:ip:${ip}`, forgotOpts);
988
+ const emailLimited = await runtime.rateLimit.trackAttempt(`forgot:email:${email}`, forgotOpts);
989
+ if (ipLimited || emailLimited) {
990
+ return c.json({ error: 'Too many attempts. Try again later.' }, 429);
991
+ }
992
+ const user = adapter.findByEmail ? await adapter.findByEmail(email) : null;
993
+ // Fire-and-forget: the response does not wait for token creation or email sending,
994
+ // which reduces obvious timing differences between registered and unregistered emails.
995
+ const msg = {
996
+ message: 'If that email is registered, a password reset link has been sent.',
997
+ };
998
+ if (user) {
999
+ void (async () => {
1000
+ try {
1001
+ const token = await createResetToken(runtime.repos.resetToken, user.id, email, runtime.config);
1002
+ eventBus.emit('auth:delivery.password_reset', { email, token });
1003
+ eventBus.emit('auth:password.reset.requested', { userId: user.id, email });
1004
+ }
1005
+ catch (err) {
1006
+ console.error('Failed to send password reset email:', err instanceof Error ? err.message : String(err));
1007
+ }
1008
+ })();
1009
+ }
1010
+ return c.json(msg, 200);
1011
+ });
1012
+ router.openapi(createRoute({
1013
+ method: 'post',
1014
+ path: '/auth/reset-password',
1015
+ summary: 'Reset password',
1016
+ description: 'Consumes a single-use reset token and sets a new password. All active sessions are revoked after a successful reset to invalidate any stolen JWTs. Rate-limited by IP.',
1017
+ tags,
1018
+ request: {
1019
+ body: {
1020
+ content: {
1021
+ 'application/json': {
1022
+ schema: z.object({
1023
+ token: z.string().describe('Single-use reset token received via email.'),
1024
+ password: resetPasswordSchema(runtime.config.passwordPolicy).describe('New password.'),
1025
+ }),
1026
+ },
1027
+ },
1028
+ description: 'Reset token and new password.',
1029
+ },
1030
+ },
1031
+ responses: {
1032
+ 200: {
1033
+ content: { 'application/json': { schema: SuccessResponse } },
1034
+ description: 'Password reset. All sessions have been revoked.',
1035
+ },
1036
+ 400: {
1037
+ content: { 'application/json': { schema: ErrorResponse } },
1038
+ description: 'Validation error, or the reset token is invalid or expired.',
1039
+ },
1040
+ 429: {
1041
+ content: { 'application/json': { schema: ErrorResponse } },
1042
+ description: 'Too many reset attempts from this IP. Try again later.',
1043
+ },
1044
+ 501: {
1045
+ content: { 'application/json': { schema: ErrorResponse } },
1046
+ description: 'The configured auth adapter does not support setPassword.',
1047
+ },
1048
+ },
1049
+ }), async (c) => {
1050
+ const ip = getClientIp(c);
1051
+ if (await runtime.rateLimit.trackAttempt(`reset:${ip}`, resetOpts)) {
1052
+ return c.json({ error: 'Too many attempts. Try again later.' }, 429);
1053
+ }
1054
+ const { token, password } = c.req.valid('json');
1055
+ const breachConfig = getConfig().breachedPassword;
1056
+ if (breachConfig) {
1057
+ const { breached } = await checkBreachedPassword(password, breachConfig, undefined, runtime.eventBus);
1058
+ if (breached && breachConfig.block !== false) {
1059
+ throw new HttpError(400, 'This password has appeared in a data breach. Please choose a different password.', 'BREACHED_PASSWORD');
1060
+ }
1061
+ }
1062
+ // consumeResetToken atomically gets and deletes — prevents concurrent replay
1063
+ const entry = await consumeResetToken(runtime.repos.resetToken, token);
1064
+ if (!entry)
1065
+ return c.json({ error: 'Invalid or expired reset token' }, 400);
1066
+ if (!adapter.setPassword) {
1067
+ return c.json({ error: 'Auth adapter does not support setPassword' }, 501);
1068
+ }
1069
+ const passwordHash = await Bun.password.hash(password);
1070
+ // Password reuse check
1071
+ const preventReuseReset = getConfig().passwordPolicy.preventReuse ?? 0;
1072
+ if (preventReuseReset > 0) {
1073
+ const isNew = await checkPasswordNotReused(adapter, entry.userId, password, preventReuseReset);
1074
+ if (!isNew) {
1075
+ return c.json({ error: 'You cannot reuse a recent password.', code: 'PASSWORD_PREVIOUSLY_USED' }, 400);
1076
+ }
1077
+ }
1078
+ const hooks = getConfig().hooks;
1079
+ const ctx = hookCtx(c);
1080
+ if (hooks.prePasswordChange)
1081
+ await hooks.prePasswordChange({ userId: entry.userId, ...ctx });
1082
+ await adapter.setPassword(entry.userId, passwordHash);
1083
+ if (preventReuseReset > 0)
1084
+ await recordPasswordChange(adapter, entry.userId, passwordHash, preventReuseReset);
1085
+ // Revoke all sessions so stolen JWTs can't stay valid after a reset
1086
+ {
1087
+ const sr = runtime.repos.session;
1088
+ const ss = await sr.getUserSessions(entry.userId, runtime.config);
1089
+ await Promise.all(ss.map(s => sr.deleteSession(s.sessionId, runtime.config)));
1090
+ }
1091
+ eventBus.emit('security.auth.password.reset', {});
1092
+ if (hooks.postPasswordChange) {
1093
+ Promise.resolve()
1094
+ .then(() => hooks.postPasswordChange({ userId: entry.userId, ...ctx }))
1095
+ .catch(e => console.error('[lifecycle] postPasswordChange hook error:', e instanceof Error ? e.message : String(e)));
1096
+ }
1097
+ return c.json({ ok: true }, 200);
1098
+ });
1099
+ }
1100
+ // ---------------------------------------------------------------------------
1101
+ // Refresh token route — only mounted when refreshTokens is configured
1102
+ // ---------------------------------------------------------------------------
1103
+ if (refreshTokens) {
1104
+ const RefreshResponse = z
1105
+ .object({
1106
+ token: z.string().describe('New short-lived JWT access token.'),
1107
+ userId: z.string().describe('Unique user ID.'),
1108
+ })
1109
+ .openapi('RefreshResponse');
1110
+ router.openapi(createRoute({
1111
+ method: 'post',
1112
+ path: '/auth/refresh',
1113
+ summary: 'Refresh access token',
1114
+ description: 'Exchanges a valid refresh token for a new access token and rotated refresh token. The old refresh token remains valid for a short grace window to handle network drops. If a previously rotated token is reused after the grace window, the entire session is invalidated (token theft detection).',
1115
+ tags,
1116
+ request: {
1117
+ body: {
1118
+ content: {
1119
+ 'application/json': {
1120
+ schema: z.object({
1121
+ refreshToken: z
1122
+ .string()
1123
+ .optional()
1124
+ .describe('Refresh token. Can also be sent via the refresh_token cookie or x-refresh-token header.'),
1125
+ }),
1126
+ },
1127
+ },
1128
+ description: 'Refresh token (optional if sent via cookie or header).',
1129
+ },
1130
+ },
1131
+ responses: {
1132
+ 200: {
1133
+ content: { 'application/json': { schema: RefreshResponse } },
1134
+ description: 'New access and refresh tokens.',
1135
+ },
1136
+ 401: {
1137
+ content: { 'application/json': { schema: ErrorResponse } },
1138
+ description: 'Invalid or expired refresh token, or session invalidated due to token theft detection.',
1139
+ },
1140
+ 429: {
1141
+ content: { 'application/json': { schema: ErrorResponse } },
1142
+ description: 'Too many refresh attempts. Try again later.',
1143
+ },
1144
+ },
1145
+ }), async (c) => {
1146
+ const ip = getClientIp(c);
1147
+ if (await runtime.rateLimit.trackAttempt(`refresh:ip:${ip}`, { max: 30, windowMs: 60_000 })) {
1148
+ return c.json({ error: 'Too many refresh attempts. Try again later.' }, 429);
1149
+ }
1150
+ const body = c.req.valid('json');
1151
+ const rt = body.refreshToken ??
1152
+ getCookie(c, COOKIE_REFRESH_TOKEN) ??
1153
+ c.req.header(HEADER_REFRESH_TOKEN) ??
1154
+ null;
1155
+ if (!rt) {
1156
+ return c.json({ error: 'Refresh token is required' }, 401);
1157
+ }
1158
+ const result = await AuthService.refresh(rt, runtime);
1159
+ setCookie(c, COOKIE_TOKEN, result.token, cookieOptions(getConfig().refreshToken?.accessTokenExpiry ?? 900));
1160
+ setCookie(c, COOKIE_REFRESH_TOKEN, result.refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
1161
+ return c.json(result, 200);
1162
+ });
1163
+ }
1164
+ // ---------------------------------------------------------------------------
1165
+ // Account deletion cancellation — only mounted when queued: true + gracePeriod > 0
1166
+ // ---------------------------------------------------------------------------
1167
+ if (accountDeletion?.queued && accountDeletion.gracePeriod) {
1168
+ router.openapi(createRoute({
1169
+ method: 'post',
1170
+ path: '/auth/cancel-deletion',
1171
+ summary: 'Cancel scheduled account deletion',
1172
+ description: 'Cancels a pending queued account deletion using the cancel token delivered via the auth:delivery.account_deletion bus event. Must be called before the grace period expires.',
1173
+ tags,
1174
+ request: {
1175
+ body: {
1176
+ content: {
1177
+ 'application/json': {
1178
+ schema: z.object({
1179
+ token: z
1180
+ .string()
1181
+ .describe('Cancel token received in the deletion scheduled notification.'),
1182
+ }),
1183
+ },
1184
+ },
1185
+ description: 'Cancel token.',
1186
+ },
1187
+ },
1188
+ responses: {
1189
+ 200: {
1190
+ content: { 'application/json': { schema: SuccessResponse } },
1191
+ description: 'Account deletion cancelled.',
1192
+ },
1193
+ 400: {
1194
+ content: { 'application/json': { schema: ErrorResponse } },
1195
+ description: 'Invalid or expired cancel token.',
1196
+ },
1197
+ },
1198
+ }), async (c) => {
1199
+ const { token } = c.req.valid('json');
1200
+ const entry = await consumeDeletionCancelToken(runtime.repos.deletionCancelToken, token);
1201
+ if (!entry)
1202
+ return c.json({ error: 'Invalid or expired cancel token' }, 400);
1203
+ // Remove the pending BullMQ job
1204
+ try {
1205
+ const appName = runtime.config.appName;
1206
+ const queue = runtime.queueFactory.createQueue(`${appName}:account-deletions`);
1207
+ const job = await queue.getJob(entry.jobId);
1208
+ if (job)
1209
+ await job.remove();
1210
+ await queue.close();
1211
+ }
1212
+ catch {
1213
+ // Job may have already executed — that's an error case but we still consumed the token
1214
+ }
1215
+ return c.json({ ok: true }, 200);
1216
+ });
1217
+ }
1218
+ // ---------------------------------------------------------------------------
1219
+ // Step-up MFA re-authentication — only mounted when stepUp is configured
1220
+ // ---------------------------------------------------------------------------
1221
+ if (stepUp) {
1222
+ const stepUpOpts = {
1223
+ windowMs: rateLimit?.mfaVerify?.windowMs ?? 15 * 60 * 1000,
1224
+ max: rateLimit?.mfaVerify?.max ?? 10,
1225
+ };
1226
+ const stepUpSchema = z.object({
1227
+ method: z
1228
+ .enum(['totp', 'emailOtp', 'webauthn', 'password', 'recovery'])
1229
+ .describe('Verification method to use.'),
1230
+ code: z.string().optional().describe('TOTP code, email OTP code, or recovery code.'),
1231
+ password: z.string().optional().describe('Account password.'),
1232
+ reauthToken: z
1233
+ .string()
1234
+ .optional()
1235
+ .describe('Reauth challenge token (required for emailOtp and webauthn methods).'),
1236
+ webauthnResponse: z
1237
+ .record(z.string(), z.unknown())
1238
+ .optional()
1239
+ .describe('WebAuthn assertion response (required for webauthn method).'),
1240
+ });
1241
+ router.use('/auth/step-up', userAuth);
1242
+ router.openapi(withSecurity(createRoute({
1243
+ method: 'post',
1244
+ path: '/auth/step-up',
1245
+ summary: 'Step-up MFA re-authentication',
1246
+ description: 'Re-authenticates the current session via TOTP, email OTP, WebAuthn, recovery code, or password to satisfy step-up requirements for sensitive operations. On success, sets mfaVerifiedAt in the session.',
1247
+ tags,
1248
+ request: {
1249
+ body: {
1250
+ content: {
1251
+ 'application/json': {
1252
+ schema: stepUpSchema,
1253
+ },
1254
+ },
1255
+ description: 'Verification credentials for re-authentication.',
1256
+ },
1257
+ },
1258
+ responses: {
1259
+ 200: {
1260
+ content: { 'application/json': { schema: z.object({ ok: z.literal(true) }) } },
1261
+ description: 'Step-up authentication successful.',
1262
+ },
1263
+ 400: {
1264
+ content: { 'application/json': { schema: ErrorResponse } },
1265
+ description: 'No verification parameter provided.',
1266
+ },
1267
+ 401: {
1268
+ content: { 'application/json': { schema: ErrorResponse } },
1269
+ description: 'Invalid credentials or no valid session.',
1270
+ },
1271
+ 429: {
1272
+ content: { 'application/json': { schema: ErrorResponse } },
1273
+ description: 'Too many step-up attempts. Try again later.',
1274
+ },
1275
+ },
1276
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
1277
+ const ip = getClientIp(c);
1278
+ if (await runtime.rateLimit.trackAttempt(`step-up:${ip}`, stepUpOpts)) {
1279
+ return c.json({ error: 'Too many step-up attempts. Try again later.' }, 429);
1280
+ }
1281
+ const userId = c.get('authUserId');
1282
+ const sessionId = c.get('sessionId');
1283
+ const body = c.req.valid('json');
1284
+ const { verifyAnyFactor } = await import('../services/mfa');
1285
+ const valid = await verifyAnyFactor(userId, sessionId, runtime, {
1286
+ method: body.method,
1287
+ code: body.code,
1288
+ password: body.password,
1289
+ reauthToken: body.reauthToken,
1290
+ webauthnResponse: body.webauthnResponse,
1291
+ });
1292
+ if (!valid) {
1293
+ eventBus.emit('security.auth.step_up.failure', { userId });
1294
+ return c.json({ error: 'Invalid credentials' }, 401);
1295
+ }
1296
+ await runtime.repos.session.setMfaVerifiedAt(sessionId);
1297
+ eventBus.emit('security.auth.step_up.success', { userId });
1298
+ return c.json({ ok: true }, 200);
1299
+ });
1300
+ }
1301
+ // ---------------------------------------------------------------------------
1302
+ // Reauth challenge — always mounted; issues challenge tokens for emailOtp / webauthn factors
1303
+ // ---------------------------------------------------------------------------
1304
+ const reauthChallengeOpts = {
1305
+ windowMs: rateLimit?.mfaVerify?.windowMs ?? 15 * 60 * 1000,
1306
+ max: rateLimit?.mfaVerify?.max ?? 10,
1307
+ };
1308
+ router.use('/auth/reauth/challenge', userAuth);
1309
+ router.openapi(withSecurity(createRoute({
1310
+ method: 'post',
1311
+ path: '/auth/reauth/challenge',
1312
+ summary: 'Request a reauth challenge',
1313
+ description: 'Issues a reauth challenge token for challenge-based MFA methods (email OTP, WebAuthn). The returned reauthToken must be passed to step-up, account deletion, or MFA disable endpoints. Direct methods (TOTP, password, recovery) do not require a challenge.',
1314
+ tags,
1315
+ responses: {
1316
+ 200: {
1317
+ content: {
1318
+ 'application/json': {
1319
+ schema: z.object({
1320
+ availableMethods: z
1321
+ .array(z.string())
1322
+ .describe('All MFA methods available for this user (including direct methods like totp and password).'),
1323
+ reauthToken: z
1324
+ .string()
1325
+ .optional()
1326
+ .describe('Challenge token for emailOtp or webauthn methods. Required when using those methods.'),
1327
+ webauthnOptions: z
1328
+ .record(z.string(), z.unknown())
1329
+ .optional()
1330
+ .describe('WebAuthn authentication options (present when WebAuthn is available).'),
1331
+ }),
1332
+ },
1333
+ },
1334
+ description: 'Reauth challenge issued.',
1335
+ },
1336
+ 400: {
1337
+ content: { 'application/json': { schema: ErrorResponse } },
1338
+ description: 'No challenge-based MFA methods configured for this user.',
1339
+ },
1340
+ 401: {
1341
+ content: { 'application/json': { schema: ErrorResponse } },
1342
+ description: 'No valid session.',
1343
+ },
1344
+ 429: {
1345
+ content: { 'application/json': { schema: ErrorResponse } },
1346
+ description: 'Too many reauth attempts. Try again later.',
1347
+ },
1348
+ },
1349
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
1350
+ const ip = getClientIp(c);
1351
+ if (await runtime.rateLimit.trackAttempt(`reauth-challenge:${ip}`, reauthChallengeOpts)) {
1352
+ return c.json({ error: 'Too many reauth attempts. Try again later.' }, 429);
1353
+ }
1354
+ const userId = c.get('authUserId');
1355
+ const sessionId = c.get('sessionId');
1356
+ const mfaMethods = getConfig().mfa && adapter.getMfaMethods ? await adapter.getMfaMethods(userId) : [];
1357
+ const hasPassword = adapter.hasPassword ? await adapter.hasPassword(userId) : false;
1358
+ // Build availableMethods — all methods the user could potentially use
1359
+ const availableMethods = [];
1360
+ if (mfaMethods.includes('totp'))
1361
+ availableMethods.push('totp');
1362
+ if (mfaMethods.includes('emailOtp'))
1363
+ availableMethods.push('emailOtp');
1364
+ if (mfaMethods.includes('webauthn'))
1365
+ availableMethods.push('webauthn');
1366
+ if (hasPassword)
1367
+ availableMethods.push('password');
1368
+ if (mfaMethods.length > 0)
1369
+ availableMethods.push('recovery');
1370
+ // Generate challenge for challenge-based methods
1371
+ const hasEmailOtp = mfaMethods.includes('emailOtp');
1372
+ const hasWebAuthn = mfaMethods.includes('webauthn');
1373
+ if (!hasEmailOtp && !hasWebAuthn) {
1374
+ return c.json({ availableMethods }, 200);
1375
+ }
1376
+ let emailOtpHash;
1377
+ let webauthnChallenge;
1378
+ let webauthnOptions;
1379
+ if (hasEmailOtp) {
1380
+ const emailOtpConfig = getConfig().mfa?.emailOtp ?? null;
1381
+ if (emailOtpConfig) {
1382
+ const { generateEmailOtpCode } = await import('../services/mfa');
1383
+ const { code, hash } = generateEmailOtpCode(runtime);
1384
+ emailOtpHash = hash;
1385
+ const user = adapter.getUser ? await adapter.getUser(userId) : null;
1386
+ if (user?.email) {
1387
+ eventBus.emit('auth:delivery.email_otp', { email: user.email, code });
1388
+ }
1389
+ }
1390
+ }
1391
+ if (hasWebAuthn) {
1392
+ const { generateWebAuthnAuthenticationOptions } = await import('../services/mfa');
1393
+ const result = await generateWebAuthnAuthenticationOptions(userId, runtime);
1394
+ if (result) {
1395
+ webauthnChallenge = result.challenge;
1396
+ webauthnOptions = result.options;
1397
+ }
1398
+ }
1399
+ const { createReauthChallenge } = await import('../lib/mfaChallenge');
1400
+ const reauthToken = await createReauthChallenge(runtime.repos.mfaChallenge, userId, sessionId, { emailOtpHash, webauthnChallenge }, runtime.config);
1401
+ return c.json({ availableMethods, reauthToken, webauthnOptions }, 200);
1402
+ });
1403
+ // ---------------------------------------------------------------------------
1404
+ // Session management
1405
+ // ---------------------------------------------------------------------------
1406
+ const SessionInfoSchema = z
1407
+ .object({
1408
+ sessionId: z.string().describe('Unique session identifier (UUID).'),
1409
+ createdAt: z.number().describe('Unix timestamp (ms) when the session was created.'),
1410
+ lastActiveAt: z
1411
+ .number()
1412
+ .describe('Unix timestamp (ms) of the most recent authenticated request (updated when trackLastActive is enabled).'),
1413
+ expiresAt: z.number().describe('Unix timestamp (ms) when the session expires.'),
1414
+ ipAddress: z.string().optional().describe('IP address of the client at session creation.'),
1415
+ userAgent: z
1416
+ .string()
1417
+ .optional()
1418
+ .describe('User-agent string of the client at session creation.'),
1419
+ isActive: z.boolean().describe('Whether the session is currently valid and unexpired.'),
1420
+ })
1421
+ .openapi('SessionInfo');
1422
+ router.use('/auth/sessions', userAuth);
1423
+ router.use('/auth/sessions/*', userAuth);
1424
+ router.openapi(withSecurity(createRoute({
1425
+ method: 'get',
1426
+ path: '/auth/sessions',
1427
+ summary: 'List sessions',
1428
+ description: 'Returns all sessions for the authenticated user. Includes inactive sessions when `sessionPolicy.includeInactiveSessions` is enabled. Requires a valid session.',
1429
+ tags,
1430
+ responses: {
1431
+ 200: {
1432
+ content: {
1433
+ 'application/json': { schema: z.object({ sessions: z.array(SessionInfoSchema) }) },
1434
+ },
1435
+ description: 'Sessions belonging to the authenticated user.',
1436
+ },
1437
+ 401: {
1438
+ content: { 'application/json': { schema: ErrorResponse } },
1439
+ description: 'No valid session.',
1440
+ },
1441
+ },
1442
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
1443
+ const userId = c.get('authUserId');
1444
+ const sessions = await runtime.repos.session.getUserSessions(userId, runtime.config);
1445
+ return c.json({ sessions }, 200);
1446
+ });
1447
+ router.openapi(withSecurity(createRoute({
1448
+ method: 'delete',
1449
+ path: '/auth/sessions/{sessionId}',
1450
+ summary: 'Revoke a session',
1451
+ description: "Revokes a specific session by ID. Users can only revoke their own sessions. Useful for 'sign out of other devices' flows. Requires a valid session.",
1452
+ tags,
1453
+ request: {
1454
+ params: z.object({ sessionId: z.string().describe('UUID of the session to revoke.') }),
1455
+ },
1456
+ responses: {
1457
+ 200: {
1458
+ content: { 'application/json': { schema: SuccessResponse } },
1459
+ description: 'Session revoked successfully.',
1460
+ },
1461
+ 401: {
1462
+ content: { 'application/json': { schema: ErrorResponse } },
1463
+ description: 'No valid session.',
1464
+ },
1465
+ 404: {
1466
+ content: { 'application/json': { schema: ErrorResponse } },
1467
+ description: 'Session not found or does not belong to the authenticated user.',
1468
+ },
1469
+ },
1470
+ }), { cookieAuth: [] }, { userToken: [] }), async (c) => {
1471
+ const userId = c.get('authUserId');
1472
+ const { sessionId } = c.req.valid('param');
1473
+ const sessions = await runtime.repos.session.getUserSessions(userId, runtime.config);
1474
+ const session = sessions.find(s => s.sessionId === sessionId);
1475
+ if (!session)
1476
+ return c.json({ error: 'Session not found' }, 404);
1477
+ await runtime.repos.session.deleteSession(sessionId, runtime.config);
1478
+ eventBus.emit('security.auth.session.revoked', { userId, sessionId });
1479
+ return c.json({ ok: true }, 200);
1480
+ });
1481
+ // ---------------------------------------------------------------------------
1482
+ // Magic link / passwordless email login — only mounted when magicLink is configured
1483
+ // ---------------------------------------------------------------------------
1484
+ if (magicLink) {
1485
+ const magicLinkRequestOpts = { windowMs: 15 * 60 * 1000, max: 5 };
1486
+ const magicLinkVerifyOpts = { windowMs: 15 * 60 * 1000, max: 10 };
1487
+ router.openapi(createRoute({
1488
+ method: 'post',
1489
+ path: '/auth/magic-link/request',
1490
+ summary: 'Request a magic link',
1491
+ description: "Sends a single-use magic link to the user's inbox. Always returns 200 regardless of whether the identifier is registered (enumeration-safe). Rate-limited by IP.",
1492
+ tags,
1493
+ request: {
1494
+ body: {
1495
+ content: {
1496
+ 'application/json': {
1497
+ schema: z.object({
1498
+ identifier: z
1499
+ .string()
1500
+ .describe('The primary identifier (email, username, or phone) to send the magic link to.'),
1501
+ }),
1502
+ },
1503
+ },
1504
+ description: 'Login identifier.',
1505
+ },
1506
+ },
1507
+ responses: {
1508
+ 200: {
1509
+ content: { 'application/json': { schema: z.object({ message: z.string() }) } },
1510
+ description: 'If an account exists, a sign-in link has been sent.',
1511
+ },
1512
+ 429: {
1513
+ content: { 'application/json': { schema: ErrorResponse } },
1514
+ description: 'Too many magic link requests from this IP. Try again later.',
1515
+ },
1516
+ },
1517
+ }), async (c) => {
1518
+ const ip = getClientIp(c);
1519
+ if (await runtime.rateLimit.trackAttempt(`magic-link-request:${ip}`, magicLinkRequestOpts)) {
1520
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
1521
+ return c.json({ error: 'Too many requests. Try again later.' }, 429);
1522
+ }
1523
+ const { identifier } = c.req.valid('json');
1524
+ const msg = { message: 'If an account exists, a sign-in link has been sent.' };
1525
+ // Find user — enumeration-safe: same response regardless of existence
1526
+ const findFn = adapter.findByIdentifier ?? adapter.findByEmail.bind(adapter);
1527
+ const user = await findFn(identifier);
1528
+ if (user) {
1529
+ void (async () => {
1530
+ try {
1531
+ const ttl = getConfig().magicLink?.ttlSeconds ?? 900;
1532
+ const token = await createMagicLinkToken(runtime.repos.magicLink, user.id, ttl);
1533
+ const link = magicLink.linkBaseUrl
1534
+ ? `${magicLink.linkBaseUrl}?token=${token}`
1535
+ : token;
1536
+ eventBus.emit('auth:delivery.magic_link', { identifier, token, link });
1537
+ }
1538
+ catch (err) {
1539
+ console.error('[magic-link] Failed to send magic link:', err instanceof Error ? err.message : String(err));
1540
+ }
1541
+ })();
1542
+ }
1543
+ return c.json(msg, 200);
1544
+ });
1545
+ router.openapi(createRoute({
1546
+ method: 'post',
1547
+ path: '/auth/magic-link/verify',
1548
+ summary: 'Verify a magic link token',
1549
+ description: 'Consumes a single-use magic link token and creates a session. Rate-limited by IP.',
1550
+ tags,
1551
+ request: {
1552
+ body: {
1553
+ content: {
1554
+ 'application/json': {
1555
+ schema: z.object({
1556
+ token: z
1557
+ .string()
1558
+ .describe('Single-use magic link token received via the sign-in link.'),
1559
+ }),
1560
+ },
1561
+ },
1562
+ description: 'Magic link token.',
1563
+ },
1564
+ },
1565
+ responses: {
1566
+ 200: {
1567
+ content: { 'application/json': { schema: TokenResponse } },
1568
+ description: 'Authenticated. Returns a session token.',
1569
+ },
1570
+ 400: {
1571
+ content: { 'application/json': { schema: ErrorResponse } },
1572
+ description: 'Invalid or expired magic link token.',
1573
+ },
1574
+ 403: {
1575
+ content: { 'application/json': { schema: ErrorResponse } },
1576
+ description: 'Account suspended.',
1577
+ },
1578
+ 429: {
1579
+ content: { 'application/json': { schema: ErrorResponse } },
1580
+ description: 'Too many attempts from this IP. Try again later.',
1581
+ },
1582
+ },
1583
+ }), async (c) => {
1584
+ const ip = getClientIp(c);
1585
+ if (await runtime.rateLimit.trackAttempt(`magic-link-verify:${ip}`, magicLinkVerifyOpts)) {
1586
+ eventBus.emit('security.rate_limit.exceeded', { meta: { path: c.req.path } });
1587
+ return c.json({ error: 'Too many attempts. Try again later.' }, 429);
1588
+ }
1589
+ const { token } = c.req.valid('json');
1590
+ const userId = await consumeMagicLinkToken(runtime.repos.magicLink, token);
1591
+ if (!userId) {
1592
+ return c.json({ error: 'Invalid or expired magic link token' }, 400);
1593
+ }
1594
+ // Check suspension before issuing session
1595
+ const { getSuspended } = await import('../lib/suspension');
1596
+ const suspensionStatus = await getSuspended(adapter, userId);
1597
+ if (suspensionStatus.suspended) {
1598
+ eventBus.emit('security.auth.login.blocked', {
1599
+ userId,
1600
+ reason: 'suspended',
1601
+ meta: { reason: 'suspended' },
1602
+ });
1603
+ return c.json({ error: 'Account suspended' }, 403);
1604
+ }
1605
+ const metadata = {
1606
+ ipAddress: ip !== 'unknown' ? ip : undefined,
1607
+ userAgent: c.req.header('user-agent') ?? undefined,
1608
+ };
1609
+ const { createSessionForUser } = await import('../services/auth');
1610
+ const { token: sessionToken, refreshToken, sessionId, } = await createSessionForUser(userId, runtime, metadata, hookCtx(c));
1611
+ setCookie(c, COOKIE_TOKEN, sessionToken, cookieOptions(refreshTokens ? (getConfig().refreshToken?.accessTokenExpiry ?? 900) : undefined));
1612
+ if (refreshToken) {
1613
+ setCookie(c, COOKIE_REFRESH_TOKEN, refreshToken, cookieOptions(getConfig().refreshToken?.refreshTokenExpiry ?? 2_592_000));
1614
+ }
1615
+ if (getConfig().csrfEnabled)
1616
+ refreshCsrfToken(c);
1617
+ emitLoginSuccess(userId, sessionId, runtime);
1618
+ const fullUser = adapter.getUser ? await adapter.getUser(userId) : null;
1619
+ const result = { token: sessionToken, userId, email: fullUser?.email, refreshToken };
1620
+ return c.json(result, 200);
1621
+ });
1622
+ }
1623
+ return router;
1624
+ };