@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
package/src/init.ts CHANGED
@@ -25,18 +25,18 @@ import { createBuiltinAuthAdapter } from "./auth/builtin-auth-adapter";
25
25
  import { errorHandler } from "./api/errors";
26
26
  import { Hono } from "hono";
27
27
  import { bodyLimit } from "hono/body-limit";
28
- import { csrf } from "hono/csrf";
29
28
  import { HonoEnv } from "./api/types";
30
29
  import { configureLogLevel } from "./utils/logging";
31
30
  import { logger } from "./utils/logger";
32
- import { requestLogger } from "./utils/request-logger";
31
+ import { configureMiddlewares } from "./init/middlewares";
32
+ import { initializeStorage } from "./init/storage";
33
+ import { mountOpenApiDocs } from "./init/docs";
34
+ import { createHealthCheck } from "./init/health";
35
+ import { createShutdown } from "./init/shutdown";
33
36
  import { configureJwt, requireAdmin, requireAuth } from "./auth";
34
37
  import {
35
38
  BackendStorageConfig,
36
- createStorageController,
37
39
  createStorageRoutes,
38
- DEFAULT_STORAGE_ID,
39
- DefaultStorageRegistry,
40
40
  StorageController,
41
41
  StorageRegistry
42
42
  } from "./storage";
@@ -82,7 +82,7 @@ export interface RebaseAuthConfig {
82
82
  * granted admin-level access without JWT verification. This is the
83
83
  * Rebase equivalent of a Firebase Service Account key.
84
84
  *
85
- * Generate with: `node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"`
85
+ * Generate with: `node -e "logger.info(require('crypto').randomBytes(48).toString('base64'))"`
86
86
  *
87
87
  * Set via `REBASE_SERVICE_KEY` in your `.env`.
88
88
  * Must be at least 32 characters.
@@ -223,9 +223,9 @@ export interface RebaseBackendConfig {
223
223
  origin: string | string[] | ((origin: string) => boolean);
224
224
  };
225
225
  /**
226
- * Backend-level hooks for intercepting admin data (users, roles)
227
- * at the API boundary. These run server-side after database reads
228
- * and before API responses are sent.
226
+ * Backend-level hooks for intercepting collection entity data
227
+ * at the REST API boundary. These run server-side after database
228
+ * operations and before API responses are sent.
229
229
  *
230
230
  * Complement the per-collection `EntityCallbacks` system which
231
231
  * handles collection CRUD operations.
@@ -309,36 +309,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
309
309
  const basePath = config.basePath || "/api";
310
310
  const isProduction = process.env.NODE_ENV === "production";
311
311
 
312
- // ─── Request Body Size Limit ─────────────────────────────────────────
313
- const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024; // 10MB default
314
- if (maxBodySize > 0) {
315
- config.app.use(`${basePath}/*`, bodyLimit({
316
- maxSize: maxBodySize,
317
- onError: (c) => {
318
- return c.json({
319
- error: {
320
- message: `Request body too large. Maximum size is ${Math.round(maxBodySize / 1024 / 1024)}MB.`,
321
- code: "PAYLOAD_TOO_LARGE"
322
- }
323
- }, 413);
324
- }
325
- }));
326
- logger.info("Request body limit configured", { maxSizeMB: Math.round(maxBodySize / 1024 / 1024) });
327
- }
328
-
329
- // ─── CSRF Protection (opt-in) ────────────────────────────────────────
330
- // BaaS APIs are consumed by mobile apps, SPAs on different origins, and
331
- // CLI/SDK tools. CSRF is only enabled when the developer explicitly
332
- // configures it with allowed origins.
333
- if (config.csrf?.origin) {
334
- config.app.use(`${basePath}/*`, csrf({
335
- origin: config.csrf.origin
336
- }));
337
- logger.info("CSRF protection enabled");
338
- }
339
-
340
- // ─── Request Logging ─────────────────────────────────────────────────
341
- config.app.use(`${basePath}/*`, requestLogger());
312
+ // Configure Hono middlewares (Request ID, body limit, CSRF, CORS warning, logging)
313
+ configureMiddlewares(config.app, basePath, isProduction, config);
342
314
 
343
315
  const collectionRegistry = new BackendCollectionRegistry();
344
316
  let activeCollections = config.collections || [];
@@ -380,7 +352,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
380
352
  initializeHistory: dbAdapter.initializeHistory,
381
353
  initializeWebsockets: dbAdapter.initializeWebsockets,
382
354
  getAdmin: dbAdapter.getAdmin,
383
- mountRoutes: dbAdapter.mountRoutes,
355
+ mountRoutes: dbAdapter.mountRoutes
384
356
  };
385
357
  bootstrappers = [wrappedBootstrapper];
386
358
  }
@@ -395,7 +367,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
395
367
 
396
368
  // 1. Initialize all drivers
397
369
  for (const bootstrapper of bootstrappers) {
398
- const b = bootstrapper as BackendBootstrapper & { id?: string; isDefault?: boolean };
370
+ const b = bootstrapper;
399
371
  logger.info("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
400
372
  if (b.isDefault) {
401
373
  defaultDriverId = b.id || bootstrapper.type;
@@ -413,7 +385,9 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
413
385
 
414
386
  if (bootstrapper.initializeRealtime) {
415
387
  const realtime = await bootstrapper.initializeRealtime({}, driverResult);
416
- realtimeServices[b.id || bootstrapper.type] = realtime as RealtimeProvider;
388
+ if (realtime) {
389
+ realtimeServices[b.id || bootstrapper.type] = realtime;
390
+ }
417
391
  }
418
392
  }
419
393
 
@@ -424,9 +398,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
424
398
  if (!defaultDriver || !defaultDriverResult) {
425
399
  throw new Error("Default driver not initialized by bootstrappers");
426
400
  }
427
- const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & {
428
- id?: string
429
- }).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
401
+ const defaultBootstrapper = bootstrappers.find(b => b.id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
430
402
  const defaultRealtimeService = defaultDriverResult.realtimeProvider;
431
403
 
432
404
  // 2. Initialize Auth & History via the default driver's bootstrapper
@@ -449,11 +421,27 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
449
421
  // Populate authConfigResult for backward compatibility
450
422
  // (the return type still exposes `auth?: BootstrappedAuth`)
451
423
  authConfigResult = {
452
- userService: authAdapter.userManagement ?? {},
424
+ userService: authAdapter.userManagement ?? {}
453
425
  };
454
426
  } else {
455
427
  // ── RebaseAuthConfig — wrap in built-in adapter ──
456
428
  const safeAuthConfig = config.auth as RebaseAuthConfig;
429
+
430
+ // Auto-discover the auth collection from activeCollections if not explicitly set
431
+ if (!safeAuthConfig.collection) {
432
+ const foundAuthCollection = activeCollections.find(c => {
433
+ const isAuth = c.auth;
434
+ return isAuth === true || (isAuth && typeof isAuth === "object" && isAuth.enabled === true);
435
+ });
436
+ if (foundAuthCollection) {
437
+ safeAuthConfig.collection = foundAuthCollection;
438
+ logger.info("Auto-discovered auth collection from collection definitions", { slug: foundAuthCollection.slug });
439
+ }
440
+ }
441
+
442
+ // Extract the collection-level auth config (if `auth` is an object, not just `true`)
443
+ const collectionAuth = safeAuthConfig.collection ? safeAuthConfig.collection.auth : undefined;
444
+ const collectionAuthConfig = (typeof collectionAuth === "object" && collectionAuth !== null) ? collectionAuth : undefined;
457
445
  if (safeAuthConfig.jwtSecret) {
458
446
  configureJwt({
459
447
  secret: safeAuthConfig.jwtSecret,
@@ -467,7 +455,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
467
455
  if (safeAuthConfig.serviceKey.length < 32) {
468
456
  throw new Error(
469
457
  "REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
470
- "Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
458
+ "Generate one with: node -e \"logger.info(require('crypto').randomBytes(48).toString('base64'))\""
471
459
  );
472
460
  }
473
461
  serviceKey = safeAuthConfig.serviceKey;
@@ -478,23 +466,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
478
466
  logger.info("Bootstrapping authentication via driver protocol");
479
467
  authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
480
468
 
481
- // Build the built-in auth adapter from bootstrapper results
482
- if (authConfigResult) {
483
- const oauthProviders: OAuthProvider<unknown>[] = [...(safeAuthConfig.providers || [])];
484
- // OAuth providers are resolved later in route mounting,
485
- // but we need them here for the adapter
486
- authAdapter = createBuiltinAuthAdapter({
487
- authRepository: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
488
- emailService: authConfigResult.emailService as import("./email").EmailService,
489
- emailConfig: safeAuthConfig.email,
490
- allowRegistration: safeAuthConfig.allowRegistration ?? false,
491
- defaultRole: safeAuthConfig.defaultRole,
492
- oauthProviders,
493
- serviceKey,
494
- hooks: config.hooks,
495
- authHooks: safeAuthConfig.hooks,
496
- });
497
- }
469
+ // The built-in auth adapter is created after OAuth providers
470
+ // are resolved (below) so it only needs to be constructed once.
498
471
 
499
472
  logger.info("Authentication initialized");
500
473
  } else {
@@ -527,50 +500,12 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
527
500
  }
528
501
 
529
502
  // 3. Initialize Storage
530
- let storageRegistry: StorageRegistry | undefined;
531
- let storageController: StorageController | undefined;
532
-
533
- if (config.storage) {
534
- logger.info("Configuring storage");
535
- const controllers: Record<string, StorageController> = {};
536
-
537
- // Helper: resolve a single storage entry to a controller
538
- const toController = (entry: BackendStorageConfig | StorageController, label: string): StorageController => {
539
- // Duck-type: if it has putObject, it's already a controller instance
540
- if (typeof (entry as StorageController).putObject === "function") {
541
- return entry as StorageController;
542
- }
543
- // Otherwise it's a config object — use the built-in factory
544
- const conf = entry as BackendStorageConfig;
545
- if (isProduction && conf.type === "local") {
546
- logger.warn(`Storage backend "${label}" uses local filesystem in production. ` +
547
- "Files will be lost on container restart. " +
548
- "Configure S3-compatible storage or a custom StorageController.");
549
- }
550
- return createStorageController(conf);
551
- };
552
-
553
- if (typeof config.storage === "object" && ("type" in config.storage || typeof (config.storage as StorageController).putObject === "function")) {
554
- // Single storage config or controller
555
- controllers[DEFAULT_STORAGE_ID] = toController(config.storage as BackendStorageConfig | StorageController, DEFAULT_STORAGE_ID);
556
- } else {
557
- // Multi-backend record
558
- for (const [storageId, entry] of Object.entries(config.storage as Record<string, BackendStorageConfig | StorageController>)) {
559
- controllers[storageId] = toController(entry, storageId);
560
- }
561
- }
562
-
563
- if (Object.keys(controllers).length > 0) {
564
- storageRegistry = DefaultStorageRegistry.create(controllers);
565
- storageController = storageRegistry.getDefault();
566
- logger.info("Initialized storage backends", { count: Object.keys(controllers).length });
567
- }
568
- }
503
+ const { storageRegistry, storageController } = initializeStorage(config.storage, isProduction);
569
504
 
570
505
  // basePath already resolved above
571
506
 
572
507
  // 4. Mount API Routes
573
- if (config.auth && authAdapter) {
508
+ if (config.auth) {
574
509
  // ── Auth Capabilities Endpoint ───────────────────────────────────
575
510
  // Exposes adapter capabilities so the frontend knows what's available
576
511
  // (login form vs external redirect, OAuth providers, etc.)
@@ -584,70 +519,39 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
584
519
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
585
520
  const oauthProviders: OAuthProvider<any>[] = [...(safeAuthConfig.providers || [])];
586
521
 
587
- if (safeAuthConfig.google?.clientId) {
588
- const { createGoogleProvider } = await import("./auth");
589
- oauthProviders.push(createGoogleProvider(safeAuthConfig.google));
590
- }
591
-
592
- if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
593
- const { createLinkedinProvider } = await import("./auth");
594
- oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as {
595
- clientId: string;
596
- clientSecret: string
597
- }));
598
- }
599
-
600
- if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
601
- const { createGitHubProvider } = await import("./auth");
602
- oauthProviders.push(createGitHubProvider(safeAuthConfig.github));
603
- }
604
-
605
- if (safeAuthConfig.microsoft?.clientId && safeAuthConfig.microsoft?.clientSecret) {
606
- const { createMicrosoftProvider } = await import("./auth");
607
- oauthProviders.push(createMicrosoftProvider(safeAuthConfig.microsoft));
608
- }
609
-
610
- if (safeAuthConfig.apple?.clientId && safeAuthConfig.apple?.teamId && safeAuthConfig.apple?.keyId && safeAuthConfig.apple?.privateKey) {
611
- const { createAppleProvider } = await import("./auth");
612
- oauthProviders.push(createAppleProvider(safeAuthConfig.apple));
613
- }
614
-
615
- if (safeAuthConfig.facebook?.clientId && safeAuthConfig.facebook?.clientSecret) {
616
- const { createFacebookProvider } = await import("./auth");
617
- oauthProviders.push(createFacebookProvider(safeAuthConfig.facebook));
618
- }
619
-
620
- if (safeAuthConfig.twitter?.clientId && safeAuthConfig.twitter?.clientSecret) {
621
- const { createTwitterProvider } = await import("./auth");
622
- oauthProviders.push(createTwitterProvider(safeAuthConfig.twitter));
623
- }
624
-
625
- if (safeAuthConfig.discord?.clientId && safeAuthConfig.discord?.clientSecret) {
626
- const { createDiscordProvider } = await import("./auth");
627
- oauthProviders.push(createDiscordProvider(safeAuthConfig.discord));
628
- }
629
-
630
- if (safeAuthConfig.gitlab?.clientId && safeAuthConfig.gitlab?.clientSecret) {
631
- const { createGitLabProvider } = await import("./auth");
632
- oauthProviders.push(createGitLabProvider(safeAuthConfig.gitlab));
633
- }
634
-
635
- if (safeAuthConfig.bitbucket?.clientId && safeAuthConfig.bitbucket?.clientSecret) {
636
- const { createBitbucketProvider } = await import("./auth");
637
- oauthProviders.push(createBitbucketProvider(safeAuthConfig.bitbucket));
638
- }
639
-
640
- if (safeAuthConfig.slack?.clientId && safeAuthConfig.slack?.clientSecret) {
641
- const { createSlackProvider } = await import("./auth");
642
- oauthProviders.push(createSlackProvider(safeAuthConfig.slack));
643
- }
644
-
645
- if (safeAuthConfig.spotify?.clientId && safeAuthConfig.spotify?.clientSecret) {
646
- const { createSpotifyProvider } = await import("./auth");
647
- oauthProviders.push(createSpotifyProvider(safeAuthConfig.spotify));
522
+ // Resolve configured OAuth providers via data-driven registration.
523
+ // Each entry maps a config key to its factory function name and required fields.
524
+ const OAUTH_PROVIDERS: Array<{
525
+ key: keyof RebaseAuthConfig;
526
+ factory: string;
527
+ requiredFields: string[];
528
+ }> = [
529
+ { key: "google", factory: "createGoogleProvider", requiredFields: ["clientId"] },
530
+ { key: "linkedin", factory: "createLinkedinProvider", requiredFields: ["clientId", "clientSecret"] },
531
+ { key: "github", factory: "createGitHubProvider", requiredFields: ["clientId", "clientSecret"] },
532
+ { key: "microsoft", factory: "createMicrosoftProvider", requiredFields: ["clientId", "clientSecret"] },
533
+ { key: "apple", factory: "createAppleProvider", requiredFields: ["clientId", "teamId", "keyId", "privateKey"] },
534
+ { key: "facebook", factory: "createFacebookProvider", requiredFields: ["clientId", "clientSecret"] },
535
+ { key: "twitter", factory: "createTwitterProvider", requiredFields: ["clientId", "clientSecret"] },
536
+ { key: "discord", factory: "createDiscordProvider", requiredFields: ["clientId", "clientSecret"] },
537
+ { key: "gitlab", factory: "createGitLabProvider", requiredFields: ["clientId", "clientSecret"] },
538
+ { key: "bitbucket", factory: "createBitbucketProvider", requiredFields: ["clientId", "clientSecret"] },
539
+ { key: "slack", factory: "createSlackProvider", requiredFields: ["clientId", "clientSecret"] },
540
+ { key: "spotify", factory: "createSpotifyProvider", requiredFields: ["clientId", "clientSecret"] }
541
+ ];
542
+
543
+ for (const { key, factory, requiredFields } of OAUTH_PROVIDERS) {
544
+ const providerConfig = safeAuthConfig[key] as Record<string, unknown> | undefined;
545
+ if (providerConfig && requiredFields.every(f => Boolean(providerConfig[f]))) {
546
+ const authModule = await import("./auth");
547
+ const createFn = (authModule as unknown as Record<string, (cfg: unknown) => OAuthProvider<unknown>>)[factory];
548
+ oauthProviders.push(createFn(providerConfig));
549
+ }
648
550
  }
649
551
 
650
552
  // Re-create the built-in adapter with all resolved OAuth providers
553
+ const reCollectionAuth = safeAuthConfig.collection ? safeAuthConfig.collection.auth : undefined;
554
+ const collectionAuthConfig = (typeof reCollectionAuth === "object" && reCollectionAuth !== null) ? reCollectionAuth : undefined;
651
555
  authAdapter = createBuiltinAuthAdapter({
652
556
  authRepository: authConfigResult!.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult!.userService as import("./auth/interfaces").AuthRepository,
653
557
  emailService: authConfigResult!.emailService as import("./email").EmailService,
@@ -656,13 +560,13 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
656
560
  defaultRole: safeAuthConfig.defaultRole,
657
561
  oauthProviders,
658
562
  serviceKey,
659
- hooks: config.hooks,
660
563
  authHooks: safeAuthConfig.hooks,
564
+ collectionAuthConfig
661
565
  });
662
566
  }
663
567
 
664
568
  // ── Mount auth & admin routes via the adapter ────────────────────
665
- if (authAdapter.createAuthRoutes) {
569
+ if (authAdapter && authAdapter.createAuthRoutes) {
666
570
  const authRoutes = authAdapter.createAuthRoutes();
667
571
  if (authRoutes) {
668
572
  config.app.route(`${basePath}/auth`, authRoutes);
@@ -670,7 +574,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
670
574
  }
671
575
  }
672
576
 
673
- if (authAdapter.createAdminRoutes) {
577
+ if (authAdapter && authAdapter.createAdminRoutes) {
674
578
  const adminRoutes = authAdapter.createAdminRoutes();
675
579
  if (adminRoutes) {
676
580
  config.app.route(`${basePath}/admin`, adminRoutes);
@@ -690,7 +594,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
690
594
  // Mount API key admin routes
691
595
  const apiKeyRoutes = createApiKeyRoutes({
692
596
  store: apiKeyStore,
693
- serviceKey,
597
+ serviceKey
694
598
  });
695
599
  config.app.route(`${basePath}/admin/api-keys`, apiKeyRoutes);
696
600
  logger.info("API key admin routes mounted", { path: `${basePath}/admin/api-keys` });
@@ -721,7 +625,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
721
625
  // maxFileSize (default 50MB), which overrides the global API body limit.
722
626
  const storageMaxSize = (
723
627
  config.storage && typeof config.storage === "object" && "type" in config.storage
724
- ? (config.storage as BackendStorageConfig & { maxFileSize?: number }).maxFileSize
628
+ ? (config.storage as BackendStorageConfig).maxFileSize
725
629
  : undefined
726
630
  ) ?? 50 * 1024 * 1024;
727
631
 
@@ -771,14 +675,14 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
771
675
  adapter: authAdapter,
772
676
  driver: defaultDriver,
773
677
  requireAuth: dataRequireAuth,
774
- apiKeyStore,
678
+ apiKeyStore
775
679
  }));
776
680
  } else {
777
681
  dataRouter.use("/*", createAuthMiddleware({
778
682
  driver: defaultDriver,
779
683
  requireAuth: dataRequireAuth,
780
684
  serviceKey,
781
- apiKeyStore,
685
+ apiKeyStore
782
686
  }));
783
687
  }
784
688
 
@@ -798,45 +702,19 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
798
702
  dataRouter.route("/", historyRoutes);
799
703
  }
800
704
 
801
- const restGenerator = new RestApiGenerator(activeCollections, defaultDriver, config.hooks?.data);
705
+ const restGenerator = new RestApiGenerator(
706
+ activeCollections,
707
+ defaultDriver,
708
+ config.hooks?.data,
709
+ authAdapter
710
+ );
802
711
  dataRouter.route("/", restGenerator.generateRoutes());
803
712
 
804
713
  config.app.route(`${basePath}/data`, dataRouter);
805
714
  }
806
715
 
807
716
  // ── OpenAPI / Swagger ─────────────────────────────────────────────────
808
- if (config.enableSwagger !== false && activeCollections.length > 0) {
809
- const { generateOpenApiSpec } = await import("./api/openapi-generator");
810
-
811
- config.app.get(`${basePath}/docs`, (c) => {
812
- const spec = generateOpenApiSpec(activeCollections, {
813
- basePath,
814
- requireAuth: resolveRequireAuth(config.auth)
815
- });
816
- return c.json(spec);
817
- });
818
-
819
- if (process.env.NODE_ENV !== "production") {
820
- config.app.get(`${basePath}/swagger`, (c) => {
821
- return c.html(`<!DOCTYPE html>
822
- <html>
823
- <head>
824
- <title>Rebase API Documentation</title>
825
- <meta charset="utf-8"/>
826
- <meta name="viewport" content="width=device-width, initial-scale=1"/>
827
- <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"/>
828
- <style>body{margin:0;padding:0;}</style>
829
- </head>
830
- <body>
831
- <div id="swagger-ui"></div>
832
- <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
833
- <script>SwaggerUIBundle({ url: '${basePath}/docs', dom_id: '#swagger-ui' });</script>
834
- </body>
835
- </html>`);
836
- });
837
- logger.info("Swagger UI available", { path: `${basePath}/swagger` });
838
- }
839
- }
717
+ await mountOpenApiDocs(config.app, basePath, config.enableSwagger, activeCollections, resolveRequireAuth(config.auth));
840
718
 
841
719
  // ─── Server-side singleton ────────────────────────────────────────────
842
720
  // Build the admin-level RebaseClient and expose it as the `rebase` singleton.
@@ -925,14 +803,14 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
925
803
  adapter: authAdapter,
926
804
  driver: defaultDriver,
927
805
  requireAuth: functionsRequireAuth,
928
- apiKeyStore,
806
+ apiKeyStore
929
807
  }));
930
808
  } else {
931
809
  functionsRouter.use("/*", createAuthMiddleware({
932
810
  driver: defaultDriver,
933
811
  requireAuth: functionsRequireAuth,
934
812
  serviceKey,
935
- apiKeyStore,
813
+ apiKeyStore
936
814
  }));
937
815
  }
938
816
 
@@ -998,101 +876,21 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
998
876
  }
999
877
  }
1000
878
 
1001
- if ((defaultBootstrapper as BackendBootstrapper & {
1002
- initializeWebsockets?: (...args: unknown[]) => unknown
1003
- }).initializeWebsockets) {
1004
- await (defaultBootstrapper as BackendBootstrapper & {
1005
- initializeWebsockets: (...args: unknown[]) => unknown
1006
- }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
879
+ if (defaultBootstrapper.initializeWebsockets && defaultRealtimeService) {
880
+ await defaultBootstrapper.initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
1007
881
  }
1008
882
 
1009
883
  logger.info("Rebase Backend Initialized");
1010
884
 
1011
885
  // ── Deep Health Check ─────────────────────────────────────────────────
1012
- const healthCheck = async (): Promise<HealthCheckResult> => {
1013
- const start = performance.now();
1014
- try {
1015
- // Use admin.executeSql if available (Postgres), otherwise try fetchCollection as a probe
1016
- const admin = defaultDriver.admin;
1017
- if (isSQLAdmin(admin)) {
1018
- await admin.executeSql("SELECT 1");
1019
- } else {
1020
- // Fallback: try a lightweight fetch to confirm driver is responsive
1021
- await defaultDriver.fetchCollection({
1022
- path: "__health_check_nonexistent__",
1023
- limit: 1
1024
- });
1025
- }
1026
- const latencyMs = Math.round(performance.now() - start);
1027
- return {
1028
- healthy: true,
1029
- latencyMs
1030
- };
1031
- } catch (error: unknown) {
1032
- const latencyMs = Math.round(performance.now() - start);
1033
- logger.error("Health check failed", {
1034
- error: error instanceof Error ? error : new Error(String(error)),
1035
- latencyMs
1036
- });
1037
- return {
1038
- healthy: false,
1039
- latencyMs,
1040
- details: {
1041
- error: error instanceof Error ? error.message : String(error)
1042
- }
1043
- };
1044
- }
1045
- };
886
+ const healthCheck = createHealthCheck(defaultDriver);
1046
887
 
1047
888
  // ── Graceful Shutdown ─────────────────────────────────────────────────
1048
- const shutdown = (timeoutMs = 15_000): Promise<void> => {
1049
- return new Promise<void>((resolve) => {
1050
- (async () => {
1051
- logger.info("Shutting down Rebase Backend...");
1052
-
1053
- // 1. Stop cron scheduler
1054
- if (cronScheduler) {
1055
- cronScheduler.stop();
1056
- logger.info("Cron scheduler stopped");
1057
- }
1058
-
1059
- // 2. Tear down realtime services (LISTEN clients, debounce timers,
1060
- // subscriptions). Must happen BEFORE pool.end() so that pending
1061
- // timer callbacks don't fire against a closed pool.
1062
- for (const [key, rt] of Object.entries(realtimeServices)) {
1063
- try {
1064
- const rtWithLifecycle = rt as RealtimeProvider & {
1065
- destroy?: () => Promise<void>;
1066
- stopListening?: () => Promise<void>
1067
- };
1068
- if (typeof rtWithLifecycle.destroy === "function") {
1069
- await rtWithLifecycle.destroy();
1070
- logger.info(`Realtime service "${key}" destroyed`);
1071
- } else if (typeof rtWithLifecycle.stopListening === "function") {
1072
- await rtWithLifecycle.stopListening();
1073
- logger.info(`Realtime service "${key}" LISTEN client stopped`);
1074
- }
1075
- } catch (err) {
1076
- logger.warn(`Error destroying realtime service "${key}":`, { error: err });
1077
- }
1078
- }
1079
-
1080
- // 3. Close the HTTP server (stop accepting, drain in-flight)
1081
- config.server.close(() => {
1082
- logger.info("HTTP server closed");
1083
- resolve();
1084
- });
1085
-
1086
- // 4. Force-resolve after timeout (unless disabled with 0)
1087
- if (timeoutMs > 0) {
1088
- setTimeout(() => {
1089
- logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
1090
- resolve();
1091
- }, timeoutMs).unref();
1092
- }
1093
- })();
1094
- });
1095
- };
889
+ const shutdown = createShutdown({
890
+ server: config.server,
891
+ cronScheduler,
892
+ realtimeServices
893
+ });
1096
894
 
1097
895
  return {
1098
896
  driverRegistry,
package/src/serve-spa.ts CHANGED
@@ -2,6 +2,7 @@ import { Hono } from "hono";
2
2
  import { serveStatic } from "@hono/node-server/serve-static";
3
3
  import * as path from "path";
4
4
  import * as fs from "fs";
5
+ import fsp from "node:fs/promises";
5
6
  import { logger } from "./utils/logger.js";
6
7
 
7
8
  /**
@@ -59,6 +60,9 @@ export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: Ser
59
60
  // Build list of paths to exclude from SPA handling
60
61
  const allExcludePaths = [apiBasePath, ...excludePaths];
61
62
 
63
+ // Cache the index.html content to avoid re-reading from disk on every navigation request.
64
+ let cachedHtml: string | null = null;
65
+
62
66
  // SPA fallback - serve index.html for all non-excluded routes
63
67
  app.get("*", async (c, next) => {
64
68
  // Skip excluded paths (API, health checks, etc.)
@@ -68,13 +72,16 @@ export function serveSPA<E extends import("hono").Env>(app: Hono<E>, config: Ser
68
72
 
69
73
  const indexPath = path.join(frontendPath, indexFile);
70
74
 
71
- if (!fs.existsSync(indexPath)) {
72
- logger.warn(`⚠️ Index file not found: ${indexPath}`);
73
- return next();
75
+ if (!cachedHtml) {
76
+ try {
77
+ cachedHtml = await fsp.readFile(indexPath, "utf-8");
78
+ } catch {
79
+ logger.warn(`⚠️ Index file not found: ${indexPath}`);
80
+ return next();
81
+ }
74
82
  }
75
83
 
76
- const html = fs.readFileSync(indexPath, "utf-8");
77
- return c.html(html);
84
+ return c.html(cachedHtml);
78
85
  });
79
86
 
80
87
  logger.info(`✅ SPA serving enabled from: ${frontendPath}`);
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { DataDriver } from "@rebasepro/types";
15
+ import { logger } from "../utils/logger";
15
16
 
16
17
  /**
17
18
  * The default driver identifier used when:
@@ -95,7 +96,7 @@ export class DefaultDriverRegistry implements DriverRegistry {
95
96
  if (!registry.has(DEFAULT_DRIVER_ID) && registry.size() > 0) {
96
97
  // If no explicit "(default)", use the first one as default
97
98
  const firstId = Object.keys(input)[0];
98
- console.warn(
99
+ logger.warn(
99
100
  `[DriverRegistry] No "${DEFAULT_DRIVER_ID}" driver provided. ` +
100
101
  `Using "${firstId}" as the default.`
101
102
  );
@@ -108,7 +109,7 @@ export class DefaultDriverRegistry implements DriverRegistry {
108
109
 
109
110
  register(id: string, delegate: DataDriver): void {
110
111
  if (this.delegates.has(id)) {
111
- console.warn(`[DriverRegistry] Overwriting driver with id "${id}"`);
112
+ logger.warn(`[DriverRegistry] Overwriting driver with id "${id}"`);
112
113
  }
113
114
  this.delegates.set(id, delegate);
114
115
  }
@@ -144,7 +145,7 @@ export class DefaultDriverRegistry implements DriverRegistry {
144
145
  }
145
146
 
146
147
  // Fallback to default with warning
147
- console.warn(
148
+ logger.warn(
148
149
  `[DriverRegistry] Driver "${id}" not found, falling back to "${DEFAULT_DRIVER_ID}"`
149
150
  );
150
151
  return this.getDefault();