@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.
- package/dist/common/src/collections/CollectionRegistry.d.ts +8 -0
- package/dist/common/src/util/entities.d.ts +22 -0
- package/dist/common/src/util/relations.d.ts +14 -4
- package/dist/common/src/util/resolutions.d.ts +1 -1
- package/dist/index-DXVBFp5V.js +37 -0
- package/dist/index-DXVBFp5V.js.map +1 -0
- package/dist/index.es.js +49931 -55
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +28475 -34569
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/api/openapi-generator.d.ts +15 -1
- package/dist/server-core/src/api/types.d.ts +1 -1
- package/dist/server-core/src/auth/admin-routes.d.ts +10 -1
- package/dist/server-core/src/auth/apple-oauth.d.ts +30 -0
- package/dist/server-core/src/auth/bitbucket-oauth.d.ts +11 -0
- package/dist/server-core/src/auth/discord-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/facebook-oauth.d.ts +14 -0
- package/dist/server-core/src/auth/github-oauth.d.ts +15 -0
- package/dist/server-core/src/auth/gitlab-oauth.d.ts +13 -0
- package/dist/server-core/src/auth/google-oauth.d.ts +5 -11
- package/dist/server-core/src/auth/index.d.ts +13 -2
- package/dist/server-core/src/auth/interfaces.d.ts +45 -6
- package/dist/server-core/src/auth/jwt.d.ts +1 -0
- package/dist/server-core/src/auth/linkedin-oauth.d.ts +18 -0
- package/dist/server-core/src/auth/microsoft-oauth.d.ts +16 -0
- package/dist/server-core/src/auth/middleware.d.ts +36 -11
- package/dist/server-core/src/auth/rate-limiter.d.ts +2 -2
- package/dist/server-core/src/auth/routes.d.ts +13 -3
- package/dist/server-core/src/auth/slack-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/spotify-oauth.d.ts +12 -0
- package/dist/server-core/src/auth/twitter-oauth.d.ts +18 -0
- package/dist/server-core/src/cron/cron-loader.d.ts +17 -0
- package/dist/server-core/src/cron/cron-routes.d.ts +14 -0
- package/dist/server-core/src/cron/cron-scheduler.d.ts +61 -0
- package/dist/server-core/src/cron/cron-store.d.ts +32 -0
- package/dist/server-core/src/cron/index.d.ts +6 -0
- package/dist/server-core/src/email/index.d.ts +2 -2
- package/dist/server-core/src/email/templates.d.ts +9 -0
- package/dist/server-core/src/email/types.d.ts +20 -23
- package/dist/server-core/src/history/history-routes.d.ts +2 -2
- package/dist/server-core/src/index.d.ts +5 -0
- package/dist/server-core/src/init.d.ts +112 -2
- package/dist/server-core/src/singleton.d.ts +35 -0
- package/dist/server-core/src/storage/LocalStorageController.d.ts +9 -9
- package/dist/server-core/src/storage/S3StorageController.d.ts +8 -8
- package/dist/server-core/src/storage/index.d.ts +18 -11
- package/dist/server-core/src/storage/routes.d.ts +3 -3
- package/dist/server-core/src/storage/types.d.ts +28 -16
- package/dist/server-core/src/utils/dev-port.d.ts +35 -0
- package/dist/server-core/src/utils/logger.d.ts +31 -0
- package/dist/server-core/src/utils/request-logger.d.ts +19 -0
- package/dist/types/src/controllers/auth.d.ts +2 -0
- package/dist/types/src/controllers/client.d.ts +119 -7
- package/dist/types/src/controllers/collection_registry.d.ts +4 -3
- package/dist/types/src/controllers/customization_controller.d.ts +7 -1
- package/dist/types/src/controllers/data.d.ts +34 -7
- package/dist/types/src/controllers/data_driver.d.ts +20 -28
- package/dist/types/src/controllers/database_admin.d.ts +2 -2
- package/dist/types/src/controllers/email.d.ts +34 -0
- package/dist/types/src/controllers/index.d.ts +1 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +4 -4
- package/dist/types/src/controllers/navigation.d.ts +5 -5
- package/dist/types/src/controllers/registry.d.ts +6 -3
- package/dist/types/src/controllers/side_entity_controller.d.ts +7 -6
- package/dist/types/src/controllers/storage.d.ts +24 -26
- package/dist/types/src/rebase_context.d.ts +8 -4
- package/dist/types/src/types/backend.d.ts +4 -1
- package/dist/types/src/types/builders.d.ts +5 -4
- package/dist/types/src/types/chips.d.ts +1 -1
- package/dist/types/src/types/collections.d.ts +169 -125
- package/dist/types/src/types/cron.d.ts +102 -0
- package/dist/types/src/types/data_source.d.ts +1 -1
- package/dist/types/src/types/entity_actions.d.ts +8 -8
- package/dist/types/src/types/entity_callbacks.d.ts +15 -15
- package/dist/types/src/types/entity_link_builder.d.ts +1 -1
- package/dist/types/src/types/entity_overrides.d.ts +2 -1
- package/dist/types/src/types/entity_views.d.ts +8 -8
- package/dist/types/src/types/export_import.d.ts +3 -3
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/plugins.d.ts +72 -18
- package/dist/types/src/types/properties.d.ts +118 -33
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/slots.d.ts +30 -6
- package/dist/types/src/types/translations.d.ts +44 -0
- package/dist/types/src/types/user_management_delegate.d.ts +1 -0
- package/package.json +84 -84
- package/scratch.ts +6 -5
- package/src/api/ast-schema-editor.ts +11 -11
- package/src/api/collections_for_test/callbacks_test_collection.ts +5 -2
- package/src/api/errors.ts +38 -14
- package/src/api/graphql/graphql-schema-generator.ts +5 -3
- package/src/api/openapi-generator.ts +613 -58
- package/src/api/rest/api-generator.ts +17 -11
- package/src/api/rest/query-parser.ts +1 -1
- package/src/api/schema-editor-routes.ts +2 -0
- package/src/api/server.ts +8 -5
- package/src/api/types.ts +1 -1
- package/src/auth/admin-routes.ts +58 -17
- package/src/auth/apple-oauth.ts +130 -0
- package/src/auth/bitbucket-oauth.ts +82 -0
- package/src/auth/discord-oauth.ts +83 -0
- package/src/auth/facebook-oauth.ts +72 -0
- package/src/auth/github-oauth.ts +110 -0
- package/src/auth/gitlab-oauth.ts +70 -0
- package/src/auth/google-oauth.ts +34 -46
- package/src/auth/index.ts +15 -2
- package/src/auth/interfaces.ts +54 -7
- package/src/auth/jwt.ts +22 -5
- package/src/auth/linkedin-oauth.ts +81 -0
- package/src/auth/microsoft-oauth.ts +88 -0
- package/src/auth/middleware.ts +194 -45
- package/src/auth/password.ts +3 -1
- package/src/auth/rate-limiter.ts +4 -4
- package/src/auth/routes.ts +197 -139
- package/src/auth/slack-oauth.ts +71 -0
- package/src/auth/spotify-oauth.ts +67 -0
- package/src/auth/twitter-oauth.ts +120 -0
- package/src/collections/loader.ts +6 -6
- package/src/cron/cron-loader.ts +89 -0
- package/src/cron/cron-routes.test.ts +265 -0
- package/src/cron/cron-routes.ts +85 -0
- package/src/cron/cron-scheduler.test.ts +421 -0
- package/src/cron/cron-scheduler.ts +413 -0
- package/src/cron/cron-store.ts +163 -0
- package/src/cron/index.ts +6 -0
- package/src/db/interfaces.ts +2 -2
- package/src/email/index.ts +3 -2
- package/src/email/smtp-email-service.ts +5 -2
- package/src/email/templates.ts +93 -6
- package/src/email/types.ts +17 -24
- package/src/functions/function-loader.ts +34 -6
- package/src/functions/function-routes.ts +2 -2
- package/src/history/history-routes.ts +4 -3
- package/src/index.ts +10 -0
- package/src/init.ts +475 -57
- package/src/services/driver-registry.ts +1 -1
- package/src/singleton.test.ts +28 -0
- package/src/singleton.ts +70 -0
- package/src/storage/LocalStorageController.ts +50 -53
- package/src/storage/S3StorageController.ts +49 -46
- package/src/storage/index.ts +27 -16
- package/src/storage/routes.ts +76 -59
- package/src/storage/storage-registry.ts +5 -5
- package/src/storage/types.ts +44 -32
- package/src/types/index.ts +4 -4
- package/src/utils/dev-port.ts +176 -0
- package/src/utils/logger.ts +143 -0
- package/src/utils/logging.ts +5 -2
- package/src/utils/request-logger.ts +66 -0
- package/test/admin-routes.test.ts +114 -65
- package/test/api-generator.test.ts +73 -30
- package/test/ast-schema-editor.test.ts +5 -3
- package/test/auth-middleware-hono.test.ts +268 -33
- package/test/auth-routes.test.ts +325 -146
- package/test/driver-registry.test.ts +4 -2
- package/test/error-propagation.test.ts +226 -0
- package/test/errors.test.ts +12 -7
- package/test/jwt-security.test.ts +18 -9
- package/test/jwt.test.ts +26 -13
- package/test/middleware.test.ts +13 -8
- package/test/query-parser.test.ts +13 -8
- package/test/rate-limiter.test.ts +7 -7
- package/test/safe-compare.test.ts +66 -0
- package/test/singleton.test.ts +59 -0
- package/test/storage-local.test.ts +36 -43
- package/test/storage-registry.test.ts +38 -36
- package/test/storage-routes.test.ts +9 -5
- package/test/storage-s3.test.ts +51 -48
- package/test.ts +6 -0
- package/tsconfig.json +1 -1
- package/vite.config.ts +7 -5
- package/dist/index-BeMqpmfQ.js +0 -239
- package/dist/index-BeMqpmfQ.js.map +0 -1
- package/dist/index-bl4J3lNb.js +0 -55823
- package/dist/index-bl4J3lNb.js.map +0 -1
- 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
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
269
|
+
logger.info("Bootstrapping authentication via driver protocol");
|
|
159
270
|
authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
|
|
160
|
-
|
|
271
|
+
logger.info("Authentication initialized");
|
|
161
272
|
} else {
|
|
162
|
-
|
|
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
|
-
|
|
280
|
+
logger.info("Bootstrapping entity history via driver protocol");
|
|
170
281
|
historyConfigResult = await defaultBootstrapper.initializeHistory(config.history, defaultDriverResult);
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
305
|
+
logger.info("Configuring storage");
|
|
183
306
|
const controllers: Record<string, StorageController> = {};
|
|
184
307
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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
|
-
|
|
337
|
+
logger.info("Initialized storage backends", { count: Object.keys(controllers).length });
|
|
198
338
|
}
|
|
199
339
|
}
|
|
200
340
|
|
|
201
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|