@rebasepro/server-core 0.4.0 → 0.5.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.
@@ -28,6 +28,21 @@ export interface ErrorResponse {
28
28
  details?: unknown;
29
29
  };
30
30
  }
31
+ /**
32
+ * General shape of errors that flow through the API error handler.
33
+ * Extends Error with optional HTTP status, error code, and details.
34
+ */
35
+ export interface RebaseApiError extends Error {
36
+ statusCode?: number;
37
+ code?: string;
38
+ details?: unknown;
39
+ }
40
+ /**
41
+ * Type guard for errors that carry optional API metadata (statusCode, code, details).
42
+ * Returns true for any Error instance — the optional properties are then
43
+ * checked via normal property access.
44
+ */
45
+ export declare function isRebaseApiError(error: unknown): error is RebaseApiError;
31
46
  /**
32
47
  * Hono error-handling middleware (`app.onError`).
33
48
  * Converts any error into the canonical `{ error: { message, code } }` shape.
@@ -9,6 +9,16 @@ export interface AccessTokenPayload {
9
9
  uid?: string;
10
10
  /** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
11
11
  aal?: "aal1" | "aal2";
12
+ /** Email claim from the JWT, if present */
13
+ email?: string;
14
+ /** Display name claim from the JWT, if present */
15
+ displayName?: string;
16
+ /** Photo URL claim from the JWT, if present */
17
+ photoURL?: string;
18
+ /** Whether MFA has been verified for this session */
19
+ mfa_verified?: boolean;
20
+ /** Authentication Methods Reference — list of methods used (e.g. 'pwd', 'otp') */
21
+ amr?: string[];
12
22
  }
13
23
  /**
14
24
  * Configure JWT settings - call this during initialization.
@@ -1,5 +1,6 @@
1
1
  import type { Entity } from "./entities";
2
2
  import type { EntityCollection, FilterValues, WhereFilterOp } from "./collections";
3
+ import type { AuthAdapter } from "./auth_adapter";
3
4
  /**
4
5
  * Abstract database connection interface.
5
6
  * Represents a connection to any database system.
@@ -182,6 +183,24 @@ export interface RealtimeProvider {
182
183
  * Notify all relevant subscribers of an entity update
183
184
  */
184
185
  notifyEntityUpdate(path: string, entityId: string, entity: Entity | null, databaseId?: string): Promise<void>;
186
+ /**
187
+ * Called when the HTTP server is ready and listening.
188
+ * Useful for providers that need the server address for callbacks.
189
+ */
190
+ onServerReady?(serverInfo: {
191
+ port: number;
192
+ hostname?: string;
193
+ }): void;
194
+ /**
195
+ * Gracefully shut down the realtime provider.
196
+ * Called during server shutdown to clean up resources.
197
+ */
198
+ destroy?(): Promise<void>;
199
+ /**
200
+ * Stop the internal LISTEN client (e.g., PostgreSQL LISTEN/NOTIFY).
201
+ * Called during graceful shutdown before closing database connections.
202
+ */
203
+ stopListening?(): Promise<void>;
185
204
  }
186
205
  /**
187
206
  * Abstract collection registry interface.
@@ -464,6 +483,22 @@ export interface BackendBootstrapper {
464
483
  * (e.g., `"postgres"`, `"mongodb"`, `"mysql"`).
465
484
  */
466
485
  type: string;
486
+ /**
487
+ * Unique identifier for this bootstrapper instance.
488
+ * Used to register the driver in the driver registry.
489
+ * Defaults to `type` if not set.
490
+ */
491
+ id?: string;
492
+ /**
493
+ * Whether this bootstrapper provides the default driver.
494
+ * When true, the coordinator uses this driver as the primary one.
495
+ */
496
+ isDefault?: boolean;
497
+ /**
498
+ * Run database migrations for this driver.
499
+ * Called by the coordinator after all drivers are initialized.
500
+ */
501
+ runMigrations?(config: unknown, driverResult: InitializedDriver): Promise<void>;
467
502
  /**
468
503
  * Create a DataDriver from the given config.
469
504
  * This is the only **required** method.
@@ -498,7 +533,7 @@ export interface BackendBootstrapper {
498
533
  /**
499
534
  * Initialize WebSocket server for realtime operations.
500
535
  */
501
- initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver").DataDriver, config?: unknown): Promise<void> | void;
536
+ initializeWebsockets?(server: unknown, realtimeService: RealtimeProvider, driver: import("../controllers/data_driver").DataDriver, config?: unknown, authAdapter?: AuthAdapter): Promise<void> | void;
502
537
  }
503
538
  /**
504
539
  * Result of `BackendBootstrapper.initializeDriver()`.
@@ -343,6 +343,23 @@ export interface BaseEntityCollection<M extends Record<string, unknown> = Record
343
343
  * Builder for the collection actions rendered in the toolbar
344
344
  */
345
345
  Actions?: ComponentRef<CollectionActionsProps>[];
346
+ /**
347
+ * The database table name for this collection.
348
+ * Automatically set for PostgreSQL collections.
349
+ * For non-SQL backends, this may be undefined.
350
+ */
351
+ table?: string;
352
+ /**
353
+ * Relations defined for this collection.
354
+ * Populated at normalization time from inline relation properties
355
+ * or explicit relation definitions.
356
+ */
357
+ relations?: Relation[];
358
+ /**
359
+ * Security rules for this collection (Row Level Security).
360
+ * When defined, the backend enforces access control policies.
361
+ */
362
+ securityRules?: SecurityRule[];
346
363
  }
347
364
  /**
348
365
  * A collection backed by PostgreSQL (or any SQL database).
@@ -436,7 +453,10 @@ export interface MongoDBCollection<M extends Record<string, unknown> = Record<st
436
453
  * @group Models
437
454
  */
438
455
  export type EntityCollection<M extends Record<string, unknown> = Record<string, unknown>, USER extends User = User> = PostgresCollection<M, USER> | FirebaseCollection<M, USER> | MongoDBCollection<M, USER>;
439
- /** An EntityCollection that supports SQL-style relations (e.g. Postgres). */
456
+ /**
457
+ * An EntityCollection that supports SQL-style relations (e.g. Postgres).
458
+ * @deprecated Use `EntityCollection` directly — `table`, `relations`, and `securityRules` are now on `BaseEntityCollection`.
459
+ */
440
460
  export type CollectionWithRelations<M extends Record<string, unknown> = Record<string, unknown>> = EntityCollection<M> & {
441
461
  table?: string;
442
462
  relations?: Relation[];
@@ -641,14 +641,6 @@ export interface MapProperty extends BaseProperty {
641
641
  * Properties that are displayed when rendered as a preview
642
642
  */
643
643
  previewProperties?: string[];
644
- /**
645
- * Allow the user to add only some keys in this map.
646
- * By default, all properties of the map have the corresponding field in
647
- * the form view. Setting this flag to true allows to pick only some.
648
- * Useful for map that can have a lot of sub-properties that may not be
649
- * needed
650
- */
651
- pickOnlySomeKeys?: boolean;
652
644
  /**
653
645
  * Render this map as a key-value table that allows to use
654
646
  * arbitrary keys. You don't need to define the properties in this case.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-core",
3
3
  "type": "module",
4
- "version": "0.4.0",
4
+ "version": "0.5.0",
5
5
  "description": "Database-Agnostic Backend Core for Rebase",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -54,10 +54,10 @@
54
54
  "ts-morph": "27.0.2",
55
55
  "ws": "^8.20.1",
56
56
  "zod": "^3.25.76",
57
- "@rebasepro/client": "0.4.0",
58
- "@rebasepro/common": "0.4.0",
59
- "@rebasepro/utils": "0.4.0",
60
- "@rebasepro/types": "0.4.0"
57
+ "@rebasepro/common": "0.5.0",
58
+ "@rebasepro/client": "0.5.0",
59
+ "@rebasepro/types": "0.5.0",
60
+ "@rebasepro/utils": "0.5.0"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/jest": "^29.5.14",
package/src/api/errors.ts CHANGED
@@ -70,13 +70,32 @@ export interface ErrorResponse {
70
70
  };
71
71
  }
72
72
 
73
+ /**
74
+ * General shape of errors that flow through the API error handler.
75
+ * Extends Error with optional HTTP status, error code, and details.
76
+ */
77
+ export interface RebaseApiError extends Error {
78
+ statusCode?: number;
79
+ code?: string;
80
+ details?: unknown;
81
+ }
82
+
83
+ /**
84
+ * Type guard for errors that carry optional API metadata (statusCode, code, details).
85
+ * Returns true for any Error instance — the optional properties are then
86
+ * checked via normal property access.
87
+ */
88
+ export function isRebaseApiError(error: unknown): error is RebaseApiError {
89
+ return error instanceof Error;
90
+ }
91
+
73
92
  /**
74
93
  * Hono error-handling middleware (`app.onError`).
75
94
  * Converts any error into the canonical `{ error: { message, code } }` shape.
76
95
  */
77
96
  export const errorHandler: ErrorHandler = (err, c) => {
78
97
  // Typecast custom error properties
79
- const error = err as Error & { statusCode?: number; code?: string; details?: unknown, name?: string };
98
+ const error: RebaseApiError = err;
80
99
 
81
100
  if (error instanceof ApiError || error.name === "ApiError") {
82
101
  // Operational errors — log at warn level
@@ -314,7 +314,7 @@ content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResp
314
314
  };
315
315
 
316
316
  // ── Subcollection routes ──────────────────────────────────────
317
- const relations = (collection as EntityCollection & { relations?: Relation[] }).relations;
317
+ const relations = collection.relations;
318
318
  if (relations && relations.length > 0) {
319
319
  for (const relation of relations) {
320
320
  const relationName = relation.relationName;
@@ -1,7 +1,7 @@
1
1
  import { Hono } from "hono";
2
2
  import { DataDriver, Entity, EntityCollection, FetchCollectionProps, DataHooks, BackendHookContext, RestFetchService } from "@rebasepro/types";
3
3
  import { QueryOptions, HonoEnv } from "../types";
4
- import { ApiError } from "../errors";
4
+ import { ApiError, isRebaseApiError } from "../errors";
5
5
  import { parseQueryOptions } from "./query-parser";
6
6
  import { httpMethodToOperation, isOperationAllowed } from "../../auth/api-keys/api-key-permission-guard";
7
7
  import type { ApiKeyMasked } from "../../auth/api-keys/api-key-types";
@@ -234,9 +234,10 @@ export class RestApiGenerator {
234
234
 
235
235
  return c.json(response, 201);
236
236
  } catch (error) {
237
- const err = error as Error & { code?: string };
238
- err.code = err.code || "BAD_REQUEST";
239
- throw err;
237
+ if (isRebaseApiError(error) && !error.code) {
238
+ error.code = "BAD_REQUEST";
239
+ }
240
+ throw error;
240
241
  }
241
242
  });
242
243
 
@@ -282,9 +283,10 @@ export class RestApiGenerator {
282
283
 
283
284
  return c.json(response);
284
285
  } catch (error) {
285
- const err = error as Error & { code?: string };
286
- err.code = err.code || "BAD_REQUEST";
287
- throw err;
286
+ if (isRebaseApiError(error) && !error.code) {
287
+ error.code = "BAD_REQUEST";
288
+ }
289
+ throw error;
288
290
  }
289
291
  });
290
292
 
@@ -386,6 +388,8 @@ entityId };
386
388
 
387
389
  this.enforceApiKeyPermission(c, c.req.param("parent"));
388
390
 
391
+ const hookCtx = this.buildHookContext(c, "GET");
392
+
389
393
  if (parsed.entityId === "count") {
390
394
  // GET /parent/:parentId/child/count — count child entities
391
395
  const queryDict = c.req.queries();
@@ -406,7 +410,12 @@ entityId };
406
410
  entityId: parsed.entityId
407
411
  });
408
412
  if (!entity) throw ApiError.notFound("Entity not found");
409
- return c.json(this.flattenEntity(entity));
413
+
414
+ const flatResult = this.flattenEntity(entity);
415
+ const hookResult = await this.applyAfterRead(c.req.param("parent"), flatResult, hookCtx);
416
+ if (!hookResult) throw ApiError.notFound("Entity not found");
417
+
418
+ return c.json(hookResult);
410
419
  } else {
411
420
  // GET /parent/:parentId/child — list entities
412
421
  const queryDict = c.req.queries();
@@ -420,13 +429,23 @@ entityId };
420
429
  order: queryOptions.orderBy?.[0]?.direction === "desc" ? "desc" : "asc",
421
430
  searchString
422
431
  });
432
+
433
+ let flatEntities = entities.map(e => this.flattenEntity(e));
434
+ flatEntities = await this.applyAfterReadBatch(c.req.param("parent"), flatEntities, hookCtx);
435
+
436
+ const total = driver.countEntities ? await driver.countEntities({
437
+ path: parsed.collectionPath,
438
+ filter: queryOptions.where as FetchCollectionProps["filter"],
439
+ searchString
440
+ }) : flatEntities.length;
441
+
423
442
  return c.json({
424
- data: entities.map(e => this.flattenEntity(e)),
443
+ data: flatEntities,
425
444
  meta: {
426
- total: entities.length,
445
+ total,
427
446
  limit: queryOptions.limit,
428
447
  offset: queryOptions.offset,
429
- hasMore: false
448
+ hasMore: (queryOptions.offset || 0) + flatEntities.length < total
430
449
  }
431
450
  });
432
451
  }
@@ -441,9 +460,14 @@ entityId };
441
460
  if (!parsed || parsed.entityId) return next();
442
461
 
443
462
  const driver = c.get("driver") || this.driver;
463
+ const hookCtx = this.buildHookContext(c, "POST");
444
464
 
445
465
  this.enforceApiKeyPermission(c, c.req.param("parent"));
446
- const body = await c.req.json().catch(() => ({}));
466
+ let body = await c.req.json().catch(() => ({}));
467
+
468
+ if (this.dataHooks?.beforeSave) {
469
+ body = await this.dataHooks.beforeSave(parsed.collectionPath, body, undefined, hookCtx);
470
+ }
447
471
 
448
472
  const entity = await driver.saveEntity({
449
473
  path: parsed.collectionPath,
@@ -451,7 +475,15 @@ entityId };
451
475
  status: "new"
452
476
  });
453
477
 
454
- return c.json(this.formatResponse(entity), 201);
478
+ const response = this.formatResponse(entity);
479
+
480
+ if (this.dataHooks?.afterSave) {
481
+ Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
482
+ console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
483
+ });
484
+ }
485
+
486
+ return c.json(response, 201);
455
487
  });
456
488
 
457
489
  // PUT /<subcollection-path>/:id — update entity
@@ -463,10 +495,15 @@ entityId };
463
495
  if (!parsed || !parsed.entityId) return next();
464
496
 
465
497
  const driver = c.get("driver") || this.driver;
498
+ const hookCtx = this.buildHookContext(c, "PUT");
466
499
 
467
500
  this.enforceApiKeyPermission(c, c.req.param("parent"));
468
501
 
469
- const body = await c.req.json().catch(() => ({}));
502
+ let body = await c.req.json().catch(() => ({}));
503
+
504
+ if (this.dataHooks?.beforeSave) {
505
+ body = await this.dataHooks.beforeSave(parsed.collectionPath, body, parsed.entityId, hookCtx);
506
+ }
470
507
 
471
508
  const entity = await driver.saveEntity({
472
509
  path: parsed.collectionPath,
@@ -475,7 +512,15 @@ entityId };
475
512
  status: "existing"
476
513
  });
477
514
 
478
- return c.json(this.formatResponse(entity));
515
+ const response = this.formatResponse(entity);
516
+
517
+ if (this.dataHooks?.afterSave) {
518
+ Promise.resolve(this.dataHooks.afterSave(parsed.collectionPath, response as Record<string, unknown>, hookCtx)).catch(err => {
519
+ console.error("[BackendHooks] data.afterSave error:", err instanceof Error ? err.message : err);
520
+ });
521
+ }
522
+
523
+ return c.json(response);
479
524
  });
480
525
 
481
526
  // DELETE /<subcollection-path>/:id — delete entity
@@ -487,6 +532,7 @@ entityId };
487
532
  if (!parsed || !parsed.entityId) return next();
488
533
 
489
534
  const driver = c.get("driver") || this.driver;
535
+ const hookCtx = this.buildHookContext(c, "DELETE");
490
536
 
491
537
  this.enforceApiKeyPermission(c, c.req.param("parent"));
492
538
 
@@ -497,8 +543,18 @@ entityId };
497
543
 
498
544
  if (!existingEntity) throw ApiError.notFound("Entity not found");
499
545
 
546
+ if (this.dataHooks?.beforeDelete) {
547
+ await this.dataHooks.beforeDelete(parsed.collectionPath, parsed.entityId, hookCtx);
548
+ }
549
+
500
550
  await driver.deleteEntity({ entity: existingEntity });
501
551
 
552
+ if (this.dataHooks?.afterDelete) {
553
+ Promise.resolve(this.dataHooks.afterDelete(parsed.collectionPath, parsed.entityId, hookCtx)).catch(err => {
554
+ console.error("[BackendHooks] data.afterDelete error:", err instanceof Error ? err.message : err);
555
+ });
556
+ }
557
+
502
558
  return new Response(null, { status: 204 });
503
559
  });
504
560
  }
package/src/api/server.ts CHANGED
@@ -120,7 +120,7 @@ export class RebaseApiServer {
120
120
  singularName: col.singularName,
121
121
  description: col.description,
122
122
  properties: Object.keys(col.properties),
123
- relations: (col as EntityCollection & { relations?: Relation[] }).relations?.map((r: Relation) => ({
123
+ relations: col.relations?.map((r: Relation) => ({
124
124
  relationName: r.relationName,
125
125
  target: typeof r.target === "function" ? r.target().slug : r.target,
126
126
  cardinality: r.cardinality,
@@ -119,12 +119,6 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
119
119
  return null;
120
120
  }
121
121
 
122
- // The decoded JWT may contain additional claims beyond the typed payload
123
- const extendedPayload = payload as AccessTokenPayload & {
124
- email?: string;
125
- displayName?: string;
126
- };
127
-
128
122
  // Resolve roles from the repository
129
123
  let roles: string[] = payload.roles || [];
130
124
  try {
@@ -137,8 +131,8 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
137
131
 
138
132
  return {
139
133
  uid: payload.userId,
140
- email: extendedPayload.email ?? "",
141
- displayName: extendedPayload.displayName ?? null,
134
+ email: payload.email ?? "",
135
+ displayName: payload.displayName ?? null,
142
136
  roles,
143
137
  isAdmin,
144
138
  rawToken: token,
@@ -163,11 +157,6 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
163
157
  return null;
164
158
  }
165
159
 
166
- const extendedPayload = payload as AccessTokenPayload & {
167
- email?: string;
168
- displayName?: string;
169
- };
170
-
171
160
  let roles: string[] = payload.roles || [];
172
161
  try {
173
162
  roles = await authRepository.getUserRoleIds(payload.userId);
@@ -179,8 +168,8 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
179
168
 
180
169
  return {
181
170
  uid: payload.userId,
182
- email: extendedPayload.email ?? "",
183
- displayName: extendedPayload.displayName ?? null,
171
+ email: payload.email ?? "",
172
+ displayName: payload.displayName ?? null,
184
173
  roles,
185
174
  isAdmin,
186
175
  rawToken: token,
package/src/auth/jwt.ts CHANGED
@@ -13,6 +13,16 @@ export interface AccessTokenPayload {
13
13
  uid?: string;
14
14
  /** Authentication Assurance Level: aal1 = password/oauth, aal2 = MFA verified */
15
15
  aal?: "aal1" | "aal2";
16
+ /** Email claim from the JWT, if present */
17
+ email?: string;
18
+ /** Display name claim from the JWT, if present */
19
+ displayName?: string;
20
+ /** Photo URL claim from the JWT, if present */
21
+ photoURL?: string;
22
+ /** Whether MFA has been verified for this session */
23
+ mfa_verified?: boolean;
24
+ /** Authentication Methods Reference — list of methods used (e.g. 'pwd', 'otp') */
25
+ amr?: string[];
16
26
  }
17
27
 
18
28
  let jwtConfig: JwtConfig = {
@@ -1171,18 +1171,14 @@ message: "Session revoked successfully" });
1171
1171
  throw ApiError.notFound("MFA factor not found");
1172
1172
  }
1173
1173
 
1174
- // Check if this is a recovery code (10 chars with hyphen) or TOTP (6 digits)
1175
- const isRecoveryCode = code.length > 6;
1176
- let isValid = false;
1174
+ // Try TOTP verification first (standard 6-digit codes)
1175
+ const secretBuffer = base32Decode(factor.secretEncrypted);
1176
+ let isValid = verifyTotp(secretBuffer, code);
1177
1177
 
1178
- if (isRecoveryCode) {
1179
- // Try recovery code
1178
+ // Fall back to recovery code verification if TOTP didn't match
1179
+ if (!isValid) {
1180
1180
  const codeHash = hashRecoveryCode(code);
1181
1181
  isValid = await authRepo.useRecoveryCode(userCtx.userId, codeHash);
1182
- } else {
1183
- // Verify TOTP
1184
- const secretBuffer = base32Decode(factor.secretEncrypted);
1185
- isValid = verifyTotp(secretBuffer, code);
1186
1182
  }
1187
1183
 
1188
1184
  if (!isValid) {
package/src/init.ts CHANGED
@@ -395,7 +395,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
395
395
 
396
396
  // 1. Initialize all drivers
397
397
  for (const bootstrapper of bootstrappers) {
398
- const b = bootstrapper as BackendBootstrapper & { id?: string; isDefault?: boolean };
398
+ const b = bootstrapper;
399
399
  logger.info("Running bootstrapper for driver", { driverId: b.id || bootstrapper.type });
400
400
  if (b.isDefault) {
401
401
  defaultDriverId = b.id || bootstrapper.type;
@@ -413,7 +413,9 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
413
413
 
414
414
  if (bootstrapper.initializeRealtime) {
415
415
  const realtime = await bootstrapper.initializeRealtime({}, driverResult);
416
- realtimeServices[b.id || bootstrapper.type] = realtime as RealtimeProvider;
416
+ if (realtime) {
417
+ realtimeServices[b.id || bootstrapper.type] = realtime;
418
+ }
417
419
  }
418
420
  }
419
421
 
@@ -424,9 +426,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
424
426
  if (!defaultDriver || !defaultDriverResult) {
425
427
  throw new Error("Default driver not initialized by bootstrappers");
426
428
  }
427
- const defaultBootstrapper = bootstrappers.find(b => (b as BackendBootstrapper & {
428
- id?: string
429
- }).id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
429
+ const defaultBootstrapper = bootstrappers.find(b => b.id === defaultDriverId || b.type === defaultDriverId) || bootstrappers[0];
430
430
  const defaultRealtimeService = defaultDriverResult.realtimeProvider;
431
431
 
432
432
  // 2. Initialize Auth & History via the default driver's bootstrapper
@@ -721,7 +721,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
721
721
  // maxFileSize (default 50MB), which overrides the global API body limit.
722
722
  const storageMaxSize = (
723
723
  config.storage && typeof config.storage === "object" && "type" in config.storage
724
- ? (config.storage as BackendStorageConfig & { maxFileSize?: number }).maxFileSize
724
+ ? (config.storage as BackendStorageConfig).maxFileSize
725
725
  : undefined
726
726
  ) ?? 50 * 1024 * 1024;
727
727
 
@@ -998,12 +998,8 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
998
998
  }
999
999
  }
1000
1000
 
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);
1001
+ if (defaultBootstrapper.initializeWebsockets && defaultRealtimeService) {
1002
+ await defaultBootstrapper.initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
1007
1003
  }
1008
1004
 
1009
1005
  logger.info("Rebase Backend Initialized");
@@ -1061,15 +1057,11 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
1061
1057
  // timer callbacks don't fire against a closed pool.
1062
1058
  for (const [key, rt] of Object.entries(realtimeServices)) {
1063
1059
  try {
1064
- const rtWithLifecycle = rt as RealtimeProvider & {
1065
- destroy?: () => Promise<void>;
1066
- stopListening?: () => Promise<void>
1067
- };
1068
- if (typeof rtWithLifecycle.destroy === "function") {
1069
- await rtWithLifecycle.destroy();
1060
+ if (typeof rt.destroy === "function") {
1061
+ await rt.destroy();
1070
1062
  logger.info(`Realtime service "${key}" destroyed`);
1071
- } else if (typeof rtWithLifecycle.stopListening === "function") {
1072
- await rtWithLifecycle.stopListening();
1063
+ } else if (typeof rt.stopListening === "function") {
1064
+ await rt.stopListening();
1073
1065
  logger.info(`Realtime service "${key}" LISTEN client stopped`);
1074
1066
  }
1075
1067
  } catch (err) {
package/test.ts DELETED
@@ -1,6 +0,0 @@
1
- interface I {
2
- verify: (payload: any) => void;
3
- }
4
- const obj: I = {
5
- verify: (payload: { code: string }) => {}
6
- };