@rebasepro/server-core 0.7.0 → 0.9.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 (300) hide show
  1. package/README.md +2 -2
  2. package/dist/api/ast-schema-editor.d.ts +21 -0
  3. package/dist/api/collections_for_test/callbacks_test_collection.d.ts +2 -0
  4. package/dist/api/errors.d.ts +53 -0
  5. package/dist/api/index.d.ts +7 -0
  6. package/dist/api/logs-routes.d.ts +37 -0
  7. package/dist/api/openapi-generator.d.ts +16 -0
  8. package/dist/api/rest/api-generator.d.ts +68 -0
  9. package/{src/api/rest/index.ts → dist/api/rest/index.d.ts} +0 -1
  10. package/dist/api/rest/query-parser.d.ts +6 -0
  11. package/dist/api/schema-editor-routes.d.ts +3 -0
  12. package/dist/api/types.d.ts +56 -0
  13. package/dist/auth/adapter-middleware.d.ts +43 -0
  14. package/dist/auth/admin-roles-route.d.ts +18 -0
  15. package/dist/auth/admin-user-ops.d.ts +79 -0
  16. package/dist/auth/admin-users-route.d.ts +28 -0
  17. package/dist/auth/api-keys/api-key-middleware.d.ts +39 -0
  18. package/{src/auth/api-keys/api-key-permission-guard.ts → dist/auth/api-keys/api-key-permission-guard.d.ts} +2 -34
  19. package/dist/auth/api-keys/api-key-routes.d.ts +20 -0
  20. package/dist/auth/api-keys/api-key-store.d.ts +35 -0
  21. package/{src/auth/api-keys/api-key-types.ts → dist/auth/api-keys/api-key-types.d.ts} +0 -6
  22. package/{src/auth/api-keys/index.ts → dist/auth/api-keys/index.d.ts} +2 -22
  23. package/dist/auth/apple-oauth.d.ts +30 -0
  24. package/{src/auth/auth-hooks.ts → dist/auth/auth-hooks.d.ts} +15 -73
  25. package/dist/auth/bitbucket-oauth.d.ts +11 -0
  26. package/dist/auth/builtin-auth-adapter.d.ts +55 -0
  27. package/dist/auth/cookie-utils.d.ts +32 -0
  28. package/{src/auth/crypto-utils.ts → dist/auth/crypto-utils.d.ts} +1 -16
  29. package/dist/auth/custom-auth-adapter.d.ts +39 -0
  30. package/dist/auth/discord-oauth.d.ts +14 -0
  31. package/dist/auth/facebook-oauth.d.ts +14 -0
  32. package/dist/auth/github-oauth.d.ts +15 -0
  33. package/dist/auth/gitlab-oauth.d.ts +13 -0
  34. package/dist/auth/google-oauth.d.ts +47 -0
  35. package/{src/auth/index.ts → dist/auth/index.d.ts} +4 -22
  36. package/{src/auth/interfaces.ts → dist/auth/interfaces.d.ts} +20 -89
  37. package/dist/auth/jwt.d.ts +67 -0
  38. package/dist/auth/linkedin-oauth.d.ts +18 -0
  39. package/dist/auth/magic-link-routes.d.ts +32 -0
  40. package/dist/auth/mfa-crypto.d.ts +23 -0
  41. package/dist/auth/mfa-routes.d.ts +7 -0
  42. package/dist/auth/mfa.d.ts +49 -0
  43. package/dist/auth/microsoft-oauth.d.ts +16 -0
  44. package/dist/auth/middleware.d.ts +158 -0
  45. package/dist/auth/password.d.ts +22 -0
  46. package/dist/auth/rate-limiter.d.ts +50 -0
  47. package/dist/auth/reset-password-admin.d.ts +29 -0
  48. package/dist/auth/rls-scope.d.ts +31 -0
  49. package/dist/auth/routes.d.ts +66 -0
  50. package/dist/auth/session-routes.d.ts +29 -0
  51. package/dist/auth/slack-oauth.d.ts +12 -0
  52. package/dist/auth/spotify-oauth.d.ts +12 -0
  53. package/dist/auth/twitter-oauth.d.ts +18 -0
  54. package/dist/collections/BackendCollectionRegistry.d.ts +13 -0
  55. package/dist/collections/loader.d.ts +5 -0
  56. package/dist/cron/cron-loader.d.ts +18 -0
  57. package/dist/cron/cron-routes.d.ts +14 -0
  58. package/dist/cron/cron-scheduler.d.ts +106 -0
  59. package/dist/cron/cron-store.d.ts +32 -0
  60. package/dist/cron/define-cron.d.ts +28 -0
  61. package/{src/cron/index.ts → dist/cron/index.d.ts} +1 -0
  62. package/dist/db/interfaces.d.ts +18 -0
  63. package/dist/dynamic-import-Dvh-K5fl.js +22 -0
  64. package/dist/dynamic-import-Dvh-K5fl.js.map +1 -0
  65. package/dist/email/index.d.ts +6 -0
  66. package/dist/email/smtp-email-service.d.ts +30 -0
  67. package/dist/email/templates.d.ts +50 -0
  68. package/{src/email/types.ts → dist/email/types.d.ts} +40 -36
  69. package/dist/env.d.ts +102 -0
  70. package/dist/functions/define-function.d.ts +55 -0
  71. package/dist/functions/function-loader.d.ts +18 -0
  72. package/dist/functions/function-routes.d.ts +10 -0
  73. package/{src/functions/index.ts → dist/functions/index.d.ts} +2 -0
  74. package/dist/history/history-routes.d.ts +23 -0
  75. package/{src/history/index.ts → dist/history/index.d.ts} +0 -1
  76. package/dist/index.d.ts +35 -0
  77. package/dist/index.es.js +18184 -0
  78. package/dist/index.es.js.map +1 -0
  79. package/dist/index.umd.js +48629 -0
  80. package/dist/index.umd.js.map +1 -0
  81. package/dist/init/docs.d.ts +4 -0
  82. package/dist/init/health.d.ts +2 -0
  83. package/dist/init/middlewares.d.ts +10 -0
  84. package/dist/init/shutdown.d.ts +65 -0
  85. package/dist/init/storage.d.ts +5 -0
  86. package/dist/init.d.ts +344 -0
  87. package/dist/jwt-BwIn8xmk.js +4200 -0
  88. package/dist/jwt-BwIn8xmk.js.map +1 -0
  89. package/dist/logger-BYU66ENZ.js +94 -0
  90. package/dist/logger-BYU66ENZ.js.map +1 -0
  91. package/dist/ms-DnYXB-Wd.js +162 -0
  92. package/dist/ms-DnYXB-Wd.js.map +1 -0
  93. package/dist/openapi-generator-Z9oYWLf_.js +586 -0
  94. package/dist/openapi-generator-Z9oYWLf_.js.map +1 -0
  95. package/dist/schema-editor-routes-D3ef8zu1.js +219 -0
  96. package/dist/schema-editor-routes-D3ef8zu1.js.map +1 -0
  97. package/dist/serve-spa.d.ts +36 -0
  98. package/dist/services/driver-registry.d.ts +78 -0
  99. package/dist/services/routed-realtime-service.d.ts +43 -0
  100. package/dist/services/webhook-service.d.ts +29 -0
  101. package/dist/singleton.d.ts +51 -0
  102. package/dist/src-B4OLmNVa.js +437 -0
  103. package/dist/src-B4OLmNVa.js.map +1 -0
  104. package/dist/src-B5WkP1Rv.js +24778 -0
  105. package/dist/src-B5WkP1Rv.js.map +1 -0
  106. package/dist/storage/GCSStorageController.d.ts +43 -0
  107. package/dist/storage/LocalStorageController.d.ts +46 -0
  108. package/dist/storage/S3StorageController.d.ts +40 -0
  109. package/dist/storage/image-transform.d.ts +50 -0
  110. package/{src/storage/index.ts → dist/storage/index.d.ts} +6 -21
  111. package/dist/storage/routes.d.ts +70 -0
  112. package/dist/storage/storage-registry.d.ts +78 -0
  113. package/dist/storage/tus-handler.d.ts +53 -0
  114. package/{src/storage/types.ts → dist/storage/types.d.ts} +32 -38
  115. package/dist/types/index.d.ts +11 -0
  116. package/dist/utils/dev-port.d.ts +45 -0
  117. package/dist/utils/dynamic-import.d.ts +25 -0
  118. package/dist/utils/logger.d.ts +31 -0
  119. package/dist/utils/logging.d.ts +9 -0
  120. package/dist/utils/request-id.d.ts +4 -0
  121. package/dist/utils/request-logger.d.ts +19 -0
  122. package/{src/utils/sql.ts → dist/utils/sql.d.ts} +5 -16
  123. package/package.json +49 -22
  124. package/coverage/clover.xml +0 -3739
  125. package/coverage/coverage-final.json +0 -31
  126. package/coverage/lcov-report/base.css +0 -224
  127. package/coverage/lcov-report/block-navigation.js +0 -87
  128. package/coverage/lcov-report/favicon.png +0 -0
  129. package/coverage/lcov-report/index.html +0 -266
  130. package/coverage/lcov-report/prettify.css +0 -1
  131. package/coverage/lcov-report/prettify.js +0 -2
  132. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  133. package/coverage/lcov-report/sorter.js +0 -210
  134. package/coverage/lcov-report/src/api/ast-schema-editor.ts.html +0 -952
  135. package/coverage/lcov-report/src/api/errors.ts.html +0 -472
  136. package/coverage/lcov-report/src/api/graphql/graphql-schema-generator.ts.html +0 -1069
  137. package/coverage/lcov-report/src/api/graphql/index.html +0 -116
  138. package/coverage/lcov-report/src/api/index.html +0 -176
  139. package/coverage/lcov-report/src/api/openapi-generator.ts.html +0 -565
  140. package/coverage/lcov-report/src/api/rest/api-generator.ts.html +0 -994
  141. package/coverage/lcov-report/src/api/rest/index.html +0 -131
  142. package/coverage/lcov-report/src/api/rest/query-parser.ts.html +0 -550
  143. package/coverage/lcov-report/src/api/schema-editor-routes.ts.html +0 -202
  144. package/coverage/lcov-report/src/api/server.ts.html +0 -823
  145. package/coverage/lcov-report/src/auth/admin-routes.ts.html +0 -973
  146. package/coverage/lcov-report/src/auth/index.html +0 -176
  147. package/coverage/lcov-report/src/auth/jwt.ts.html +0 -574
  148. package/coverage/lcov-report/src/auth/middleware.ts.html +0 -745
  149. package/coverage/lcov-report/src/auth/password.ts.html +0 -310
  150. package/coverage/lcov-report/src/auth/services.ts.html +0 -2074
  151. package/coverage/lcov-report/src/collections/index.html +0 -116
  152. package/coverage/lcov-report/src/collections/loader.ts.html +0 -232
  153. package/coverage/lcov-report/src/db/auth-schema.ts.html +0 -523
  154. package/coverage/lcov-report/src/db/data-transformer.ts.html +0 -1753
  155. package/coverage/lcov-report/src/db/entityService.ts.html +0 -700
  156. package/coverage/lcov-report/src/db/index.html +0 -146
  157. package/coverage/lcov-report/src/db/services/EntityFetchService.ts.html +0 -4048
  158. package/coverage/lcov-report/src/db/services/EntityPersistService.ts.html +0 -883
  159. package/coverage/lcov-report/src/db/services/RelationService.ts.html +0 -3121
  160. package/coverage/lcov-report/src/db/services/entity-helpers.ts.html +0 -442
  161. package/coverage/lcov-report/src/db/services/index.html +0 -176
  162. package/coverage/lcov-report/src/db/services/index.ts.html +0 -124
  163. package/coverage/lcov-report/src/generate-drizzle-schema-logic.ts.html +0 -1960
  164. package/coverage/lcov-report/src/index.html +0 -116
  165. package/coverage/lcov-report/src/services/driver-registry.ts.html +0 -631
  166. package/coverage/lcov-report/src/services/index.html +0 -131
  167. package/coverage/lcov-report/src/services/postgresDataDriver.ts.html +0 -3025
  168. package/coverage/lcov-report/src/storage/LocalStorageController.ts.html +0 -1189
  169. package/coverage/lcov-report/src/storage/S3StorageController.ts.html +0 -970
  170. package/coverage/lcov-report/src/storage/index.html +0 -161
  171. package/coverage/lcov-report/src/storage/storage-registry.ts.html +0 -646
  172. package/coverage/lcov-report/src/storage/types.ts.html +0 -451
  173. package/coverage/lcov-report/src/utils/drizzle-conditions.ts.html +0 -3082
  174. package/coverage/lcov-report/src/utils/index.html +0 -116
  175. package/coverage/lcov.info +0 -7179
  176. package/jest.config.cjs +0 -19
  177. package/src/api/ast-schema-editor.ts +0 -289
  178. package/src/api/collections_for_test/callbacks_test_collection.ts +0 -61
  179. package/src/api/errors.ts +0 -278
  180. package/src/api/graphql/graphql-schema-generator.ts +0 -383
  181. package/src/api/graphql/index.ts +0 -2
  182. package/src/api/index.ts +0 -11
  183. package/src/api/logs-routes.ts +0 -132
  184. package/src/api/openapi-generator.ts +0 -727
  185. package/src/api/rest/api-generator-count.test.ts +0 -126
  186. package/src/api/rest/api-generator.ts +0 -747
  187. package/src/api/rest/query-parser.ts +0 -300
  188. package/src/api/schema-editor-routes.ts +0 -41
  189. package/src/api/server.ts +0 -271
  190. package/src/api/types.ts +0 -104
  191. package/src/auth/adapter-middleware.ts +0 -114
  192. package/src/auth/admin-roles-route.ts +0 -36
  193. package/src/auth/admin-user-ops.ts +0 -236
  194. package/src/auth/admin-users-route.ts +0 -302
  195. package/src/auth/api-keys/api-key-middleware.ts +0 -133
  196. package/src/auth/api-keys/api-key-routes.ts +0 -193
  197. package/src/auth/api-keys/api-key-store.ts +0 -340
  198. package/src/auth/apple-oauth.ts +0 -122
  199. package/src/auth/bitbucket-oauth.ts +0 -84
  200. package/src/auth/builtin-auth-adapter.ts +0 -380
  201. package/src/auth/custom-auth-adapter.ts +0 -86
  202. package/src/auth/discord-oauth.ts +0 -85
  203. package/src/auth/facebook-oauth.ts +0 -74
  204. package/src/auth/github-oauth.ts +0 -112
  205. package/src/auth/gitlab-oauth.ts +0 -72
  206. package/src/auth/google-oauth.ts +0 -223
  207. package/src/auth/jwt.ts +0 -208
  208. package/src/auth/linkedin-oauth.ts +0 -83
  209. package/src/auth/magic-link-routes.ts +0 -167
  210. package/src/auth/mfa-crypto.ts +0 -91
  211. package/src/auth/mfa-routes.ts +0 -323
  212. package/src/auth/mfa.ts +0 -161
  213. package/src/auth/microsoft-oauth.ts +0 -90
  214. package/src/auth/middleware.ts +0 -369
  215. package/src/auth/password.ts +0 -77
  216. package/src/auth/rate-limiter.ts +0 -225
  217. package/src/auth/reset-password-admin.ts +0 -160
  218. package/src/auth/rls-scope.ts +0 -58
  219. package/src/auth/routes.ts +0 -802
  220. package/src/auth/session-routes.ts +0 -353
  221. package/src/auth/slack-oauth.ts +0 -73
  222. package/src/auth/spotify-oauth.ts +0 -69
  223. package/src/auth/twitter-oauth.ts +0 -128
  224. package/src/collections/BackendCollectionRegistry.ts +0 -20
  225. package/src/collections/loader.ts +0 -50
  226. package/src/cron/cron-loader.ts +0 -84
  227. package/src/cron/cron-routes.test.ts +0 -265
  228. package/src/cron/cron-routes.ts +0 -85
  229. package/src/cron/cron-scheduler.test.ts +0 -559
  230. package/src/cron/cron-scheduler.ts +0 -581
  231. package/src/cron/cron-store.ts +0 -166
  232. package/src/db/interfaces.ts +0 -60
  233. package/src/email/index.ts +0 -19
  234. package/src/email/smtp-email-service.ts +0 -123
  235. package/src/email/templates.ts +0 -470
  236. package/src/env.ts +0 -227
  237. package/src/functions/function-loader.ts +0 -116
  238. package/src/functions/function-routes.ts +0 -31
  239. package/src/history/history-routes.ts +0 -129
  240. package/src/index.ts +0 -70
  241. package/src/init/docs.ts +0 -47
  242. package/src/init/health.ts +0 -37
  243. package/src/init/middlewares.ts +0 -61
  244. package/src/init/shutdown.ts +0 -56
  245. package/src/init/storage.ts +0 -57
  246. package/src/init.ts +0 -1041
  247. package/src/serve-spa.ts +0 -89
  248. package/src/services/driver-registry.ts +0 -183
  249. package/src/services/routed-realtime-service.ts +0 -113
  250. package/src/services/webhook-service.ts +0 -155
  251. package/src/singleton.ts +0 -70
  252. package/src/storage/LocalStorageController.ts +0 -389
  253. package/src/storage/S3StorageController.ts +0 -300
  254. package/src/storage/image-transform.ts +0 -218
  255. package/src/storage/routes.ts +0 -368
  256. package/src/storage/storage-registry.ts +0 -188
  257. package/src/storage/tus-handler.ts +0 -315
  258. package/src/types/index.ts +0 -27
  259. package/src/utils/dev-port.ts +0 -196
  260. package/src/utils/logger.ts +0 -143
  261. package/src/utils/logging.ts +0 -40
  262. package/src/utils/request-id.ts +0 -40
  263. package/src/utils/request-logger.ts +0 -72
  264. package/test/api-generator.test.ts +0 -588
  265. package/test/api-key-permission-guard.test.ts +0 -144
  266. package/test/ast-schema-editor.test.ts +0 -89
  267. package/test/auth-middleware-hono.test.ts +0 -556
  268. package/test/auth-routes.test.ts +0 -1098
  269. package/test/backend-hooks-data.test.ts +0 -477
  270. package/test/custom-auth-adapter.test.ts +0 -189
  271. package/test/driver-registry.test.ts +0 -282
  272. package/test/email-templates.test.ts +0 -174
  273. package/test/env.test.ts +0 -128
  274. package/test/error-propagation.test.ts +0 -226
  275. package/test/errors-hono.test.ts +0 -133
  276. package/test/errors.test.ts +0 -155
  277. package/test/function-loader.test.ts +0 -127
  278. package/test/graphql-schema-generator.test.ts +0 -554
  279. package/test/jwt-security.test.ts +0 -182
  280. package/test/jwt.test.ts +0 -326
  281. package/test/mfa.test.ts +0 -197
  282. package/test/middleware.test.ts +0 -305
  283. package/test/multi-datasource-routing.test.ts +0 -113
  284. package/test/password.test.ts +0 -165
  285. package/test/query-parser.test.ts +0 -233
  286. package/test/rate-limiter.test.ts +0 -102
  287. package/test/reset-password-admin.test.ts +0 -113
  288. package/test/routed-realtime-service.test.ts +0 -86
  289. package/test/safe-compare.test.ts +0 -66
  290. package/test/singleton.test.ts +0 -59
  291. package/test/smtp-email-service.test.ts +0 -169
  292. package/test/storage-local.test.ts +0 -271
  293. package/test/storage-registry.test.ts +0 -282
  294. package/test/storage-routes.test.ts +0 -222
  295. package/test/storage-s3.test.ts +0 -304
  296. package/test/transform-auth-response.test.ts +0 -305
  297. package/test/webhook-service.test.ts +0 -260
  298. package/tsconfig.json +0 -54
  299. package/tsconfig.prod.json +0 -23
  300. package/vite.config.ts +0 -86
@@ -1,83 +0,0 @@
1
- import type { OAuthProvider, OAuthProviderProfile } from "./interfaces";
2
- import { z } from "zod";
3
- import { logger } from "../utils/logger";
4
-
5
- export interface LinkedinUserInfo {
6
- linkedinId: string;
7
- email: string;
8
- displayName: string | null;
9
- photoUrl: string | null;
10
- emailVerified: boolean;
11
- }
12
-
13
- /**
14
- * Creates a LinkedIn OAuth Provider integration
15
- */
16
- export function createLinkedinProvider(config: { clientId: string, clientSecret: string }): OAuthProvider<{ code: string; redirectUri: string }> {
17
- return {
18
- id: "linkedin",
19
- schema: z.object({
20
- code: z.string().min(1, "Auth code is required"),
21
- redirectUri: z.string().url("Valid redirect URI is required")
22
- }),
23
- verify: async (payload: { code: string; redirectUri: string }): Promise<OAuthProviderProfile | null> => {
24
- try {
25
- // Exchange code for access token
26
- const tokenResponse = await fetch("https://www.linkedin.com/oauth/v2/accessToken", {
27
- method: "POST",
28
- headers: {
29
- "Content-Type": "application/x-www-form-urlencoded"
30
- },
31
- body: new URLSearchParams({
32
- grant_type: "authorization_code",
33
- code: payload.code,
34
- redirect_uri: payload.redirectUri,
35
- client_id: config.clientId,
36
- client_secret: config.clientSecret
37
- })
38
- });
39
-
40
- if (!tokenResponse.ok) {
41
- const errBody = await tokenResponse.text();
42
- logger.error("Failed to get LinkedIn access token", { detail: errBody });
43
- return null;
44
- }
45
-
46
- const tokenData = await tokenResponse.json() as { access_token: string; id_token?: string };
47
- const accessToken = tokenData.access_token;
48
-
49
- // Fetch User Info using OIDC userinfo endpoint
50
- const profileResponse = await fetch("https://api.linkedin.com/v2/userinfo", {
51
- headers: {
52
- "Authorization": `Bearer ${accessToken}`
53
- }
54
- });
55
-
56
- if (!profileResponse.ok) {
57
- const errBody = await profileResponse.text();
58
- logger.error("Failed to get LinkedIn user info", { detail: errBody });
59
- return null;
60
- }
61
-
62
- const profileData = await profileResponse.json() as {
63
- sub: string;
64
- email: string;
65
- name?: string;
66
- picture?: string;
67
- email_verified?: boolean;
68
- };
69
-
70
- return {
71
- providerId: profileData.sub,
72
- email: profileData.email,
73
- displayName: profileData.name || null,
74
- photoUrl: profileData.picture || null,
75
- emailVerified: profileData.email_verified === true
76
- };
77
- } catch (error) {
78
- logger.error("LinkedIn OAuth error", { error: error });
79
- return null;
80
- }
81
- }
82
- };
83
- }
@@ -1,167 +0,0 @@
1
- import { Hono } from "hono";
2
- import type { AuthModuleConfig } from "./routes";
3
- import type { ResolvedAuthHooks } from "./auth-hooks";
4
- import type { HonoEnv } from "../api/types";
5
- import { ApiError } from "../api/errors";
6
- import { generateSecureToken, hashToken } from "./admin-user-ops";
7
- import { getMagicLinkTemplate } from "../email/templates";
8
- import { strictAuthLimiter } from "./rate-limiter";
9
- import { z } from "zod";
10
- import { logger } from "../utils/logger";
11
- import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
12
-
13
- /**
14
- * Magic link token expiry (15 minutes from now)
15
- */
16
- function getMagicLinkExpiry(): Date {
17
- return new Date(Date.now() + 15 * 60 * 1000);
18
- }
19
-
20
- /**
21
- * Mount magic link routes onto the auth router.
22
- *
23
- * Follows the same delegation pattern as `mountMfaRoutes()` and `mountSessionRoutes()`.
24
- */
25
- export function mountMagicLinkRoutes(deps: {
26
- router: Hono<HonoEnv>;
27
- config: AuthModuleConfig;
28
- ops: ResolvedAuthHooks;
29
- parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T;
30
- buildAuthResponse: (
31
- user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; metadata?: Record<string, unknown> | null },
32
- roleIds: string[],
33
- accessToken: string,
34
- refreshToken: string
35
- ) => unknown;
36
- createSessionAndTokens: (userId: string, userAgent: string, ipAddress: string) => Promise<{ roleIds: string[]; accessToken: string; refreshToken: string }>;
37
- applyTransformHook: (
38
- response: AuthResponsePayload,
39
- method: TransformAuthResponseContext["method"],
40
- request: Request,
41
- userId: string
42
- ) => Promise<AuthResponsePayload>;
43
- }) {
44
- const { router, config, ops, parseBody, buildAuthResponse, createSessionAndTokens, applyTransformHook } = deps;
45
- const { authRepo, emailService, emailConfig } = config;
46
-
47
- const magicLinkSchema = z.object({
48
- email: z.string().email("Invalid email address").max(255)
49
- });
50
-
51
- const verifyMagicLinkSchema = z.object({
52
- token: z.string().min(1, "Token is required")
53
- });
54
-
55
- function isEmailConfigured(): boolean {
56
- return !!(emailService && emailService.isConfigured());
57
- }
58
-
59
- /**
60
- * POST /auth/magic-link
61
- * Request a magic link email
62
- */
63
- router.post("/magic-link", strictAuthLimiter, async (c) => {
64
- const { email } = parseBody(magicLinkSchema, await c.req.json());
65
-
66
- // Require email service
67
- if (!isEmailConfigured()) {
68
- throw ApiError.serviceUnavailable("Email service not configured. Magic link login is not available.", "EMAIL_NOT_CONFIGURED");
69
- }
70
-
71
- // Always return success (security: don't reveal if email exists)
72
- const user = await authRepo.getUserByEmail(email);
73
-
74
- if (user) {
75
- // Call beforeLogin hook if provided (throw to reject)
76
- if (ops.beforeLogin) {
77
- await ops.beforeLogin(email, "magic-link");
78
- }
79
-
80
- // Generate magic link token
81
- const token = generateSecureToken();
82
- const tokenHash = hashToken(token);
83
- const expiresAt = getMagicLinkExpiry();
84
-
85
- await authRepo.createMagicLinkToken(user.id, tokenHash, expiresAt);
86
-
87
- // Build magic link URL
88
- const baseUrl = emailConfig?.magicLinkUrl || emailConfig?.resetPasswordUrl || "";
89
- const magicLinkUrl = `${baseUrl}/auth/magic-link?token=${token}`;
90
-
91
- // Get email template
92
- const appName = emailConfig?.appName || "Rebase";
93
- const templateFn = emailConfig?.templates?.magicLink;
94
- const emailContent = templateFn
95
- ? templateFn(magicLinkUrl, { email: user.email, displayName: user.displayName })
96
- : getMagicLinkTemplate(magicLinkUrl, { email: user.email, displayName: user.displayName }, appName);
97
-
98
- // Send email
99
- try {
100
- await emailService!.send({
101
- to: user.email,
102
- subject: emailContent.subject,
103
- html: emailContent.html,
104
- text: emailContent.text
105
- });
106
- } catch (emailError: unknown) {
107
- logger.error("Failed to send magic link email", { error: emailError instanceof Error ? emailError.message : emailError });
108
- // Don't reveal email sending failure to client
109
- }
110
- }
111
-
112
- // Always return success
113
- return c.json({
114
- success: true,
115
- message: "If an account with that email exists, a magic link has been sent."
116
- });
117
- });
118
-
119
- /**
120
- * POST /auth/magic-link/verify
121
- * Verify magic link token and create session
122
- */
123
- router.post("/magic-link/verify", strictAuthLimiter, async (c) => {
124
- const { token } = parseBody(verifyMagicLinkSchema, await c.req.json());
125
-
126
- // Find valid token
127
- const tokenHash = hashToken(token);
128
- const storedToken = await authRepo.findValidMagicLinkToken(tokenHash);
129
-
130
- if (!storedToken) {
131
- throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
132
- }
133
-
134
- // Mark token as used (one-time use)
135
- await authRepo.markMagicLinkTokenUsed(tokenHash);
136
-
137
- // Get user
138
- const user = await authRepo.getUserById(storedToken.userId);
139
- if (!user) {
140
- throw ApiError.badRequest("Invalid or expired magic link", "INVALID_TOKEN");
141
- }
142
-
143
- // Clicking a magic link proves email ownership — auto-verify
144
- if (!user.emailVerified) {
145
- await authRepo.setEmailVerified(user.id, true);
146
- user.emailVerified = true;
147
- }
148
-
149
- // Create session
150
- const { roleIds, accessToken, refreshToken } = await createSessionAndTokens(
151
- user.id,
152
- c.req.header("user-agent") || "unknown",
153
- c.req.header("x-forwarded-for") || "unknown"
154
- );
155
-
156
- // Fire onAuthenticated hook (fire-and-forget)
157
- if (ops.onAuthenticated) {
158
- ops.onAuthenticated(user, "magic-link").catch(err => {
159
- logger.error("[AuthHooks] onAuthenticated error", { error: err instanceof Error ? err.message : err });
160
- });
161
- }
162
-
163
- const authResponse = buildAuthResponse(user, roleIds, accessToken, refreshToken) as AuthResponsePayload;
164
- const finalResponse = await applyTransformHook(authResponse, "magic-link", c.req.raw, user.id);
165
- return c.json(finalResponse);
166
- });
167
- }
@@ -1,91 +0,0 @@
1
- /**
2
- * Envelope encryption for TOTP secrets using AES-256-GCM.
3
- *
4
- * - Derives a 32-byte key via SHA-256 from `MFA_ENCRYPTION_KEY` or `JWT_SECRET`.
5
- * - Generates a random 12-byte IV per encryption call.
6
- * - Returns ciphertexts in the format `iv_hex:authTag_hex:ciphertext_hex`.
7
- *
8
- * @module
9
- */
10
-
11
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
12
- import { logger } from "../utils/logger";
13
-
14
- const ALGORITHM = "aes-256-gcm";
15
- const IV_BYTES = 12;
16
- const AUTH_TAG_LENGTH = 16;
17
-
18
- /**
19
- * Resolve the raw key string from environment variables.
20
- *
21
- * Prefers `MFA_ENCRYPTION_KEY`; falls back to `JWT_SECRET` with a warning.
22
- * Throws if neither is set.
23
- */
24
- function resolveKeyString(): string {
25
- const explicit = process.env.MFA_ENCRYPTION_KEY;
26
- if (explicit) {
27
- return explicit;
28
- }
29
-
30
- const fallback = process.env.JWT_SECRET;
31
- if (fallback) {
32
- logger.warn(
33
- "[MFA-Crypto] MFA_ENCRYPTION_KEY is not set — falling back to JWT_SECRET. " +
34
- "Set a dedicated MFA_ENCRYPTION_KEY for production."
35
- );
36
- return fallback;
37
- }
38
-
39
- throw new Error(
40
- "Cannot encrypt TOTP secret: neither MFA_ENCRYPTION_KEY nor JWT_SECRET is configured."
41
- );
42
- }
43
-
44
- /**
45
- * Derive a deterministic 32-byte AES-256 key from the raw secret string.
46
- */
47
- function deriveKey(raw: string): Buffer {
48
- return createHash("sha256").update(raw).digest();
49
- }
50
-
51
- /**
52
- * Encrypt a plaintext TOTP secret.
53
- *
54
- * @param plaintext - The Base32 TOTP secret to encrypt.
55
- * @returns A string in the format `iv_hex:authTag_hex:ciphertext_hex`.
56
- */
57
- export function encryptTotpSecret(plaintext: string): string {
58
- const key = deriveKey(resolveKeyString());
59
- const iv = randomBytes(IV_BYTES);
60
-
61
- const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
62
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
63
- const authTag = cipher.getAuthTag();
64
-
65
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted.toString("hex")}`;
66
- }
67
-
68
- /**
69
- * Decrypt a previously encrypted TOTP secret.
70
- *
71
- * @param ciphertext - A string in the format `iv_hex:authTag_hex:ciphertext_hex`.
72
- * @returns The original Base32 TOTP secret.
73
- */
74
- export function decryptTotpSecret(ciphertext: string): string {
75
- const parts = ciphertext.split(":");
76
- if (parts.length !== 3) {
77
- throw new Error("Invalid encrypted TOTP secret format — expected iv:authTag:ciphertext");
78
- }
79
-
80
- const [ivHex, authTagHex, encryptedHex] = parts;
81
- const key = deriveKey(resolveKeyString());
82
- const iv = Buffer.from(ivHex, "hex");
83
- const authTag = Buffer.from(authTagHex, "hex");
84
- const encrypted = Buffer.from(encryptedHex, "hex");
85
-
86
- const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
87
- decipher.setAuthTag(authTag);
88
-
89
- const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
90
- return decrypted.toString("utf8");
91
- }
@@ -1,323 +0,0 @@
1
- import { Hono } from "hono";
2
- import { z } from "zod";
3
- import { ApiError } from "../api/errors";
4
- import { HonoEnv } from "../api/types";
5
- import { requireAuth } from "./middleware";
6
- import { logger } from "../utils/logger";
7
- import {
8
- generateTotpSecret,
9
- verifyTotp,
10
- base32Decode,
11
- generateRecoveryCodes,
12
- hashRecoveryCode
13
- } from "./mfa";
14
- import { encryptTotpSecret, decryptTotpSecret } from "./mfa-crypto";
15
- import {
16
- generateAccessToken,
17
- generateRefreshToken,
18
- hashRefreshToken,
19
- getRefreshTokenExpiry,
20
- getAccessTokenExpiry,
21
- type AccessTokenPayload
22
- } from "./jwt";
23
- import type { AuthModuleConfig } from "./routes";
24
- import { resolveAuthHooks } from "./auth-hooks";
25
- import type { AuthResponsePayload, TransformAuthResponseContext } from "@rebasepro/types";
26
-
27
- export function mountMfaRoutes(
28
- router: Hono<HonoEnv>,
29
- config: AuthModuleConfig,
30
- ops: ReturnType<typeof resolveAuthHooks>,
31
- parseBody: <T>(schema: z.ZodSchema<T>, body: unknown) => T,
32
- applyTransformHook?: (
33
- response: AuthResponsePayload,
34
- method: TransformAuthResponseContext["method"],
35
- request: Request,
36
- userId: string
37
- ) => Promise<AuthResponsePayload>
38
- ): void {
39
- const authRepo = config.authRepo;
40
- const emailConfig = config.emailConfig;
41
-
42
- /**
43
- * POST /auth/mfa/enroll
44
- * Start MFA enrollment: generate TOTP secret and recovery codes
45
- */
46
- router.post("/mfa/enroll", requireAuth, async (c) => {
47
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
48
- if (!userCtx) {
49
- throw ApiError.unauthorized("Not authenticated");
50
- }
51
-
52
- const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
53
- const friendlyName = typeof body.friendlyName === "string" ? body.friendlyName : undefined;
54
- const issuer = typeof body.issuer === "string" ? body.issuer : emailConfig?.appName || "Rebase";
55
-
56
- // Get user for account name
57
- const user = await authRepo.getUserById(userCtx.userId);
58
- if (!user) {
59
- throw ApiError.notFound("User not found");
60
- }
61
-
62
- // Generate TOTP secret
63
- const { secret, uri } = generateTotpSecret(issuer, user.email);
64
-
65
- // Encrypt the TOTP secret before storage (AES-256-GCM envelope encryption)
66
- const encryptedSecret = encryptTotpSecret(secret);
67
- const factor = await authRepo.createMfaFactor(
68
- user.id,
69
- "totp",
70
- encryptedSecret,
71
- friendlyName
72
- );
73
-
74
- // Generate recovery codes
75
- const codes = generateRecoveryCodes(10);
76
- const codeHashes = codes.map(hashRecoveryCode);
77
- await authRepo.createRecoveryCodes(user.id, codeHashes);
78
-
79
- return c.json(
80
- {
81
- factor: {
82
- id: factor.id,
83
- factorType: factor.factorType,
84
- friendlyName: factor.friendlyName
85
- },
86
- totp: {
87
- secret,
88
- uri,
89
- qrUri: uri
90
- },
91
- recoveryCodes: codes
92
- },
93
- 201
94
- );
95
- });
96
-
97
- /**
98
- * POST /auth/mfa/verify
99
- * Verify TOTP code to complete MFA enrollment
100
- */
101
- router.post("/mfa/verify", requireAuth, async (c) => {
102
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
103
- if (!userCtx) {
104
- throw ApiError.unauthorized("Not authenticated");
105
- }
106
-
107
- const verifySchema = z.object({
108
- factorId: z.string().min(1, "Factor ID is required"),
109
- code: z.string().length(6, "Code must be 6 digits")
110
- });
111
- const { factorId, code } = parseBody(verifySchema, await c.req.json());
112
-
113
- // Get the factor
114
- const factor = await authRepo.getMfaFactorById(factorId);
115
- if (!factor || factor.userId !== userCtx.userId) {
116
- throw ApiError.notFound("MFA factor not found");
117
- }
118
-
119
- if (factor.verified) {
120
- throw ApiError.badRequest("Factor is already verified", "ALREADY_VERIFIED");
121
- }
122
-
123
- // Decrypt and verify the TOTP code
124
- const decryptedSecret = decryptTotpSecret(factor.secretEncrypted);
125
- const secretBuffer = base32Decode(decryptedSecret);
126
- const isValid = verifyTotp(secretBuffer, code);
127
-
128
- if (!isValid) {
129
- throw ApiError.unauthorized("Invalid TOTP code", "INVALID_CODE");
130
- }
131
-
132
- // Mark factor as verified
133
- await authRepo.verifyMfaFactor(factorId);
134
-
135
- return c.json({
136
- success: true,
137
- message: "MFA factor verified and enrolled"
138
- });
139
- });
140
-
141
- /**
142
- * POST /auth/mfa/challenge
143
- * Create an MFA challenge during login (user has MFA enrolled)
144
- */
145
- router.post("/mfa/challenge", requireAuth, async (c) => {
146
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
147
- if (!userCtx) {
148
- throw ApiError.unauthorized("Not authenticated");
149
- }
150
-
151
- const challengeSchema = z.object({
152
- factorId: z.string().min(1, "Factor ID is required")
153
- });
154
- const { factorId } = parseBody(challengeSchema, await c.req.json());
155
-
156
- // Verify the factor belongs to this user and is verified
157
- const factor = await authRepo.getMfaFactorById(factorId);
158
- if (!factor || factor.userId !== userCtx.userId) {
159
- throw ApiError.notFound("MFA factor not found");
160
- }
161
-
162
- if (!factor.verified) {
163
- throw ApiError.badRequest("MFA factor is not yet verified", "FACTOR_NOT_VERIFIED");
164
- }
165
-
166
- const ipAddress = c.req.header("x-forwarded-for") || "unknown";
167
- const challenge = await authRepo.createMfaChallenge(factorId, ipAddress);
168
-
169
- return c.json({
170
- challengeId: challenge.id,
171
- factorId: challenge.factorId,
172
- expiresAt: new Date(Date.now() + 5 * 60 * 1000).toISOString()
173
- });
174
- });
175
-
176
- /**
177
- * POST /auth/mfa/challenge/verify
178
- * Verify a TOTP code for an active challenge, upgrade aal1 → aal2
179
- */
180
- router.post("/mfa/challenge/verify", requireAuth, async (c) => {
181
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
182
- if (!userCtx) {
183
- throw ApiError.unauthorized("Not authenticated");
184
- }
185
-
186
- const challengeVerifySchema = z.object({
187
- challengeId: z.string().min(1, "Challenge ID is required"),
188
- code: z.string().min(1, "Code is required")
189
- });
190
- const { challengeId, code } = parseBody(challengeVerifySchema, await c.req.json());
191
-
192
- // Find the challenge
193
- const challenge = await authRepo.getMfaChallengeById(challengeId);
194
- if (!challenge) {
195
- throw ApiError.badRequest("Invalid or expired challenge", "INVALID_CHALLENGE");
196
- }
197
-
198
- // Get the factor and verify ownership
199
- const factor = await authRepo.getMfaFactorById(challenge.factorId);
200
- if (!factor || factor.userId !== userCtx.userId) {
201
- throw ApiError.notFound("MFA factor not found");
202
- }
203
-
204
- // Try TOTP verification first (standard 6-digit codes)
205
- const decryptedSecret = decryptTotpSecret(factor.secretEncrypted);
206
- const secretBuffer = base32Decode(decryptedSecret);
207
- let isValid = verifyTotp(secretBuffer, code);
208
-
209
- // Fall back to recovery code verification if TOTP didn't match
210
- if (!isValid) {
211
- const codeHash = hashRecoveryCode(code);
212
- isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
213
- }
214
-
215
- if (!isValid) {
216
- throw ApiError.unauthorized("Invalid verification code", "INVALID_CODE");
217
- }
218
-
219
- // Mark challenge as verified
220
- await authRepo.verifyMfaChallenge(challengeId);
221
-
222
- // Generate new access token with aal2
223
- const roles = await authRepo.getUserRoles(userCtx.userId);
224
- const roleIds = roles.map((r) => r.id);
225
- const accessToken = generateAccessToken(userCtx.userId, roleIds, "aal2");
226
- const refreshToken = generateRefreshToken();
227
-
228
- // Create new refresh token
229
- await authRepo.createRefreshToken(
230
- userCtx.userId,
231
- hashRefreshToken(refreshToken),
232
- getRefreshTokenExpiry(),
233
- c.req.header("user-agent") || "unknown",
234
- c.req.header("x-forwarded-for") || "unknown"
235
- );
236
-
237
- // Fire onMfaVerified hook
238
- if (ops.onMfaVerified) {
239
- ops.onMfaVerified(userCtx.userId, factor.id).catch((err) => {
240
- logger.error("[AuthHooks] onMfaVerified error", {
241
- error: err instanceof Error ? err.message : err
242
- });
243
- });
244
- }
245
-
246
- let mfaResponse: AuthResponsePayload = {
247
- tokens: {
248
- accessToken,
249
- refreshToken,
250
- accessTokenExpiresAt: getAccessTokenExpiry()
251
- }
252
- };
253
- if (applyTransformHook) {
254
- mfaResponse = await applyTransformHook(mfaResponse, "mfa", c.req.raw, userCtx.userId);
255
- }
256
- return c.json(mfaResponse);
257
- });
258
-
259
- /**
260
- * GET /auth/mfa/factors
261
- * List enrolled MFA factors for the current user
262
- */
263
- router.get("/mfa/factors", requireAuth, async (c) => {
264
- const userCtx = c.get("user") as { userId: string; roles?: string[] } | undefined;
265
- if (!userCtx) {
266
- throw ApiError.unauthorized("Not authenticated");
267
- }
268
-
269
- const factors = await authRepo.getMfaFactors(userCtx.userId);
270
- return c.json({
271
- factors: factors.map((f) => ({
272
- id: f.id,
273
- factorType: f.factorType,
274
- friendlyName: f.friendlyName,
275
- verified: f.verified,
276
- createdAt: f.createdAt
277
- }))
278
- });
279
- });
280
-
281
- /**
282
- * DELETE /auth/mfa/unenroll
283
- * Remove an MFA factor
284
- */
285
- router.delete("/mfa/unenroll", requireAuth, async (c) => {
286
- const userCtx = c.get("user") as AccessTokenPayload | undefined;
287
- if (!userCtx) {
288
- throw ApiError.unauthorized("Not authenticated");
289
- }
290
-
291
- // Require aal2 (MFA-verified session) to unenroll MFA factors
292
- if (userCtx.aal !== "aal2") {
293
- throw ApiError.forbidden(
294
- "MFA verification required to unenroll. Please re-authenticate with your second factor.",
295
- "AAL2_REQUIRED"
296
- );
297
- }
298
-
299
- const unenrollSchema = z.object({
300
- factorId: z.string().min(1, "Factor ID is required")
301
- });
302
- const { factorId } = parseBody(unenrollSchema, await c.req.json());
303
-
304
- // Verify ownership
305
- const factor = await authRepo.getMfaFactorById(factorId);
306
- if (!factor || factor.userId !== userCtx.userId) {
307
- throw ApiError.notFound("MFA factor not found");
308
- }
309
-
310
- await authRepo.deleteMfaFactor(factorId, userCtx.userId);
311
-
312
- // If no more verified factors, clean up recovery codes
313
- const hasFactors = await authRepo.hasVerifiedMfaFactors(userCtx.userId);
314
- if (!hasFactors) {
315
- await authRepo.deleteAllRecoveryCodes(userCtx.userId);
316
- }
317
-
318
- return c.json({
319
- success: true,
320
- message: "MFA factor removed"
321
- });
322
- });
323
- }