@rebasepro/server-core 0.3.0 → 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.
Files changed (40) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +5 -8
  2. package/dist/common/src/data/query_builder.d.ts +6 -2
  3. package/dist/index.es.js +318 -282
  4. package/dist/index.es.js.map +1 -1
  5. package/dist/index.umd.js +318 -282
  6. package/dist/index.umd.js.map +1 -1
  7. package/dist/server-core/src/api/rest/query-parser.d.ts +2 -0
  8. package/dist/server-core/src/api/types.d.ts +2 -1
  9. package/dist/server-core/src/email/types.d.ts +1 -0
  10. package/dist/server-core/src/init.d.ts +31 -1
  11. package/dist/types/src/controllers/auth.d.ts +2 -2
  12. package/dist/types/src/controllers/client.d.ts +25 -40
  13. package/dist/types/src/controllers/data.d.ts +21 -3
  14. package/dist/types/src/controllers/data_driver.d.ts +5 -0
  15. package/dist/types/src/controllers/email.d.ts +2 -0
  16. package/dist/types/src/types/auth_adapter.d.ts +3 -56
  17. package/dist/types/src/types/backend.d.ts +2 -2
  18. package/dist/types/src/types/backend_hooks.d.ts +2 -17
  19. package/dist/types/src/types/collections.d.ts +9 -5
  20. package/dist/types/src/types/entity_views.d.ts +19 -28
  21. package/dist/types/src/types/properties.d.ts +9 -7
  22. package/dist/types/src/types/user_management_delegate.d.ts +16 -53
  23. package/dist/types/src/users/index.d.ts +0 -1
  24. package/dist/types/src/users/user.d.ts +0 -1
  25. package/package.json +5 -5
  26. package/src/api/rest/api-generator.ts +11 -9
  27. package/src/api/rest/query-parser.ts +184 -63
  28. package/src/api/types.ts +2 -1
  29. package/src/auth/admin-routes.ts +2 -91
  30. package/src/auth/builtin-auth-adapter.ts +5 -55
  31. package/src/auth/custom-auth-adapter.ts +1 -1
  32. package/src/email/smtp-email-service.ts +31 -0
  33. package/src/email/types.ts +1 -0
  34. package/src/init.ts +136 -24
  35. package/src/storage/image-transform.ts +2 -1
  36. package/test/admin-routes.test.ts +0 -169
  37. package/test/backend-hooks-admin.test.ts +0 -25
  38. package/test/custom-auth-adapter.test.ts +2 -10
  39. package/test/smtp-email-service.test.ts +169 -0
  40. package/dist/types/src/users/roles.d.ts +0 -14
@@ -5,7 +5,7 @@ import { requireAuth, requireAdmin, createRequireAuth } from "./middleware";
5
5
  import type { AuthHooks } from "./auth-hooks";
6
6
  import { resolveAuthHooks } from "./auth-hooks";
7
7
  import { AuthModuleConfig } from "./routes";
8
- import type { BackendHooks, AdminUser, AdminRole, BackendHookContext } from "@rebasepro/types";
8
+ import type { BackendHooks, AdminUser, BackendHookContext } from "@rebasepro/types";
9
9
 
10
10
  interface AdminRouteOptions extends AuthModuleConfig {
11
11
  serviceKey?: string;
@@ -99,12 +99,7 @@ export function createAdminRoutes(config: AdminRouteOptions): Hono<HonoEnv> {
99
99
  return results.filter((u): u is AdminUser => u !== null);
100
100
  }
101
101
 
102
- /** Apply roles.afterRead hook to an array and filter nulls */
103
- async function applyRoleAfterReadBatch(roles: AdminRole[], ctx: BackendHookContext): Promise<AdminRole[]> {
104
- if (!hooks?.roles?.afterRead) return roles;
105
- const results = await Promise.all(roles.map(r => hooks!.roles!.afterRead!(r, ctx)));
106
- return results.filter((r): r is AdminRole => r !== null);
107
- }
102
+
108
103
 
109
104
  /** Convert a DB user record + role IDs into the AdminUser API shape */
110
105
  function toAdminUser(u: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; createdAt?: Date | string; updatedAt?: Date | string }, roles: string[]): AdminUser {
@@ -533,91 +528,7 @@ displayName: existing.displayName }, appName);
533
528
  return c.json({ success: true });
534
529
  });
535
530
 
536
- router.get("/roles", requireAdmin, async (c) => {
537
- const roles = await authRepo.listRoles();
538
- const hookCtx = buildHookContext(c, "GET");
539
-
540
- let adminRoles: AdminRole[] = roles.map(r => ({
541
- id: r.id,
542
- name: r.name,
543
- isAdmin: r.isAdmin,
544
- defaultPermissions: r.defaultPermissions
545
- }));
546
-
547
- adminRoles = await applyRoleAfterReadBatch(adminRoles, hookCtx);
548
-
549
- return c.json({ roles: adminRoles });
550
- });
551
-
552
- router.get("/roles/:roleId", requireAdmin, async (c) => {
553
- const roleId = c.req.param("roleId");
554
- const role = await authRepo.getRoleById(roleId);
555
-
556
- if (!role) {
557
- throw ApiError.notFound("Role not found");
558
- }
559
-
560
- return c.json({ role });
561
- });
562
-
563
- router.post("/roles", requireAdmin, async (c) => {
564
- const body = await c.req.json();
565
- const { id, name, isAdmin, defaultPermissions } = body;
566
-
567
- if (!id || !name) {
568
- throw ApiError.badRequest("Role ID and name are required", "INVALID_INPUT");
569
- }
570
-
571
- const existing = await authRepo.getRoleById(id);
572
- if (existing) {
573
- throw ApiError.conflict("Role already exists", "ROLE_EXISTS");
574
- }
575
-
576
- const role = await authRepo.createRole({
577
- id,
578
- name,
579
- isAdmin: isAdmin ?? false,
580
- defaultPermissions: defaultPermissions ?? null
581
- });
582
531
 
583
- return c.json({ role }, 201);
584
- });
585
-
586
- router.put("/roles/:roleId", requireAdmin, async (c) => {
587
- const roleId = c.req.param("roleId");
588
- const body = await c.req.json();
589
- const { name, isAdmin, defaultPermissions } = body;
590
-
591
- const existing = await authRepo.getRoleById(roleId);
592
- if (!existing) {
593
- throw ApiError.notFound("Role not found");
594
- }
595
-
596
- const role = await authRepo.updateRole(roleId, {
597
- name,
598
- isAdmin,
599
- defaultPermissions
600
- });
601
-
602
- return c.json({ role });
603
- });
604
-
605
- router.delete("/roles/:roleId", requireAdmin, async (c) => {
606
- const roleId = c.req.param("roleId");
607
-
608
- if (["admin", "editor", "viewer"].includes(roleId)) {
609
- throw ApiError.badRequest("Cannot delete built-in roles", "BUILTIN_ROLE");
610
- }
611
-
612
- const existing = await authRepo.getRoleById(roleId);
613
- if (!existing) {
614
- throw ApiError.notFound("Role not found");
615
- }
616
-
617
- await authRepo.deleteRole(roleId);
618
-
619
- return c.json({ success: true });
620
- });
621
532
 
622
533
  return router;
623
534
  }
@@ -15,13 +15,10 @@ import type {
15
15
  AuthenticatedUser,
16
16
  AuthAdapterCapabilities,
17
17
  UserManagementAdapter,
18
- RoleManagementAdapter,
19
18
  AuthUserListOptions,
20
19
  AuthUserListResult,
21
20
  AuthUserData,
22
21
  AuthCreateUserData,
23
- AuthRoleData,
24
- AuthCreateRoleData,
25
22
  BootstrappedAuth,
26
23
  BackendHooks,
27
24
  } from "@rebasepro/types";
@@ -131,8 +128,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
131
128
  // Resolve roles from the repository
132
129
  let roles: string[] = payload.roles || [];
133
130
  try {
134
- const userRoles = await authRepository.getUserRoles(payload.userId);
135
- roles = userRoles.map((r) => r.id);
131
+ roles = await authRepository.getUserRoleIds(payload.userId);
136
132
  } catch {
137
133
  // Fall back to token roles if repository lookup fails
138
134
  }
@@ -174,8 +170,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
174
170
 
175
171
  let roles: string[] = payload.roles || [];
176
172
  try {
177
- const userRoles = await authRepository.getUserRoles(payload.userId);
178
- roles = userRoles.map((r) => r.id);
173
+ roles = await authRepository.getUserRoleIds(payload.userId);
179
174
  } catch {
180
175
  // Fall back to token roles if repository lookup fails
181
176
  }
@@ -194,7 +189,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
194
189
 
195
190
  userManagement: createUserManagementFromRepo(authRepository, resolvedOps, authHooks),
196
191
 
197
- roleManagement: createRoleManagementFromRepo(authRepository),
192
+
198
193
 
199
194
  createAuthRoutes(): Hono<HonoEnv> | undefined {
200
195
  return createAuthRoutes({
@@ -327,9 +322,8 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
327
322
  }
328
323
  },
329
324
 
330
- async getUserRoles(userId: string): Promise<AuthRoleData[]> {
331
- const roles = await repo.getUserRoles(userId);
332
- return roles.map(toAuthRoleData);
325
+ async getUserRoles(userId: string): Promise<string[]> {
326
+ return repo.getUserRoleIds(userId);
333
327
  },
334
328
 
335
329
  async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
@@ -338,40 +332,6 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
338
332
  };
339
333
  }
340
334
 
341
- function createRoleManagementFromRepo(repo: AuthRepository): RoleManagementAdapter {
342
- return {
343
- async listRoles(): Promise<AuthRoleData[]> {
344
- const roles = await repo.listRoles();
345
- return roles.map(toAuthRoleData);
346
- },
347
-
348
- async getRoleById(id: string): Promise<AuthRoleData | null> {
349
- const role = await repo.getRoleById(id);
350
- return role ? toAuthRoleData(role) : null;
351
- },
352
-
353
- async createRole(data: AuthCreateRoleData): Promise<AuthRoleData> {
354
- const role = await repo.createRole({
355
- id: data.id,
356
- name: data.name,
357
- isAdmin: data.isAdmin,
358
- defaultPermissions: data.defaultPermissions,
359
- collectionPermissions: data.collectionPermissions,
360
- });
361
- return toAuthRoleData(role);
362
- },
363
-
364
- async updateRole(id: string, data: Partial<AuthRoleData>): Promise<AuthRoleData | null> {
365
- const role = await repo.updateRole(id, data);
366
- return role ? toAuthRoleData(role) : null;
367
- },
368
-
369
- async deleteRole(id: string): Promise<void> {
370
- await repo.deleteRole(id);
371
- },
372
- };
373
- }
374
-
375
335
  function toAuthUserData(user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; metadata?: Record<string, unknown>; createdAt?: Date; updatedAt?: Date }): AuthUserData {
376
336
  return {
377
337
  id: user.id,
@@ -384,13 +344,3 @@ function toAuthUserData(user: { id: string; email: string; displayName?: string
384
344
  updatedAt: user.updatedAt,
385
345
  };
386
346
  }
387
-
388
- function toAuthRoleData(role: { id: string; name: string; isAdmin: boolean; defaultPermissions?: unknown; collectionPermissions?: unknown }): AuthRoleData {
389
- return {
390
- id: role.id,
391
- name: role.name,
392
- isAdmin: role.isAdmin,
393
- defaultPermissions: role.defaultPermissions as AuthRoleData["defaultPermissions"],
394
- collectionPermissions: role.collectionPermissions as AuthRoleData["collectionPermissions"],
395
- };
396
- }
@@ -76,7 +76,7 @@ export function createCustomAuthAdapter(options: CustomAuthAdapterOptions): Auth
76
76
 
77
77
  userManagement: options.userManagement,
78
78
 
79
- roleManagement: options.roleManagement,
79
+
80
80
 
81
81
  getCapabilities() {
82
82
  return defaultCapabilities;
@@ -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,21 +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";
48
+
26
49
  import { createHistoryRoutes } from "./history";
27
- import { EmailConfig, createEmailService } from "./email";
28
50
  import type { EmailService } from "./email";
51
+ import { createEmailService, EmailConfig } from "./email";
29
52
  import type { OAuthProvider } from "./auth/interfaces";
30
53
  import type { AuthHooks } from "./auth/auth-hooks";
31
54
  import { _initRebase } from "./singleton";
32
55
 
33
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;
34
73
  jwtSecret?: string;
35
74
  accessExpiresIn?: string;
36
75
  refreshExpiresIn?: string;
@@ -83,6 +122,7 @@ export interface RebaseAuthConfig {
83
122
  * ```
84
123
  */
85
124
  hooks?: AuthHooks;
125
+
86
126
  [key: string]: unknown;
87
127
  }
88
128
 
@@ -135,6 +175,20 @@ export interface RebaseBackendConfig {
135
175
  */
136
176
  storage?: BackendStorageConfig | StorageController | Record<string, BackendStorageConfig | StorageController>;
137
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[];
138
192
  enableSwagger?: boolean;
139
193
  functionsDir?: string;
140
194
  cronsDir?: string;
@@ -218,11 +272,13 @@ export interface RebaseBackendInstance {
218
272
  storageController?: StorageController;
219
273
  collectionRegistry: BackendCollectionRegistry;
220
274
  cronScheduler?: import("./cron").CronScheduler;
275
+
221
276
  /**
222
277
  * Deep health check that verifies database connectivity.
223
278
  * Returns latency and component status.
224
279
  */
225
280
  healthCheck(): Promise<HealthCheckResult>;
281
+
226
282
  /**
227
283
  * Graceful shutdown helper for the BaaS instance.
228
284
  * Stops the cron scheduler and closes the HTTP server, allowing
@@ -288,8 +344,20 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
288
344
  let activeCollections = config.collections || [];
289
345
  if (config.collectionsDir && activeCollections.length === 0) {
290
346
  activeCollections = await loadCollectionsFromDirectory(config.collectionsDir);
291
- logger.info("Auto-discovered collections", { count: activeCollections.length,
292
- dir: config.collectionsDir });
347
+ logger.info("Auto-discovered collections", {
348
+ count: activeCollections.length,
349
+ dir: config.collectionsDir
350
+ });
351
+ }
352
+
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");
293
361
  }
294
362
 
295
363
  const realtimeServices: Record<string, RealtimeProvider> = {};
@@ -333,8 +401,10 @@ dir: config.collectionsDir });
333
401
  defaultDriverId = b.id || bootstrapper.type;
334
402
  }
335
403
 
336
- const driverResult = await bootstrapper.initializeDriver({ collections: activeCollections,
337
- collectionRegistry });
404
+ const driverResult = await bootstrapper.initializeDriver({
405
+ collections: activeCollections,
406
+ collectionRegistry
407
+ });
338
408
  delegates[b.id || bootstrapper.type] = driverResult.driver;
339
409
 
340
410
  if ((b.id || bootstrapper.type) === defaultDriverId || !defaultDriverResult) {
@@ -354,7 +424,9 @@ collectionRegistry });
354
424
  if (!defaultDriver || !defaultDriverResult) {
355
425
  throw new Error("Default driver not initialized by bootstrappers");
356
426
  }
357
- 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];
358
430
  const defaultRealtimeService = defaultDriverResult.realtimeProvider;
359
431
 
360
432
  // 2. Initialize Auth & History via the default driver's bootstrapper
@@ -378,7 +450,6 @@ collectionRegistry });
378
450
  // (the return type still exposes `auth?: BootstrappedAuth`)
379
451
  authConfigResult = {
380
452
  userService: authAdapter.userManagement ?? {},
381
- roleService: authAdapter.roleManagement ?? {},
382
453
  };
383
454
  } else {
384
455
  // ── RebaseAuthConfig — wrap in built-in adapter ──
@@ -520,7 +591,10 @@ collectionRegistry });
520
591
 
521
592
  if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
522
593
  const { createLinkedinProvider } = await import("./auth");
523
- oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as { clientId: string; clientSecret: string }));
594
+ oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as {
595
+ clientId: string;
596
+ clientSecret: string
597
+ }));
524
598
  }
525
599
 
526
600
  if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
@@ -790,6 +864,29 @@ collectionRegistry });
790
864
  if (emailService) {
791
865
  Object.assign(serverClient, { email: emailService });
792
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
+ }
879
+ }
880
+
881
+ // Attach raw SQL capability when the driver supports it (Postgres, MySQL).
882
+ // Document databases (MongoDB, Firestore) won't have this.
883
+ const driverAdmin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
884
+ if (isSQLAdmin(driverAdmin)) {
885
+ Object.assign(serverClient, {
886
+ sql: (query: string, options?: { database?: string; role?: string }) =>
887
+ driverAdmin.executeSql(query, options)
888
+ });
889
+ logger.info("SQL capability attached to singleton");
793
890
  }
794
891
 
795
892
  _initRebase(serverClient);
@@ -842,8 +939,10 @@ collectionRegistry });
842
939
  const fnRoutes = createFunctionRoutes(loadedFunctions);
843
940
  functionsRouter.route("/", fnRoutes);
844
941
  config.app.route(`${basePath}/functions`, functionsRouter);
845
- logger.info("Mounted custom functions", { count: loadedFunctions.length,
846
- path: `${basePath}/functions` });
942
+ logger.info("Mounted custom functions", {
943
+ count: loadedFunctions.length,
944
+ path: `${basePath}/functions`
945
+ });
847
946
  }
848
947
  }
849
948
 
@@ -892,13 +991,19 @@ path: `${basePath}/functions` });
892
991
  // Start the scheduler
893
992
  cronScheduler.start();
894
993
 
895
- logger.info("Mounted cron jobs", { count: loadedCronJobs.length,
896
- path: `${basePath}/cron` });
994
+ logger.info("Mounted cron jobs", {
995
+ count: loadedCronJobs.length,
996
+ path: `${basePath}/cron`
997
+ });
897
998
  }
898
999
  }
899
1000
 
900
- if ((defaultBootstrapper as BackendBootstrapper & { initializeWebsockets?: (...args: unknown[]) => unknown }).initializeWebsockets) {
901
- 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);
902
1007
  }
903
1008
 
904
1009
  logger.info("Rebase Backend Initialized");
@@ -913,12 +1018,16 @@ path: `${basePath}/cron` });
913
1018
  await admin.executeSql("SELECT 1");
914
1019
  } else {
915
1020
  // Fallback: try a lightweight fetch to confirm driver is responsive
916
- await defaultDriver.fetchCollection({ path: "__health_check_nonexistent__",
917
- limit: 1 });
1021
+ await defaultDriver.fetchCollection({
1022
+ path: "__health_check_nonexistent__",
1023
+ limit: 1
1024
+ });
918
1025
  }
919
1026
  const latencyMs = Math.round(performance.now() - start);
920
- return { healthy: true,
921
- latencyMs };
1027
+ return {
1028
+ healthy: true,
1029
+ latencyMs
1030
+ };
922
1031
  } catch (error: unknown) {
923
1032
  const latencyMs = Math.round(performance.now() - start);
924
1033
  logger.error("Health check failed", {
@@ -952,7 +1061,10 @@ latencyMs };
952
1061
  // timer callbacks don't fire against a closed pool.
953
1062
  for (const [key, rt] of Object.entries(realtimeServices)) {
954
1063
  try {
955
- 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
+ };
956
1068
  if (typeof rtWithLifecycle.destroy === "function") {
957
1069
  await rtWithLifecycle.destroy();
958
1070
  logger.info(`Realtime service "${key}" destroyed`);
@@ -6,9 +6,10 @@
6
6
  * in-memory cache to avoid redundant processing.
7
7
  */
8
8
 
9
- // Lazy-load sharp to avoid crashing when it isn't installed (e.g. in tests)
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
10
  let sharpFactory: ((input: Buffer | Uint8Array) => any) | undefined;
11
11
 
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
13
  async function getSharp(): Promise<(input: Buffer | Uint8Array) => any> {
13
14
  if (!sharpFactory) {
14
15
  try {