@rebasepro/server-core 0.6.0 → 0.7.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/package.json +14 -14
- package/src/api/errors.ts +16 -2
- package/src/api/graphql/graphql-schema-generator.ts +45 -10
- package/src/api/rest/api-generator.ts +19 -2
- package/src/api/server.ts +10 -2
- package/src/api/types.ts +6 -0
- 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 +28 -4
- 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 +27 -0
- 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 +10 -1
- package/src/auth/reset-password-admin.ts +17 -1
- package/src/auth/routes.ts +77 -8
- 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/init.ts +137 -5
- package/src/services/routed-realtime-service.ts +113 -0
- package/src/storage/routes.ts +4 -0
- package/src/utils/dev-port.ts +13 -7
- package/test/auth-routes.test.ts +54 -4
- package/test/custom-auth-adapter.test.ts +20 -1
- 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/transform-auth-response.test.ts +305 -0
- package/vite.config.ts +5 -0
- package/build-errors.txt +0 -52
- package/dist/api/ast-schema-editor.d.ts +0 -21
- package/dist/api/collections_for_test/callbacks_test_collection.d.ts +0 -2
- package/dist/api/errors.d.ts +0 -53
- package/dist/api/graphql/graphql-schema-generator.d.ts +0 -35
- package/dist/api/graphql/index.d.ts +0 -1
- package/dist/api/index.d.ts +0 -9
- package/dist/api/logs-routes.d.ts +0 -37
- package/dist/api/openapi-generator.d.ts +0 -16
- package/dist/api/rest/api-generator.d.ts +0 -88
- package/dist/api/rest/index.d.ts +0 -1
- package/dist/api/rest/query-parser.d.ts +0 -11
- package/dist/api/schema-editor-routes.d.ts +0 -3
- package/dist/api/server.d.ts +0 -40
- package/dist/api/types.d.ts +0 -98
- package/dist/auth/adapter-middleware.d.ts +0 -37
- package/dist/auth/admin-user-ops.d.ts +0 -79
- package/dist/auth/api-keys/api-key-middleware.d.ts +0 -39
- package/dist/auth/api-keys/api-key-permission-guard.d.ts +0 -32
- package/dist/auth/api-keys/api-key-routes.d.ts +0 -20
- package/dist/auth/api-keys/api-key-store.d.ts +0 -35
- package/dist/auth/api-keys/api-key-types.d.ts +0 -88
- package/dist/auth/api-keys/index.d.ts +0 -17
- package/dist/auth/apple-oauth.d.ts +0 -30
- package/dist/auth/auth-hooks.d.ts +0 -225
- package/dist/auth/bitbucket-oauth.d.ts +0 -11
- package/dist/auth/builtin-auth-adapter.d.ts +0 -49
- package/dist/auth/crypto-utils.d.ts +0 -16
- package/dist/auth/custom-auth-adapter.d.ts +0 -39
- package/dist/auth/discord-oauth.d.ts +0 -14
- package/dist/auth/facebook-oauth.d.ts +0 -14
- package/dist/auth/github-oauth.d.ts +0 -15
- package/dist/auth/gitlab-oauth.d.ts +0 -13
- package/dist/auth/google-oauth.d.ts +0 -47
- package/dist/auth/index.d.ts +0 -36
- package/dist/auth/interfaces.d.ts +0 -401
- package/dist/auth/jwt.d.ts +0 -55
- package/dist/auth/linkedin-oauth.d.ts +0 -18
- package/dist/auth/mfa-routes.d.ts +0 -6
- package/dist/auth/mfa.d.ts +0 -49
- package/dist/auth/microsoft-oauth.d.ts +0 -16
- package/dist/auth/middleware.d.ts +0 -106
- package/dist/auth/password.d.ts +0 -22
- package/dist/auth/rate-limiter.d.ts +0 -50
- package/dist/auth/reset-password-admin.d.ts +0 -29
- package/dist/auth/rls-scope.d.ts +0 -31
- package/dist/auth/routes.d.ts +0 -33
- package/dist/auth/session-routes.d.ts +0 -25
- package/dist/auth/slack-oauth.d.ts +0 -12
- package/dist/auth/spotify-oauth.d.ts +0 -12
- package/dist/auth/twitter-oauth.d.ts +0 -18
- package/dist/backend-CIxN4FVm.js +0 -15
- package/dist/backend-CIxN4FVm.js.map +0 -1
- package/dist/chunk-Dze3rakg.js +0 -42
- package/dist/collections/BackendCollectionRegistry.d.ts +0 -13
- package/dist/collections/loader.d.ts +0 -5
- package/dist/cron/cron-loader.d.ts +0 -17
- package/dist/cron/cron-routes.d.ts +0 -14
- package/dist/cron/cron-scheduler.d.ts +0 -106
- package/dist/cron/cron-store.d.ts +0 -32
- package/dist/cron/index.d.ts +0 -6
- package/dist/db/interfaces.d.ts +0 -18
- package/dist/dist-CZKP-Xz4.js +0 -832
- package/dist/dist-CZKP-Xz4.js.map +0 -1
- package/dist/email/index.d.ts +0 -6
- package/dist/email/smtp-email-service.d.ts +0 -25
- package/dist/email/templates.d.ts +0 -42
- package/dist/email/types.d.ts +0 -108
- package/dist/env.d.ts +0 -102
- package/dist/from-VbwD7xRf.js +0 -3849
- package/dist/from-VbwD7xRf.js.map +0 -1
- package/dist/functions/function-loader.d.ts +0 -17
- package/dist/functions/function-routes.d.ts +0 -10
- package/dist/functions/index.d.ts +0 -3
- package/dist/history/history-routes.d.ts +0 -23
- package/dist/history/index.d.ts +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.es.js +0 -61726
- package/dist/index.es.js.map +0 -1
- package/dist/index.umd.js +0 -73216
- package/dist/index.umd.js.map +0 -1
- package/dist/init/docs.d.ts +0 -4
- package/dist/init/health.d.ts +0 -2
- package/dist/init/middlewares.d.ts +0 -10
- package/dist/init/shutdown.d.ts +0 -11
- package/dist/init/storage.d.ts +0 -5
- package/dist/init.d.ts +0 -258
- package/dist/jwt-DHcQRGC3.js +0 -4168
- package/dist/jwt-DHcQRGC3.js.map +0 -1
- package/dist/logger-BYU66ENZ.js +0 -94
- package/dist/logger-BYU66ENZ.js.map +0 -1
- package/dist/ms-BeBSuOXN.js +0 -125
- package/dist/ms-BeBSuOXN.js.map +0 -1
- package/dist/multipart-parser-CedBDOeC.js +0 -299
- package/dist/multipart-parser-CedBDOeC.js.map +0 -1
- package/dist/serve-spa.d.ts +0 -30
- package/dist/services/driver-registry.d.ts +0 -78
- package/dist/services/webhook-service.d.ts +0 -29
- package/dist/singleton.d.ts +0 -35
- package/dist/src-COaj0G3P.js +0 -1182
- package/dist/src-COaj0G3P.js.map +0 -1
- package/dist/storage/LocalStorageController.d.ts +0 -46
- package/dist/storage/S3StorageController.d.ts +0 -36
- package/dist/storage/image-transform.d.ts +0 -50
- package/dist/storage/index.d.ts +0 -28
- package/dist/storage/routes.d.ts +0 -38
- package/dist/storage/storage-registry.d.ts +0 -78
- package/dist/storage/tus-handler.d.ts +0 -51
- package/dist/storage/types.d.ts +0 -103
- package/dist/types/index.d.ts +0 -11
- package/dist/utils/dev-port.d.ts +0 -35
- package/dist/utils/logger.d.ts +0 -31
- package/dist/utils/logging.d.ts +0 -9
- package/dist/utils/request-id.d.ts +0 -4
- package/dist/utils/request-logger.d.ts +0 -19
- package/dist/utils/sql.d.ts +0 -27
|
@@ -57,7 +57,8 @@ export function createSpotifyProvider(config: { clientId: string; clientSecret:
|
|
|
57
57
|
providerId: p.id,
|
|
58
58
|
email: p.email,
|
|
59
59
|
displayName: p.display_name || null,
|
|
60
|
-
photoUrl: p.images?.[0]?.url || null
|
|
60
|
+
photoUrl: p.images?.[0]?.url || null,
|
|
61
|
+
emailVerified: true
|
|
61
62
|
};
|
|
62
63
|
} catch (error) {
|
|
63
64
|
logger.error("Spotify OAuth error", { error: error });
|
|
@@ -84,6 +84,7 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
84
84
|
// generate a placeholder email — the user can update it later.
|
|
85
85
|
// For apps with elevated access, fetch from v1.1 endpoint.
|
|
86
86
|
let email: string | null = null;
|
|
87
|
+
let emailVerified = false;
|
|
87
88
|
try {
|
|
88
89
|
const emailResponse = await fetch(
|
|
89
90
|
"https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true",
|
|
@@ -94,6 +95,9 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
94
95
|
if (emailResponse.ok) {
|
|
95
96
|
const emailData = await emailResponse.json() as { email?: string };
|
|
96
97
|
email = emailData.email || null;
|
|
98
|
+
if (email) {
|
|
99
|
+
emailVerified = true;
|
|
100
|
+
}
|
|
97
101
|
}
|
|
98
102
|
} catch {
|
|
99
103
|
// Elevated access not available — fall through
|
|
@@ -104,13 +108,16 @@ export function createTwitterProvider(config: { clientId: string; clientSecret:
|
|
|
104
108
|
// This allows the account to be created and linked; the user should
|
|
105
109
|
// update their email through the profile settings.
|
|
106
110
|
email = `${profileData.id}@twitter.placeholder.rebase`;
|
|
111
|
+
// Placeholder emails are NOT verified — prevents auto-linking to existing accounts
|
|
112
|
+
emailVerified = false;
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
return {
|
|
110
116
|
providerId: profileData.id,
|
|
111
117
|
email,
|
|
112
118
|
displayName: profileData.name || profileData.username || null,
|
|
113
|
-
photoUrl: profileData.profile_image_url?.replace("_normal", "_400x400") || null
|
|
119
|
+
photoUrl: profileData.profile_image_url?.replace("_normal", "_400x400") || null,
|
|
120
|
+
emailVerified
|
|
114
121
|
};
|
|
115
122
|
} catch (error) {
|
|
116
123
|
logger.error("Twitter OAuth error", { error: error });
|
package/src/cron/cron-store.ts
CHANGED
|
@@ -43,7 +43,8 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
|
|
|
43
43
|
return undefined;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const exec =
|
|
46
|
+
const exec = (sqlText: string, options?: { params?: unknown[] }) =>
|
|
47
|
+
admin.executeSql(sqlText, options?.params ? { params: options.params } : undefined);
|
|
47
48
|
|
|
48
49
|
return {
|
|
49
50
|
async ensureTable(): Promise<void> {
|
|
@@ -80,22 +81,22 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
|
|
|
80
81
|
try {
|
|
81
82
|
const resultJson = entry.result !== undefined ? JSON.stringify(entry.result) : null;
|
|
82
83
|
const logsJson = entry.logs.length > 0 ? JSON.stringify(entry.logs) : null;
|
|
83
|
-
const errorEscaped = entry.error ? entry.error.replace(/'/g, "''") : null;
|
|
84
84
|
|
|
85
|
-
await exec(
|
|
86
|
-
INSERT INTO ${TABLE} (job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
await exec(
|
|
86
|
+
`INSERT INTO ${TABLE} (job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual)
|
|
87
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, $9)`,
|
|
88
|
+
{ params: [
|
|
89
|
+
entry.jobId,
|
|
90
|
+
entry.startedAt,
|
|
91
|
+
entry.finishedAt,
|
|
92
|
+
entry.durationMs,
|
|
93
|
+
entry.success,
|
|
94
|
+
entry.error || null,
|
|
95
|
+
resultJson,
|
|
96
|
+
logsJson,
|
|
97
|
+
entry.manual
|
|
98
|
+
]}
|
|
99
|
+
);
|
|
99
100
|
} catch (err) {
|
|
100
101
|
// Non-blocking — log persistence should never crash the scheduler
|
|
101
102
|
logger.error(`[cron-store] Failed to persist log for "${entry.jobId}"`, { error: err });
|
|
@@ -104,13 +105,14 @@ export function createCronStore(driver: DataDriver): CronStore | undefined {
|
|
|
104
105
|
|
|
105
106
|
async fetchLogs(jobId: string, limit = 50): Promise<CronJobLogEntry[]> {
|
|
106
107
|
try {
|
|
107
|
-
const rows = await exec(
|
|
108
|
-
SELECT job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
const rows = await exec(
|
|
109
|
+
`SELECT job_id, started_at, finished_at, duration_ms, success, error, result, logs, manual
|
|
110
|
+
FROM ${TABLE}
|
|
111
|
+
WHERE job_id = $1
|
|
112
|
+
ORDER BY started_at DESC
|
|
113
|
+
LIMIT $2`,
|
|
114
|
+
{ params: [jobId, limit] }
|
|
115
|
+
);
|
|
114
116
|
|
|
115
117
|
return rows.map(rowToLogEntry);
|
|
116
118
|
} catch (err) {
|
package/src/email/index.ts
CHANGED
|
@@ -10,9 +10,10 @@ export type {
|
|
|
10
10
|
PasswordResetTemplateFunction,
|
|
11
11
|
EmailVerificationTemplateFunction,
|
|
12
12
|
UserInvitationTemplateFunction,
|
|
13
|
-
WelcomeEmailTemplateFunction
|
|
13
|
+
WelcomeEmailTemplateFunction,
|
|
14
|
+
MagicLinkTemplateFunction
|
|
14
15
|
} from "./types";
|
|
15
16
|
|
|
16
17
|
export { SMTPEmailService, createEmailService } from "./smtp-email-service";
|
|
17
18
|
|
|
18
|
-
export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate } from "./templates";
|
|
19
|
+
export { getPasswordResetTemplate, getEmailVerificationTemplate, getUserInvitationTemplate, getWelcomeEmailTemplate, getMagicLinkTemplate } from "./templates";
|
package/src/email/templates.ts
CHANGED
|
@@ -386,3 +386,85 @@ Si tienes alguna pregunta, no dudes en contactarnos respondiendo a este correo.
|
|
|
386
386
|
html,
|
|
387
387
|
text };
|
|
388
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Default magic link email template
|
|
392
|
+
*/
|
|
393
|
+
export function getMagicLinkTemplate(
|
|
394
|
+
magicLinkUrl: string,
|
|
395
|
+
user: TemplateUser,
|
|
396
|
+
appName = "Rebase"
|
|
397
|
+
): { subject: string; html: string; text: string } {
|
|
398
|
+
const greeting = getGreeting(user);
|
|
399
|
+
|
|
400
|
+
const subject = `Sign in to ${appName}`;
|
|
401
|
+
|
|
402
|
+
const html = `
|
|
403
|
+
<!DOCTYPE html>
|
|
404
|
+
<html>
|
|
405
|
+
<head>
|
|
406
|
+
<meta charset="utf-8">
|
|
407
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
408
|
+
<title>${subject}</title>
|
|
409
|
+
</head>
|
|
410
|
+
<body style="margin: 0; padding: 0; background-color: #f8fafc;">
|
|
411
|
+
<div style="${styles.container}">
|
|
412
|
+
<div style="${styles.card}">
|
|
413
|
+
<h1 style="${styles.heading}">Sign In to ${appName}</h1>
|
|
414
|
+
|
|
415
|
+
<p style="${styles.paragraph}">
|
|
416
|
+
Hi ${greeting},
|
|
417
|
+
</p>
|
|
418
|
+
|
|
419
|
+
<p style="${styles.paragraph}">
|
|
420
|
+
We received a request to sign in to your ${appName} account.
|
|
421
|
+
Click the button below to log in:
|
|
422
|
+
</p>
|
|
423
|
+
|
|
424
|
+
<div style="text-align: center;">
|
|
425
|
+
<a href="${magicLinkUrl}" style="${styles.button}">Sign In</a>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<p style="${styles.paragraph}">
|
|
429
|
+
Or copy and paste this link into your browser:
|
|
430
|
+
</p>
|
|
431
|
+
<p style="color: #3b82f6; word-break: break-all; font-size: 14px;">
|
|
432
|
+
${magicLinkUrl}
|
|
433
|
+
</p>
|
|
434
|
+
|
|
435
|
+
<div style="${styles.warning}">
|
|
436
|
+
⏰ This link will expire in 15 minutes for security reasons and can only be used once.
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div style="${styles.footer}">
|
|
440
|
+
<p style="margin: 0;">
|
|
441
|
+
If you didn't request this sign-in link, you can safely ignore this email.
|
|
442
|
+
No action is needed.
|
|
443
|
+
</p>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
</body>
|
|
448
|
+
</html>
|
|
449
|
+
`.trim();
|
|
450
|
+
|
|
451
|
+
const text = `
|
|
452
|
+
Sign In to ${appName}
|
|
453
|
+
|
|
454
|
+
Hi ${greeting},
|
|
455
|
+
|
|
456
|
+
We received a request to sign in to your ${appName} account.
|
|
457
|
+
|
|
458
|
+
Click this link to log in:
|
|
459
|
+
${magicLinkUrl}
|
|
460
|
+
|
|
461
|
+
This link will expire in 15 minutes for security reasons and can only be used once.
|
|
462
|
+
|
|
463
|
+
If you didn't request this sign-in link, you can safely ignore this email.
|
|
464
|
+
No action is needed.
|
|
465
|
+
`.trim();
|
|
466
|
+
|
|
467
|
+
return { subject,
|
|
468
|
+
html,
|
|
469
|
+
text };
|
|
470
|
+
}
|
package/src/email/types.ts
CHANGED
|
@@ -57,6 +57,14 @@ export type WelcomeEmailTemplateFunction = (
|
|
|
57
57
|
appName: string
|
|
58
58
|
) => { subject: string; html: string; text?: string };
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Template function for magic link emails
|
|
62
|
+
*/
|
|
63
|
+
export type MagicLinkTemplateFunction = (
|
|
64
|
+
magicLinkUrl: string,
|
|
65
|
+
user: { email: string; displayName?: string | null }
|
|
66
|
+
) => { subject: string; html: string; text?: string };
|
|
67
|
+
|
|
60
68
|
/**
|
|
61
69
|
* Complete email configuration
|
|
62
70
|
*/
|
|
@@ -89,6 +97,13 @@ export interface EmailConfig {
|
|
|
89
97
|
*/
|
|
90
98
|
verifyEmailUrl?: string;
|
|
91
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Base URL for magic link login (e.g., "https://myapp.com")
|
|
102
|
+
* The magic link will be: {baseUrl}/auth/magic-link?token={token}
|
|
103
|
+
* Falls back to `resetPasswordUrl` if not set.
|
|
104
|
+
*/
|
|
105
|
+
magicLinkUrl?: string;
|
|
106
|
+
|
|
92
107
|
/**
|
|
93
108
|
* Application name to use in email templates
|
|
94
109
|
*/
|
|
@@ -102,5 +117,6 @@ export interface EmailConfig {
|
|
|
102
117
|
emailVerification?: EmailVerificationTemplateFunction;
|
|
103
118
|
userInvitation?: UserInvitationTemplateFunction;
|
|
104
119
|
welcomeEmail?: WelcomeEmailTemplateFunction;
|
|
120
|
+
magicLink?: MagicLinkTemplateFunction;
|
|
105
121
|
};
|
|
106
122
|
}
|
package/src/init.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
BootstrappedAuth,
|
|
6
6
|
DatabaseAdapter,
|
|
7
7
|
DataDriver,
|
|
8
|
+
DataSourceDefinition,
|
|
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";
|
|
@@ -123,6 +126,12 @@ export interface RebaseAuthConfig {
|
|
|
123
126
|
*/
|
|
124
127
|
hooks?: AuthHooks;
|
|
125
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Enable magic link (passwordless email) authentication.
|
|
131
|
+
* Requires email to be configured.
|
|
132
|
+
*/
|
|
133
|
+
magicLink?: boolean;
|
|
134
|
+
|
|
126
135
|
[key: string]: unknown;
|
|
127
136
|
}
|
|
128
137
|
|
|
@@ -133,6 +142,16 @@ export interface RebaseBackendConfig {
|
|
|
133
142
|
app: Hono<HonoEnv>;
|
|
134
143
|
basePath?: string;
|
|
135
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Declared data sources, shared with the frontend `<Rebase dataSources>`.
|
|
147
|
+
*
|
|
148
|
+
* Used to resolve each collection's engine (capabilities) and transport.
|
|
149
|
+
* Collections on a `direct`/`custom` transport are client-only: the backend
|
|
150
|
+
* still owns their schema/registry but does **not** generate server data
|
|
151
|
+
* routes for them. Server-mediated sources (the default) need no entry.
|
|
152
|
+
*/
|
|
153
|
+
dataSources?: DataSourceDefinition[];
|
|
154
|
+
|
|
136
155
|
/**
|
|
137
156
|
* Database bootstrappers.
|
|
138
157
|
*/
|
|
@@ -313,6 +332,11 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
313
332
|
configureMiddlewares(config.app, basePath, isProduction, config);
|
|
314
333
|
|
|
315
334
|
const collectionRegistry = new BackendCollectionRegistry();
|
|
335
|
+
// Declared data sources — drives engine resolution (capabilities) and the
|
|
336
|
+
// server-vs-direct transport distinction. Set before collections register
|
|
337
|
+
// so normalization can resolve each collection's engine.
|
|
338
|
+
const dataSourceRegistry = createDataSourceRegistry(config.dataSources);
|
|
339
|
+
collectionRegistry.setDataSources(dataSourceRegistry);
|
|
316
340
|
let activeCollections = config.collections || [];
|
|
317
341
|
if (config.collectionsDir && activeCollections.length === 0) {
|
|
318
342
|
activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
|
|
@@ -401,6 +425,56 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
401
425
|
const defaultBootstrapper = bootstrappers.find(b => b.id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
|
|
402
426
|
const defaultRealtimeService = defaultDriverResult.realtimeProvider;
|
|
403
427
|
|
|
428
|
+
// Resolve a collection path (e.g. "products", "authors/1/posts") to its
|
|
429
|
+
// data-source key — shared by the data-driver router and the realtime
|
|
430
|
+
// router. Falls back to the default key for unknown paths.
|
|
431
|
+
const keyForCollectionPath = (collectionPath: string): string => {
|
|
432
|
+
const slug = collectionPath.replace(/^\/+/, "").split("/")[0]?.split("?")[0];
|
|
433
|
+
if (!slug) return DEFAULT_DRIVER_ID;
|
|
434
|
+
const collection = collectionRegistry.get(slug) ?? collectionRegistry.getCollectionByPath(slug);
|
|
435
|
+
if (!collection) return DEFAULT_DRIVER_ID;
|
|
436
|
+
return resolveDataSource(collection, dataSourceRegistry).key;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// ── Data-source misconfiguration check ────────────────────────────────
|
|
440
|
+
// A server-transport collection whose resolved data-source key has no
|
|
441
|
+
// registered driver delegate would silently fall back to the default
|
|
442
|
+
// driver — i.e. land in the wrong database. Warn loudly so this surfaces
|
|
443
|
+
// at boot rather than as mysterious data going to the wrong engine.
|
|
444
|
+
{
|
|
445
|
+
const unresolved = new Map<string, string[]>();
|
|
446
|
+
const nonRlsEngines = new Set<string>();
|
|
447
|
+
for (const collection of activeCollections) {
|
|
448
|
+
const ds = resolveDataSource(collection, dataSourceRegistry);
|
|
449
|
+
if (ds.transport !== "server") continue; // direct/custom are client-only
|
|
450
|
+
// Server engines without row-level security enforce authorization
|
|
451
|
+
// only at the application layer — surface this so it isn't a
|
|
452
|
+
// silent assumption. (The default Postgres engine supports RLS.)
|
|
453
|
+
if (!ds.capabilities.supportsRLS) nonRlsEngines.add(ds.engine);
|
|
454
|
+
if (ds.key === DEFAULT_DRIVER_ID) continue; // always maps to the default
|
|
455
|
+
if (!driverRegistry.has(ds.key)) {
|
|
456
|
+
const slugs = unresolved.get(ds.key) ?? [];
|
|
457
|
+
slugs.push(collection.slug ?? collection.name ?? "?");
|
|
458
|
+
unresolved.set(ds.key, slugs);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const [key, slugs] of unresolved) {
|
|
462
|
+
logger.warn(
|
|
463
|
+
`[DataSource] No driver registered for data source "${key}" ` +
|
|
464
|
+
`(used by: ${slugs.join(", ")}). These collections will fall back to the ` +
|
|
465
|
+
`default driver "${defaultDriverId}" — register a bootstrapper with this id, ` +
|
|
466
|
+
`or mark the data source as a direct/custom transport in \`dataSources\`.`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
for (const engine of nonRlsEngines) {
|
|
470
|
+
logger.warn(
|
|
471
|
+
`[DataSource] Engine "${engine}" does not support row-level security; ` +
|
|
472
|
+
`authorization for its collections is enforced only at the application layer ` +
|
|
473
|
+
`(authentication still applies). Ensure app-level checks or engine-native rules are in place.`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
404
478
|
// 2. Initialize Auth & History via the default driver's bootstrapper
|
|
405
479
|
let authConfigResult: BootstrappedAuth | undefined = undefined;
|
|
406
480
|
let serviceKey: string | undefined;
|
|
@@ -439,6 +513,23 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
439
513
|
}
|
|
440
514
|
}
|
|
441
515
|
|
|
516
|
+
// The built-in auth subsystem (users, sessions, repository) is
|
|
517
|
+
// bootstrapped on the DEFAULT driver. If the auth collection is
|
|
518
|
+
// routed to a non-default data source, login would read/write the
|
|
519
|
+
// default engine while the collection's data views hit another —
|
|
520
|
+
// a split-brain user store. Warn loudly.
|
|
521
|
+
if (safeAuthConfig.collection) {
|
|
522
|
+
const authDs = resolveDataSource(safeAuthConfig.collection, dataSourceRegistry);
|
|
523
|
+
if (authDs.key !== DEFAULT_DRIVER_ID) {
|
|
524
|
+
logger.warn(
|
|
525
|
+
`[Auth] The auth collection "${safeAuthConfig.collection.slug}" is on data source ` +
|
|
526
|
+
`"${authDs.key}", but the built-in auth system always uses the default data source. ` +
|
|
527
|
+
`Move the auth collection to the default data source, or replace auth with an AuthAdapter ` +
|
|
528
|
+
`that manages users in "${authDs.key}".`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
442
533
|
// Extract the collection-level auth config (if `auth` is an object, not just `true`)
|
|
443
534
|
const collectionAuth = safeAuthConfig.collection ? safeAuthConfig.collection.auth : undefined;
|
|
444
535
|
const collectionAuthConfig = (typeof collectionAuth === "object" && collectionAuth !== null) ? collectionAuth : undefined;
|
|
@@ -561,7 +652,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
561
652
|
oauthProviders,
|
|
562
653
|
serviceKey,
|
|
563
654
|
authHooks: safeAuthConfig.hooks,
|
|
564
|
-
collectionAuthConfig
|
|
655
|
+
collectionAuthConfig,
|
|
656
|
+
enableMagicLink: safeAuthConfig.magicLink ?? false
|
|
565
657
|
});
|
|
566
658
|
}
|
|
567
659
|
|
|
@@ -668,18 +760,39 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
668
760
|
);
|
|
669
761
|
}
|
|
670
762
|
|
|
763
|
+
// Multi-data-source routing: when more than one database engine is
|
|
764
|
+
// registered (e.g. Postgres + MongoDB in one instance), resolve the
|
|
765
|
+
// delegate per request from the request's collection data source. The
|
|
766
|
+
// auth middleware then scopes that delegate (RLS for Postgres, no-op
|
|
767
|
+
// for engines without `withAuth()`) into the request context. For a
|
|
768
|
+
// single-engine backend this is omitted — behaviour is unchanged.
|
|
769
|
+
const dataPathMarker = `${basePath}/data/`;
|
|
770
|
+
const resolveRequestDriver = (reqPath: string): DataDriver => {
|
|
771
|
+
const i = reqPath.indexOf(dataPathMarker);
|
|
772
|
+
const collectionPath = i >= 0 ? reqPath.slice(i + dataPathMarker.length) : reqPath;
|
|
773
|
+
const key = keyForCollectionPath(collectionPath);
|
|
774
|
+
// Use the authoritative default for the default key; otherwise the
|
|
775
|
+
// named delegate, falling back to default if it isn't registered.
|
|
776
|
+
if (!key || key === DEFAULT_DRIVER_ID) return defaultDriver;
|
|
777
|
+
return driverRegistry.get(key) ?? defaultDriver;
|
|
778
|
+
};
|
|
779
|
+
const multiEngine = bootstrappers.length > 1;
|
|
780
|
+
const resolveDriver = multiEngine ? ((c: { req: { path: string } }) => resolveRequestDriver(c.req.path)) : undefined;
|
|
781
|
+
|
|
671
782
|
// Use adapter middleware when an AuthAdapter is available,
|
|
672
783
|
// falling back to the built-in JWT middleware otherwise.
|
|
673
784
|
if (authAdapter) {
|
|
674
785
|
dataRouter.use("/*", createAdapterAuthMiddleware({
|
|
675
786
|
adapter: authAdapter,
|
|
676
787
|
driver: defaultDriver,
|
|
788
|
+
resolveDriver,
|
|
677
789
|
requireAuth: dataRequireAuth,
|
|
678
790
|
apiKeyStore
|
|
679
791
|
}));
|
|
680
792
|
} else {
|
|
681
793
|
dataRouter.use("/*", createAuthMiddleware({
|
|
682
794
|
driver: defaultDriver,
|
|
795
|
+
resolveDriver,
|
|
683
796
|
requireAuth: dataRequireAuth,
|
|
684
797
|
serviceKey,
|
|
685
798
|
apiKeyStore
|
|
@@ -702,8 +815,15 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
702
815
|
dataRouter.route("/", historyRoutes);
|
|
703
816
|
}
|
|
704
817
|
|
|
818
|
+
// Only generate server data routes for server-mediated collections.
|
|
819
|
+
// Collections on a direct/custom transport are client-only — the
|
|
820
|
+
// backend must not expose a (mis-engined) endpoint for them.
|
|
821
|
+
const serverCollections = activeCollections.filter(
|
|
822
|
+
(collection) => resolveDataSource(collection, dataSourceRegistry).transport === "server"
|
|
823
|
+
);
|
|
824
|
+
|
|
705
825
|
const restGenerator = new RestApiGenerator(
|
|
706
|
-
|
|
826
|
+
serverCollections,
|
|
707
827
|
defaultDriver,
|
|
708
828
|
config.hooks?.data,
|
|
709
829
|
authAdapter
|
|
@@ -876,8 +996,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
876
996
|
}
|
|
877
997
|
}
|
|
878
998
|
|
|
879
|
-
|
|
880
|
-
|
|
999
|
+
// With multiple realtime-capable engines, route subscriptions to the
|
|
1000
|
+
// provider owning each collection (the realtime counterpart of the data
|
|
1001
|
+
// router). The single WebSocket server is driven by this composite.
|
|
1002
|
+
// Single-engine setups use the default provider unchanged.
|
|
1003
|
+
const effectiveRealtimeService: RealtimeProvider = Object.keys(realtimeServices).length > 1
|
|
1004
|
+
? createRoutedRealtimeService({
|
|
1005
|
+
providers: realtimeServices,
|
|
1006
|
+
defaultKey: defaultDriverId,
|
|
1007
|
+
resolveKey: keyForCollectionPath
|
|
1008
|
+
})
|
|
1009
|
+
: defaultRealtimeService as RealtimeProvider;
|
|
1010
|
+
|
|
1011
|
+
if (defaultBootstrapper.initializeWebsockets && effectiveRealtimeService) {
|
|
1012
|
+
await defaultBootstrapper.initializeWebsockets(config.server, effectiveRealtimeService, defaultDriver, config.auth, authAdapter);
|
|
881
1013
|
}
|
|
882
1014
|
|
|
883
1015
|
logger.info("Rebase Backend Initialized");
|
|
@@ -896,7 +1028,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
896
1028
|
driverRegistry,
|
|
897
1029
|
driver: defaultDriver,
|
|
898
1030
|
realtimeServices,
|
|
899
|
-
realtimeService:
|
|
1031
|
+
realtimeService: effectiveRealtimeService,
|
|
900
1032
|
auth: authConfigResult,
|
|
901
1033
|
history: historyConfigResult,
|
|
902
1034
|
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
|
+
}
|
package/src/storage/routes.ts
CHANGED
|
@@ -145,6 +145,10 @@ export function createStorageRoutes(config: StorageRoutesConfig): Hono<HonoEnv>
|
|
|
145
145
|
* Path: /file/{bucket}/{path} or /file/{path}
|
|
146
146
|
*/
|
|
147
147
|
router.get("/file/*", readAuthMiddleware, async (c) => {
|
|
148
|
+
// Allow cross-origin loading so admin frontends on different
|
|
149
|
+
// ports (dev) or domains (CDN) can render images via <img>.
|
|
150
|
+
c.header("Cross-Origin-Resource-Policy", "cross-origin");
|
|
151
|
+
|
|
148
152
|
const rawPath = extractWildcardPath(c);
|
|
149
153
|
if (!rawPath) {
|
|
150
154
|
throw ApiError.notFound("File not found");
|