@rebasepro/server-core 0.2.5 → 0.4.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.
@@ -1,4 +1,4 @@
1
- import type { VectorSearchParams } from "@rebasepro/types";
1
+ import type { VectorSearchParams, LogicalCondition, FilterCondition } from "@rebasepro/types";
2
2
  import { QueryOptions } from "../types";
3
3
 
4
4
  /**
@@ -20,6 +20,86 @@ export function mapOperator(op: string): string | null {
20
20
  }
21
21
  }
22
22
 
23
+ function getLastValue(val: unknown): unknown {
24
+ if (Array.isArray(val)) {
25
+ return val[val.length - 1];
26
+ }
27
+ return val;
28
+ }
29
+
30
+ export function parseLogicalString(str: string): FilterCondition | LogicalCondition {
31
+ str = str.trim();
32
+ if (str.startsWith("or(") && str.endsWith(")")) {
33
+ const inner = str.slice(3, -1);
34
+ return { type: "or", conditions: parseLogicalList(inner) };
35
+ }
36
+ if (str.startsWith("and(") && str.endsWith(")")) {
37
+ const inner = str.slice(4, -1);
38
+ return { type: "and", conditions: parseLogicalList(inner) };
39
+ }
40
+
41
+ // It's a leaf condition: field.op.val
42
+ const firstDot = str.indexOf(".");
43
+ if (firstDot === -1) {
44
+ return { column: str, operator: "==", value: true };
45
+ }
46
+ const field = str.substring(0, firstDot);
47
+ const rest = str.substring(firstDot + 1);
48
+ const secondDot = rest.indexOf(".");
49
+ let op = "eq";
50
+ let valStr = rest;
51
+ if (secondDot !== -1) {
52
+ op = rest.substring(0, secondDot);
53
+ valStr = rest.substring(secondDot + 1);
54
+ } else {
55
+ op = "eq";
56
+ valStr = rest;
57
+ }
58
+
59
+ const rebaseOp = (mapOperator(op) || "==") as FilterCondition["operator"];
60
+ let parsedVal: unknown = valStr;
61
+ if (valStr === "true") parsedVal = true;
62
+ else if (valStr === "false") parsedVal = false;
63
+ else if (valStr === "null") parsedVal = null;
64
+ else if (!isNaN(Number(valStr)) && valStr.trim() !== "") parsedVal = Number(valStr);
65
+ else if (valStr.startsWith("(")) {
66
+ const arrayContent = valStr.endsWith(")") ? valStr.slice(1, -1) : valStr.slice(1);
67
+ parsedVal = arrayContent.split(",").map(v => {
68
+ const trimmed = v.trim();
69
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
70
+ if (trimmed === "true") return true;
71
+ if (trimmed === "false") return false;
72
+ if (trimmed === "null") return null;
73
+ return trimmed;
74
+ });
75
+ }
76
+
77
+ return { column: field, operator: rebaseOp, value: parsedVal };
78
+ }
79
+
80
+ function parseLogicalList(str: string): (FilterCondition | LogicalCondition)[] {
81
+ const list: (FilterCondition | LogicalCondition)[] = [];
82
+ let depth = 0;
83
+ let current = "";
84
+
85
+ for (let i = 0; i < str.length; i++) {
86
+ const char = str[i];
87
+ if (char === "(") depth++;
88
+ if (char === ")") depth--;
89
+
90
+ if (char === "," && depth === 0) {
91
+ list.push(parseLogicalString(current));
92
+ current = "";
93
+ } else {
94
+ current += char;
95
+ }
96
+ }
97
+ if (current) {
98
+ list.push(parseLogicalString(current));
99
+ }
100
+ return list;
101
+ }
102
+
23
103
  /**
24
104
  * Parse query parameters into QueryOptions
25
105
  */
@@ -27,10 +107,15 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
27
107
  const options: QueryOptions = {};
28
108
 
29
109
  // Pagination
30
- if (query.limit) options.limit = parseInt(String(query.limit));
31
- if (query.offset) options.offset = parseInt(String(query.offset));
32
- if (query.page) {
33
- const page = parseInt(String(query.page));
110
+ const limitVal = getLastValue(query.limit);
111
+ if (limitVal) options.limit = parseInt(String(limitVal));
112
+
113
+ const offsetVal = getLastValue(query.offset);
114
+ if (offsetVal) options.offset = parseInt(String(offsetVal));
115
+
116
+ const pageVal = getLastValue(query.page);
117
+ if (pageVal) {
118
+ const page = parseInt(String(pageVal));
34
119
  const limit = options.limit || 20;
35
120
  options.offset = (page - 1) * limit;
36
121
  }
@@ -38,59 +123,88 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
38
123
  // Filtering
39
124
  options.where = {};
40
125
 
126
+ // Handle logical conditions
127
+ const orVal = getLastValue(query.or);
128
+ const andVal = getLastValue(query.and);
129
+ if (orVal) {
130
+ let orStr = String(orVal).trim();
131
+ if (orStr.startsWith("(") && orStr.endsWith(")")) {
132
+ orStr = orStr.slice(1, -1);
133
+ }
134
+ options.logical = {
135
+ type: "or",
136
+ conditions: parseLogicalList(orStr)
137
+ };
138
+ } else if (andVal) {
139
+ let andStr = String(andVal).trim();
140
+ if (andStr.startsWith("(") && andStr.endsWith(")")) {
141
+ andStr = andStr.slice(1, -1);
142
+ }
143
+ options.logical = {
144
+ type: "and",
145
+ conditions: parseLogicalList(andStr)
146
+ };
147
+ }
41
148
 
42
149
  // PostgREST-style filtering: ?field=op.value
43
- const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold"];
150
+ const reservedQueryKeys = ["limit", "offset", "page", "orderBy", "include", "fields", "searchString", "vector_search", "vector", "vector_distance", "vector_threshold", "or", "and"];
44
151
  for (const [key, rawValue] of Object.entries(query)) {
45
152
  if (reservedQueryKeys.includes(key)) continue;
46
153
 
47
- const value = Array.isArray(rawValue) ? rawValue[rawValue.length - 1] : rawValue;
48
-
49
- if (typeof value === "string") {
50
- const parts = value.split(".");
51
- if (parts.length >= 2) {
52
- const op = parts[0];
53
- const val = parts.slice(1).join(".");
54
- const rebaseOp = mapOperator(op);
55
-
56
- if (rebaseOp) {
57
- let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
58
- // Attempt to parse primitive types or arrays
59
- if (val === "true") parsedVal = true;
60
- else if (val === "false") parsedVal = false;
61
- else if (val === "null") parsedVal = null;
62
- else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
63
- else if (val.startsWith("(")) {
64
- // Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
65
- const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
66
- parsedVal = arrayContent.split(",").map(v => {
67
- const trimmed = v.trim();
68
- if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
69
- if (trimmed === "true") return true;
70
- if (trimmed === "false") return false;
71
- if (trimmed === "null") return null;
72
- return trimmed;
73
- });
74
- }
154
+ const rawValues = Array.isArray(rawValue) ? rawValue : [rawValue];
155
+ const conditions: [string, unknown][] = [];
156
+
157
+ for (const value of rawValues) {
158
+ if (typeof value === "string") {
159
+ const parts = value.split(".");
160
+ if (parts.length >= 2) {
161
+ const op = parts[0];
162
+ const val = parts.slice(1).join(".");
163
+ const rebaseOp = mapOperator(op);
75
164
 
76
- options.where[key] = [rebaseOp, parsedVal];
165
+ if (rebaseOp) {
166
+ let parsedVal: string | number | boolean | null | (string | number | boolean | null)[] = val;
167
+ // Attempt to parse primitive types or arrays
168
+ if (val === "true") parsedVal = true;
169
+ else if (val === "false") parsedVal = false;
170
+ else if (val === "null") parsedVal = null;
171
+ else if (!isNaN(Number(val)) && val.trim() !== "") parsedVal = Number(val);
172
+ else if (val.startsWith("(")) {
173
+ // Array for 'in' or 'not-in' ops (e.g. (1,2,3) or (a,b,c))
174
+ const arrayContent = val.endsWith(")") ? val.slice(1, -1) : val.slice(1);
175
+ parsedVal = arrayContent.split(",").map(v => {
176
+ const trimmed = v.trim();
177
+ if (!isNaN(Number(trimmed)) && trimmed !== "") return Number(trimmed);
178
+ if (trimmed === "true") return true;
179
+ if (trimmed === "false") return false;
180
+ if (trimmed === "null") return null;
181
+ return trimmed;
182
+ });
183
+ }
184
+
185
+ conditions.push([rebaseOp, parsedVal]);
186
+ } else {
187
+ // Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
188
+ let parsedVal: string | number | boolean | null = value;
189
+ if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
190
+ conditions.push(["==", parsedVal]);
191
+ }
77
192
  } else {
78
- // Fallback: assume implicit eq if the dot wasn't an operator (e.g. email or float)
193
+ // Implicit eq
79
194
  let parsedVal: string | number | boolean | null = value;
80
- if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
81
- options.where[key] = ["==", parsedVal];
195
+ if (value === "true") parsedVal = true;
196
+ else if (value === "false") parsedVal = false;
197
+ else if (value === "null") parsedVal = null;
198
+ else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
199
+
200
+ conditions.push(["==", parsedVal]);
82
201
  }
83
- } else {
84
- // Implicit eq
85
- let parsedVal: string | number | boolean | null = value;
86
- if (value === "true") parsedVal = true;
87
- else if (value === "false") parsedVal = false;
88
- else if (value === "null") parsedVal = null;
89
- else if (!isNaN(Number(value)) && value.trim() !== "") parsedVal = Number(value);
90
-
91
- options.where[key] = ["==", parsedVal];
92
202
  }
93
203
  }
204
+
205
+ if (conditions.length > 0) {
206
+ options.where[key] = conditions.length === 1 ? conditions[0] : conditions;
207
+ }
94
208
  }
95
209
 
96
210
  if (Object.keys(options.where).length === 0) {
@@ -98,15 +212,16 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
98
212
  }
99
213
 
100
214
  // Sorting
101
- if (query.orderBy) {
215
+ const orderByVal = getLastValue(query.orderBy);
216
+ if (orderByVal) {
102
217
  try {
103
- options.orderBy = typeof query.orderBy === "string"
104
- ? JSON.parse(query.orderBy)
105
- : query.orderBy;
218
+ options.orderBy = typeof orderByVal === "string"
219
+ ? JSON.parse(orderByVal)
220
+ : orderByVal;
106
221
  } catch {
107
222
  // Try simple format: "field:direction"
108
- if (typeof query.orderBy === "string") {
109
- const [field, direction] = query.orderBy.split(":");
223
+ if (typeof orderByVal === "string") {
224
+ const [field, direction] = orderByVal.split(":");
110
225
  const dir = (direction === "desc" ? "desc" : "asc") as "asc" | "desc";
111
226
  options.orderBy = [
112
227
  {
@@ -119,8 +234,9 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
119
234
  }
120
235
 
121
236
  // Relation includes
122
- if (query.include) {
123
- const includeStr = String(query.include).trim();
237
+ const includeVal = getLastValue(query.include);
238
+ if (includeVal) {
239
+ const includeStr = String(includeVal).trim();
124
240
  if (includeStr === "*") {
125
241
  options.include = ["*"];
126
242
  } else {
@@ -129,14 +245,17 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
129
245
  }
130
246
 
131
247
  // Field selection
132
- if (query.fields) {
133
- const fieldsStr = String(query.fields).trim();
248
+ const fieldsVal = getLastValue(query.fields);
249
+ if (fieldsVal) {
250
+ const fieldsStr = String(fieldsVal).trim();
134
251
  options.fields = fieldsStr.split(",").map(s => s.trim()).filter(Boolean);
135
252
  }
136
253
 
137
254
  // Vector similarity search
138
- if (query.vector_search && query.vector) {
139
- const vectorStr = String(query.vector);
255
+ const vectorSearchVal = getLastValue(query.vector_search);
256
+ const vectorVal = getLastValue(query.vector);
257
+ if (vectorSearchVal && vectorVal) {
258
+ const vectorStr = String(vectorVal);
140
259
  let queryVector: number[];
141
260
  try {
142
261
  queryVector = JSON.parse(vectorStr) as number[];
@@ -147,19 +266,21 @@ export function parseQueryOptions(query: Record<string, unknown>): QueryOptions
147
266
  throw new Error("Invalid vector format. Expected JSON array of numbers, e.g. [0.1,0.2,0.3]");
148
267
  }
149
268
 
150
- const distanceParam = query.vector_distance ? String(query.vector_distance) : "cosine";
269
+ const distanceParamVal = getLastValue(query.vector_distance);
270
+ const distanceParam = distanceParamVal ? String(distanceParamVal) : "cosine";
151
271
  if (distanceParam !== "cosine" && distanceParam !== "l2" && distanceParam !== "inner_product") {
152
272
  throw new Error(`Invalid vector_distance: ${distanceParam}. Expected: cosine, l2, or inner_product`);
153
273
  }
154
274
 
155
275
  const vectorSearch: VectorSearchParams = {
156
- property: String(query.vector_search),
276
+ property: String(vectorSearchVal),
157
277
  vector: queryVector,
158
278
  distance: distanceParam,
159
279
  };
160
280
 
161
- if (query.vector_threshold) {
162
- const threshold = parseFloat(String(query.vector_threshold));
281
+ const thresholdVal = getLastValue(query.vector_threshold);
282
+ if (thresholdVal) {
283
+ const threshold = parseFloat(String(thresholdVal));
163
284
  if (isNaN(threshold)) {
164
285
  throw new Error("Invalid vector_threshold. Expected a number.");
165
286
  }
package/src/api/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EntityCollection, VectorSearchParams } from "@rebasepro/types";
1
+ import { EntityCollection, VectorSearchParams, LogicalCondition } from "@rebasepro/types";
2
2
  import { AuthResult } from "../auth/middleware";
3
3
  import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
4
  import { DataDriver } from "@rebasepro/types";
@@ -77,6 +77,7 @@ export interface QueryOptions {
77
77
  limit?: number;
78
78
  offset?: number;
79
79
  where?: Record<string, unknown>;
80
+ logical?: LogicalCondition;
80
81
  orderBy?: Array<{ field: string; direction: "asc" | "desc" }>;
81
82
  include?: string[];
82
83
  /** Columns to return in the response (field-level selection) */
@@ -1,6 +1,18 @@
1
1
  import { createTransport, Transporter } from "nodemailer";
2
2
  import { EmailService, EmailSendOptions, EmailConfig } from "./types";
3
3
 
4
+ /**
5
+ * Safely parse a hostname from a URL string
6
+ */
7
+ function getHostname(urlStr: string): string | undefined {
8
+ try {
9
+ const url = new URL(urlStr.includes("://") ? urlStr : `https://${urlStr}`);
10
+ return url.hostname;
11
+ } catch {
12
+ return undefined;
13
+ }
14
+ }
15
+
4
16
  /**
5
17
  * SMTP Email Service implementation using Nodemailer
6
18
  */
@@ -12,7 +24,26 @@ export class SMTPEmailService implements EmailService {
12
24
  this.config = config;
13
25
 
14
26
  if (config.smtp) {
27
+ let smtpName = config.smtp.name;
28
+ if (!smtpName) {
29
+ const urlsToTry = [
30
+ process.env.FRONTEND_URL,
31
+ config.resetPasswordUrl,
32
+ config.verifyEmailUrl
33
+ ];
34
+ for (const urlStr of urlsToTry) {
35
+ if (urlStr) {
36
+ const hostname = getHostname(urlStr);
37
+ if (hostname) {
38
+ smtpName = hostname;
39
+ break;
40
+ }
41
+ }
42
+ }
43
+ }
44
+
15
45
  this.transporter = createTransport({
46
+ name: smtpName,
16
47
  host: config.smtp.host,
17
48
  port: config.smtp.port,
18
49
  secure: config.smtp.secure ?? (config.smtp.port === 465),
@@ -22,6 +22,7 @@ export interface SMTPConfig {
22
22
  user: string;
23
23
  pass: string;
24
24
  };
25
+ name?: string;
25
26
  }
26
27
 
27
28
  /**
package/src/init.ts CHANGED
@@ -1,7 +1,21 @@
1
- import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin, BackendHooks, AuthAdapter, DatabaseAdapter } from "@rebasepro/types";
1
+ import {
2
+ AuthAdapter,
3
+ BackendBootstrapper,
4
+ BackendHooks,
5
+ BootstrappedAuth,
6
+ DatabaseAdapter,
7
+ DataDriver,
8
+ EntityCollection,
9
+ HealthCheckResult,
10
+ InitializedDriver,
11
+ isPostgresCollection,
12
+ isSQLAdmin,
13
+ RealtimeProvider,
14
+ SecurityRule
15
+ } from "@rebasepro/types";
2
16
  import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
3
17
  import { loadCollectionsFromDirectory } from "./collections/loader";
4
- import { DriverRegistry, DEFAULT_DRIVER_ID, DefaultDriverRegistry } from "./services/driver-registry";
18
+ import { DEFAULT_DRIVER_ID, DefaultDriverRegistry, DriverRegistry } from "./services/driver-registry";
5
19
  import { Server } from "http";
6
20
 
7
21
  import { RestApiGenerator } from "./api/rest/api-generator";
@@ -16,22 +30,46 @@ import { HonoEnv } from "./api/types";
16
30
  import { configureLogLevel } from "./utils/logging";
17
31
  import { logger } from "./utils/logger";
18
32
  import { requestLogger } from "./utils/request-logger";
19
- import { createAdminRoutes, createAuthRoutes, requireAuth, requireAdmin, configureJwt } from "./auth";
20
- import { createStorageController, createStorageRoutes, DEFAULT_STORAGE_ID, DefaultStorageRegistry, BackendStorageConfig, StorageController, StorageRegistry } from "./storage";
33
+ import { configureJwt, requireAdmin, requireAuth } from "./auth";
34
+ import {
35
+ BackendStorageConfig,
36
+ createStorageController,
37
+ createStorageRoutes,
38
+ DEFAULT_STORAGE_ID,
39
+ DefaultStorageRegistry,
40
+ StorageController,
41
+ StorageRegistry
42
+ } from "./storage";
43
+ import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
21
44
  import { createApiKeyStore } from "./auth/api-keys/api-key-store";
22
45
  import { createApiKeyRoutes } from "./auth/api-keys/api-key-routes";
23
- import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
24
46
  import { createApiKeyRateLimiter } from "./auth/rate-limiter";
25
47
  import { createRebaseClient } from "@rebasepro/client";
26
- import { defaultUsersCollection } from "@rebasepro/common";
48
+
27
49
  import { createHistoryRoutes } from "./history";
28
- import { EmailConfig, createEmailService } from "./email";
29
50
  import type { EmailService } from "./email";
51
+ import { createEmailService, EmailConfig } from "./email";
30
52
  import type { OAuthProvider } from "./auth/interfaces";
31
53
  import type { AuthHooks } from "./auth/auth-hooks";
32
54
  import { _initRebase } from "./singleton";
33
55
 
34
56
  export interface RebaseAuthConfig {
57
+ /**
58
+ * The collection that represents auth users.
59
+ *
60
+ * When provided, this collection's underlying database table is used
61
+ * for all auth operations (login, registration, password reset, etc.).
62
+ *
63
+ * Import the built-in default:
64
+ * ```ts
65
+ * import { defaultUsersCollection } from "@rebasepro/common";
66
+ * auth: { collection: defaultUsersCollection, jwtSecret: "..." }
67
+ * ```
68
+ *
69
+ * Or pass your own collection with the required auth fields
70
+ * (email, passwordHash, displayName, etc.).
71
+ */
72
+ collection?: EntityCollection;
35
73
  jwtSecret?: string;
36
74
  accessExpiresIn?: string;
37
75
  refreshExpiresIn?: string;
@@ -84,6 +122,7 @@ export interface RebaseAuthConfig {
84
122
  * ```
85
123
  */
86
124
  hooks?: AuthHooks;
125
+
87
126
  [key: string]: unknown;
88
127
  }
89
128
 
@@ -136,6 +175,20 @@ export interface RebaseBackendConfig {
136
175
  */
137
176
  storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
138
177
  history?: unknown;
178
+ /**
179
+ * Default security rules applied to any collection that does not define
180
+ * its own `securityRules`. Opt-in — if not set, collections without
181
+ * explicit rules remain unrestricted (beyond `requireAuth`).
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * defaultSecurityRules: [
186
+ * { operation: "select", access: "public" },
187
+ * { operations: ["insert", "update", "delete"], roles: ["admin"] }
188
+ * ]
189
+ * ```
190
+ */
191
+ defaultSecurityRules?: SecurityRule[];
139
192
  enableSwagger?: boolean;
140
193
  functionsDir?: string;
141
194
  cronsDir?: string;
@@ -219,11 +272,13 @@ export interface RebaseBackendInstance {
219
272
  storageController?: StorageController;
220
273
  collectionRegistry: BackendCollectionRegistry;
221
274
  cronScheduler?: import("./cron").CronScheduler;
275
+
222
276
  /**
223
277
  * Deep health check that verifies database connectivity.
224
278
  * Returns latency and component status.
225
279
  */
226
280
  healthCheck(): Promise<HealthCheckResult>;
281
+
227
282
  /**
228
283
  * Graceful shutdown helper for the BaaS instance.
229
284
  * Stops the cron scheduler and closes the HTTP server, allowing
@@ -289,15 +344,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
289
344
  let activeCollections = config.collections || [];
290
345
  if (config.collectionsDir && activeCollections.length === 0) {
291
346
  activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
292
- logger.info("Auto-discovered collections", { count: activeCollections.length,
293
- dir: config.collectionsDir });
347
+ logger.info("Auto-discovered collections", {
348
+ count: activeCollections.length,
349
+ dir: config.collectionsDir
350
+ });
294
351
  }
295
352
 
296
- if (config.auth) {
297
- // Prepend defaultUsersCollection if not overridden by the developer's collections
298
- activeCollections = Array.from(
299
- new Map([defaultUsersCollection, ...activeCollections].map(c => [c.slug, c])).values()
300
- );
353
+ // Apply default security rules to collections that don't define their own
354
+ if (config.defaultSecurityRules?.length) {
355
+ for (const collection of activeCollections) {
356
+ if (isPostgresCollection(collection) && (!collection.securityRules || collection.securityRules.length === 0)) {
357
+ collection.securityRules = config.defaultSecurityRules;
358
+ }
359
+ }
360
+ logger.info("Default security rules applied to collections without explicit rules");
301
361
  }
302
362
 
303
363
  const realtimeServices: Record<string, RealtimeProvider> = {};
@@ -341,8 +401,10 @@ dir: config.collectionsDir });
341
401
  defaultDriverId = b.id || bootstrapper.type;
342
402
  }
343
403
 
344
- const driverResult = await bootstrapper.initializeDriver({ collections: activeCollections,
345
- collectionRegistry });
404
+ const driverResult = await bootstrapper.initializeDriver({
405
+ collections: activeCollections,
406
+ collectionRegistry
407
+ });
346
408
  delegates[b.id || bootstrapper.type] = driverResult.driver;
347
409
 
348
410
  if ((b.id || bootstrapper.type) === defaultDriverId || !defaultDriverResult) {
@@ -362,7 +424,9 @@ collectionRegistry });
362
424
  if (!defaultDriver || !defaultDriverResult) {
363
425
  throw new Error("Default driver not initialized by bootstrappers");
364
426
  }
365
- const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & { id?: string }).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
427
+ const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & {
428
+ id?: string
429
+ }).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
366
430
  const defaultRealtimeService = defaultDriverResult.realtimeProvider;
367
431
 
368
432
  // 2. Initialize Auth & History via the default driver's bootstrapper
@@ -527,7 +591,10 @@ collectionRegistry });
527
591
 
528
592
  if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
529
593
  const { createLinkedinProvider } = await import("./auth");
530
- oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as { clientId: string; clientSecret: string }));
594
+ oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as {
595
+ clientId: string;
596
+ clientSecret: string
597
+ }));
531
598
  }
532
599
 
533
600
  if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
@@ -797,6 +864,18 @@ collectionRegistry });
797
864
  if (emailService) {
798
865
  Object.assign(serverClient, { email: emailService });
799
866
  logger.info("Email service attached to singleton", { configured: emailService.isConfigured() });
867
+
868
+ if (emailService.isConfigured() && typeof emailService.verifyConnection === "function") {
869
+ emailService.verifyConnection().then((success) => {
870
+ if (!success) {
871
+ logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.");
872
+ } else {
873
+ logger.info("SMTP connection verified successfully.");
874
+ }
875
+ }).catch((err) => {
876
+ logger.warn("Warning: SMTP connection verification failed. Email delivery may fail.", { error: err });
877
+ });
878
+ }
800
879
  }
801
880
 
802
881
  // Attach raw SQL capability when the driver supports it (Postgres, MySQL).
@@ -860,8 +939,10 @@ collectionRegistry });
860
939
  const fnRoutes = createFunctionRoutes(loadedFunctions);
861
940
  functionsRouter.route("/", fnRoutes);
862
941
  config.app.route(`${basePath}/functions`, functionsRouter);
863
- logger.info("Mounted custom functions", { count: loadedFunctions.length,
864
- path: `${basePath}/functions` });
942
+ logger.info("Mounted custom functions", {
943
+ count: loadedFunctions.length,
944
+ path: `${basePath}/functions`
945
+ });
865
946
  }
866
947
  }
867
948
 
@@ -910,13 +991,19 @@ path: `${basePath}/functions` });
910
991
  // Start the scheduler
911
992
  cronScheduler.start();
912
993
 
913
- logger.info("Mounted cron jobs", { count: loadedCronJobs.length,
914
- path: `${basePath}/cron` });
994
+ logger.info("Mounted cron jobs", {
995
+ count: loadedCronJobs.length,
996
+ path: `${basePath}/cron`
997
+ });
915
998
  }
916
999
  }
917
1000
 
918
- if ((defaultBootstrapper as BackendBootstrapper & { initializeWebsockets?: (...args: unknown[]) => unknown }).initializeWebsockets) {
919
- await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
1001
+ if ((defaultBootstrapper as BackendBootstrapper & {
1002
+ initializeWebsockets?: (...args: unknown[]) => unknown
1003
+ }).initializeWebsockets) {
1004
+ await (defaultBootstrapper as BackendBootstrapper & {
1005
+ initializeWebsockets: (...args: unknown[]) => unknown
1006
+ }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
920
1007
  }
921
1008
 
922
1009
  logger.info("Rebase Backend Initialized");
@@ -931,12 +1018,16 @@ path: `${basePath}/cron` });
931
1018
  await admin.executeSql("SELECT 1");
932
1019
  } else {
933
1020
  // Fallback: try a lightweight fetch to confirm driver is responsive
934
- await defaultDriver.fetchCollection({ path: "__health_check_nonexistent__",
935
- limit: 1 });
1021
+ await defaultDriver.fetchCollection({
1022
+ path: "__health_check_nonexistent__",
1023
+ limit: 1
1024
+ });
936
1025
  }
937
1026
  const latencyMs = Math.round(performance.now() - start);
938
- return { healthy: true,
939
- latencyMs };
1027
+ return {
1028
+ healthy: true,
1029
+ latencyMs
1030
+ };
940
1031
  } catch (error: unknown) {
941
1032
  const latencyMs = Math.round(performance.now() - start);
942
1033
  logger.error("Health check failed", {
@@ -970,7 +1061,10 @@ latencyMs };
970
1061
  // timer callbacks don't fire against a closed pool.
971
1062
  for (const [key, rt] of Object.entries(realtimeServices)) {
972
1063
  try {
973
- const rtWithLifecycle = rt as RealtimeProvider & { destroy?: () => Promise<void>; stopListening?: () => Promise<void> };
1064
+ const rtWithLifecycle = rt as RealtimeProvider & {
1065
+ destroy?: () => Promise<void>;
1066
+ stopListening?: () => Promise<void>
1067
+ };
974
1068
  if (typeof rtWithLifecycle.destroy === "function") {
975
1069
  await rtWithLifecycle.destroy();
976
1070
  logger.info(`Realtime service "${key}" destroyed`);