@rebasepro/server-postgresql 0.2.3 → 0.2.4

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.umd.js CHANGED
@@ -59,6 +59,70 @@
59
59
  connectionString
60
60
  };
61
61
  }
62
+ function createDirectDatabaseConnection(connectionString, schema, poolConfig) {
63
+ const opts = {
64
+ ...DEFAULT_POOL,
65
+ max: 5,
66
+ ...poolConfig
67
+ };
68
+ const pgPoolConfig = {
69
+ connectionString,
70
+ max: opts.max,
71
+ idleTimeoutMillis: opts.idleTimeoutMillis,
72
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
73
+ query_timeout: opts.queryTimeout,
74
+ statement_timeout: opts.statementTimeout,
75
+ keepAlive: opts.keepAlive,
76
+ keepAliveInitialDelayMillis: 0
77
+ };
78
+ const pool = new pg.Pool(pgPoolConfig);
79
+ pool.on("error", (err) => {
80
+ console.error("[pg-direct-pool] Unexpected pool error:", err.message);
81
+ });
82
+ const db = schema ? nodePostgres.drizzle(pool, {
83
+ schema
84
+ }) : nodePostgres.drizzle(pool);
85
+ return {
86
+ db,
87
+ pool,
88
+ connectionString
89
+ };
90
+ }
91
+ function createReadReplicaConnection(connectionString, schema, poolConfig) {
92
+ const opts = {
93
+ ...DEFAULT_POOL,
94
+ max: 10,
95
+ ...poolConfig
96
+ };
97
+ const pgPoolConfig = {
98
+ connectionString,
99
+ max: opts.max,
100
+ idleTimeoutMillis: opts.idleTimeoutMillis,
101
+ connectionTimeoutMillis: opts.connectionTimeoutMillis,
102
+ query_timeout: opts.queryTimeout,
103
+ statement_timeout: opts.statementTimeout,
104
+ keepAlive: opts.keepAlive,
105
+ keepAliveInitialDelayMillis: 0
106
+ };
107
+ const pool = new pg.Pool(pgPoolConfig);
108
+ pool.on("error", (err) => {
109
+ console.error("[pg-replica-pool] Unexpected pool error:", err.message);
110
+ });
111
+ const db = schema ? nodePostgres.drizzle(pool, {
112
+ schema
113
+ }) : nodePostgres.drizzle(pool);
114
+ return {
115
+ db,
116
+ pool,
117
+ connectionString
118
+ };
119
+ }
120
+ const connection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
121
+ __proto__: null,
122
+ createDirectDatabaseConnection,
123
+ createPostgresDatabaseConnection,
124
+ createReadReplicaConnection
125
+ }, Symbol.toStringTag, { value: "Module" }));
62
126
  class Vector {
63
127
  value;
64
128
  constructor(value) {
@@ -978,6 +1042,9 @@
978
1042
  return output;
979
1043
  }
980
1044
  for (const key in source) {
1045
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1046
+ continue;
1047
+ }
981
1048
  if (Object.prototype.hasOwnProperty.call(source, key)) {
982
1049
  const sourceValue = source[key];
983
1050
  const outputValue = output[key];
@@ -2652,6 +2719,90 @@
2652
2719
  };
2653
2720
  }
2654
2721
  }
2722
+ const defaultUsersCollection = {
2723
+ name: "Users",
2724
+ singularName: "User",
2725
+ slug: "users",
2726
+ table: "users",
2727
+ schema: "rebase",
2728
+ icon: "Users",
2729
+ group: "Settings",
2730
+ properties: {
2731
+ id: {
2732
+ name: "ID",
2733
+ type: "string",
2734
+ isId: "uuid"
2735
+ },
2736
+ email: {
2737
+ name: "Email",
2738
+ type: "string",
2739
+ validation: {
2740
+ required: true,
2741
+ unique: true
2742
+ }
2743
+ },
2744
+ password_hash: {
2745
+ name: "Password Hash",
2746
+ type: "string",
2747
+ ui: {
2748
+ hideFromCollection: true
2749
+ }
2750
+ },
2751
+ display_name: {
2752
+ name: "Display Name",
2753
+ type: "string"
2754
+ },
2755
+ photo_url: {
2756
+ name: "Photo URL",
2757
+ type: "string"
2758
+ },
2759
+ email_verified: {
2760
+ name: "Email Verified",
2761
+ type: "boolean",
2762
+ defaultValue: false
2763
+ },
2764
+ email_verification_token: {
2765
+ name: "Email Verification Token",
2766
+ type: "string",
2767
+ ui: {
2768
+ hideFromCollection: true
2769
+ }
2770
+ },
2771
+ email_verification_sent_at: {
2772
+ name: "Email Verification Sent At",
2773
+ type: "date",
2774
+ ui: {
2775
+ hideFromCollection: true
2776
+ }
2777
+ },
2778
+ metadata: {
2779
+ name: "Metadata",
2780
+ type: "map",
2781
+ defaultValue: {},
2782
+ ui: {
2783
+ hideFromCollection: true
2784
+ }
2785
+ },
2786
+ created_at: {
2787
+ name: "Created At",
2788
+ type: "date",
2789
+ autoValue: "on_create",
2790
+ ui: {
2791
+ readOnly: true,
2792
+ hideFromCollection: true
2793
+ }
2794
+ },
2795
+ updated_at: {
2796
+ name: "Updated At",
2797
+ type: "date",
2798
+ autoValue: "on_update",
2799
+ ui: {
2800
+ readOnly: true,
2801
+ hideFromCollection: true
2802
+ }
2803
+ }
2804
+ }
2805
+ };
2655
2806
  function mapOperator(op) {
2656
2807
  switch (op) {
2657
2808
  case "==":
@@ -2992,7 +3143,13 @@
2992
3143
  for (const [field, filterParam] of Object.entries(filter)) {
2993
3144
  if (!filterParam) continue;
2994
3145
  const [op, value] = filterParam;
2995
- const fieldColumn = table[field];
3146
+ let fieldColumn = table[field];
3147
+ if (!fieldColumn) {
3148
+ const relationKey = `${field}_id`;
3149
+ if (relationKey in table) {
3150
+ fieldColumn = table[relationKey];
3151
+ }
3152
+ }
2996
3153
  if (!fieldColumn) {
2997
3154
  console.warn(`Filtering by field '${field}', but it does not exist in table for collection '${collectionPath}'`);
2998
3155
  continue;
@@ -3034,6 +3191,17 @@
3034
3191
  return null;
3035
3192
  case "array-contains":
3036
3193
  return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3194
+ case "array-contains-any":
3195
+ if (Array.isArray(value) && value.length > 0) {
3196
+ const textValues = value.map((v) => String(v));
3197
+ return drizzleOrm.sql`${column} ?| array[${drizzleOrm.sql.join(textValues.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)}]`;
3198
+ }
3199
+ return drizzleOrm.sql`${column} @> ${JSON.stringify([value])}`;
3200
+ case "not-in":
3201
+ if (Array.isArray(value) && value.length > 0) {
3202
+ return drizzleOrm.sql`${column} NOT IN (${drizzleOrm.sql.join(value.map((v) => drizzleOrm.sql`${v}`), drizzleOrm.sql`, `)})`;
3203
+ }
3204
+ return null;
3037
3205
  default:
3038
3206
  console.warn(`Unsupported filter operation: ${op}`);
3039
3207
  return null;
@@ -3549,6 +3717,40 @@
3549
3717
  return null;
3550
3718
  }
3551
3719
  }
3720
+ /**
3721
+ * Build vector similarity search expressions for pgvector.
3722
+ *
3723
+ * Returns:
3724
+ * - `orderBy`: SQL expression to ORDER BY distance (ascending = closest first)
3725
+ * - `filter`: optional WHERE clause for distance threshold
3726
+ * - `distanceSelect`: SQL expression for selecting the distance as `_distance`
3727
+ */
3728
+ static buildVectorSearchConditions(table, vectorSearch) {
3729
+ const column = table[vectorSearch.property];
3730
+ if (!column) {
3731
+ throw new Error(`Vector column '${vectorSearch.property}' not found in table`);
3732
+ }
3733
+ const vectorLiteral = `'[${vectorSearch.vector.join(",")}]'::vector`;
3734
+ const distanceFn = vectorSearch.distance || "cosine";
3735
+ let operator;
3736
+ switch (distanceFn) {
3737
+ case "cosine":
3738
+ operator = "<=>";
3739
+ break;
3740
+ case "l2":
3741
+ operator = "<->";
3742
+ break;
3743
+ case "inner_product":
3744
+ operator = "<#>";
3745
+ break;
3746
+ }
3747
+ const distanceExpr = drizzleOrm.sql`${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}`;
3748
+ return {
3749
+ orderBy: distanceExpr,
3750
+ filter: vectorSearch.threshold != null ? drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)}) < ${vectorSearch.threshold}` : void 0,
3751
+ distanceSelect: drizzleOrm.sql`(${column} ${drizzleOrm.sql.raw(operator)} ${drizzleOrm.sql.raw(vectorLiteral)})`
3752
+ };
3753
+ }
3552
3754
  }
3553
3755
  const PostgresConditionBuilder = DrizzleConditionBuilder;
3554
3756
  function getColumnMeta(col) {
@@ -5494,7 +5696,7 @@
5494
5696
  const qb = this.getQueryBuilder(tableName);
5495
5697
  const withConfig = this.buildWithConfig(collection);
5496
5698
  const hasRelations = withConfig && Object.keys(withConfig).length > 0;
5497
- if (qb && !options.searchString && !hasRelations) {
5699
+ if (qb && !options.searchString && !hasRelations && !options.vectorSearch) {
5498
5700
  try {
5499
5701
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
5500
5702
  const results2 = await qb.findMany(queryOpts);
@@ -5508,7 +5710,14 @@
5508
5710
  console.warn(`[EntityFetchService] db.query.findMany failed for ${collectionPath}, falling back to db.select:`, e);
5509
5711
  }
5510
5712
  }
5511
- let query = this.db.select().from(table).$dynamic();
5713
+ let vectorMeta;
5714
+ if (options.vectorSearch) {
5715
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
5716
+ }
5717
+ let query = vectorMeta ? this.db.select({
5718
+ table_row: table,
5719
+ _distance: vectorMeta.distanceSelect
5720
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5512
5721
  const allConditions = [];
5513
5722
  if (options.searchString) {
5514
5723
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5519,12 +5728,17 @@
5519
5728
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5520
5729
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5521
5730
  }
5731
+ if (vectorMeta?.filter) {
5732
+ allConditions.push(vectorMeta.filter);
5733
+ }
5522
5734
  if (allConditions.length > 0) {
5523
5735
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5524
5736
  if (finalCondition) query = query.where(finalCondition);
5525
5737
  }
5526
5738
  const orderExpressions = [];
5527
- if (options.orderBy) {
5739
+ if (vectorMeta) {
5740
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
5741
+ } else if (options.orderBy) {
5528
5742
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5529
5743
  if (orderByField) {
5530
5744
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5540,10 +5754,14 @@
5540
5754
  if (finalCondition) query = query.where(finalCondition);
5541
5755
  }
5542
5756
  }
5543
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
5757
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5544
5758
  if (limitValue) query = query.limit(limitValue);
5545
5759
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5546
- const results = await query;
5760
+ const rawResults = await query;
5761
+ const results = vectorMeta ? rawResults.map((r) => ({
5762
+ ...r.table_row,
5763
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
5764
+ })) : rawResults;
5547
5765
  return this.processEntityResults(results, collection, collectionPath, idInfo, options.databaseId, false, idInfoArray);
5548
5766
  }
5549
5767
  /**
@@ -5765,7 +5983,7 @@
5765
5983
  const idField = table[idInfo.fieldName];
5766
5984
  const tableName = drizzleOrm.getTableName(table);
5767
5985
  const qb = this.getQueryBuilder(tableName);
5768
- if (qb && !options.searchString) {
5986
+ if (qb && !options.searchString && !options.vectorSearch) {
5769
5987
  try {
5770
5988
  const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
5771
5989
  const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
@@ -5911,7 +6129,14 @@
5911
6129
  const idInfoArray = getPrimaryKeys(collection, this.registry);
5912
6130
  const idInfo = idInfoArray[0];
5913
6131
  const idField = table[idInfo.fieldName];
5914
- let query = this.db.select().from(table).$dynamic();
6132
+ let vectorMeta;
6133
+ if (options.vectorSearch) {
6134
+ vectorMeta = DrizzleConditionBuilder.buildVectorSearchConditions(table, options.vectorSearch);
6135
+ }
6136
+ let query = vectorMeta ? this.db.select({
6137
+ table_row: table,
6138
+ _distance: vectorMeta.distanceSelect
6139
+ }).from(table).$dynamic() : this.db.select().from(table).$dynamic();
5915
6140
  const allConditions = [];
5916
6141
  if (options.searchString) {
5917
6142
  const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
@@ -5922,12 +6147,17 @@
5922
6147
  const filterConditions = this.buildFilterConditions(options.filter, table, collectionPath);
5923
6148
  if (filterConditions.length > 0) allConditions.push(...filterConditions);
5924
6149
  }
6150
+ if (vectorMeta?.filter) {
6151
+ allConditions.push(vectorMeta.filter);
6152
+ }
5925
6153
  if (allConditions.length > 0) {
5926
6154
  const finalCondition = DrizzleConditionBuilder.combineConditionsWithAnd(allConditions);
5927
6155
  if (finalCondition) query = query.where(finalCondition);
5928
6156
  }
5929
6157
  const orderExpressions = [];
5930
- if (options.orderBy) {
6158
+ if (vectorMeta) {
6159
+ orderExpressions.push(drizzleOrm.asc(vectorMeta.orderBy));
6160
+ } else if (options.orderBy) {
5931
6161
  const orderByField = this.resolveOrderByField(table, options.orderBy, collection);
5932
6162
  if (orderByField) {
5933
6163
  orderExpressions.push(options.order === "asc" ? drizzleOrm.asc(orderByField) : drizzleOrm.desc(orderByField));
@@ -5935,10 +6165,17 @@
5935
6165
  }
5936
6166
  orderExpressions.push(drizzleOrm.desc(idField));
5937
6167
  if (orderExpressions.length > 0) query = query.orderBy(...orderExpressions);
5938
- const limitValue = options.searchString ? options.limit || 50 : options.limit;
6168
+ const limitValue = options.vectorSearch ? options.limit || 10 : options.searchString ? options.limit || 50 : options.limit;
5939
6169
  if (limitValue) query = query.limit(limitValue);
5940
6170
  if (options.offset && options.offset > 0) query = query.offset(options.offset);
5941
- return await query;
6171
+ const rawResults = await query;
6172
+ if (vectorMeta) {
6173
+ return rawResults.map((r) => ({
6174
+ ...r.table_row,
6175
+ _distance: typeof r._distance === "number" ? r._distance : parseFloat(String(r._distance))
6176
+ }));
6177
+ }
6178
+ return rawResults;
5942
6179
  }
5943
6180
  /**
5944
6181
  * Check if the Drizzle instance has the relational query API available
@@ -6692,7 +6929,8 @@
6692
6929
  startAfter,
6693
6930
  orderBy,
6694
6931
  searchString,
6695
- order
6932
+ order,
6933
+ vectorSearch
6696
6934
  }) {
6697
6935
  const entities = await this.entityService.fetchCollection(path2, {
6698
6936
  filter,
@@ -6702,7 +6940,8 @@
6702
6940
  offset,
6703
6941
  startAfter,
6704
6942
  databaseId: collection?.databaseId,
6705
- searchString
6943
+ searchString,
6944
+ vectorSearch
6706
6945
  });
6707
6946
  const {
6708
6947
  collection: resolvedCollection,
@@ -7545,6 +7784,7 @@
7545
7784
  length: 255
7546
7785
  }),
7547
7786
  emailVerificationSentAt: pgCore.timestamp("email_verification_sent_at"),
7787
+ isAnonymous: pgCore.boolean("is_anonymous").default(false).notNull(),
7548
7788
  metadata: pgCore.jsonb("metadata").$type().default({}).notNull(),
7549
7789
  createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7550
7790
  updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
@@ -7559,8 +7799,7 @@
7559
7799
  }).notNull(),
7560
7800
  isAdmin: pgCore.boolean("is_admin").default(false).notNull(),
7561
7801
  defaultPermissions: pgCore.jsonb("default_permissions").$type(),
7562
- collectionPermissions: pgCore.jsonb("collection_permissions").$type(),
7563
- config: pgCore.jsonb("config").$type()
7802
+ collectionPermissions: pgCore.jsonb("collection_permissions").$type()
7564
7803
  });
7565
7804
  const userRoles2 = rolesTableCreator("user_roles", {
7566
7805
  userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
@@ -7632,6 +7871,48 @@
7632
7871
  }, (table) => ({
7633
7872
  uniqueProviderId: pgCore.unique("unique_provider_id").on(table.provider, table.providerId)
7634
7873
  }));
7874
+ const mfaFactors2 = rolesTableCreator("mfa_factors", {
7875
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7876
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7877
+ onDelete: "cascade"
7878
+ }),
7879
+ factorType: pgCore.varchar("factor_type", {
7880
+ length: 20
7881
+ }).notNull(),
7882
+ // 'totp'
7883
+ secretEncrypted: pgCore.varchar("secret_encrypted", {
7884
+ length: 500
7885
+ }).notNull(),
7886
+ friendlyName: pgCore.varchar("friendly_name", {
7887
+ length: 255
7888
+ }),
7889
+ verified: pgCore.boolean("verified").default(false).notNull(),
7890
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7891
+ updatedAt: pgCore.timestamp("updated_at").defaultNow().notNull()
7892
+ });
7893
+ const mfaChallenges2 = rolesTableCreator("mfa_challenges", {
7894
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7895
+ factorId: pgCore.uuid("factor_id").notNull().references(() => mfaFactors2.id, {
7896
+ onDelete: "cascade"
7897
+ }),
7898
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull(),
7899
+ verifiedAt: pgCore.timestamp("verified_at"),
7900
+ ipAddress: pgCore.varchar("ip_address", {
7901
+ length: 45
7902
+ }),
7903
+ expiresAt: pgCore.timestamp("expires_at").notNull()
7904
+ });
7905
+ const recoveryCodes2 = rolesTableCreator("recovery_codes", {
7906
+ id: pgCore.uuid("id").defaultRandom().primaryKey(),
7907
+ userId: pgCore.uuid("user_id").notNull().references(() => users2.id, {
7908
+ onDelete: "cascade"
7909
+ }),
7910
+ codeHash: pgCore.varchar("code_hash", {
7911
+ length: 255
7912
+ }).notNull(),
7913
+ usedAt: pgCore.timestamp("used_at"),
7914
+ createdAt: pgCore.timestamp("created_at").defaultNow().notNull()
7915
+ });
7635
7916
  return {
7636
7917
  rolesSchema,
7637
7918
  usersSchema: usersSchema2,
@@ -7641,7 +7922,10 @@
7641
7922
  refreshTokens: refreshTokens2,
7642
7923
  passwordResetTokens: passwordResetTokens2,
7643
7924
  appConfig: appConfig2,
7644
- userIdentities: userIdentities2
7925
+ userIdentities: userIdentities2,
7926
+ mfaFactors: mfaFactors2,
7927
+ mfaChallenges: mfaChallenges2,
7928
+ recoveryCodes: recoveryCodes2
7645
7929
  };
7646
7930
  }
7647
7931
  const defaultAuthSchema = createAuthSchema("rebase", "rebase");
@@ -7654,13 +7938,18 @@
7654
7938
  const passwordResetTokens = defaultAuthSchema.passwordResetTokens;
7655
7939
  const appConfig = defaultAuthSchema.appConfig;
7656
7940
  const userIdentities = defaultAuthSchema.userIdentities;
7941
+ const mfaFactors = defaultAuthSchema.mfaFactors;
7942
+ const mfaChallenges = defaultAuthSchema.mfaChallenges;
7943
+ const recoveryCodes = defaultAuthSchema.recoveryCodes;
7657
7944
  const usersRelations = drizzleOrm.relations(users, ({
7658
7945
  many
7659
7946
  }) => ({
7660
7947
  userRoles: many(userRoles),
7661
7948
  refreshTokens: many(refreshTokens),
7662
7949
  passwordResetTokens: many(passwordResetTokens),
7663
- userIdentities: many(userIdentities)
7950
+ userIdentities: many(userIdentities),
7951
+ mfaFactors: many(mfaFactors),
7952
+ recoveryCodes: many(recoveryCodes)
7664
7953
  }));
7665
7954
  const rolesRelations = drizzleOrm.relations(roles, ({
7666
7955
  many
@@ -7703,6 +7992,32 @@
7703
7992
  references: [users.id]
7704
7993
  })
7705
7994
  }));
7995
+ const mfaFactorsRelations = drizzleOrm.relations(mfaFactors, ({
7996
+ one,
7997
+ many
7998
+ }) => ({
7999
+ user: one(users, {
8000
+ fields: [mfaFactors.userId],
8001
+ references: [users.id]
8002
+ }),
8003
+ challenges: many(mfaChallenges)
8004
+ }));
8005
+ const mfaChallengesRelations = drizzleOrm.relations(mfaChallenges, ({
8006
+ one
8007
+ }) => ({
8008
+ factor: one(mfaFactors, {
8009
+ fields: [mfaChallenges.factorId],
8010
+ references: [mfaFactors.id]
8011
+ })
8012
+ }));
8013
+ const recoveryCodesRelations = drizzleOrm.relations(recoveryCodes, ({
8014
+ one
8015
+ }) => ({
8016
+ user: one(users, {
8017
+ fields: [recoveryCodes.userId],
8018
+ references: [users.id]
8019
+ })
8020
+ }));
7706
8021
  const resolveColumnName = (propName, prop) => {
7707
8022
  if (prop && "columnName" in prop && typeof prop.columnName === "string") {
7708
8023
  return prop.columnName;
@@ -8306,90 +8621,6 @@ ${tableRelations.join(",\n")}
8306
8621
  schemaContent += tablesExport + enumsExport + relationsExport;
8307
8622
  return schemaContent;
8308
8623
  };
8309
- const defaultUsersCollection = {
8310
- name: "Users",
8311
- singularName: "User",
8312
- slug: "users",
8313
- table: "users",
8314
- schema: "rebase",
8315
- icon: "Users",
8316
- group: "Settings",
8317
- properties: {
8318
- id: {
8319
- name: "ID",
8320
- type: "string",
8321
- isId: "uuid"
8322
- },
8323
- email: {
8324
- name: "Email",
8325
- type: "string",
8326
- validation: {
8327
- required: true,
8328
- unique: true
8329
- }
8330
- },
8331
- password_hash: {
8332
- name: "Password Hash",
8333
- type: "string",
8334
- ui: {
8335
- hideFromCollection: true
8336
- }
8337
- },
8338
- display_name: {
8339
- name: "Display Name",
8340
- type: "string"
8341
- },
8342
- photo_url: {
8343
- name: "Photo URL",
8344
- type: "string"
8345
- },
8346
- email_verified: {
8347
- name: "Email Verified",
8348
- type: "boolean",
8349
- defaultValue: false
8350
- },
8351
- email_verification_token: {
8352
- name: "Email Verification Token",
8353
- type: "string",
8354
- ui: {
8355
- hideFromCollection: true
8356
- }
8357
- },
8358
- email_verification_sent_at: {
8359
- name: "Email Verification Sent At",
8360
- type: "date",
8361
- ui: {
8362
- hideFromCollection: true
8363
- }
8364
- },
8365
- metadata: {
8366
- name: "Metadata",
8367
- type: "map",
8368
- defaultValue: {},
8369
- ui: {
8370
- hideFromCollection: true
8371
- }
8372
- },
8373
- created_at: {
8374
- name: "Created At",
8375
- type: "date",
8376
- autoValue: "on_create",
8377
- ui: {
8378
- readOnly: true,
8379
- hideFromCollection: true
8380
- }
8381
- },
8382
- updated_at: {
8383
- name: "Updated At",
8384
- type: "date",
8385
- autoValue: "on_update",
8386
- ui: {
8387
- readOnly: true,
8388
- hideFromCollection: true
8389
- }
8390
- }
8391
- }
8392
- };
8393
8624
  const formatTerminalText = (text, options = {}) => {
8394
8625
  let codes = "";
8395
8626
  if (options.bold) codes += "\x1B[1m";
@@ -8455,10 +8686,7 @@ ${tableRelations.join(",\n")}
8455
8686
  if (!collections || !Array.isArray(collections)) {
8456
8687
  collections = [];
8457
8688
  }
8458
- const hasUsersCollection = collections.some((c) => c.slug === "users");
8459
- if (!hasUsersCollection) {
8460
- collections.push(defaultUsersCollection);
8461
- }
8689
+ collections = Array.from(new Map([defaultUsersCollection, ...collections].map((c) => [c.slug, c])).values());
8462
8690
  collections.sort((a, b) => a.slug.localeCompare(b.slug));
8463
8691
  const schemaContent = await generateSchema(collections);
8464
8692
  if (outputPath) {
@@ -8519,6 +8747,13 @@ ${tableRelations.join(",\n")}
8519
8747
  this.entityService = new EntityService(db, registry);
8520
8748
  }
8521
8749
  clients = /* @__PURE__ */ new Map();
8750
+ // Broadcast channels: channel name → set of client IDs
8751
+ channels = /* @__PURE__ */ new Map();
8752
+ // Presence: channel → Map<clientId, { state, lastSeen }>
8753
+ presence = /* @__PURE__ */ new Map();
8754
+ presenceInterval;
8755
+ static PRESENCE_TIMEOUT_MS = 3e4;
8756
+ // 30s
8522
8757
  entityService;
8523
8758
  // Enhanced subscriptions storage with full request parameters
8524
8759
  _subscriptions = /* @__PURE__ */ new Map();
@@ -8645,8 +8880,19 @@ ${tableRelations.join(",\n")}
8645
8880
  }
8646
8881
  }
8647
8882
  }
8883
+ for (const [channel, members] of this.channels.entries()) {
8884
+ if (members.has(clientId)) {
8885
+ members.delete(clientId);
8886
+ this.removePresence(clientId, channel);
8887
+ if (members.size === 0) this.channels.delete(channel);
8888
+ }
8889
+ }
8890
+ for (const [channel] of this.presence) {
8891
+ this.removePresence(clientId, channel);
8892
+ }
8648
8893
  }
8649
8894
  async handleMessage(clientId, message, authContext) {
8895
+ const payload = message.payload;
8650
8896
  switch (message.type) {
8651
8897
  case "subscribe_collection":
8652
8898
  await this.handleCollectionSubscription(clientId, message.payload, authContext);
@@ -8657,6 +8903,25 @@ ${tableRelations.join(",\n")}
8657
8903
  case "unsubscribe":
8658
8904
  await this.handleUnsubscribe(clientId, message.subscriptionId);
8659
8905
  break;
8906
+ case "join_channel":
8907
+ this.joinChannel(clientId, payload?.channel);
8908
+ break;
8909
+ case "leave_channel":
8910
+ this.leaveChannel(clientId, payload?.channel);
8911
+ break;
8912
+ case "broadcast":
8913
+ this.broadcastToChannel(clientId, payload?.channel, payload?.event, payload?.payload);
8914
+ break;
8915
+ case "presence_track":
8916
+ this.joinChannel(clientId, payload?.channel);
8917
+ this.trackPresence(clientId, payload?.channel, payload?.state ?? {});
8918
+ break;
8919
+ case "presence_untrack":
8920
+ this.removePresence(clientId, payload?.channel);
8921
+ break;
8922
+ case "presence_state":
8923
+ this.sendPresenceState(clientId, payload?.channel);
8924
+ break;
8660
8925
  default:
8661
8926
  this.sendError(clientId, "Unknown message type " + message.type, message.subscriptionId);
8662
8927
  }
@@ -9114,6 +9379,132 @@ ${tableRelations.join(",\n")}
9114
9379
  return parentPaths;
9115
9380
  }
9116
9381
  // =============================================================================
9382
+ // Broadcast Channels
9383
+ // =============================================================================
9384
+ /** Join a broadcast channel */
9385
+ joinChannel(clientId, channel) {
9386
+ if (!this.channels.has(channel)) {
9387
+ this.channels.set(channel, /* @__PURE__ */ new Set());
9388
+ }
9389
+ this.channels.get(channel).add(clientId);
9390
+ this.debugLog(`📡 [Broadcast] Client ${clientId} joined channel: ${channel}`);
9391
+ }
9392
+ /** Leave a broadcast channel */
9393
+ leaveChannel(clientId, channel) {
9394
+ const members = this.channels.get(channel);
9395
+ if (members) {
9396
+ members.delete(clientId);
9397
+ if (members.size === 0) this.channels.delete(channel);
9398
+ }
9399
+ this.removePresence(clientId, channel);
9400
+ }
9401
+ /** Broadcast a message to all clients in a channel except sender */
9402
+ broadcastToChannel(clientId, channel, event, payload) {
9403
+ const members = this.channels.get(channel);
9404
+ if (!members) return;
9405
+ const message = JSON.stringify({
9406
+ type: "broadcast",
9407
+ channel,
9408
+ event,
9409
+ payload
9410
+ });
9411
+ for (const memberId of members) {
9412
+ if (memberId === clientId) continue;
9413
+ const ws$1 = this.clients.get(memberId);
9414
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9415
+ ws$1.send(message);
9416
+ }
9417
+ }
9418
+ }
9419
+ // =============================================================================
9420
+ // Presence
9421
+ // =============================================================================
9422
+ /** Track presence in a channel */
9423
+ trackPresence(clientId, channel, state) {
9424
+ if (!this.presence.has(channel)) {
9425
+ this.presence.set(channel, /* @__PURE__ */ new Map());
9426
+ }
9427
+ const channelPresence = this.presence.get(channel);
9428
+ channelPresence.set(clientId, {
9429
+ state,
9430
+ lastSeen: Date.now()
9431
+ });
9432
+ this.broadcastPresenceDiff(channel, {
9433
+ [clientId]: state
9434
+ }, {});
9435
+ this.ensurePresenceCleanup();
9436
+ }
9437
+ /** Remove presence from a channel */
9438
+ removePresence(clientId, channel) {
9439
+ const channelPresence = this.presence.get(channel);
9440
+ if (!channelPresence) return;
9441
+ const entry = channelPresence.get(clientId);
9442
+ if (entry) {
9443
+ channelPresence.delete(clientId);
9444
+ this.broadcastPresenceDiff(channel, {}, {
9445
+ [clientId]: entry.state
9446
+ });
9447
+ }
9448
+ if (channelPresence.size === 0) {
9449
+ this.presence.delete(channel);
9450
+ }
9451
+ }
9452
+ /** Send full presence state to a specific client */
9453
+ sendPresenceState(clientId, channel) {
9454
+ const channelPresence = this.presence.get(channel);
9455
+ const presences = {};
9456
+ if (channelPresence) {
9457
+ for (const [id, {
9458
+ state
9459
+ }] of channelPresence) {
9460
+ presences[id] = state;
9461
+ }
9462
+ }
9463
+ const ws$1 = this.clients.get(clientId);
9464
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9465
+ ws$1.send(JSON.stringify({
9466
+ type: "presence_state",
9467
+ channel,
9468
+ presences
9469
+ }));
9470
+ }
9471
+ }
9472
+ /** Broadcast presence diff (joins/leaves) to channel */
9473
+ broadcastPresenceDiff(channel, joins, leaves) {
9474
+ const members = this.channels.get(channel);
9475
+ if (!members) return;
9476
+ const message = JSON.stringify({
9477
+ type: "presence_diff",
9478
+ channel,
9479
+ joins,
9480
+ leaves
9481
+ });
9482
+ for (const memberId of members) {
9483
+ const ws$1 = this.clients.get(memberId);
9484
+ if (ws$1 && ws$1.readyState === ws.WebSocket.OPEN) {
9485
+ ws$1.send(message);
9486
+ }
9487
+ }
9488
+ }
9489
+ /** Periodic cleanup for stale presences */
9490
+ ensurePresenceCleanup() {
9491
+ if (this.presenceInterval) return;
9492
+ this.presenceInterval = setInterval(() => {
9493
+ const now = Date.now();
9494
+ for (const [channel, channelPresence] of this.presence) {
9495
+ for (const [clientId, entry] of channelPresence) {
9496
+ if (now - entry.lastSeen > RealtimeService.PRESENCE_TIMEOUT_MS) {
9497
+ this.removePresence(clientId, channel);
9498
+ }
9499
+ }
9500
+ }
9501
+ if (this.presence.size === 0 && this.presenceInterval) {
9502
+ clearInterval(this.presenceInterval);
9503
+ this.presenceInterval = void 0;
9504
+ }
9505
+ }, 1e4);
9506
+ }
9507
+ // =============================================================================
9117
9508
  // Lifecycle / Cleanup
9118
9509
  // =============================================================================
9119
9510
  /**
@@ -9134,6 +9525,12 @@ ${tableRelations.join(",\n")}
9134
9525
  }
9135
9526
  this._subscriptions.clear();
9136
9527
  this.subscriptionCallbacks.clear();
9528
+ this.channels.clear();
9529
+ this.presence.clear();
9530
+ if (this.presenceInterval) {
9531
+ clearInterval(this.presenceInterval);
9532
+ this.presenceInterval = void 0;
9533
+ }
9137
9534
  await this.stopListening();
9138
9535
  this.clients.clear();
9139
9536
  this.debugLog("🧹 [RealtimeService] destroy() complete — all resources released.");
@@ -9780,8 +10177,14 @@ ${tableRelations.join(",\n")}
9780
10177
  break;
9781
10178
  case "subscribe_collection":
9782
10179
  case "subscribe_entity":
9783
- case "unsubscribe": {
9784
- wsDebug("🔄 [WebSocket Server] Routing subscription message to RealtimeService:", type);
10180
+ case "unsubscribe":
10181
+ case "join_channel":
10182
+ case "leave_channel":
10183
+ case "broadcast":
10184
+ case "presence_track":
10185
+ case "presence_untrack":
10186
+ case "presence_state": {
10187
+ wsDebug("🔄 [WebSocket Server] Routing realtime message to RealtimeService:", type);
9785
10188
  const session = clientSessions.get(clientId);
9786
10189
  const authContext = session?.user ? {
9787
10190
  userId: session.user.userId,
@@ -9900,11 +10303,6 @@ ${tableRelations.join(",\n")}
9900
10303
  create: true,
9901
10304
  edit: true,
9902
10305
  delete: true
9903
- },
9904
- config: {
9905
- createCollections: true,
9906
- editCollections: "all",
9907
- deleteCollections: "all"
9908
10306
  }
9909
10307
  }, {
9910
10308
  id: "editor",
@@ -9915,11 +10313,6 @@ ${tableRelations.join(",\n")}
9915
10313
  create: true,
9916
10314
  edit: true,
9917
10315
  delete: true
9918
- },
9919
- config: {
9920
- createCollections: true,
9921
- editCollections: "own",
9922
- deleteCollections: "own"
9923
10316
  }
9924
10317
  }, {
9925
10318
  id: "viewer",
@@ -9930,11 +10323,10 @@ ${tableRelations.join(",\n")}
9930
10323
  create: false,
9931
10324
  edit: false,
9932
10325
  delete: false
9933
- },
9934
- config: null
10326
+ }
9935
10327
  }];
9936
10328
  async function ensureAuthTablesExist(db, registry) {
9937
- console.log("🔍 Checking auth tables...");
10329
+ serverCore.logger.info("🔍 Checking auth tables...");
9938
10330
  try {
9939
10331
  let usersTableName = '"users"';
9940
10332
  let userIdType = "TEXT";
@@ -10004,7 +10396,6 @@ ${tableRelations.join(",\n")}
10004
10396
  is_admin BOOLEAN DEFAULT FALSE,
10005
10397
  default_permissions JSONB,
10006
10398
  collection_permissions JSONB,
10007
- config JSONB,
10008
10399
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10009
10400
  )
10010
10401
  `);
@@ -10087,34 +10478,85 @@ ${tableRelations.join(",\n")}
10087
10478
  `);
10088
10479
  });
10089
10480
  await seedDefaultRoles(db, rolesTableName);
10090
- console.log("✅ Auth tables ready");
10481
+ await db.execute(drizzleOrm.sql`
10482
+ ALTER TABLE ${drizzleOrm.sql.raw(usersTableName)}
10483
+ ADD COLUMN IF NOT EXISTS is_anonymous BOOLEAN DEFAULT FALSE
10484
+ `);
10485
+ const mfaFactorsTableName = `"${rolesSchema}"."mfa_factors"`;
10486
+ const mfaChallengesTableName = `"${rolesSchema}"."mfa_challenges"`;
10487
+ const recoveryCodesTableName = `"${rolesSchema}"."recovery_codes"`;
10488
+ await db.execute(drizzleOrm.sql`
10489
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaFactorsTableName)} (
10490
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10491
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10492
+ factor_type TEXT NOT NULL DEFAULT 'totp',
10493
+ secret_encrypted TEXT NOT NULL,
10494
+ friendly_name TEXT,
10495
+ verified BOOLEAN DEFAULT FALSE,
10496
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10497
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10498
+ )
10499
+ `);
10500
+ await db.execute(drizzleOrm.sql`
10501
+ CREATE INDEX IF NOT EXISTS idx_mfa_factors_user
10502
+ ON ${drizzleOrm.sql.raw(mfaFactorsTableName)}(user_id)
10503
+ `);
10504
+ await db.execute(drizzleOrm.sql`
10505
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(mfaChallengesTableName)} (
10506
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10507
+ factor_id TEXT NOT NULL REFERENCES ${drizzleOrm.sql.raw(mfaFactorsTableName)}(id) ON DELETE CASCADE,
10508
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
10509
+ verified_at TIMESTAMP WITH TIME ZONE,
10510
+ ip_address TEXT,
10511
+ expires_at TIMESTAMP WITH TIME ZONE NOT NULL
10512
+ )
10513
+ `);
10514
+ await db.execute(drizzleOrm.sql`
10515
+ CREATE INDEX IF NOT EXISTS idx_mfa_challenges_factor
10516
+ ON ${drizzleOrm.sql.raw(mfaChallengesTableName)}(factor_id)
10517
+ `);
10518
+ await db.execute(drizzleOrm.sql`
10519
+ CREATE TABLE IF NOT EXISTS ${drizzleOrm.sql.raw(recoveryCodesTableName)} (
10520
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
10521
+ user_id ${drizzleOrm.sql.raw(userIdType)} NOT NULL REFERENCES ${drizzleOrm.sql.raw(usersTableName)}(id) ON DELETE CASCADE,
10522
+ code_hash TEXT NOT NULL,
10523
+ used_at TIMESTAMP WITH TIME ZONE,
10524
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
10525
+ )
10526
+ `);
10527
+ await db.execute(drizzleOrm.sql`
10528
+ CREATE INDEX IF NOT EXISTS idx_recovery_codes_user
10529
+ ON ${drizzleOrm.sql.raw(recoveryCodesTableName)}(user_id)
10530
+ `);
10531
+ serverCore.logger.info("✅ Auth tables ready");
10091
10532
  } catch (error) {
10092
- console.error("❌ Failed to create auth tables:", error);
10093
- console.warn("⚠️ Continuing without creating auth tables.");
10533
+ serverCore.logger.error("❌ Failed to create auth tables", {
10534
+ error
10535
+ });
10536
+ serverCore.logger.warn("⚠️ Continuing without creating auth tables.");
10094
10537
  }
10095
10538
  }
10096
10539
  async function seedDefaultRoles(db, rolesTableName) {
10097
10540
  const result = await db.execute(drizzleOrm.sql`SELECT COUNT(*) as count FROM ${drizzleOrm.sql.raw(rolesTableName)}`);
10098
10541
  const count = parseInt(result.rows[0]?.count || "0", 10);
10099
10542
  if (count > 0) {
10100
- console.log(`📋 Found ${count} existing roles`);
10543
+ serverCore.logger.info(`📋 Found ${count} existing roles`);
10101
10544
  return;
10102
10545
  }
10103
- console.log("🌱 Seeding default roles...");
10546
+ serverCore.logger.info("🌱 Seeding default roles...");
10104
10547
  for (const role of DEFAULT_ROLES) {
10105
10548
  await db.execute(drizzleOrm.sql`
10106
- INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions, config)
10549
+ INSERT INTO ${drizzleOrm.sql.raw(rolesTableName)} (id, name, is_admin, default_permissions)
10107
10550
  VALUES (
10108
10551
  ${role.id},
10109
10552
  ${role.name},
10110
10553
  ${role.is_admin},
10111
- ${JSON.stringify(role.default_permissions)}::jsonb,
10112
- ${role.config ? JSON.stringify(role.config) : null}::jsonb
10554
+ ${JSON.stringify(role.default_permissions)}::jsonb
10113
10555
  )
10114
10556
  ON CONFLICT (id) DO NOTHING
10115
10557
  `);
10116
10558
  }
10117
- console.log("✅ Default roles created: admin, editor, viewer");
10559
+ serverCore.logger.info("✅ Default roles created: admin, editor, viewer");
10118
10560
  }
10119
10561
  function getColumnKey(table, ...keys2) {
10120
10562
  if (!table) return void 0;
@@ -10168,12 +10610,13 @@ ${tableRelations.join(",\n")}
10168
10610
  const emailVerified = row.email_verified ?? row.emailVerified ?? false;
10169
10611
  const emailVerificationToken = row.email_verification_token ?? row.emailVerificationToken ?? null;
10170
10612
  const emailVerificationSentAt = row.email_verification_sent_at ?? row.emailVerificationSentAt ?? null;
10613
+ const isAnonymous = row.is_anonymous ?? row.isAnonymous ?? false;
10171
10614
  const createdAt = row.created_at ?? row.createdAt;
10172
10615
  const updatedAt = row.updated_at ?? row.updatedAt;
10173
10616
  const metadata = {
10174
10617
  ...row.metadata || {}
10175
10618
  };
10176
- 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"]);
10619
+ 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"]);
10177
10620
  for (const [key, val] of Object.entries(row)) {
10178
10621
  if (!knownKeys.has(key)) {
10179
10622
  const camelKey = camelCase(key);
@@ -10189,6 +10632,7 @@ ${tableRelations.join(",\n")}
10189
10632
  emailVerified,
10190
10633
  emailVerificationToken,
10191
10634
  emailVerificationSentAt: emailVerificationSentAt ? new Date(emailVerificationSentAt) : null,
10635
+ isAnonymous,
10192
10636
  createdAt: createdAt ? new Date(createdAt) : /* @__PURE__ */ new Date(),
10193
10637
  updatedAt: updatedAt ? new Date(updatedAt) : /* @__PURE__ */ new Date(),
10194
10638
  metadata
@@ -10205,6 +10649,7 @@ ${tableRelations.join(",\n")}
10205
10649
  const emailVerifiedKey = getColumnKey(this.usersTable, "emailVerified", "email_verified") || "emailVerified";
10206
10650
  const emailVerificationTokenKey = getColumnKey(this.usersTable, "emailVerificationToken", "email_verification_token") || "emailVerificationToken";
10207
10651
  const emailVerificationSentAtKey = getColumnKey(this.usersTable, "emailVerificationSentAt", "email_verification_sent_at") || "emailVerificationSentAt";
10652
+ const isAnonymousKey = getColumnKey(this.usersTable, "isAnonymous", "is_anonymous") || "isAnonymous";
10208
10653
  const createdAtKey = getColumnKey(this.usersTable, "createdAt", "created_at") || "createdAt";
10209
10654
  const updatedAtKey = getColumnKey(this.usersTable, "updatedAt", "updated_at") || "updatedAt";
10210
10655
  const metadataKey = getColumnKey(this.usersTable, "metadata") || "metadata";
@@ -10216,6 +10661,7 @@ ${tableRelations.join(",\n")}
10216
10661
  if ("emailVerified" in data) payload[emailVerifiedKey] = data.emailVerified;
10217
10662
  if ("emailVerificationToken" in data) payload[emailVerificationTokenKey] = data.emailVerificationToken;
10218
10663
  if ("emailVerificationSentAt" in data) payload[emailVerificationSentAtKey] = data.emailVerificationSentAt;
10664
+ if ("isAnonymous" in data) payload[isAnonymousKey] = data.isAnonymous;
10219
10665
  if ("createdAt" in data) payload[createdAtKey] = data.createdAt;
10220
10666
  if ("updatedAt" in data) payload[updatedAtKey] = data.updatedAt;
10221
10667
  const metadata = {
@@ -10224,7 +10670,7 @@ ${tableRelations.join(",\n")}
10224
10670
  const remainingMetadata = {};
10225
10671
  for (const [key, val] of Object.entries(metadata)) {
10226
10672
  const tableColKey = getColumnKey(this.usersTable, key);
10227
- if (tableColKey && tableColKey !== idKey && tableColKey !== emailKey && tableColKey !== passwordHashKey && tableColKey !== displayNameKey && tableColKey !== photoUrlKey && tableColKey !== emailVerifiedKey && tableColKey !== emailVerificationTokenKey && tableColKey !== emailVerificationSentAtKey && tableColKey !== createdAtKey && tableColKey !== updatedAtKey && tableColKey !== metadataKey) {
10673
+ 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) {
10228
10674
  payload[tableColKey] = val;
10229
10675
  } else {
10230
10676
  remainingMetadata[key] = val;
@@ -10412,7 +10858,7 @@ ${tableRelations.join(",\n")}
10412
10858
  async getUserRoles(userId) {
10413
10859
  const rolesSchema = pgCore.getTableConfig(this.rolesTable).schema || "public";
10414
10860
  const result = await this.db.execute(drizzleOrm.sql`
10415
- SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions, r.config
10861
+ SELECT r.id, r.name, r.is_admin, r.default_permissions, r.collection_permissions
10416
10862
  FROM ${drizzleOrm.sql.raw(`"${rolesSchema}"."roles"`)} r
10417
10863
  INNER JOIN ${drizzleOrm.sql.raw(`"${rolesSchema}"."user_roles"`)} ur ON r.id = ur.role_id
10418
10864
  WHERE ur.user_id = ${userId}
@@ -10422,8 +10868,7 @@ ${tableRelations.join(",\n")}
10422
10868
  name: row.name,
10423
10869
  isAdmin: row.is_admin,
10424
10870
  defaultPermissions: row.default_permissions,
10425
- collectionPermissions: row.collection_permissions,
10426
- config: row.config
10871
+ collectionPermissions: row.collection_permissions
10427
10872
  }));
10428
10873
  }
10429
10874
  /**
@@ -10489,7 +10934,7 @@ ${tableRelations.join(",\n")}
10489
10934
  async getRoleById(id) {
10490
10935
  const tableName = this.getQualifiedRolesTableName();
10491
10936
  const result = await this.db.execute(drizzleOrm.sql`
10492
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10937
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10493
10938
  FROM ${drizzleOrm.sql.raw(tableName)}
10494
10939
  WHERE id = ${id}
10495
10940
  `);
@@ -10500,14 +10945,13 @@ ${tableRelations.join(",\n")}
10500
10945
  name: row.name,
10501
10946
  isAdmin: row.is_admin,
10502
10947
  defaultPermissions: row.default_permissions,
10503
- collectionPermissions: row.collection_permissions,
10504
- config: row.config
10948
+ collectionPermissions: row.collection_permissions
10505
10949
  };
10506
10950
  }
10507
10951
  async listRoles() {
10508
10952
  const tableName = this.getQualifiedRolesTableName();
10509
10953
  const result = await this.db.execute(drizzleOrm.sql`
10510
- SELECT id, name, is_admin, default_permissions, collection_permissions, config
10954
+ SELECT id, name, is_admin, default_permissions, collection_permissions
10511
10955
  FROM ${drizzleOrm.sql.raw(tableName)}
10512
10956
  ORDER BY name
10513
10957
  `);
@@ -10516,23 +10960,21 @@ ${tableRelations.join(",\n")}
10516
10960
  name: row.name,
10517
10961
  isAdmin: row.is_admin,
10518
10962
  defaultPermissions: row.default_permissions,
10519
- collectionPermissions: row.collection_permissions,
10520
- config: row.config
10963
+ collectionPermissions: row.collection_permissions
10521
10964
  }));
10522
10965
  }
10523
10966
  async createRole(data) {
10524
10967
  const tableName = this.getQualifiedRolesTableName();
10525
10968
  const result = await this.db.execute(drizzleOrm.sql`
10526
- INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions, config)
10969
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (id, name, is_admin, default_permissions, collection_permissions)
10527
10970
  VALUES (
10528
10971
  ${data.id},
10529
10972
  ${data.name},
10530
10973
  ${data.isAdmin ?? false},
10531
10974
  ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : null}::jsonb,
10532
- ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb,
10533
- ${data.config ? JSON.stringify(data.config) : null}::jsonb
10975
+ ${data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null}::jsonb
10534
10976
  )
10535
- RETURNING id, name, is_admin, default_permissions, collection_permissions, config
10977
+ RETURNING id, name, is_admin, default_permissions, collection_permissions
10536
10978
  `);
10537
10979
  const row = result.rows[0];
10538
10980
  return {
@@ -10540,8 +10982,7 @@ ${tableRelations.join(",\n")}
10540
10982
  name: row.name,
10541
10983
  isAdmin: row.is_admin,
10542
10984
  defaultPermissions: row.default_permissions,
10543
- collectionPermissions: row.collection_permissions,
10544
- config: row.config
10985
+ collectionPermissions: row.collection_permissions
10545
10986
  };
10546
10987
  }
10547
10988
  async updateRole(id, data) {
@@ -10554,8 +10995,7 @@ ${tableRelations.join(",\n")}
10554
10995
  name = ${data.name ?? existing.name},
10555
10996
  is_admin = ${data.isAdmin ?? existing.isAdmin},
10556
10997
  default_permissions = ${data.defaultPermissions ? JSON.stringify(data.defaultPermissions) : JSON.stringify(existing.defaultPermissions)}::jsonb,
10557
- collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb,
10558
- config = ${data.config ? JSON.stringify(data.config) : existing.config ? JSON.stringify(existing.config) : null}::jsonb
10998
+ collection_permissions = ${data.collectionPermissions !== void 0 ? data.collectionPermissions ? JSON.stringify(data.collectionPermissions) : null : existing.collectionPermissions ? JSON.stringify(existing.collectionPermissions) : null}::jsonb
10559
10999
  WHERE id = ${id}
10560
11000
  `);
10561
11001
  return this.getRoleById(id);
@@ -10834,8 +11274,7 @@ ${tableRelations.join(",\n")}
10834
11274
  return this.roleService.createRole({
10835
11275
  ...data,
10836
11276
  defaultPermissions: data.defaultPermissions ?? null,
10837
- collectionPermissions: data.collectionPermissions ?? null,
10838
- config: data.config ?? null
11277
+ collectionPermissions: data.collectionPermissions ?? null
10839
11278
  });
10840
11279
  }
10841
11280
  async updateRole(id, data) {
@@ -10878,6 +11317,219 @@ ${tableRelations.join(",\n")}
10878
11317
  async deleteExpiredTokens() {
10879
11318
  await this.tokenRepository.deleteExpiredTokens();
10880
11319
  }
11320
+ // MFA operations (delegate to MfaService)
11321
+ _mfaService = null;
11322
+ getMfaService() {
11323
+ if (!this._mfaService) {
11324
+ this._mfaService = new MfaService(this.db);
11325
+ }
11326
+ return this._mfaService;
11327
+ }
11328
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11329
+ return this.getMfaService().createMfaFactor(userId, factorType, secretEncrypted, friendlyName);
11330
+ }
11331
+ async getMfaFactors(userId) {
11332
+ return this.getMfaService().getMfaFactors(userId);
11333
+ }
11334
+ async getMfaFactorById(factorId) {
11335
+ return this.getMfaService().getMfaFactorById(factorId);
11336
+ }
11337
+ async verifyMfaFactor(factorId) {
11338
+ return this.getMfaService().verifyMfaFactor(factorId);
11339
+ }
11340
+ async deleteMfaFactor(factorId, userId) {
11341
+ return this.getMfaService().deleteMfaFactor(factorId, userId);
11342
+ }
11343
+ async createMfaChallenge(factorId, ipAddress) {
11344
+ return this.getMfaService().createMfaChallenge(factorId, ipAddress);
11345
+ }
11346
+ async getMfaChallengeById(challengeId) {
11347
+ return this.getMfaService().getMfaChallengeById(challengeId);
11348
+ }
11349
+ async verifyMfaChallenge(challengeId) {
11350
+ return this.getMfaService().verifyMfaChallenge(challengeId);
11351
+ }
11352
+ async createRecoveryCodes(userId, codeHashes) {
11353
+ return this.getMfaService().createRecoveryCodes(userId, codeHashes);
11354
+ }
11355
+ async useRecoveryCode(userId, codeHash) {
11356
+ return this.getMfaService().useRecoveryCode(userId, codeHash);
11357
+ }
11358
+ async getUnusedRecoveryCodeCount(userId) {
11359
+ return this.getMfaService().getUnusedRecoveryCodeCount(userId);
11360
+ }
11361
+ async deleteAllRecoveryCodes(userId) {
11362
+ return this.getMfaService().deleteAllRecoveryCodes(userId);
11363
+ }
11364
+ async hasVerifiedMfaFactors(userId) {
11365
+ return this.getMfaService().hasVerifiedMfaFactors(userId);
11366
+ }
11367
+ }
11368
+ class MfaService {
11369
+ constructor(db, schemaName = "rebase") {
11370
+ this.db = db;
11371
+ this.schemaName = schemaName;
11372
+ }
11373
+ qualify(tableName) {
11374
+ return `"${this.schemaName}"."${tableName}"`;
11375
+ }
11376
+ async createMfaFactor(userId, factorType, secretEncrypted, friendlyName) {
11377
+ const tableName = this.qualify("mfa_factors");
11378
+ const result = await this.db.execute(drizzleOrm.sql`
11379
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, factor_type, secret_encrypted, friendly_name)
11380
+ VALUES (${userId}, ${factorType}, ${secretEncrypted}, ${friendlyName ?? null})
11381
+ RETURNING id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11382
+ `);
11383
+ const row = result.rows[0];
11384
+ return {
11385
+ id: row.id,
11386
+ userId: row.user_id,
11387
+ factorType: row.factor_type,
11388
+ friendlyName: row.friendly_name ?? void 0,
11389
+ verified: row.verified,
11390
+ createdAt: new Date(row.created_at),
11391
+ updatedAt: new Date(row.updated_at)
11392
+ };
11393
+ }
11394
+ async getMfaFactors(userId) {
11395
+ const tableName = this.qualify("mfa_factors");
11396
+ const result = await this.db.execute(drizzleOrm.sql`
11397
+ SELECT id, user_id, factor_type, friendly_name, verified, created_at, updated_at
11398
+ FROM ${drizzleOrm.sql.raw(tableName)}
11399
+ WHERE user_id = ${userId}
11400
+ ORDER BY created_at
11401
+ `);
11402
+ return result.rows.map((row) => ({
11403
+ id: row.id,
11404
+ userId: row.user_id,
11405
+ factorType: row.factor_type,
11406
+ friendlyName: row.friendly_name ?? void 0,
11407
+ verified: row.verified,
11408
+ createdAt: new Date(row.created_at),
11409
+ updatedAt: new Date(row.updated_at)
11410
+ }));
11411
+ }
11412
+ async getMfaFactorById(factorId) {
11413
+ const tableName = this.qualify("mfa_factors");
11414
+ const result = await this.db.execute(drizzleOrm.sql`
11415
+ SELECT id, user_id, factor_type, secret_encrypted, friendly_name, verified, created_at, updated_at
11416
+ FROM ${drizzleOrm.sql.raw(tableName)}
11417
+ WHERE id = ${factorId}
11418
+ `);
11419
+ if (result.rows.length === 0) return null;
11420
+ const row = result.rows[0];
11421
+ return {
11422
+ id: row.id,
11423
+ userId: row.user_id,
11424
+ factorType: row.factor_type,
11425
+ secretEncrypted: row.secret_encrypted,
11426
+ friendlyName: row.friendly_name ?? void 0,
11427
+ verified: row.verified,
11428
+ createdAt: new Date(row.created_at),
11429
+ updatedAt: new Date(row.updated_at)
11430
+ };
11431
+ }
11432
+ async verifyMfaFactor(factorId) {
11433
+ const tableName = this.qualify("mfa_factors");
11434
+ await this.db.execute(drizzleOrm.sql`
11435
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11436
+ SET verified = TRUE, updated_at = NOW()
11437
+ WHERE id = ${factorId}
11438
+ `);
11439
+ }
11440
+ async deleteMfaFactor(factorId, userId) {
11441
+ const tableName = this.qualify("mfa_factors");
11442
+ await this.db.execute(drizzleOrm.sql`
11443
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)}
11444
+ WHERE id = ${factorId} AND user_id = ${userId}
11445
+ `);
11446
+ }
11447
+ async createMfaChallenge(factorId, ipAddress) {
11448
+ const tableName = this.qualify("mfa_challenges");
11449
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1e3);
11450
+ const result = await this.db.execute(drizzleOrm.sql`
11451
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (factor_id, ip_address, expires_at)
11452
+ VALUES (${factorId}, ${ipAddress ?? null}, ${expiresAt})
11453
+ RETURNING id, factor_id, created_at, verified_at, ip_address
11454
+ `);
11455
+ const row = result.rows[0];
11456
+ return {
11457
+ id: row.id,
11458
+ factorId: row.factor_id,
11459
+ createdAt: new Date(row.created_at),
11460
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11461
+ ipAddress: row.ip_address ?? void 0
11462
+ };
11463
+ }
11464
+ async getMfaChallengeById(challengeId) {
11465
+ const tableName = this.qualify("mfa_challenges");
11466
+ const result = await this.db.execute(drizzleOrm.sql`
11467
+ SELECT id, factor_id, created_at, verified_at, ip_address, expires_at
11468
+ FROM ${drizzleOrm.sql.raw(tableName)}
11469
+ WHERE id = ${challengeId} AND expires_at > NOW() AND verified_at IS NULL
11470
+ `);
11471
+ if (result.rows.length === 0) return null;
11472
+ const row = result.rows[0];
11473
+ return {
11474
+ id: row.id,
11475
+ factorId: row.factor_id,
11476
+ createdAt: new Date(row.created_at),
11477
+ verifiedAt: row.verified_at ? new Date(row.verified_at) : void 0,
11478
+ ipAddress: row.ip_address ?? void 0
11479
+ };
11480
+ }
11481
+ async verifyMfaChallenge(challengeId) {
11482
+ const tableName = this.qualify("mfa_challenges");
11483
+ await this.db.execute(drizzleOrm.sql`
11484
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11485
+ SET verified_at = NOW()
11486
+ WHERE id = ${challengeId}
11487
+ `);
11488
+ }
11489
+ async createRecoveryCodes(userId, codeHashes) {
11490
+ const tableName = this.qualify("recovery_codes");
11491
+ await this.db.execute(drizzleOrm.sql`
11492
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11493
+ `);
11494
+ for (const hash of codeHashes) {
11495
+ await this.db.execute(drizzleOrm.sql`
11496
+ INSERT INTO ${drizzleOrm.sql.raw(tableName)} (user_id, code_hash)
11497
+ VALUES (${userId}, ${hash})
11498
+ `);
11499
+ }
11500
+ }
11501
+ async useRecoveryCode(userId, codeHash) {
11502
+ const tableName = this.qualify("recovery_codes");
11503
+ const result = await this.db.execute(drizzleOrm.sql`
11504
+ UPDATE ${drizzleOrm.sql.raw(tableName)}
11505
+ SET used_at = NOW()
11506
+ WHERE user_id = ${userId} AND code_hash = ${codeHash} AND used_at IS NULL
11507
+ RETURNING id
11508
+ `);
11509
+ return result.rows.length > 0;
11510
+ }
11511
+ async getUnusedRecoveryCodeCount(userId) {
11512
+ const tableName = this.qualify("recovery_codes");
11513
+ const result = await this.db.execute(drizzleOrm.sql`
11514
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11515
+ WHERE user_id = ${userId} AND used_at IS NULL
11516
+ `);
11517
+ return result.rows[0].count;
11518
+ }
11519
+ async deleteAllRecoveryCodes(userId) {
11520
+ const tableName = this.qualify("recovery_codes");
11521
+ await this.db.execute(drizzleOrm.sql`
11522
+ DELETE FROM ${drizzleOrm.sql.raw(tableName)} WHERE user_id = ${userId}
11523
+ `);
11524
+ }
11525
+ async hasVerifiedMfaFactors(userId) {
11526
+ const tableName = this.qualify("mfa_factors");
11527
+ const result = await this.db.execute(drizzleOrm.sql`
11528
+ SELECT COUNT(*)::int as count FROM ${drizzleOrm.sql.raw(tableName)}
11529
+ WHERE user_id = ${userId} AND verified = TRUE
11530
+ `);
11531
+ return result.rows[0].count > 0;
11532
+ }
10881
11533
  }
10882
11534
  const DEFAULT_RETENTION = {
10883
11535
  maxEntries: 200,
@@ -11088,7 +11740,7 @@ ${tableRelations.join(",\n")}
11088
11740
  const registry = new PostgresCollectionRegistry();
11089
11741
  if (collections) {
11090
11742
  registry.registerMultiple(collections);
11091
- console.log(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11743
+ serverCore.logger.info(`📋 [PostgresRegistry] Registered ${registry.getCollections().length} collections: [${registry.getCollections().map((c) => c.slug).join(", ")}]`);
11092
11744
  }
11093
11745
  if (pgConfig.schema?.tables) {
11094
11746
  Object.values(pgConfig.schema.tables).forEach((table) => {
@@ -11114,10 +11766,28 @@ ${tableRelations.join(",\n")}
11114
11766
  try {
11115
11767
  await schemaAwareDb.execute(drizzleOrm.sql`SELECT 1`);
11116
11768
  } catch (err) {
11117
- console.error("❌ Failed to connect to PostgreSQL:", err);
11118
- console.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11769
+ serverCore.logger.error("❌ Failed to connect to PostgreSQL", {
11770
+ error: err
11771
+ });
11772
+ serverCore.logger.warn("⚠️ Continuing without initial database verification. Drizzle/PG will attempt to connect on subsequent queries.");
11119
11773
  }
11120
11774
  const realtimeService = new RealtimeService(schemaAwareDb, registry);
11775
+ let readDb;
11776
+ const readUrl = process.env.DATABASE_READ_URL;
11777
+ if (readUrl && readUrl !== pgConfig.connectionString) {
11778
+ try {
11779
+ const {
11780
+ createReadReplicaConnection: createReadReplicaConnection2
11781
+ } = await Promise.resolve().then(() => connection);
11782
+ const readResources = createReadReplicaConnection2(readUrl, mergedSchema);
11783
+ readDb = readResources.db;
11784
+ serverCore.logger.info("📖 [PostgresBootstrapper] Read replica connection established");
11785
+ } catch (err) {
11786
+ serverCore.logger.warn("⚠️ Could not connect to read replica, falling back to primary for all queries", {
11787
+ error: err
11788
+ });
11789
+ }
11790
+ }
11121
11791
  const poolManager = pgConfig.adminConnectionString ? new DatabasePoolManager(pgConfig.adminConnectionString) : void 0;
11122
11792
  const driver = new PostgresBackendDriver(schemaAwareDb, realtimeService, registry, void 0, poolManager);
11123
11793
  realtimeService.setDataDriver(driver);
@@ -11125,18 +11795,24 @@ ${tableRelations.join(",\n")}
11125
11795
  try {
11126
11796
  await driver.branchService.ensureBranchMetadataTable();
11127
11797
  } catch (err) {
11128
- console.warn("⚠️ Could not initialize branch metadata table:", err);
11798
+ serverCore.logger.warn("⚠️ Could not initialize branch metadata table", {
11799
+ error: err
11800
+ });
11129
11801
  }
11130
11802
  }
11131
- if (pgConfig.connectionString) {
11803
+ const directUrl = process.env.DATABASE_DIRECT_URL || pgConfig.connectionString;
11804
+ if (directUrl) {
11132
11805
  try {
11133
- await realtimeService.startListening(pgConfig.connectionString);
11806
+ await realtimeService.startListening(directUrl);
11134
11807
  } catch (err) {
11135
- console.warn("⚠️ Cross-instance realtime could not be started:", err);
11808
+ serverCore.logger.warn("⚠️ Cross-instance realtime could not be started", {
11809
+ error: err
11810
+ });
11136
11811
  }
11137
11812
  }
11138
11813
  const internals = {
11139
11814
  db: schemaAwareDb,
11815
+ readDb,
11140
11816
  registry,
11141
11817
  realtimeService,
11142
11818
  driver,
@@ -11273,14 +11949,22 @@ ${tableRelations.join(",\n")}
11273
11949
  exports2.RealtimeService = RealtimeService;
11274
11950
  exports2.appConfig = appConfig;
11275
11951
  exports2.createAuthSchema = createAuthSchema;
11952
+ exports2.createDirectDatabaseConnection = createDirectDatabaseConnection;
11276
11953
  exports2.createPostgresAdapter = createPostgresAdapter;
11277
11954
  exports2.createPostgresBootstrapper = createPostgresBootstrapper;
11278
11955
  exports2.createPostgresDatabaseConnection = createPostgresDatabaseConnection;
11279
11956
  exports2.createPostgresWebSocket = createPostgresWebSocket;
11957
+ exports2.createReadReplicaConnection = createReadReplicaConnection;
11280
11958
  exports2.generateSchema = generateSchema;
11959
+ exports2.mfaChallenges = mfaChallenges;
11960
+ exports2.mfaChallengesRelations = mfaChallengesRelations;
11961
+ exports2.mfaFactors = mfaFactors;
11962
+ exports2.mfaFactorsRelations = mfaFactorsRelations;
11281
11963
  exports2.passwordResetTokens = passwordResetTokens;
11282
11964
  exports2.passwordResetTokensRelations = passwordResetTokensRelations;
11283
11965
  exports2.rebaseSchema = rebaseSchema;
11966
+ exports2.recoveryCodes = recoveryCodes;
11967
+ exports2.recoveryCodesRelations = recoveryCodesRelations;
11284
11968
  exports2.refreshTokens = refreshTokens;
11285
11969
  exports2.refreshTokensRelations = refreshTokensRelations;
11286
11970
  exports2.roles = roles;