@rebasepro/server-core 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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 (176) hide show
  1. package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
  2. package/dist/common/src/util/entities.d.ts +22 -0
  3. package/dist/common/src/util/relations.d.ts +14 -4
  4. package/dist/common/src/util/resolutions.d.ts +1 -1
  5. package/dist/index-DXVBFp5V.js +37 -0
  6. package/dist/index-DXVBFp5V.js.map +1 -0
  7. package/dist/index.es.js +49931 -55
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/index.umd.js +28475 -34569
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
  12. package/dist/server-core/src/api/types.d.ts +1 -1
  13. package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
  14. package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
  15. package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
  16. package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
  17. package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
  18. package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
  19. package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
  20. package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
  21. package/dist/server-core/src/auth/index.d.ts +13 -2
  22. package/dist/server-core/src/auth/interfaces.d.ts +45 -6
  23. package/dist/server-core/src/auth/jwt.d.ts +1 -0
  24. package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
  25. package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
  26. package/dist/server-core/src/auth/middleware.d.ts +36 -11
  27. package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
  28. package/dist/server-core/src/auth/routes.d.ts +13 -3
  29. package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
  30. package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
  31. package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
  32. package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
  33. package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
  34. package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
  35. package/dist/server-core/src/cron/cron-store.d.ts +32 -0
  36. package/dist/server-core/src/cron/index.d.ts +6 -0
  37. package/dist/server-core/src/email/index.d.ts +2 -2
  38. package/dist/server-core/src/email/templates.d.ts +9 -0
  39. package/dist/server-core/src/email/types.d.ts +20 -23
  40. package/dist/server-core/src/history/history-routes.d.ts +2 -2
  41. package/dist/server-core/src/index.d.ts +5 -0
  42. package/dist/server-core/src/init.d.ts +112 -2
  43. package/dist/server-core/src/singleton.d.ts +35 -0
  44. package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
  45. package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
  46. package/dist/server-core/src/storage/index.d.ts +18 -11
  47. package/dist/server-core/src/storage/routes.d.ts +3 -3
  48. package/dist/server-core/src/storage/types.d.ts +28 -16
  49. package/dist/server-core/src/utils/dev-port.d.ts +35 -0
  50. package/dist/server-core/src/utils/logger.d.ts +31 -0
  51. package/dist/server-core/src/utils/request-logger.d.ts +19 -0
  52. package/dist/types/src/controllers/auth.d.ts +2 -0
  53. package/dist/types/src/controllers/client.d.ts +119 -7
  54. package/dist/types/src/controllers/collection_registry.d.ts +4 -3
  55. package/dist/types/src/controllers/customization_controller.d.ts +7 -1
  56. package/dist/types/src/controllers/data.d.ts +34 -7
  57. package/dist/types/src/controllers/data_driver.d.ts +20 -28
  58. package/dist/types/src/controllers/database_admin.d.ts +2 -2
  59. package/dist/types/src/controllers/email.d.ts +34 -0
  60. package/dist/types/src/controllers/index.d.ts +1 -0
  61. package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
  62. package/dist/types/src/controllers/navigation.d.ts +5 -5
  63. package/dist/types/src/controllers/registry.d.ts +6 -3
  64. package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
  65. package/dist/types/src/controllers/storage.d.ts +24 -26
  66. package/dist/types/src/rebase_context.d.ts +8 -4
  67. package/dist/types/src/types/backend.d.ts +4 -1
  68. package/dist/types/src/types/builders.d.ts +5 -4
  69. package/dist/types/src/types/chips.d.ts +1 -1
  70. package/dist/types/src/types/collections.d.ts +169 -125
  71. package/dist/types/src/types/cron.d.ts +102 -0
  72. package/dist/types/src/types/data_source.d.ts +1 -1
  73. package/dist/types/src/types/entity_actions.d.ts +8 -8
  74. package/dist/types/src/types/entity_callbacks.d.ts +15 -15
  75. package/dist/types/src/types/entity_link_builder.d.ts +1 -1
  76. package/dist/types/src/types/entity_overrides.d.ts +2 -1
  77. package/dist/types/src/types/entity_views.d.ts +8 -8
  78. package/dist/types/src/types/export_import.d.ts +3 -3
  79. package/dist/types/src/types/index.d.ts +1 -0
  80. package/dist/types/src/types/plugins.d.ts +72 -18
  81. package/dist/types/src/types/properties.d.ts +118 -33
  82. package/dist/types/src/types/relations.d.ts +1 -1
  83. package/dist/types/src/types/slots.d.ts +30 -6
  84. package/dist/types/src/types/translations.d.ts +44 -0
  85. package/dist/types/src/types/user_management_delegate.d.ts +1 -0
  86. package/package.json +84 -84
  87. package/scratch.ts +6 -5
  88. package/src/api/ast-schema-editor.ts +11 -11
  89. package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
  90. package/src/api/errors.ts +38 -14
  91. package/src/api/graphql/graphql-schema-generator.ts +5 -3
  92. package/src/api/openapi-generator.ts +613 -58
  93. package/src/api/rest/api-generator.ts +17 -11
  94. package/src/api/rest/query-parser.ts +1 -1
  95. package/src/api/schema-editor-routes.ts +2 -0
  96. package/src/api/server.ts +8 -5
  97. package/src/api/types.ts +1 -1
  98. package/src/auth/admin-routes.ts +58 -17
  99. package/src/auth/apple-oauth.ts +130 -0
  100. package/src/auth/bitbucket-oauth.ts +82 -0
  101. package/src/auth/discord-oauth.ts +83 -0
  102. package/src/auth/facebook-oauth.ts +72 -0
  103. package/src/auth/github-oauth.ts +110 -0
  104. package/src/auth/gitlab-oauth.ts +70 -0
  105. package/src/auth/google-oauth.ts +34 -46
  106. package/src/auth/index.ts +15 -2
  107. package/src/auth/interfaces.ts +54 -7
  108. package/src/auth/jwt.ts +22 -5
  109. package/src/auth/linkedin-oauth.ts +81 -0
  110. package/src/auth/microsoft-oauth.ts +88 -0
  111. package/src/auth/middleware.ts +194 -45
  112. package/src/auth/password.ts +3 -1
  113. package/src/auth/rate-limiter.ts +4 -4
  114. package/src/auth/routes.ts +197 -139
  115. package/src/auth/slack-oauth.ts +71 -0
  116. package/src/auth/spotify-oauth.ts +67 -0
  117. package/src/auth/twitter-oauth.ts +120 -0
  118. package/src/collections/loader.ts +6 -6
  119. package/src/cron/cron-loader.ts +89 -0
  120. package/src/cron/cron-routes.test.ts +265 -0
  121. package/src/cron/cron-routes.ts +85 -0
  122. package/src/cron/cron-scheduler.test.ts +421 -0
  123. package/src/cron/cron-scheduler.ts +413 -0
  124. package/src/cron/cron-store.ts +163 -0
  125. package/src/cron/index.ts +6 -0
  126. package/src/db/interfaces.ts +2 -2
  127. package/src/email/index.ts +3 -2
  128. package/src/email/smtp-email-service.ts +5 -2
  129. package/src/email/templates.ts +93 -6
  130. package/src/email/types.ts +17 -24
  131. package/src/functions/function-loader.ts +34 -6
  132. package/src/functions/function-routes.ts +2 -2
  133. package/src/history/history-routes.ts +4 -3
  134. package/src/index.ts +10 -0
  135. package/src/init.ts +475 -57
  136. package/src/services/driver-registry.ts +1 -1
  137. package/src/singleton.test.ts +28 -0
  138. package/src/singleton.ts +70 -0
  139. package/src/storage/LocalStorageController.ts +50 -53
  140. package/src/storage/S3StorageController.ts +49 -46
  141. package/src/storage/index.ts +27 -16
  142. package/src/storage/routes.ts +76 -59
  143. package/src/storage/storage-registry.ts +5 -5
  144. package/src/storage/types.ts +44 -32
  145. package/src/types/index.ts +4 -4
  146. package/src/utils/dev-port.ts +176 -0
  147. package/src/utils/logger.ts +143 -0
  148. package/src/utils/logging.ts +5 -2
  149. package/src/utils/request-logger.ts +66 -0
  150. package/test/admin-routes.test.ts +114 -65
  151. package/test/api-generator.test.ts +73 -30
  152. package/test/ast-schema-editor.test.ts +5 -3
  153. package/test/auth-middleware-hono.test.ts +268 -33
  154. package/test/auth-routes.test.ts +325 -146
  155. package/test/driver-registry.test.ts +4 -2
  156. package/test/error-propagation.test.ts +226 -0
  157. package/test/errors.test.ts +12 -7
  158. package/test/jwt-security.test.ts +18 -9
  159. package/test/jwt.test.ts +26 -13
  160. package/test/middleware.test.ts +13 -8
  161. package/test/query-parser.test.ts +13 -8
  162. package/test/rate-limiter.test.ts +7 -7
  163. package/test/safe-compare.test.ts +66 -0
  164. package/test/singleton.test.ts +59 -0
  165. package/test/storage-local.test.ts +36 -43
  166. package/test/storage-registry.test.ts +38 -36
  167. package/test/storage-routes.test.ts +9 -5
  168. package/test/storage-s3.test.ts +51 -48
  169. package/test.ts +6 -0
  170. package/tsconfig.json +1 -1
  171. package/vite.config.ts +7 -5
  172. package/dist/index-BeMqpmfQ.js +0 -239
  173. package/dist/index-BeMqpmfQ.js.map +0 -1
  174. package/dist/index-bl4J3lNb.js +0 -55823
  175. package/dist/index-bl4J3lNb.js.map +0 -1
  176. package/vite.config.ts.timestamp-1775065397568-8a853255edf6e.mjs +0 -46
package/src/init.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider } from "@rebasepro/types";
1
+ import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin } from "@rebasepro/types";
2
2
  import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
3
3
  import { loadCollectionsFromDirectory } from "./collections/loader";
4
4
  import { DriverRegistry, DEFAULT_DRIVER_ID, DefaultDriverRegistry } from "./services/driver-registry";
@@ -8,12 +8,20 @@ import { RestApiGenerator } from "./api/rest/api-generator";
8
8
  import { createAuthMiddleware } from "./auth/middleware";
9
9
  import { errorHandler } from "./api/errors";
10
10
  import { Hono } from "hono";
11
+ import { bodyLimit } from "hono/body-limit";
12
+ import { csrf } from "hono/csrf";
11
13
  import { HonoEnv } from "./api/types";
12
14
  import { configureLogLevel } from "./utils/logging";
15
+ import { logger } from "./utils/logger";
16
+ import { requestLogger } from "./utils/request-logger";
13
17
  import { createAdminRoutes, createAuthRoutes, requireAuth, requireAdmin, configureJwt } from "./auth";
14
18
  import { createStorageController, createStorageRoutes, DEFAULT_STORAGE_ID, DefaultStorageRegistry, BackendStorageConfig, StorageController, StorageRegistry } from "./storage";
19
+ import { createRebaseClient } from "@rebasepro/client";
15
20
  import { createHistoryRoutes } from "./history";
16
- import { EmailConfig } from "./email";
21
+ import { EmailConfig, createEmailService } from "./email";
22
+ import type { EmailService } from "./email";
23
+ import type { OAuthProvider } from "./auth/interfaces";
24
+ import { _initRebase } from "./singleton";
17
25
 
18
26
  export interface RebaseAuthConfig {
19
27
  jwtSecret?: string;
@@ -21,9 +29,34 @@ export interface RebaseAuthConfig {
21
29
  refreshExpiresIn?: string;
22
30
  requireAuth?: boolean;
23
31
  allowRegistration?: boolean;
32
+ /**
33
+ * A static secret key for server-to-server / script authentication.
34
+ *
35
+ * When a request includes `Authorization: Bearer <serviceKey>`, it is
36
+ * granted admin-level access without JWT verification. This is the
37
+ * Rebase equivalent of a Firebase Service Account key.
38
+ *
39
+ * Generate with: `node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"`
40
+ *
41
+ * Set via `REBASE_SERVICE_KEY` in your `.env`.
42
+ * Must be at least 32 characters.
43
+ */
44
+ serviceKey?: string;
24
45
  email?: EmailConfig;
25
46
  google?: { clientId: string };
47
+ linkedin?: { clientId: string; clientSecret: string };
48
+ github?: { clientId: string; clientSecret: string };
49
+ microsoft?: { clientId: string; clientSecret: string; tenantId?: string };
50
+ apple?: { clientId: string; teamId: string; keyId: string; privateKey: string };
51
+ facebook?: { clientId: string; clientSecret: string };
52
+ twitter?: { clientId: string; clientSecret: string };
53
+ discord?: { clientId: string; clientSecret: string };
54
+ gitlab?: { clientId: string; clientSecret: string; baseUrl?: string };
55
+ bitbucket?: { clientId: string; clientSecret: string };
56
+ slack?: { clientId: string; clientSecret: string };
57
+ spotify?: { clientId: string; clientSecret: string };
26
58
  defaultRole?: string;
59
+ providers?: OAuthProvider<any>[];
27
60
  [key: string]: unknown;
28
61
  }
29
62
 
@@ -38,10 +71,42 @@ export interface RebaseBackendConfig {
38
71
  level?: "error" | "warn" | "info" | "debug";
39
72
  };
40
73
  auth?: RebaseAuthConfig;
41
- storage?: BackendStorageConfig | Record<string, BackendStorageConfig>;
74
+ /**
75
+ * Storage configuration. Accepts:
76
+ *
77
+ * - A `BackendStorageConfig` object (`{ type: 'local' | 's3', ... }`)
78
+ * - A `StorageController` instance (for custom providers like GCS, Azure, etc.)
79
+ * - A `Record<string, ...>` of either, for multi-backend setups
80
+ */
81
+ storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
42
82
  history?: unknown;
43
83
  enableSwagger?: boolean;
44
84
  functionsDir?: string;
85
+ cronsDir?: string;
86
+ /**
87
+ * Maximum request body size in bytes for API routes (default: 10MB).
88
+ * Set to 0 to disable the global limit entirely.
89
+ *
90
+ * Note: Storage upload routes use their own limit from the storage config's
91
+ * `maxFileSize` property (default: 50MB), which takes precedence over this.
92
+ */
93
+ maxBodySize?: number;
94
+ /**
95
+ * CSRF protection configuration. **Opt-in** — disabled by default.
96
+ *
97
+ * BaaS APIs are consumed by mobile apps, SPAs on different domains,
98
+ * and CLI tools, so CSRF is intentionally not enabled unless you
99
+ * explicitly configure it with allowed origins.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * csrf: { origin: ["https://myapp.com", "https://admin.myapp.com"] }
104
+ * ```
105
+ */
106
+ csrf?: {
107
+ /** Allowed origins for CSRF validation. */
108
+ origin: string | string[] | ((origin: string) => boolean);
109
+ };
45
110
  }
46
111
 
47
112
  export interface RebaseBackendInstance {
@@ -54,32 +119,28 @@ export interface RebaseBackendInstance {
54
119
  storageRegistry?: StorageRegistry;
55
120
  storageController?: StorageController;
56
121
  collectionRegistry: BackendCollectionRegistry;
122
+ cronScheduler?: import("./cron").CronScheduler;
123
+ /**
124
+ * Deep health check that verifies database connectivity.
125
+ * Returns latency and component status.
126
+ */
127
+ healthCheck(): Promise<HealthCheckResult>;
128
+ /**
129
+ * Graceful shutdown helper for the BaaS instance.
130
+ * Stops the cron scheduler and closes the HTTP server, allowing
131
+ * in-flight requests to drain within the given timeout.
132
+ *
133
+ * @param timeoutMs - Maximum time (ms) to wait for drain before force-exit (default: 15000).
134
+ * Pass 0 to skip the force-exit timer (useful in tests).
135
+ */
136
+ shutdown(timeoutMs?: number): Promise<void>;
57
137
  }
58
138
 
59
139
  export async function initializeRebaseBackend(config: RebaseBackendConfig): Promise<RebaseBackendInstance> {
60
- try {
61
- return await _initializeRebaseBackend(config);
62
- } catch (error: unknown) {
63
- console.error("❌ Critical error during Rebase Backend initialization:", error);
64
-
65
- const basePath = config.basePath || "/api";
66
- config.app.use(`${basePath}/*`, async (c) => {
67
- return c.json({
68
- error: {
69
- message: "Backend initialization failed. Please check the backend server logs.",
70
- code: "backend-init-failed"
71
- }
72
- }, 503);
73
- });
74
-
75
- return {
76
- __failed: true,
77
- driverRegistry: DefaultDriverRegistry.create({}),
78
- driver: {} as unknown as DataDriver,
79
- realtimeServices: {},
80
- realtimeService: {} as unknown as RealtimeProvider,
81
- } as unknown as RebaseBackendInstance;
82
- }
140
+ // No try/catch: let init errors propagate to the caller.
141
+ // The app entry point (e.g. startServer()) should catch and process.exit(1).
142
+ // Returning a fake instance hides critical failures and leads to silent data loss.
143
+ return await _initializeRebaseBackend(config);
83
144
  }
84
145
 
85
146
  async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<RebaseBackendInstance> {
@@ -89,13 +150,48 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
89
150
  configureLogLevel();
90
151
  }
91
152
 
92
- console.log("🔥 Initializing Rebase Backend (Bootstrapper Protocol V2)");
153
+ logger.info("Initializing Rebase Backend (Bootstrapper Protocol V2)");
154
+
155
+ const basePath = config.basePath || "/api";
156
+ const isProduction = process.env.NODE_ENV === "production";
157
+
158
+ // ─── Request Body Size Limit ─────────────────────────────────────────
159
+ const maxBodySize = config.maxBodySize ?? 10 * 1024 * 1024; // 10MB default
160
+ if (maxBodySize > 0) {
161
+ config.app.use(`${basePath}/*`, bodyLimit({
162
+ maxSize: maxBodySize,
163
+ onError: (c) => {
164
+ return c.json({
165
+ error: {
166
+ message: `Request body too large. Maximum size is ${Math.round(maxBodySize / 1024 / 1024)}MB.`,
167
+ code: "PAYLOAD_TOO_LARGE"
168
+ }
169
+ }, 413);
170
+ }
171
+ }));
172
+ logger.info("Request body limit configured", { maxSizeMB: Math.round(maxBodySize / 1024 / 1024) });
173
+ }
174
+
175
+ // ─── CSRF Protection (opt-in) ────────────────────────────────────────
176
+ // BaaS APIs are consumed by mobile apps, SPAs on different origins, and
177
+ // CLI/SDK tools. CSRF is only enabled when the developer explicitly
178
+ // configures it with allowed origins.
179
+ if (config.csrf?.origin) {
180
+ config.app.use(`${basePath}/*`, csrf({
181
+ origin: config.csrf.origin
182
+ }));
183
+ logger.info("CSRF protection enabled");
184
+ }
185
+
186
+ // ─── Request Logging ─────────────────────────────────────────────────
187
+ config.app.use(`${basePath}/*`, requestLogger());
93
188
 
94
189
  const collectionRegistry = new BackendCollectionRegistry();
95
190
  let activeCollections = config.collections || [];
96
191
  if (config.collectionsDir && activeCollections.length === 0) {
97
192
  activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
98
- console.log(`📁 Auto-discovered ${activeCollections.length} collections from ${config.collectionsDir}`);
193
+ logger.info("Auto-discovered collections", { count: activeCollections.length,
194
+ dir: config.collectionsDir });
99
195
  }
100
196
 
101
197
  const realtimeServices: Record<string, RealtimeProvider> = {};
@@ -108,19 +204,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
108
204
 
109
205
  let defaultDriverId = DEFAULT_DRIVER_ID;
110
206
 
111
- let defaultDriverResult: import("@rebasepro/types").InitializedDriver | undefined = undefined;
207
+ let defaultDriverResult: InitializedDriver | undefined = undefined;
112
208
 
113
209
  // 1. Initialize all drivers
114
210
  for (const bootstrapper of bootstrappers) {
115
211
  const b = bootstrapper as BackendBootstrapper & { id?: string; isDefault?: boolean };
116
- console.log(`📦 Running bootstrapper for driver: "${b.id || bootstrapper.type}"`);
212
+ logger.info("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
117
213
  if (b.isDefault) {
118
214
  defaultDriverId = b.id || bootstrapper.type;
119
215
  }
120
216
 
121
- const driverResult = await bootstrapper.initializeDriver({ collections: activeCollections, collectionRegistry });
217
+ const driverResult = await bootstrapper.initializeDriver({ collections: activeCollections,
218
+ collectionRegistry });
122
219
  delegates[b.id || bootstrapper.type] = driverResult.driver;
123
-
220
+
124
221
  if ((b.id || bootstrapper.type) === defaultDriverId || !defaultDriverResult) {
125
222
  defaultDriverResult = driverResult;
126
223
  }
@@ -143,6 +240,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
143
240
 
144
241
  // 2. Initialize Auth & History via the default driver's bootstrapper
145
242
  let authConfigResult: BootstrappedAuth | undefined = undefined;
243
+ let serviceKey: string | undefined;
244
+
146
245
  if (config.auth) {
147
246
  // Secure JWT setup proactively within core package memory to eliminate dual-package hazards
148
247
  const safeAuthConfig = config.auth;
@@ -154,23 +253,47 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
154
253
  });
155
254
  }
156
255
 
256
+ // ── Service Key Validation ───────────────────────────────────────
257
+ if (safeAuthConfig.serviceKey) {
258
+ if (safeAuthConfig.serviceKey.length < 32) {
259
+ throw new Error(
260
+ "REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
261
+ "Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
262
+ );
263
+ }
264
+ serviceKey = safeAuthConfig.serviceKey;
265
+ logger.info("Service key configured for script/server-to-server authentication");
266
+ }
267
+
157
268
  if (defaultBootstrapper.initializeAuth) {
158
- console.log("🔐 Bootstrapping authentication via driver protocol...");
269
+ logger.info("Bootstrapping authentication via driver protocol");
159
270
  authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
160
- console.log("Authentication initialized");
271
+ logger.info("Authentication initialized");
161
272
  } else {
162
- console.warn("⚠️ Auth requested but default bootstrapper does not support initializeAuth");
273
+ logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
163
274
  }
164
275
  }
165
276
 
166
277
  let historyConfigResult: Record<string, unknown> | undefined = undefined;
167
278
  if (config.history) {
168
279
  if (defaultBootstrapper.initializeHistory) {
169
- console.log("📜 Bootstrapping entity history via driver protocol...");
280
+ logger.info("Bootstrapping entity history via driver protocol");
170
281
  historyConfigResult = await defaultBootstrapper.initializeHistory(config.history, defaultDriverResult);
171
- console.log("✅ Entity history initialized");
282
+
283
+ // Inject the historyService into the driver so saveEntity/deleteEntity can record history.
284
+ // The driver was created during initializeDriver() (before history was initialized),
285
+ // so we must set it retroactively here.
286
+ if (historyConfigResult?.historyService && defaultDriverResult.internals) {
287
+ const internals = defaultDriverResult.internals as Record<string, unknown>;
288
+ const driver = internals.driver as Record<string, unknown> | undefined;
289
+ if (driver && "historyService" in driver) {
290
+ driver.historyService = historyConfigResult.historyService;
291
+ }
292
+ }
293
+
294
+ logger.info("Entity history initialized");
172
295
  } else {
173
- console.warn("⚠️ History requested but default bootstrapper does not support initializeHistory");
296
+ logger.warn("History requested but default bootstrapper does not support initializeHistory");
174
297
  }
175
298
  }
176
299
 
@@ -179,42 +302,123 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
179
302
  let storageController: StorageController | undefined;
180
303
 
181
304
  if (config.storage) {
182
- console.log("📁 Configuring storage...");
305
+ logger.info("Configuring storage");
183
306
  const controllers: Record<string, StorageController> = {};
184
307
 
185
- if (typeof config.storage === "object" && "type" in config.storage) {
186
- const controller = createStorageController(config.storage as BackendStorageConfig);
187
- controllers[DEFAULT_STORAGE_ID] = controller;
308
+ // Helper: resolve a single storage entry to a controller
309
+ const toController = (entry: BackendStorageConfig | StorageController, label: string): StorageController => {
310
+ // Duck-type: if it has putObject, it's already a controller instance
311
+ if (typeof (entry as StorageController).putObject === "function") {
312
+ return entry as StorageController;
313
+ }
314
+ // Otherwise it's a config object — use the built-in factory
315
+ const conf = entry as BackendStorageConfig;
316
+ if (isProduction && conf.type === "local") {
317
+ logger.warn(`Storage backend "${label}" uses local filesystem in production. ` +
318
+ "Files will be lost on container restart. " +
319
+ "Configure S3-compatible storage or a custom StorageController.");
320
+ }
321
+ return createStorageController(conf);
322
+ };
323
+
324
+ if (typeof config.storage === "object" && ("type" in config.storage || typeof (config.storage as StorageController).putObject === "function")) {
325
+ // Single storage config or controller
326
+ controllers[DEFAULT_STORAGE_ID] = toController(config.storage as BackendStorageConfig | StorageController, DEFAULT_STORAGE_ID);
188
327
  } else {
189
- for (const [storageId, storageConfig] of Object.entries(config.storage as Record<string, BackendStorageConfig>)) {
190
- controllers[storageId] = createStorageController(storageConfig);
328
+ // Multi-backend record
329
+ for (const [storageId, entry] of Object.entries(config.storage as Record<string, BackendStorageConfig | StorageController>)) {
330
+ controllers[storageId] = toController(entry, storageId);
191
331
  }
192
332
  }
193
333
 
194
334
  if (Object.keys(controllers).length > 0) {
195
335
  storageRegistry = DefaultStorageRegistry.create(controllers);
196
336
  storageController = storageRegistry.getDefault();
197
- console.log(`✅ Initialized ${Object.keys(controllers).length} storage backend(s)`);
337
+ logger.info("Initialized storage backends", { count: Object.keys(controllers).length });
198
338
  }
199
339
  }
200
340
 
201
- const basePath = config.basePath || "/api";
341
+ // basePath already resolved above
202
342
 
203
343
  // 4. Mount API Routes
204
344
  if (config.auth && authConfigResult) {
345
+ const oauthProviders: OAuthProvider<any>[] = [...(config.auth.providers || [])];
346
+
347
+ if (config.auth.google?.clientId) {
348
+ const { createGoogleProvider } = await import("./auth");
349
+ oauthProviders.push(createGoogleProvider(config.auth.google.clientId));
350
+ }
351
+
352
+ if (config.auth.linkedin?.clientId && config.auth.linkedin?.clientSecret) {
353
+ const { createLinkedinProvider } = await import("./auth");
354
+ oauthProviders.push(createLinkedinProvider(config.auth.linkedin as { clientId: string; clientSecret: string }));
355
+ }
356
+
357
+ if (config.auth.github?.clientId && config.auth.github?.clientSecret) {
358
+ const { createGitHubProvider } = await import("./auth");
359
+ oauthProviders.push(createGitHubProvider(config.auth.github));
360
+ }
361
+
362
+ if (config.auth.microsoft?.clientId && config.auth.microsoft?.clientSecret) {
363
+ const { createMicrosoftProvider } = await import("./auth");
364
+ oauthProviders.push(createMicrosoftProvider(config.auth.microsoft));
365
+ }
366
+
367
+ if (config.auth.apple?.clientId && config.auth.apple?.teamId && config.auth.apple?.keyId && config.auth.apple?.privateKey) {
368
+ const { createAppleProvider } = await import("./auth");
369
+ oauthProviders.push(createAppleProvider(config.auth.apple));
370
+ }
371
+
372
+ if (config.auth.facebook?.clientId && config.auth.facebook?.clientSecret) {
373
+ const { createFacebookProvider } = await import("./auth");
374
+ oauthProviders.push(createFacebookProvider(config.auth.facebook));
375
+ }
376
+
377
+ if (config.auth.twitter?.clientId && config.auth.twitter?.clientSecret) {
378
+ const { createTwitterProvider } = await import("./auth");
379
+ oauthProviders.push(createTwitterProvider(config.auth.twitter));
380
+ }
381
+
382
+ if (config.auth.discord?.clientId && config.auth.discord?.clientSecret) {
383
+ const { createDiscordProvider } = await import("./auth");
384
+ oauthProviders.push(createDiscordProvider(config.auth.discord));
385
+ }
386
+
387
+ if (config.auth.gitlab?.clientId && config.auth.gitlab?.clientSecret) {
388
+ const { createGitLabProvider } = await import("./auth");
389
+ oauthProviders.push(createGitLabProvider(config.auth.gitlab));
390
+ }
391
+
392
+ if (config.auth.bitbucket?.clientId && config.auth.bitbucket?.clientSecret) {
393
+ const { createBitbucketProvider } = await import("./auth");
394
+ oauthProviders.push(createBitbucketProvider(config.auth.bitbucket));
395
+ }
396
+
397
+ if (config.auth.slack?.clientId && config.auth.slack?.clientSecret) {
398
+ const { createSlackProvider } = await import("./auth");
399
+ oauthProviders.push(createSlackProvider(config.auth.slack));
400
+ }
401
+
402
+ if (config.auth.spotify?.clientId && config.auth.spotify?.clientSecret) {
403
+ const { createSpotifyProvider } = await import("./auth");
404
+ oauthProviders.push(createSpotifyProvider(config.auth.spotify));
405
+ }
406
+
205
407
  const authRoutes = createAuthRoutes({
206
408
  authRepo: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
207
409
  emailService: authConfigResult.emailService as import("./email").EmailService,
208
410
  emailConfig: config.auth.email,
209
411
  allowRegistration: config.auth.allowRegistration ?? false,
210
- defaultRole: config.auth.defaultRole
412
+ defaultRole: config.auth.defaultRole,
413
+ oauthProviders
211
414
  });
212
415
  config.app.route(`${basePath}/auth`, authRoutes);
213
416
 
214
- const adminRoutes = createAdminRoutes({
417
+ const adminRoutes = createAdminRoutes({
215
418
  authRepo: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
216
419
  emailService: authConfigResult.emailService as import("./email").EmailService,
217
420
  emailConfig: config.auth.email,
421
+ serviceKey
218
422
  });
219
423
  config.app.route(`${basePath}/admin`, adminRoutes);
220
424
  }
@@ -223,30 +427,68 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
223
427
  if (process.env.NODE_ENV !== "production") {
224
428
  const { createSchemaEditorRoutes } = await import("./api/schema-editor-routes");
225
429
  const schemaEditorRoutes = createSchemaEditorRoutes(config.collectionsDir);
226
-
430
+
227
431
  if (config.auth?.requireAuth !== false && !!config.auth?.jwtSecret) {
228
432
  schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
229
433
  }
230
-
434
+
231
435
  config.app.route(`${basePath}/schema-editor`, schemaEditorRoutes);
232
- console.log(`✅ Schema Editor mounted at ${basePath}/schema-editor`);
436
+ logger.info("Schema Editor mounted", { path: `${basePath}/schema-editor` });
233
437
  }
234
438
  }
235
439
 
236
440
  if (storageController) {
441
+ // Storage uploads get their own body limit, derived from the storage config's
442
+ // maxFileSize (default 50MB), which overrides the global API body limit.
443
+ const storageMaxSize = (
444
+ config.storage && typeof config.storage === "object" && "type" in config.storage
445
+ ? (config.storage as BackendStorageConfig & { maxFileSize?: number }).maxFileSize
446
+ : undefined
447
+ ) ?? 50 * 1024 * 1024;
448
+
237
449
  const storageRoutes = createStorageRoutes({
238
450
  controller: storageController,
239
451
  requireAuth: config.auth?.requireAuth ?? true
240
452
  });
453
+
454
+ // Apply a permissive body limit specifically for the upload endpoint
455
+ storageRoutes.use("/upload", bodyLimit({
456
+ maxSize: storageMaxSize,
457
+ onError: (c) => {
458
+ return c.json({
459
+ error: {
460
+ message: `File too large. Maximum upload size is ${Math.round(storageMaxSize / 1024 / 1024)}MB.`,
461
+ code: "PAYLOAD_TOO_LARGE"
462
+ }
463
+ }, 413);
464
+ }
465
+ }));
466
+
241
467
  config.app.route(`${basePath}/storage`, storageRoutes);
242
468
  }
243
469
 
244
470
  if (activeCollections.length > 0) {
245
471
  const dataRouter = new Hono<HonoEnv>();
472
+ dataRouter.onError(errorHandler);
473
+
474
+ // Secure by default: require auth when auth is configured.
475
+ // Developers who intentionally want public data access (relying
476
+ // entirely on Postgres RLS) must explicitly set `auth.requireAuth: false`.
477
+ const dataRequireAuth = config.auth?.requireAuth !== false;
478
+
479
+ if (!dataRequireAuth) {
480
+ logger.warn(
481
+ "Data routes running WITHOUT authentication enforcement. " +
482
+ "Access control is fully delegated to Postgres RLS policies. " +
483
+ "If no RLS policies exist, data is publicly accessible. " +
484
+ "Set auth.requireAuth to true (or remove it) to require authentication."
485
+ );
486
+ }
246
487
 
247
488
  dataRouter.use("/*", createAuthMiddleware({
248
489
  driver: defaultDriver,
249
- requireAuth: false // true BaaS — delegate completely to Postgres RLS
490
+ requireAuth: dataRequireAuth,
491
+ serviceKey
250
492
  }));
251
493
 
252
494
  // Mount history routes BEFORE the REST API subcollection catch-all so
@@ -266,6 +508,70 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
266
508
  config.app.route(`${basePath}/data`, dataRouter);
267
509
  }
268
510
 
511
+ // ── OpenAPI / Swagger ─────────────────────────────────────────────────
512
+ if (config.enableSwagger !== false && activeCollections.length > 0) {
513
+ const { generateOpenApiSpec } = await import("./api/openapi-generator");
514
+
515
+ config.app.get(`${basePath}/docs`, (c) => {
516
+ const spec = generateOpenApiSpec(activeCollections, {
517
+ basePath,
518
+ requireAuth: config.auth?.requireAuth !== false
519
+ });
520
+ return c.json(spec);
521
+ });
522
+
523
+ if (process.env.NODE_ENV !== "production") {
524
+ config.app.get(`${basePath}/swagger`, (c) => {
525
+ return c.html(`<!DOCTYPE html>
526
+ <html>
527
+ <head>
528
+ <title>Rebase API Documentation</title>
529
+ <meta charset="utf-8"/>
530
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
531
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css"/>
532
+ <style>body{margin:0;padding:0;}</style>
533
+ </head>
534
+ <body>
535
+ <div id="swagger-ui"></div>
536
+ <script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
537
+ <script>SwaggerUIBundle({ url: '${basePath}/docs', dom_id: '#swagger-ui' });</script>
538
+ </body>
539
+ </html>`);
540
+ });
541
+ logger.info("Swagger UI available", { path: `${basePath}/swagger` });
542
+ }
543
+ }
544
+
545
+ // ─── Server-side singleton ────────────────────────────────────────────
546
+ // Build the admin-level RebaseClient and expose it as the `rebase` singleton.
547
+ // This client bypasses the network and uses Hono's internal request handler.
548
+ // websocketUrl is explicitly empty to prevent opening a WebSocket connection.
549
+ const serverClient = createRebaseClient({
550
+ baseUrl: "http://localhost",
551
+ apiPath: basePath,
552
+ websocketUrl: "",
553
+ fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
554
+ return await config.app.request(input as string | Request | URL, init);
555
+ }
556
+ });
557
+
558
+ // Attach email service to the server client when configured.
559
+ // The email service may come from the auth bootstrapper or from the auth config directly.
560
+ let emailService: EmailService | undefined;
561
+ if (authConfigResult?.emailService) {
562
+ emailService = authConfigResult.emailService as EmailService;
563
+ } else if (config.auth?.email) {
564
+ emailService = createEmailService(config.auth.email);
565
+ }
566
+
567
+ if (emailService) {
568
+ Object.assign(serverClient, { email: emailService });
569
+ logger.info("Email service attached to singleton", { configured: emailService.isConfigured() });
570
+ }
571
+
572
+ _initRebase(serverClient);
573
+ logger.info("Rebase singleton initialized");
574
+
269
575
  // 5. Mount Custom Functions
270
576
  if (config.functionsDir) {
271
577
  const { loadFunctionsFromDirectory } = await import("./functions/function-loader");
@@ -275,17 +581,67 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
275
581
 
276
582
  if (loadedFunctions.length > 0) {
277
583
  const functionsRouter = new Hono<HonoEnv>();
584
+ functionsRouter.onError(errorHandler);
585
+
586
+ // Custom functions follow the same auth policy as data routes.
587
+ // Per-route auth can be further refined inside individual functions.
588
+ const functionsRequireAuth = config.auth?.requireAuth !== false;
278
589
 
279
- // Apply auth middleware — delegates to RLS, per-route auth is up to the function
280
590
  functionsRouter.use("/*", createAuthMiddleware({
281
591
  driver: defaultDriver,
282
- requireAuth: false
592
+ requireAuth: functionsRequireAuth,
593
+ serviceKey
283
594
  }));
284
595
 
285
596
  const fnRoutes = createFunctionRoutes(loadedFunctions);
286
597
  functionsRouter.route("/", fnRoutes);
287
598
  config.app.route(`${basePath}/functions`, functionsRouter);
288
- console.log(`⚡ Mounted ${loadedFunctions.length} custom function(s) at ${basePath}/functions`);
599
+ logger.info("Mounted custom functions", { count: loadedFunctions.length,
600
+ path: `${basePath}/functions` });
601
+ }
602
+ }
603
+
604
+ // 6. Mount Cron Jobs
605
+ let cronScheduler: import("./cron").CronScheduler | undefined;
606
+ if (config.cronsDir) {
607
+ const { loadCronJobsFromDirectory } = await import("./cron/cron-loader");
608
+ const { CronScheduler } = await import("./cron/cron-scheduler");
609
+ const { createCronRoutes } = await import("./cron/cron-routes");
610
+ const { createCronStore } = await import("./cron/cron-store");
611
+
612
+ const loadedCronJobs = await loadCronJobsFromDirectory(config.cronsDir);
613
+
614
+ if (loadedCronJobs.length > 0) {
615
+ cronScheduler = new CronScheduler();
616
+
617
+ // The cron scheduler uses the same serverClient as the singleton.
618
+ // ctx.client inside cron handlers IS the same `rebase` instance.
619
+ cronScheduler.setClient(serverClient);
620
+
621
+ cronScheduler.registerJobs(loadedCronJobs);
622
+
623
+ // Attach database persistence if the driver supports SQL
624
+ const store = createCronStore(defaultDriver);
625
+ if (store) {
626
+ await store.ensureTable();
627
+ cronScheduler.setStore(store);
628
+ }
629
+
630
+ const cronRouter = new Hono<HonoEnv>();
631
+
632
+ // Cron admin routes require authentication + admin role
633
+ if (config.auth?.requireAuth !== false && !!config.auth?.jwtSecret) {
634
+ cronRouter.use("/*", requireAuth, requireAdmin);
635
+ }
636
+
637
+ cronRouter.route("/", createCronRoutes(cronScheduler));
638
+ config.app.route(`${basePath}/cron`, cronRouter);
639
+
640
+ // Start the scheduler
641
+ cronScheduler.start();
642
+
643
+ logger.info("Mounted cron jobs", { count: loadedCronJobs.length,
644
+ path: `${basePath}/cron` });
289
645
  }
290
646
  }
291
647
 
@@ -293,7 +649,66 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
293
649
  await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth);
294
650
  }
295
651
 
296
- console.log("Rebase Backend Initialized");
652
+ logger.info("Rebase Backend Initialized");
653
+
654
+ // ── Deep Health Check ─────────────────────────────────────────────────
655
+ const healthCheck = async (): Promise<HealthCheckResult> => {
656
+ const start = performance.now();
657
+ try {
658
+ // Use admin.executeSql if available (Postgres), otherwise try fetchCollection as a probe
659
+ const admin = defaultDriver.admin;
660
+ if (isSQLAdmin(admin)) {
661
+ await admin.executeSql("SELECT 1");
662
+ } else {
663
+ // Fallback: try a lightweight fetch to confirm driver is responsive
664
+ await defaultDriver.fetchCollection({ path: "__health_check_nonexistent__",
665
+ limit: 1 });
666
+ }
667
+ const latencyMs = Math.round(performance.now() - start);
668
+ return { healthy: true,
669
+ latencyMs };
670
+ } catch (error: unknown) {
671
+ const latencyMs = Math.round(performance.now() - start);
672
+ logger.error("Health check failed", {
673
+ error: error instanceof Error ? error : new Error(String(error)),
674
+ latencyMs
675
+ });
676
+ return {
677
+ healthy: false,
678
+ latencyMs,
679
+ details: {
680
+ error: error instanceof Error ? error.message : String(error)
681
+ }
682
+ };
683
+ }
684
+ };
685
+
686
+ // ── Graceful Shutdown ─────────────────────────────────────────────────
687
+ const shutdown = (timeoutMs = 15_000): Promise<void> => {
688
+ return new Promise<void>((resolve) => {
689
+ logger.info("Shutting down Rebase Backend...");
690
+
691
+ // 1. Stop cron scheduler
692
+ if (cronScheduler) {
693
+ cronScheduler.stop();
694
+ logger.info("Cron scheduler stopped");
695
+ }
696
+
697
+ // 2. Close the HTTP server (stop accepting, drain in-flight)
698
+ config.server.close(() => {
699
+ logger.info("HTTP server closed");
700
+ resolve();
701
+ });
702
+
703
+ // 3. Force-resolve after timeout (unless disabled with 0)
704
+ if (timeoutMs > 0) {
705
+ setTimeout(() => {
706
+ logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
707
+ resolve();
708
+ }, timeoutMs).unref();
709
+ }
710
+ });
711
+ };
297
712
 
298
713
  return {
299
714
  driverRegistry,
@@ -304,6 +719,9 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
304
719
  history: historyConfigResult,
305
720
  storageRegistry,
306
721
  storageController,
307
- collectionRegistry
722
+ collectionRegistry,
723
+ cronScheduler,
724
+ healthCheck,
725
+ shutdown
308
726
  };
309
727
  }