@rebasepro/server-postgresql 0.2.3 → 0.3.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 (50) hide show
  1. package/dist/common/src/collections/default-collections.d.ts +12 -0
  2. package/dist/common/src/collections/index.d.ts +1 -0
  3. package/dist/common/src/util/permissions.d.ts +1 -0
  4. package/dist/index.es.js +844 -160
  5. package/dist/index.es.js.map +1 -1
  6. package/dist/index.umd.js +842 -158
  7. package/dist/index.umd.js.map +1 -1
  8. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +1 -1
  9. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +1 -0
  10. package/dist/server-postgresql/src/auth/services.d.ts +43 -1
  11. package/dist/server-postgresql/src/connection.d.ts +25 -0
  12. package/dist/server-postgresql/src/schema/auth-schema.d.ts +2382 -35
  13. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +4 -0
  14. package/dist/server-postgresql/src/services/entityService.d.ts +2 -0
  15. package/dist/server-postgresql/src/services/realtimeService.d.ts +20 -0
  16. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +18 -0
  17. package/dist/types/src/controllers/auth.d.ts +2 -24
  18. package/dist/types/src/controllers/client.d.ts +0 -3
  19. package/dist/types/src/controllers/collection_registry.d.ts +1 -1
  20. package/dist/types/src/controllers/data_driver.d.ts +18 -0
  21. package/dist/types/src/controllers/registry.d.ts +5 -4
  22. package/dist/types/src/rebase_context.d.ts +1 -1
  23. package/dist/types/src/types/auth_adapter.d.ts +2 -4
  24. package/dist/types/src/types/collections.d.ts +0 -4
  25. package/dist/types/src/types/component_ref.d.ts +1 -1
  26. package/dist/types/src/types/cron.d.ts +1 -1
  27. package/dist/types/src/types/entity_views.d.ts +1 -0
  28. package/dist/types/src/types/export_import.d.ts +1 -1
  29. package/dist/types/src/types/formex.d.ts +2 -2
  30. package/dist/types/src/types/properties.d.ts +2 -2
  31. package/dist/types/src/types/translations.d.ts +28 -12
  32. package/dist/types/src/types/user_management_delegate.d.ts +6 -4
  33. package/dist/types/src/users/roles.d.ts +0 -8
  34. package/package.json +6 -6
  35. package/src/PostgresBackendDriver.ts +4 -2
  36. package/src/PostgresBootstrapper.ts +27 -8
  37. package/src/auth/ensure-tables.ts +79 -17
  38. package/src/auth/services.ts +292 -23
  39. package/src/connection.ts +77 -0
  40. package/src/data-transformer.ts +2 -2
  41. package/src/schema/auth-schema.ts +80 -14
  42. package/src/schema/generate-drizzle-schema.ts +6 -6
  43. package/src/services/EntityFetchService.ts +69 -10
  44. package/src/services/entityService.ts +2 -0
  45. package/src/services/realtimeService.ts +214 -2
  46. package/src/utils/drizzle-conditions.ts +74 -2
  47. package/src/websocket.ts +10 -2
  48. package/test/auth-services.test.ts +15 -28
  49. package/test/drizzle-conditions.test.ts +168 -0
  50. package/vite.config.ts +1 -1
package/dist/index.es.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Pool, Client } from "pg";
2
2
  import { drizzle } from "drizzle-orm/node-postgres";
3
3
  import { sql, inArray, eq, and, or, ilike, asc, desc, gt, lt, getTableName as getTableName$1, count, relations, isTable } from "drizzle-orm";
4
- import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, varchar, boolean, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
4
+ import { PgVarchar, PgText, PgChar, pgSchema, pgTable, timestamp, jsonb, boolean, varchar, uuid, primaryKey, unique, getTableConfig } from "drizzle-orm/pg-core";
5
5
  import { createHash, randomUUID } from "crypto";
6
6
  import * as fs from "fs";
7
7
  import { promises } from "fs";
@@ -11,7 +11,7 @@ import chokidar from "chokidar";
11
11
  import { WebSocket, WebSocketServer } from "ws";
12
12
  import { EventEmitter } from "events";
13
13
  import { inspect } from "util";
14
- import { extractUserFromToken, createEmailService } from "@rebasepro/server-core";
14
+ import { extractUserFromToken, logger, createEmailService } from "@rebasepro/server-core";
15
15
  const DEFAULT_POOL = {
16
16
  max: 20,
17
17
  idleTimeoutMillis: 3e4,
@@ -51,6 +51,70 @@ function createPostgresDatabaseConnection(connectionString, schema, poolConfig)
51
51
  connectionString
52
52
  };
53
53
  }
54
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
55
+ const opts = {
56
+ ...DEFAULT_POOL,
57
+ max: 5,
58
+ ...poolConfig
59
+ };
60
+ const pgPoolConfig = {
61
+ connectionString,
62
+ max: opts.max,
63
+ idleTimeoutMillis: opts.idleTimeoutMillis,
64
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
65
+ query_timeout: opts.queryTimeout,
66
+ statement_timeout: opts.statementTimeout,
67
+ keepAlive: opts.keepAlive,
68
+ keepAliveInitialDelayMillis: 0
69
+ };
70
+ const pool = new Pool(pgPoolConfig);
71
+ pool.on("error", (err) => {
72
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
73
+ });
74
+ const db = schema ? drizzle(pool, {
75
+ schema
76
+ }) : drizzle(pool);
77
+ return {
78
+ db,
79
+ pool,
80
+ connectionString
81
+ };
82
+ }
83
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
84
+ const opts = {
85
+ ...DEFAULT_POOL,
86
+ max: 10,
87
+ ...poolConfig
88
+ };
89
+ const pgPoolConfig = {
90
+ connectionString,
91
+ max: opts.max,
92
+ idleTimeoutMillis: opts.idleTimeoutMillis,
93
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
94
+ query_timeout: opts.queryTimeout,
95
+ statement_timeout: opts.statementTimeout,
96
+ keepAlive: opts.keepAlive,
97
+ keepAliveInitialDelayMillis: 0
98
+ };
99
+ const pool = new Pool(pgPoolConfig);
100
+ pool.on("error", (err) => {
101
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
102
+ });
103
+ const db = schema ? drizzle(pool, {
104
+ schema
105
+ }) : drizzle(pool);
106
+ return {
107
+ db,
108
+ pool,
109
+ connectionString
110
+ };
111
+ }
112
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
113
+ __proto__: null,
114
+ createDirectDatabaseConnection,
115
+ createPostgresDatabaseConnection,
116
+ createReadReplicaConnection
117
+ }, Symbol.toStringTag, { value: "Module" }));
54
118
  class Vector {
55
119
  value;
56
120
  constructor(value) {
@@ -970,6 +1034,9 @@ function mergeDeep(target, source, ignoreUndefined = false) {
970
1034
  return output;
971
1035
  }
972
1036
  for (const key in source) {
1037
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1038
+ continue;
1039
+ }
973
1040
  if (Object.prototype.hasOwnProperty.call(source, key)) {
974
1041
  const sourceValue = source[key];
975
1042
  const outputValue = output[key];
@@ -2644,6 +2711,90 @@ class CollectionRegistry {
2644
2711
  };
2645
2712
  }
2646
2713
  }
2714
+ const defaultUsersCollection = {
2715
+ name: "Users",
2716
+ singularName: "User",
2717
+ slug: "users",
2718
+ table: "users",
2719
+ schema: "rebase",
2720
+ icon: "Users",
2721
+ group: "Settings",
2722
+ properties: {
2723
+ id: {
2724
+ name: "ID",
2725
+ type: "string",
2726
+ isId: "uuid"
2727
+ },
2728
+ email: {
2729
+ name: "Email",
2730
+ type: "string",
2731
+ validation: {
2732
+ required: true,
2733
+ unique: true
2734
+ }
2735
+ },
2736
+ password_hash: {
2737
+ name: "Password Hash",
2738
+ type: "string",
2739
+ ui: {
2740
+ hideFromCollection: true
2741
+ }
2742
+ },
2743
+ display_name: {
2744
+ name: "Display Name",
2745
+ type: "string"
2746
+ },
2747
+ photo_url: {
2748
+ name: "Photo URL",
2749
+ type: "string"
2750
+ },
2751
+ email_verified: {
2752
+ name: "Email Verified",
2753
+ type: "boolean",
2754
+ defaultValue: false
2755
+ },
2756
+ email_verification_token: {
2757
+ name: "Email Verification Token",
2758
+ type: "string",
2759
+ ui: {
2760
+ hideFromCollection: true
2761
+ }
2762
+ },
2763
+ email_verification_sent_at: {
2764
+ name: "Email Verification Sent At",
2765
+ type: "date",
2766
+ ui: {
2767
+ hideFromCollection: true
2768
+ }
2769
+ },
2770
+ metadata: {
2771
+ name: "Metadata",
2772
+ type: "map",
2773
+ defaultValue: {},
2774
+ ui: {
2775
+ hideFromCollection: true
2776
+ }
2777
+ },
2778
+ created_at: {
2779
+ name: "Created At",
2780
+ type: "date",
2781
+ autoValue: "on_create",
2782
+ ui: {
2783
+ readOnly: true,
2784
+ hideFromCollection: true
2785
+ }
2786
+ },
2787
+ updated_at: {
2788
+ name: "Updated At",
2789
+ type: "date",
2790
+ autoValue: "on_update",
2791
+ ui: {
2792
+ readOnly: true,
2793
+ hideFromCollection: true
2794
+ }
2795
+ }
2796
+ }
2797
+ };
2647
2798
  function mapOperator(op) {
2648
2799
  switch (op) {
2649
2800
  case "==":
@@ -2984,7 +3135,13 @@ class DrizzleConditionBuilder {
2984
3135
  for (const [field, filterParam] of Object.entries(filter)) {
2985
3136
  if (!filterParam) continue;
2986
3137
  const [op, value] = filterParam;
2987
- const fieldColumn = table[field];
3138
+ let fieldColumn = table[field];
3139
+ if (!fieldColumn) {
3140
+ const relationKey = `${field}_id`;
3141
+ if (relationKey in table) {
3142
+ fieldColumn = table[relationKey];
3143
+ }
3144
+ }
2988
3145
  if (!fieldColumn) {
2989
3146
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2990
3147
  continue;
@@ -3026,6 +3183,17 @@ class DrizzleConditionBuilder {
3026
3183
  return null;
3027
3184
  case "array-contains":
3028
3185
  return sql`${column} @> ${JSON.stringify([value])}`;
3186
+ case "array-contains-any":
3187
+ if (Array.isArray(value) && value.length > 0) {
3188
+ const textValues = value.map((v) => String(v));
3189
+ return sql`${column} ?| array[${sql.join(textValues.map((v) => sql`${v}`), sql`, `)}]`;
3190
+ }
3191
+ return sql`${column} @> ${JSON.stringify([value])}`;
3192
+ case "not-in":
3193
+ if (Array.isArray(value) && value.length > 0) {
3194
+ return sql`${column} NOT IN (${sql.join(value.map((v) => sql`${v}`), sql`, `)})`;
3195
+ }
3196
+ return null;
3029
3197
  default:
3030
3198
  console.warn(`Unsupported filter operation: ${op}`);
3031
3199
  return null;
@@ -3541,6 +3709,40 @@ class DrizzleConditionBuilder {
3541
3709
  return null;
3542
3710
  }
3543
3711
  }
3712
+ /**
3713
+ * Build vector similarity search expressions for pgvector.
3714
+ *
3715
+ * Returns:
3716
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3717
+ * - `filter`: optional WHERE clause for distance threshold
3718
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3719
+ */
3720
+ static buildVectorSearchConditions(table, vectorSearch) {
3721
+ const column = table[vectorSearch.property];
3722
+ if (!column) {
3723
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3724
+ }
3725
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3726
+ const distanceFn = vectorSearch.distance || "cosine";
3727
+ let operator;
3728
+ switch (distanceFn) {
3729
+ case "cosine":
3730
+ operator = "<=>";
3731
+ break;
3732
+ case "l2":
3733
+ operator = "<->";
3734
+ break;
3735
+ case "inner_product":
3736
+ operator = "<#>";
3737
+ break;
3738
+ }
3739
+ const distanceExpr = sql`${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}`;
3740
+ return {
3741
+ orderBy: distanceExpr,
3742
+ filter: vectorSearch.threshold != null ? sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3743
+ distanceSelect: sql`(${column} ${sql.raw(operator)} ${sql.raw(vectorLiteral)})`
3744
+ };
3745
+ }
3544
3746
  }
3545
3747
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3546
3748
  function getColumnMeta(col) {
@@ -5486,7 +5688,7 @@ class EntityFetchService {
5486
5688
  const qb = this.getQueryBuilder(tableName);
5487
5689
  const withConfig = this.buildWithConfig(collection);
5488
5690
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5489
- if (qb && !options.searchString && !hasRelations) {
5691
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5490
5692
  try {
5491
5693
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5492
5694
  const results2 = await qb.findMany(queryOpts);
@@ -5500,7 +5702,14 @@ class EntityFetchService {
5500
5702
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5501
5703
  }
5502
5704
  }
5503
- let query = this.db.select().from(table).$dynamic();
5705
+ let vectorMeta;
5706
+ if (options.vectorSearch) {
5707
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5708
+ }
5709
+ let query = vectorMeta ? this.db.select({
5710
+ table_row: table,
5711
+ _distance: vectorMeta.distanceSelect
5712
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5504
5713
  const allConditions = [];
5505
5714
  if (options.searchString) {
5506
5715
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5511,12 +5720,17 @@ class EntityFetchService {
5511
5720
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5512
5721
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5513
5722
  }
5723
+ if (vectorMeta?.filter) {
5724
+ allConditions.push(vectorMeta.filter);
5725
+ }
5514
5726
  if (allConditions.length > 0) {
5515
5727
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5516
5728
  if (finalCondition) query = query.where(finalCondition);
5517
5729
  }
5518
5730
  const orderExpressions = [];
5519
- if (options.orderBy) {
5731
+ if (vectorMeta) {
5732
+ orderExpressions.push(asc(vectorMeta.orderBy));
5733
+ } else if (options.orderBy) {
5520
5734
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5521
5735
  if (orderByField) {
5522
5736
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5532,10 +5746,14 @@ class EntityFetchService {
5532
5746
  if (finalCondition) query = query.where(finalCondition);
5533
5747
  }
5534
5748
  }
5535
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5749
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5536
5750
  if (limitValue) query = query.limit(limitValue);
5537
5751
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5538
- const results = await query;
5752
+ const rawResults = await query;
5753
+ const results = vectorMeta ? rawResults.map((r) => ({
5754
+ ...r.table_row,
5755
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5756
+ })) : rawResults;
5539
5757
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5540
5758
  }
5541
5759
  /**
@@ -5757,7 +5975,7 @@ class EntityFetchService {
5757
5975
  const idField = table[idInfo.fieldName];
5758
5976
  const tableName = getTableName$1(table);
5759
5977
  const qb = this.getQueryBuilder(tableName);
5760
- if (qb && !options.searchString) {
5978
+ if (qb && !options.searchString && !options.vectorSearch) {
5761
5979
  try {
5762
5980
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5763
5981
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5903,7 +6121,14 @@ class EntityFetchService {
5903
6121
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5904
6122
  const idInfo = idInfoArray[0];
5905
6123
  const idField = table[idInfo.fieldName];
5906
- let query = this.db.select().from(table).$dynamic();
6124
+ let vectorMeta;
6125
+ if (options.vectorSearch) {
6126
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6127
+ }
6128
+ let query = vectorMeta ? this.db.select({
6129
+ table_row: table,
6130
+ _distance: vectorMeta.distanceSelect
6131
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5907
6132
  const allConditions = [];
5908
6133
  if (options.searchString) {
5909
6134
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5914,12 +6139,17 @@ class EntityFetchService {
5914
6139
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5915
6140
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5916
6141
  }
6142
+ if (vectorMeta?.filter) {
6143
+ allConditions.push(vectorMeta.filter);
6144
+ }
5917
6145
  if (allConditions.length > 0) {
5918
6146
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5919
6147
  if (finalCondition) query = query.where(finalCondition);
5920
6148
  }
5921
6149
  const orderExpressions = [];
5922
- if (options.orderBy) {
6150
+ if (vectorMeta) {
6151
+ orderExpressions.push(asc(vectorMeta.orderBy));
6152
+ } else if (options.orderBy) {
5923
6153
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5924
6154
  if (orderByField) {
5925
6155
  orderExpressions.push(options.order === "asc" ? asc(orderByField) : desc(orderByField));
@@ -5927,10 +6157,17 @@ class EntityFetchService {
5927
6157
  }
5928
6158
  orderExpressions.push(desc(idField));
5929
6159
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5930
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6160
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5931
6161
  if (limitValue) query = query.limit(limitValue);
5932
6162
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5933
- return await query;
6163
+ const rawResults = await query;
6164
+ if (vectorMeta) {
6165
+ return rawResults.map((r) => ({
6166
+ ...r.table_row,
6167
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6168
+ }));
6169
+ }
6170
+ return rawResults;
5934
6171
  }
5935
6172
  /**
5936
6173
  * Check if the Drizzle instance has the relational query API available
@@ -6684,7 +6921,8 @@ class PostgresBackendDriver {
6684
6921
  startAfter,
6685
6922
  orderBy,
6686
6923
  searchString,
6687
- order
6924
+ order,
6925
+ vectorSearch
6688
6926
  }) {
6689
6927
  const entities = await this.entityService.fetchCollection(path2, {
6690
6928
  filter,
@@ -6694,7 +6932,8 @@ class PostgresBackendDriver {
6694
6932
  offset,
6695
6933
  startAfter,
6696
6934
  databaseId: collection?.databaseId,
6697
- searchString
6935
+ searchString,
6936
+ vectorSearch
6698
6937
  });
6699
6938
  const {
6700
6939
  collection: resolvedCollection,
@@ -7537,6 +7776,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7537
7776
  length: 255
7538
7777
  }),
7539
7778
  emailVerificationSentAt: timestamp("email_verification_sent_at"),
7779
+ isAnonymous: boolean("is_anonymous").default(false).notNull(),
7540
7780
  metadata: jsonb("metadata").$type().default({}).notNull(),
7541
7781
  createdAt: timestamp("created_at").defaultNow().notNull(),
7542
7782
  updatedAt: timestamp("updated_at").defaultNow().notNull()
@@ -7551,8 +7791,7 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7551
7791
  }).notNull(),
7552
7792
  isAdmin: boolean("is_admin").default(false).notNull(),
7553
7793
  defaultPermissions: jsonb("default_permissions").$type(),
7554
- collectionPermissions: jsonb("collection_permissions").$type(),
7555
- config: jsonb("config").$type()
7794
+ collectionPermissions: jsonb("collection_permissions").$type()
7556
7795
  });
7557
7796
  const userRoles2 = rolesTableCreator("user_roles", {
7558
7797
  userId: uuid("user_id").notNull().references(() => users2.id, {
@@ -7624,6 +7863,48 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7624
7863
  }, (table) => ({
7625
7864
  uniqueProviderId: unique("unique_provider_id").on(table.provider, table.providerId)
7626
7865
  }));
7866
+ const mfaFactors2 = rolesTableCreator("mfa_factors", {
7867
+ id: uuid("id").defaultRandom().primaryKey(),
7868
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7869
+ onDelete: "cascade"
7870
+ }),
7871
+ factorType: varchar("factor_type", {
7872
+ length: 20
7873
+ }).notNull(),
7874
+ // 'totp'
7875
+ secretEncrypted: varchar("secret_encrypted", {
7876
+ length: 500
7877
+ }).notNull(),
7878
+ friendlyName: varchar("friendly_name", {
7879
+ length: 255
7880
+ }),
7881
+ verified: boolean("verified").default(false).notNull(),
7882
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7883
+ updatedAt: timestamp("updated_at").defaultNow().notNull()
7884
+ });
7885
+ const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
7886
+ id: uuid("id").defaultRandom().primaryKey(),
7887
+ factorId: uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7888
+ onDelete: "cascade"
7889
+ }),
7890
+ createdAt: timestamp("created_at").defaultNow().notNull(),
7891
+ verifiedAt: timestamp("verified_at"),
7892
+ ipAddress: varchar("ip_address", {
7893
+ length: 45
7894
+ }),
7895
+ expiresAt: timestamp("expires_at").notNull()
7896
+ });
7897
+ const recoveryCodes2 = rolesTableCreator("recovery_codes", {
7898
+ id: uuid("id").defaultRandom().primaryKey(),
7899
+ userId: uuid("user_id").notNull().references(() => users2.id, {
7900
+ onDelete: "cascade"
7901
+ }),
7902
+ codeHash: varchar("code_hash", {
7903
+ length: 255
7904
+ }).notNull(),
7905
+ usedAt: timestamp("used_at"),
7906
+ createdAt: timestamp("created_at").defaultNow().notNull()
7907
+ });
7627
7908
  return {
7628
7909
  rolesSchema,
7629
7910
  usersSchema: usersSchema2,
@@ -7633,7 +7914,10 @@ function createAuthSchema(rolesSchemaName = "rebase", usersSchemaName = "rebase"
7633
7914
  refreshTokens: refreshTokens2,
7634
7915
  passwordResetTokens: passwordResetTokens2,
7635
7916
  appConfig: appConfig2,
7636
- userIdentities: userIdentities2
7917
+ userIdentities: userIdentities2,
7918
+ mfaFactors: mfaFactors2,
7919
+ mfaChallenges: mfaChallenges2,
7920
+ recoveryCodes: recoveryCodes2
7637
7921
  };
7638
7922
  }
7639
7923
  const defaultAuthSchema = createAuthSchema("rebase", "rebase");
@@ -7646,13 +7930,18 @@ const refreshTokens = defaultAuthSchema.refreshTokens;
7646
7930
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7647
7931
  const appConfig = defaultAuthSchema.appConfig;
7648
7932
  const userIdentities = defaultAuthSchema.userIdentities;
7933
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7934
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7935
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7649
7936
  const usersRelations = relations(users, ({
7650
7937
  many
7651
7938
  }) => ({
7652
7939
  userRoles: many(userRoles),
7653
7940
  refreshTokens: many(refreshTokens),
7654
7941
  passwordResetTokens: many(passwordResetTokens),
7655
- userIdentities: many(userIdentities)
7942
+ userIdentities: many(userIdentities),
7943
+ mfaFactors: many(mfaFactors),
7944
+ recoveryCodes: many(recoveryCodes)
7656
7945
  }));
7657
7946
  const rolesRelations = relations(roles, ({
7658
7947
  many
@@ -7695,6 +7984,32 @@ const userIdentitiesRelations = relations(userIdentities, ({
7695
7984
  references: [users.id]
7696
7985
  })
7697
7986
  }));
7987
+ const mfaFactorsRelations = relations(mfaFactors, ({
7988
+ one,
7989
+ many
7990
+ }) => ({
7991
+ user: one(users, {
7992
+ fields: [mfaFactors.userId],
7993
+ references: [users.id]
7994
+ }),
7995
+ challenges: many(mfaChallenges)
7996
+ }));
7997
+ const mfaChallengesRelations = relations(mfaChallenges, ({
7998
+ one
7999
+ }) => ({
8000
+ factor: one(mfaFactors, {
8001
+ fields: [mfaChallenges.factorId],
8002
+ references: [mfaFactors.id]
8003
+ })
8004
+ }));
8005
+ const recoveryCodesRelations = relations(recoveryCodes, ({
8006
+ one
8007
+ }) => ({
8008
+ user: one(users, {
8009
+ fields: [recoveryCodes.userId],
8010
+ references: [users.id]
8011
+ })
8012
+ }));
7698
8013
  const resolveColumnName = (propName, prop) => {
7699
8014
  if (prop && "columnName" in prop && typeof prop.columnName === "string") {
7700
8015
  return prop.columnName;
@@ -8298,90 +8613,6 @@ ${tableRelations.join(",\n")}
8298
8613
  schemaContent += tablesExport + enumsExport + relationsExport;
8299
8614
  return schemaContent;
8300
8615
  };
8301
- const defaultUsersCollection = {
8302
- name: "Users",
8303
- singularName: "User",
8304
- slug: "users",
8305
- table: "users",
8306
- schema: "rebase",
8307
- icon: "Users",
8308
- group: "Settings",
8309
- properties: {
8310
- id: {
8311
- name: "ID",
8312
- type: "string",
8313
- isId: "uuid"
8314
- },
8315
- email: {
8316
- name: "Email",
8317
- type: "string",
8318
- validation: {
8319
- required: true,
8320
- unique: true
8321
- }
8322
- },
8323
- password_hash: {
8324
- name: "Password Hash",
8325
- type: "string",
8326
- ui: {
8327
- hideFromCollection: true
8328
- }
8329
- },
8330
- display_name: {
8331
- name: "Display Name",
8332
- type: "string"
8333
- },
8334
- photo_url: {
8335
- name: "Photo URL",
8336
- type: "string"
8337
- },
8338
- email_verified: {
8339
- name: "Email Verified",
8340
- type: "boolean",
8341
- defaultValue: false
8342
- },
8343
- email_verification_token: {
8344
- name: "Email Verification Token",
8345
- type: "string",
8346
- ui: {
8347
- hideFromCollection: true
8348
- }
8349
- },
8350
- email_verification_sent_at: {
8351
- name: "Email Verification Sent At",
8352
- type: "date",
8353
- ui: {
8354
- hideFromCollection: true
8355
- }
8356
- },
8357
- metadata: {
8358
- name: "Metadata",
8359
- type: "map",
8360
- defaultValue: {},
8361
- ui: {
8362
- hideFromCollection: true
8363
- }
8364
- },
8365
- created_at: {
8366
- name: "Created At",
8367
- type: "date",
8368
- autoValue: "on_create",
8369
- ui: {
8370
- readOnly: true,
8371
- hideFromCollection: true
8372
- }
8373
- },
8374
- updated_at: {
8375
- name: "Updated At",
8376
- type: "date",
8377
- autoValue: "on_update",
8378
- ui: {
8379
- readOnly: true,
8380
- hideFromCollection: true
8381
- }
8382
- }
8383
- }
8384
- };
8385
8616
  const formatTerminalText = (text, options = {}) => {
8386
8617
  let codes = "";
8387
8618
  if (options.bold) codes += "\x1B[1m";
@@ -8447,10 +8678,7 @@ const runGeneration = async (collectionsFilePath, outputPath) => {
8447
8678
  if (!collections || !Array.isArray(collections)) {
8448
8679
  collections = [];
8449
8680
  }
8450
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8451
- if (!hasUsersCollection) {
8452
- collections.push(defaultUsersCollection);
8453
- }
8681
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8454
8682
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8455
8683
  const schemaContent = await generateSchema(collections);
8456
8684
  if (outputPath) {
@@ -8511,6 +8739,13 @@ class RealtimeService extends EventEmitter {
8511
8739
  this.entityService = new EntityService(db, registry);
8512
8740
  }
8513
8741
  clients = /* @__PURE__ */ new Map();
8742
+ // Broadcast channels: channel name → set of client IDs
8743
+ channels = /* @__PURE__ */ new Map();
8744
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8745
+ presence = /* @__PURE__ */ new Map();
8746
+ presenceInterval;
8747
+ static PRESENCE_TIMEOUT_MS = 3e4;
8748
+ // 30s
8514
8749
  entityService;
8515
8750
  // Enhanced subscriptions storage with full request parameters
8516
8751
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8637,8 +8872,19 @@ class RealtimeService extends EventEmitter {
8637
8872
  }
8638
8873
  }
8639
8874
  }
8875
+ for (const [channel, members] of this.channels.entries()) {
8876
+ if (members.has(clientId)) {
8877
+ members.delete(clientId);
8878
+ this.removePresence(clientId, channel);
8879
+ if (members.size === 0) this.channels.delete(channel);
8880
+ }
8881
+ }
8882
+ for (const [channel] of this.presence) {
8883
+ this.removePresence(clientId, channel);
8884
+ }
8640
8885
  }
8641
8886
  async handleMessage(clientId, message, authContext) {
8887
+ const payload = message.payload;
8642
8888
  switch (message.type) {
8643
8889
  case "subscribe_collection":
8644
8890
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8649,6 +8895,25 @@ class RealtimeService extends EventEmitter {
8649
8895
  case "unsubscribe":
8650
8896
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8651
8897
  break;
8898
+ case "join_channel":
8899
+ this.joinChannel(clientId, payload?.channel);
8900
+ break;
8901
+ case "leave_channel":
8902
+ this.leaveChannel(clientId, payload?.channel);
8903
+ break;
8904
+ case "broadcast":
8905
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8906
+ break;
8907
+ case "presence_track":
8908
+ this.joinChannel(clientId, payload?.channel);
8909
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8910
+ break;
8911
+ case "presence_untrack":
8912
+ this.removePresence(clientId, payload?.channel);
8913
+ break;
8914
+ case "presence_state":
8915
+ this.sendPresenceState(clientId, payload?.channel);
8916
+ break;
8652
8917
  default:
8653
8918
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8654
8919
  }
@@ -9106,6 +9371,132 @@ class RealtimeService extends EventEmitter {
9106
9371
  return parentPaths;
9107
9372
  }
9108
9373
  // =============================================================================
9374
+ // Broadcast Channels
9375
+ // =============================================================================
9376
+ /** Join a broadcast channel */
9377
+ joinChannel(clientId, channel) {
9378
+ if (!this.channels.has(channel)) {
9379
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9380
+ }
9381
+ this.channels.get(channel).add(clientId);
9382
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9383
+ }
9384
+ /** Leave a broadcast channel */
9385
+ leaveChannel(clientId, channel) {
9386
+ const members = this.channels.get(channel);
9387
+ if (members) {
9388
+ members.delete(clientId);
9389
+ if (members.size === 0) this.channels.delete(channel);
9390
+ }
9391
+ this.removePresence(clientId, channel);
9392
+ }
9393
+ /** Broadcast a message to all clients in a channel except sender */
9394
+ broadcastToChannel(clientId, channel, event, payload) {
9395
+ const members = this.channels.get(channel);
9396
+ if (!members) return;
9397
+ const message = JSON.stringify({
9398
+ type: "broadcast",
9399
+ channel,
9400
+ event,
9401
+ payload
9402
+ });
9403
+ for (const memberId of members) {
9404
+ if (memberId === clientId) continue;
9405
+ const ws = this.clients.get(memberId);
9406
+ if (ws && ws.readyState === WebSocket.OPEN) {
9407
+ ws.send(message);
9408
+ }
9409
+ }
9410
+ }
9411
+ // =============================================================================
9412
+ // Presence
9413
+ // =============================================================================
9414
+ /** Track presence in a channel */
9415
+ trackPresence(clientId, channel, state) {
9416
+ if (!this.presence.has(channel)) {
9417
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9418
+ }
9419
+ const channelPresence = this.presence.get(channel);
9420
+ channelPresence.set(clientId, {
9421
+ state,
9422
+ lastSeen: Date.now()
9423
+ });
9424
+ this.broadcastPresenceDiff(channel, {
9425
+ [clientId]: state
9426
+ }, {});
9427
+ this.ensurePresenceCleanup();
9428
+ }
9429
+ /** Remove presence from a channel */
9430
+ removePresence(clientId, channel) {
9431
+ const channelPresence = this.presence.get(channel);
9432
+ if (!channelPresence) return;
9433
+ const entry = channelPresence.get(clientId);
9434
+ if (entry) {
9435
+ channelPresence.delete(clientId);
9436
+ this.broadcastPresenceDiff(channel, {}, {
9437
+ [clientId]: entry.state
9438
+ });
9439
+ }
9440
+ if (channelPresence.size === 0) {
9441
+ this.presence.delete(channel);
9442
+ }
9443
+ }
9444
+ /** Send full presence state to a specific client */
9445
+ sendPresenceState(clientId, channel) {
9446
+ const channelPresence = this.presence.get(channel);
9447
+ const presences = {};
9448
+ if (channelPresence) {
9449
+ for (const [id, {
9450
+ state
9451
+ }] of channelPresence) {
9452
+ presences[id] = state;
9453
+ }
9454
+ }
9455
+ const ws = this.clients.get(clientId);
9456
+ if (ws && ws.readyState === WebSocket.OPEN) {
9457
+ ws.send(JSON.stringify({
9458
+ type: "presence_state",
9459
+ channel,
9460
+ presences
9461
+ }));
9462
+ }
9463
+ }
9464
+ /** Broadcast presence diff (joins/leaves) to channel */
9465
+ broadcastPresenceDiff(channel, joins, leaves) {
9466
+ const members = this.channels.get(channel);
9467
+ if (!members) return;
9468
+ const message = JSON.stringify({
9469
+ type: "presence_diff",
9470
+ channel,
9471
+ joins,
9472
+ leaves
9473
+ });
9474
+ for (const memberId of members) {
9475
+ const ws = this.clients.get(memberId);
9476
+ if (ws && ws.readyState === WebSocket.OPEN) {
9477
+ ws.send(message);
9478
+ }
9479
+ }
9480
+ }
9481
+ /** Periodic cleanup for stale presences */
9482
+ ensurePresenceCleanup() {
9483
+ if (this.presenceInterval) return;
9484
+ this.presenceInterval = setInterval(() => {
9485
+ const now = Date.now();
9486
+ for (const [channel, channelPresence] of this.presence) {
9487
+ for (const [clientId, entry] of channelPresence) {
9488
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9489
+ this.removePresence(clientId, channel);
9490
+ }
9491
+ }
9492
+ }
9493
+ if (this.presence.size === 0 && this.presenceInterval) {
9494
+ clearInterval(this.presenceInterval);
9495
+ this.presenceInterval = void 0;
9496
+ }
9497
+ }, 1e4);
9498
+ }
9499
+ // =============================================================================
9109
9500
  // Lifecycle / Cleanup
9110
9501
  // =============================================================================
9111
9502
  /**
@@ -9126,6 +9517,12 @@ class RealtimeService extends EventEmitter {
9126
9517
  }
9127
9518
  this._subscriptions.clear();
9128
9519
  this.subscriptionCallbacks.clear();
9520
+ this.channels.clear();
9521
+ this.presence.clear();
9522
+ if (this.presenceInterval) {
9523
+ clearInterval(this.presenceInterval);
9524
+ this.presenceInterval = void 0;
9525
+ }
9129
9526
  await this.stopListening();
9130
9527
  this.clients.clear();
9131
9528
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9772,8 +10169,14 @@ function createPostgresWebSocket(server, realtimeService, driver, authConfig, au
9772
10169
  break;
9773
10170
  case "subscribe_collection":
9774
10171
  case "subscribe_entity":
9775
- case "unsubscribe": {
9776
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10172
+ case "unsubscribe":
10173
+ case "join_channel":
10174
+ case "leave_channel":
10175
+ case "broadcast":
10176
+ case "presence_track":
10177
+ case "presence_untrack":
10178
+ case "presence_state": {
10179
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9777
10180
  const session = clientSessions.get(clientId);
9778
10181
  const authContext = session?.user ? {
9779
10182
  userId: session.user.userId,
@@ -9892,11 +10295,6 @@ const DEFAULT_ROLES = [{
9892
10295
  create: true,
9893
10296
  edit: true,
9894
10297
  delete: true
9895
- },
9896
- config: {
9897
- createCollections: true,
9898
- editCollections: "all",
9899
- deleteCollections: "all"
9900
10298
  }
9901
10299
  }, {
9902
10300
  id: "editor",
@@ -9907,11 +10305,6 @@ const DEFAULT_ROLES = [{
9907
10305
  create: true,
9908
10306
  edit: true,
9909
10307
  delete: true
9910
- },
9911
- config: {
9912
- createCollections: true,
9913
- editCollections: "own",
9914
- deleteCollections: "own"
9915
10308
  }
9916
10309
  }, {
9917
10310
  id: "viewer",
@@ -9922,11 +10315,10 @@ const DEFAULT_ROLES = [{
9922
10315
  create: false,
9923
10316
  edit: false,
9924
10317
  delete: false
9925
- },
9926
- config: null
10318
+ }
9927
10319
  }];
9928
10320
  async function ensureAuthTablesExist(db, registry) {
9929
- console.log("🔍 Checking auth tables...");
10321
+ logger.info("🔍 Checking auth tables...");
9930
10322
  try {
9931
10323
  let usersTableName = '"users"';
9932
10324
  let userIdType = "TEXT";
@@ -9996,7 +10388,6 @@ async function ensureAuthTablesExist(db, registry) {
9996
10388
  is_admin BOOLEAN DEFAULT FALSE,
9997
10389
  default_permissions JSONB,
9998
10390
  collection_permissions JSONB,
9999
- config JSONB,
10000
10391
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10001
10392
  )
10002
10393
  `);
@@ -10079,34 +10470,85 @@ async function ensureAuthTablesExist(db, registry) {
10079
10470
  `);
10080
10471
  });
10081
10472
  await seedDefaultRoles(db, rolesTableName);
10082
- console.log("✅ Auth tables ready");
10473
+ await db.execute(sql`
10474
+ ALTER TABLE ${sql.raw(usersTableName)}
10475
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10476
+ `);
10477
+ const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
10478
+ const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
10479
+ const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
10480
+ await db.execute(sql`
10481
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaFactorsTableName)} (
10482
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10483
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10484
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10485
+ secret_encrypted TEXT NOT NULL,
10486
+ friendly_name TEXT,
10487
+ verified BOOLEAN DEFAULT FALSE,
10488
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10489
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10490
+ )
10491
+ `);
10492
+ await db.execute(sql`
10493
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10494
+ ON ${sql.raw(mfaFactorsTableName)}(user_id)
10495
+ `);
10496
+ await db.execute(sql`
10497
+ CREATE TABLE IF NOT EXISTS ${sql.raw(mfaChallengesTableName)} (
10498
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10499
+ factor_id TEXT NOT NULL REFERENCES ${sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10500
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10501
+ verified_at TIMESTAMP WITH TIME ZONE,
10502
+ ip_address TEXT,
10503
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10504
+ )
10505
+ `);
10506
+ await db.execute(sql`
10507
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10508
+ ON ${sql.raw(mfaChallengesTableName)}(factor_id)
10509
+ `);
10510
+ await db.execute(sql`
10511
+ CREATE TABLE IF NOT EXISTS ${sql.raw(recoveryCodesTableName)} (
10512
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10513
+ user_id ${sql.raw(userIdType)} NOT NULL REFERENCES ${sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10514
+ code_hash TEXT NOT NULL,
10515
+ used_at TIMESTAMP WITH TIME ZONE,
10516
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10517
+ )
10518
+ `);
10519
+ await db.execute(sql`
10520
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10521
+ ON ${sql.raw(recoveryCodesTableName)}(user_id)
10522
+ `);
10523
+ logger.info("✅ Auth tables ready");
10083
10524
  } catch (error) {
10084
- console.error("❌ Failed to create auth tables:", error);
10085
- console.warn("⚠️ Continuing without creating auth tables.");
10525
+ logger.error("❌ Failed to create auth tables", {
10526
+ error
10527
+ });
10528
+ logger.warn("⚠️ Continuing without creating auth tables.");
10086
10529
  }
10087
10530
  }
10088
10531
  async function seedDefaultRoles(db, rolesTableName) {
10089
10532
  const result = await db.execute(sql`SELECT COUNT(*) as count FROM ${sql.raw(rolesTableName)}`);
10090
10533
  const count2 = parseInt(result.rows[0]?.count || "0", 10);
10091
10534
  if (count2 > 0) {
10092
- console.log(`📋 Found ${count2} existing roles`);
10535
+ logger.info(`📋 Found ${count2} existing roles`);
10093
10536
  return;
10094
10537
  }
10095
- console.log("🌱 Seeding default roles...");
10538
+ logger.info("🌱 Seeding default roles...");
10096
10539
  for (const role of DEFAULT_ROLES) {
10097
10540
  await db.execute(sql`
10098
- INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10541
+ INSERT INTO ${sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
10099
10542
  VALUES (
10100
10543
  ${role.id},
10101
10544
  ${role.name},
10102
10545
  ${role.is_admin},
10103
- ${JSON.stringify(role.default_permissions)}::jsonb,
10104
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10546
+ ${JSON.stringify(role.default_permissions)}::jsonb
10105
10547
  )
10106
10548
  ON CONFLICT (id) DO NOTHING
10107
10549
  `);
10108
10550
  }
10109
- console.log("✅ Default roles created: admin, editor, viewer");
10551
+ logger.info("✅ Default roles created: admin, editor, viewer");
10110
10552
  }
10111
10553
  function getColumnKey(table, ...keys2) {
10112
10554
  if (!table) return void 0;
@@ -10160,12 +10602,13 @@ class UserService {
10160
10602
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10161
10603
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10162
10604
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10605
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10163
10606
  const createdAt = row.created_at ?? row.createdAt;
10164
10607
  const updatedAt = row.updated_at ?? row.updatedAt;
10165
10608
  const metadata = {
10166
10609
  ...row.metadata || {}
10167
10610
  };
10168
- const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
10611
+ const knownKeys = /* @__PURE__ */ new Set(["id", "uid", "email", "password_hash", "passwordHash", "display_name", "displayName", "photo_url", "photoUrl", "photoURL", "email_verified", "emailVerified", "email_verification_token", "emailVerificationToken", "email_verification_sent_at", "emailVerificationSentAt", "is_anonymous", "isAnonymous", "created_at", "createdAt", "updated_at", "updatedAt", "metadata"]);
10169
10612
  for (const [key, val] of Object.entries(row)) {
10170
10613
  if (!knownKeys.has(key)) {
10171
10614
  const camelKey = camelCase(key);
@@ -10181,6 +10624,7 @@ class UserService {
10181
10624
  emailVerified,
10182
10625
  emailVerificationToken,
10183
10626
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10627
+ isAnonymous,
10184
10628
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10185
10629
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10186
10630
  metadata
@@ -10197,6 +10641,7 @@ class UserService {
10197
10641
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10198
10642
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10199
10643
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10644
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10200
10645
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10201
10646
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10202
10647
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10208,6 +10653,7 @@ class UserService {
10208
10653
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10209
10654
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10210
10655
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10656
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10211
10657
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10212
10658
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10213
10659
  const metadata = {
@@ -10216,7 +10662,7 @@ class UserService {
10216
10662
  const remainingMetadata = {};
10217
10663
  for (const [key, val] of Object.entries(metadata)) {
10218
10664
  const tableColKey = getColumnKey(this.usersTable, key);
10219
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10665
+ if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== isAnonymousKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10220
10666
  payload[tableColKey] = val;
10221
10667
  } else {
10222
10668
  remainingMetadata[key] = val;
@@ -10404,7 +10850,7 @@ class UserService {
10404
10850
  async getUserRoles(userId) {
10405
10851
  const rolesSchema = getTableConfig(this.rolesTable).schema || "public";
10406
10852
  const result = await this.db.execute(sql`
10407
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10853
+ SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
10408
10854
  FROM ${sql.raw(`"${rolesSchema}"."roles"`)} r
10409
10855
  INNER JOIN ${sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10410
10856
  WHERE ur.user_id = ${userId}
@@ -10414,8 +10860,7 @@ class UserService {
10414
10860
  name: row.name,
10415
10861
  isAdmin: row.is_admin,
10416
10862
  defaultPermissions: row.default_permissions,
10417
- collectionPermissions: row.collection_permissions,
10418
- config: row.config
10863
+ collectionPermissions: row.collection_permissions
10419
10864
  }));
10420
10865
  }
10421
10866
  /**
@@ -10481,7 +10926,7 @@ class RoleService {
10481
10926
  async getRoleById(id) {
10482
10927
  const tableName = this.getQualifiedRolesTableName();
10483
10928
  const result = await this.db.execute(sql`
10484
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10929
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10485
10930
  FROM ${sql.raw(tableName)}
10486
10931
  WHERE id = ${id}
10487
10932
  `);
@@ -10492,14 +10937,13 @@ class RoleService {
10492
10937
  name: row.name,
10493
10938
  isAdmin: row.is_admin,
10494
10939
  defaultPermissions: row.default_permissions,
10495
- collectionPermissions: row.collection_permissions,
10496
- config: row.config
10940
+ collectionPermissions: row.collection_permissions
10497
10941
  };
10498
10942
  }
10499
10943
  async listRoles() {
10500
10944
  const tableName = this.getQualifiedRolesTableName();
10501
10945
  const result = await this.db.execute(sql`
10502
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10946
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10503
10947
  FROM ${sql.raw(tableName)}
10504
10948
  ORDER BY name
10505
10949
  `);
@@ -10508,23 +10952,21 @@ class RoleService {
10508
10952
  name: row.name,
10509
10953
  isAdmin: row.is_admin,
10510
10954
  defaultPermissions: row.default_permissions,
10511
- collectionPermissions: row.collection_permissions,
10512
- config: row.config
10955
+ collectionPermissions: row.collection_permissions
10513
10956
  }));
10514
10957
  }
10515
10958
  async createRole(data) {
10516
10959
  const tableName = this.getQualifiedRolesTableName();
10517
10960
  const result = await this.db.execute(sql`
10518
- INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10961
+ INSERT INTO ${sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
10519
10962
  VALUES (
10520
10963
  ${data.id},
10521
10964
  ${data.name},
10522
10965
  ${data.isAdmin ?? false},
10523
10966
  ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10524
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10525
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10967
+ ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
10526
10968
  )
10527
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10969
+ RETURNING id, name, is_admin, default_permissions, collection_permissions
10528
10970
  `);
10529
10971
  const row = result.rows[0];
10530
10972
  return {
@@ -10532,8 +10974,7 @@ class RoleService {
10532
10974
  name: row.name,
10533
10975
  isAdmin: row.is_admin,
10534
10976
  defaultPermissions: row.default_permissions,
10535
- collectionPermissions: row.collection_permissions,
10536
- config: row.config
10977
+ collectionPermissions: row.collection_permissions
10537
10978
  };
10538
10979
  }
10539
10980
  async updateRole(id, data) {
@@ -10546,8 +10987,7 @@ class RoleService {
10546
10987
  name = ${data.name ?? existing.name},
10547
10988
  is_admin = ${data.isAdmin ?? existing.isAdmin},
10548
10989
  default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10549
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10550
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10990
+ collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
10551
10991
  WHERE id = ${id}
10552
10992
  `);
10553
10993
  return this.getRoleById(id);
@@ -10826,8 +11266,7 @@ class PostgresAuthRepository {
10826
11266
  return this.roleService.createRole({
10827
11267
  ...data,
10828
11268
  defaultPermissions: data.defaultPermissions ?? null,
10829
- collectionPermissions: data.collectionPermissions ?? null,
10830
- config: data.config ?? null
11269
+ collectionPermissions: data.collectionPermissions ?? null
10831
11270
  });
10832
11271
  }
10833
11272
  async updateRole(id, data) {
@@ -10870,6 +11309,219 @@ class PostgresAuthRepository {
10870
11309
  async deleteExpiredTokens() {
10871
11310
  await this.tokenRepository.deleteExpiredTokens();
10872
11311
  }
11312
+ // MFA operations (delegate to MfaService)
11313
+ _mfaService = null;
11314
+ getMfaService() {
11315
+ if (!this._mfaService) {
11316
+ this._mfaService = new MfaService(this.db);
11317
+ }
11318
+ return this._mfaService;
11319
+ }
11320
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11321
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11322
+ }
11323
+ async getMfaFactors(userId) {
11324
+ return this.getMfaService().getMfaFactors(userId);
11325
+ }
11326
+ async getMfaFactorById(factorId) {
11327
+ return this.getMfaService().getMfaFactorById(factorId);
11328
+ }
11329
+ async verifyMfaFactor(factorId) {
11330
+ return this.getMfaService().verifyMfaFactor(factorId);
11331
+ }
11332
+ async deleteMfaFactor(factorId, userId) {
11333
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11334
+ }
11335
+ async createMfaChallenge(factorId, ipAddress) {
11336
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11337
+ }
11338
+ async getMfaChallengeById(challengeId) {
11339
+ return this.getMfaService().getMfaChallengeById(challengeId);
11340
+ }
11341
+ async verifyMfaChallenge(challengeId) {
11342
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11343
+ }
11344
+ async createRecoveryCodes(userId, codeHashes) {
11345
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11346
+ }
11347
+ async useRecoveryCode(userId, codeHash) {
11348
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11349
+ }
11350
+ async getUnusedRecoveryCodeCount(userId) {
11351
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11352
+ }
11353
+ async deleteAllRecoveryCodes(userId) {
11354
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11355
+ }
11356
+ async hasVerifiedMfaFactors(userId) {
11357
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11358
+ }
11359
+ }
11360
+ class MfaService {
11361
+ constructor(db, schemaName = "rebase") {
11362
+ this.db = db;
11363
+ this.schemaName = schemaName;
11364
+ }
11365
+ qualify(tableName) {
11366
+ return `"${this.schemaName}"."${tableName}"`;
11367
+ }
11368
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11369
+ const tableName = this.qualify("mfa_factors");
11370
+ const result = await this.db.execute(sql`
11371
+ INSERT INTO ${sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11372
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11373
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11374
+ `);
11375
+ const row = result.rows[0];
11376
+ return {
11377
+ id: row.id,
11378
+ userId: row.user_id,
11379
+ factorType: row.factor_type,
11380
+ friendlyName: row.friendly_name ?? void 0,
11381
+ verified: row.verified,
11382
+ createdAt: new Date(row.created_at),
11383
+ updatedAt: new Date(row.updated_at)
11384
+ };
11385
+ }
11386
+ async getMfaFactors(userId) {
11387
+ const tableName = this.qualify("mfa_factors");
11388
+ const result = await this.db.execute(sql`
11389
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11390
+ FROM ${sql.raw(tableName)}
11391
+ WHERE user_id = ${userId}
11392
+ ORDER BY created_at
11393
+ `);
11394
+ return result.rows.map((row) => ({
11395
+ id: row.id,
11396
+ userId: row.user_id,
11397
+ factorType: row.factor_type,
11398
+ friendlyName: row.friendly_name ?? void 0,
11399
+ verified: row.verified,
11400
+ createdAt: new Date(row.created_at),
11401
+ updatedAt: new Date(row.updated_at)
11402
+ }));
11403
+ }
11404
+ async getMfaFactorById(factorId) {
11405
+ const tableName = this.qualify("mfa_factors");
11406
+ const result = await this.db.execute(sql`
11407
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11408
+ FROM ${sql.raw(tableName)}
11409
+ WHERE id = ${factorId}
11410
+ `);
11411
+ if (result.rows.length === 0) return null;
11412
+ const row = result.rows[0];
11413
+ return {
11414
+ id: row.id,
11415
+ userId: row.user_id,
11416
+ factorType: row.factor_type,
11417
+ secretEncrypted: row.secret_encrypted,
11418
+ friendlyName: row.friendly_name ?? void 0,
11419
+ verified: row.verified,
11420
+ createdAt: new Date(row.created_at),
11421
+ updatedAt: new Date(row.updated_at)
11422
+ };
11423
+ }
11424
+ async verifyMfaFactor(factorId) {
11425
+ const tableName = this.qualify("mfa_factors");
11426
+ await this.db.execute(sql`
11427
+ UPDATE ${sql.raw(tableName)}
11428
+ SET verified = TRUE, updated_at = NOW()
11429
+ WHERE id = ${factorId}
11430
+ `);
11431
+ }
11432
+ async deleteMfaFactor(factorId, userId) {
11433
+ const tableName = this.qualify("mfa_factors");
11434
+ await this.db.execute(sql`
11435
+ DELETE FROM ${sql.raw(tableName)}
11436
+ WHERE id = ${factorId} AND user_id = ${userId}
11437
+ `);
11438
+ }
11439
+ async createMfaChallenge(factorId, ipAddress) {
11440
+ const tableName = this.qualify("mfa_challenges");
11441
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11442
+ const result = await this.db.execute(sql`
11443
+ INSERT INTO ${sql.raw(tableName)} (factor_id, ip_address, expires_at)
11444
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11445
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11446
+ `);
11447
+ const row = result.rows[0];
11448
+ return {
11449
+ id: row.id,
11450
+ factorId: row.factor_id,
11451
+ createdAt: new Date(row.created_at),
11452
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11453
+ ipAddress: row.ip_address ?? void 0
11454
+ };
11455
+ }
11456
+ async getMfaChallengeById(challengeId) {
11457
+ const tableName = this.qualify("mfa_challenges");
11458
+ const result = await this.db.execute(sql`
11459
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11460
+ FROM ${sql.raw(tableName)}
11461
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11462
+ `);
11463
+ if (result.rows.length === 0) return null;
11464
+ const row = result.rows[0];
11465
+ return {
11466
+ id: row.id,
11467
+ factorId: row.factor_id,
11468
+ createdAt: new Date(row.created_at),
11469
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11470
+ ipAddress: row.ip_address ?? void 0
11471
+ };
11472
+ }
11473
+ async verifyMfaChallenge(challengeId) {
11474
+ const tableName = this.qualify("mfa_challenges");
11475
+ await this.db.execute(sql`
11476
+ UPDATE ${sql.raw(tableName)}
11477
+ SET verified_at = NOW()
11478
+ WHERE id = ${challengeId}
11479
+ `);
11480
+ }
11481
+ async createRecoveryCodes(userId, codeHashes) {
11482
+ const tableName = this.qualify("recovery_codes");
11483
+ await this.db.execute(sql`
11484
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11485
+ `);
11486
+ for (const hash of codeHashes) {
11487
+ await this.db.execute(sql`
11488
+ INSERT INTO ${sql.raw(tableName)} (user_id, code_hash)
11489
+ VALUES (${userId}, ${hash})
11490
+ `);
11491
+ }
11492
+ }
11493
+ async useRecoveryCode(userId, codeHash) {
11494
+ const tableName = this.qualify("recovery_codes");
11495
+ const result = await this.db.execute(sql`
11496
+ UPDATE ${sql.raw(tableName)}
11497
+ SET used_at = NOW()
11498
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11499
+ RETURNING id
11500
+ `);
11501
+ return result.rows.length > 0;
11502
+ }
11503
+ async getUnusedRecoveryCodeCount(userId) {
11504
+ const tableName = this.qualify("recovery_codes");
11505
+ const result = await this.db.execute(sql`
11506
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11507
+ WHERE user_id = ${userId} AND used_at IS NULL
11508
+ `);
11509
+ return result.rows[0].count;
11510
+ }
11511
+ async deleteAllRecoveryCodes(userId) {
11512
+ const tableName = this.qualify("recovery_codes");
11513
+ await this.db.execute(sql`
11514
+ DELETE FROM ${sql.raw(tableName)} WHERE user_id = ${userId}
11515
+ `);
11516
+ }
11517
+ async hasVerifiedMfaFactors(userId) {
11518
+ const tableName = this.qualify("mfa_factors");
11519
+ const result = await this.db.execute(sql`
11520
+ SELECT COUNT(*)::int as count FROM ${sql.raw(tableName)}
11521
+ WHERE user_id = ${userId} AND verified = TRUE
11522
+ `);
11523
+ return result.rows[0].count > 0;
11524
+ }
10873
11525
  }
10874
11526
  const DEFAULT_RETENTION = {
10875
11527
  maxEntries: 200,
@@ -11080,7 +11732,7 @@ function createPostgresBootstrapper(pgConfig) {
11080
11732
  const registry = new PostgresCollectionRegistry();
11081
11733
  if (collections) {
11082
11734
  registry.registerMultiple(collections);
11083
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11735
+ logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11084
11736
  }
11085
11737
  if (pgConfig.schema?.tables) {
11086
11738
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -11106,10 +11758,28 @@ function createPostgresBootstrapper(pgConfig) {
11106
11758
  try {
11107
11759
  await schemaAwareDb.execute(sql`SELECT 1`);
11108
11760
  } catch (err) {
11109
- console.error("❌ Failed to connect to PostgreSQL:", err);
11110
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11761
+ logger.error("❌ Failed to connect to PostgreSQL", {
11762
+ error: err
11763
+ });
11764
+ logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11111
11765
  }
11112
11766
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11767
+ let readDb;
11768
+ const readUrl = process.env.DATABASE_READ_URL;
11769
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11770
+ try {
11771
+ const {
11772
+ createReadReplicaConnection: createReadReplicaConnection2
11773
+ } = await Promise.resolve().then(() => connection);
11774
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11775
+ readDb = readResources.db;
11776
+ logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11777
+ } catch (err) {
11778
+ logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11779
+ error: err
11780
+ });
11781
+ }
11782
+ }
11113
11783
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
11114
11784
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
11115
11785
  realtimeService.setDataDriver(driver);
@@ -11117,18 +11787,24 @@ function createPostgresBootstrapper(pgConfig) {
11117
11787
  try {
11118
11788
  await driver.branchService.ensureBranchMetadataTable();
11119
11789
  } catch (err) {
11120
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11790
+ logger.warn("⚠️ Could not initialize branch metadata table", {
11791
+ error: err
11792
+ });
11121
11793
  }
11122
11794
  }
11123
- if (pgConfig.connectionString) {
11795
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11796
+ if (directUrl) {
11124
11797
  try {
11125
- await realtimeService.startListening(pgConfig.connectionString);
11798
+ await realtimeService.startListening(directUrl);
11126
11799
  } catch (err) {
11127
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11800
+ logger.warn("⚠️ Cross-instance realtime could not be started", {
11801
+ error: err
11802
+ });
11128
11803
  }
11129
11804
  }
11130
11805
  const internals = {
11131
11806
  db: schemaAwareDb,
11807
+ readDb,
11132
11808
  registry,
11133
11809
  realtimeService,
11134
11810
  driver,
@@ -11266,14 +11942,22 @@ export {
11266
11942
  RealtimeService,
11267
11943
  appConfig,
11268
11944
  createAuthSchema,
11945
+ createDirectDatabaseConnection,
11269
11946
  createPostgresAdapter,
11270
11947
  createPostgresBootstrapper,
11271
11948
  createPostgresDatabaseConnection,
11272
11949
  createPostgresWebSocket,
11950
+ createReadReplicaConnection,
11273
11951
  generateSchema,
11952
+ mfaChallenges,
11953
+ mfaChallengesRelations,
11954
+ mfaFactors,
11955
+ mfaFactorsRelations,
11274
11956
  passwordResetTokens,
11275
11957
  passwordResetTokensRelations,
11276
11958
  rebaseSchema,
11959
+ recoveryCodes,
11960
+ recoveryCodesRelations,
11277
11961
  refreshTokens,
11278
11962
  refreshTokensRelations,
11279
11963
  roles,