@rebasepro/server-core 0.1.2 → 0.2.1
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/LICENSE +22 -6
- package/dist/common/src/util/entities.d.ts +2 -2
- package/dist/common/src/util/relations.d.ts +1 -1
- package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
- package/dist/index-BZoAtuqi.js.map +1 -0
- package/dist/index.es.js +15851 -15065
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +15825 -15035
- package/dist/index.umd.js.map +1 -1
- package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
- package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
- package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
- package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
- package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
- package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
- package/dist/server-core/src/auth/index.d.ts +7 -0
- package/dist/server-core/src/auth/interfaces.d.ts +2 -0
- package/dist/server-core/src/auth/middleware.d.ts +18 -0
- package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
- package/dist/server-core/src/auth/routes.d.ts +7 -1
- package/dist/server-core/src/env.d.ts +131 -0
- package/dist/server-core/src/index.d.ts +2 -0
- package/dist/server-core/src/init.d.ts +62 -3
- package/dist/types/src/controllers/auth.d.ts +9 -8
- package/dist/types/src/controllers/client.d.ts +3 -0
- package/dist/types/src/types/auth_adapter.d.ts +356 -0
- package/dist/types/src/types/collections.d.ts +67 -2
- package/dist/types/src/types/database_adapter.d.ts +94 -0
- package/dist/types/src/types/entity_actions.d.ts +7 -1
- package/dist/types/src/types/entity_callbacks.d.ts +1 -1
- package/dist/types/src/types/entity_views.d.ts +36 -1
- package/dist/types/src/types/index.d.ts +2 -0
- package/dist/types/src/types/plugins.d.ts +1 -1
- package/dist/types/src/types/properties.d.ts +24 -5
- package/dist/types/src/types/property_config.d.ts +6 -2
- package/dist/types/src/types/relations.d.ts +1 -1
- package/dist/types/src/types/translations.d.ts +8 -0
- package/dist/types/src/users/user.d.ts +5 -0
- package/package.json +26 -26
- package/src/api/errors.ts +1 -1
- package/src/api/graphql/graphql-schema-generator.ts +7 -0
- package/src/api/openapi-generator.ts +13 -1
- package/src/api/rest/api-generator-count.test.ts +14 -12
- package/src/api/rest/query-parser.ts +2 -20
- package/src/auth/adapter-middleware.ts +83 -0
- package/src/auth/admin-routes.ts +36 -43
- package/src/auth/auth-overrides.ts +172 -0
- package/src/auth/builtin-auth-adapter.ts +384 -0
- package/src/auth/crypto-utils.ts +31 -0
- package/src/auth/custom-auth-adapter.ts +85 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/interfaces.ts +2 -0
- package/src/auth/jwt.ts +3 -1
- package/src/auth/middleware.ts +2 -46
- package/src/auth/rls-scope.ts +58 -0
- package/src/auth/routes.ts +74 -32
- package/src/cron/cron-scheduler.test.ts +9 -9
- package/src/cron/cron-scheduler.ts +1 -1
- package/src/env.ts +224 -0
- package/src/index.ts +4 -0
- package/src/init.ts +355 -135
- package/src/storage/routes.ts +1 -19
- package/src/utils/logging.ts +3 -3
- package/test/admin-routes.test.ts +10 -4
- package/test/auth-routes.test.ts +2 -2
- package/test/backend-hooks-admin.test.ts +32 -12
- package/test/custom-auth-adapter.test.ts +177 -0
- package/test/env.test.ts +138 -0
- package/test/query-parser.test.ts +0 -29
- package/tsconfig.json +3 -0
- package/dist/index-DXVBFp5V.js.map +0 -1
package/src/init.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin, BackendHooks } from "@rebasepro/types";
|
|
1
|
+
import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin, BackendHooks, AuthAdapter, DatabaseAdapter } 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";
|
|
@@ -6,6 +6,8 @@ import { Server } from "http";
|
|
|
6
6
|
|
|
7
7
|
import { RestApiGenerator } from "./api/rest/api-generator";
|
|
8
8
|
import { createAuthMiddleware } from "./auth/middleware";
|
|
9
|
+
import { createAdapterAuthMiddleware } from "./auth/adapter-middleware";
|
|
10
|
+
import { createBuiltinAuthAdapter } from "./auth/builtin-auth-adapter";
|
|
9
11
|
import { errorHandler } from "./api/errors";
|
|
10
12
|
import { Hono } from "hono";
|
|
11
13
|
import { bodyLimit } from "hono/body-limit";
|
|
@@ -21,6 +23,7 @@ import { createHistoryRoutes } from "./history";
|
|
|
21
23
|
import { EmailConfig, createEmailService } from "./email";
|
|
22
24
|
import type { EmailService } from "./email";
|
|
23
25
|
import type { OAuthProvider } from "./auth/interfaces";
|
|
26
|
+
import type { AuthOverrides } from "./auth/auth-overrides";
|
|
24
27
|
import { _initRebase } from "./singleton";
|
|
25
28
|
|
|
26
29
|
export interface RebaseAuthConfig {
|
|
@@ -56,7 +59,26 @@ export interface RebaseAuthConfig {
|
|
|
56
59
|
slack?: { clientId: string; clientSecret: string };
|
|
57
60
|
spotify?: { clientId: string; clientSecret: string };
|
|
58
61
|
defaultRole?: string;
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
63
|
providers?: OAuthProvider<any>[];
|
|
64
|
+
/**
|
|
65
|
+
* Override specific parts of the built-in auth implementation.
|
|
66
|
+
*
|
|
67
|
+
* Each override replaces one piece of the default behavior while
|
|
68
|
+
* keeping everything else intact. Unset overrides fall through
|
|
69
|
+
* to the built-in defaults (scrypt passwords, standard validation, etc.).
|
|
70
|
+
*
|
|
71
|
+
* @example bcrypt passwords with a custom hash
|
|
72
|
+
* ```ts
|
|
73
|
+
* import bcrypt from "bcrypt";
|
|
74
|
+
*
|
|
75
|
+
* overrides: {
|
|
76
|
+
* hashPassword: (pw) => bcrypt.hash(pw, 12),
|
|
77
|
+
* verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
|
|
78
|
+
* }
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
overrides?: AuthOverrides;
|
|
60
82
|
[key: string]: unknown;
|
|
61
83
|
}
|
|
62
84
|
|
|
@@ -66,11 +88,40 @@ export interface RebaseBackendConfig {
|
|
|
66
88
|
server: Server;
|
|
67
89
|
app: Hono<HonoEnv>;
|
|
68
90
|
basePath?: string;
|
|
69
|
-
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Database bootstrappers.
|
|
94
|
+
*/
|
|
95
|
+
bootstrappers?: BackendBootstrapper[];
|
|
96
|
+
/**
|
|
97
|
+
* Database adapter.
|
|
98
|
+
*
|
|
99
|
+
* When set, this takes precedence over `bootstrappers`.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* import { createPostgresAdapter } from "@rebasepro/server-postgresql";
|
|
104
|
+
* database: createPostgresAdapter({ connection: db, schema }),
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
database?: DatabaseAdapter;
|
|
108
|
+
|
|
70
109
|
logging?: {
|
|
71
110
|
level?: "error" | "warn" | "info" | "debug";
|
|
72
111
|
};
|
|
73
|
-
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Authentication configuration.
|
|
115
|
+
*
|
|
116
|
+
* Accepts **either**:
|
|
117
|
+
* - `RebaseAuthConfig` — built-in configuration
|
|
118
|
+
* - `AuthAdapter` — pluggable adapter for external auth (Clerk, Auth0, etc.)
|
|
119
|
+
*
|
|
120
|
+
* When a plain config object is provided, the built-in adapter is created
|
|
121
|
+
* automatically from the bootstrapper's `initializeAuth()` result.
|
|
122
|
+
*/
|
|
123
|
+
auth?: RebaseAuthConfig | AuthAdapter;
|
|
124
|
+
|
|
74
125
|
/**
|
|
75
126
|
* Storage configuration. Accepts:
|
|
76
127
|
*
|
|
@@ -83,6 +134,12 @@ export interface RebaseBackendConfig {
|
|
|
83
134
|
enableSwagger?: boolean;
|
|
84
135
|
functionsDir?: string;
|
|
85
136
|
cronsDir?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Enable/disable database persistence for cron job execution logs.
|
|
139
|
+
* When set to false, cron jobs will run but logs will not be persisted to the database.
|
|
140
|
+
* Default: true.
|
|
141
|
+
*/
|
|
142
|
+
cronPersistence?: boolean;
|
|
86
143
|
/**
|
|
87
144
|
* Maximum request body size in bytes for API routes (default: 10MB).
|
|
88
145
|
* Set to 0 to disable the global limit entirely.
|
|
@@ -118,6 +175,34 @@ export interface RebaseBackendConfig {
|
|
|
118
175
|
hooks?: BackendHooks;
|
|
119
176
|
}
|
|
120
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Type guard to detect whether the `auth` config is an `AuthAdapter`
|
|
180
|
+
* (has a `verifyRequest` method) vs a plain `RebaseAuthConfig` (plain object).
|
|
181
|
+
*/
|
|
182
|
+
export function isAuthAdapter(auth: RebaseAuthConfig | AuthAdapter): auth is AuthAdapter {
|
|
183
|
+
return typeof auth === "object" && auth !== null && "verifyRequest" in auth && typeof (auth as AuthAdapter).verifyRequest === "function";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Type guard to detect whether `database` is a `DatabaseAdapter`.
|
|
188
|
+
*/
|
|
189
|
+
export function isDatabaseAdapter(db: unknown): db is DatabaseAdapter {
|
|
190
|
+
return typeof db === "object" && db !== null && "initializeDriver" in db && "type" in db && !("initializeAuth" in db);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolve the `requireAuth` flag from either a `RebaseAuthConfig` or an `AuthAdapter`.
|
|
195
|
+
*
|
|
196
|
+
* - `RebaseAuthConfig` has an explicit `requireAuth` boolean
|
|
197
|
+
* - `AuthAdapter` always implies auth is required (secure by default)
|
|
198
|
+
* - If no auth config is provided at all, default to `true`
|
|
199
|
+
*/
|
|
200
|
+
function resolveRequireAuth(auth?: RebaseAuthConfig | AuthAdapter): boolean {
|
|
201
|
+
if (!auth) return true;
|
|
202
|
+
if (isAuthAdapter(auth)) return true; // AuthAdapters are always secure-by-default
|
|
203
|
+
return (auth as RebaseAuthConfig).requireAuth !== false;
|
|
204
|
+
}
|
|
205
|
+
|
|
121
206
|
export interface RebaseBackendInstance {
|
|
122
207
|
driverRegistry: DriverRegistry;
|
|
123
208
|
driver: DataDriver;
|
|
@@ -159,7 +244,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
159
244
|
configureLogLevel();
|
|
160
245
|
}
|
|
161
246
|
|
|
162
|
-
logger.info("Initializing Rebase Backend
|
|
247
|
+
logger.info("Initializing Rebase Backend");
|
|
163
248
|
|
|
164
249
|
const basePath = config.basePath || "/api";
|
|
165
250
|
const isProduction = process.env.NODE_ENV === "production";
|
|
@@ -205,10 +290,31 @@ dir: config.collectionsDir });
|
|
|
205
290
|
|
|
206
291
|
const realtimeServices: Record<string, RealtimeProvider> = {};
|
|
207
292
|
const delegates: Record<string, DataDriver> = {};
|
|
208
|
-
|
|
293
|
+
|
|
294
|
+
// ─── Resolve bootstrappers ───────────────────────────────────────────
|
|
295
|
+
let bootstrappers: BackendBootstrapper[] = config.bootstrappers || [];
|
|
296
|
+
if (config.database) {
|
|
297
|
+
const dbAdapter = config.database;
|
|
298
|
+
logger.info("Using DatabaseAdapter", { type: dbAdapter.type });
|
|
299
|
+
const wrappedBootstrapper: BackendBootstrapper = {
|
|
300
|
+
type: dbAdapter.type,
|
|
301
|
+
initializeDriver: (initConfig: unknown) =>
|
|
302
|
+
dbAdapter.initializeDriver(initConfig as import("@rebasepro/types").DatabaseAdapterInitConfig),
|
|
303
|
+
initializeRealtime: dbAdapter.initializeRealtime
|
|
304
|
+
? (_config: unknown, driverResult: InitializedDriver) =>
|
|
305
|
+
dbAdapter.initializeRealtime!(driverResult)
|
|
306
|
+
: undefined,
|
|
307
|
+
initializeAuth: dbAdapter.initializeAuth,
|
|
308
|
+
initializeHistory: dbAdapter.initializeHistory,
|
|
309
|
+
initializeWebsockets: dbAdapter.initializeWebsockets,
|
|
310
|
+
getAdmin: dbAdapter.getAdmin,
|
|
311
|
+
mountRoutes: dbAdapter.mountRoutes,
|
|
312
|
+
};
|
|
313
|
+
bootstrappers = [wrappedBootstrapper];
|
|
314
|
+
}
|
|
209
315
|
|
|
210
316
|
if (bootstrappers.length === 0) {
|
|
211
|
-
throw new Error("No bootstrappers provided. Cannot initialize database drivers.");
|
|
317
|
+
throw new Error("No bootstrappers or database adapter provided. Cannot initialize database drivers.");
|
|
212
318
|
}
|
|
213
319
|
|
|
214
320
|
let defaultDriverId = DEFAULT_DRIVER_ID;
|
|
@@ -250,36 +356,75 @@ collectionRegistry });
|
|
|
250
356
|
// 2. Initialize Auth & History via the default driver's bootstrapper
|
|
251
357
|
let authConfigResult: BootstrappedAuth | undefined = undefined;
|
|
252
358
|
let serviceKey: string | undefined;
|
|
359
|
+
let authAdapter: AuthAdapter | undefined;
|
|
253
360
|
|
|
254
361
|
if (config.auth) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
secret: safeAuthConfig.jwtSecret,
|
|
260
|
-
accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
|
|
261
|
-
refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
|
|
262
|
-
});
|
|
263
|
-
}
|
|
362
|
+
if (isAuthAdapter(config.auth)) {
|
|
363
|
+
// ── New path: User provided an AuthAdapter directly ──────────
|
|
364
|
+
authAdapter = config.auth;
|
|
365
|
+
serviceKey = authAdapter.serviceKey;
|
|
264
366
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (safeAuthConfig.serviceKey.length < 32) {
|
|
268
|
-
throw new Error(
|
|
269
|
-
"REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
|
|
270
|
-
"Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
|
|
271
|
-
);
|
|
367
|
+
if (authAdapter.initialize) {
|
|
368
|
+
await authAdapter.initialize();
|
|
272
369
|
}
|
|
273
|
-
serviceKey = safeAuthConfig.serviceKey;
|
|
274
|
-
logger.info("Service key configured for script/server-to-server authentication");
|
|
275
|
-
}
|
|
276
370
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
authConfigResult
|
|
280
|
-
|
|
371
|
+
logger.info("Using AuthAdapter", { id: authAdapter.id });
|
|
372
|
+
|
|
373
|
+
// Populate authConfigResult for backward compatibility
|
|
374
|
+
// (the return type still exposes `auth?: BootstrappedAuth`)
|
|
375
|
+
authConfigResult = {
|
|
376
|
+
userService: authAdapter.userManagement ?? {},
|
|
377
|
+
roleService: authAdapter.roleManagement ?? {},
|
|
378
|
+
};
|
|
281
379
|
} else {
|
|
282
|
-
|
|
380
|
+
// ── RebaseAuthConfig — wrap in built-in adapter ──
|
|
381
|
+
const safeAuthConfig = config.auth as RebaseAuthConfig;
|
|
382
|
+
if (safeAuthConfig.jwtSecret) {
|
|
383
|
+
configureJwt({
|
|
384
|
+
secret: safeAuthConfig.jwtSecret,
|
|
385
|
+
accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
|
|
386
|
+
refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Service Key Validation ───────────────────────────────────
|
|
391
|
+
if (safeAuthConfig.serviceKey) {
|
|
392
|
+
if (safeAuthConfig.serviceKey.length < 32) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
"REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
|
|
395
|
+
"Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
serviceKey = safeAuthConfig.serviceKey;
|
|
399
|
+
logger.info("Service key configured for script/server-to-server authentication");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (defaultBootstrapper.initializeAuth) {
|
|
403
|
+
logger.info("Bootstrapping authentication via driver protocol");
|
|
404
|
+
authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
|
|
405
|
+
|
|
406
|
+
// Build the built-in auth adapter from bootstrapper results
|
|
407
|
+
if (authConfigResult) {
|
|
408
|
+
const oauthProviders: OAuthProvider<unknown>[] = [...(safeAuthConfig.providers || [])];
|
|
409
|
+
// OAuth providers are resolved later in route mounting,
|
|
410
|
+
// but we need them here for the adapter
|
|
411
|
+
authAdapter = createBuiltinAuthAdapter({
|
|
412
|
+
authRepository: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
|
|
413
|
+
emailService: authConfigResult.emailService as import("./email").EmailService,
|
|
414
|
+
emailConfig: safeAuthConfig.email,
|
|
415
|
+
allowRegistration: safeAuthConfig.allowRegistration ?? false,
|
|
416
|
+
defaultRole: safeAuthConfig.defaultRole,
|
|
417
|
+
oauthProviders,
|
|
418
|
+
serviceKey,
|
|
419
|
+
hooks: config.hooks,
|
|
420
|
+
overrides: safeAuthConfig.overrides,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
logger.info("Authentication initialized");
|
|
425
|
+
} else {
|
|
426
|
+
logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
|
|
427
|
+
}
|
|
283
428
|
}
|
|
284
429
|
}
|
|
285
430
|
|
|
@@ -350,87 +495,110 @@ collectionRegistry });
|
|
|
350
495
|
// basePath already resolved above
|
|
351
496
|
|
|
352
497
|
// 4. Mount API Routes
|
|
353
|
-
if (config.auth &&
|
|
354
|
-
|
|
498
|
+
if (config.auth && authAdapter) {
|
|
499
|
+
// ── Auth Capabilities Endpoint ───────────────────────────────────
|
|
500
|
+
// Exposes adapter capabilities so the frontend knows what's available
|
|
501
|
+
// (login form vs external redirect, OAuth providers, etc.)
|
|
502
|
+
config.app.get(`${basePath}/auth/config`, async (c) => {
|
|
503
|
+
const capabilities = await authAdapter!.getCapabilities();
|
|
504
|
+
return c.json(capabilities);
|
|
505
|
+
});
|
|
355
506
|
|
|
356
|
-
if (config.auth
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
507
|
+
if (!isAuthAdapter(config.auth)) {
|
|
508
|
+
const safeAuthConfig = config.auth as RebaseAuthConfig;
|
|
509
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
510
|
+
const oauthProviders: OAuthProvider<any>[] = [...(safeAuthConfig.providers || [])];
|
|
360
511
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
512
|
+
if (safeAuthConfig.google?.clientId) {
|
|
513
|
+
const { createGoogleProvider } = await import("./auth");
|
|
514
|
+
oauthProviders.push(createGoogleProvider(safeAuthConfig.google));
|
|
515
|
+
}
|
|
365
516
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
517
|
+
if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
|
|
518
|
+
const { createLinkedinProvider } = await import("./auth");
|
|
519
|
+
oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as { clientId: string; clientSecret: string }));
|
|
520
|
+
}
|
|
370
521
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
522
|
+
if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
|
|
523
|
+
const { createGitHubProvider } = await import("./auth");
|
|
524
|
+
oauthProviders.push(createGitHubProvider(safeAuthConfig.github));
|
|
525
|
+
}
|
|
375
526
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
527
|
+
if (safeAuthConfig.microsoft?.clientId && safeAuthConfig.microsoft?.clientSecret) {
|
|
528
|
+
const { createMicrosoftProvider } = await import("./auth");
|
|
529
|
+
oauthProviders.push(createMicrosoftProvider(safeAuthConfig.microsoft));
|
|
530
|
+
}
|
|
380
531
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
532
|
+
if (safeAuthConfig.apple?.clientId && safeAuthConfig.apple?.teamId && safeAuthConfig.apple?.keyId && safeAuthConfig.apple?.privateKey) {
|
|
533
|
+
const { createAppleProvider } = await import("./auth");
|
|
534
|
+
oauthProviders.push(createAppleProvider(safeAuthConfig.apple));
|
|
535
|
+
}
|
|
385
536
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
537
|
+
if (safeAuthConfig.facebook?.clientId && safeAuthConfig.facebook?.clientSecret) {
|
|
538
|
+
const { createFacebookProvider } = await import("./auth");
|
|
539
|
+
oauthProviders.push(createFacebookProvider(safeAuthConfig.facebook));
|
|
540
|
+
}
|
|
390
541
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
542
|
+
if (safeAuthConfig.twitter?.clientId && safeAuthConfig.twitter?.clientSecret) {
|
|
543
|
+
const { createTwitterProvider } = await import("./auth");
|
|
544
|
+
oauthProviders.push(createTwitterProvider(safeAuthConfig.twitter));
|
|
545
|
+
}
|
|
395
546
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
547
|
+
if (safeAuthConfig.discord?.clientId && safeAuthConfig.discord?.clientSecret) {
|
|
548
|
+
const { createDiscordProvider } = await import("./auth");
|
|
549
|
+
oauthProviders.push(createDiscordProvider(safeAuthConfig.discord));
|
|
550
|
+
}
|
|
400
551
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
552
|
+
if (safeAuthConfig.gitlab?.clientId && safeAuthConfig.gitlab?.clientSecret) {
|
|
553
|
+
const { createGitLabProvider } = await import("./auth");
|
|
554
|
+
oauthProviders.push(createGitLabProvider(safeAuthConfig.gitlab));
|
|
555
|
+
}
|
|
405
556
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
557
|
+
if (safeAuthConfig.bitbucket?.clientId && safeAuthConfig.bitbucket?.clientSecret) {
|
|
558
|
+
const { createBitbucketProvider } = await import("./auth");
|
|
559
|
+
oauthProviders.push(createBitbucketProvider(safeAuthConfig.bitbucket));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (safeAuthConfig.slack?.clientId && safeAuthConfig.slack?.clientSecret) {
|
|
563
|
+
const { createSlackProvider } = await import("./auth");
|
|
564
|
+
oauthProviders.push(createSlackProvider(safeAuthConfig.slack));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (safeAuthConfig.spotify?.clientId && safeAuthConfig.spotify?.clientSecret) {
|
|
568
|
+
const { createSpotifyProvider } = await import("./auth");
|
|
569
|
+
oauthProviders.push(createSpotifyProvider(safeAuthConfig.spotify));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Re-create the built-in adapter with all resolved OAuth providers
|
|
573
|
+
authAdapter = createBuiltinAuthAdapter({
|
|
574
|
+
authRepository: authConfigResult!.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult!.userService as import("./auth/interfaces").AuthRepository,
|
|
575
|
+
emailService: authConfigResult!.emailService as import("./email").EmailService,
|
|
576
|
+
emailConfig: safeAuthConfig.email,
|
|
577
|
+
allowRegistration: safeAuthConfig.allowRegistration ?? false,
|
|
578
|
+
defaultRole: safeAuthConfig.defaultRole,
|
|
579
|
+
oauthProviders,
|
|
580
|
+
serviceKey,
|
|
581
|
+
hooks: config.hooks,
|
|
582
|
+
overrides: safeAuthConfig.overrides,
|
|
583
|
+
});
|
|
409
584
|
}
|
|
410
585
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
586
|
+
// ── Mount auth & admin routes via the adapter ────────────────────
|
|
587
|
+
if (authAdapter.createAuthRoutes) {
|
|
588
|
+
const authRoutes = authAdapter.createAuthRoutes();
|
|
589
|
+
if (authRoutes) {
|
|
590
|
+
config.app.route(`${basePath}/auth`, authRoutes);
|
|
591
|
+
logger.info("Auth routes mounted via adapter", { adapter: authAdapter.id });
|
|
592
|
+
}
|
|
414
593
|
}
|
|
415
594
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
});
|
|
424
|
-
config.app.route(`${basePath}/auth`, authRoutes);
|
|
425
|
-
|
|
426
|
-
const adminRoutes = createAdminRoutes({
|
|
427
|
-
authRepo: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
|
|
428
|
-
emailService: authConfigResult.emailService as import("./email").EmailService,
|
|
429
|
-
emailConfig: config.auth.email,
|
|
430
|
-
serviceKey,
|
|
431
|
-
hooks: config.hooks
|
|
432
|
-
});
|
|
433
|
-
config.app.route(`${basePath}/admin`, adminRoutes);
|
|
595
|
+
if (authAdapter.createAdminRoutes) {
|
|
596
|
+
const adminRoutes = authAdapter.createAdminRoutes();
|
|
597
|
+
if (adminRoutes) {
|
|
598
|
+
config.app.route(`${basePath}/admin`, adminRoutes);
|
|
599
|
+
logger.info("Admin routes mounted via adapter", { adapter: authAdapter.id });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
434
602
|
}
|
|
435
603
|
|
|
436
604
|
if (config.collectionsDir) {
|
|
@@ -438,7 +606,13 @@ collectionRegistry });
|
|
|
438
606
|
const { createSchemaEditorRoutes } = await import("./api/schema-editor-routes");
|
|
439
607
|
const schemaEditorRoutes = createSchemaEditorRoutes(config.collectionsDir);
|
|
440
608
|
|
|
441
|
-
if (
|
|
609
|
+
if (authAdapter && !isAuthAdapter(config.auth!)) {
|
|
610
|
+
const safeAuth = config.auth as RebaseAuthConfig;
|
|
611
|
+
if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) {
|
|
612
|
+
schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
|
|
613
|
+
}
|
|
614
|
+
} else if (authAdapter) {
|
|
615
|
+
// External auth adapter — still protect schema editor
|
|
442
616
|
schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
|
|
443
617
|
}
|
|
444
618
|
|
|
@@ -458,7 +632,7 @@ collectionRegistry });
|
|
|
458
632
|
|
|
459
633
|
const storageRoutes = createStorageRoutes({
|
|
460
634
|
controller: storageController,
|
|
461
|
-
requireAuth: config.auth
|
|
635
|
+
requireAuth: resolveRequireAuth(config.auth)
|
|
462
636
|
});
|
|
463
637
|
|
|
464
638
|
// Apply a permissive body limit specifically for the upload endpoint
|
|
@@ -484,7 +658,7 @@ collectionRegistry });
|
|
|
484
658
|
// Secure by default: require auth when auth is configured.
|
|
485
659
|
// Developers who intentionally want public data access (relying
|
|
486
660
|
// entirely on Postgres RLS) must explicitly set `auth.requireAuth: false`.
|
|
487
|
-
const dataRequireAuth = config.auth
|
|
661
|
+
const dataRequireAuth = resolveRequireAuth(config.auth);
|
|
488
662
|
|
|
489
663
|
if (!dataRequireAuth) {
|
|
490
664
|
logger.warn(
|
|
@@ -495,11 +669,21 @@ collectionRegistry });
|
|
|
495
669
|
);
|
|
496
670
|
}
|
|
497
671
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
672
|
+
// Use adapter middleware when an AuthAdapter is available,
|
|
673
|
+
// falling back to the built-in JWT middleware otherwise.
|
|
674
|
+
if (authAdapter) {
|
|
675
|
+
dataRouter.use("/*", createAdapterAuthMiddleware({
|
|
676
|
+
adapter: authAdapter,
|
|
677
|
+
driver: defaultDriver,
|
|
678
|
+
requireAuth: dataRequireAuth,
|
|
679
|
+
}));
|
|
680
|
+
} else {
|
|
681
|
+
dataRouter.use("/*", createAuthMiddleware({
|
|
682
|
+
driver: defaultDriver,
|
|
683
|
+
requireAuth: dataRequireAuth,
|
|
684
|
+
serviceKey
|
|
685
|
+
}));
|
|
686
|
+
}
|
|
503
687
|
|
|
504
688
|
// Mount history routes BEFORE the REST API subcollection catch-all so
|
|
505
689
|
// that /:slug/:entityId/history is matched by the dedicated handler first.
|
|
@@ -525,7 +709,7 @@ collectionRegistry });
|
|
|
525
709
|
config.app.get(`${basePath}/docs`, (c) => {
|
|
526
710
|
const spec = generateOpenApiSpec(activeCollections, {
|
|
527
711
|
basePath,
|
|
528
|
-
requireAuth: config.auth
|
|
712
|
+
requireAuth: resolveRequireAuth(config.auth)
|
|
529
713
|
});
|
|
530
714
|
return c.json(spec);
|
|
531
715
|
});
|
|
@@ -560,6 +744,7 @@ collectionRegistry });
|
|
|
560
744
|
baseUrl: "http://localhost",
|
|
561
745
|
apiPath: basePath,
|
|
562
746
|
websocketUrl: "",
|
|
747
|
+
token: serviceKey,
|
|
563
748
|
fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
564
749
|
return await config.app.request(input as string | Request | URL, init);
|
|
565
750
|
}
|
|
@@ -570,8 +755,8 @@ collectionRegistry });
|
|
|
570
755
|
let emailService: EmailService | undefined;
|
|
571
756
|
if (authConfigResult?.emailService) {
|
|
572
757
|
emailService = authConfigResult.emailService as EmailService;
|
|
573
|
-
} else if (config.auth
|
|
574
|
-
emailService = createEmailService(config.auth.email);
|
|
758
|
+
} else if (config.auth && !isAuthAdapter(config.auth) && (config.auth as RebaseAuthConfig).email) {
|
|
759
|
+
emailService = createEmailService((config.auth as RebaseAuthConfig).email!);
|
|
575
760
|
}
|
|
576
761
|
|
|
577
762
|
if (emailService) {
|
|
@@ -607,13 +792,22 @@ collectionRegistry });
|
|
|
607
792
|
|
|
608
793
|
// Custom functions follow the same auth policy as data routes.
|
|
609
794
|
// Per-route auth can be further refined inside individual functions.
|
|
610
|
-
const functionsRequireAuth = config.auth
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
795
|
+
const functionsRequireAuth = resolveRequireAuth(config.auth);
|
|
796
|
+
|
|
797
|
+
// Use adapter middleware when available, fallback to built-in
|
|
798
|
+
if (authAdapter) {
|
|
799
|
+
functionsRouter.use("/*", createAdapterAuthMiddleware({
|
|
800
|
+
adapter: authAdapter,
|
|
801
|
+
driver: defaultDriver,
|
|
802
|
+
requireAuth: functionsRequireAuth,
|
|
803
|
+
}));
|
|
804
|
+
} else {
|
|
805
|
+
functionsRouter.use("/*", createAuthMiddleware({
|
|
806
|
+
driver: defaultDriver,
|
|
807
|
+
requireAuth: functionsRequireAuth,
|
|
808
|
+
serviceKey
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
617
811
|
|
|
618
812
|
const fnRoutes = createFunctionRoutes(loadedFunctions);
|
|
619
813
|
functionsRouter.route("/", fnRoutes);
|
|
@@ -642,8 +836,9 @@ path: `${basePath}/functions` });
|
|
|
642
836
|
|
|
643
837
|
cronScheduler.registerJobs(loadedCronJobs);
|
|
644
838
|
|
|
645
|
-
// Attach database persistence if the driver supports SQL
|
|
646
|
-
const
|
|
839
|
+
// Attach database persistence if the driver supports SQL and persistence is enabled
|
|
840
|
+
const admin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
|
|
841
|
+
const store = (admin && config.cronPersistence !== false) ? createCronStore(defaultDriver) : undefined;
|
|
647
842
|
if (store) {
|
|
648
843
|
await store.ensureTable();
|
|
649
844
|
cronScheduler.setStore(store);
|
|
@@ -652,7 +847,12 @@ path: `${basePath}/functions` });
|
|
|
652
847
|
const cronRouter = new Hono<HonoEnv>();
|
|
653
848
|
|
|
654
849
|
// Cron admin routes require authentication + admin role
|
|
655
|
-
if (
|
|
850
|
+
if (authAdapter && !isAuthAdapter(config.auth!)) {
|
|
851
|
+
const safeAuth = config.auth as RebaseAuthConfig;
|
|
852
|
+
if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) {
|
|
853
|
+
cronRouter.use("/*", requireAuth, requireAdmin);
|
|
854
|
+
}
|
|
855
|
+
} else if (authAdapter) {
|
|
656
856
|
cronRouter.use("/*", requireAuth, requireAdmin);
|
|
657
857
|
}
|
|
658
858
|
|
|
@@ -668,7 +868,7 @@ path: `${basePath}/cron` });
|
|
|
668
868
|
}
|
|
669
869
|
|
|
670
870
|
if ((defaultBootstrapper as BackendBootstrapper & { initializeWebsockets?: (...args: unknown[]) => unknown }).initializeWebsockets) {
|
|
671
|
-
await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth);
|
|
871
|
+
await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
672
872
|
}
|
|
673
873
|
|
|
674
874
|
logger.info("Rebase Backend Initialized");
|
|
@@ -708,27 +908,47 @@ latencyMs };
|
|
|
708
908
|
// ── Graceful Shutdown ─────────────────────────────────────────────────
|
|
709
909
|
const shutdown = (timeoutMs = 15_000): Promise<void> => {
|
|
710
910
|
return new Promise<void>((resolve) => {
|
|
711
|
-
|
|
911
|
+
(async () => {
|
|
912
|
+
logger.info("Shutting down Rebase Backend...");
|
|
712
913
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
914
|
+
// 1. Stop cron scheduler
|
|
915
|
+
if (cronScheduler) {
|
|
916
|
+
cronScheduler.stop();
|
|
917
|
+
logger.info("Cron scheduler stopped");
|
|
918
|
+
}
|
|
718
919
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
920
|
+
// 2. Tear down realtime services (LISTEN clients, debounce timers,
|
|
921
|
+
// subscriptions). Must happen BEFORE pool.end() so that pending
|
|
922
|
+
// timer callbacks don't fire against a closed pool.
|
|
923
|
+
for (const [key, rt] of Object.entries(realtimeServices)) {
|
|
924
|
+
try {
|
|
925
|
+
const rtWithLifecycle = rt as RealtimeProvider & { destroy?: () => Promise<void>; stopListening?: () => Promise<void> };
|
|
926
|
+
if (typeof rtWithLifecycle.destroy === "function") {
|
|
927
|
+
await rtWithLifecycle.destroy();
|
|
928
|
+
logger.info(`Realtime service "${key}" destroyed`);
|
|
929
|
+
} else if (typeof rtWithLifecycle.stopListening === "function") {
|
|
930
|
+
await rtWithLifecycle.stopListening();
|
|
931
|
+
logger.info(`Realtime service "${key}" LISTEN client stopped`);
|
|
932
|
+
}
|
|
933
|
+
} catch (err) {
|
|
934
|
+
logger.warn(`Error destroying realtime service "${key}":`, { error: err });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
724
937
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
|
|
938
|
+
// 3. Close the HTTP server (stop accepting, drain in-flight)
|
|
939
|
+
config.server.close(() => {
|
|
940
|
+
logger.info("HTTP server closed");
|
|
729
941
|
resolve();
|
|
730
|
-
}
|
|
731
|
-
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// 4. Force-resolve after timeout (unless disabled with 0)
|
|
945
|
+
if (timeoutMs > 0) {
|
|
946
|
+
setTimeout(() => {
|
|
947
|
+
logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
|
|
948
|
+
resolve();
|
|
949
|
+
}, timeoutMs).unref();
|
|
950
|
+
}
|
|
951
|
+
})();
|
|
732
952
|
});
|
|
733
953
|
};
|
|
734
954
|
|