@rebasepro/server-core 0.6.1 → 0.8.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.
- package/dist/api/graphql/graphql-schema-generator.d.ts +9 -0
- package/dist/api/rest/api-generator.d.ts +2 -14
- package/dist/api/rest/query-parser.d.ts +2 -5
- package/dist/api/types.d.ts +8 -2
- package/dist/auth/adapter-middleware.d.ts +7 -1
- package/dist/auth/admin-roles-route.d.ts +18 -0
- package/dist/auth/admin-users-route.d.ts +28 -0
- package/dist/auth/api-keys/api-key-types.d.ts +8 -0
- package/dist/auth/auth-hooks.d.ts +17 -0
- package/dist/auth/builtin-auth-adapter.d.ts +3 -1
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/interfaces.d.ts +32 -2
- package/dist/auth/magic-link-routes.d.ts +30 -0
- package/dist/auth/mfa-crypto.d.ts +23 -0
- package/dist/auth/mfa-routes.d.ts +2 -1
- package/dist/auth/middleware.d.ts +9 -2
- package/dist/auth/routes.d.ts +3 -1
- package/dist/auth/session-routes.d.ts +2 -0
- package/dist/backend-CIxN4FVm.js.map +1 -1
- package/dist/base64-js-C_frYBkI.js +1687 -0
- package/dist/base64-js-C_frYBkI.js.map +1 -0
- package/dist/{dist-CZKP-Xz4.js → dist-DMO-zF6D.js} +2 -2
- package/dist/{dist-CZKP-Xz4.js.map → dist-DMO-zF6D.js.map} +1 -1
- package/dist/email/index.d.ts +2 -2
- package/dist/email/templates.d.ts +8 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/functions/define-function.d.ts +50 -0
- package/dist/functions/index.d.ts +2 -0
- package/dist/index.d.ts +16 -18
- package/dist/index.es.js +1947 -2356
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +40615 -10203
- package/dist/index.umd.js.map +1 -1
- package/dist/init.d.ts +68 -12
- package/dist/jws-BqRRaK11.js +616 -0
- package/dist/jws-BqRRaK11.js.map +1 -0
- package/dist/{jwt-DHcQRGC3.js → jwt-BETC8a1J.js} +3 -611
- package/dist/jwt-BETC8a1J.js.map +1 -0
- package/dist/services/routed-realtime-service.d.ts +43 -0
- package/dist/{src-COaj0G3P.js → src-CB2PIpBe.js} +2 -2
- package/dist/{src-COaj0G3P.js.map → src-CB2PIpBe.js.map} +1 -1
- package/dist/src-DjzOT1kG.js +29746 -0
- package/dist/src-DjzOT1kG.js.map +1 -0
- package/dist/storage/GCSStorageController.d.ts +43 -0
- package/dist/storage/index.d.ts +5 -2
- package/dist/storage/routes.d.ts +33 -1
- package/dist/storage/tus-handler.d.ts +3 -1
- package/dist/storage/types.d.ts +28 -3
- package/dist/utils/dev-port.d.ts +2 -0
- package/package.json +13 -5
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +50 -15
- package/src/api/openapi-generator.ts +2 -2
- package/src/api/rest/api-generator.ts +44 -123
- package/src/api/rest/query-parser.ts +6 -23
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +8 -2
- package/src/auth/adapter-middleware.ts +10 -2
- package/src/auth/admin-roles-route.ts +36 -0
- package/src/auth/admin-users-route.ts +302 -0
- package/src/auth/api-keys/api-key-middleware.ts +4 -3
- package/src/auth/api-keys/api-key-routes.ts +12 -2
- package/src/auth/api-keys/api-key-store.ts +83 -66
- package/src/auth/api-keys/api-key-types.ts +8 -0
- package/src/auth/apple-oauth.ts +2 -1
- package/src/auth/auth-hooks.ts +21 -0
- package/src/auth/bitbucket-oauth.ts +2 -1
- package/src/auth/builtin-auth-adapter.ts +29 -6
- package/src/auth/custom-auth-adapter.ts +2 -0
- package/src/auth/discord-oauth.ts +2 -1
- package/src/auth/facebook-oauth.ts +2 -1
- package/src/auth/github-oauth.ts +2 -1
- package/src/auth/gitlab-oauth.ts +2 -1
- package/src/auth/google-oauth.ts +8 -4
- package/src/auth/index.ts +2 -0
- package/src/auth/interfaces.ts +38 -2
- package/src/auth/linkedin-oauth.ts +2 -1
- package/src/auth/magic-link-routes.ts +167 -0
- package/src/auth/mfa-crypto.ts +91 -0
- package/src/auth/mfa-routes.ts +34 -10
- package/src/auth/microsoft-oauth.ts +2 -1
- package/src/auth/middleware.ts +14 -5
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +78 -10
- package/src/auth/session-routes.ts +15 -3
- package/src/auth/slack-oauth.ts +2 -1
- package/src/auth/spotify-oauth.ts +2 -1
- package/src/auth/twitter-oauth.ts +8 -1
- package/src/cron/cron-store.ts +25 -23
- package/src/email/index.ts +3 -2
- package/src/email/templates.ts +82 -0
- package/src/email/types.ts +16 -0
- package/src/functions/define-function.ts +59 -0
- package/src/functions/function-loader.ts +16 -0
- package/src/functions/index.ts +2 -0
- package/src/index.ts +70 -37
- package/src/init.ts +214 -25
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/storage/GCSStorageController.ts +334 -0
- package/src/storage/index.ts +9 -3
- package/src/storage/routes.ts +191 -23
- package/src/storage/tus-handler.ts +16 -4
- package/src/storage/types.ts +25 -3
- package/src/utils/dev-port.ts +13 -7
- package/test/api-generator.test.ts +1 -1
- package/test/auth-config-types.test.ts +40 -0
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- package/test/define-function.test.ts +45 -0
- package/test/env.test.ts +9 -19
- package/test/multi-datasource-routing.test.ts +113 -0
- package/test/routed-realtime-service.test.ts +86 -0
- package/test/storage-routes.test.ts +160 -0
- package/test/transform-auth-response.test.ts +305 -0
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/test/backend-hooks-data.test.ts +0 -477
package/src/init.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AuthAdapter,
|
|
3
3
|
BackendBootstrapper,
|
|
4
|
-
BackendHooks,
|
|
5
4
|
BootstrappedAuth,
|
|
6
5
|
DatabaseAdapter,
|
|
7
6
|
DataDriver,
|
|
7
|
+
DataSourceDefinition,
|
|
8
|
+
EntityCallbacks,
|
|
8
9
|
EntityCollection,
|
|
9
10
|
HealthCheckResult,
|
|
10
11
|
InitializedDriver,
|
|
@@ -13,9 +14,11 @@ import {
|
|
|
13
14
|
RealtimeProvider,
|
|
14
15
|
SecurityRule
|
|
15
16
|
} from "@rebasepro/types";
|
|
17
|
+
import { createDataSourceRegistry, resolveDataSource } from "@rebasepro/common";
|
|
16
18
|
import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
|
|
17
19
|
import { loadCollectionsFromDirectory } from "./collections/loader";
|
|
18
20
|
import { DEFAULT_DRIVER_ID, DefaultDriverRegistry, DriverRegistry } from "./services/driver-registry";
|
|
21
|
+
import { createRoutedRealtimeService } from "./services/routed-realtime-service";
|
|
19
22
|
import { Server } from "http";
|
|
20
23
|
|
|
21
24
|
import { RestApiGenerator } from "./api/rest/api-generator";
|
|
@@ -80,7 +83,7 @@ export interface RebaseAuthConfig {
|
|
|
80
83
|
*
|
|
81
84
|
* When a request includes `Authorization: Bearer <serviceKey>`, it is
|
|
82
85
|
* granted admin-level access without JWT verification. This is the
|
|
83
|
-
* Rebase equivalent of a
|
|
86
|
+
* Rebase equivalent of a Service Account key.
|
|
84
87
|
*
|
|
85
88
|
* Generate with: `node -e "logger.info(require('crypto').randomBytes(48).toString('base64'))"`
|
|
86
89
|
*
|
|
@@ -89,6 +92,14 @@ export interface RebaseAuthConfig {
|
|
|
89
92
|
*/
|
|
90
93
|
serviceKey?: string;
|
|
91
94
|
email?: EmailConfig;
|
|
95
|
+
// ── Convenience shortcuts ─────────────────────────────────────────
|
|
96
|
+
// Each named field below is syntactic sugar that internally resolves
|
|
97
|
+
// to an `OAuthProvider` via the corresponding `create*Provider`
|
|
98
|
+
// factory at startup. They are equivalent to constructing the
|
|
99
|
+
// provider manually and passing it in the `providers` array.
|
|
100
|
+
//
|
|
101
|
+
// For providers not listed here, or for full control over the
|
|
102
|
+
// provider configuration, use the `providers` array directly.
|
|
92
103
|
google?: { clientId: string; clientSecret?: string };
|
|
93
104
|
linkedin?: { clientId: string; clientSecret: string };
|
|
94
105
|
github?: { clientId: string; clientSecret: string };
|
|
@@ -102,8 +113,31 @@ export interface RebaseAuthConfig {
|
|
|
102
113
|
slack?: { clientId: string; clientSecret: string };
|
|
103
114
|
spotify?: { clientId: string; clientSecret: string };
|
|
104
115
|
defaultRole?: string;
|
|
105
|
-
|
|
106
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Canonical array of OAuth providers.
|
|
118
|
+
*
|
|
119
|
+
* This is the primary extension point for **all** OAuth integrations.
|
|
120
|
+
* Each entry is an `OAuthProvider<unknown>` constructed via one of
|
|
121
|
+
* the `create*Provider` factories exported from `@rebasepro/server-core`
|
|
122
|
+
* (e.g. `createGoogleProvider`, `createGitHubProvider`).
|
|
123
|
+
*
|
|
124
|
+
* The named convenience fields above (`google`, `github`, etc.) are
|
|
125
|
+
* automatically resolved into this array at startup. You can mix both
|
|
126
|
+
* approaches; named fields and explicit entries are merged (named
|
|
127
|
+
* fields are appended after explicit entries).
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { createGoogleProvider } from "@rebasepro/server-core";
|
|
132
|
+
*
|
|
133
|
+
* auth: {
|
|
134
|
+
* providers: [
|
|
135
|
+
* createGoogleProvider({ clientId: "…", clientSecret: "…" }),
|
|
136
|
+
* ],
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
providers?: OAuthProvider<unknown>[];
|
|
107
141
|
/**
|
|
108
142
|
* Override specific parts of the built-in auth implementation.
|
|
109
143
|
*
|
|
@@ -123,7 +157,11 @@ export interface RebaseAuthConfig {
|
|
|
123
157
|
*/
|
|
124
158
|
hooks?: AuthHooks;
|
|
125
159
|
|
|
126
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Enable magic link (passwordless email) authentication.
|
|
162
|
+
* Requires email to be configured.
|
|
163
|
+
*/
|
|
164
|
+
magicLink?: boolean;
|
|
127
165
|
}
|
|
128
166
|
|
|
129
167
|
export interface RebaseBackendConfig {
|
|
@@ -133,6 +171,16 @@ export interface RebaseBackendConfig {
|
|
|
133
171
|
app: Hono<HonoEnv>;
|
|
134
172
|
basePath?: string;
|
|
135
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Declared data sources, shared with the frontend `<Rebase dataSources>`.
|
|
176
|
+
*
|
|
177
|
+
* Used to resolve each collection's engine (capabilities) and transport.
|
|
178
|
+
* Collections on a `direct`/`custom` transport are client-only: the backend
|
|
179
|
+
* still owns their schema/registry but does **not** generate server data
|
|
180
|
+
* routes for them. Server-mediated sources (the default) need no entry.
|
|
181
|
+
*/
|
|
182
|
+
dataSources?: DataSourceDefinition[];
|
|
183
|
+
|
|
136
184
|
/**
|
|
137
185
|
* Database bootstrappers.
|
|
138
186
|
*/
|
|
@@ -169,11 +217,22 @@ export interface RebaseBackendConfig {
|
|
|
169
217
|
/**
|
|
170
218
|
* Storage configuration. Accepts:
|
|
171
219
|
*
|
|
172
|
-
* - A `BackendStorageConfig` object (`{ type: 'local' | 's3', ... }`)
|
|
173
|
-
* - A `StorageController` instance (for custom providers like
|
|
220
|
+
* - A `BackendStorageConfig` object (`{ type: 'local' | 's3' | 'gcs', ... }`)
|
|
221
|
+
* - A `StorageController` instance (for custom providers like Azure, etc.)
|
|
174
222
|
* - A `Record<string, ...>` of either, for multi-backend setups
|
|
175
223
|
*/
|
|
176
224
|
storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Declared storage sources. Drives the client-side StorageSourceRegistry
|
|
228
|
+
* and the transport distinction (server vs direct).
|
|
229
|
+
*
|
|
230
|
+
* Server-backed sources are auto-derived from the `storage` map — you
|
|
231
|
+
* only need explicit entries for "direct" transport sources (e.g.
|
|
232
|
+
* external storage) that the backend does not proxy.
|
|
233
|
+
*/
|
|
234
|
+
storageSources?: import("@rebasepro/types").StorageSourceDefinition[];
|
|
235
|
+
|
|
177
236
|
history?: unknown;
|
|
178
237
|
/**
|
|
179
238
|
* Default security rules applied to any collection that does not define
|
|
@@ -223,14 +282,24 @@ export interface RebaseBackendConfig {
|
|
|
223
282
|
origin: string | string[] | ((origin: string) => boolean);
|
|
224
283
|
};
|
|
225
284
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
285
|
+
* Global lifecycle callbacks applied to every collection.
|
|
286
|
+
*
|
|
287
|
+
* Same type as per-collection `callbacks` — fires on **every** data path
|
|
288
|
+
* (REST API, WebSocket / realtime, server-side `rebase.data`).
|
|
229
289
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
290
|
+
* Execution order: global callbacks → collection callbacks → property callbacks.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* ```ts
|
|
294
|
+
* callbacks: {
|
|
295
|
+
* afterRead({ entity, collection }) {
|
|
296
|
+
* console.log(`Read ${collection.slug}/${entity.id}`);
|
|
297
|
+
* return entity;
|
|
298
|
+
* }
|
|
299
|
+
* }
|
|
300
|
+
* ```
|
|
232
301
|
*/
|
|
233
|
-
|
|
302
|
+
callbacks?: EntityCallbacks;
|
|
234
303
|
}
|
|
235
304
|
|
|
236
305
|
/**
|
|
@@ -313,6 +382,16 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
313
382
|
configureMiddlewares(config.app, basePath, isProduction, config);
|
|
314
383
|
|
|
315
384
|
const collectionRegistry = new BackendCollectionRegistry();
|
|
385
|
+
// Declared data sources — drives engine resolution (capabilities) and the
|
|
386
|
+
// server-vs-direct transport distinction. Set before collections register
|
|
387
|
+
// so normalization can resolve each collection's engine.
|
|
388
|
+
const dataSourceRegistry = createDataSourceRegistry(config.dataSources);
|
|
389
|
+
collectionRegistry.setDataSources(dataSourceRegistry);
|
|
390
|
+
|
|
391
|
+
// Global lifecycle callbacks — applied to every collection, on all data paths.
|
|
392
|
+
if (config.callbacks) {
|
|
393
|
+
collectionRegistry.setGlobalCallbacks(config.callbacks);
|
|
394
|
+
}
|
|
316
395
|
let activeCollections = config.collections || [];
|
|
317
396
|
if (config.collectionsDir && activeCollections.length === 0) {
|
|
318
397
|
activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
|
|
@@ -401,6 +480,56 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
401
480
|
const defaultBootstrapper = bootstrappers.find(b => b.id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
|
|
402
481
|
const defaultRealtimeService = defaultDriverResult.realtimeProvider;
|
|
403
482
|
|
|
483
|
+
// Resolve a collection path (e.g. "products", "authors/1/posts") to its
|
|
484
|
+
// data-source key — shared by the data-driver router and the realtime
|
|
485
|
+
// router. Falls back to the default key for unknown paths.
|
|
486
|
+
const keyForCollectionPath = (collectionPath: string): string => {
|
|
487
|
+
const slug = collectionPath.replace(/^\/+/, "").split("/")[0]?.split("?")[0];
|
|
488
|
+
if (!slug) return DEFAULT_DRIVER_ID;
|
|
489
|
+
const collection = collectionRegistry.get(slug) ?? collectionRegistry.getCollectionByPath(slug);
|
|
490
|
+
if (!collection) return DEFAULT_DRIVER_ID;
|
|
491
|
+
return resolveDataSource(collection, dataSourceRegistry).key;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// ── Data-source misconfiguration check ────────────────────────────────
|
|
495
|
+
// A server-transport collection whose resolved data-source key has no
|
|
496
|
+
// registered driver delegate would silently fall back to the default
|
|
497
|
+
// driver — i.e. land in the wrong database. Warn loudly so this surfaces
|
|
498
|
+
// at boot rather than as mysterious data going to the wrong engine.
|
|
499
|
+
{
|
|
500
|
+
const unresolved = new Map<string, string[]>();
|
|
501
|
+
const nonRlsEngines = new Set<string>();
|
|
502
|
+
for (const collection of activeCollections) {
|
|
503
|
+
const ds = resolveDataSource(collection, dataSourceRegistry);
|
|
504
|
+
if (ds.transport !== "server") continue; // direct/custom are client-only
|
|
505
|
+
// Server engines without row-level security enforce authorization
|
|
506
|
+
// only at the application layer — surface this so it isn't a
|
|
507
|
+
// silent assumption. (The default Postgres engine supports RLS.)
|
|
508
|
+
if (!ds.capabilities.supportsRLS) nonRlsEngines.add(ds.engine);
|
|
509
|
+
if (ds.key === DEFAULT_DRIVER_ID) continue; // always maps to the default
|
|
510
|
+
if (!driverRegistry.has(ds.key)) {
|
|
511
|
+
const slugs = unresolved.get(ds.key) ?? [];
|
|
512
|
+
slugs.push(collection.slug ?? collection.name ?? "?");
|
|
513
|
+
unresolved.set(ds.key, slugs);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
for (const [key, slugs] of unresolved) {
|
|
517
|
+
logger.warn(
|
|
518
|
+
`[DataSource] No driver registered for data source "${key}" ` +
|
|
519
|
+
`(used by: ${slugs.join(", ")}). These collections will fall back to the ` +
|
|
520
|
+
`default driver "${defaultDriverId}" — register a bootstrapper with this id, ` +
|
|
521
|
+
`or mark the data source as a direct/custom transport in \`dataSources\`.`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
for (const engine of nonRlsEngines) {
|
|
525
|
+
logger.warn(
|
|
526
|
+
`[DataSource] Engine "${engine}" does not support row-level security; ` +
|
|
527
|
+
`authorization for its collections is enforced only at the application layer ` +
|
|
528
|
+
`(authentication still applies). Ensure app-level checks or engine-native rules are in place.`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
404
533
|
// 2. Initialize Auth & History via the default driver's bootstrapper
|
|
405
534
|
let authConfigResult: BootstrappedAuth | undefined = undefined;
|
|
406
535
|
let serviceKey: string | undefined;
|
|
@@ -439,6 +568,23 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
439
568
|
}
|
|
440
569
|
}
|
|
441
570
|
|
|
571
|
+
// The built-in auth subsystem (users, sessions, repository) is
|
|
572
|
+
// bootstrapped on the DEFAULT driver. If the auth collection is
|
|
573
|
+
// routed to a non-default data source, login would read/write the
|
|
574
|
+
// default engine while the collection's data views hit another —
|
|
575
|
+
// a split-brain user store. Warn loudly.
|
|
576
|
+
if (safeAuthConfig.collection) {
|
|
577
|
+
const authDs = resolveDataSource(safeAuthConfig.collection, dataSourceRegistry);
|
|
578
|
+
if (authDs.key !== DEFAULT_DRIVER_ID) {
|
|
579
|
+
logger.warn(
|
|
580
|
+
`[Auth] The auth collection "${safeAuthConfig.collection.slug}" is on data source ` +
|
|
581
|
+
`"${authDs.key}", but the built-in auth system always uses the default data source. ` +
|
|
582
|
+
`Move the auth collection to the default data source, or replace auth with an AuthAdapter ` +
|
|
583
|
+
`that manages users in "${authDs.key}".`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
442
588
|
// Extract the collection-level auth config (if `auth` is an object, not just `true`)
|
|
443
589
|
const collectionAuth = safeAuthConfig.collection ? safeAuthConfig.collection.auth : undefined;
|
|
444
590
|
const collectionAuthConfig = (typeof collectionAuth === "object" && collectionAuth !== null) ? collectionAuth : undefined;
|
|
@@ -516,8 +662,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
516
662
|
|
|
517
663
|
if (!isAuthAdapter(config.auth)) {
|
|
518
664
|
const safeAuthConfig = config.auth as RebaseAuthConfig;
|
|
519
|
-
|
|
520
|
-
const oauthProviders: OAuthProvider<any>[] = [...(safeAuthConfig.providers || [])];
|
|
665
|
+
const oauthProviders: OAuthProvider<unknown>[] = [...(safeAuthConfig.providers || [])];
|
|
521
666
|
|
|
522
667
|
// Resolve configured OAuth providers via data-driven registration.
|
|
523
668
|
// Each entry maps a config key to its factory function name and required fields.
|
|
@@ -561,7 +706,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
561
706
|
oauthProviders,
|
|
562
707
|
serviceKey,
|
|
563
708
|
authHooks: safeAuthConfig.hooks,
|
|
564
|
-
collectionAuthConfig
|
|
709
|
+
collectionAuthConfig,
|
|
710
|
+
enableMagicLink: safeAuthConfig.magicLink ?? false
|
|
565
711
|
});
|
|
566
712
|
}
|
|
567
713
|
|
|
@@ -631,7 +777,10 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
631
777
|
|
|
632
778
|
const storageRoutes = createStorageRoutes({
|
|
633
779
|
controller: storageController,
|
|
634
|
-
|
|
780
|
+
registry: storageRegistry,
|
|
781
|
+
sources: config.storageSources,
|
|
782
|
+
requireAuth: resolveRequireAuth(config.auth),
|
|
783
|
+
authAdapter
|
|
635
784
|
});
|
|
636
785
|
|
|
637
786
|
// Apply a permissive body limit specifically for the upload endpoint
|
|
@@ -668,18 +817,39 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
668
817
|
);
|
|
669
818
|
}
|
|
670
819
|
|
|
820
|
+
// Multi-data-source routing: when more than one database engine is
|
|
821
|
+
// registered (e.g. Postgres + MongoDB in one instance), resolve the
|
|
822
|
+
// delegate per request from the request's collection data source. The
|
|
823
|
+
// auth middleware then scopes that delegate (RLS for Postgres, no-op
|
|
824
|
+
// for engines without `withAuth()`) into the request context. For a
|
|
825
|
+
// single-engine backend this is omitted — behaviour is unchanged.
|
|
826
|
+
const dataPathMarker = `${basePath}/data/`;
|
|
827
|
+
const resolveRequestDriver = (reqPath: string): DataDriver => {
|
|
828
|
+
const i = reqPath.indexOf(dataPathMarker);
|
|
829
|
+
const collectionPath = i >= 0 ? reqPath.slice(i + dataPathMarker.length) : reqPath;
|
|
830
|
+
const key = keyForCollectionPath(collectionPath);
|
|
831
|
+
// Use the authoritative default for the default key; otherwise the
|
|
832
|
+
// named delegate, falling back to default if it isn't registered.
|
|
833
|
+
if (!key || key === DEFAULT_DRIVER_ID) return defaultDriver;
|
|
834
|
+
return driverRegistry.get(key) ?? defaultDriver;
|
|
835
|
+
};
|
|
836
|
+
const multiEngine = bootstrappers.length > 1;
|
|
837
|
+
const resolveDriver = multiEngine ? ((c: { req: { path: string } }) => resolveRequestDriver(c.req.path)) : undefined;
|
|
838
|
+
|
|
671
839
|
// Use adapter middleware when an AuthAdapter is available,
|
|
672
840
|
// falling back to the built-in JWT middleware otherwise.
|
|
673
841
|
if (authAdapter) {
|
|
674
842
|
dataRouter.use("/*", createAdapterAuthMiddleware({
|
|
675
843
|
adapter: authAdapter,
|
|
676
844
|
driver: defaultDriver,
|
|
845
|
+
resolveDriver,
|
|
677
846
|
requireAuth: dataRequireAuth,
|
|
678
847
|
apiKeyStore
|
|
679
848
|
}));
|
|
680
849
|
} else {
|
|
681
850
|
dataRouter.use("/*", createAuthMiddleware({
|
|
682
851
|
driver: defaultDriver,
|
|
852
|
+
resolveDriver,
|
|
683
853
|
requireAuth: dataRequireAuth,
|
|
684
854
|
serviceKey,
|
|
685
855
|
apiKeyStore
|
|
@@ -702,10 +872,16 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
702
872
|
dataRouter.route("/", historyRoutes);
|
|
703
873
|
}
|
|
704
874
|
|
|
875
|
+
// Only generate server data routes for server-mediated collections.
|
|
876
|
+
// Collections on a direct/custom transport are client-only — the
|
|
877
|
+
// backend must not expose a (mis-engined) endpoint for them.
|
|
878
|
+
const serverCollections = activeCollections.filter(
|
|
879
|
+
(collection) => resolveDataSource(collection, dataSourceRegistry).transport === "server"
|
|
880
|
+
);
|
|
881
|
+
|
|
705
882
|
const restGenerator = new RestApiGenerator(
|
|
706
|
-
|
|
883
|
+
serverCollections,
|
|
707
884
|
defaultDriver,
|
|
708
|
-
config.hooks?.data,
|
|
709
885
|
authAdapter
|
|
710
886
|
);
|
|
711
887
|
dataRouter.route("/", restGenerator.generateRoutes());
|
|
@@ -793,9 +969,10 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
793
969
|
const functionsRouter = new Hono<HonoEnv>();
|
|
794
970
|
functionsRouter.onError(errorHandler);
|
|
795
971
|
|
|
796
|
-
// Custom functions
|
|
797
|
-
//
|
|
798
|
-
|
|
972
|
+
// Custom functions do NOT require authentication at the global level by default.
|
|
973
|
+
// This allows custom functions to define public endpoints (like webhooks).
|
|
974
|
+
// Per-route auth can be further refined inside individual functions using `requireAuth`.
|
|
975
|
+
const functionsRequireAuth = false;
|
|
799
976
|
|
|
800
977
|
// Use adapter middleware when available, fallback to built-in
|
|
801
978
|
if (authAdapter) {
|
|
@@ -876,8 +1053,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
876
1053
|
}
|
|
877
1054
|
}
|
|
878
1055
|
|
|
879
|
-
|
|
880
|
-
|
|
1056
|
+
// With multiple realtime-capable engines, route subscriptions to the
|
|
1057
|
+
// provider owning each collection (the realtime counterpart of the data
|
|
1058
|
+
// router). The single WebSocket server is driven by this composite.
|
|
1059
|
+
// Single-engine setups use the default provider unchanged.
|
|
1060
|
+
const effectiveRealtimeService: RealtimeProvider = Object.keys(realtimeServices).length > 1
|
|
1061
|
+
? createRoutedRealtimeService({
|
|
1062
|
+
providers: realtimeServices,
|
|
1063
|
+
defaultKey: defaultDriverId,
|
|
1064
|
+
resolveKey: keyForCollectionPath
|
|
1065
|
+
})
|
|
1066
|
+
: defaultRealtimeService as RealtimeProvider;
|
|
1067
|
+
|
|
1068
|
+
if (defaultBootstrapper.initializeWebsockets && effectiveRealtimeService) {
|
|
1069
|
+
await defaultBootstrapper.initializeWebsockets(config.server, effectiveRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
881
1070
|
}
|
|
882
1071
|
|
|
883
1072
|
logger.info("Rebase Backend Initialized");
|
|
@@ -896,7 +1085,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
896
1085
|
driverRegistry,
|
|
897
1086
|
driver: defaultDriver,
|
|
898
1087
|
realtimeServices,
|
|
899
|
-
realtimeService:
|
|
1088
|
+
realtimeService: effectiveRealtimeService,
|
|
900
1089
|
auth: authConfigResult,
|
|
901
1090
|
history: historyConfigResult,
|
|
902
1091
|
storageRegistry,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { RealtimeProvider, Entity } from "@rebasepro/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A realtime client message as forwarded by the WebSocket server.
|
|
5
|
+
*/
|
|
6
|
+
interface ClientMessage {
|
|
7
|
+
type: string;
|
|
8
|
+
payload?: Record<string, unknown>;
|
|
9
|
+
subscriptionId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The concrete realtime service surface the WebSocket server drives — the
|
|
14
|
+
* typed {@link RealtimeProvider} plus the client-connection methods that
|
|
15
|
+
* every engine's realtime service implements.
|
|
16
|
+
*/
|
|
17
|
+
export interface WsRealtimeService extends RealtimeProvider {
|
|
18
|
+
addClient(clientId: string, ws: unknown): void;
|
|
19
|
+
handleClientMessage(clientId: string, message: ClientMessage, authContext?: unknown): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Channel/presence/broadcast messages are engine-agnostic pub/sub. */
|
|
23
|
+
const CHANNEL_MESSAGE_TYPES = new Set([
|
|
24
|
+
"join_channel", "leave_channel", "broadcast",
|
|
25
|
+
"presence_track", "presence_untrack", "presence_state"
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export interface RoutedRealtimeOptions {
|
|
29
|
+
/** Per-engine realtime providers, keyed by data-source key. */
|
|
30
|
+
providers: Record<string, RealtimeProvider>;
|
|
31
|
+
/** Key of the default provider (handles channels/presence/broadcast). */
|
|
32
|
+
defaultKey: string;
|
|
33
|
+
/** Resolve a collection path to its data-source key. */
|
|
34
|
+
resolveKey: (collectionPath: string) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compose multiple per-engine {@link RealtimeProvider}s into one that routes
|
|
39
|
+
* each subscription to the provider owning the subscribed collection — the
|
|
40
|
+
* realtime counterpart of `buildRoutedRebaseData`.
|
|
41
|
+
*
|
|
42
|
+
* The WebSocket server stays single and engine-agnostic; this composite is
|
|
43
|
+
* passed in its place. Routing rules:
|
|
44
|
+
* - `subscribe_collection` / `subscribe_entity` → the provider for the
|
|
45
|
+
* collection's data source (by `payload.path`).
|
|
46
|
+
* - `unsubscribe` → forwarded to all providers (a no-op on non-owners).
|
|
47
|
+
* - channel / presence / broadcast → the default provider (these are global
|
|
48
|
+
* pub/sub, not bound to an engine).
|
|
49
|
+
* - `addClient` and lifecycle (`onServerReady`/`destroy`/`stopListening`) →
|
|
50
|
+
* all providers (each registers its own ws close handler for cleanup).
|
|
51
|
+
*/
|
|
52
|
+
export function createRoutedRealtimeService(opts: RoutedRealtimeOptions): WsRealtimeService {
|
|
53
|
+
const { providers, defaultKey, resolveKey } = opts;
|
|
54
|
+
|
|
55
|
+
const asWs = (p: RealtimeProvider): WsRealtimeService => p as unknown as WsRealtimeService;
|
|
56
|
+
const all = (): WsRealtimeService[] => Object.values(providers).map(asWs);
|
|
57
|
+
const fallback = (): WsRealtimeService => asWs(providers[defaultKey] ?? Object.values(providers)[0]);
|
|
58
|
+
const forPath = (path?: string): WsRealtimeService => {
|
|
59
|
+
if (!path) return fallback();
|
|
60
|
+
const key = resolveKey(path);
|
|
61
|
+
return asWs(providers[key] ?? providers[defaultKey] ?? Object.values(providers)[0]);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
addClient(clientId, ws) {
|
|
66
|
+
for (const p of all()) p.addClient?.(clientId, ws);
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async handleClientMessage(clientId, message, authContext) {
|
|
70
|
+
const { type } = message;
|
|
71
|
+
if (type === "subscribe_collection" || type === "subscribe_entity") {
|
|
72
|
+
await forPath(message.payload?.path as string | undefined)
|
|
73
|
+
.handleClientMessage(clientId, message, authContext);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (type === "unsubscribe") {
|
|
77
|
+
// The owning provider acts; others no-op on an unknown id.
|
|
78
|
+
await Promise.all(all().map((p) => p.handleClientMessage(clientId, message, authContext)));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Channels/presence/broadcast (and anything else) → default provider.
|
|
82
|
+
await fallback().handleClientMessage(clientId, message, authContext);
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
subscribeToCollection(subscriptionId, config, callback) {
|
|
86
|
+
forPath((config as { path?: string }).path).subscribeToCollection(subscriptionId, config, callback);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
subscribeToEntity(subscriptionId, config, callback) {
|
|
90
|
+
forPath((config as { path?: string }).path).subscribeToEntity(subscriptionId, config, callback);
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
unsubscribe(subscriptionId) {
|
|
94
|
+
for (const p of all()) p.unsubscribe(subscriptionId);
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
async notifyEntityUpdate(path: string, entityId: string, entity: Entity | null, databaseId?: string) {
|
|
98
|
+
await forPath(path).notifyEntityUpdate(path, entityId, entity, databaseId);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
onServerReady(serverInfo) {
|
|
102
|
+
for (const p of all()) p.onServerReady?.(serverInfo);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
async destroy() {
|
|
106
|
+
await Promise.all(all().map((p) => p.destroy?.()));
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
async stopListening() {
|
|
110
|
+
await Promise.all(all().map((p) => p.stopListening?.()));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|