@rebasepro/server-core 0.4.0 → 0.6.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 (282) hide show
  1. package/README.md +62 -25
  2. package/dist/{server-core/src/api → api}/errors.d.ts +20 -2
  3. package/dist/{server-core/src/api → api}/rest/api-generator.d.ts +8 -2
  4. package/dist/{server-core/src/api → api}/types.d.ts +2 -0
  5. package/dist/auth/admin-user-ops.d.ts +79 -0
  6. package/dist/{server-core/src/auth → auth}/api-keys/index.d.ts +2 -2
  7. package/dist/{server-core/src/auth → auth}/auth-hooks.d.ts +37 -9
  8. package/dist/{server-core/src/auth → auth}/builtin-auth-adapter.d.ts +4 -4
  9. package/dist/{server-core/src/auth → auth}/index.d.ts +5 -2
  10. package/dist/{server-core/src/auth → auth}/jwt.d.ts +10 -0
  11. package/dist/auth/mfa-routes.d.ts +6 -0
  12. package/dist/auth/reset-password-admin.d.ts +29 -0
  13. package/dist/auth/session-routes.d.ts +25 -0
  14. package/dist/backend-CIxN4FVm.js +15 -0
  15. package/dist/backend-CIxN4FVm.js.map +1 -0
  16. package/dist/chunk-Dze3rakg.js +42 -0
  17. package/dist/{server-core/src/cron → cron}/cron-scheduler.d.ts +1 -1
  18. package/dist/dist-CZKP-Xz4.js +832 -0
  19. package/dist/dist-CZKP-Xz4.js.map +1 -0
  20. package/dist/env.d.ts +102 -0
  21. package/dist/from-VbwD7xRf.js +3849 -0
  22. package/dist/from-VbwD7xRf.js.map +1 -0
  23. package/dist/{server-core/src/index.d.ts → index.d.ts} +1 -0
  24. package/dist/index.es.js +60871 -50424
  25. package/dist/index.es.js.map +1 -1
  26. package/dist/index.umd.js +73151 -51256
  27. package/dist/index.umd.js.map +1 -1
  28. package/dist/init/docs.d.ts +4 -0
  29. package/dist/init/health.d.ts +2 -0
  30. package/dist/init/middlewares.d.ts +10 -0
  31. package/dist/init/shutdown.d.ts +11 -0
  32. package/dist/init/storage.d.ts +5 -0
  33. package/dist/{server-core/src/init.d.ts → init.d.ts} +4 -4
  34. package/dist/jwt-DHcQRGC3.js +4168 -0
  35. package/dist/jwt-DHcQRGC3.js.map +1 -0
  36. package/dist/logger-BYU66ENZ.js +94 -0
  37. package/dist/logger-BYU66ENZ.js.map +1 -0
  38. package/dist/ms-BeBSuOXN.js +125 -0
  39. package/dist/ms-BeBSuOXN.js.map +1 -0
  40. package/dist/multipart-parser-CedBDOeC.js +299 -0
  41. package/dist/multipart-parser-CedBDOeC.js.map +1 -0
  42. package/dist/src-COaj0G3P.js +1182 -0
  43. package/dist/src-COaj0G3P.js.map +1 -0
  44. package/dist/{server-core/src/storage → storage}/image-transform.d.ts +3 -1
  45. package/dist/utils/request-id.d.ts +4 -0
  46. package/package.json +29 -29
  47. package/src/api/collections_for_test/callbacks_test_collection.ts +7 -6
  48. package/src/api/errors.ts +95 -20
  49. package/src/api/graphql/graphql-schema-generator.ts +15 -10
  50. package/src/api/logs-routes.ts +5 -2
  51. package/src/api/openapi-generator.ts +1 -1
  52. package/src/api/rest/api-generator-count.test.ts +21 -10
  53. package/src/api/rest/api-generator.ts +151 -36
  54. package/src/api/rest/query-parser.ts +18 -12
  55. package/src/api/server.ts +13 -2
  56. package/src/api/types.ts +2 -0
  57. package/src/auth/adapter-middleware.ts +17 -10
  58. package/src/auth/admin-user-ops.ts +236 -0
  59. package/src/auth/api-keys/api-key-middleware.ts +15 -9
  60. package/src/auth/api-keys/api-key-permission-guard.ts +1 -1
  61. package/src/auth/api-keys/api-key-routes.ts +3 -3
  62. package/src/auth/api-keys/api-key-store.ts +14 -8
  63. package/src/auth/api-keys/index.ts +2 -2
  64. package/src/auth/apple-oauth.ts +4 -3
  65. package/src/auth/auth-hooks.ts +50 -10
  66. package/src/auth/bitbucket-oauth.ts +5 -4
  67. package/src/auth/builtin-auth-adapter.ts +64 -54
  68. package/src/auth/custom-auth-adapter.ts +4 -5
  69. package/src/auth/discord-oauth.ts +5 -4
  70. package/src/auth/facebook-oauth.ts +5 -4
  71. package/src/auth/github-oauth.ts +6 -5
  72. package/src/auth/gitlab-oauth.ts +5 -4
  73. package/src/auth/google-oauth.ts +2 -1
  74. package/src/auth/index.ts +6 -2
  75. package/src/auth/jwt.ts +15 -4
  76. package/src/auth/linkedin-oauth.ts +4 -3
  77. package/src/auth/mfa-routes.ts +299 -0
  78. package/src/auth/mfa.ts +2 -1
  79. package/src/auth/microsoft-oauth.ts +5 -4
  80. package/src/auth/middleware.ts +5 -4
  81. package/src/auth/rate-limiter.ts +1 -1
  82. package/src/auth/reset-password-admin.ts +144 -0
  83. package/src/auth/rls-scope.ts +1 -1
  84. package/src/auth/routes.ts +52 -600
  85. package/src/auth/session-routes.ts +341 -0
  86. package/src/auth/slack-oauth.ts +6 -5
  87. package/src/auth/spotify-oauth.ts +5 -4
  88. package/src/auth/twitter-oauth.ts +4 -3
  89. package/src/collections/loader.ts +5 -4
  90. package/src/cron/cron-scheduler.test.ts +23 -11
  91. package/src/cron/cron-scheduler.ts +14 -8
  92. package/src/cron/cron-store.ts +1 -1
  93. package/src/email/smtp-email-service.ts +3 -2
  94. package/src/env.ts +11 -10
  95. package/src/index.ts +1 -0
  96. package/src/init/docs.ts +47 -0
  97. package/src/init/health.ts +37 -0
  98. package/src/init/middlewares.ts +61 -0
  99. package/src/init/shutdown.ts +56 -0
  100. package/src/init/storage.ts +57 -0
  101. package/src/init.ts +93 -295
  102. package/src/serve-spa.ts +12 -5
  103. package/src/services/driver-registry.ts +4 -3
  104. package/src/storage/S3StorageController.ts +1 -2
  105. package/src/storage/image-transform.ts +25 -10
  106. package/src/storage/routes.ts +30 -11
  107. package/src/storage/storage-registry.ts +4 -3
  108. package/src/storage/tus-handler.ts +12 -12
  109. package/src/utils/request-id.ts +40 -0
  110. package/src/utils/request-logger.ts +6 -0
  111. package/test/api-generator.test.ts +90 -2
  112. package/test/api-key-permission-guard.test.ts +24 -12
  113. package/test/auth-routes.test.ts +5 -3
  114. package/test/backend-hooks-data.test.ts +99 -30
  115. package/test/custom-auth-adapter.test.ts +16 -15
  116. package/test/email-templates.test.ts +10 -5
  117. package/test/env.test.ts +10 -10
  118. package/test/function-loader.test.ts +7 -4
  119. package/test/graphql-schema-generator.test.ts +554 -0
  120. package/test/jwt-security.test.ts +1 -1
  121. package/test/query-parser.test.ts +0 -1
  122. package/test/reset-password-admin.test.ts +113 -0
  123. package/test/singleton.test.ts +5 -5
  124. package/test/smtp-email-service.test.ts +21 -21
  125. package/test/webhook-service.test.ts +22 -11
  126. package/tsconfig.json +2 -0
  127. package/tsconfig.prod.json +3 -0
  128. package/vite.config.ts +5 -5
  129. package/dist/common/src/collections/CollectionRegistry.d.ts +0 -56
  130. package/dist/common/src/collections/default-collections.d.ts +0 -9
  131. package/dist/common/src/collections/index.d.ts +0 -2
  132. package/dist/common/src/data/buildRebaseData.d.ts +0 -14
  133. package/dist/common/src/data/query_builder.d.ts +0 -55
  134. package/dist/common/src/index.d.ts +0 -4
  135. package/dist/common/src/util/builders.d.ts +0 -57
  136. package/dist/common/src/util/callbacks.d.ts +0 -6
  137. package/dist/common/src/util/collections.d.ts +0 -11
  138. package/dist/common/src/util/common.d.ts +0 -2
  139. package/dist/common/src/util/conditions.d.ts +0 -26
  140. package/dist/common/src/util/entities.d.ts +0 -58
  141. package/dist/common/src/util/enums.d.ts +0 -3
  142. package/dist/common/src/util/index.d.ts +0 -16
  143. package/dist/common/src/util/navigation_from_path.d.ts +0 -34
  144. package/dist/common/src/util/navigation_utils.d.ts +0 -20
  145. package/dist/common/src/util/parent_references_from_path.d.ts +0 -6
  146. package/dist/common/src/util/paths.d.ts +0 -14
  147. package/dist/common/src/util/permissions.d.ts +0 -6
  148. package/dist/common/src/util/references.d.ts +0 -2
  149. package/dist/common/src/util/relations.d.ts +0 -22
  150. package/dist/common/src/util/resolutions.d.ts +0 -72
  151. package/dist/common/src/util/storage.d.ts +0 -24
  152. package/dist/index-Cr1D21av.js +0 -49
  153. package/dist/index-Cr1D21av.js.map +0 -1
  154. package/dist/server-core/src/auth/admin-routes.d.ts +0 -27
  155. package/dist/server-core/src/env.d.ts +0 -137
  156. package/dist/types/src/controllers/analytics_controller.d.ts +0 -7
  157. package/dist/types/src/controllers/auth.d.ts +0 -104
  158. package/dist/types/src/controllers/client.d.ts +0 -168
  159. package/dist/types/src/controllers/collection_registry.d.ts +0 -46
  160. package/dist/types/src/controllers/customization_controller.d.ts +0 -60
  161. package/dist/types/src/controllers/data.d.ts +0 -207
  162. package/dist/types/src/controllers/data_driver.d.ts +0 -218
  163. package/dist/types/src/controllers/database_admin.d.ts +0 -11
  164. package/dist/types/src/controllers/dialogs_controller.d.ts +0 -36
  165. package/dist/types/src/controllers/effective_role.d.ts +0 -4
  166. package/dist/types/src/controllers/email.d.ts +0 -36
  167. package/dist/types/src/controllers/index.d.ts +0 -18
  168. package/dist/types/src/controllers/local_config_persistence.d.ts +0 -20
  169. package/dist/types/src/controllers/navigation.d.ts +0 -225
  170. package/dist/types/src/controllers/registry.d.ts +0 -63
  171. package/dist/types/src/controllers/side_dialogs_controller.d.ts +0 -67
  172. package/dist/types/src/controllers/side_entity_controller.d.ts +0 -97
  173. package/dist/types/src/controllers/snackbar.d.ts +0 -24
  174. package/dist/types/src/controllers/storage.d.ts +0 -171
  175. package/dist/types/src/index.d.ts +0 -4
  176. package/dist/types/src/rebase_context.d.ts +0 -122
  177. package/dist/types/src/types/auth_adapter.d.ts +0 -301
  178. package/dist/types/src/types/backend.d.ts +0 -536
  179. package/dist/types/src/types/backend_hooks.d.ts +0 -172
  180. package/dist/types/src/types/builders.d.ts +0 -15
  181. package/dist/types/src/types/chips.d.ts +0 -5
  182. package/dist/types/src/types/collections.d.ts +0 -941
  183. package/dist/types/src/types/component_ref.d.ts +0 -47
  184. package/dist/types/src/types/cron.d.ts +0 -102
  185. package/dist/types/src/types/data_source.d.ts +0 -64
  186. package/dist/types/src/types/database_adapter.d.ts +0 -94
  187. package/dist/types/src/types/entities.d.ts +0 -145
  188. package/dist/types/src/types/entity_actions.d.ts +0 -104
  189. package/dist/types/src/types/entity_callbacks.d.ts +0 -173
  190. package/dist/types/src/types/entity_link_builder.d.ts +0 -7
  191. package/dist/types/src/types/entity_overrides.d.ts +0 -10
  192. package/dist/types/src/types/entity_views.d.ts +0 -87
  193. package/dist/types/src/types/export_import.d.ts +0 -21
  194. package/dist/types/src/types/formex.d.ts +0 -40
  195. package/dist/types/src/types/index.d.ts +0 -28
  196. package/dist/types/src/types/locales.d.ts +0 -4
  197. package/dist/types/src/types/modify_collections.d.ts +0 -5
  198. package/dist/types/src/types/plugins.d.ts +0 -282
  199. package/dist/types/src/types/properties.d.ts +0 -1181
  200. package/dist/types/src/types/property_config.d.ts +0 -74
  201. package/dist/types/src/types/relations.d.ts +0 -336
  202. package/dist/types/src/types/slots.d.ts +0 -262
  203. package/dist/types/src/types/translations.d.ts +0 -900
  204. package/dist/types/src/types/user_management_delegate.d.ts +0 -86
  205. package/dist/types/src/types/websockets.d.ts +0 -78
  206. package/dist/types/src/users/index.d.ts +0 -1
  207. package/dist/types/src/users/user.d.ts +0 -50
  208. package/src/auth/admin-routes.ts +0 -534
  209. package/test/admin-routes.test.ts +0 -544
  210. package/test/backend-hooks-admin.test.ts +0 -388
  211. package/test.ts +0 -6
  212. /package/dist/{server-core/src/api → api}/ast-schema-editor.d.ts +0 -0
  213. /package/dist/{server-core/src/api → api}/collections_for_test/callbacks_test_collection.d.ts +0 -0
  214. /package/dist/{server-core/src/api → api}/graphql/graphql-schema-generator.d.ts +0 -0
  215. /package/dist/{server-core/src/api → api}/graphql/index.d.ts +0 -0
  216. /package/dist/{server-core/src/api → api}/index.d.ts +0 -0
  217. /package/dist/{server-core/src/api → api}/logs-routes.d.ts +0 -0
  218. /package/dist/{server-core/src/api → api}/openapi-generator.d.ts +0 -0
  219. /package/dist/{server-core/src/api → api}/rest/index.d.ts +0 -0
  220. /package/dist/{server-core/src/api → api}/rest/query-parser.d.ts +0 -0
  221. /package/dist/{server-core/src/api → api}/schema-editor-routes.d.ts +0 -0
  222. /package/dist/{server-core/src/api → api}/server.d.ts +0 -0
  223. /package/dist/{server-core/src/auth → auth}/adapter-middleware.d.ts +0 -0
  224. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-middleware.d.ts +0 -0
  225. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-permission-guard.d.ts +0 -0
  226. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-routes.d.ts +0 -0
  227. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-store.d.ts +0 -0
  228. /package/dist/{server-core/src/auth → auth}/api-keys/api-key-types.d.ts +0 -0
  229. /package/dist/{server-core/src/auth → auth}/apple-oauth.d.ts +0 -0
  230. /package/dist/{server-core/src/auth → auth}/bitbucket-oauth.d.ts +0 -0
  231. /package/dist/{server-core/src/auth → auth}/crypto-utils.d.ts +0 -0
  232. /package/dist/{server-core/src/auth → auth}/custom-auth-adapter.d.ts +0 -0
  233. /package/dist/{server-core/src/auth → auth}/discord-oauth.d.ts +0 -0
  234. /package/dist/{server-core/src/auth → auth}/facebook-oauth.d.ts +0 -0
  235. /package/dist/{server-core/src/auth → auth}/github-oauth.d.ts +0 -0
  236. /package/dist/{server-core/src/auth → auth}/gitlab-oauth.d.ts +0 -0
  237. /package/dist/{server-core/src/auth → auth}/google-oauth.d.ts +0 -0
  238. /package/dist/{server-core/src/auth → auth}/interfaces.d.ts +0 -0
  239. /package/dist/{server-core/src/auth → auth}/linkedin-oauth.d.ts +0 -0
  240. /package/dist/{server-core/src/auth → auth}/mfa.d.ts +0 -0
  241. /package/dist/{server-core/src/auth → auth}/microsoft-oauth.d.ts +0 -0
  242. /package/dist/{server-core/src/auth → auth}/middleware.d.ts +0 -0
  243. /package/dist/{server-core/src/auth → auth}/password.d.ts +0 -0
  244. /package/dist/{server-core/src/auth → auth}/rate-limiter.d.ts +0 -0
  245. /package/dist/{server-core/src/auth → auth}/rls-scope.d.ts +0 -0
  246. /package/dist/{server-core/src/auth → auth}/routes.d.ts +0 -0
  247. /package/dist/{server-core/src/auth → auth}/slack-oauth.d.ts +0 -0
  248. /package/dist/{server-core/src/auth → auth}/spotify-oauth.d.ts +0 -0
  249. /package/dist/{server-core/src/auth → auth}/twitter-oauth.d.ts +0 -0
  250. /package/dist/{server-core/src/collections → collections}/BackendCollectionRegistry.d.ts +0 -0
  251. /package/dist/{server-core/src/collections → collections}/loader.d.ts +0 -0
  252. /package/dist/{server-core/src/cron → cron}/cron-loader.d.ts +0 -0
  253. /package/dist/{server-core/src/cron → cron}/cron-routes.d.ts +0 -0
  254. /package/dist/{server-core/src/cron → cron}/cron-store.d.ts +0 -0
  255. /package/dist/{server-core/src/cron → cron}/index.d.ts +0 -0
  256. /package/dist/{server-core/src/db → db}/interfaces.d.ts +0 -0
  257. /package/dist/{server-core/src/email → email}/index.d.ts +0 -0
  258. /package/dist/{server-core/src/email → email}/smtp-email-service.d.ts +0 -0
  259. /package/dist/{server-core/src/email → email}/templates.d.ts +0 -0
  260. /package/dist/{server-core/src/email → email}/types.d.ts +0 -0
  261. /package/dist/{server-core/src/functions → functions}/function-loader.d.ts +0 -0
  262. /package/dist/{server-core/src/functions → functions}/function-routes.d.ts +0 -0
  263. /package/dist/{server-core/src/functions → functions}/index.d.ts +0 -0
  264. /package/dist/{server-core/src/history → history}/history-routes.d.ts +0 -0
  265. /package/dist/{server-core/src/history → history}/index.d.ts +0 -0
  266. /package/dist/{server-core/src/serve-spa.d.ts → serve-spa.d.ts} +0 -0
  267. /package/dist/{server-core/src/services → services}/driver-registry.d.ts +0 -0
  268. /package/dist/{server-core/src/services → services}/webhook-service.d.ts +0 -0
  269. /package/dist/{server-core/src/singleton.d.ts → singleton.d.ts} +0 -0
  270. /package/dist/{server-core/src/storage → storage}/LocalStorageController.d.ts +0 -0
  271. /package/dist/{server-core/src/storage → storage}/S3StorageController.d.ts +0 -0
  272. /package/dist/{server-core/src/storage → storage}/index.d.ts +0 -0
  273. /package/dist/{server-core/src/storage → storage}/routes.d.ts +0 -0
  274. /package/dist/{server-core/src/storage → storage}/storage-registry.d.ts +0 -0
  275. /package/dist/{server-core/src/storage → storage}/tus-handler.d.ts +0 -0
  276. /package/dist/{server-core/src/storage → storage}/types.d.ts +0 -0
  277. /package/dist/{server-core/src/types → types}/index.d.ts +0 -0
  278. /package/dist/{server-core/src/utils → utils}/dev-port.d.ts +0 -0
  279. /package/dist/{server-core/src/utils → utils}/logger.d.ts +0 -0
  280. /package/dist/{server-core/src/utils → utils}/logging.d.ts +0 -0
  281. /package/dist/{server-core/src/utils → utils}/request-logger.d.ts +0 -0
  282. /package/dist/{server-core/src/utils → utils}/sql.d.ts +0 -0
@@ -1,6 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { ApiError, errorHandler } from "../api/errors";
3
- import { randomBytes, createHash } from "crypto";
3
+ import { randomBytes } from "crypto";
4
+ import { generateSecureToken, hashToken } from "./admin-user-ops";
4
5
  import type { AuthRepository, OAuthProvider, CreateUserData } from "./interfaces";
5
6
  import { generateAccessToken, generateRefreshToken, hashRefreshToken, getRefreshTokenExpiry, getAccessTokenExpiry } from "./jwt";
6
7
  import type { AuthHooks } from "./auth-hooks";
@@ -11,7 +12,9 @@ import { getPasswordResetTemplate, getEmailVerificationTemplate, getWelcomeEmail
11
12
  import { HonoEnv } from "../api/types";
12
13
  import { defaultAuthLimiter, strictAuthLimiter } from "./rate-limiter";
13
14
  import { z } from "zod";
14
- import { generateTotpSecret, verifyTotp, base32Decode, generateRecoveryCodes, hashRecoveryCode } from "./mfa";
15
+ import { logger } from "../utils/logger";
16
+ import { mountMfaRoutes } from "./mfa-routes";
17
+ import { mountSessionRoutes } from "./session-routes";
15
18
 
16
19
  /**
17
20
  * Shared configuration for auth and admin route factories.
@@ -68,19 +71,6 @@ function buildAuthResponse(
68
71
  };
69
72
  }
70
73
 
71
- /**
72
- * Generate a secure random token
73
- */
74
- function generateSecureToken(): string {
75
- return randomBytes(40).toString("hex");
76
- }
77
-
78
- /**
79
- * Hash a token for database storage
80
- */
81
- function hashToken(token: string): string {
82
- return createHash("sha256").update(token).digest("hex");
83
- }
84
74
 
85
75
  /**
86
76
  * Get password reset token expiry (1 hour from now)
@@ -102,8 +92,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
102
92
  router.onError(errorHandler);
103
93
 
104
94
  const authRepo = config.authRepo;
105
- const { emailService, emailConfig, allowRegistration = false, authHooks } = config;
106
- const ops = resolveAuthHooks(authHooks);
95
+ const { emailService, emailConfig, allowRegistration = false } = config;
96
+ const ops = resolveAuthHooks(config.authHooks);
107
97
 
108
98
  // ── Zod input schemas ──────────────────────────────────────────────
109
99
  const registerSchema = z.object({
@@ -141,7 +131,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
141
131
  function parseBody<T>(schema: z.ZodSchema<T>, body: unknown): T {
142
132
  const result = schema.safeParse(body);
143
133
  if (!result.success) {
144
- const messages = result.error.errors.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
134
+ const messages = result.error.issues.map(e => `${e.path.join(".")}: ${e.message}`).join(". ");
145
135
  throw ApiError.badRequest(messages, "INVALID_INPUT");
146
136
  }
147
137
  return result.data;
@@ -182,7 +172,7 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
182
172
  html: emailContent.html,
183
173
  text: emailContent.text
184
174
  }).catch(err => {
185
- console.error("Failed to send welcome email:", err instanceof Error ? err.message : err);
175
+ logger.error("Failed to send welcome email", { error: err instanceof Error ? err.message : err });
186
176
  });
187
177
  }
188
178
 
@@ -195,11 +185,13 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
195
185
 
196
186
  // Allow customization of access token claims via hook
197
187
  let customClaims: Record<string, unknown> | undefined;
198
- if (authHooks?.customizeAccessToken) {
188
+ if (ops.customizeAccessToken) {
199
189
  const user = await authRepo.getUserById(userId);
200
190
  if (user) {
201
- const defaultClaims: Record<string, unknown> = { userId, roles: roleIds, aal: "aal1" };
202
- customClaims = await authHooks.customizeAccessToken(defaultClaims, user);
191
+ const defaultClaims: Record<string, unknown> = { userId,
192
+ roles: roleIds,
193
+ aal: "aal1" };
194
+ customClaims = await ops.customizeAccessToken(defaultClaims, user);
203
195
  }
204
196
  }
205
197
 
@@ -214,7 +206,9 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
214
206
  ipAddress
215
207
  );
216
208
 
217
- return { roleIds, accessToken, refreshToken };
209
+ return { roleIds,
210
+ accessToken,
211
+ refreshToken };
218
212
  }
219
213
 
220
214
  /**
@@ -253,8 +247,8 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
253
247
  passwordHash,
254
248
  displayName: displayName || undefined
255
249
  };
256
- if (authHooks?.beforeUserCreate) {
257
- createData = await authHooks.beforeUserCreate(createData);
250
+ if (ops.beforeUserCreate) {
251
+ createData = await ops.beforeUserCreate(createData);
258
252
  }
259
253
  const user = await authRepo.createUser(createData);
260
254
 
@@ -282,18 +276,18 @@ export function createAuthRoutes(config: AuthModuleConfig): Hono<HonoEnv> {
282
276
  displayName: user.displayName });
283
277
 
284
278
  // Fire afterUserCreate hook
285
- if (authHooks?.afterUserCreate) {
279
+ if (ops.afterUserCreate) {
286
280
  try {
287
- await authHooks.afterUserCreate(user);
281
+ await ops.afterUserCreate(user);
288
282
  } catch (err) {
289
- console.error("[AuthHooks] afterUserCreate error:", err instanceof Error ? err.message : err);
283
+ logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
290
284
  }
291
285
  }
292
286
 
293
287
  // Fire onAuthenticated hook (fire-and-forget)
294
- if (authHooks?.onAuthenticated) {
295
- authHooks.onAuthenticated(user, "register").catch(err => {
296
- console.error("[AuthHooks] onAuthenticated error:", err instanceof Error ? err.message : err);
288
+ if (ops.onAuthenticated) {
289
+ ops.onAuthenticated(user, "register").catch(err => {
290
+ logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
297
291
  });
298
292
  }
299
293
 
@@ -308,15 +302,15 @@ displayName: user.displayName });
308
302
  const { email, password } = parseBody(loginSchema, await c.req.json());
309
303
 
310
304
  // Call beforeLogin hook if provided (throw to reject)
311
- if (authHooks?.beforeLogin) {
312
- await authHooks.beforeLogin(email, "login");
305
+ if (ops.beforeLogin) {
306
+ await ops.beforeLogin(email, "login");
313
307
  }
314
308
 
315
309
  let user;
316
310
 
317
- if (authHooks?.verifyCredentials) {
311
+ if (ops.verifyCredentials) {
318
312
  // Full credential verification override
319
- user = await authHooks.verifyCredentials(email, password, authRepo);
313
+ user = await ops.verifyCredentials(email, password, authRepo);
320
314
  if (!user) {
321
315
  throw ApiError.unauthorized("Invalid email or password", "INVALID_CREDENTIALS");
322
316
  }
@@ -344,9 +338,9 @@ displayName: user.displayName });
344
338
  );
345
339
 
346
340
  // Fire onAuthenticated hook (fire-and-forget)
347
- if (authHooks?.onAuthenticated) {
348
- authHooks.onAuthenticated(user, "login").catch(err => {
349
- console.error("[AuthHooks] onAuthenticated error:", err instanceof Error ? err.message : err);
341
+ if (ops.onAuthenticated) {
342
+ ops.onAuthenticated(user, "login").catch(err => {
343
+ logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
350
344
  });
351
345
  }
352
346
 
@@ -399,11 +393,11 @@ displayName: user.displayName });
399
393
  await authRepo.linkUserIdentity(user.id, provider.id, externalUser.providerId, { email: externalUser.email });
400
394
 
401
395
  // Fire afterUserCreate hook
402
- if (authHooks?.afterUserCreate) {
396
+ if (ops.afterUserCreate) {
403
397
  try {
404
- await authHooks.afterUserCreate(user);
398
+ await ops.afterUserCreate(user);
405
399
  } catch (err) {
406
- console.error("[AuthHooks] afterUserCreate error:", err instanceof Error ? err.message : err);
400
+ logger.error("[AuthHooks] afterUserCreate error", { error: err instanceof Error ? err.message : err });
407
401
  }
408
402
  }
409
403
 
@@ -487,7 +481,7 @@ displayName: user.displayName }, appName);
487
481
  text: emailContent.text
488
482
  });
489
483
  } catch (emailError: unknown) {
490
- console.error("Failed to send password reset email:", emailError instanceof Error ? emailError.message : emailError);
484
+ logger.error("Failed to send password reset email", { error: emailError instanceof Error ? emailError.message : emailError });
491
485
  // Don't reveal email sending failure to client
492
486
  }
493
487
  }
@@ -531,9 +525,9 @@ displayName: user.displayName }, appName);
531
525
  await authRepo.deleteAllRefreshTokensForUser(storedToken.userId);
532
526
 
533
527
  // Fire onPasswordReset hook (fire-and-forget)
534
- if (authHooks?.onPasswordReset) {
535
- authHooks.onPasswordReset(storedToken.userId).catch(err => {
536
- console.error("[AuthHooks] onPasswordReset error:", err instanceof Error ? err.message : err);
528
+ if (ops.onPasswordReset) {
529
+ ops.onPasswordReset(storedToken.userId).catch(err => {
530
+ logger.error("[AuthHooks] onPasswordReset error", { error: err instanceof Error ? err.message : err });
537
531
  });
538
532
  }
539
533
 
@@ -686,11 +680,13 @@ message: "Email verified successfully" });
686
680
 
687
681
  // Allow customization of access token claims via hook
688
682
  let customClaims: Record<string, unknown> | undefined;
689
- if (authHooks?.customizeAccessToken) {
683
+ if (ops.customizeAccessToken) {
690
684
  const user = await authRepo.getUserById(storedToken.userId);
691
685
  if (user) {
692
- const defaultClaims: Record<string, unknown> = { userId: storedToken.userId, roles: roleIds, aal: "aal1" };
693
- customClaims = await authHooks.customizeAccessToken(defaultClaims, user);
686
+ const defaultClaims: Record<string, unknown> = { userId: storedToken.userId,
687
+ roles: roleIds,
688
+ aal: "aal1" };
689
+ customClaims = await ops.customizeAccessToken(defaultClaims, user);
694
690
  }
695
691
  }
696
692
 
@@ -719,563 +715,19 @@ message: "Email verified successfully" });
719
715
  });
720
716
  });
721
717
 
722
- /**
723
- * POST /auth/logout
724
- * Invalidate refresh token
725
- */
726
- router.post("/logout", async (c) => {
727
- const { refreshToken } = parseBody(logoutSchema, await c.req.json());
728
-
729
- if (refreshToken) {
730
- const tokenHash = hashRefreshToken(refreshToken);
731
- await authRepo.deleteRefreshToken(tokenHash);
732
- }
733
-
734
- // Call afterLogout hook (fire-and-forget)
735
- // Extract userId from the access token if present
736
- const authHeader = c.req.header("authorization");
737
- if (authHooks?.afterLogout && authHeader?.startsWith("Bearer ")) {
738
- const { verifyAccessToken } = await import("./jwt");
739
- const payload = verifyAccessToken(authHeader.substring(7));
740
- if (payload) {
741
- authHooks.afterLogout(payload.userId).catch(err => {
742
- console.error("[AuthHooks] afterLogout error:", err instanceof Error ? err.message : err);
743
- });
744
- }
745
- }
746
-
747
- return c.json({ success: true });
748
- });
749
-
750
- /**
751
- * GET /auth/sessions
752
- * Get active refresh tokens (sessions) for the current user
753
- */
754
- router.get("/sessions", requireAuth, async (c) => {
755
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
756
- if (!userCtx) {
757
- throw ApiError.unauthorized("Not authenticated");
758
- }
759
-
760
- const currentRefreshToken = c.req.header("x-refresh-token") as string;
761
- const currentTokenHash = currentRefreshToken ? hashRefreshToken(currentRefreshToken) : null;
762
-
763
- const sessions = await authRepo.listRefreshTokensForUser(userCtx.userId);
764
-
765
- const mappedSessions = sessions.map(s => ({
766
- id: s.id,
767
- userAgent: s.userAgent,
768
- ipAddress: s.ipAddress,
769
- createdAt: s.createdAt,
770
- isCurrentSession: currentTokenHash ? s.tokenHash === currentTokenHash : false
771
- }));
772
-
773
- return c.json({ sessions: mappedSessions });
774
- });
775
-
776
- /**
777
- * DELETE /auth/sessions
778
- * Delete all refresh tokens for the current user (remote logout every device)
779
- */
780
- router.delete("/sessions", requireAuth, async (c) => {
781
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
782
- if (!userCtx) {
783
- throw ApiError.unauthorized("Not authenticated");
784
- }
785
-
786
- await authRepo.deleteAllRefreshTokensForUser(userCtx.userId);
787
- return c.json({ success: true,
788
- message: "All sessions revoked successfully" });
789
- });
790
-
791
- /**
792
- * DELETE /auth/sessions/:id
793
- * Delete a specific refresh token (remote logout)
794
- */
795
- router.delete("/sessions/:id", requireAuth, async (c) => {
796
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
797
- if (!userCtx) {
798
- throw ApiError.unauthorized("Not authenticated");
799
- }
800
-
801
- const id = c.req.param("id");
802
- if (!id) {
803
- throw ApiError.badRequest("Session ID is required", "INVALID_INPUT");
804
- }
805
-
806
- await authRepo.deleteRefreshTokenById(id, userCtx.userId);
807
- return c.json({ success: true,
808
- message: "Session revoked successfully" });
809
- });
810
-
811
- /**
812
- * GET /auth/me
813
- * Get current authenticated user
814
- */
815
- router.get("/me", requireAuth, async (c) => {
816
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
817
- if (!userCtx) {
818
- throw ApiError.unauthorized("Not authenticated");
819
- }
820
-
821
- const result = await authRepo.getUserWithRoles(userCtx.userId);
822
- if (!result) {
823
- throw ApiError.notFound("User not found");
824
- }
825
-
826
- return c.json({
827
- user: {
828
- uid: result.user.id,
829
- email: result.user.email,
830
- displayName: result.user.displayName,
831
- photoURL: result.user.photoUrl,
832
- emailVerified: result.user.emailVerified,
833
- roles: result.roles.map(r => r.id),
834
- metadata: result.user.metadata ?? {}
835
- }
836
- });
837
- });
838
-
839
- /**
840
- * PATCH /auth/me
841
- * Update current authenticated user profile
842
- */
843
- router.patch("/me", requireAuth, async (c) => {
844
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
845
- if (!userCtx) {
846
- throw ApiError.unauthorized("Not authenticated");
847
- }
848
-
849
- const { displayName, photoURL } = parseBody(updateProfileSchema, await c.req.json());
850
-
851
- const updatedUser = await authRepo.updateUser(userCtx.userId, {
852
- displayName: displayName !== undefined ? displayName : undefined,
853
- photoUrl: photoURL !== undefined ? photoURL : undefined
854
- });
855
-
856
- if (!updatedUser) {
857
- throw ApiError.notFound("User not found");
858
- }
859
-
860
- const result = await authRepo.getUserWithRoles(userCtx.userId);
861
- if (!result) {
862
- throw ApiError.notFound("User not found");
863
- }
864
-
865
- return c.json({
866
- user: {
867
- uid: result.user.id,
868
- email: result.user.email,
869
- displayName: result.user.displayName,
870
- photoURL: result.user.photoUrl,
871
- emailVerified: result.user.emailVerified,
872
- roles: result.roles.map(r => r.id),
873
- metadata: result.user.metadata ?? {}
874
- }
875
- });
876
- });
877
-
878
- /**
879
- * GET /auth/config
880
- * Get public auth configuration (for frontend to know what's available)
881
- */
882
- router.get("/config", defaultAuthLimiter, async (c) => {
883
- // Determine if setup is needed using the persistent bootstrap flag
884
- // when available, falling back to user-count check for backward compat.
885
- let needsSetup: boolean;
886
- if (config.isBootstrapCompleted) {
887
- needsSetup = !(await config.isBootstrapCompleted());
888
- } else {
889
- const allUsers = await authRepo.listUsers();
890
- needsSetup = allUsers.length === 0;
891
- }
892
-
893
- // Registration is allowed when explicitly enabled OR during initial setup
894
- const registrationAllowed = needsSetup || !!allowRegistration;
895
-
896
- // Build the list of enabled OAuth providers for frontend discovery.
897
- const enabledProviders = (config.oauthProviders || []).map(p => p.id);
898
-
899
- return c.json({
900
- needsSetup,
901
- registrationEnabled: registrationAllowed,
902
- emailServiceEnabled: isEmailConfigured(),
903
- enabledProviders
904
- });
905
- });
906
-
907
- // ═══════════════════════════════════════════════════════════════════════
908
- // ANONYMOUS SIGN-IN
909
- // ═══════════════════════════════════════════════════════════════════════
910
-
911
- /**
912
- * POST /auth/anonymous
913
- * Create an anonymous user with temporary credentials
914
- */
915
- router.post("/anonymous", strictAuthLimiter, async (c) => {
916
- const anonId = randomBytes(16).toString("hex");
917
- const anonEmail = `anon_${anonId.slice(0, 8)}@anonymous.local`;
918
-
919
- let createData: CreateUserData = {
920
- email: anonEmail,
921
- emailVerified: false,
922
- isAnonymous: true
923
- };
924
-
925
- if (authHooks?.beforeUserCreate) {
926
- createData = await authHooks.beforeUserCreate(createData);
927
- }
928
-
929
- const user = await authRepo.createUser(createData);
930
-
931
- // Assign default role (follow register route pattern, but never auto-admin)
932
- if (config.defaultRole) {
933
- await authRepo.assignDefaultRole(user.id, config.defaultRole);
934
- }
935
-
936
- const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
937
- user.id,
938
- c.req.header("user-agent") || "unknown",
939
- c.req.header("x-forwarded-for") || "unknown"
940
- );
941
-
942
- // Fire afterUserCreate hook
943
- if (authHooks?.afterUserCreate) {
944
- authHooks.afterUserCreate(user).catch(err => {
945
- console.error("[AuthHooks] afterUserCreate error:", err instanceof Error ? err.message : err);
946
- });
947
- }
948
-
949
- // Fire onAuthenticated hook
950
- if (authHooks?.onAuthenticated) {
951
- authHooks.onAuthenticated(user, "anonymous").catch(err => {
952
- console.error("[AuthHooks] onAuthenticated error:", err instanceof Error ? err.message : err);
953
- });
954
- }
955
-
956
- return c.json(buildAuthResponse(user, roleIds, accessToken, refreshToken), 201);
957
- });
958
-
959
- /**
960
- * POST /auth/anonymous/link
961
- * Upgrade an anonymous user to a permanent account with email/password
962
- */
963
- router.post("/anonymous/link", requireAuth, async (c) => {
964
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
965
- if (!userCtx) {
966
- throw ApiError.unauthorized("Not authenticated");
967
- }
968
-
969
- const user = await authRepo.getUserById(userCtx.userId);
970
- if (!user?.isAnonymous) {
971
- throw ApiError.badRequest("User is not anonymous", "NOT_ANONYMOUS");
972
- }
973
-
974
- const linkSchema = z.object({
975
- email: z.string().email("Invalid email address").max(255),
976
- password: z.string().min(1, "Password is required").max(128)
977
- });
978
- const { email, password } = parseBody(linkSchema, await c.req.json());
979
-
980
- // Validate password strength
981
- const passwordValidation = ops.validatePasswordStrength(password);
982
- if (!passwordValidation.valid) {
983
- throw ApiError.badRequest(passwordValidation.errors.join(". "), "WEAK_PASSWORD");
984
- }
985
-
986
- // Check if email is already taken
987
- const existingUser = await authRepo.getUserByEmail(email.toLowerCase());
988
- if (existingUser) {
989
- throw ApiError.conflict("Email already registered", "EMAIL_EXISTS");
990
- }
991
-
992
- // Hash password
993
- const passwordHash = await ops.hashPassword(password);
994
-
995
- // Update user: set email, password, remove anonymous flag
996
- const updatedUser = await authRepo.updateUser(user.id, {
997
- email: email.toLowerCase(),
998
- passwordHash,
999
- isAnonymous: false
1000
- });
1001
-
1002
- if (!updatedUser) {
1003
- throw ApiError.notFound("User not found");
1004
- }
1005
-
1006
- // Generate new tokens with updated identity
1007
- const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
1008
- user.id,
1009
- c.req.header("user-agent") || "unknown",
1010
- c.req.header("x-forwarded-for") || "unknown"
1011
- );
1012
-
1013
- return c.json(buildAuthResponse(updatedUser, roleIds, accessToken, refreshToken));
718
+ mountSessionRoutes({
719
+ router,
720
+ config,
721
+ ops,
722
+ parseBody,
723
+ buildAuthResponse,
724
+ createSessionAndTokens
1014
725
  });
1015
726
 
1016
727
  // ═══════════════════════════════════════════════════════════════════════
1017
728
  // MFA / TOTP
1018
729
  // ═══════════════════════════════════════════════════════════════════════
1019
-
1020
- /**
1021
- * POST /auth/mfa/enroll
1022
- * Start MFA enrollment: generate TOTP secret and recovery codes
1023
- */
1024
- router.post("/mfa/enroll", requireAuth, async (c) => {
1025
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1026
- if (!userCtx) {
1027
- throw ApiError.unauthorized("Not authenticated");
1028
- }
1029
-
1030
- const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
1031
- const friendlyName = typeof body.friendlyName === "string" ? body.friendlyName : undefined;
1032
- const issuer = typeof body.issuer === "string" ? body.issuer : (emailConfig?.appName || "Rebase");
1033
-
1034
- // Get user for account name
1035
- const user = await authRepo.getUserById(userCtx.userId);
1036
- if (!user) {
1037
- throw ApiError.notFound("User not found");
1038
- }
1039
-
1040
- // Generate TOTP secret
1041
- const { secret, uri } = generateTotpSecret(issuer, user.email);
1042
-
1043
- // Store the factor (unverified until user confirms with a valid code)
1044
- const factor = await authRepo.createMfaFactor(
1045
- user.id,
1046
- "totp",
1047
- secret, // In production, encrypt this before storage
1048
- friendlyName
1049
- );
1050
-
1051
- // Generate recovery codes
1052
- const codes = generateRecoveryCodes(10);
1053
- const codeHashes = codes.map(hashRecoveryCode);
1054
- await authRepo.createRecoveryCodes(user.id, codeHashes);
1055
-
1056
- return c.json({
1057
- factor: {
1058
- id: factor.id,
1059
- factorType: factor.factorType,
1060
- friendlyName: factor.friendlyName
1061
- },
1062
- totp: {
1063
- secret,
1064
- uri,
1065
- qrUri: uri // Client can use a QR library to render this
1066
- },
1067
- recoveryCodes: codes
1068
- }, 201);
1069
- });
1070
-
1071
- /**
1072
- * POST /auth/mfa/verify
1073
- * Verify TOTP code to complete MFA enrollment
1074
- */
1075
- router.post("/mfa/verify", requireAuth, async (c) => {
1076
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1077
- if (!userCtx) {
1078
- throw ApiError.unauthorized("Not authenticated");
1079
- }
1080
-
1081
- const verifySchema = z.object({
1082
- factorId: z.string().min(1, "Factor ID is required"),
1083
- code: z.string().length(6, "Code must be 6 digits")
1084
- });
1085
- const { factorId, code } = parseBody(verifySchema, await c.req.json());
1086
-
1087
- // Get the factor
1088
- const factor = await authRepo.getMfaFactorById(factorId);
1089
- if (!factor || factor.userId !== userCtx.userId) {
1090
- throw ApiError.notFound("MFA factor not found");
1091
- }
1092
-
1093
- if (factor.verified) {
1094
- throw ApiError.badRequest("Factor is already verified", "ALREADY_VERIFIED");
1095
- }
1096
-
1097
- // Verify the TOTP code
1098
- const secretBuffer = base32Decode(factor.secretEncrypted);
1099
- const isValid = verifyTotp(secretBuffer, code);
1100
-
1101
- if (!isValid) {
1102
- throw ApiError.unauthorized("Invalid TOTP code", "INVALID_CODE");
1103
- }
1104
-
1105
- // Mark factor as verified
1106
- await authRepo.verifyMfaFactor(factorId);
1107
-
1108
- return c.json({ success: true, message: "MFA factor verified and enrolled" });
1109
- });
1110
-
1111
- /**
1112
- * POST /auth/mfa/challenge
1113
- * Create an MFA challenge during login (user has MFA enrolled)
1114
- */
1115
- router.post("/mfa/challenge", requireAuth, async (c) => {
1116
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1117
- if (!userCtx) {
1118
- throw ApiError.unauthorized("Not authenticated");
1119
- }
1120
-
1121
- const challengeSchema = z.object({
1122
- factorId: z.string().min(1, "Factor ID is required")
1123
- });
1124
- const { factorId } = parseBody(challengeSchema, await c.req.json());
1125
-
1126
- // Verify the factor belongs to this user and is verified
1127
- const factor = await authRepo.getMfaFactorById(factorId);
1128
- if (!factor || factor.userId !== userCtx.userId) {
1129
- throw ApiError.notFound("MFA factor not found");
1130
- }
1131
-
1132
- if (!factor.verified) {
1133
- throw ApiError.badRequest("MFA factor is not yet verified", "FACTOR_NOT_VERIFIED");
1134
- }
1135
-
1136
- const ipAddress = c.req.header("x-forwarded-for") || "unknown";
1137
- const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);
1138
-
1139
- return c.json({
1140
- challengeId: challenge.id,
1141
- factorId: challenge.factorId,
1142
- expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
1143
- });
1144
- });
1145
-
1146
- /**
1147
- * POST /auth/mfa/challenge/verify
1148
- * Verify a TOTP code for an active challenge, upgrade aal1 → aal2
1149
- */
1150
- router.post("/mfa/challenge/verify", requireAuth, async (c) => {
1151
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1152
- if (!userCtx) {
1153
- throw ApiError.unauthorized("Not authenticated");
1154
- }
1155
-
1156
- const challengeVerifySchema = z.object({
1157
- challengeId: z.string().min(1, "Challenge ID is required"),
1158
- code: z.string().min(1, "Code is required")
1159
- });
1160
- const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());
1161
-
1162
- // Find the challenge
1163
- const challenge = await authRepo.getMfaChallengeById(challengeId);
1164
- if (!challenge) {
1165
- throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
1166
- }
1167
-
1168
- // Get the factor and verify ownership
1169
- const factor = await authRepo.getMfaFactorById(challenge.factorId);
1170
- if (!factor || factor.userId !== userCtx.userId) {
1171
- throw ApiError.notFound("MFA factor not found");
1172
- }
1173
-
1174
- // Check if this is a recovery code (10 chars with hyphen) or TOTP (6 digits)
1175
- const isRecoveryCode = code.length > 6;
1176
- let isValid = false;
1177
-
1178
- if (isRecoveryCode) {
1179
- // Try recovery code
1180
- const codeHash = hashRecoveryCode(code);
1181
- isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
1182
- } else {
1183
- // Verify TOTP
1184
- const secretBuffer = base32Decode(factor.secretEncrypted);
1185
- isValid = verifyTotp(secretBuffer, code);
1186
- }
1187
-
1188
- if (!isValid) {
1189
- throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
1190
- }
1191
-
1192
- // Mark challenge as verified
1193
- await authRepo.verifyMfaChallenge(challengeId);
1194
-
1195
- // Generate new access token with aal2
1196
- const roles = await authRepo.getUserRoles(userCtx.userId);
1197
- const roleIds = roles.map(r => r.id);
1198
- const accessToken = generateAccessToken(userCtx.userId, roleIds, "aal2");
1199
- const refreshToken = generateRefreshToken();
1200
-
1201
- // Create new refresh token
1202
- await authRepo.createRefreshToken(
1203
- userCtx.userId,
1204
- hashRefreshToken(refreshToken),
1205
- getRefreshTokenExpiry(),
1206
- c.req.header("user-agent") || "unknown",
1207
- c.req.header("x-forwarded-for") || "unknown"
1208
- );
1209
-
1210
- // Fire onMfaVerified hook
1211
- if (authHooks?.onMfaVerified) {
1212
- authHooks.onMfaVerified(userCtx.userId, factor.id).catch(err => {
1213
- console.error("[AuthHooks] onMfaVerified error:", err instanceof Error ? err.message : err);
1214
- });
1215
- }
1216
-
1217
- return c.json({
1218
- tokens: {
1219
- accessToken,
1220
- refreshToken,
1221
- accessTokenExpiresAt: getAccessTokenExpiry()
1222
- }
1223
- });
1224
- });
1225
-
1226
- /**
1227
- * GET /auth/mfa/factors
1228
- * List enrolled MFA factors for the current user
1229
- */
1230
- router.get("/mfa/factors", requireAuth, async (c) => {
1231
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1232
- if (!userCtx) {
1233
- throw ApiError.unauthorized("Not authenticated");
1234
- }
1235
-
1236
- const factors = await authRepo.getMfaFactors(userCtx.userId);
1237
- return c.json({
1238
- factors: factors.map(f => ({
1239
- id: f.id,
1240
- factorType: f.factorType,
1241
- friendlyName: f.friendlyName,
1242
- verified: f.verified,
1243
- createdAt: f.createdAt
1244
- }))
1245
- });
1246
- });
1247
-
1248
- /**
1249
- * DELETE /auth/mfa/unenroll
1250
- * Remove an MFA factor
1251
- */
1252
- router.delete("/mfa/unenroll", requireAuth, async (c) => {
1253
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
1254
- if (!userCtx) {
1255
- throw ApiError.unauthorized("Not authenticated");
1256
- }
1257
-
1258
- const unenrollSchema = z.object({
1259
- factorId: z.string().min(1, "Factor ID is required")
1260
- });
1261
- const { factorId } = parseBody(unenrollSchema, await c.req.json());
1262
-
1263
- // Verify ownership
1264
- const factor = await authRepo.getMfaFactorById(factorId);
1265
- if (!factor || factor.userId !== userCtx.userId) {
1266
- throw ApiError.notFound("MFA factor not found");
1267
- }
1268
-
1269
- await authRepo.deleteMfaFactor(factorId, userCtx.userId);
1270
-
1271
- // If no more verified factors, clean up recovery codes
1272
- const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.userId);
1273
- if (!hasFactors) {
1274
- await authRepo.deleteAllRecoveryCodes(userCtx.userId);
1275
- }
1276
-
1277
- return c.json({ success: true, message: "MFA factor removed" });
1278
- });
730
+ mountMfaRoutes(router, config, ops, parseBody);
1279
731
 
1280
732
  return router;
1281
733
  }